diff options
Diffstat (limited to 'chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp')
-rw-r--r-- | chromium/third_party/skia/tools/skpdiff/SkPMetric.cpp | 466 |
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; +} |