-
Notifications
You must be signed in to change notification settings - Fork 715
[css-color-4] Conversion precision for hue in LCH model creates false results #5309
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
Comments
In library I work on I've set precision such as converting *I've normalized D65 to have same precision as D50 |
Yes this is why some libraries represent such poorly-defined hues as NaN. |
See also #4928 |
So this requires three spec changes:
|
Hi Igor @snigo Here's the solution I'm using in SeeLab:
I'm using the Chroma value to determine if the hue should be clamped. And ALSO: I stay in D65 because I am not doing anything related to print, CMYK, ProPhoto, or comparing to D50, etc. I'm only working with RGB image, color, or display spaces that are D65 so that's all that's needed, which helps reduce noise/errors. In addition I've pre-calculated all the constants and rounded to 20 places which had a great effect on reducing the noise for grey sRGB colors, and the pre-calcs improve performance too.
You can see that I think I could get even lower noise in C if I recalc the sRGB -> XYZ matrix to a higher precision too. |
Here is the solution we are using in color.js: from: {
lab (Lab) {
// Convert to polar form
let [L, a, b] = Lab;
let hue;
const ε = 0.0005;
if (Math.abs(a) < ε && Math.abs(b) < ε) {
hue = NaN;
}
else {
hue = Math.atan2(b, a) * 180 / Math.PI;
}
return [
L, // L is still L
Math.sqrt(a ** 2 + b ** 2), // Chroma
angles.constrain(hue) // Hue, in degrees [0 to 360)
];
}
Hmm, we are doing an epsilon on a and b which gives a square area on the neutral axis. Using chroma gives a circular area and would be better. We return
Okay, but that means the Lab value you calculate and published Lab measurements, or the Lab result returned from a commercial spectroradiometer will be different. I wasn't sure about this either, but some expert guidance plus a desire to be compatible with ICC workflows plus my own experimenting on round-trip error produced by Bradford CAT between D65 to D50 to D65 again, convinced me this was not a significant source of error.
Yes, early rounding has often been a source of trouble. That is why the sRGB specification changed the transfer function during standardization, because the early testing was done at high precision while the published proposal was rounded to insufficient number of significant digits and the linear and curved portions of the transfer function didn't actually meet! (Not that this had any visible effect below 10 bits per component). Likewise the CIE publication 15 was revised to give the constants as rational numbers rather than a rounded-off floating point value. Color.js also uses those: ε: 216/24389, // 6^3/29^3
κ: 24389/27, // 29^3/3^3 and again for Jzazbz: b: 1.15,
g: 0.66,
n:2610 / (2 ** 14),
ninv: (2 ** 14) / 2610,
c1: 3424 / (2 ** 12),
c2: 2413 / (2 ** 7),
c3: 2392 / (2 ** 7),
p: 1.7 * 2523 / (2 ** 5),
pinv: (2 ** 5) / (1.7 * 2523),
Perhaps, although the precision limit is that the defining chromaticities for most colorspaces are only given to 2 or 3 significant figures. It isn't really noise (in the sense of measurement noise, although that can certainly be a factor especially for spectroradiometric measurements of dark colors unless care is taken to specify a longer integration time) but simply numerical instability as a and b tend to zero. Returning |
We have the same in color.js whites: {
D50: [0.96422, 1.00000, 0.82521],
D65: [0.95047, 1.00000, 1.08883],
}, The sample code in the specification is intended to be clear, simple and easy to read. Production code needs more error checking and handling of corner cases. I should really add the |
One thing I have noticed with sRGB to Lab/LCH conversion as it produces chaotic and very incorrect hue value in LCH color model. Converting any shade of gray to Lab/LCH will result in components
a
andb
very close to zero, but still not zero. It is absolutely fine for calculating chroma, as square root of those numbers will still result in number very close to zero, however calculating hue withMath.atan()
gives very high range of (falsy) values whenever there is any difference betweena
andb
, which always there!Rounding to 3 decimal places of
a
andb
solves problem for any shade of gray resulting in correct value of0
, however creates incorrect results for some highly desaturated colors likehsl(0 1% 1%)
orhsl(0 1% 99%)
, which in my opinion is better comparing to incorrect results for any shade of gray (including white and black)The text was updated successfully, but these errors were encountered: