Reading png rgb channels, Julia vs Python

So here is a very simple 8x8 png image:
img_5

Opening it in GIMP:

So this pixel has RGBA(233, 173, 4, 244) components.

Using Python:

import imageio
img = imageio.imread('img.png')
r = img[:, :, 0]
g = img[:, :, 1]
b = img[:, :, 2]
a = img[:, :, 3]

print((r[2, 2], g[2, 2], b[2, 2], a[2, 2]))

gives (233, 173, 4, 244), so far so good.

Now using Julia:

using ColorTypes
using ImageMagick
using PNGFiles

for mod ∈ (ImageMagick, PNGFiles)
  img = open("img.png") do io
    kw = mod == PNGFiles ? (;expand_paletted=true) : ()
    mod.load(io; kw...)
  end

  int255(c) = round.(Int, 255c)
  r, g, b, a = int255(red.(img)), int255(green.(img)), int255(blue.(img)), int255(alpha.(img))

  println((r[3, 3], g[3, 3], b[3, 3], a[3, 3]))
end

gives

(245, 215, 34, 244)
(245, 214, 39, 244)

Only the alpha channel is identical. G and B channels have different values using PNGFiles or ImageMagick. And the RGB values are far from what is displayed in GIMP or by using Python.

What am I missing here ?

is this a linear vs srgb issue?

It looks like Python is returning raw srgb values from the file but Julia is converting them to linear values.
You want linear values if you’re doing any math on them. For instance, averaging or adding two values together in srgb space will return a wrong result.

1 Like

that said, the conversion does lose considerable precision unless Julia is using more than 8 bits.

1 Like

Wait, isn’t it the other way around? (Disclaimer: I am very much not an expert)

From the excellent Color Spaces – Bartosz Ciechanowski, we see that
(intensity value) = (((encoded value) + 0.055)/1.055)2.4

So if the encoded value is 245/255 then the actual intensity is indeed about 232/255. So seems like Julia’s color and image packages are giving the encoded values, and Python’s is giving the actual linear intensity?

(I think one way to double check this: do using Colors; Gray(.5) in an editor that displays color output, and you’ll see what looks to the human eye like mid-gray—meaning the argument to Gray must be the encoded value, not linear value, because the gray that humans see as halfway between dark and light actually has ~18% intensity.)

2 Likes

Thanks for pointing towards srgb vs linear scale:

Using

  # ...
  linear(srgb) = begin
    # en.wikipedia.org/wiki/SRGB#Transformation
    lin = similar(srgb)
    for I in eachindex(srgb)
      lin[I] = if (v = srgb[I])  > .04045
        ((v + .055) / 1.055)^2.4
      else
        v / 12.92
      end
    end
    lin
  end
  lin255(x) = round.(Int, 255linear(x))
  raw255(x) = round.(Int, 255x)
  r, g, b, a = lin255(red.(img)), lin255(green.(img)), lin255(blue.(img)), raw255(alpha.(img))
  # ...

now gives the same R, G, B, A matrices as Python when using ImageMagick but different results for PNGFiles (rounding issues ?) so I’m more confident trusting the data output from ImageMagick, since it matches the implement of imagio (underlying pillow library).

Hi, I’m one of the authors of PNGFiles.jl. Could you try importing the image using gamma=1.0 and share the image in question?

The image in small but in the original post (right click, save as) or here is the direct url: https://global.discourse-cdn.com/julialang/original/3X/5/c/5c5cdaa477dcae516c7183c3e710ef65a0bb6ab0.png.

With gamma=nothing: (233, 171, 5, 244)
With gamma=1. or gamma=.45455: (208, 107, 0, 244)

Furthermore:

$ convert img.png txt:- | grep '2,2:'
2,2: (59881,44461,1028)  #E9AD04  rgb(233,173,4)
$ julia -E '44461 / 65535 * 255'
173.0

@drvi, I have assembled a simple example at https://github.com/t-bltg/PngPixel.jl.

Using the libpng_api from PNGFiles, I can get the correct RGB values.

However I still don’t understand why using PNGFiles.load(...) gives the wrong values.

@t-bltg Thanks for investigating! Can you try the following branch of PNGFiles?

] add PNGFiles#paletted_images_fix

julia> using PNGFiles

julia> map(x->(Int(x.r.i), Int(x.g.i), Int(x.b.i), Int(x.alpha.i)), PNGFiles.load("test.png"))[3,3]
(233, 173, 4, 244)

Odd, I get different results:

(@v1.7) pkg> add PNGFiles#paletted_images_fix
    Updating git-repo `https://github.com/JuliaIO/PNGFiles.jl.git`
    Updating registry at `~/.julia/registries/General.toml`
   Resolving package versions...
    Updating `~/.julia/environments/v1.7/Project.toml`
  [f57f5aa1] ~ PNGFiles v0.3.12 ⇒ v0.3.12 `https://github.com/JuliaIO/PNGFiles.jl.git#paletted_images_fix`
    Updating `~/.julia/environments/v1.7/Manifest.toml`
  [f57f5aa1] ~ PNGFiles v0.3.12 ⇒ v0.3.12 `https://github.com/JuliaIO/PNGFiles.jl.git#paletted_images_fix`
Precompiling project...
  1 dependency successfully precompiled in 4 seconds (44 already precompiled)

julia> using PNGFiles

julia> map(x->(Int(x.r.i), Int(x.g.i), Int(x.b.i), Int(x.alpha.i)), PNGFiles.load("img.png"))[3,3]
(245, 214, 39, 244)

Hmm, would you mind opening an issue on the github repo? You can just link to this thread and preferably include the outputs of the following:

Just to be sure we’re talking about the same exact picture:

julia> (x->(Int(x.r.i), Int(x.g.i), Int(x.b.i), Int(x.alpha.i)))(PNGFiles.load(Downloads.download("https://global.discourse-cdn.com/julialang/original/3X/5/c/5c5cdaa477dcae516c7183c3e710ef65a0bb6ab0.png"))[3,3])
# expected: (233, 173, 4, 244)

and package versions

] st

and system info

julia> versioninfo()

Thanks!

Opened at https://github.com/JuliaIO/PNGFiles.jl/issues/48.

1 Like

Doh I always forget which is approximately ^1/2.2 and which is approximately ^2.2.

Which is dumb. All I have to remember is that SRGB values are higher than linear at the low end and vice versa at the high end.

1 Like