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

Gamut Mapping Lch vs Oklch #118

Closed
facelessuser opened this issue Feb 18, 2022 · 8 comments
Closed

Gamut Mapping Lch vs Oklch #118

facelessuser opened this issue Feb 18, 2022 · 8 comments
Labels
S: needs-decision A decision needs to be made regarding request. T: enhancement Enhancement.
Milestone

Comments

@facelessuser
Copy link
Owner

I think Oklch is an excellent interpolation space. It doesn't have the weird purple shifts that Lch does, and it just seems to do a better job.

What I am questioning is whether Oklch is a better gamut mapping algorithm. I'm considering backing out the use of Oklch for gamut mapping. I'd argue that generally, Lch gives me visually what I would expect as far as gamut mapping is concerned, but Oklch/Oklab gives me better interpolation results compared to Lch/Lab.

w3c/csswg-drafts#7071

@gir-bot gir-bot added the S: triage Issue needs triage. label Feb 18, 2022
@facelessuser
Copy link
Owner Author

I'm not saying Oklch can't be used to do gamut mapping, but I think the current algorithm isn't quite working so well.

@facelessuser
Copy link
Owner Author

facelessuser commented Feb 19, 2022

I've rolled back to using Lch for now. Oklab is still the default interpolation space as it works much better, but Lch seems to give more expected results when it comes to gamut mapping in gradients and such. The cost of running ∆E2000 is of course a bit more expensive, but we are visually getting colors that make more sense.

I'll leave this issue open to see how conversations go in regards to the CSS specification.

  1. I'm having a hard time finding a way to justify using Oklch if visually things turn out weird when using it. Certain color spaces have their strengths and weakness. Oklch/Oklab seem better suited for interpolation, just not gamut mapping via chroma reduction. There seems to be some interesting approaches with Oklab/Oklch here: https://bottosson.github.io/posts/gamutclipping/. These algorithms are also fairly complicated and currently tuned for sRGB specifically.
  2. The CSS algorithm seems generally more efficient than the approach colorjs.io uses (which is basically what we use), but it is not without some quirks that cause some not so smooth transitions sometimes. I've expressed this in some other issues where I've been evaluating the algorithm.

Generally, I've tried to sort of match the CSS specification where it makes sense, but I'd rather give good results for gamut mapping rather than blindly adopt the CSS approach, especially since we are in the very early stages. Things could certainly change in the future as is often the case with the CSS spec.

@facelessuser
Copy link
Owner Author

I've been looking into this, and it appears that some colors that are outside the spectral locus do not quite map back in Oklch as one would normally expect. This is most likely because the calculations are optimal for colors within the spectral locus. As you push outside that range, I imagine, in some hues, the calculations go off the rails. This doesn't make the model bad, just not ideal for these extreme cases. I think LCH just deals better with these cases.

I'm looking at taking some of the earlier CSS simplifications to the gamut mapping algorithm and applying them to our LCH implementation. This would remove our ∆E2000 usage and greatly increase gamut mapping speed.

Originally, I thought the algorithm was just not that good because Oklch gamut mapping was already not great in some cases, but overall, it doesn't seem that bad or that different than what we are already doing. The one exception is their shortcut out of the binary search that uses color distancing. It doesn't create "bad" colors, just more choppy results in interpolations. We'll probably leave the shortcut out.

@facelessuser
Copy link
Owner Author

facelessuser commented Feb 24, 2022

I think I understand better why the distancing is incorporated in the algorithm. It helps yellow and cyan not to have super low chroma. But it does make some interpolations a little choppier, but you get better yellow and cyan in CIE LCH at a quicker speed. Our current algorithm does a good job keeping the chroma pretty high for yellow and cyan, giving smooth transitions but is more expensive as we rely heavier on the distancing.

@facelessuser
Copy link
Owner Author

I still think our current MINDE gives better results. I think we'll eat the cost of speed for now.

@facelessuser
Copy link
Owner Author

facelessuser commented Feb 26, 2022

Spent a little more time on this as I'd really like us to get some of the speed boosts.

The CSS algorithm overcorrects the chroma too much in some cases by being too aggressive in the chroma reduction. The current algorithm we use is a bit too heavy on the delta E checks causing major slow downs.

I think I've found a way to blend the two algorithms to give us a very noticeable speed boost, but prevent us from overcorrecting the chroma quite as much. Basically, aside from relying on LCH instead of Oklch, we follow the CSS algorithm in the beginning:

  • increase the lower chroma boundary if the color is in gamut and perform no delta E checks.
  • If out of gamut, we compare the current color to a clipped version of itself, and if the delta E is above the JND, we lower the chroma upper chroma boundary.
  • Once we detect a case where we are out of gamut and the delta E is below the JND, we set that as the lower chroma boundary and we can then stop performing "in gamut" checks.
  • We continue until we are "close enough" to the JND and then kick out of the loop.

The key is that the CSS algorithm sometimes returns very low under the JND. When this happens, chroma reduction can be quite aggressive. On the other hand, our current algorithm waited until the high and low chroma boundaries converged, and since we performed delta E checks on every iteration, it was a bit more expensive.

Now we avoid doing delta E checks when in gamut increasing speed. Once both our low and high boundary are out of gamut, we tune the chroma in order to get as close to the JND as possible keep hue close enough without forcing chroma too low. We pick up additional speed by kicking out when delta E is close to JND instead of waiting for chroma boundaries to converge.

In testing, we can see we are not as fast as the CSS algorithm, but are faster than our old algorithm by a noticeable amount. We also have results close to the old method giving us a less aggressive chroma reduction, giving us better results in general.

Here we see the conversion of a display P3 yellow to LCH:

>>> Color('color(display-p3 1 1 0)').convert('lch')
color(--lch 97.366 123.27 98.13 / 1)

Below are the timings of various implementations

CSS Old New
706 usec 2.57 msec 1.12 msec

The New way is about twice as fast as the old way, but obviously the CSS algorithm is twice as fast as the new way, but results aren't quite as good due to over correction of the chroma.

Here we see the results of fitting it with the old algorithm:

>>> Color('color(display-p3 1 1 0)').convert('lch').fit('srgb')
color(--lch 97.248 94.365 99.221 / 1)

Here we see the CSS results:

>>> Color('color(display-p3 1 1 0)').convert('lch').fit('srgb')
color(--lch 97.004 61.39 99.806 / 1)

Here we see the new method:

>>> Color('color(display-p3 1 1 0)').convert('lch').fit('srgb')
color(--lch 97.247 94.364 99.22 / 1)

We can see the new method is much faster than the old one but more on par with chroma reduction not being quite as aggressive.

@facelessuser facelessuser added this to the 1.0.0 milestone Mar 3, 2022
@facelessuser facelessuser mentioned this issue Mar 6, 2022
12 tasks
@facelessuser facelessuser added T: enhancement Enhancement. S: needs-decision A decision needs to be made regarding request. and removed S: triage Issue needs triage. labels May 6, 2022
@facelessuser facelessuser removed this from the 1.0.0 milestone Jul 7, 2022
@facelessuser
Copy link
Owner Author

Removed milestone. We have a current, CSS-compatible GMA. Will it be the final one? 🤷🏻

Either way, we have something that works well, and people can use the CSS if they like. We can always add new ones and deprecate old ones. This shouldn't block our 1.0 release.

@facelessuser facelessuser added this to the Post 1.0 milestone Jul 22, 2022
@facelessuser
Copy link
Owner Author

I think what I've realized about gamut mapping and OkLCh is that gamut mapping with OkLCh is fine if you are gamut mapping within an acceptable range for OkLCh. So if you are shifting a hue in OkLCh, gamut mapping it in OkLCh will probably be fine. But if you are shifting a hue in another cylindrical space, the way it is mapped in that color space could swing the color outside OkLCh's acceptable range. For instance, LCh can do this.

What this simply means is that OkLCh has a reasonable limit to its algorithm. It simply breaks down with a very wide gamut of colors. LCh just has a more sane way of handling very extreme colors. The way that OkLCh bends the colors to do better hue preservation causes some weird behavior when the color is closer to the edge of the visible spectrum (or beyond).

OkLCh is not the only color space that has such behaviors, but OkLCh definitely has some more extreme adverse effects compared to others. Compare the behavior of Prophoto RGB rendered in OkLCh vs LCh. We can see a far more extreme response in OkLCh.

Screenshot 2023-06-15 at 9 32 28 PM Screenshot 2023-06-15 at 9 34 23 PM

A color like JzCzhz has a more extreme response than LCh, but still not as intense as OkLCh.

Screenshot 2023-06-15 at 9 31 58 PM

This just shows why we can get odd results when gamut mapping. This doesn't mean OkLCh is a bad color space, just that it has limits to use in some circumstances.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S: needs-decision A decision needs to be made regarding request. T: enhancement Enhancement.
Projects
None yet
Development

No branches or pull requests

2 participants