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