Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistency in handling hues #379

Closed
kimikage opened this issue Dec 15, 2019 · 22 comments · Fixed by #407
Closed

Inconsistency in handling hues #379

kimikage opened this issue Dec 15, 2019 · 22 comments · Fixed by #407

Comments

@kimikage
Copy link
Collaborator

Unnormalized hues are somewhat useful in the interpolation. In other words, the interpolation across 0° is not possible with only the normalized hues.

However, the manners of input range checking in conversions vary (cf. #378 ), and some conversions cannot handle unnormalized hues correctly.

using Colors

range(HSV(0,1,1), HSV(360,1,1),length=120)
range(HSV(0,1,1), HSV(-360,1,1),length=120)
range(HSV(0,1,1), HSV(720,1,1),length=120)
range(HSL(0,1,0.5), HSL(720,1,0.5),length=120)
range(HSI(0,1,1/3), HSI(720,1,1/3),length=120)
range(LCHab(70,70,0), LCHab(70,70,720),length=120)
range(LCHuv(70,70,0), LCHuv(70,70,720),length=120)

hue

I think that the strictness of input range checking should be more consistent. Apart from that, it might be a good idea to add some utility functions related to the hue, such as normalize_hue(c).

@kimikage
Copy link
Collaborator Author

kimikage commented Dec 15, 2019

Just out of my curiosity, @johnnychen94, what are your concerns? Use of unnormalized hues? Backward compatibility? Additional costs for the normalizing?

@johnnychen94
Copy link
Member

johnnychen94 commented Dec 15, 2019

Hmmm, I'm not familiar with this topic, but as far as I can think of, normalizing a channel needs maxvalue of that channel, do we have some kind of assumption/defaults on it? For Grays and RGBs we can infer the max value from the storage type (e.g., N0f8 => 255), but since hue is stored as floats, we generally don't have such information, right? If you need to manually pass a maxvalue to normalize_hue, it might not be so convenient as you expect.

@kimikage
Copy link
Collaborator Author

kimikage commented Dec 15, 2019

I see. Because of the problems which you point out, I focused only on the hue, which should be simply normalized to [0, 360], not to [0, 1].

For example:

normalize_hue(c::T) where {T <: HSV} = T(mod(c.h, 360), c.s, c.v)

BTW, I think the color gamut problem must be solved somewhere (cf. JuliaGraphics/ColorTypes.jl#140). It is probably too early to put the gamut support in Colors.jl v1.0.

@johnnychen94
Copy link
Member

johnnychen94 commented Dec 15, 2019

I'm thinking of such kind of normalization:

function normalize_hue(x::T; maxvalue, minvalue) where T<:HSV
    HSV(gamutmax(T)[1]*(x.h-minvalue)/(maxvalue-minvalue), x.s, x.v)
end

julia> normalize_hue(HSV(-360, 1, 1); maxvalue=360, minvalue=-360)
HSV{Float32}(0.0f0,1.0f0,1.0f0)

As you can see, if we set maxvalue, minvalue = gamutmax(T)[1], gamutmin(T)[1], this normalization becomes an identity map since minvalue==0

@kimikage
Copy link
Collaborator Author

kimikage commented Dec 15, 2019

Although I don't think the idea is bad, I can't imagine the practical use cases. At least, the behavior with the hues which are not normalized to [0,360] is currently undefined (not documented).
"Apart from that" means that.

@timholy
Copy link
Member

timholy commented Dec 17, 2019

When a particular field represents an angle in degrees, generally we should interpret that number modulo 360. It's probably just that no one really needed this systematically before, or not enough to submit a PR, or we lacked tests to catch changes which broke this behavior. It would be fine to fix this.

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 24, 2020

I will modifiy the conversion codes for HSV, HSL and HSI.
BTW, is this expected behavior?

julia> HSV(colorant"red")
HSV{Float32}(360.0f0,1.0f0,1.0f0)

Of course, this is definitely red, but this gives strange results in the interpolation.
hsv

Edit:
Umm...

@test convert(HSVA{Float64}, red32) == HSVA{Float64}(360, 1, 1, 1)

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 26, 2020

Benchmark

I think the current RGB <--> {HSV, HSL, HSI} conversions are too slow.:fearful:
I guess the reason that the conversions to (A)RGB{N0f8} are slow is the inlining failure due to checkval, which can throw errors.
However, it seems that SIMD optimization is hardly applied, so the conversions cannot be made much faster. 😭 This may be a problem with the LLVM backend rather than Julia.
See the results of (A)HSV{Float32}->(A)RGB{Float32} on v1.0.5 (#379 (comment)), which are successfully vectorized.
Edit:
I made the RGB --> HSV / HSL conversions mostly compatible with the vectorization.:smiling_imp:

Script

using BenchmarkTools
using FixedPointNumbers
using Colors

rgb_n0f8 = rand(RGB{N0f8},64,64);
rgb_f32  = rand(RGB{Float32},64,64);
rgb_f64  = rand(RGB{Float64},64,64);
argb_n0f8 = rand(ARGB{N0f8},64,64);
argb_f32  = rand(ARGB{Float32},64,64);
argb_f64  = rand(ARGB{Float64},64,64);

rgbs = (rgb_n0f8, rgb_f32, rgb_f64, argb_n0f8, argb_f32, argb_f64);

for C in (HSV, HSL, HSI)
    AC = alphacolor(C)
    
    for T in (Float32, Float64)
        for mat in rgbs
            XC = eltype(mat) <: Color ? C{T} : AC{T}
            println(eltype(mat), "-> $XC")
            @btime convert.($XC, view($mat,:,:))
        end
        for mat in rgbs
            XC = eltype(mat) <: Color ? C{T} : AC{T}
            println(eltype(mat), "<- $XC")
            h = convert.(XC, mat)
            r = convert.(eltype(mat),h)
            all(t->isapprox(t...),zip(r, mat)) || @warn("failed") # verification
            @btime convert.(eltype($mat), view($h,:,:))
        end
    end
end

Julia v1.3.1 x86_64-w64-mingw32

julia> versioninfo()
Julia Version 1.3.1
Commit 2d5741174c (2019-12-30 21:36 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, skylake)

RGB --> HSV (unit: μs)

w64 (A)HSV
{Float32}
v0.11.2
(A)HSV
{Float32}
optimized
(A)HSV
{Float64}
v0.11.2
(A)HSV
{Float64}
optimized
RGB{N0f8} 51.100 6.220 52.300 29.299
RGB{Float32} 55.799 7.633 52.900 13.100
RGB{Float64} 54.600 12.499 55.000 12.500
ARGB{N0f8} 53.400 6.040 54.901 35.601
ARGB{Float32} 56.700 22.900 54.100 14.400
ARGB{Float64} 56.400 12.100 55.300 29.400

HSV --> RGB (unit: μs)

w64 (A)HSV
{Float32}
v0.11.2
(A)HSV
{Float32}
optimized
(A)HSV
{Float64}
v0.11.2
(A)HSV
{Float64}
optimized
RGB{N0f8} 84.699 45.600 90.100 48.499
RGB{Float32} 44.500 9.999 70.900 19.300
RGB{Float64} 71.999 36.800 45.900 29.899
ARGB{N0f8} 104.100 71.900 106.100 71.900
ARGB{Float32} 47.799 20.600 69.800 43.100
ARGB{Float64} 68.801 38.900 46.200 30.500

RGB --> HSL (unit: μs)

w64 (A)HSL
{Float32}
v0.11.2
(A)HSL
{Float32}
optimized
(A)HSL
{Float64}
v0.11.2
(A)HSL
{Float64}
optimized
RGB{N0f8} 77.599 6.675 78.500 36.499
RGB{Float32} 63.199 8.333 67.399 15.300
RGB{Float64} 47.399 13.300 65.600 15.599
ARGB{N0f8} 82.300 72.799 91.900 78.799
ARGB{Float32} 63.600 23.800 66.300 15.799
ARGB{Float64} 47.300 13.400 66.200 26.499

HSL --> RGB (unit: μs)

w64 (A)HSL
{Float32}
v0.11.2
(A)HSL
{Float32}
optimized
(A)HSL
{Float64}
v0.11.2
(A)HSL
{Float64}
optimized
RGB{N0f8} 110.100 46.499 101.199 47.800
RGB{Float32} 68.299 10.100 67.700 19.300
RGB{Float64} 64.499 36.900 68.300 30.700
ARGB{N0f8} 129.201 72.600 117.800 87.600
ARGB{Float32} 66.401 20.899 67.700 52.299
ARGB{Float64} 78.500 38.900 67.100 31.299

RGB --> HSI (unit: μs)

w64 (A)HSI
{Float32}
v0.11.2
(A)HSI
{Float32}
optimized
(A)HSI
{Float64}
v0.11.2
(A)HSI
{Float64}
optimized
RGB{N0f8} 125.899 116.899 145.399 176.600
RGB{Float32} 163.700 151.400 166.000 179.399
RGB{Float64} 190.201 179.099 183.100 171.999
ARGB{N0f8} 131.899 134.600 111.000 159.199
ARGB{Float32} 163.100 84.899 171.200 107.100
ARGB{Float64} 190.600 105.599 187.099 102.200

HSI{Float64} is slower because its intermediate variables have been changed to Float64 from Float32(floattype).

HSI --> RGB (unit: μs)

w64 (A)HSI
{Float32}
v0.11.2
(A)HSI
{Float32}
optimized
(A)HSI
{Float64}
v0.11.2
(A)HSI
{Float64}
optimized
RGB{N0f8} 141.000 111.300 178.600 119.200
RGB{Float32} 132.000 109.500 204.501 122.100
RGB{Float64} 136.900 119.400 180.800 118.700
ARGB{N0f8} 185.000 155.499 263.401 170.200
ARGB{Float32} 135.800 109.199 167.100 121.600
ARGB{Float64} 144.700 120.600 155.401 117.701

@kimikage
Copy link
Collaborator Author

Note that range returns normalized hue now. (cf. PR #405)
hue

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 28, 2020

Benchmark 2

Julia v1.0.5 x86_64-pc-linux-gnu (Debian on WSL)

julia> versioninfo()
Julia Version 1.0.5
Commit 3af96bcefc (2019-09-09 19:06 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, skylake)

RGB --> HSV (unit: μs)

linux (A)HSV
{Float32}
v0.11.2
(A)HSV
{Float32}
optimized
(A)HSV
{Float64}
v0.11.2
(A)HSV
{Float64}
optimized
RGB{N0f8} 52.200 5.933 55.100 38.900
RGB{Float32} 55.000 7.250 56.800 14.100
RGB{Float64} 51.300 12.700 57.900 13.900
ARGB{N0f8} 52.800 5.700 56.500 37.400
ARGB{Float32} 55.800 22.800 57.500 15.000
ARGB{Float64} 53.600 13.400 53.800 29.900

HSV --> RGB (unit: μs)

linux (A)HSV
{Float32}
v0.11.2
(A)HSV
{Float32}
optimized
(A)HSV
{Float64}
v0.11.2
(A)HSV
{Float64}
optimized
RGB{N0f8} 71.700 47.200 76.600 48.400
RGB{Float32} 43.200 10.200 49.500 20.100
RGB{Float64} 44.500 36.400 45.900 29.400
ARGB{N0f8} 92.300 71.500 93.400 71.800
ARGB{Float32} 44.700 21.400 48.100 43.200
ARGB{Float64} 52.700 38.700 47.100 29.900

RGB --> HSL (unit: μs)

linux (A)HSL
{Float32}
v0.11.2
(A)HSL
{Float32}
optimized
(A)HSL
{Float64}
v0.11.2
(A)HSL
{Float64}
optimized
RGB{N0f8} 78.300 6.220 78.200 35.600
RGB{Float32} 63.300 7.775 70.100 15.500
RGB{Float64} 47.000 13.400 66.700 14.700
ARGB{N0f8} 80.600 69.800 90.200 77.900
ARGB{Float32} 68.900 24.900 70.800 16.200
ARGB{Float64} 50.300 13.500 67.000 26.000

HSL --> RGB (unit: μs)

linux (A)HSL
{Float32}
v0.11.2
(A)HSL
{Float32}
optimized
(A)HSL
{Float64}
v0.11.2
(A)HSL
{Float64}
optimized
RGB{N0f8} 81.200 48.100 80.400 50.000
RGB{Float32} 64.800 10.400 67.100 20.000
RGB{Float64} 59.900 37.300 65.500 31.100
ARGB{N0f8} 100.300 72.100 103.400 72.500
ARGB{Float32} 64.700 21.600 70.300 51.600
ARGB{Float64} 60.500 38.400 68.900 31.500

RGB --> HSI (unit: μs)

linux (A)HSI
{Float32}
v0.11.2
(A)HSI
{Float32}
optimized
(A)HSI
{Float64}
v0.11.2
(A)HSI
{Float64}
optimized
RGB{N0f8} 130.200 116.500 146.400 176.100
RGB{Float32} 190.800 151.600 164.600 177.000
RGB{Float64} 224.100 177.600 180.000 171.400
ARGB{N0f8} 137.200 135.300 106.100 159.900
ARGB{Float32} 158.800 84.200 170.400 105.500
ARGB{Float64} 190.100 103.600 179.600 99.900

HSI{Float64} is slower because its intermediate variables have been changed to Float64 from Float32(floattype).

HSI --> RGB (unit: μs)

linux (A)HSI
{Float32}
v0.11.2
(A)HSI
{Float32}
optimized
(A)HSI
{Float64}
v0.11.2
(A)HSI
{Float64}
optimized
RGB{N0f8} 120.900 112.600 144.400 117.000
RGB{Float32} 123.500 108.900 133.600 116.400
RGB{Float64} 108.500 119.200 130.800 113.500
ARGB{N0f8} 149.200 151.800 174.200 157.500
ARGB{Float32} 108.100 108.500 136.800 119.300
ARGB{Float64} 110.400 119.700 130.700 114.600

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 29, 2020

BTW, I was going to send a PR for the improved version of #351 after a consensus was reached on the gamut or rand issues. However, the consensus building is likely to take some time. First, I have to start by attracting the developers and users.
I would like to submit the improved version of #351 with static conversion matrices instead of ones calculated dynamically from the color primaries.

@kimikage
Copy link
Collaborator Author

FYI, unfortunately, Julia nightly build can optimize the following muladd to a simple multiplication 😭:

div60(x::T) where T <: Union{Float32, Float64} = muladd(x, T(1/960), x * T(0x1p-6))

IMO, such an optimization is too drastic, but some people want it, so it is not a bug.
I have used fma instead of muladd here, but the same problem may occur in FixedPointNumbers.

@kimikage
Copy link
Collaborator Author

Quiz: Do you know what is different? 😄

function _hsx_to_rgb(im::UInt8, v::T, n::T, m::T) where T <:Union{Float16, Float32, Float64}
    vu, nu, mu = reinterpret.(Unsigned, (v, n, m)) # prompt the compiler to use conditional moves
    r = ifelse((im & 0b100001) != 0x0, vu, ifelse((im & 0b010010) != 0x0, nu, mu))
    g = ifelse((im & 0b000110) != 0x0, vu, ifelse((im & 0b001001) != 0x0, nu, mu))
    b = ifelse((im & 0b011000) != 0x0, vu, ifelse((im & 0b100100) != 0x0, nu, mu))
    return reinterpret.(T, (r, g, b))
end
function _hsx_to_rgb(im::UInt8, v::T, n::T, m::T) where T <:Union{Float16, Float32, Float64}
    vu, nu, mu = reinterpret.(Unsigned, (v, n, m)) # prompt the compiler to use conditional moves
    r = ifelse((im & 0b100001) == 0x0, ifelse((im & 0b010010) == 0x0, mu, nu), vu)
    g = ifelse((im & 0b000110) == 0x0, ifelse((im & 0b001001) == 0x0, mu, nu), vu)
    b = ifelse((im & 0b011000) == 0x0, ifelse((im & 0b100100) == 0x0, mu, nu), vu)
    return reinterpret.(T, (r, g, b))
end

@johnnychen94
Copy link
Member

Quiz: Do you know what is different? 😄

Both are black magic spell to me 😕

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 29, 2020

I think this is "visually" easy to understand, so not so black. 😄

if     hue <  60; im = 0b000001 # ---------+
elseif hue < 120; im = 0b000010 # --------+|
elseif hue < 180; im = 0b000100 # -------+||
elseif hue < 240; im = 0b001000 # ------+|||
elseif hue < 300; im = 0b010000 # -----+||||
else            ; im = 0b100000 # ----+|||||
end                             #     ||||||
(hue < 60 || hue >= 300) === ((im & 0b100001) != 0x0)

I will add this comment to the source code.

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 29, 2020

In short, the root of the speed problem is the inlining. As pointed out earlier in FixedPointNumbers, there is no advantage to appending @inline to cnvt(). The caller of cnvt() (i.e. ColorTypes._convert()) fails inlining. We should not add @inline to ColorTypes._convert(). Inlining the methods which do not benefit from inlining has a fatal performance penalty.

Edit:
In fact, some conversions of transparent colors with the N0f8 elements cause noticeable slowdowns due to incomplete inlining.

@kimikage
Copy link
Collaborator Author

kimikage commented Feb 29, 2020

I have used fma instead of muladd here, but the same problem may occur in FixedPointNumbers.

# Disable LLVM's fma if it is incorrect, e.g. because LLVM falls back
# onto a broken system libm; if so, use openlibm's fma instead

https://github.com/JuliaLang/julia/blob/4d8f889464d5d9168e111d698b1c74a71ff24614/base/floatfuncs.jl#L321-L335

😱

if Sys.ARCH !== :i686
    _fma(x, y, z) = fma(x, y, z)
else
    _fma(x, y, z) = muladd(x, y, z)
end

😓
It is undesirable that the behavior change depending on the environment. However, on x86, the fma instruction set is usually used , so far as the compiler does not get rid of muladd.

@johnnychen94
Copy link
Member

Hmmm, perhaps the color processing world doesn't need that kind of accuracy?

@timholy
Copy link
Member

timholy commented Mar 1, 2020

after a consensus was reached on the gamut or rand issues.

If the main discussion is somewhere besides JuliaGraphics/ColorTypes.jl#150, can you provide a link?

@kimikage
Copy link
Collaborator Author

kimikage commented Mar 1, 2020

cf. JuliaGraphics/ColorTypes.jl#150 (comment)

@kimikage
Copy link
Collaborator Author

kimikage commented Mar 1, 2020

I will also look for workarounds other than fma.

Originally posted by @kimikage in #407

julia> versioninfo()
Julia Version 1.5.0-DEV.376
Commit ea067fb221 (2020-03-01 09:36 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-9.0.1 (ORCJIT, skylake)

julia> div60(x::Float64) = fma(x, 1/960, x * 0x1p-6); # What I just want is, 1 `vmul` and 1 `vfmadd`

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vmulsd  xmm1, xmm0, qword ptr [rax]
        movabs  rax, 761602336
        vfmadd132sd     xmm0, xmm1, qword ptr [rax]
...snip...

julia> div60(x::Float64) = muladd(x, 1/960, x * 0x1p-6); # but I don't want to use `fma()`, so, let's use `muladd()`

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vmulsd  xmm0, xmm0, qword ptr [rax] # Do you know the intent that I used the distributive law?
...snip...

julia> div60(x::Float64) = x * (1/960) + x * 0x1p-6; # OK, keep it simple stupid

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vmulsd  xmm1, xmm0, qword ptr [rax]
        movabs  rax, 761602592
        vmulsd  xmm0, xmm0, qword ptr [rax]
        vaddsd  xmm0, xmm1, xmm0            # Good! But you can get rid of this `add` by `fma`!
...snip...
        
julia> div60(x::Float64) = muladd(x, 1/960, muladd(x, 0x1p-6, 0.0)); # OK, OK. You can optimize `muladd` into `mul`.

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vxorpd  xmm1, xmm1, xmm1
        vfmadd231sd     xmm1, xmm0, qword ptr [rax] # Nice double `vfmadd`s!
        movabs  rax, 761601960
        vfmadd132sd     xmm0, xmm1, qword ptr [rax]
...snip...

julia> div60(x::Float64) = muladd(x, 1/960, muladd(x, 0x1p-7, x * 0x1p-7)); # You know the distributive law, right?

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vmulsd  xmm1, xmm0, qword ptr [rax]  # 1 `vmul` is OK,
        vaddsd  xmm1, xmm1, xmm1             # but did you forget the distributive law?
        movabs  rax, 761602088
        vfmadd132sd     xmm0, xmm1, qword ptr [rax]
...snip...

julia> div60(x::Float64) = muladd(x, 1/960, muladd(x, -0x1p-7, x * 0x3p-7)); # I apologize for the same 0x1p-7 confusing you.
div60 (generic function with 1 method)

julia> @code_native syntax=:intel div60(1.0)
...snip...
        movabs  rax, offset .rodata.cst8
        vmulsd  xmm0, xmm0, qword ptr [rax] # You know the distributive law! 
                                            # But you should learn that floating-point arithmetic does not have full associativity!
...snip...

💢💢💢

kimikage added a commit that referenced this issue Mar 4, 2020
This adds the clamping and hue normalization for sources to HSx-->RGB conversions.
This also adds the clamping for destinations to HSI-->RGB conversion.
@kimikage
Copy link
Collaborator Author

kimikage commented Mar 4, 2020

Solved(?) by #407 (comment)

Please report when you get wrong colors or significant slowdowns.

HSx-->RGB conversions seems to work fine. 😄
http://juliagraphics.github.io/Colors.jl/latest/constructionandconversion/#Available-colorspaces-1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants