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

MSC() is strange #349

Open
kimikage opened this issue Sep 11, 2019 · 6 comments
Open

MSC() is strange #349

kimikage opened this issue Sep 11, 2019 · 6 comments

Comments

@kimikage
Copy link
Collaborator

kimikage commented Sep 11, 2019

MSC() have been introduced to realize the colormap function since 3e2dcf1 .
I think MSC() is strange in some respects.

1. Name

As MSC() is not a constructor but an ordinaryl function, it is to be desired that the name is lowercase to follow the Style Guide, even though the original reference paper uses MSC().
In particular, I think the naming is important because MSC is exported.
However, renaming MSC to msc, most_saturated_color, maximally_saturated_color or something else is not sufficient to solve the problems. The reasons are as follows.

By the way, although "colorfulness", "chroma" and "saturation" are often used loosely, the term "chroma" is used in the CIELAB and CIELUV color spaces. Perhaps "saturation" may mean that it is saturated in sRGB HSV space, though.

2. Return value

MSC(h) returns LCHuv color, but MSC(h, l) returns saturation value.
So, if we follow the behavior, they must have different names and especially the latter should be renamed.
Do we really need two different functions?

3. Color space

The current MSC() calculates in Luv(LCHuv) color space. I think the behavior is OK, but the name MSC() and its arguments are not informative about the color space.
The function to get the maximally saturated color in Lab color space, might improve distinguishable_colors().
When we add such a variant function or method, we should modify the interface.

4. Validity

Edit: see added comments below

I wrote the following ugly function using binary search:

function find_maximum_chroma(c::LCHuv, low, high)
    high-low < 1e-4 && return low

    mid = (low + high) / 2
    lchm = LCHuv(c.l, mid, c.h)
    rgbm = convert(RGB, lchm)
    notclamped = max(red(rgbm), green(rgbm), blue(rgbm)) < 1 &&
                 min(red(rgbm), green(rgbm), blue(rgbm)) > 0
    if notclamped || (lchm, convert(LCHuv, rgbm), atol=1e-4)
        return find_maximum_chroma(c, mid, high)
    else
        return find_maximum_chroma(c, low, mid)
    end
end

And then I got some strange results:

julia> find_maximum_chroma(LCHuv(90, NaN, 0), 0, 180) # 180 >= the maximum chroma in sRGB
22.20139503479004

julia> MSC(0, 90)
32.79945146698043

julia> convert(RGB, LCHuv(90, 22, 0)) # not saturated
RGB{Float32}(0.99905473f0,0.85173935f0,0.8769549f0)

julia> convert(RGB, LCHuv(90, 23, 0)) # saturated
RGB{Float32}(1.0f0,0.8500634f0,0.87646854f0)

julia> convert(RGB, LCHuv(90, 32, 0)) # of course, saturated
RGB{Float32}(1.0f0,0.83477974f0,0.87207484f0)

The disagreement is found in not only light colors but also purple colors:

julia> find_maximum_chroma(LCHuv(50, NaN, 280), 0, 180)
126.75390243530273

julia> MSC(280, 50)
121.16055070757858

julia> convert(RGB, LCHuv(50, 126, 280)) # not saturated
RGB{Float32}(0.6351404f0,0.24796246f0,0.99639124f0)

I don't know whether it is a feature. I have not investigated the cause of it.

Although it is not the main cause, I found a discrepancy in the gamma correction:

Colors.jl/src/algorithms.jl

Lines 233 to 234 in e7f4723

#gamma inversion
cp = cp <= 0.003 ? 12.92cp : 1.055cp^(1.0/g)-0.05

function srgb_compand(v)
v <= 0.0031308 ? 12.92v : 1.055v^(1/2.4) - 0.055
end

Proposal

What about a new function maximize_chroma(c::Union{Luv,LCHuv}; ltol=0, htol=0)?
Where ltol: lightness tolerance, htol: hue tolerance.
The current MSC() will be redefined as:

MSC(h) = maximize_chroma(LCHuv(0,0,h), ltol=Inf) 

MSC(h, l) = maximize_chroma(LCHuv(l,0,h)).c

Well, it is still in the planning stage and I am not ready for implementing it.

@kimikage
Copy link
Collaborator Author

kimikage commented Sep 13, 2019

  1. Validity

Although it is not the main cause, I found a discrepancy in the gamma correction:

Using srgb_compand(v), MSC(h) passes the following test.

for hsv_h in 0:0.1:360
    hsv = HSV(hsv_h,1.0,1.0) # most saturated
    lch = convert(LCHuv, hsv)
    msc = MSC(lch.h)
    @test msc  lch atol=1e-6
end

So, the following patch may not be necessary.

Colors.jl/src/algorithms.jl

Lines 195 to 203 in e7f4723

#check if we are directly on the edge of the RGB cube (within some tolerance)
for edge in [h0, h1, h2, h3, h4, h5]
if edge - 200eps() < h < edge + 200eps()
col[p] = edge in [h0, h2, h4] ? 0.0 : 1.0
col[o] = 0.0
col[t] = 1.0
return convert(LCHuv, RGB(col[1],col[2],col[3]))
end
end

@kimikage
Copy link
Collaborator Author

kimikage commented Sep 13, 2019

  1. Validity

The disagreement is found in not only light colors but also purple colors:

It turns out it was a simple reason. MSC(h, l) uses the linear interpolation.

Colors.jl/src/algorithms.jl

Lines 259 to 260 in e7f4723

a=(pend.l-l)/(pend.l-pmid.l)
a*(pmid.c-pend.c)+pend.c

The sRGB gamut is not triangular in L-C section, especially in blue to red via purple, as shown in Figure 3 from "Generating Color Palettes using Intuitive Parameters".

hue: 0° hue: 280°

see also: https://commons.wikimedia.org/wiki/File:SRGB_gamut_within_CIELUV_color_space_mesh.webm

It may be a good approximation for generating colormaps. However, I doubt it is sufficient for any purpose.

@timholy
Copy link
Member

timholy commented Sep 13, 2019

Really nice diagnosis and analysis, @kimikage. Your instincts are excellent, when you think you have a solution you like I look forward to your proposal.

@kimikage
Copy link
Collaborator Author

After hours of trial and error, I found that, somehow, the ugly function find_maximum_chroma (simplified version) is faster than any other methods I tried, even though it requires more than 20 iterations. 😅
Did the tail call optimization work well?

kimikage added a commit to kimikage/Colors.jl that referenced this issue Sep 13, 2019
kimikage added a commit to kimikage/Colors.jl that referenced this issue Sep 13, 2019
@timholy
Copy link
Member

timholy commented Sep 14, 2019

Did the tail call optimization work well?

Julia doesn't offer intrinsic support for TCO, except in cases where the result can be computed at compile time. But here it doesn't matter because most of the time is taken up by the v^(1/2.4) operation in srgb_compand.

kimikage added a commit that referenced this issue Sep 17, 2019
Fix erroneous `MSC(h)` and improve accuracy of `MSC(h, l)` (#349)
@timholy timholy modified the milestone: 1.0 Nov 22, 2019
@timholy timholy mentioned this issue Nov 22, 2019
4 tasks
@kimikage
Copy link
Collaborator Author

kimikage commented Jan 7, 2020

Probably maximize_chroma needs a lot of magic numbers. For this reason, I want to settle the RGB conversion matrix first.

However, it is not strictly "the first". It is related to the problem of gamut (cf. #372 (comment)).
Moreover, it is related to the problem with rand in ColorTypes.jl (cf. JuliaGraphics/ColorTypes.jl#125, JuliaGraphics/ColorTypes.jl#140).

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

No branches or pull requests

3 participants
@timholy @kimikage and others