Skip to content

Commit

Permalink
Merge pull request #7 from mapbox/yiq-color
Browse files Browse the repository at this point in the history
YIQ-based color metric
  • Loading branch information
mourner committed Oct 23, 2015
2 parents eed59ff + 34ab682 commit 7265115
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 33 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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 |
Expand Down
63 changes: 36 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Binary file modified test/fixtures/1diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/2diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/3diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/4diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down

0 comments on commit 7265115

Please sign in to comment.