Description
Hi,
I noticed a small but consistent numerical difference between the ΔE00 results from culori
and a reference implementation of the CIEDE2000 formula. After analyzing the source, it seems the discrepancy originates from the logic around hue angle interpolation, particularly here difference.js#L145
The current implementation does not fully handle the case where hue angles are on opposite sides of the color wheel, leading to a slight discontinuity. As a result, the maximum ΔE00 difference can reach ~0.000045 in some edge cases.
Proposed Fix
Adjust the hue difference and mean hue computation to properly wrap around 2π. A corrected version — verified to match a reliable reference with error < 1e-13 — is available here: https://github.com/michel-leonard/ciede2000-color-matching
Or directly in this adapted function:
Click to expand
const deltaE00_test = (lStd, aStd, bStd, lSmp, aSmp, bSmp) => {
const Kl = 1,
Kc = 1,
Kh = 1
let cStd = Math.sqrt(aStd * aStd + bStd * bStd);
let cSmp = Math.sqrt(aSmp * aSmp + bSmp * bSmp);
let cAvg = (cStd + cSmp) / 2;
let G =
0.5 *
(1 -
Math.sqrt(
Math.pow(cAvg, 7) / (Math.pow(cAvg, 7) + Math.pow(25, 7))
));
let apStd = aStd * (1 + G);
let apSmp = aSmp * (1 + G);
let cpStd = Math.sqrt(apStd * apStd + bStd * bStd);
let cpSmp = Math.sqrt(apSmp * apSmp + bSmp * bSmp);
let hpStd =
Math.abs(apStd) + Math.abs(bStd) === 0 ?
0 :
Math.atan2(bStd, apStd);
hpStd += (hpStd < 0) * 2 * Math.PI;
let hpSmp =
Math.abs(apSmp) + Math.abs(bSmp) === 0 ?
0 :
Math.atan2(bSmp, apSmp);
hpSmp += (hpSmp < 0) * 2 * Math.PI;
let dL = lSmp - lStd;
let dC = cpSmp - cpStd;
let dhp = cpStd * cpSmp === 0 ? 0 : hpSmp - hpStd;
let hp = (hpStd + hpSmp) / 2;
if (Math.PI < Math.abs(hpStd - hpSmp)) {
if (dhp < 0) dhp -= 2 * Math.PI;
else dhp += 2 * Math.PI;
hp += Math.PI;
}
let dH = 2 * Math.sqrt(cpStd * cpSmp) * Math.sin(dhp / 2);
let Lp = (lStd + lSmp) / 2;
let Cp = (cpStd + cpSmp) / 2;
let Lpm50 = Math.pow(Lp - 50, 2);
let T =
1 -
0.17 * Math.cos(hp - Math.PI / 6) +
0.24 * Math.cos(2 * hp) +
0.32 * Math.cos(3 * hp + Math.PI / 30) -
0.2 * Math.cos(4 * hp - (63 * Math.PI) / 180);
let Sl = 1 + (0.015 * Lpm50) / Math.sqrt(20 + Lpm50);
let Sc = 1 + 0.045 * Cp;
let Sh = 1 + 0.015 * Cp * T;
let deltaTheta =
((30 * Math.PI) / 180) *
Math.exp(-1 * Math.pow(((180 / Math.PI) * hp - 275) / 25, 2));
let Rc =
2 *
Math.sqrt(Math.pow(Cp, 7) / (Math.pow(Cp, 7) + Math.pow(25, 7)));
let Rt = -1 * Math.sin(2 * deltaTheta) * Rc;
return Math.sqrt(
Math.pow(dL / (Kl * Sl), 2) +
Math.pow(dC / (Kc * Sc), 2) +
Math.pow(dH / (Kh * Sh), 2) +
(((Rt * dC) / (Kc * Sc)) * dH) / (Kh * Sh)
);
};
Summary
Reference | Test | Max ΔE00 Error |
---|---|---|
culori | ref impl | ~0.0000457 |
fixed | ref impl | ~1.1e-13 |
Thanks for your great work on culori
! I hope this helps make the CIEDE2000 result even more precise.
Best,
Michel