diff --git a/README.md b/README.md index 9d69842..62c2ff2 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ The smallest, simplest and fastest JavaScript pixel-level image comparison library, originally created to compare screenshots in tests. -Features accurate [anti-aliased pixels detection](http://www.ee.ktu.lt/journal/2009/7/25_ISSN_1392-1215_Anti-aliased%20Pxel%20and%20Intensity%20Slope%20Detector.pdf) -and [perceptual color metrics](https://en.wikipedia.org/wiki/YUV). +Features accurate **anti-aliased pixels detection** +and **perceptual color difference metrics**. Inspired by [Resemble.js](https://github.com/Huddle/Resemble.js) and [Blink-diff](https://github.com/yahoo/blink-diff). @@ -20,6 +20,11 @@ so it's **blazing fast** and can be used in **any environment** (Node or browser var numDiffPixels = pixelmatch(img1, img2, diff, 800, 600, {threshold: 0.1}); ``` +Implements ideas from the following papers: + +- [Measuring perceived color difference using YIQ NTSC transmission color space in mobile applications](http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf) (2010 Y. Kotsarenko, F. Ramos) +- [Anti-aliased pixel and intensity slope detector](http://www.ee.ktu.lt/journal/2009/7/25_ISSN_1392-1215_Anti-aliased%20Pxel%20and%20Intensity%20Slope%20Detector.pdf) (2009 V. Vyšniauskas) + ### Example output | expected | actual | diff | diff --git a/index.js b/index.js index e32fcc2..4af9de6 100644 --- a/index.js +++ b/index.js @@ -8,8 +8,9 @@ function pixelmatch(img1, img2, output, width, height, options) { var threshold = options.threshold === undefined ? 0.1 : options.threshold; - // maximum acceptable square YUV distance between two colors - var maxDelta = 255 * 255 * 3 * threshold * threshold, + // maximum acceptable square distance between two colors; + // 35215 is the maximum possible value for the YIQ difference metric + var maxDelta = 35215 * threshold * threshold, diff = 0; // compare each pixel of one image against the other one @@ -35,10 +36,10 @@ function pixelmatch(img1, img2, output, width, height, options) { diff++; } - } else { + } else if (output) { // pixels are similar; draw background as grayscale image blended with white - var val = 255 - 0.1 * (255 - grayPixel(img1, pos)) * img1[pos + 3] / 255; - if (output) drawPixel(output, pos, val, val, val); + var val = blend(grayPixel(img1, pos), 0.1); + drawPixel(output, pos, val, val, val); } } } @@ -107,32 +108,38 @@ function antialiased(img, x1, y1, width, height, img2) { (!antialiased(img, maxX, maxY, width, height) && !antialiased(img2, maxX, maxY, width, height)); } -// calculate either the squared YUV distance between colors, -// or just the brightness differene (Y component) if yOnly is true +// calculate color difference according to the paper "Measuring perceived color difference +// using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos -function colorDelta(img1, img2, i, j, yOnly) { - var a1 = img1[i + 3] / 255, - a2 = img2[j + 3] / 255, +function colorDelta(img1, img2, k, m, yOnly) { + var a1 = img1[k + 3] / 255, + a2 = img2[m + 3] / 255, - r1 = img1[i + 0] * a1, - g1 = img1[i + 1] * a1, - b1 = img1[i + 2] * a1, + r1 = blend(img1[k + 0], a1), + g1 = blend(img1[k + 1], a1), + b1 = blend(img1[k + 2], a1), - r2 = img2[j + 0] * a2, - g2 = img2[j + 1] * a2, - b2 = img2[j + 2] * a2, + r2 = blend(img2[m + 0], a2), + g2 = blend(img2[m + 1], a2), + b2 = blend(img2[m + 2], a2), - y1 = 0.299 * r1 + 0.587 * g1 + 0.114 * b1, - y2 = 0.299 * r2 + 0.587 * g2 + 0.114 * b2, + y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2); - yd = y1 - y2; + if (yOnly) return y; // brightness difference only - if (yOnly) return yd; + var i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2), + q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2); - var ud = 0.492 * (b1 - y1) - 0.492 * (b2 - y2), - vd = 0.877 * (r1 - y1) - 0.877 * (r2 - y2); + return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q; +} + +function rgb2y(r, g, b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; } +function rgb2i(r, g, b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; } +function rgb2q(r, g, b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; } - return (yd * yd) + (ud * ud) + (vd * vd); +// blend semi-transparent color with white +function blend(c, a) { + return 255 + (c - 255) * a; } function drawPixel(output, pos, r, g, b) { @@ -142,8 +149,10 @@ function drawPixel(output, pos, r, g, b) { output[pos + 3] = 255; } -function grayPixel(img, pos) { - return 0.30 * img[pos + 0] + - 0.59 * img[pos + 1] + - 0.11 * img[pos + 2]; +function grayPixel(img, i) { + var a = img[i + 3] / 255, + r = blend(img[i + 0], a), + g = blend(img[i + 1], a), + b = blend(img[i + 2], a); + return rgb2y(r, g, b); } diff --git a/test/fixtures/1diff.png b/test/fixtures/1diff.png index ef0cb15..bf874eb 100644 Binary files a/test/fixtures/1diff.png and b/test/fixtures/1diff.png differ diff --git a/test/fixtures/2diff.png b/test/fixtures/2diff.png index fd844ec..f075b7d 100644 Binary files a/test/fixtures/2diff.png and b/test/fixtures/2diff.png differ diff --git a/test/fixtures/3diff.png b/test/fixtures/3diff.png index f9a751e..6a9c82b 100644 Binary files a/test/fixtures/3diff.png and b/test/fixtures/3diff.png differ diff --git a/test/fixtures/4diff.png b/test/fixtures/4diff.png index cc4da06..ce533ec 100644 Binary files a/test/fixtures/4diff.png and b/test/fixtures/4diff.png differ diff --git a/test/test.js b/test/test.js index b551cdf..bf84c99 100644 --- a/test/test.js +++ b/test/test.js @@ -6,10 +6,10 @@ var PNG = require('pngjs2').PNG, path = require('path'), match = require('../.'); -diffTest('1a', '1b', '1diff', 0.03, false, 144); -diffTest('2a', '2b', '2diff', 0.03, false, 12785); -diffTest('3a', '3b', '3diff', 0.03, false, 212); -diffTest('4a', '4b', '4diff', 0.03, false, 36383); +diffTest('1a', '1b', '1diff', 0.05, false, 143); +diffTest('2a', '2b', '2diff', 0.05, false, 12439); +diffTest('3a', '3b', '3diff', 0.05, false, 212); +diffTest('4a', '4b', '4diff', 0.05, false, 36089); function diffTest(imgPath1, imgPath2, diffPath, threshold, includeAA, expectedMismatch) { var name = 'comparing ' + imgPath1 + ' to ' + imgPath2 +