Skip to content

Commit d51a437

Browse files
committed
[css-color-4] Add gamut mapping section. Fix #5191
1 parent 85aa82e commit d51a437

File tree

2 files changed

+200
-4
lines changed

2 files changed

+200
-4
lines changed

css-color-4/Overview.bs

+181-2
Original file line numberDiff line numberDiff line change
@@ -3172,7 +3172,7 @@ Device-independent Colors: CIE Lab and LCH, OKLab and OKLCH</h2>
31723172
The hue angles between CIE LCH and OKLCH are broadly similar,
31733173
but not identical.
31743174

3175-
<figure>
3175+
<figure id="CIELCH-blue-hueshift">
31763176
<img src="images/CIELCH-blue-slice.png" width="541" height="411"
31773177
alt="diagram showing purpling in CIE LCH">
31783178
<figcaption>A constant CIE LCH hue slice,
@@ -3181,7 +3181,7 @@ Device-independent Colors: CIE Lab and LCH, OKLab and OKLCH</h2>
31813181
</figcaption>
31823182
</figure>
31833183

3184-
<figure>
3184+
<figure id="OKLCH-blue-no-hueshift">
31853185
<img src="images/OKLCH-blue-slice.png" width="552" height="405"
31863186
alt="diagram showing hue constancy in OKLCH">
31873187
<figcaption>A constant OKLCH hue slice,
@@ -4642,9 +4642,188 @@ Hue interpolation</h3>
46424642

46434643
Issue(4928):
46444644

4645+
<h2 id="gamut-mapping">
4646+
Gamut Mapping
4647+
</h2>
4648+
4649+
<h3 id="gamut-mapping-intro">An Introduction to Gamut Mapping</h3>
4650+
4651+
<p><i>This section is non-normative</i></p>
4652+
4653+
When a color in an origin colorspace
4654+
is converted to another, destination colorspace
4655+
which has a smaller gamut,
4656+
some colors will be outside the destination gamut.
4657+
4658+
For intermediate color calculations,
4659+
these out of gamut values are preserved.
4660+
However, if the destination is the display device
4661+
(a screen, or a printer)
4662+
then out of gamut values ust be converted to
4663+
an in-gamut color.
4664+
4665+
Gamut mapping is the process of finding an in-gamut color
4666+
with the least objectionable change in visual apprearance.
4667+
4668+
The simplest and least acceptable method
4669+
is simply to clip the component values
4670+
to the displayable range.
4671+
This changes the proportions of (for an RGB display)
4672+
the three primary colors,
4673+
resulting in a hue shift.
4674+
4675+
A better method is to mapped colors
4676+
in a perceptually uniform colorspace,
4677+
by finding the closest in-gamut color
4678+
(so-called minimum ΔE or <dfn export>MINDE</dfn>).
4679+
Clearly, the sucess of this technique
4680+
depends on
4681+
the degree of uniformity of the gamut mapping colorspace
4682+
and the predictive accuracy of the deltaE function used.
4683+
4684+
However, when doing gamut mapping
4685+
(and we are really talking about gamut reduction, here),
4686+
changes in Hue are <em>particularly</em> objectionable;
4687+
changes in Chroma are more tolerable,
4688+
and
4689+
small changes in Lightness can also be acceptable
4690+
especially if the alternative is a larger Chroma reduction.
4691+
MINDE weights changes in each dimension equally,
4692+
and thus gives suboptimal results.
4693+
4694+
To improve on MINDE algorithms,
4695+
colors are mapped in a perceptually uniform, <em>polar</em> colorspace
4696+
by holding the hue constant,
4697+
and reducing the chroma until the color falls in gamut.
4698+
4699+
<div class="example" id="ex-gamutmap-p3-yellow-to-srgb">
4700+
In this example, Display P3 primary yellow
4701+
is being mapped to an sRGB display.
4702+
The gamut mapping colorspace is OKLCH.
46454703

4704+
<pre class="lang-css">
4705+
color(display-p3 1 1 0) is
4706+
color(srgb 1 1 -0.3463) which is
4707+
color(oklch 0.96476 <b>0.24503</b> 110.23).
4708+
</pre>
46464709

4710+
By progressively reducing the chroma component
4711+
until the resulting color falls inside the sRGB gamut
4712+
(has no components negative, or greater than one)
4713+
a gamut mapped color is obtained.
46474714

4715+
<pre class="lang-css">
4716+
<span class="swatch" style="--color: rgb(99.116% 99.733% 0.001%)"></span> color(oklch 0.96476 <b>0.21094</b> 110.23) which is
4717+
<span class="swatch" style="--color: rgb(99.116% 99.733% 0.001%)"></span> color(srgb 0.99116 0.99733 0.00001)
4718+
</pre>
4719+
<figure id="gamutmap-p3-yellow">
4720+
<figcaption>A constant-hue slice of OKLCH colorspace.
4721+
The vertical axis represents lightness,
4722+
the horizontal axis is chroma.
4723+
The color to be mapped,
4724+
shown as a yellow circle,
4725+
has the chroma reduced
4726+
while keeping hue and lightness constant.
4727+
The color therefore moves along the maroon line in the diagram,
4728+
towards the neutral axis on the left.
4729+
The gamut boundary of sRGB
4730+
is shown in green.
4731+
</figcaption>
4732+
<img src="./images/slice-ok-110.23.svg" alt="">
4733+
</figure>
4734+
</div>
4735+
4736+
A practical implementation will converge more quickly than a linear reduction;
4737+
either by binary search,
4738+
or by computing an analytical intersection
4739+
of the line of constant hue and lightness with the gamut boundary.
4740+
4741+
Also, this simple approach will give sub-optimal results
4742+
for certain colors, principally very light colors
4743+
like yellow and cyan,
4744+
if the upper edge of the gamut boundary is shallow,
4745+
or even slightly concave.
4746+
The line of constant lightness can skim just above the gamut boundary,
4747+
resulting in an excessively low chroma in those cases.
4748+
4749+
The choice of colorspace will affect the acceptability of the gamut mapped colors.
4750+
4751+
Note: Using the CIE LCH colorspace
4752+
and deltaE2000 distance metric,
4753+
is known to give suboptimal results
4754+
with <a href="#CIELCH-blue-hueshift">significant hue shifts</a>,
4755+
for colors in the hue range
4756+
270° to 330°.
4757+
Using OKLCH colorspace
4758+
and deltaEOK distance metric
4759+
<a href="OKLCH-blue-no-hueshift">avoids this issue</a>.
4760+
4761+
4762+
<h3 id="css-gamut-mapping">
4763+
CSS Gamut Mapping to an RGB destination
4764+
</h3>
4765+
4766+
This <dfn export>CSS gamut mapping algorithm</dfn>
4767+
applies to individual,
4768+
Standard Dynamic Range (SDR) CSS colors
4769+
which are out of gamut
4770+
of an RGB display
4771+
and thus require gamut mapping.
4772+
It implements a relative colorimetric intent,
4773+
and colors inside the destination gamut are unchanged.
4774+
4775+
Note: other situations,
4776+
in particular mapping to printer gamuts
4777+
where the maximum black level is significantly above zero,
4778+
will require different algorithms
4779+
which align the respective black and white points,
4780+
which will result in lightness changes
4781+
for very light and very dark colors
4782+
as chroma is reduced..
4783+
4784+
Note: this algorithm is for individual, distinct colors;
4785+
for color images,
4786+
where relationships between neighboring pixels are important
4787+
and detail and texture must be preserved,
4788+
a perceptual rendering intent is more appropriate
4789+
and in that case,
4790+
colors inside the destinatin gamut
4791+
may well be changed.
4792+
4793+
CSS gamut mapping occurs in the OKLCH colorspace,
4794+
and the color difference formula used is deltaEOK.
4795+
4796+
The skimming/concavity problem is substantialy improved
4797+
by a two-stage in-gamut check.
4798+
For the binary search implementation,
4799+
at each step,
4800+
the deltaEOK is computed between the current mapped color
4801+
and a clipped version of that color.
4802+
If the current color is outside the gamut boundary,
4803+
but the deltaEOK between it and the clipped version
4804+
is below a threshold for a <em>just noticeable difference</em> (JND),
4805+
the clipped version of the color is returned as the mapped result.
4806+
Effectively, this is doing a MINDE mapping at each stage,
4807+
but constrained so the hue and lightness changes
4808+
are very small,
4809+
and thus are not noticeable.
4810+
4811+
For the analytical implementation,
4812+
having found the exact intersection,
4813+
project outwards (higher chroma) along the line of constant lightness
4814+
until the deltaEOK between the projected point
4815+
and a clipped version of that point
4816+
exceeds one JND.
4817+
4818+
For the OKLCH colorspace,
4819+
one JND is is an OKLCH difference of 0.02.
4820+
4821+
4822+
<!-- color(display-p3 1 1 0)
4823+
color(oklch 0.96476 0.24503 110.23)
4824+
mapped
4825+
color(oklch 0.9651 0.20983 109.359)
4826+
-->
46484827

46494828
<!--
46504829
██████ ████████ ██ ██ ██ ████████ ████████ ██ ██ ██ ████████ ██████

css-color-4/images/slice-ok-110.23.svg

+19-2
Loading

0 commit comments

Comments
 (0)