1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287 | /++ Colour conversion and comparison
+ Authors: Mio Iwakura, mio.iwakura@gmail.com +/
module treecount.colours;
import core.stdc.stdio;
import std.math, std.range, std.stdio;
import gl3n.linalg;
import treecount.coords, treecount.globals, treecount.math;
/++ The CIEDE2000 algorithm for computing the difference between two colours
+
+ Adjusting the weighting factors is not recommended unless you know what you
+ are doing. For our purposes, the defaults of 1.0 are currently used.
+
+ If you compile with the debug tag `deltaE_00`, intermediate values will be
+ printed to standard out for every invocation of the function for debugging
+ purposes.
+ Standards: Conforms to CIEDE2000 specification
+ Returns: The difference between the two input colours as defined by the
+ CIEDE2000 algorithm. Small values mean the colours are close,
+ big values mean they are far away. The range of values seems to
+ be around [0, 100] but I'm not an expert on this algorithm and did
+ not find clear documentation regarding the range of values produced
+ by the algorithm.
+ Params:
+ colour1 = The first input colour
+ colour2 = The second input colour
+ kL = A weighting factor for lightness
+ kC = A weighting factor for chroma
+ kH = A weighting factor for hue
+ Bugs: The unit tests all pass with phobos's default std.math.approxEqual,
+ but I've noticed small discrepancies, a few of which are documented
+ in code comments. If you have any insights or suggestions, please
+ don't hesitate to submit an issue and/or open a pull request!
+ Github issues is the prefered platform for discussion, but if
+ nothing else, you can contact the author of this module by email.
+ See_Also:
+ $(LINK http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf) +/
float deltaE_00(in Lab colour1, in Lab colour2, in float kL = 1.0, in float kC = 1.0,
in float kH = 1.0) pure nothrow @nogc @trusted
{
/* The number comments correspond to sections in
* "The CIEDE2000 Color-Difference Formula: Implementation Notes,
* Supplementary Test Data, and Mathematical Observations" */
/* 1. */
//(2, 3)
immutable avgC = (hypot(colour1.a, colour1.b) + hypot(colour2.a, colour2.b)) / 2;
//(4)
immutable G = 0.5 * (1 - sqrt(avgC ^^ 7 / (avgC ^^ 7 + 25L ^^ 7)));
auto a(float colourA) pure nothrow @nogc @safe
{
immutable result = (1 + G) * colourA;
return result;
}
//(5)
immutable a1 = a(colour1.a);
immutable a2 = a(colour2.a);
//BUG: pair 22 a2 -> 4.9449 instead of 4.9450
// 4.94492, these bugs aren't just bankers rounding!
//NOTE: equations 6-7 are Lab -> LCh
//(6)
immutable C1 = hypot(a1, colour1.b);
immutable C2 = hypot(a2, colour2.b);
//BUG: pair 21 C2 -> 4.7955 instead of 4.7954
//BUG: pair 22 C2 -> 4.9449 instead of 4.9450
auto h(float a, float b) pure nothrow @nogc @safe
{
//article does not mention floating point quirks,
//but I assume accounting for it is necessary
immutable result = a.approxEqual(0) && b.approxEqual(0) ? 0 : atan2(b, a).toDegrees();
return result;
}
//(7)
immutable h1 = h(a1, colour1.b);
//BUG: various values are slightly off
immutable h2 = h(a2, colour2.b);
/* 2. */
//(8)
immutable deltaL = colour2.L - colour1.L;
//(9)
immutable deltaC = C2 - C1;
//(10)
immutable C1C2 = C1 * C2;
float deltah = h2 - h1;
if (C1C2.approxEqual(0.0))
deltah = 0.0;
else if (deltah > 180.0)
deltah -= 360.0;
else if (deltah < -180.0)
deltah += 360.0;
//(11)
immutable deltaH = 2 * sqrt(C1C2) * sin((deltah / 2).toRadians());
/* 3. */
//(12)
immutable avgL = (colour1.L + colour2.L) / 2;
//(13)
immutable avgNewC = (C1 + C2) / 2;
//(14)
float avgh;
{
immutable h1ph2 = h1 + h2;
if (C1C2.approxEqual(0.0))
avgh = h1ph2;
else if (abs(h1 - h2) <= 180.0)
avgh = h1ph2 / 2.0;
else if (h1ph2 < 360.0)
avgh = (h1ph2 + 360.0) / 2.0;
else
avgh = (h1ph2 - 360.0) / 2.0;
}
//(15)
immutable T = 1 - 0.17 * cos((avgh - 30).toRadians()) + 0.24 * cos((2 * avgh).toRadians()) + 0.32 * cos(
(3 * avgh + 6).toRadians()) - 0.20 * cos((4 * avgh - 63).toRadians());
//(16)
immutable deltaTheta = 30 * exp(-1 * ((avgh - 275) / 25) ^^ 2);
//(17)
immutable RC = 2 * sqrt(avgNewC ^^ 7 / (avgNewC ^^ 7 + 25L ^^ 7));
//(18)
immutable SL = 1 + (0.015 * (avgL - 50) ^^ 2) / (sqrt(20 + (avgL - 50) ^^ 2));
//(19)
immutable SC = 1 + 0.045 * avgNewC;
//(20)
immutable SH = 1 + 0.015 * avgNewC * T;
//(21)
immutable RT = -1 * sin((2 * deltaTheta).toRadians()) * RC;
//(22)
immutable dCdkCSC = deltaC / (kC * SC);
immutable dHdkHSH = deltaH / (kH * SH);
debug (deltaE_00)
{
printf(
"%.4f\t%.4f\t%.4f\t%.4Lf\t%.4Lf\t%.4lf\t%.4lf\t" ~ "%.4Lf\t%.4lf\t%.4Lf\t%.4Lf\t%.4Lf\t%.4Lf\t%.4Lf\n",
colour1.L, colour1.a, colour1.b, a1, C1, h1, avgh, G, T, SL, SC,
SH, RT, sqrt((deltaL / (kL * SL)) ^^ 2 + dCdkCSC ^^ 2 + dHdkHSH ^^ 2 + RT * dCdkCSC * dHdkHSH));
printf("%.4f\t%.4f\t%.4f\t%.4Lf\t%.4Lf\t%.4lf\n", colour2.L, colour2.a,
colour2.b, a2, C2, h2);
}
immutable result = sqrt((deltaL / (kL * SL)) ^^ 2 + dCdkCSC ^^ 2 + dHdkHSH ^^ 2 + RT * dCdkCSC * dHdkHSH);
return result;
}
///
unittest
{
immutable Lab[34] inputA = [{L:
50.0000, a : 2.6772, b : -79.7751}, {L:
50.0000, a : 3.1571, b : -77.2803}, {L:
50.0000, a : 2.8361, b : -74.0200}, {L:
50.0000, a : -1.3802, b : -84.2814}, {L:
50.0000, a : -1.1848, b : -84.8006}, {L:
50.0000, a : -0.9009, b : -85.5211}, {L:
50.0000, a : 0.0000, b : 0.0000}, {L:
50.0000, a : -1.0000, b : 2.0000}, {L:
50.0000, a : 2.4900, b : -0.0010}, {L:
50.0000, a : 2.4900, b : -0.0010}, {L:
50.0000, a : 2.4900, b : -0.0010}, {L:
50.0000, a : 2.4900, b : -0.0010}, {L:
50.0000, a : -0.0010, b : 2.4900}, {L:
50.0000, a : -0.0010, b : 2.4900}, {L:
50.0000, a : -0.0010, b : 2.4900}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
50.0000, a : 2.5000, b : 0.0000}, {L:
60.2574, a : -34.0099, b : 36.2677}, {L:
63.0109, a : -31.0961, b : -5.8663}, {L:
61.2901, a : 3.7196, b : -5.3901}, {L:
35.0831, a : -44.1164, b : 3.7933}, {L:
22.7233, a : 20.0904, b : -46.6940}, {L:
36.4612, a : 47.8580, b : 18.3852}, {L:
90.8027, a : -2.0831, b : 1.4410}, {L:
90.9257, a : -0.5406, b : -0.9208}, {L:
6.7747, a : -0.2908, b : -2.4247}, {L:
2.0776, a : 0.0795, b : -1.1350}];
immutable Lab[34] inputB = [{L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : 0.0000, b : -82.7485}, {L:
50.0000, a : -1.0000, b : 2.0000}, {L:
50.0000, a : 0.0000, b : 0.0000}, {L:
50.0000, a : -2.4900, b : 0.0009}, {L:
50.0000, a : -2.4900, b : 0.0010}, {L:
50.0000, a : -2.4900, b : 0.0011}, {L:
50.0000, a : -2.4900, b : 0.0012}, {L:
50.0000, a : 0.0009, b : -2.4900}, {L:
50.0000, a : 0.0010, b : -2.4900}, {L:
50.0000, a : 0.0011, b : -2.4900}, {L:
50.0000, a : 0.0000, b : -2.5000}, {L:
73.0000, a : 25.0000, b : -18.0000}, {L:
61.0000, a : -5.0000, b : 29.0000}, {L:
56.0000, a : -27.0000, b : -3.0000}, {L:
58.0000, a : 24.0000, b : 15.0000}, {L:
50.0000, a : 3.1736, b : 0.5854}, {L:
50.0000, a : 3.2972, b : 0.0000}, {L:
50.0000, a : 1.8634, b : 0.5757}, {L:
50.0000, a : 3.2592, b : 0.3350}, {L:
60.4626, a : -34.1751, b : 39.4387}, {L:
62.8187, a : -29.7946, b : -4.0864}, {L:
61.4292, a : 2.2480, b : -4.9620}, {L:
35.0232, a : -40.0716, b : 1.5901}, {L:
23.0331, a : 14.9730, b : -42.5619}, {L:
36.2715, a : 50.5065, b : 21.2231}, {L:
91.1528, a : -1.6435, b : 0.0447}, {L:
88.6381, a : -0.8985, b : -0.7239}, {L:
5.8714, a : -0.0985, b : -2.2286}, {L:
0.9033, a : -0.0636, b : -0.5514}];
immutable float[34] expectedResults = [
2.0425, 2.8615, 3.4412, 1.0000, 1.0000, 1.0000, 2.3669, 2.3669, 7.1792,
7.1792, 7.2195, 7.2195, 4.8045, 4.8045, 4.7461, 4.3065, 27.1492,
22.8977, 31.9030, 19.4535, 1.0000, 1.0000, 1.0000, 1.0000, 1.2644,
1.2630, 1.8731, 1.8645, 2.0373, 1.4146, 1.4441, 1.5381, 0.6377, 0.9082
];
float[] actualResultA;
float[] actualResultB;
debug (deltaE_00)
{
foreach (ref a, ref b; lockstep(inputA[], inputB[]))
{
actualResultA ~= deltaE_00(a, b);
}
}
else
{
writeln("[deltaE_00 test]");
foreach (ref a, ref b; lockstep(inputA[], inputB[]))
{
actualResultA ~= deltaE_00(a, b);
actualResultB ~= deltaE_00(b, a);
}
foreach (size_t i, ref a, ref b, ref e; lockstep(actualResultA[],
actualResultB[], expectedResults[]))
{
writefln("(%s) ΔE₀₀¹²: %.4f\tΔE₀₀²¹: %.4f\tΔE₀₀: %.4f",
i + 1, a, b, e);
assert(a.approxEqual(e) && b.approxEqual(e));
}
}
}
/++ Calculates the average colour of every pixel of the sample
+ in Lab space
+ Returns: the average colour
+ Params:
+ rect = the sample to average +/
auto calculateAvgColour(in vec4 rect)
in
{
assert(!imageLab.empty);
}
body
{
/** This naive implementation suffers from floating-point error.
* I recommend using pairwise summation because it could be sped up with
* SIMD, which is probably the best option to get better speed and accuracy.
* OpenCL would be too much overhead, because our samples are small, although
* task parallelism could be viable if samples are averaged in batch. */
immutable sample = rect.asPixelCoords();
auto average = Lab(0.0f, 0.0f, 0.0f);
for (int i = sample.y; i < sample.y + sample.q; ++i)
for (int j = sample.x; j < sample.x + sample.p; ++j)
{
immutable idx = j + i * originalImageDimensions.x;
average.L += imageLab[idx].L;
average.a += imageLab[idx].a;
average.b += imageLab[idx].b;
}
average.L /= sample.p * sample.q;
average.a /= sample.p * sample.q;
average.b /= sample.p * sample.q;
return average;
}
///A colour in the CIE L*a*b* (CIELAB) colour space
struct Lab
{
float L; ///L* component representing lightness
float a; ///a* component representing position between red/magenta and green
float b; ///b* component representing position between yellow and blue
private float _p; //padding
/* The padding is because OpenCL uses 32bpp images, and we want to be able
* to cast the output of our rgb to lab kernel to Lab[] */
}
|