blob: 47880ac2916d372fc1eaf46f6fa175eec8b2dc71 [file] [log] [blame]
zachr@google.comc0a75a82013-06-28 15:34:56 +00001#include <cmath>
commit-bot@chromium.org89558c92014-05-21 11:57:12 +00002#include <math.h>
zachr@google.comc0a75a82013-06-28 15:34:56 +00003
4#include "SkBitmap.h"
5#include "skpdiff_util.h"
6#include "SkPMetric.h"
zachr@google.com92fe0732013-07-16 15:47:07 +00007#include "SkPMetricUtil_generated.h"
zachr@google.comc0a75a82013-06-28 15:34:56 +00008
9struct RGB {
10 float r, g, b;
11};
12
13struct LAB {
14 float l, a, b;
15};
16
17template<class T>
18struct Image2D {
19 int width;
20 int height;
21 T* image;
22
23 Image2D(int w, int h)
24 : width(w),
25 height(h) {
26 SkASSERT(w > 0);
27 SkASSERT(h > 0);
28 image = SkNEW_ARRAY(T, w * h);
29 }
30
31 ~Image2D() {
32 SkDELETE_ARRAY(image);
33 }
34
35 void readPixel(int x, int y, T* pixel) const {
36 SkASSERT(x >= 0);
37 SkASSERT(y >= 0);
38 SkASSERT(x < width);
39 SkASSERT(y < height);
40 *pixel = image[y * width + x];
41 }
42
zachr@google.coma79d40e2013-07-16 12:57:29 +000043 T* getRow(int y) const {
44 return &image[y * width];
45 }
46
zachr@google.comc0a75a82013-06-28 15:34:56 +000047 void writePixel(int x, int y, const T& pixel) {
48 SkASSERT(x >= 0);
49 SkASSERT(y >= 0);
50 SkASSERT(x < width);
51 SkASSERT(y < height);
52 image[y * width + x] = pixel;
53 }
54};
55
56typedef Image2D<float> ImageL;
57typedef Image2D<RGB> ImageRGB;
58typedef Image2D<LAB> ImageLAB;
59
60template<class T>
61struct ImageArray
62{
63 int slices;
64 Image2D<T>** image;
65
66 ImageArray(int w, int h, int s)
67 : slices(s) {
68 SkASSERT(s > 0);
69 image = SkNEW_ARRAY(Image2D<T>*, s);
70 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
71 image[sliceIndex] = SkNEW_ARGS(Image2D<T>, (w, h));
72 }
73 }
74
75 ~ImageArray() {
76 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
77 SkDELETE(image[sliceIndex]);
78 }
79 SkDELETE_ARRAY(image);
80 }
81
82 Image2D<T>* getLayer(int z) const {
83 SkASSERT(z >= 0);
84 SkASSERT(z < slices);
85 return image[z];
86 }
87};
88
89typedef ImageArray<float> ImageL3D;
90
91
92#define MAT_ROW_MULT(rc,gc,bc) r*rc + g*gc + b*bc
93
zachr@google.com35f02fb2013-07-22 17:05:24 +000094static void adobergb_to_cielab(float r, float g, float b, LAB* lab) {
zachr@google.comc0a75a82013-06-28 15:34:56 +000095 // Conversion of Adobe RGB to XYZ taken from from "Adobe RGB (1998) ColorImage Encoding"
96 // URL:http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
97 // Section: 4.3.5.3
98 // See Also: http://en.wikipedia.org/wiki/Adobe_rgb
99 float x = MAT_ROW_MULT(0.57667f, 0.18556f, 0.18823f);
100 float y = MAT_ROW_MULT(0.29734f, 0.62736f, 0.07529f);
101 float z = MAT_ROW_MULT(0.02703f, 0.07069f, 0.99134f);
102
103 // The following is the white point in XYZ, so it's simply the row wise addition of the above
104 // matrix.
105 const float xw = 0.5767f + 0.185556f + 0.188212f;
106 const float yw = 0.297361f + 0.627355f + 0.0752847f;
107 const float zw = 0.0270328f + 0.0706879f + 0.991248f;
108
109 // This is the XYZ color point relative to the white point
110 float f[3] = { x / xw, y / yw, z / zw };
111
112 // Conversion from XYZ to LAB taken from
113 // http://en.wikipedia.org/wiki/CIELAB#Forward_transformation
114 for (int i = 0; i < 3; i++) {
115 if (f[i] >= 0.008856f) {
zachr@google.com92fe0732013-07-16 15:47:07 +0000116 f[i] = SkPMetricUtil::get_cube_root(f[i]);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000117 } else {
118 f[i] = 7.787f * f[i] + 4.0f / 29.0f;
119 }
120 }
121 lab->l = 116.0f * f[1] - 16.0f;
122 lab->a = 500.0f * (f[0] - f[1]);
123 lab->b = 200.0f * (f[1] - f[2]);
124}
125
126/// Converts a 8888 bitmap to LAB color space and puts it into the output
scroggo@google.com086364b2013-11-12 14:41:20 +0000127static bool bitmap_to_cielab(const SkBitmap* bitmap, ImageLAB* outImageLAB) {
128 SkBitmap bm8888;
commit-bot@chromium.org28fcae22014-04-11 17:15:40 +0000129 if (bitmap->colorType() != kN32_SkColorType) {
130 if (!bitmap->copyTo(&bm8888, kN32_SkColorType)) {
scroggo@google.com086364b2013-11-12 14:41:20 +0000131 return false;
132 }
133 bitmap = &bm8888;
134 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000135
136 int width = bitmap->width();
137 int height = bitmap->height();
138 SkASSERT(outImageLAB->width == width);
139 SkASSERT(outImageLAB->height == height);
140
141 bitmap->lockPixels();
142 RGB rgb;
143 LAB lab;
144 for (int y = 0; y < height; y++) {
145 unsigned char* row = (unsigned char*)bitmap->getAddr(0, y);
146 for (int x = 0; x < width; x++) {
147 // Perform gamma correction which is assumed to be 2.2
zachr@google.com92fe0732013-07-16 15:47:07 +0000148 rgb.r = SkPMetricUtil::get_gamma(row[x * 4 + 2]);
149 rgb.g = SkPMetricUtil::get_gamma(row[x * 4 + 1]);
150 rgb.b = SkPMetricUtil::get_gamma(row[x * 4 + 0]);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000151 adobergb_to_cielab(rgb.r, rgb.g, rgb.b, &lab);
152 outImageLAB->writePixel(x, y, lab);
153 }
154 }
155 bitmap->unlockPixels();
scroggo@google.com086364b2013-11-12 14:41:20 +0000156 return true;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000157}
158
159// From Barten SPIE 1989
160static float contrast_sensitivity(float cyclesPerDegree, float luminance) {
161 float a = 440.0f * powf(1.0f + 0.7f / luminance, -0.2f);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000162 float b = 0.3f * powf(1.0f + 100.0f / luminance, 0.15f);
commit-bot@chromium.org89558c92014-05-21 11:57:12 +0000163 float exp = expf(-b * cyclesPerDegree);
164 float root = sqrtf(1.0f + 0.06f * expf(b * cyclesPerDegree));
165 if (!std::isfinite(exp) || !std::isfinite(root)) {
166 return 0;
167 }
168 return a * cyclesPerDegree * exp * root;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000169}
170
zachr@google.com92fe0732013-07-16 15:47:07 +0000171#if 0
172// We're keeping these around for reference and in case the lookup tables are no longer desired.
173// They are no longer called by any code in this file.
174
zachr@google.comc0a75a82013-06-28 15:34:56 +0000175// From Daly 1993
zachr@google.com92fe0732013-07-16 15:47:07 +0000176 static float visual_mask(float contrast) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000177 float x = powf(392.498f * contrast, 0.7f);
178 x = powf(0.0153f * x, 4.0f);
179 return powf(1.0f + x, 0.25f);
180}
181
182// From Ward Larson Siggraph 1997
183static float threshold_vs_intensity(float adaptationLuminance) {
184 float logLum = log10f(adaptationLuminance);
185 float x;
186 if (logLum < -3.94f) {
187 x = -2.86f;
188 } else if (logLum < -1.44f) {
189 x = powf(0.405f * logLum + 1.6f, 2.18) - 2.86f;
190 } else if (logLum < -0.0184f) {
191 x = logLum - 0.395f;
192 } else if (logLum < 1.9f) {
193 x = powf(0.249f * logLum + 0.65f, 2.7f) - 0.72f;
194 } else {
195 x = logLum - 1.255f;
196 }
197 return powf(10.0f, x);
198}
199
zachr@google.com92fe0732013-07-16 15:47:07 +0000200#endif
201
zachr@google.comc0a75a82013-06-28 15:34:56 +0000202/// Simply takes the L channel from the input and puts it into the output
203static void lab_to_l(const ImageLAB* imageLAB, ImageL* outImageL) {
204 for (int y = 0; y < imageLAB->height; y++) {
205 for (int x = 0; x < imageLAB->width; x++) {
206 LAB lab;
207 imageLAB->readPixel(x, y, &lab);
208 outImageL->writePixel(x, y, lab.l);
209 }
210 }
211}
212
213/// Convolves an image with the given filter in one direction and saves it to the output image
zachr@google.coma79d40e2013-07-16 12:57:29 +0000214static void convolve(const ImageL* imageL, bool vertical, ImageL* outImageL) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000215 SkASSERT(imageL->width == outImageL->width);
216 SkASSERT(imageL->height == outImageL->height);
zachr@google.coma79d40e2013-07-16 12:57:29 +0000217
218 const float matrix[] = { 0.05f, 0.25f, 0.4f, 0.25f, 0.05f };
219 const int matrixCount = sizeof(matrix) / sizeof(float);
220 const int radius = matrixCount / 2;
221
222 // Keep track of what rows are being operated on for quick access.
223 float* rowPtrs[matrixCount]; // Because matrixCount is constant, this won't create a VLA
224 for (int y = radius; y < matrixCount; y++) {
225 rowPtrs[y] = imageL->getRow(y - radius);
226 }
227 float* writeRow = outImageL->getRow(0);
228
zachr@google.comc0a75a82013-06-28 15:34:56 +0000229 for (int y = 0; y < imageL->height; y++) {
230 for (int x = 0; x < imageL->width; x++) {
231 float lSum = 0.0f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000232 for (int xx = -radius; xx <= radius; xx++) {
233 int nx = x;
234 int ny = y;
235
236 // We mirror at edges so that edge pixels that the filter weighting still makes
237 // sense.
238 if (vertical) {
239 ny += xx;
240 if (ny < 0) {
241 ny = -ny;
242 }
243 if (ny >= imageL->height) {
244 ny = imageL->height + (imageL->height - ny - 1);
245 }
246 } else {
247 nx += xx;
248 if (nx < 0) {
249 nx = -nx;
250 }
251 if (nx >= imageL->width) {
252 nx = imageL->width + (imageL->width - nx - 1);
253 }
254 }
255
zachr@google.comc0a75a82013-06-28 15:34:56 +0000256 float weight = matrix[xx + radius];
zachr@google.coma79d40e2013-07-16 12:57:29 +0000257 lSum += rowPtrs[ny - y + radius][nx] * weight;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000258 }
zachr@google.coma79d40e2013-07-16 12:57:29 +0000259 writeRow[x] = lSum;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000260 }
zachr@google.coma79d40e2013-07-16 12:57:29 +0000261 // As we move down, scroll the row pointers down with us
262 for (int y = 0; y < matrixCount - 1; y++)
263 {
264 rowPtrs[y] = rowPtrs[y + 1];
265 }
266 rowPtrs[matrixCount - 1] += imageL->width;
267 writeRow += imageL->width;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000268 }
269}
270
djsollen@google.comefc51b72013-11-12 18:29:17 +0000271static double pmetric(const ImageLAB* baselineLAB, const ImageLAB* testLAB, int* poiCount) {
272 SkASSERT(baselineLAB);
273 SkASSERT(testLAB);
274 SkASSERT(poiCount);
275
zachr@google.comc0a75a82013-06-28 15:34:56 +0000276 int width = baselineLAB->width;
277 int height = baselineLAB->height;
zachr@google.com35f02fb2013-07-22 17:05:24 +0000278 int maxLevels = 0;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000279
zachr@google.com35f02fb2013-07-22 17:05:24 +0000280 // Calculates how many levels to make by how many times the image can be divided in two
281 int smallerDimension = width < height ? width : height;
282 for ( ; smallerDimension > 1; smallerDimension /= 2) {
283 maxLevels++;
284 }
285
scroggo@google.com086364b2013-11-12 14:41:20 +0000286 // We'll be creating new arrays with maxLevels - 2, and ImageL3D requires maxLevels > 0,
287 // so just return failure if we're less than 3.
288 if (maxLevels <= 2) {
289 return 0.0;
290 }
291
zachr@google.com35f02fb2013-07-22 17:05:24 +0000292 const float fov = SK_ScalarPI / 180.0f * 45.0f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000293 float contrastSensitivityMax = contrast_sensitivity(3.248f, 100.0f);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000294 float pixelsPerDegree = width / (2.0f * tanf(fov * 0.5f) * 180.0f / SK_ScalarPI);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000295
296 ImageL3D baselineL(width, height, maxLevels);
297 ImageL3D testL(width, height, maxLevels);
298 ImageL scratchImageL(width, height);
299 float* cyclesPerDegree = SkNEW_ARRAY(float, maxLevels);
300 float* thresholdFactorFrequency = SkNEW_ARRAY(float, maxLevels - 2);
301 float* contrast = SkNEW_ARRAY(float, maxLevels - 2);
302
303 lab_to_l(baselineLAB, baselineL.getLayer(0));
304 lab_to_l(testLAB, testL.getLayer(0));
305
306 // Compute cpd - Cycles per degree on the pyramid
307 cyclesPerDegree[0] = 0.5f * pixelsPerDegree;
308 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
309 cyclesPerDegree[levelIndex] = cyclesPerDegree[levelIndex - 1] * 0.5f;
310 }
311
zachr@google.com92fe0732013-07-16 15:47:07 +0000312 // Contrast sensitivity is based on image dimensions. Therefore it cannot be statically
313 // generated.
314 float* contrastSensitivityTable = SkNEW_ARRAY(float, maxLevels * 1000);
315 for (int levelIndex = 0; levelIndex < maxLevels; levelIndex++) {
316 for (int csLum = 0; csLum < 1000; csLum++) {
317 contrastSensitivityTable[levelIndex * 1000 + csLum] =
318 contrast_sensitivity(cyclesPerDegree[levelIndex], (float)csLum / 10.0f + 1e-5f);
319 }
320 }
321
zachr@google.comc0a75a82013-06-28 15:34:56 +0000322 // Compute G - The convolved lum for the baseline
323 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
zachr@google.coma79d40e2013-07-16 12:57:29 +0000324 convolve(baselineL.getLayer(levelIndex - 1), false, &scratchImageL);
325 convolve(&scratchImageL, true, baselineL.getLayer(levelIndex));
zachr@google.comc0a75a82013-06-28 15:34:56 +0000326 }
327 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
zachr@google.coma79d40e2013-07-16 12:57:29 +0000328 convolve(testL.getLayer(levelIndex - 1), false, &scratchImageL);
329 convolve(&scratchImageL, true, testL.getLayer(levelIndex));
zachr@google.comc0a75a82013-06-28 15:34:56 +0000330 }
331
332 // Compute F_freq - The elevation f
333 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
334 float cpd = cyclesPerDegree[levelIndex];
335 thresholdFactorFrequency[levelIndex] = contrastSensitivityMax /
336 contrast_sensitivity(cpd, 100.0f);
337 }
338
zachr@google.comc0a75a82013-06-28 15:34:56 +0000339 // Calculate F
340 for (int y = 0; y < height; y++) {
341 for (int x = 0; x < width; x++) {
342 float lBaseline;
343 float lTest;
344 baselineL.getLayer(0)->readPixel(x, y, &lBaseline);
345 testL.getLayer(0)->readPixel(x, y, &lTest);
346
347 float avgLBaseline;
348 float avgLTest;
349 baselineL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLBaseline);
350 testL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLTest);
351
352 float lAdapt = 0.5f * (avgLBaseline + avgLTest);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000353 if (lAdapt < 1e-5f) {
354 lAdapt = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000355 }
356
357 float contrastSum = 0.0f;
358 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
359 float baselineL0, baselineL1, baselineL2;
360 float testL0, testL1, testL2;
361 baselineL.getLayer(levelIndex + 0)->readPixel(x, y, &baselineL0);
362 testL. getLayer(levelIndex + 0)->readPixel(x, y, &testL0);
363 baselineL.getLayer(levelIndex + 1)->readPixel(x, y, &baselineL1);
364 testL. getLayer(levelIndex + 1)->readPixel(x, y, &testL1);
365 baselineL.getLayer(levelIndex + 2)->readPixel(x, y, &baselineL2);
366 testL. getLayer(levelIndex + 2)->readPixel(x, y, &testL2);
367
368 float baselineContrast1 = fabsf(baselineL0 - baselineL1);
369 float testContrast1 = fabsf(testL0 - testL1);
370 float numerator = (baselineContrast1 > testContrast1) ?
371 baselineContrast1 : testContrast1;
372
373 float baselineContrast2 = fabsf(baselineL2);
374 float testContrast2 = fabsf(testL2);
375 float denominator = (baselineContrast2 > testContrast2) ?
376 baselineContrast2 : testContrast2;
377
378 // Avoid divides by close to zero
zachr@google.com35f02fb2013-07-22 17:05:24 +0000379 if (denominator < 1e-5f) {
380 denominator = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000381 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000382 contrast[levelIndex] = numerator / denominator;
383 contrastSum += contrast[levelIndex];
384 }
385
zachr@google.com35f02fb2013-07-22 17:05:24 +0000386 if (contrastSum < 1e-5f) {
387 contrastSum = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000388 }
389
390 float F = 0.0f;
391 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
zachr@google.com92fe0732013-07-16 15:47:07 +0000392 float contrastSensitivity = contrastSensitivityTable[levelIndex * 1000 +
393 (int)(lAdapt * 10.0)];
394 float mask = SkPMetricUtil::get_visual_mask(contrast[levelIndex] *
395 contrastSensitivity);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000396
397 F += contrast[levelIndex] +
398 thresholdFactorFrequency[levelIndex] * mask / contrastSum;
399 }
400
401 if (F < 1.0f) {
402 F = 1.0f;
403 }
404
405 if (F > 10.0f) {
406 F = 10.0f;
407 }
408
409
410 bool isFailure = false;
zachr@google.com92fe0732013-07-16 15:47:07 +0000411 if (fabsf(lBaseline - lTest) > F * SkPMetricUtil::get_threshold_vs_intensity(lAdapt)) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000412 isFailure = true;
413 } else {
414 LAB baselineColor;
415 LAB testColor;
416 baselineLAB->readPixel(x, y, &baselineColor);
417 testLAB->readPixel(x, y, &testColor);
418 float contrastA = baselineColor.a - testColor.a;
419 float contrastB = baselineColor.b - testColor.b;
420 float colorScale = 1.0f;
421 if (lAdapt < 10.0f) {
422 colorScale = lAdapt / 10.0f;
423 }
424 colorScale *= colorScale;
425
426 if ((contrastA * contrastA + contrastB * contrastB) * colorScale > F)
427 {
428 isFailure = true;
429 }
430 }
431
432 if (isFailure) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000433 (*poiCount)++;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000434 }
435 }
436 }
437
438 SkDELETE_ARRAY(cyclesPerDegree);
439 SkDELETE_ARRAY(contrast);
440 SkDELETE_ARRAY(thresholdFactorFrequency);
zachr@google.com92fe0732013-07-16 15:47:07 +0000441 SkDELETE_ARRAY(contrastSensitivityTable);
djsollen@google.comefc51b72013-11-12 18:29:17 +0000442 return 1.0 - (double)(*poiCount) / (width * height);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000443}
444
djsollen@google.comefc51b72013-11-12 18:29:17 +0000445bool SkPMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeMask, Result* result) const {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000446 double startTime = get_seconds();
zachr@google.comc0a75a82013-06-28 15:34:56 +0000447
448 // Ensure the images are comparable
449 if (baseline->width() != test->width() || baseline->height() != test->height() ||
450 baseline->width() <= 0 || baseline->height() <= 0) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000451 return false;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000452 }
453
454 ImageLAB baselineLAB(baseline->width(), baseline->height());
455 ImageLAB testLAB(baseline->width(), baseline->height());
456
scroggo@google.com086364b2013-11-12 14:41:20 +0000457 if (!bitmap_to_cielab(baseline, &baselineLAB) || !bitmap_to_cielab(test, &testLAB)) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000458 return true;
scroggo@google.com086364b2013-11-12 14:41:20 +0000459 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000460
djsollen@google.comefc51b72013-11-12 18:29:17 +0000461 result->poiCount = 0;
462 result->result = pmetric(&baselineLAB, &testLAB, &result->poiCount);
463 result->timeElapsed = get_seconds() - startTime;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000464
djsollen@google.comefc51b72013-11-12 18:29:17 +0000465 return true;
zachr@google.com572b54d2013-06-28 16:27:33 +0000466}