RGB multiplication and implications for var (#119, #125)
Currently multiplication of two RGBs is undefined, but sometimes this causes problems (#119, "tinting" as in https://discourse.julialang.org/t/loaderror-dimensionmismatch/35507?u=tim.holy). In #119, we hit on the possibility of defining ⋅ (\cdot) as the "dot product" (with channels viewed as a vector), * operating channelwise, and ⊗ (\otimes) some day being the outer product (though we currently lack a type to express this result). I'll modify this proposal below, but for now let's run with it and explore the implications. An important route lies through abs2...
Currently, abs2(c) returns a scalar, which in this proposed framework would be equivalent to defining abs2(c) = c⋅c. This is similar to how abs2 behaves with respect to the real and imaginary components of a complex number. This results in the following:
julia> using ColorVectorSpace, Statistics
julia> r = rand(RGB{Float32}, 2)
2-element Array{RGB{Float32},1} with eltype RGB{Float32}:
RGB{Float32}(0.6334499f0,0.34752238f0,0.17738128f0)
RGB{Float32}(0.35009634f0,0.38727677f0,0.6068548f0)
julia> var(r)
0.13315858f0
However, if you load Images it changes the behavior of var to be channelwise, because Images specializes var for arrays of Colorants ☹️ . Rather than overloading var for certain element types, we could let * be channelwise multiplication and define abs2(c) = c*c, and it should even make it robust with respect to the dims keyword argument (#125, CC @wizofe).
However, since this package is called ColorVectorSpace it's also worth noting this comparison:
julia> using Statistics, StaticArrays
julia> A = rand(3, 5)
3×5 Array{Float64,2}:
0.901707 0.763397 0.0841844 0.164515 0.542334
0.247929 0.690001 0.492793 0.275766 0.175159
0.0860073 0.120248 0.0454239 0.403493 0.529168
julia> var(A; dims=2)
3×1 Array{Float64,2}:
0.129401636194079
0.04475575432189567
0.04655327916103
julia> a = reinterpret(SVector{3,Float64}, A)
1×5 reinterpret(SArray{Tuple{3},Float64,1,3}, ::Array{Float64,2}):
[0.901707, 0.247929, 0.0860073] … [0.542334, 0.175159, 0.529168]
julia> var(a; dims=2)
ERROR: MethodError: no method matching abs2(::SArray{Tuple{3},Float64,1,3})
Closest candidates are:
abs2(::Missing) at missing.jl:100
abs2(::Bool) at bool.jl:84
abs2(::Real) at number.jl:157
...
Stacktrace:
[1] (::Statistics.var"#12#13")(::SArray{Tuple{3},Float64,1,3}) at /home/tim/src/julia-1/usr/share/julia/stdlib/v1.3/Statistics/src/Statistics.jl:300
...
so this is an issue that doesn't have an agreed-upon resolution elsewhere in the ecosystem. We might actually want to be able to pass an operator, and the thinking above inspires me to propose var(⋅, a), var(*, a), and var(⊗, a).
However, this conflicts with the very clear decision that u*v for two vectors u and v is deliberately undefined, and specifically does not mean elementwise multiplication (that's what broadcasting is for). Unfortunately, it's not possible to pass broadcasted-* (.*) as an argument to a function. Consequently we might consider choosing an operator to be a synonym for elementwise multiplication, and leaving * undefined for RGB. Options include ∗ (\ast), • (\bullet), and ⊙ (\odot). I worry that \ast looks too similar to * and would be confused for it; if we go this way I think my preference is for \odot but curious to see what others think.
By this token abs2 should be undefined for colors, var(a) should throw an error for color arrays, but var(⊙, a) should do what users want.
Because of the analogy with arrays-of-vectors I'll open a similar issue in Statistics.jl and see what folks outside of the JuliaImages world think. (Update: https://github.com/JuliaLang/Statistics.jl/issues/29)
Interaction between color math and conversions
Another factor that would argue in favor of changing the behavior of abs2 is the following set of logical-sounding conclusions:
- if
x is a real number, then Gray(x) is basically the same thing (other than being a display hint)
RGB(Gray(x)) should return RGB(x, x, x), so by the same token RGB(x, x, x) is essentially equivalent to x
Our current scheme has a big gotcha:
julia> x = 0.5
0.5
julia> abs2(x)
0.25
julia> abs2(RGB(x, x, x))
0.75
so we're in a situation where a and b can be equivalent but abs2(a) is very different from abs2(b). If we changed abs2 to behave channelwise, we'd restore consistency. An error would also fix it; we don't object to -1 == -1+0im but having sqrt return an error for just one of them.
Result types from arithmetic (#38)
I originally designed the rules to be inspired to unitful arithmetic. However, most others who have commented seem to expect that colors will be "poisoning" in much the same way that NaN is poisoning in arithmetic. (#38 (comment) as well as the OP in #38).
Given that we support Gray(0.1)^2 and there is no type encoding Gray^2, I now think the poisoning metaphor is overall the better choice.
I suspect there is one interesting exception to the poisoning rule, however, which comes from linear algebra:
julia> v = SVector((0.2, 0.1, 0.8))
3-element SArray{Tuple{3},Float64,1,3} with indices SOneTo(3):
0.2
0.1
0.8
julia> u = SVector((0.1, 0.2, 0.8))
3-element SArray{Tuple{3},Float64,1,3} with indices SOneTo(3):
0.1
0.2
0.8
julia> u \ v
0.9855072463768115
Note the returned value is a scalar, not an SVector{3}. (The value minimizes norm(α*u - v) for real-valued α.) Should we define \ for Gray and RGB? / cannot be defined for RGB (EDIT: in the denominator), but it can for Gray; should Gray(0.2) / Gray(0.1) likewise be "colorless"? That is a little bit more like the unitful design. Is that a good thing, or would that be annoying?
RGB multiplication and implications for
var(#119, #125)Currently multiplication of two RGBs is undefined, but sometimes this causes problems (#119, "tinting" as in https://discourse.julialang.org/t/loaderror-dimensionmismatch/35507?u=tim.holy). In #119, we hit on the possibility of defining
⋅(\cdot) as the "dot product" (with channels viewed as a vector),*operating channelwise, and⊗(\otimes) some day being the outer product (though we currently lack a type to express this result). I'll modify this proposal below, but for now let's run with it and explore the implications. An important route lies throughabs2...Currently,
abs2(c)returns a scalar, which in this proposed framework would be equivalent to definingabs2(c) = c⋅c. This is similar to howabs2behaves with respect to the real and imaginary components of a complex number. This results in the following:However, if you load Images it changes the behavior of☹️ . Rather than overloading
varto be channelwise, because Images specializesvarfor arrays of Colorantsvarfor certain element types, we could let*be channelwise multiplication and defineabs2(c) = c*c, and it should even make it robust with respect to thedimskeyword argument (#125, CC @wizofe).However, since this package is called ColorVectorSpace it's also worth noting this comparison:
so this is an issue that doesn't have an agreed-upon resolution elsewhere in the ecosystem. We might actually want to be able to pass an operator, and the thinking above inspires me to propose
var(⋅, a),var(*, a), andvar(⊗, a).However, this conflicts with the very clear decision that
u*vfor two vectorsuandvis deliberately undefined, and specifically does not mean elementwise multiplication (that's what broadcasting is for). Unfortunately, it's not possible to pass broadcasted-*(.*) as an argument to a function. Consequently we might consider choosing an operator to be a synonym for elementwise multiplication, and leaving*undefined for RGB. Options include∗(\ast),•(\bullet), and⊙(\odot). I worry that\astlooks too similar to*and would be confused for it; if we go this way I think my preference is for\odotbut curious to see what others think.By this token
abs2should be undefined for colors,var(a)should throw an error for color arrays, butvar(⊙, a)should do what users want.Because of the analogy with arrays-of-vectors I'll open a similar issue in Statistics.jl and see what folks outside of the JuliaImages world think. (Update: https://github.com/JuliaLang/Statistics.jl/issues/29)
Interaction between color math and conversions
Another factor that would argue in favor of changing the behavior of
abs2is the following set of logical-sounding conclusions:xis a real number, thenGray(x)is basically the same thing (other than being a display hint)RGB(Gray(x))should returnRGB(x, x, x), so by the same tokenRGB(x, x, x)is essentially equivalent toxOur current scheme has a big gotcha:
so we're in a situation where
aandbcan be equivalent butabs2(a)is very different fromabs2(b). If we changedabs2to behave channelwise, we'd restore consistency. An error would also fix it; we don't object to-1 == -1+0imbut havingsqrtreturn an error for just one of them.Result types from arithmetic (#38)
I originally designed the rules to be inspired to unitful arithmetic. However, most others who have commented seem to expect that colors will be "poisoning" in much the same way that
NaNis poisoning in arithmetic. (#38 (comment) as well as the OP in #38).Given that we support
Gray(0.1)^2and there is no type encodingGray^2, I now think the poisoning metaphor is overall the better choice.I suspect there is one interesting exception to the poisoning rule, however, which comes from linear algebra:
Note the returned value is a scalar, not an
SVector{3}. (The value minimizesnorm(α*u - v)for real-valuedα.) Should we define\forGrayandRGB?/cannot be defined forRGB(EDIT: in the denominator), but it can forGray; shouldGray(0.2) / Gray(0.1)likewise be "colorless"? That is a little bit more like the unitful design. Is that a good thing, or would that be annoying?