Skip to content

Small numerical inconsistency in CIEDE2000 implementation (line 145) causes ΔE00 error of 4e-5 #258

Open
@michel-leonard

Description

@michel-leonard

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions