Skip to content

Commit 63dcf2c

Browse files
committed
[css-color-4] add sample code for deltaE2000
1 parent a7073af commit 63dcf2c

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

css-color-4/Overview.bs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ spec:css-color-4; type:dfn; text:gamut
3838
"href": "https://www.loc.gov/preservation/digital/formats/fdd/fdd000022.shtml",
3939
"title": "TIFF Revision 6.0",
4040
"date": "3 June 1992"
41+
},
42+
"Sharma": {
43+
"href": "http://www2.ece.rochester.edu/~gsharma/ciede2000/",
44+
"title": "The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations",
45+
"authors": "G. Sharma, W. Wu, E. N. Dalal",
46+
"journal": "Color Research and Application, vol. 30. No. 1, pp. 21-30",
47+
"date": "February 2005"
4148
}
4249
}
4350
</pre>
@@ -4301,6 +4308,39 @@ path: conversions.js
43014308
highlight: js
43024309
</pre>
43034310

4311+
<h2 id="color-difference-code">Sample code for ΔE2000 color difference</h2>
4312+
4313+
<em>This section is not normative.</em>
4314+
4315+
<p>
4316+
The simplest color difference metric, ΔE76,
4317+
is simply the Euclidean distance in Lab colorspace.
4318+
While this is a good first approximation,
4319+
color-critical industries such as printing and fabric dyeing
4320+
soon developed improved formulae.
4321+
Currently, the most widely used formula
4322+
is ΔE2000.
4323+
It corrects a number of known asymmetries and non-linearities
4324+
compared to ΔE76.
4325+
Because the formula is complex,
4326+
and critically dependent on the sign
4327+
of various intermediate calculations,
4328+
implementations are often incorrect [[Sharma]].
4329+
</p>
4330+
4331+
<p>
4332+
The sample code below has been
4333+
<a href="https://colorjs.io/tests/delta.html#deltae-2000">validated</a>
4334+
to five significant figures
4335+
against the test suite of paired Lab values and expected ΔE2000
4336+
published by [[Sharma]] and is correct.
4337+
</p>
4338+
4339+
<pre class="include-code lang-javascript">
4340+
path: deltaE2000.js
4341+
highlight: js
4342+
</pre>
4343+
43044344
<h2 id="deprecated-system-colors" class="no-num">
43054345
Appendix A: Deprecated CSS System Colors</h2>
43064346

css-color-4/deltaE2000.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// deltaE2000 is a statistically significant improvement
2+
// over deltaE76 and deltaE94,
3+
// and is recommended by the CIE and Idealliance
4+
// especially for color differences less than 10 deltaE76
5+
// but is wicked complicated
6+
// and many implementations have small errors!
7+
8+
9+
function deltaE2000 (reference, sample) {
10+
11+
// Given a reference and a sample color,
12+
// both in CIE Lab,
13+
// calculate deltaE 2000.
14+
15+
// This implementation assumes the parametric
16+
// weighting factors kL, kC and kH
17+
// (for the influence of viewing conditions)
18+
// are all 1, as seems typical.
19+
20+
let [L1, a1, b1] = reference;
21+
let [L2, a2, b2] = sample;
22+
let C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
23+
let C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
24+
25+
let Cbar = (C1 + C2)/2; // mean Chroma
26+
27+
// calculate a-axis asymmetry factor from mean Chroma
28+
// this turns JND ellipses for near-neutral colors back into circles
29+
let C7 = Math.pow(Cbar, 7);
30+
const Gfactor = Math.pow(25, 7);
31+
let G = 0.5 * (1 - Math.sqrt(C7/(C7+Gfactor)));
32+
33+
// scale a axes by asymmetry factor
34+
// this by the way is why there is no Lab2000 colorspace
35+
let adash1 = (1 + G) * a1;
36+
let adash2 = (1 + G) * a2;
37+
38+
// calculate new Chroma from scaled a and original b axes
39+
let Cdash1 = Math.sqrt(adash1 ** 2 + b1 ** 2);
40+
let Cdash2 = Math.sqrt(adash2 ** 2 + b2 ** 2);
41+
42+
// calculate new hues, with zero hue for true neutrals
43+
// and in degrees, not radians
44+
const π = Math.PI;
45+
const r2d = 180 / π;
46+
const d2r = π / 180;
47+
let h1 = (adash1 === 0 && b1 === 0)? 0: Math.atan2(b1, adash1);
48+
let h2 = (adash2 === 0 && b2 === 0)? 0: Math.atan2(b2, adash2);
49+
50+
if (h1 < 0) {
51+
h1 += 2 * π;
52+
}
53+
if (h2 < 0) {
54+
h2 += 2 * π;
55+
}
56+
57+
h1 *= r2d;
58+
h2 *= r2d;
59+
60+
// Lightness and Chroma differences; sign matters
61+
let ΔL = L2 - L1;
62+
let ΔC = Cdash2 - Cdash1;
63+
64+
// Hue difference, taking care to get the sign correct
65+
let hdiff = h2 - h1;
66+
let hsum = h1 + h2;
67+
let habs = Math.abs(hdiff);
68+
let Δh;
69+
70+
if (Cdash1 == 0 && Cdash2 == 0) {
71+
Δh = 0;
72+
}
73+
else if (habs <= 180) {
74+
Δh = hdiff;
75+
}
76+
else if (hdiff > 180) {
77+
Δh = hdiff - 360;
78+
}
79+
else if (hdiff < -180) {
80+
Δh = hdiff + 360;
81+
}
82+
else {
83+
console.log("the unthinkable has happened");
84+
}
85+
86+
// weighted Hue difference, more for larger Chroma
87+
let ΔH = 2 * Math.sqrt(Cdash2 * Cdash1) * Math.sin(Δh * d2r / 2);
88+
89+
// calculate mean Lightness and Chroma
90+
let Ldash = (L1 + L2)/2;
91+
let Cdash = (Cdash1 + Cdash2)/2;
92+
let Cdash7 = Math.pow(Cdash, 7);
93+
94+
// Compensate for non-linearity in the blue region of Lab.
95+
// Four possibilities for hue weighting factor,
96+
// depending on the angles, to get the correct sign
97+
let hdash;
98+
if (Cdash1 == 0 && Cdash2 == 0) {
99+
hdash = hsum; // which should be zero
100+
}
101+
else if (habs <= 180) {
102+
hdash = hsum / 2;
103+
}
104+
else if (hsum < 360) {
105+
hdash = (hsum + 360) / 2;
106+
}
107+
else {
108+
hdash = (hsum - 360) / 2;
109+
}
110+
111+
// positional corrections to the lack of uniformity of CIELAB
112+
// These are all trying to make JND ellipsoids more like spheres
113+
114+
// SL Lightness crispening factor
115+
// a background with L=50 is assumed
116+
let lsq = (Ldash - 50) ** 2;
117+
let SL = 1 + ((0.015 * lsq) / Math.sqrt(20 + lsq));
118+
119+
// SC Chroma factor, similar to those in CMC and deltaE 94 formulae
120+
let SC = 1 + 0.045 * Cdash;
121+
122+
// Cross term T for blue non-linearity
123+
let T = 1;
124+
T -= (0.17 * Math.cos(( hdash - 30) * d2r));
125+
T += (0.24 * Math.cos( 2 * hdash * d2r));
126+
T += (0.32 * Math.cos(((3 * hdash) + 6) * d2r));
127+
T -= (0.20 * Math.cos(((4 * hdash) - 63) * d2r));
128+
129+
// SH Hue factor depends on Chroma,
130+
// as well as adjusted hue angle like deltaE94.
131+
let SH = 1 + 0.015 * Cdash * T;
132+
133+
// RT Hue rotation term compensates for rotation of JND ellipses
134+
// and Munsell constant hue lines
135+
// in the medium-high Chroma blue region
136+
// (Hue 225 to 315)
137+
let Δθ = 30 * Math.exp(-1 * (((hdash - 275)/25) ** 2));
138+
let RC = 2 * Math.sqrt(Cdash7/(Cdash7 + Gfactor));
139+
let RT = -1 * Math.sin(2 * Δθ * d2r) * RC;
140+
141+
// Finally calculate the deltaE, term by term as root sum of squares
142+
let dE = (ΔL / SL) ** 2;
143+
dE += (ΔC / SC) ** 2;
144+
dE += (ΔH / SH) ** 2;
145+
dE += RT * (ΔC / SC) * (ΔH / SH);
146+
return Math.sqrt(dE);
147+
// Yay!!!
148+
};

0 commit comments

Comments
 (0)