Arithmetic on colors

Hello there,
I want to find the total Sum of Squared Errors of a set of colors from their mean. Using Colors.
The colors and the mean look something like this, where m = mean(cols).

cols[1:10] = RGB{FixedPointNumbers.Normed{UInt8,8}}[RGB{N0f8}(0.086,0.051,0.024), RGB{N0f8}(0.094,0.059,0.031), RGB{N0f8}(0.106,0.071,0.051), RGB{N0f8}(0.141,0.106,0.086), RGB{N0f8}(0.102,0.067,0.047), RGB{N0f8}(0.141,0.106,0.086), RGB{N0f8}(0.176,0.149,0.125), RGB{N0f8}(0.078,0.051,0.027), RGB{N0f8}(0.114,0.086,0.063), RGB{N0f8}(0.149,0.122,0.098)]
m = RGB{Float64}(0.5797770398481967,0.5710550284629976,0.5525899746995568)

But

sse = sum(abs2, cols .- m)

throws error
`
ERROR: LoadError: MethodError: no method matching depwarn(::String, ::Symbol; force=false)

`

I am wondering how to do it? I will be finding SSE of sections of image over and over again. What is the best way to do it? Convert the original image to Floats? I will also be using hex(m) to save the mean color to an svg image.

Edit MWE:

cols = rand(RGB{Float64}, 4, 4)
m = mean(cols)
cols .- m              # Also a Matrix of RGB{Float64} with some negative values!!!
sum(abs2, cols .- m)  # Errors out

Thanks.

The error has nothing to do with colors. It’s just that you cannot take abs2 of an array, it only works on scalars:

julia> abs2([-2, 0, 1])
ERROR: MethodError: no method matching abs2(::Vector{Int64})

In order to work on arrays, you must broadcast or map the function over the array (or loop):

julia> abs2.([-2, 0, 1])
3-element Vector{Int64}:
 4
 0
 1

julia> map(abs2, [-2, 0, 1])
3-element Vector{Int64}:
 4
 0
 1

Sorry, just realized my mistake and updated the post as you were typing yours.

Yeah, I saw that. Just poor timing. But maybe you can make the example self contained? Make a code snippet with using Colors and then generate a random color vector. It’s so much easer if I can just copy-paste your example directly into my REPL. It’s called ‘making a minimum working example’, and is extremely useful.

Did not feel like this deserved an MWE. Just that RGB{Float64} does not have abs2 defined! That’s it. If I do

abs3(a) = a.r*a.r+a.g*a.g+a.b*a.b
sum(abs3, cols)

works. I feel I should not have to do this.

I don’t think abs2 makes sense for colors. What is the absolute value of a color? Are there negative colors?

There should almost always be an MWE. Especially this time, it would have simplified the interaction considerably.

Sorry again. Back to the original use case.
It definitely makes sense to calculate the SSE of a bunch of colors from their mean.

While I cannot answer this question off the top of my head, in the sense of explaining why abs2 doesn’t work, I perhaps could if I looked at the data. But without an MWE, I’m not getting anywhere.

If I do

julia> using Colors

julia> cols = RGB{FixedPointNumbers.Normed{UInt8,8}}[RGB{N0f8}(0.086,0.051,0.024), RGB{N0f8}(0.094,0.059,0.031), RGB{N0f8}(0.106,0.071,0.051), RGB{N0f8}(0.141,0.106,0.086), RGB{N0f8}(0.102,0.067,0.047), RGB{N0f8}(0.141,0.106,0.086), RGB{N0f8}(0.176,0.149,0.125), RGB{N0f8}(0.078,0.051,0.027), RGB{N0f8}(0.114,0.086,0.063), RGB{N0f8}(0.149,0.122,0.098)]
ERROR: UndefVarError: FixedPointNumbers not defined

I just get an error.

There really needs to be an MWE here.

So, I installed Images.jl, and took a look. Now I can show you why abs2 doesn’t make sense:

julia> using Images

julia> cols = [RGB{N0f8}(0.086,0.051,0.024), RGB{N0f8}(0.094,0.059,0.031)]
2-element Array{RGB{N0f8},1} with eltype RGB{N0f8}:
 RGB{N0f8}(0.086,0.051,0.024)
 RGB{N0f8}(0.094,0.059,0.031)

julia> x = cols[1] - cols[2]
RGB{N0f8}(0.996,0.996,0.996)

julia> y = cols[2] - cols[1]
RGB{N0f8}(0.008,0.008,0.008)

julia> x.r^2 + x.g^2 + x.b^2
0.969N0f8

julia> y.r^2 + y.g^2 + y.b^2
0.0N0f8

As you can see, there are no negative colors, the values just wrap around. As a consequence abs2(cols[1] - cols[2]) is different from abs2(cols[2] - cols[1]) (if we defined abs2 as the sum of the squares of the rgb channels). That’s not what you want.

If you want a distance measure, it needs to be defined differently, because subtraction is not behaving as you expect on colors.

As you can see, there are a few things going on here. This is what MWEs are for.

1 Like

This is provided by ColorVectorSpace.jl:

julia> sse = sum(abs2, cols .- m)
6.888337554125739

Better yet, use the norm function (which is the same thing divided by 3 for RGB colors, because it is normalized to be the same for grayscale and color images):

julia> norm(cols .- m)^2 * 3
6.888337554125741
2 Likes

But I get:

julia> ColorVectorSpace.norm(cols[2] .- cols[1])^2 * 3
0.0

julia> ColorVectorSpace.norm(cols[1] .- cols[2])^2 * 3
2.976470717669955

Is this the right behaviour for color distance?

1 Like

Looks like this is underflow due to the fixed-point precision:

julia> cols[2] - cols[1]
RGB{N0f8}(0.008,0.008,0.008)

julia> norm(cols[2] - cols[1])
0.0

julia> norm(RGB{Float64}(cols[2] - cols[1]))
0.00784313725490196

In particular, the problem stems from the underflow:

julia> N0f8(0.008)^2
0.0N0f8

Arguably, this should be changed — since the output of norm is a Float64, this computation should be performed by promoting operands to the output precision. Or at the very least some scaling should be done to prevent spurious overflow/underflow (similar to what norm and hypot do for floating-point vectors).

(Update: Filed issue ColorVectorSpace.jl#183.)

The concept of arithmetic on colors is a bit fuzzy to me. It makes sense to add (mix) colors. So I guess subtraction also might work. But what should happen when you subtract a strong color from a weak color? Should it wrap around, or saturate, or return negative values?

A distance function seems like it should do something like subtracting the minimum from the maximum instead of taking the absolute or square of the difference.

I guess that in the physical world, if we have no red and try to subtract red, we should always end up with no red. However, using the ColorVectorSpace, we get a “negative” red:

RGB{Float64}(0,1,1) - RGB{Float64}(1,1,1) == RGB{Float64}(-1.0,0.0,0.0)

Such colors live only in the “mathematical world” and are invalid for display purposes? They would need to be saturated/confined to 0-1 limits, in order to be plotted.

Arthmetic on colors, especially the use-case given here, is very fundamental to image and video storage. All your jpgs and pngs do it thousands or millions of times. They replace a blob with a single color and see how well it fits (at some level). Wavelets, compressed sensing they all do this. Not every mathematical operation needs to map to the real world. Case in point: “Imaginary” numbers.
I feel this is a bug in Colours.jl and need to be thought through.

That doesn’t mean that one shouldn’t strive to make the operations internally consistent.

So what’s the right behavior of RGB{N0f8} under subtraction, then? They are based on UInt8 which are unsigned, and therefore incapable of holding negative values. Which makes sense for storing color values, but makes subtraction (and many other types of arithmetic) problematic.

That was exactly my point.

I now see you made an MWE using RGB{Float64}. That’s probably the right approach. Doing what you want in unsigned arithmetic is probably needlessly complicated.

1 Like