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

[css-color-4] Define gamut mapping #5191

Closed
LeaVerou opened this issue Jun 11, 2020 · 14 comments
Closed

[css-color-4] Define gamut mapping #5191

LeaVerou opened this issue Jun 11, 2020 · 14 comments
Labels
css-color-4 Current Work

Comments

@LeaVerou
Copy link
Member

Since we now support a number of wide gamut spaces (display-p3, prophoto etc, but also lab/lch whose gamut is the entire spectral locus), we might want to define gamut mapping as it can dramatically affect what is displayed. Leaving it up to the implementation sabotages any effort at device independent color.

This is not a new issue: on screens whose gamuts are smaller than sRGB, CSS colors still have to be gamut mapped to be displayed. However, it is currently undefined, and up to implementations.

One constraint is that we cannot use sophisticated rendering intents that take the entire webpage into account, like what is used in photography, since that would be prohibitively expensive, not to mention inherently insecure. So, we need to specify a gamut mapping algorithm that operates entirely on the <color> value.

LCH chroma reduction is a common one, but fails terribly for yellows, producing results far worse than even simple component clipping. E.g. when color(display-p3 1 1 0) is converted to sRGB using chroma reduction via binary search, it produces rgb(100% 97.5% 77.4%), a light yellow, whereas something closer to rgb(95% 100% 0%) would have been a far better fit.

@svgeesus has found some papers & book references that would be helpful, but I'll leave it up to him to link/quote them.

@LeaVerou LeaVerou added the css-color-4 Current Work label Jun 11, 2020
@faceless2
Copy link

faceless2 commented Jun 11, 2020

That's an interesting proposal. On my to-do list was filing an issue on the rendering-intent property which can currently be specified as part of a @color-profile rule.

The problem is that it sets the intent for the color-space as a whole, but that's not how I expect it will be used (if it's used at all, which quite frankly I'm doubtful of). For example, if I'm drawing a gradient in CMYK, I may want to use "perceptual" intent, but if I then want to do some block colors in the same colorspace, say for a barchart, I'd want to use "saturation".

So the rendering-intent really needs to be specified on the element, not on the color-profile.

It's of such limited applicability for browsers (if it applies at all, it's only when the color they need to display is specified with a CMYK ICC color-profile) that I expect browser vendors can get away with ignoring it completely. For the CSS-to-PDF vendors, the rendering intent is applied to an individual drawing operation, not the color-space as a whole, so this is a more natural fit.

So hopefully promoting it from the @color-profile rule to a regular property will make next to no difference for anyone.

Anyway, the point I came here to make was that if you're going to do this, and also need to define some sort of gamut-mapping function, it might not be a bad idea to roll it into the same property. So, for example, your element could have rendering-intent: saturation to use the saturation tables in the ICC profiles if available, or rendering-intent: whizzy-lch-function to use whatever @svgeesus digs up.

@LeaVerou
Copy link
Member Author

@faceless2 You raise a good point: While a perceptual rendering intent is prohibitively expensive for the Web, it may be a viable option for other host environments, and whatever we specify should certainly allow for such superior rendering intents, but it cannot mandate them.

You're also correct that it cannot be specified per color space, because CSS is more granular than that. However, even per-element is insufficient granularity I'm afraid. As you point out, each use case calls for a different rendering intent. Also, in the general case, even on a single gradient, you can have colors that come from multiple sources, e.g. variables from a different stylesheet. It would be rather weird and surprising to have the same color variable displayed a different way based on where it's used.

@svgeesus
Copy link
Contributor

Does anyone really use the saturation rendering intent? Making 1990's style ugly hypersaturated business graphics seems like a very niche case.

Absolute colorimetric means no white point adaptation and attempting to preserve absolute luminance. Apart from testing and research I get the impression it really isn't that useful

Browsers may well use the perceptual intent for rendering photographic images, but since perceptual is like CSS auto i.e. "do something magic and poorly specified" we can't guarantee that colors in CSS and colors in an image will match if both use perceptual. So in practice the options are:

  1. Use perceptual for photographic images and relative colorimetric for non-photographic images and CSS. Give up on color matching from CSS to colors in the photos.

  2. Use relative colorimetric for everything, and give up on smooth gradients between OOG and IG colors.

@faceless2
Copy link

Yeah I know. I've just been reading up on ISO17972 - revisiting rendering intents is like comparing brain-surgery to phrenology. I agree absolute makes no sense. As for the rest, I suppose I'm just saying that if it's to be defined it should be per-element.

@LeaVerou if we have multiple colors in a gradient we have to collapse to a single space anyway which is the space of the element they're being rendered in, not where the variable was defined. So maybe not that weird. And I can promise you that no-one on the PDF side of the output is going to be doing anything perceptual based on the document contents :-) Rendering-intent will be used to select a lookup table in the ICC profile, if it's used at all.

@LeaVerou
Copy link
Member Author

@svgeesus We cannot give up on smooth gradients between out of gamut and in gamut colors, out of gamut colors are very common, especially in LCH interpolation.

@svgeesus
Copy link
Contributor

svgeesus commented Sep 5, 2020

@LeaVerou wrote:

LCH chroma reduction is a common one, but fails terribly for yellows, producing results far worse than even simple component clipping. E.g. when color(display-p3 1 1 0) is converted to sRGB using chroma reduction via binary search, it produces rgb(100% 97.5% 77.4%), a light yellow, whereas something closer to rgb(95% 100% 0%) would have been a far better fit.

The simple method is to reduce Chroma (either progressively, or by binary search) until the color is in gamut. It fails terribly for light yellows and cyans, due to concavity of the gamut boundary. However, a slight modification produces much better results:

  • start with startColor
  • binary search reduction of Chroma, giving currentColor
  • per-component clip, giving currentClippedColor
  • done if deltaE2000 between currentClippedColor and currentColor < threshold

This is better described (with examples and diagrams) on Color.js gamut mapping page. The concavity issue is shown on the first diagram, where chroma reduction actually pushes the modified color outside P3 until it finally drops back when massively desaturated.

When color(display-p3 1 1 0) is converted to sRGB using chroma reduction via this modified method (with a threshold of 2, which is barely visible), it produces rgb(100% 99.1% 9.1%), which is a much better fit.

Having examined this for P3 -> sRGB and for REC2020 -> P3 gamut mapping, which are the two that I expect to be most common, I feel fairly comfortable proposing this for the specification, for Lab/LCH/RGB to RGB mapping.

RGB to CMYK, CMYK to RGB, and CMYK to CMYK are likely to use ICC profiles, and v4 ICC will map this in two stages, relative to the Perceptual Reference Material Gamut. Since that part is defined by ICC I don't think the spec needs to cover it.

@jrus
Copy link

jrus commented Nov 26, 2020

All of the standard “rendering intents” as handled by e.g. Photoshop have pretty bad results. To be honest every automatic gamut mapping tool I have ever used was pretty much garbage. I think gamut mapping can be done properly in an automatic way, but in my own practical imaging I always put in manual effort to get the effect I want.

The ideal for photographs is to do something fancy where you try to balance (a) preserving local (color and lightness) contrast at different spatial scales so you don’t end up with areas that are blown out or overly compressed vs. (b) keeping colors that are out of gamut as colorful as possible, all while (c) trying to preserving hue. I have ideas about how to implement this automatically, but haven’t seen any practical implementations that work too well.

For something like CSS colors, each color should be treated independently; making the result depend on surrounding colors is going to be counterintuitive and confusing.

  • If there is a well known source gamut (e.g. mapping some specific print gamut to a specific screen or vice versa) it’s possible to make an effort to avoid hard clipping (i.e. collapsing multiple source colors to the same destination color on the gamut boundary), by applying some kind of nonlinear function with shape based on both gamut sizes (usually this means that even colors which are nominally inside the target gamut will have colorfulness reduced somewhat if the source gamut is wider in that region). Even when gamuts are known I would avoid making colors more colorful than the source gamut supports; this is sometimes recommended and can make images look punchier but also can trample authors’ intentions.

  • If the source colors are in CIELAB or the like, then the only reasonable thing to do IMO is leave in-gamut colors alone and clip the out-of-gamut colors to some point on the gamut boundary, hopefully along a direction that preserves intended color attributes as well as possible.

Plausible methods for reducing colorfulness to bring out-of-gamut colors inside include mapping inward along a constant hue/lightness line, mapping with constant hue toward middle gray, or mapping with constant hue toward a gray whose lightness depends on the hue. All three of these work better than what most software does by default. There are various ways to get fancier than this but the added complexity isn’t worth what I think are mixed results.

I would not recommend specifying a root-finding method. Binary search is slower than necessary (especially if the intermediate color space is expensive to compute), and gives wrong results sometimes because there are sometimes areas in-gamut but with colors of the same hue/lightness but reduced chroma which fall out of the gamut.

Per-component clip does not give good results at all but instead dramatically distorts all color relationships and tramples on authors’ intentions, unless colors are so close to the gamut boundary that the clipping is trivial.

Gamut mapping should be done in some color space that more or less separates hue/value/chroma cleanly in a way that matches human perception. CIELAB works okay but the blue–purple shift can be annoying sometimes. CIECAM02 and IPT are both decent in my experience. I haven’t experimented with other possibilities.

People interested in this topic should read Ján Morovič’s monograph. I don’t agree with all of the conclusions but it is a good survey of the state of the art as of 2008 https://www.wiley.com/en-us/Color+Gamut+Mapping-p-9780470030325

I don’t think stuff like “saturation” intent (at least, in any way I have ever seen it interpreted) deserve attention at all. Completely worthless results in any context. In practice it is just as horrible for bar charts as for photographs. (I do think something reasonable with the same goals as saturation intent could plausibly be invented, and might technically qualify under the vague definitions provided by the ICC profile spec [“The colour rendering of the perceptual and saturation rendering intents is vendor specific. The former, which is useful for general reproduction of pictorial images, typically includes tone scale adjustments to map the dynamic range of one medium to that of another, and gamut reshaping and mapping to deal with gamut mismatches.”], but its results would not be similar to existing implementations.)

@jrus
Copy link

jrus commented Nov 26, 2020

@LeaVerou wrote:

We cannot give up on smooth gradients between out of gamut and in gamut colors, out of gamut colors are very common, especially in LCH interpolation.

There are not really any good choices here. You can maybe map each endpoint into gamut before interpolation, but then (even in-gamut) colors part way along the gradient are not going to be interpreted the same way across different devices.

What are your criteria for “smooth”? How many derivatives do you need to have continuous?

@svgeesus
Copy link
Contributor

svgeesus commented Dec 1, 2020

For something like CSS colors, each color should be treated independently; making the result depend on surrounding colors is going to be counterintuitive and confusing.

Agreed.

If the source colors are in CIELAB or the like, then the only reasonable thing to do IMO is leave in-gamut colors alone and clip the out-of-gamut colors to some point on the gamut boundary, hopefully along a direction that preserves intended color attributes as well as possible.

Yes.

Plausible methods for reducing colorfulness to bring out-of-gamut colors inside include mapping inward along a constant hue/lightness line, mapping with constant hue toward middle gray, or mapping with constant hue toward a gray whose lightness depends on the hue.

Yes, the first of those is the plan. We have implemented this in color.js and it seems to work well. In brief, work in CIE LCH and reduce Chroma (thus preserving Hue and Lightness) until a point "close" to the gamut boundary is reached. Closeness is determined by computing the DeltaE2000 between the current reduced-Chroma color and a per-component-clipped copy of the current reduced-Chroma color. This avoids excessive Chroma reduction caused by skating along the top of the gamut surface, especially with slight concavities.

Per-component clip does not give good results at all but instead dramatically distorts all color relationships and tramples on authors’ intentions, unless colors are so close to the gamut boundary that the clipping is trivial.

Agreed. Per-component clip is a terrible method which, currently is mandated (from CSS Color 3). The approach described above judiciously applies clip in the trivial case you mention.

Gamut mapping should be done in some color space that more or less separates hue/value/chroma cleanly in a way that matches human perception. CIELAB works okay but the blue–purple shift can be annoying sometimes. CIECAM02 and IPT are both decent in my experience. I haven’t experimented with other possibilities.

I have studied the blue hue non-linearity in CIE LCH and compared with Jzazbz 1 which was different but not better; I have implemented IPT but not yet run the study. I would avoid CIECAM02 for two reasons: firstly because the required information for 10 degree surround, ambient luminance, and surround is not available which means that default values are used, collapsing it from a color appearance model into a simple colorimetry model; and secondly because of numerical instability and non-invertibility as discussed here. CIECAM16 solved the latter problem but not the former one.

People interested in this topic should read Ján Morovič’s monograph.

Yes, I have it and it is very helpful. Much of it relates to the gamut mapping of photographic images, which does not apply directly to colors in CSS for the reason you mentioned, but it is a valuable work.

I don’t think stuff like “saturation” intent (at least, in any way I have ever seen it interpreted) deserve attention at all.

Yes, it seems completely useless; "I want these colors as saturated as possible but don't care what colors they are" might have been relevant to 1990s Harvard Graphics charts but certainly isn't now.

[1] An Efficient Uniform Color Space for High Dynamic Range and Wide Gamut Imagery. Safdar, Muhammad; Luo, M Ronnier.
Optics Express Vol. 25, Issue 13, pp. 15131-15151 (2017)
https://www.osapublishing.org/oe/fulltext.cfm?uri=oe-25-13-15131&id=368272

@svgeesus
Copy link
Contributor

svgeesus commented Dec 1, 2020

I forgot to mention

Hue linearity of color spaces for wide color gamut and high dynamic range. Zhao, B.; Luo, M. Ronnier. Journal of the Optical Society of America Vol. 37, No. 5 (May 2020)
https://www.osapublishing.org/josaa/abstract.cfm?uri=josaa-37-5-865

Which compares CIELAB, CIECAM16, IPT and Jzazbz.

@jrus
Copy link

jrus commented Dec 6, 2020

One more thing. Since binary search does not always return correct results for mapping into RGB gamut, one possible alternative to specify:

Take your curve (either a line pointed toward gray along constant hue/lightness or some other curve that eventually intersects the gamut boundary) and intersect it with all of the six surfaces R = 0, R = 1, G = 0, G = 1, B = 0, B = 1, and then choose the most colorful intersection among those 6 which still satisfies 0≤R≤1, 0≤G≤1, 0≤B≤1, and is less colorful than the original point.

Once this is implemented and results are correct there are various ways to skip some of the intersections or speed up the computations, but implementors should probably be left to handle the choice of specific optimizations.

@Myndex
Copy link
Member

Myndex commented Mar 1, 2021

Hi @LeaVerou

LCH chroma reduction is a common one, but fails terribly for yellows, producing results far worse than even simple component clipping.

I've been conducting some experiments here to remap only based on the CSS color, knowing little or nothing about the surrounds. In some ways it is similar to the contrast problem, though multiplied — and since gamut mapping affects contrast.... YIKES!!

I've been experimenting with various appearance models including SAPC variants and other experimental code. I don't have concrete answers yet.

One thing: in film and TV we (almost by default) use weighted soft-clipping, especially for one-light dailies and some temp proxies. Of course final grading is done manually, shot by shot, by a colorist with a system like DaVinci Resolve (which you can download free).

And also everything we do uses LUTs — lots and lots of LUTs. But ultimately, the color grade is a time consuming manual operation.

There is no "really wonderful" gamut mapping magical bullet, well, except my friend Stu's invention that happens to be called Magic Bullet, LOL. And it is basically a fancy adjustable LUT.

What's my point already...?

LUTS!

There are a small, finite number of spaces. And it is even easier for web content because everything is on a self illuminated display, everything is an RGB color model, and everything is the standard D65.
(except ProPhoto & I still don't understand why a non-display profile like ProPhoto is in CSS, and ICC v2/v4 PCS which has limited utility for web content and is a performance hog, totally useless for streaming).

LUTs are already in use to some degree on the user-side for device profiles. A LUT for working-space to a display space is typically considered the best practice outside of manual color grading. An RGB -> RGB LUT is typically small and efficient, unlike an RGB -> CMYK LUT. This also gives some control back to the author: a set of LUTs sent as a single file, perhaps in the CSS. Fingerprinting is less a concern as all the LUTs will be there and a media query will select the appropriate one.

AccessibLUT

Another advantage is the potential creation of "accessible fallback LUTs" that could in one file increase/decrease contrast, saturation, polarity (maintaining correct contrast) etc etc.

Andy

@svgeesus
Copy link
Contributor

From #6642

in the gamut mapping section, require gamut reduction to use OKLCH chroma reduction and deltaE OK, rather than CIE LCH chroma reduction and deltaE 2000. This is both better and faster.

@svgeesus
Copy link
Contributor

@jrus wrote:

One more thing. Since binary search does not always return correct results for mapping into RGB gamut, one possible alternative to specify:

Take your curve (either a line pointed toward gray along constant hue/lightness or some other curve that eventually intersects the gamut boundary) and intersect it with all of the six surfaces R = 0, R = 1, G = 0, G = 1, B = 0, B = 1, and then choose the most colorful intersection among those 6 which still satisfies 0≤R≤1, 0≤G≤1, 0≤B≤1, and is less colorful than the original point.

Once this is implemented and results are correct there are various ways to skip some of the intersections or speed up the computations, but implementors should probably be left to handle the choice of specific optimizations.

Yes. Finding that intersection analytically, or finding a fast approximation to the geometric intersection, is the approach examines (and well explained) in Studying Gamut Clipping .

And I agree about optimizations. I intend to describe what must be done, and point out some consequences:

  • for full-range RGB spaces, the white and black points are aligned
  • for RGB spaces, for a constant hue plane, the lower bound is a straight line if the Lightness transfer function is a simple power law
  • for RGB spaces, for a constant hue plane, the upper bound can be approximated by a straight line but that will result in visible artifacts due to the slight concavity

The choice of solving this fully analytically, or by geometric approximation, or by binary search, is then an implementer choice based on complexity, speed, and engineering effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
css-color-4 Current Work
Projects
None yet
Development

No branches or pull requests

5 participants