OLD | NEW |
---|---|
(Empty) | |
1 #include <cmath> | |
2 | |
3 #include "SkBitmap.h" | |
4 #include "skpdiff_util.h" | |
5 #include "SkPMetric.h" | |
6 | |
7 struct RGB { | |
8 float r, g, b; | |
9 }; | |
10 | |
11 struct LAB { | |
12 float l, a, b; | |
13 }; | |
14 | |
15 template<class T> | |
16 struct Image2D { | |
17 int width; | |
18 int height; | |
19 T* image; | |
20 | |
21 Image2D(int w, int h) | |
22 : width(w), | |
23 height(h) { | |
24 SkASSERT(w > 0); | |
25 SkASSERT(h > 0); | |
26 image = SkNEW_ARRAY(T, w * h); | |
27 } | |
28 | |
29 ~Image2D() { | |
30 SkDELETE_ARRAY(image); | |
31 } | |
32 | |
33 void readPixel(int x, int y, T* pixel) const { | |
34 SkASSERT(x >= 0); | |
35 SkASSERT(y >= 0); | |
36 SkASSERT(x < width); | |
37 SkASSERT(y < height); | |
38 *pixel = image[y * width + x]; | |
39 } | |
40 | |
41 void writePixel(int x, int y, const T& pixel) { | |
42 SkASSERT(x >= 0); | |
43 SkASSERT(y >= 0); | |
44 SkASSERT(x < width); | |
45 SkASSERT(y < height); | |
46 image[y * width + x] = pixel; | |
47 } | |
48 }; | |
49 | |
50 typedef Image2D<float> ImageL; | |
51 typedef Image2D<RGB> ImageRGB; | |
52 typedef Image2D<LAB> ImageLAB; | |
53 | |
54 template<class T> | |
bsalomon
2013/06/27 18:31:15
It seems like the template param should be restric
Zach Reizner
2013/06/27 18:48:49
I like the idea of moving the template params.
Th
| |
55 struct Image3D | |
56 { | |
57 int slices; | |
58 T** image; | |
59 | |
60 Image3D(int w, int h, int s) | |
61 : slices(s) { | |
62 SkASSERT(s > 0); | |
63 image = SkNEW_ARRAY(T*, s); | |
64 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) { | |
65 image[sliceIndex] = SkNEW_ARGS(T, (w, h)); | |
66 } | |
67 } | |
68 | |
69 ~Image3D() { | |
70 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) { | |
71 SkDELETE(image[sliceIndex]); | |
72 } | |
73 SkDELETE_ARRAY(image); | |
74 } | |
75 | |
76 T* getLayer(int z) const { | |
77 SkASSERT(z >= 0); | |
78 SkASSERT(z < slices); | |
79 return image[z]; | |
80 } | |
81 }; | |
82 | |
83 typedef Image3D<ImageL> ImageL3D; | |
84 | |
85 | |
86 #define MAT_ROW_MULT(rc,gc,bc) r*rc + g*gc + b*bc | |
87 | |
88 | |
89 void adobergb_to_cielab(float r, float g, float b, LAB* lab) { | |
bsalomon
2013/06/27 18:31:15
Does this mean the input is in Adobe RGB?
http://e
Zach Reizner
2013/06/27 18:48:49
The input is assumed to be Adobe RGB. Adobe RGB ha
| |
90 // Conversion of Adobe RGB to XYZ taken from from "Adobe RGB (1998) ColorIma ge Encoding" | |
91 // URL:http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf | |
92 // Section: 4.3.5.3 | |
93 // See Also: http://en.wikipedia.org/wiki/Adobe_rgb | |
94 float x = MAT_ROW_MULT(0.57667f, 0.18556f, 0.18823f); | |
95 float y = MAT_ROW_MULT(0.29734f, 0.62736f, 0.07529f); | |
96 float z = MAT_ROW_MULT(0.02703f, 0.07069f, 0.99134f); | |
97 | |
98 // The following is the white point in XYZ, so it's simply the row wise addi tion of the above | |
99 // matrix. | |
100 const float xw = 0.5767f + 0.185556f + 0.188212f; | |
101 const float yw = 0.297361f + 0.627355f + 0.0752847f; | |
102 const float zw = 0.0270328f + 0.0706879f + 0.991248f; | |
103 | |
104 // This is the XYZ color point relative to the white point | |
105 float f[3] = { x / xw, y / yw, z / zw }; | |
106 | |
107 // Conversion from XYZ to LAB taken from | |
108 // http://en.wikipedia.org/wiki/CIELAB#Forward_transformation | |
109 for (int i = 0; i < 3; i++) { | |
110 if (f[i] >= 0.008856f) { | |
111 f[i] = powf(f[i], 1.0f / 3.0f); | |
112 } else { | |
113 f[i] = 7.787f * f[i] + 4.0f / 29.0f; | |
114 } | |
115 } | |
116 lab->l = 116.0f * f[1] - 16.0f; | |
117 lab->a = 500.0f * (f[0] - f[1]); | |
118 lab->b = 200.0f * (f[1] - f[2]); | |
119 } | |
120 | |
121 /// Converts a 8888 bitmap to LAB color space and puts it into the output | |
122 static void bitmap_to_cielab(const SkBitmap* bitmap, ImageLAB* outImageLAB) { | |
123 SkASSERT(bitmap->config() == SkBitmap::kARGB_8888_Config); | |
124 | |
125 int width = bitmap->width(); | |
126 int height = bitmap->height(); | |
127 SkASSERT(outImageLAB->width == width); | |
128 SkASSERT(outImageLAB->height == height); | |
129 | |
130 bitmap->lockPixels(); | |
131 RGB rgb; | |
132 LAB lab; | |
133 for (int y = 0; y < height; y++) { | |
134 unsigned char* row = (unsigned char*)bitmap->getAddr(0, y); | |
135 for (int x = 0; x < width; x++) { | |
136 // Perform gamma correction which is assumed to be 2.2 | |
137 rgb.r = powf(row[x * 4 + 2] / 255.0f, 2.2f); | |
138 rgb.g = powf(row[x * 4 + 1] / 255.0f, 2.2f); | |
139 rgb.b = powf(row[x * 4 + 0] / 255.0f, 2.2f); | |
140 adobergb_to_cielab(rgb.r, rgb.g, rgb.b, &lab); | |
141 outImageLAB->writePixel(x, y, lab); | |
142 } | |
143 } | |
144 bitmap->unlockPixels(); | |
145 } | |
146 | |
147 // From Barten SPIE 1989 | |
148 static float contrast_sensitivity(float cyclesPerDegree, float luminance) { | |
149 float a = 440.0f * powf(1.0f + 0.7f / luminance, -0.2f); | |
150 float b = 0.3f * powf(1 + 100.0 / luminance, 0.15f); | |
151 return a * | |
152 cyclesPerDegree * | |
153 expf(-b * cyclesPerDegree) * | |
154 sqrtf(1.0f + 0.06f * expf(b * cyclesPerDegree)); | |
155 } | |
156 | |
157 // From Daly 1993 | |
158 static float visual_mask(float contrast) { | |
159 float x = powf(392.498f * contrast, 0.7f); | |
160 x = powf(0.0153f * x, 4.0f); | |
161 return powf(1.0f + x, 0.25f); | |
162 } | |
163 | |
164 // From Ward Larson Siggraph 1997 | |
165 static float threshold_vs_intensity(float adaptationLuminance) { | |
166 float logLum = log10f(adaptationLuminance); | |
167 float x; | |
168 if (logLum < -3.94f) { | |
169 x = -2.86f; | |
170 } else if (logLum < -1.44f) { | |
171 x = powf(0.405f * logLum + 1.6f, 2.18) - 2.86f; | |
172 } else if (logLum < -0.0184f) { | |
173 x = logLum - 0.395f; | |
174 } else if (logLum < 1.9f) { | |
175 x = powf(0.249f * logLum + 0.65f, 2.7f) - 0.72f; | |
176 } else { | |
177 x = logLum - 1.255f; | |
178 } | |
179 return powf(10.0f, x); | |
180 } | |
181 | |
182 /// Simply takes the L channel from the input and puts it into the output | |
183 static void lab_to_l(const ImageLAB* imageLAB, ImageL* outImageL) { | |
184 for (int y = 0; y < imageLAB->height; y++) { | |
185 for (int x = 0; x < imageLAB->width; x++) { | |
186 LAB lab; | |
187 imageLAB->readPixel(x, y, &lab); | |
188 outImageL->writePixel(x, y, lab.l); | |
189 } | |
190 } | |
191 } | |
192 | |
193 /// Convolves an image with the given filter in one direction and saves it to th e output image | |
194 static void convolve(const ImageL* imageL, | |
195 bool vertical, const float* matrix, int radius, | |
196 ImageL* outImageL) { | |
197 SkASSERT(imageL->width == outImageL->width); | |
198 SkASSERT(imageL->height == outImageL->height); | |
199 for (int y = 0; y < imageL->height; y++) { | |
200 for (int x = 0; x < imageL->width; x++) { | |
201 float lSum = 0.0f; | |
202 float l; | |
203 for (int xx = -radius; xx <= radius; xx++) { | |
204 int nx = x; | |
205 int ny = y; | |
206 if (vertical) { | |
207 ny += xx; | |
208 } else { | |
209 nx += xx; | |
210 } | |
211 if (nx < 0) { | |
bsalomon
2013/06/27 18:31:15
should these four ifs be done inside the vertical/
| |
212 nx = -nx; | |
213 } | |
214 if (ny < 0) { | |
215 ny = -ny; | |
216 } | |
217 if (nx >= imageL->width) { | |
218 nx = imageL->width + (imageL->width - nx - 1); | |
219 } | |
220 if (ny >= imageL->height) { | |
221 ny = imageL->height + (imageL->height - ny - 1); | |
222 } | |
223 imageL->readPixel(nx, ny, &l); | |
224 float weight = matrix[xx + radius]; | |
225 lSum += l * weight; | |
226 } | |
227 outImageL->writePixel(x, y, lSum); | |
228 } | |
229 } | |
230 } | |
231 | |
232 float pmetric(const ImageLAB* baselineLAB, const ImageLAB* testLAB) { | |
233 int width = baselineLAB->width; | |
234 int height = baselineLAB->height; | |
235 int maxLevels = (int)log2(width < height ? width : height); | |
236 | |
237 const float fov = M_PI / 180.0f * 45.0f; | |
238 float contrastSensitivityMax = contrast_sensitivity(3.248f, 100.0f); | |
239 float pixelsPerDegree = width / (2.0f * tanf(fov * 0.5f) * 180.0f / M_PI); | |
240 | |
241 ImageL3D baselineL(width, height, maxLevels); | |
242 ImageL3D testL(width, height, maxLevels); | |
243 ImageL scratchImageL(width, height); | |
244 float* cyclesPerDegree = SkNEW_ARRAY(float, maxLevels); | |
245 float* thresholdFactorFrequency = SkNEW_ARRAY(float, maxLevels - 2); | |
246 float* contrast = SkNEW_ARRAY(float, maxLevels - 2); | |
247 | |
248 lab_to_l(baselineLAB, baselineL.getLayer(0)); | |
249 lab_to_l(testLAB, testL.getLayer(0)); | |
250 | |
251 // Compute cpd - Cycles per degree on the pyramid | |
252 cyclesPerDegree[0] = 0.5f * pixelsPerDegree; | |
253 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) { | |
254 cyclesPerDegree[levelIndex] = cyclesPerDegree[levelIndex - 1] * 0.5f; | |
255 } | |
256 | |
257 const float filterMatrix[] = { 0.05f, 0.25f, 0.4f, 0.25f, 0.05f }; | |
258 // Compute G - The convolved lum for the baseline | |
259 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) { | |
260 convolve(baselineL.getLayer(levelIndex - 1), false, filterMatrix, 2, &sc ratchImageL); | |
261 convolve(&scratchImageL, true, filterMatrix, 2, baselineL.getLayer(level Index)); | |
262 } | |
263 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) { | |
264 convolve(testL.getLayer(levelIndex - 1), false, filterMatrix, 2, &scratc hImageL); | |
265 convolve(&scratchImageL, true, filterMatrix, 2, testL.getLayer(levelInde x)); | |
266 } | |
267 | |
268 // Compute F_freq - The elevation f | |
269 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) { | |
270 float cpd = cyclesPerDegree[levelIndex]; | |
271 thresholdFactorFrequency[levelIndex] = contrastSensitivityMax / | |
272 contrast_sensitivity(cpd, 100.0f) ; | |
273 } | |
274 | |
275 int failures = 0; | |
276 // Calculate F | |
277 for (int y = 0; y < height; y++) { | |
278 for (int x = 0; x < width; x++) { | |
279 float lBaseline; | |
280 float lTest; | |
281 baselineL.getLayer(0)->readPixel(x, y, &lBaseline); | |
282 testL.getLayer(0)->readPixel(x, y, &lTest); | |
283 | |
284 float avgLBaseline; | |
285 float avgLTest; | |
286 baselineL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLBaseline); | |
287 testL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLTest); | |
288 | |
289 float lAdapt = 0.5f * (avgLBaseline + avgLTest); | |
290 if (lAdapt < 1e-5) { | |
291 lAdapt = 1e-5; | |
292 } | |
293 | |
294 float contrastSum = 0.0f; | |
295 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) { | |
296 float baselineL0, baselineL1, baselineL2; | |
297 float testL0, testL1, testL2; | |
298 baselineL.getLayer(levelIndex + 0)->readPixel(x, y, &baselineL0) ; | |
299 testL. getLayer(levelIndex + 0)->readPixel(x, y, &testL0); | |
300 baselineL.getLayer(levelIndex + 1)->readPixel(x, y, &baselineL1) ; | |
301 testL. getLayer(levelIndex + 1)->readPixel(x, y, &testL1); | |
302 baselineL.getLayer(levelIndex + 2)->readPixel(x, y, &baselineL2) ; | |
303 testL. getLayer(levelIndex + 2)->readPixel(x, y, &testL2); | |
304 | |
305 float baselineContrast1 = fabsf(baselineL0 - baselineL1); | |
306 float testContrast1 = fabsf(testL0 - testL1); | |
307 float numerator = (baselineContrast1 > testContrast1) ? | |
308 baselineContrast1 : testContrast1; | |
309 | |
310 float baselineContrast2 = fabsf(baselineL2); | |
311 float testContrast2 = fabsf(testL2); | |
312 float denominator = (baselineContrast2 > testContrast2) ? | |
313 baselineContrast2 : testContrast2; | |
314 | |
315 // Avoid divides by close to zero | |
316 if (denominator < 1e-5) { | |
317 denominator = 1e-5; | |
318 } | |
319 | |
320 contrast[levelIndex] = numerator / denominator; | |
321 contrastSum += contrast[levelIndex]; | |
322 } | |
323 | |
324 if (contrastSum < 1e-5) { | |
325 contrastSum = 1e-5; | |
326 } | |
327 | |
328 float F = 0.0f; | |
329 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) { | |
330 float mask = visual_mask(contrast[levelIndex] * | |
331 contrast_sensitivity(cyclesPerDegree[levelIndex], l Adapt)); | |
332 | |
333 F += contrast[levelIndex] + | |
334 thresholdFactorFrequency[levelIndex] * mask / contrastSum; | |
335 } | |
336 | |
337 if (F < 1.0f) { | |
338 F = 1.0f; | |
339 } | |
340 | |
341 if (F > 10.0f) { | |
342 F = 10.0f; | |
343 } | |
344 | |
345 | |
346 bool isFailure = false; | |
347 if (fabsf(lBaseline - lTest) > F * threshold_vs_intensity(lAdapt)) { | |
348 isFailure = true; | |
349 } else { | |
350 LAB baselineColor; | |
351 LAB testColor; | |
352 baselineLAB->readPixel(x, y, &baselineColor); | |
353 testLAB->readPixel(x, y, &testColor); | |
354 float contrastA = baselineColor.a - testColor.a; | |
355 float contrastB = baselineColor.b - testColor.b; | |
356 float colorScale = 1.0f; | |
357 if (lAdapt < 10.0f) { | |
358 colorScale = lAdapt / 10.0f; | |
359 } | |
360 colorScale *= colorScale; | |
361 | |
362 if ((contrastA * contrastA + contrastB * contrastB) * colorScale > F) | |
363 { | |
364 isFailure = true; | |
365 } | |
366 } | |
367 | |
368 if (isFailure) { | |
369 failures++; | |
370 } | |
371 } | |
372 } | |
373 | |
374 SkDELETE_ARRAY(cyclesPerDegree); | |
375 SkDELETE_ARRAY(contrast); | |
376 SkDELETE_ARRAY(thresholdFactorFrequency); | |
377 return (double)failures; | |
378 } | |
379 | |
380 const char* SkPMetric::getName() { | |
381 return "perceptual"; | |
382 } | |
383 | |
384 int SkPMetric::queueDiff(SkBitmap* baseline, SkBitmap* test) { | |
385 int diffID = fQueuedDiffs.count(); | |
386 double startTime = get_seconds(); | |
387 QueuedDiff* diff = fQueuedDiffs.push(); | |
388 | |
389 // Ensure the images are comparable | |
390 if (baseline->width() != test->width() || baseline->height() != test->height () || | |
391 baseline->width() <= 0 || baseline->height() <= 0) { | |
392 diff->finished = true; | |
393 diff->result = 0.0; | |
394 return diffID; | |
395 } | |
396 | |
397 ImageLAB baselineLAB(baseline->width(), baseline->height()); | |
398 ImageLAB testLAB(baseline->width(), baseline->height()); | |
399 | |
400 bitmap_to_cielab(baseline, &baselineLAB); | |
401 bitmap_to_cielab(test, &testLAB); | |
402 | |
403 diff->result = pmetric(&baselineLAB, &testLAB); | |
404 | |
405 SkDebugf("Time: %f\n", (get_seconds() - startTime)); | |
406 | |
407 return diffID; | |
408 } | |
409 | |
410 | |
411 bool SkPMetric::isFinished(int id) { | |
412 return fQueuedDiffs[id].finished; | |
413 } | |
414 | |
415 double SkPMetric::getResult(int id) { | |
416 return fQueuedDiffs[id].result; | |
417 } | |
OLD | NEW |