summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp')
-rw-r--r--chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp466
1 files changed, 466 insertions, 0 deletions
diff --git a/chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp b/chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp
new file mode 100644
index 00000000000..a433d9f4b30
--- /dev/null
+++ b/chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp
@@ -0,0 +1,466 @@
+#include <cmath>
+#include <math.h>
+
+#include "SkBitmap.h"
+#include "skpdiff_util.h"
+#include "SkPMetric.h"
+#include "SkPMetricUtil_generated.h"
+
+struct RGB {
+ float r, g, b;
+};
+
+struct LAB {
+ float l, a, b;
+};
+
+template<class T>
+struct Image2D {
+ int width;
+ int height;
+ T* image;
+
+ Image2D(int w, int h)
+ : width(w),
+ height(h) {
+ SkASSERT(w > 0);
+ SkASSERT(h > 0);
+ image = SkNEW_ARRAY(T, w * h);
+ }
+
+ ~Image2D() {
+ SkDELETE_ARRAY(image);
+ }
+
+ void readPixel(int x, int y, T* pixel) const {
+ SkASSERT(x >= 0);
+ SkASSERT(y >= 0);
+ SkASSERT(x < width);
+ SkASSERT(y < height);
+ *pixel = image[y * width + x];
+ }
+
+ T* getRow(int y) const {
+ return &image[y * width];
+ }
+
+ void writePixel(int x, int y, const T& pixel) {
+ SkASSERT(x >= 0);
+ SkASSERT(y >= 0);
+ SkASSERT(x < width);
+ SkASSERT(y < height);
+ image[y * width + x] = pixel;
+ }
+};
+
+typedef Image2D<float> ImageL;
+typedef Image2D<RGB> ImageRGB;
+typedef Image2D<LAB> ImageLAB;
+
+template<class T>
+struct ImageArray
+{
+ int slices;
+ Image2D<T>** image;
+
+ ImageArray(int w, int h, int s)
+ : slices(s) {
+ SkASSERT(s > 0);
+ image = SkNEW_ARRAY(Image2D<T>*, s);
+ for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
+ image[sliceIndex] = SkNEW_ARGS(Image2D<T>, (w, h));
+ }
+ }
+
+ ~ImageArray() {
+ for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
+ SkDELETE(image[sliceIndex]);
+ }
+ SkDELETE_ARRAY(image);
+ }
+
+ Image2D<T>* getLayer(int z) const {
+ SkASSERT(z >= 0);
+ SkASSERT(z < slices);
+ return image[z];
+ }
+};
+
+typedef ImageArray<float> ImageL3D;
+
+
+#define MAT_ROW_MULT(rc,gc,bc) r*rc + g*gc + b*bc
+
+static void adobergb_to_cielab(float r, float g, float b, LAB* lab) {
+ // Conversion of Adobe RGB to XYZ taken from from "Adobe RGB (1998) ColorImage Encoding"
+ // URL:http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
+ // Section: 4.3.5.3
+ // See Also: http://en.wikipedia.org/wiki/Adobe_rgb
+ float x = MAT_ROW_MULT(0.57667f, 0.18556f, 0.18823f);
+ float y = MAT_ROW_MULT(0.29734f, 0.62736f, 0.07529f);
+ float z = MAT_ROW_MULT(0.02703f, 0.07069f, 0.99134f);
+
+ // The following is the white point in XYZ, so it's simply the row wise addition of the above
+ // matrix.
+ const float xw = 0.5767f + 0.185556f + 0.188212f;
+ const float yw = 0.297361f + 0.627355f + 0.0752847f;
+ const float zw = 0.0270328f + 0.0706879f + 0.991248f;
+
+ // This is the XYZ color point relative to the white point
+ float f[3] = { x / xw, y / yw, z / zw };
+
+ // Conversion from XYZ to LAB taken from
+ // http://en.wikipedia.org/wiki/CIELAB#Forward_transformation
+ for (int i = 0; i < 3; i++) {
+ if (f[i] >= 0.008856f) {
+ f[i] = SkPMetricUtil::get_cube_root(f[i]);
+ } else {
+ f[i] = 7.787f * f[i] + 4.0f / 29.0f;
+ }
+ }
+ lab->l = 116.0f * f[1] - 16.0f;
+ lab->a = 500.0f * (f[0] - f[1]);
+ lab->b = 200.0f * (f[1] - f[2]);
+}
+
+/// Converts a 8888 bitmap to LAB color space and puts it into the output
+static bool bitmap_to_cielab(const SkBitmap* bitmap, ImageLAB* outImageLAB) {
+ SkBitmap bm8888;
+ if (bitmap->colorType() != kN32_SkColorType) {
+ if (!bitmap->copyTo(&bm8888, kN32_SkColorType)) {
+ return false;
+ }
+ bitmap = &bm8888;
+ }
+
+ int width = bitmap->width();
+ int height = bitmap->height();
+ SkASSERT(outImageLAB->width == width);
+ SkASSERT(outImageLAB->height == height);
+
+ bitmap->lockPixels();
+ RGB rgb;
+ LAB lab;
+ for (int y = 0; y < height; y++) {
+ unsigned char* row = (unsigned char*)bitmap->getAddr(0, y);
+ for (int x = 0; x < width; x++) {
+ // Perform gamma correction which is assumed to be 2.2
+ rgb.r = SkPMetricUtil::get_gamma(row[x * 4 + 2]);
+ rgb.g = SkPMetricUtil::get_gamma(row[x * 4 + 1]);
+ rgb.b = SkPMetricUtil::get_gamma(row[x * 4 + 0]);
+ adobergb_to_cielab(rgb.r, rgb.g, rgb.b, &lab);
+ outImageLAB->writePixel(x, y, lab);
+ }
+ }
+ bitmap->unlockPixels();
+ return true;
+}
+
+// From Barten SPIE 1989
+static float contrast_sensitivity(float cyclesPerDegree, float luminance) {
+ float a = 440.0f * powf(1.0f + 0.7f / luminance, -0.2f);
+ float b = 0.3f * powf(1.0f + 100.0f / luminance, 0.15f);
+ float exp = expf(-b * cyclesPerDegree);
+ float root = sqrtf(1.0f + 0.06f * expf(b * cyclesPerDegree));
+ if (!SkScalarIsFinite(exp) || !SkScalarIsFinite(root)) {
+ return 0;
+ }
+ return a * cyclesPerDegree * exp * root;
+}
+
+#if 0
+// We're keeping these around for reference and in case the lookup tables are no longer desired.
+// They are no longer called by any code in this file.
+
+// From Daly 1993
+ static float visual_mask(float contrast) {
+ float x = powf(392.498f * contrast, 0.7f);
+ x = powf(0.0153f * x, 4.0f);
+ return powf(1.0f + x, 0.25f);
+}
+
+// From Ward Larson Siggraph 1997
+static float threshold_vs_intensity(float adaptationLuminance) {
+ float logLum = log10f(adaptationLuminance);
+ float x;
+ if (logLum < -3.94f) {
+ x = -2.86f;
+ } else if (logLum < -1.44f) {
+ x = powf(0.405f * logLum + 1.6f, 2.18) - 2.86f;
+ } else if (logLum < -0.0184f) {
+ x = logLum - 0.395f;
+ } else if (logLum < 1.9f) {
+ x = powf(0.249f * logLum + 0.65f, 2.7f) - 0.72f;
+ } else {
+ x = logLum - 1.255f;
+ }
+ return powf(10.0f, x);
+}
+
+#endif
+
+/// Simply takes the L channel from the input and puts it into the output
+static void lab_to_l(const ImageLAB* imageLAB, ImageL* outImageL) {
+ for (int y = 0; y < imageLAB->height; y++) {
+ for (int x = 0; x < imageLAB->width; x++) {
+ LAB lab;
+ imageLAB->readPixel(x, y, &lab);
+ outImageL->writePixel(x, y, lab.l);
+ }
+ }
+}
+
+/// Convolves an image with the given filter in one direction and saves it to the output image
+static void convolve(const ImageL* imageL, bool vertical, ImageL* outImageL) {
+ SkASSERT(imageL->width == outImageL->width);
+ SkASSERT(imageL->height == outImageL->height);
+
+ const float matrix[] = { 0.05f, 0.25f, 0.4f, 0.25f, 0.05f };
+ const int matrixCount = sizeof(matrix) / sizeof(float);
+ const int radius = matrixCount / 2;
+
+ // Keep track of what rows are being operated on for quick access.
+ float* rowPtrs[matrixCount]; // Because matrixCount is constant, this won't create a VLA
+ for (int y = radius; y < matrixCount; y++) {
+ rowPtrs[y] = imageL->getRow(y - radius);
+ }
+ float* writeRow = outImageL->getRow(0);
+
+ for (int y = 0; y < imageL->height; y++) {
+ for (int x = 0; x < imageL->width; x++) {
+ float lSum = 0.0f;
+ for (int xx = -radius; xx <= radius; xx++) {
+ int nx = x;
+ int ny = y;
+
+ // We mirror at edges so that edge pixels that the filter weighting still makes
+ // sense.
+ if (vertical) {
+ ny += xx;
+ if (ny < 0) {
+ ny = -ny;
+ }
+ if (ny >= imageL->height) {
+ ny = imageL->height + (imageL->height - ny - 1);
+ }
+ } else {
+ nx += xx;
+ if (nx < 0) {
+ nx = -nx;
+ }
+ if (nx >= imageL->width) {
+ nx = imageL->width + (imageL->width - nx - 1);
+ }
+ }
+
+ float weight = matrix[xx + radius];
+ lSum += rowPtrs[ny - y + radius][nx] * weight;
+ }
+ writeRow[x] = lSum;
+ }
+ // As we move down, scroll the row pointers down with us
+ for (int y = 0; y < matrixCount - 1; y++)
+ {
+ rowPtrs[y] = rowPtrs[y + 1];
+ }
+ rowPtrs[matrixCount - 1] += imageL->width;
+ writeRow += imageL->width;
+ }
+}
+
+static double pmetric(const ImageLAB* baselineLAB, const ImageLAB* testLAB, int* poiCount) {
+ SkASSERT(baselineLAB);
+ SkASSERT(testLAB);
+ SkASSERT(poiCount);
+
+ int width = baselineLAB->width;
+ int height = baselineLAB->height;
+ int maxLevels = 0;
+
+ // Calculates how many levels to make by how many times the image can be divided in two
+ int smallerDimension = width < height ? width : height;
+ for ( ; smallerDimension > 1; smallerDimension /= 2) {
+ maxLevels++;
+ }
+
+ // We'll be creating new arrays with maxLevels - 2, and ImageL3D requires maxLevels > 0,
+ // so just return failure if we're less than 3.
+ if (maxLevels <= 2) {
+ return 0.0;
+ }
+
+ const float fov = SK_ScalarPI / 180.0f * 45.0f;
+ float contrastSensitivityMax = contrast_sensitivity(3.248f, 100.0f);
+ float pixelsPerDegree = width / (2.0f * tanf(fov * 0.5f) * 180.0f / SK_ScalarPI);
+
+ ImageL3D baselineL(width, height, maxLevels);
+ ImageL3D testL(width, height, maxLevels);
+ ImageL scratchImageL(width, height);
+ float* cyclesPerDegree = SkNEW_ARRAY(float, maxLevels);
+ float* thresholdFactorFrequency = SkNEW_ARRAY(float, maxLevels - 2);
+ float* contrast = SkNEW_ARRAY(float, maxLevels - 2);
+
+ lab_to_l(baselineLAB, baselineL.getLayer(0));
+ lab_to_l(testLAB, testL.getLayer(0));
+
+ // Compute cpd - Cycles per degree on the pyramid
+ cyclesPerDegree[0] = 0.5f * pixelsPerDegree;
+ for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
+ cyclesPerDegree[levelIndex] = cyclesPerDegree[levelIndex - 1] * 0.5f;
+ }
+
+ // Contrast sensitivity is based on image dimensions. Therefore it cannot be statically
+ // generated.
+ float* contrastSensitivityTable = SkNEW_ARRAY(float, maxLevels * 1000);
+ for (int levelIndex = 0; levelIndex < maxLevels; levelIndex++) {
+ for (int csLum = 0; csLum < 1000; csLum++) {
+ contrastSensitivityTable[levelIndex * 1000 + csLum] =
+ contrast_sensitivity(cyclesPerDegree[levelIndex], (float)csLum / 10.0f + 1e-5f);
+ }
+ }
+
+ // Compute G - The convolved lum for the baseline
+ for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
+ convolve(baselineL.getLayer(levelIndex - 1), false, &scratchImageL);
+ convolve(&scratchImageL, true, baselineL.getLayer(levelIndex));
+ }
+ for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
+ convolve(testL.getLayer(levelIndex - 1), false, &scratchImageL);
+ convolve(&scratchImageL, true, testL.getLayer(levelIndex));
+ }
+
+ // Compute F_freq - The elevation f
+ for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
+ float cpd = cyclesPerDegree[levelIndex];
+ thresholdFactorFrequency[levelIndex] = contrastSensitivityMax /
+ contrast_sensitivity(cpd, 100.0f);
+ }
+
+ // Calculate F
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ float lBaseline;
+ float lTest;
+ baselineL.getLayer(0)->readPixel(x, y, &lBaseline);
+ testL.getLayer(0)->readPixel(x, y, &lTest);
+
+ float avgLBaseline;
+ float avgLTest;
+ baselineL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLBaseline);
+ testL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLTest);
+
+ float lAdapt = 0.5f * (avgLBaseline + avgLTest);
+ if (lAdapt < 1e-5f) {
+ lAdapt = 1e-5f;
+ }
+
+ float contrastSum = 0.0f;
+ for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
+ float baselineL0, baselineL1, baselineL2;
+ float testL0, testL1, testL2;
+ baselineL.getLayer(levelIndex + 0)->readPixel(x, y, &baselineL0);
+ testL. getLayer(levelIndex + 0)->readPixel(x, y, &testL0);
+ baselineL.getLayer(levelIndex + 1)->readPixel(x, y, &baselineL1);
+ testL. getLayer(levelIndex + 1)->readPixel(x, y, &testL1);
+ baselineL.getLayer(levelIndex + 2)->readPixel(x, y, &baselineL2);
+ testL. getLayer(levelIndex + 2)->readPixel(x, y, &testL2);
+
+ float baselineContrast1 = fabsf(baselineL0 - baselineL1);
+ float testContrast1 = fabsf(testL0 - testL1);
+ float numerator = (baselineContrast1 > testContrast1) ?
+ baselineContrast1 : testContrast1;
+
+ float baselineContrast2 = fabsf(baselineL2);
+ float testContrast2 = fabsf(testL2);
+ float denominator = (baselineContrast2 > testContrast2) ?
+ baselineContrast2 : testContrast2;
+
+ // Avoid divides by close to zero
+ if (denominator < 1e-5f) {
+ denominator = 1e-5f;
+ }
+ contrast[levelIndex] = numerator / denominator;
+ contrastSum += contrast[levelIndex];
+ }
+
+ if (contrastSum < 1e-5f) {
+ contrastSum = 1e-5f;
+ }
+
+ float F = 0.0f;
+ for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
+ float contrastSensitivity = contrastSensitivityTable[levelIndex * 1000 +
+ (int)(lAdapt * 10.0)];
+ float mask = SkPMetricUtil::get_visual_mask(contrast[levelIndex] *
+ contrastSensitivity);
+
+ F += contrast[levelIndex] +
+ thresholdFactorFrequency[levelIndex] * mask / contrastSum;
+ }
+
+ if (F < 1.0f) {
+ F = 1.0f;
+ }
+
+ if (F > 10.0f) {
+ F = 10.0f;
+ }
+
+
+ bool isFailure = false;
+ if (fabsf(lBaseline - lTest) > F * SkPMetricUtil::get_threshold_vs_intensity(lAdapt)) {
+ isFailure = true;
+ } else {
+ LAB baselineColor;
+ LAB testColor;
+ baselineLAB->readPixel(x, y, &baselineColor);
+ testLAB->readPixel(x, y, &testColor);
+ float contrastA = baselineColor.a - testColor.a;
+ float contrastB = baselineColor.b - testColor.b;
+ float colorScale = 1.0f;
+ if (lAdapt < 10.0f) {
+ colorScale = lAdapt / 10.0f;
+ }
+ colorScale *= colorScale;
+
+ if ((contrastA * contrastA + contrastB * contrastB) * colorScale > F)
+ {
+ isFailure = true;
+ }
+ }
+
+ if (isFailure) {
+ (*poiCount)++;
+ }
+ }
+ }
+
+ SkDELETE_ARRAY(cyclesPerDegree);
+ SkDELETE_ARRAY(contrast);
+ SkDELETE_ARRAY(thresholdFactorFrequency);
+ SkDELETE_ARRAY(contrastSensitivityTable);
+ return 1.0 - (double)(*poiCount) / (width * height);
+}
+
+bool SkPMetric::diff(SkBitmap* baseline, SkBitmap* test, bool computeMask, Result* result) const {
+ double startTime = get_seconds();
+
+ // Ensure the images are comparable
+ if (baseline->width() != test->width() || baseline->height() != test->height() ||
+ baseline->width() <= 0 || baseline->height() <= 0) {
+ return false;
+ }
+
+ ImageLAB baselineLAB(baseline->width(), baseline->height());
+ ImageLAB testLAB(baseline->width(), baseline->height());
+
+ if (!bitmap_to_cielab(baseline, &baselineLAB) || !bitmap_to_cielab(test, &testLAB)) {
+ return true;
+ }
+
+ result->poiCount = 0;
+ result->result = pmetric(&baselineLAB, &testLAB, &result->poiCount);
+ result->timeElapsed = get_seconds() - startTime;
+
+ return true;
+}