d3 / d3-color

Color spaces! RGB, HSL, Cubehelix, CIELAB, and more.
https://d3js.org/d3-color
ISC License
398 stars 91 forks source link

Apply chromatic adaptation between D50 and D65 standard referents #43

Closed danburzo closed 6 years ago

danburzo commented 6 years ago

I've added an example implementation for the chromatic adaptation described in #42. I still need to fix the tests after I find a reliable source to check against.

(I've also added tap-spec to the dev dependencies, which makes the test output look nicer)

danburzo commented 6 years ago

Also, since we only use the XYZ space as an intermediary between RGB and Lab, to my mind we could pre-multiply the XYZ ↔ RGB and D50 ↔ D65 matrices and simplify the code, with the price of a more "obscure" matrix.

danburzo commented 6 years ago

And I think the matrices may need adjustment, as described in this issue: https://github.com/w3c/csswg-drafts/issues/2492 :/

danburzo commented 6 years ago

I've fixed the HCL/Lab tests, where I've seen a slight change in the numbers, with the one surprise on HCL('white'), which now offers a more straightforward value.

The tests can be verified in Matlab, with:

by (ab)using the interactive examples. I've tested a few numbers and they seem to match.

danburzo commented 6 years ago

In regards to the matrices: after clarifying that the ICC standard uses an inaccurate D50 white point, I went for the standard D50 definition and used the direct linear RGBXYZD50 matrices from Bruce Lindbloom's website.

danburzo commented 6 years ago

P.S. I'd like to check first the effect of the changes to the LAB / HCL color spaces to d3-interpolate and other color scale packages that depend on d3-color

mbostock commented 6 years ago

Nice work. It’d be good to have a set of reference values from another implementation—perhaps chromatist or something from R/Python would be a good choice. (If I’m not mistaken, chroma.js’s implementation is based on d3-color, so it’s not really an independent reference.)

danburzo commented 6 years ago

I'll try to get some reference values from one of them, or maybe http://www.colour-science.org/.

danburzo commented 6 years ago

I made an Observable notebook to compare the interpolation in three libraries: https://beta.observablehq.com/@danburzo/culori-scales-test

(Culori uses the same chromatic adaptation as this pull request).

There are some problems around interpolating in LCH with achromatic colors, related to the hue of white. In the previous implementation it was 158.2, and in the current one it's zero. (For what it's worth, white gives me L: 100, a: 0, b: 0 regardless of illuminant choice when using Matlab's rgb2lab function).

Chroma puts H: NaN if C = 0, allowing it to not interpolate on the H, whose value is I think of no importance for achromatic colors.

mbostock commented 6 years ago

Here’s a notebook I made to compare the behavior of Chromatist and d3-color:

https://beta.observablehq.com/@mbostock/chromatist-vs-d3-color

danburzo commented 6 years ago

Here are some datasets obtained with Colour Science python package (the files starting with pycolour-). I converted between sRGB and Lab with a chromatic adaptation between D50 and D65 using the Bradford matrix, using this script*

* I had to pass in more precise sRGB ↔️ XYZ matrices, since the original ones produced a bit of conversion error.

danburzo commented 6 years ago

For the current implementation of d3-color (with D65 illuminant) I get (script here):

maximum LAB -> sRGB error: 0.005395595768882623
maximum LAB -> sRGB (clamped) error: 1
maximum sRGB -> LAB error: 0.013983545325596024

And for the D50 implementation (script here):

maximum LAB -> sRGB error: 0.0025086315553252106
maximum LAB -> sRGB (clamped) error: 1
maximum sRGB -> LAB error: 0.02080372021691621

The "clamped" error compares the resulting RGBs when converted to [0, 255] and in both implementations it matches python colour with at most a variation of ±1 on a single R/G/B channel.

danburzo commented 6 years ago

(In regards to the tangential question on LCH interpolation, I've moved it to d3-interpolate)

danburzo commented 6 years ago

I've looked at the achromatic interpolation thing and the numbers don't look good for this PR.

With this test I checked the maximum Chroma I'll get from achromatic colors in the D65 and D50 implementation:

D50: 0.003457073518231546
D65: 0.00001795054880958058

So while it's true the non-zero Chroma results from artifacts in the conversion process, doing a plain D65 conversion lets us reasonably assume C = 0 for numbers very close to zero. However, by also applying a chromatic adaptation matrix, the Chroma is much more vulnerable to these artifacts, and I don't think it makes sense to assume C = 0 for C < 0.0035.

The last couple of days have made it clear getting accurate CIELab D50 values using simple computations is sisyphean...

mbostock commented 6 years ago

It would be “cheating”, but we could also consider checking for achromatic colors in RGB space (r = g = b) before converting to Lab, and set a = b = 0:

https://github.com/d3/d3-color/blob/3806fc588a275d688ff1741b41e70cc2250efcf5/src/lab.js#L14-L28

Then when converting from Lab to Hcl, if a = b = 0, set h = NaN instead of 0:

https://github.com/d3/d3-color/blob/3806fc588a275d688ff1741b41e70cc2250efcf5/src/lab.js#L80-L85

mbostock commented 6 years ago

In re. to previous comment, proposed #45.

svgeesus commented 6 years ago

Chroma puts H: NaN if C = 0, allowing it to not interpolate on the H, whose value is I think of no importance for achromatic colors.

Sounds like a case for atan2 rather than atan (avoid the divide by zero trap)

svgeesus commented 6 years ago

It would be “cheating”, but we could also consider checking for achromatic colors in RGB space (r = g = b) before converting to Lab, and set a = b = 0:

That does assume that the screen produces perfectly aligned ramps; which tends to only be true on a calibrated screen when adjustments to the video LUT have been made to ensure this.

danburzo commented 6 years ago

It would be “cheating”, but we could also consider checking for achromatic colors in RGB space (r = g = b) before converting to Lab, and set a = b = 0.

I think having a shortcut for achromatic RGB colors neatly expresses the intent, rather than computing the Lab and estimating a = b = 0 for very small values of a and b.

mbostock commented 6 years ago

Merged with #45 into #46.