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

Chroma reduction for gamut mapping produces very poor results in certain cases #32

Closed
LeaVerou opened this issue May 24, 2020 · 13 comments
Labels
bug Something isn't working

Comments

@LeaVerou
Copy link
Member

Most notably, when mapping color(display-p3 1 1 0) to sRGB. Instead of producing yellow, it produces… rgb(100% 97.5% 77.5%), i.e. a light yellow.

Tests here: https://colorjs.io/tests/gamut.html

It appears that this happens because the red component remains slightly above 1 + ε, even with substantial chroma reduction:

image

CC @tabatkins as he originally implemented the gamut mapping algorithm so he may be interested.

@LeaVerou LeaVerou added the bug Something isn't working label May 24, 2020
@LeaVerou LeaVerou added this to the Public release milestone May 24, 2020
@tabatkins
Copy link
Collaborator

Ah I see, it skirts right next to the boundary for a long while before it finally touches. I agree, that doesn't match expectation; a brighter yellow (with slightly less red) is much closer to the author's intent.

Hmm, getting this right is a little tricky. Seems like the right approach might be to check, on each hop, if we're close to the gamut in absolute terms (that is, if a small channel clip would bring us in-gamut, with minimal color-warping), and if so, treat that as in-gamut for the purpose of the binary search.

That should find us a value with minimal-to-zero blue in this case, and only a tiny warp in hue.

I wonder if we want to be slightly more sophisticated than binary search, too - start the search with a linear spread of probes along the chrome-reduction path, then weight closeness-to-gamut against amount-of-chroma-reduction to find the interval to search in.

@tabatkins
Copy link
Collaborator

Hmmm, that final suggestion of mine might also help us deal with the "overhang" problem, where the varying-chroma line has multiple in-gamut and out-of-gamut sections. Binary search can skip over the overhang, even if it's close to the starting color, and force us to the much lower-chroma solution; and if the starting color is between the two in-gamut segments, looking only at decreased chroma values will never find the higher-chroma segment, even if it's just barely higher than the starting color!

So doing a quick probe of several locations, mostly below but some above, the starting color and checking if they're "close" to being in-gamut (that is, if channel-clipping would be only a tiny change), could really help.

@svgeesus
Copy link
Member

Looking at the blue component, the first few Chroma reduction steps go:

  • -1.27 (original color)
  • -0.827
  • -0.307
  • 0.157

so after 3 iterations the blue component is well inside gamut. Smaller Chroma steps, or binary search that explores the interval between -0.307 and 0.157, would get us close to zero on blue.

Perhaps, to detect "close to boundary" compute ΔE2000 between the current color and a per-component clipped version of the current color?

@tabatkins
Copy link
Collaborator

I was thinking even simpler - just a quick "in the output space, is the distance between the current point and a channel-clipped version below ε?", but doing the distance computation in ΔE is probably smarter. ^_^

@svgeesus
Copy link
Member

svgeesus commented Jun 5, 2020

I did some calculations. deltaE2000 between P3 yellow and sRGB yellow is 5 (noticeable side by side, but fairly similar) while deltaE2000 between P3 yellow and our super-desaturated gamut mapped yellow is a whopping 22.4.

yellows

@svgeesus
Copy link
Member

svgeesus commented Jun 5, 2020

Ok even better, looking at the values Lea posted before and doing a clip when it is close, we get a deltaE2000 of 0.79 which is barely visible:

yellow2

@tabatkins
Copy link
Collaborator

First of all, nice.

Second, yeah, I've been thinking about this somewhat wrong. Our goal is to find the color as close to the original as possible which is in the output gamut. So we don't need to do any of that "balance out the chroma reduction vs the clipping distance", we just need to minimize the ΔE along the chroma-reduction line.

So yeah, sample a smattering of points in either direction (each 10 points of chroma?) from the starting point along the chroma increase/decrease line, channel-clipping each into the output gamut and measuring the ΔE from the starting color. Find the point with the smallest ΔE, and sub-sample on either side to find the minimum ΔE within the precision we want to care about.

I'm curious if there are any degenerate cases here we'd want to guard against, where the minimum ΔE would be for a channel-clipped color but still be fairly large. It's probably better to stay constant-hue-and-lightness in that case, right? So maybe check if the smallest ΔE from the initial sampling is above some threshold (5? 10?) and if so, just stick with the normal binary-search along the chroma-reduction line (aided by the fact that you already know approximately where it is, so you can start the search pretty accurately); this way you'll only shift the hue/lightness a tiny bit when it's warranted.

@svgeesus
Copy link
Member

svgeesus commented Jun 9, 2020

A recent analysis Colour gamut mapping between small and large colour gamuts: Part I. gamut compression

Note that most color gamut mapping research is concerned with natural, photographic images. Images containing vector-style graphics and type are rarely considered. Colors that form a palette of colors used in a Web page are pretty much never considered. However, the paper above examines several approaches including mapping towards the black point or white point rather than along lines of constant Lightness. In addition to CIE Lab, it also examines CAM02-UCS and Jzazbz for gamut mapping

@svgeesus
Copy link
Member

This is very well illustrated in Kenichiro Masaoka, Yuichi Kusakabe, Takayuki Yamashita, Yukihiro Nishida, Tetsuomi Ikeda, and Masayuki Sugawara. Algorithm Design for Gamut Mapping From UHDTV to HDTV. Journal of Display Technology, vol. 12, No. 7, July 2016

Their GMA works in Lab and is hue preserving except for yellow and cyan highlights, where is is lightness preserving with a hue shift.

masaoka

@svgeesus
Copy link
Member

Here is P3 yellow, with LCH Chroma reduced to the neutral axis. The RGB values are linear-light P3. The color wedge shows sRGB values, if in gamut; salmon, if outside sRGB and red if outside P3. Notice the red curve goes up (so, out of gamut) before finally dropping again.

https://drafts.csswg.org/css-color-4/images/lab-yellow-LCH-fade.svg

Here is the same thing but at each stage, I calculate the deltaE2000 between the current color and the color clipped to sRGB. If the deltaE is less than 2, the clipped color is displayed. Notice the red curve hugs the top edge now because clipping to sRGB also means it is inside P3 gamut. Notice how we get an in-gamut color much earlier. Starting Chroma for P3 yellow is 123.27 and we get an in-gamut color by Chroma 103. I did also try with deltaE76 but (in addition to it wildly over estimating the color difference for very saturated colors) it was a bit worse on gamut mapping, giving an in-gamut value at a Chroma of 95.

https://drafts.csswg.org/css-color-4/images/lab-yellow-LCH-clip-fade.svg

(Obligatory sigh that GitHub lamely does not allow SVG images, even though it displays them just fine if they are in a repo)

@svgeesus
Copy link
Member

Improved

@svgeesus
Copy link
Member

svgeesus commented Jul 15, 2020

So it is better, but I can still get closer by manually tweaking LCH chroma until the deltaE is at minimum

image

image

@svgeesus
Copy link
Member

Although the current algorithm doesn't find the optimal result, a deltaE2000 error of 0.3 is imperceptible. This issue can be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants