Closed danburzo closed 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.
And I think the matrices may need adjustment, as described in this issue: https://github.com/w3c/csswg-drafts/issues/2492 :/
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:
rgb2lab([r g b], 'WhitePoint', 'd50')
where r
, g
, and b
are [0..1]
.rgb2lab([L a b], 'WhitePoint', 'd50')
by (ab)using the interactive examples. I've tested a few numbers and they seem to match.
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 RGB ↔ XYZD50 matrices from Bruce Lindbloom's website.
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
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.)
I'll try to get some reference values from one of them, or maybe http://www.colour-science.org/.
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.
Here’s a notebook I made to compare the behavior of Chromatist and d3-color:
https://beta.observablehq.com/@mbostock/chromatist-vs-d3-color
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.
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.
(In regards to the tangential question on LCH interpolation, I've moved it to d3-interpolate)
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...
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
In re. to previous comment, proposed #45.
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)
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.
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.
Merged with #45 into #46.
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)