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

Fix atypical achromatic response cases in ray trace #421

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coloraide/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(3, 3, 0, "final")
__version_info__ = Version(3, 3, 1, "final")
__version__ = __version_info__._get_canonical()
19 changes: 15 additions & 4 deletions coloraide/gamut/fit_raytrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,16 +214,27 @@ def fit(
light = mapcolor[l]
hue = mapcolor[h]
achroma[c] = 0
achromatic = achroma.convert(space)[:-1]

# Floating point math can cause some deviations between the max and min
# value in the achromatic RGB color. This is usually not an issue, but
# some perceptual spaces, such as CAM16 or HCT, may compensate for adapting
# luminance which may give an achromatic that is not quite achromatic,
# causing a more sizeable delta between the max and min value in the
# achromatic RGB color. To compensate for such deviations, take the
# average value of the RGB components and use that as the achromatic point.
# When dealing with simple floating point deviations, little to no change
# is observed, but for spaces like CAM16 or HCT, this can provide more
# reasonable gamut mapping.
achromatic = [sum(achroma.convert(space)[:-1]) / 3] * 3

# Return white or black if the achromatic version is not within the RGB cube.
# HDR colors currently use the RGB maximum lightness. We do not currently
# clip HDR colors to SDR white, but that could be done if required.
mn, mx = alg.minmax(achromatic)
bmx = bmax[0]
if mx >= bmx:
point = achromatic[0]
if point >= bmx:
color.update(space, bmax, mapcolor[-1])
elif mn <= 0:
elif point <= 0:
color.update(space, [0.0, 0.0, 0.0], mapcolor[-1])
else:
# Create a ray from our current color to the color with zero chroma.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 3.3.1

- **FIX**: Ray trace gamut mapping algorithm will better handle perceptual spaces like CAM16 and HCT which have
atypical achromatic responses. This prevents unexpected cutoff close to white.
- **FIX**: Fix some documentation examples regarding gamut mapping in HCT.

## 3.3

- **NEW**: Extend the `Cylindrical` mixin class to expose `radial_name()` and `radial_index()` on the color space to
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/demos/3d_models.html
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ <h1>ColorAide Color Space Models</h1>
let colorSpaces = null
let colorGamuts = null
let lastModel = null
let package = 'coloraide-3.3-py3-none-any.whl'
let package = 'coloraide-3.3.1-py3-none-any.whl'
const defaultSpace = 'lab'
const defaultGamut = 'srgb'
const exceptions = new Set(['hwb', 'ryb', 'ryb-biased'])
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/demos/colorpicker.html
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ <h1>ColorAide Color Picker</h1>
let pyodide = null
let webspace = ''
let initial = 'oklab(0.69 0.13 -0.1 / 0.85)'
let package = 'coloraide-3.3-py3-none-any.whl'
let package = 'coloraide-3.3.1-py3-none-any.whl'

const base = `${window.location.origin}/${window.location.pathname.split('/')[1]}/playground/`
package = base + package
Expand Down
6 changes: 5 additions & 1 deletion docs/src/markdown/distance.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,14 @@ This approach was specifically added to help produce tonal palettes, but with th
approach to chroma reduction in any perceptual space](./gamut.md#ray-tracing-chroma-reduction-in-any-perceptual-space),
users can defer to the ray tracing approach which does not require a special ∆E method and it performs much faster.

On occasions, MINDE approach can be slightly more accurate very close to white due to the way ray trace handles HCT's
atypical achromatic response, but differences should be imperceptible to the eye at such lightness levels making the
the improved performance of the ray trace approach much more desirable.

```py play
c = Color('hct', [325, 24, 50])
tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'raytrace', 'hct': 0.0}) for tone in tones])
Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'raytrace', 'pspace': 'hct'}) for tone in tones])
```
///

Expand Down
6 changes: 5 additions & 1 deletion docs/src/markdown/gamut.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,14 @@ approach to chroma reduction in any perceptual space](#ray-tracing-chroma-reduct
recommended that users apply that approach as it performs a tight chroma reduction much quicker, and it doesn't require
a special ∆E method.

On occasions, MINDE approach can be slightly more accurate very close to white due to the way ray trace handles HCT's
atypical achromatic response, but differences should be imperceptible to the eye at such lightness levels making the
the improved performance of the ray trace approach much more desirable.

```py play
c = Color('hct', [325, 24, 50])
tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]
Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'raytrace', 'hct': 0.0}) for tone in tones])
Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit={'method': 'raytrace', 'pspace': 'hct'}) for tone in tones])
```
///

Expand Down
2 changes: 1 addition & 1 deletion docs/src/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ extra_css:
- assets/coloraide-extras/extra.css
extra_javascript:
- https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js
- playground-config-3905dc29.js
- playground-config-91c1cf40.js
- https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js
- assets/coloraide-extras/extra-notebook.js

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var colorNotebook = {
"playgroundWheels": ['Pygments-2.16.1-py3-none-any.whl', 'coloraide-3.3-py3-none-any.whl'],
"notebookWheels": ['pyyaml', 'Markdown-3.5.1-py3-none-any.whl', 'pymdown_extensions-10.5-py3-none-any.whl', 'Pygments-2.16.1-py3-none-any.whl', 'coloraide-3.3-py3-none-any.whl'],
"playgroundWheels": ['Pygments-2.16.1-py3-none-any.whl', 'coloraide-3.3.1-py3-none-any.whl'],
"notebookWheels": ['pyyaml', 'Markdown-3.5.1-py3-none-any.whl', 'pymdown_extensions-10.5-py3-none-any.whl', 'Pygments-2.16.1-py3-none-any.whl', 'coloraide-3.3.1-py3-none-any.whl'],
"defaultPlayground": "import coloraide\ncoloraide.__version__\nColor('red')"
}
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ extra_css:
- assets/coloraide-extras/extra-e1cd7ecf37.css
extra_javascript:
- https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js
- playground-config-3905dc29.js
- playground-config-91c1cf40.js
- https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js
- assets/coloraide-extras/extra-notebook-Cs6O_Czb.js

Expand Down
2 changes: 1 addition & 1 deletion tests/test_acescc.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class TestACESccSerialize(util.ColorAssertsPyTest):
(
'color(--acescc 1.5 0.3 0)',
{'color': True, 'fit': 'lch-raytrace'},
'color(--acescc 1.468 0.3 -0.00071)'
'color(--acescc 1.468 0.3 -0.00064)'
),
('color(--acescc 1.5 0.2 0)', {'fit': False}, 'color(--acescc 1.5 0.2 0)')
]
Expand Down
30 changes: 13 additions & 17 deletions tools/raytrace_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,32 +217,33 @@ def simulate_raytrace_gamut_mapping(args):

orig = color.space()
mapcolor = color.convert(pspace, norm=False) if orig != pspace else color.clone().normalize(nans=False)
achroma = mapcolor.clone()
first = mapcolor.clone()
if is_lab:
l, a, b = mapcolor._space.indexes() # type: ignore[attr-defined]
chroma, hue = alg.rect_to_polar(mapcolor[a], mapcolor[b])
mapcolor[a] = 0
mapcolor[b] = 0
achroma[a] = 0
achroma[b] = 0
else:
l, c, h = mapcolor._space.indexes() # type: ignore[attr-defined]
chroma = mapcolor[c]
hue = mapcolor[h]
mapcolor[c] = 0
achroma = mapcolor.clone().convert(space, in_place=True)[:-1]
achroma[c] = 0
achromatic = [sum(achroma.clone().convert(space, in_place=True)[:-1]) / 3] * 3

# Return white or black if the achromatic version is not within the RGB cube.
mn, mx = alg.minmax(achroma)
bmx = bmax[0]
if mx >= bmx:
point = achromatic[0]
if point >= bmx:
color.update(space, bmax, mapcolor[-1])
points.append(first.convert(space)[:-1])
points.append(color.convert(space)[:-1])
points.append(achroma)
elif mn <= 0:
points.append(achromatic)
elif point <= 0:
color.update(space, [0.0, 0.0, 0.0], mapcolor[-1])
points.append(first.convert(space)[:-1])
points.append(color.convert(space)[:-1])
points.append(achroma)
points.append(achromatic)
else:
light = mapcolor[l]
if is_lab:
Expand Down Expand Up @@ -270,11 +271,11 @@ def simulate_raytrace_gamut_mapping(args):
gamutcolor[l] = light
gamutcolor[h] = hue
gamutcolor.convert(space, in_place=True)
intersection = raytrace_box(achroma, gamutcolor[:-1], bmax=bmax)
intersection = raytrace_box(achromatic, gamutcolor[:-1], bmax=bmax)
if intersection:
points.append(gamutcolor[:-1])
points.append(intersection)
points.append(achroma)
points.append(achromatic)
gamutcolor[:-1] = intersection
continue
break # pragma: no cover
Expand Down Expand Up @@ -318,15 +319,10 @@ def simulate_raytrace_gamut_mapping(args):
fig.add_traces(data)

if args.gamut_interp:
if is_lab:
mapcolor[a] = 0
mapcolor[b] = 0
else:
mapcolor[c] = 0
plot_interpolation(
fig,
space,
first.to_string(fit=False) + ';' + mapcolor.to_string(fit=False),
first.to_string(fit=False) + ';' + achroma.to_string(fit=False),
pspace,
'linear',
'shorter',
Expand Down
Loading