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