Skip to content

Commit 7265115

Browse files
committed
Merge pull request #7 from mapbox/yiq-color
YIQ-based color metric
2 parents eed59ff + 34ab682 commit 7265115

File tree

7 files changed

+47
-33
lines changed

7 files changed

+47
-33
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
The smallest, simplest and fastest JavaScript pixel-level image comparison library,
88
originally created to compare screenshots in tests.
99

10-
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)
11-
and [perceptual color metrics](https://en.wikipedia.org/wiki/YUV).
10+
Features accurate **anti-aliased pixels detection**
11+
and **perceptual color difference metrics**.
1212

1313
Inspired by [Resemble.js](https://github.com/Huddle/Resemble.js)
1414
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
2020
var numDiffPixels = pixelmatch(img1, img2, diff, 800, 600, {threshold: 0.1});
2121
```
2222

23+
Implements ideas from the following papers:
24+
25+
- [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)
26+
- [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)
27+
2328
### Example output
2429

2530
| expected | actual | diff |

index.js

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ function pixelmatch(img1, img2, output, width, height, options) {
88

99
var threshold = options.threshold === undefined ? 0.1 : options.threshold;
1010

11-
// maximum acceptable square YUV distance between two colors
12-
var maxDelta = 255 * 255 * 3 * threshold * threshold,
11+
// maximum acceptable square distance between two colors;
12+
// 35215 is the maximum possible value for the YIQ difference metric
13+
var maxDelta = 35215 * threshold * threshold,
1314
diff = 0;
1415

1516
// compare each pixel of one image against the other one
@@ -35,10 +36,10 @@ function pixelmatch(img1, img2, output, width, height, options) {
3536
diff++;
3637
}
3738

38-
} else {
39+
} else if (output) {
3940
// pixels are similar; draw background as grayscale image blended with white
40-
var val = 255 - 0.1 * (255 - grayPixel(img1, pos)) * img1[pos + 3] / 255;
41-
if (output) drawPixel(output, pos, val, val, val);
41+
var val = blend(grayPixel(img1, pos), 0.1);
42+
drawPixel(output, pos, val, val, val);
4243
}
4344
}
4445
}
@@ -107,32 +108,38 @@ function antialiased(img, x1, y1, width, height, img2) {
107108
(!antialiased(img, maxX, maxY, width, height) && !antialiased(img2, maxX, maxY, width, height));
108109
}
109110

110-
// calculate either the squared YUV distance between colors,
111-
// or just the brightness differene (Y component) if yOnly is true
111+
// calculate color difference according to the paper "Measuring perceived color difference
112+
// using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos
112113

113-
function colorDelta(img1, img2, i, j, yOnly) {
114-
var a1 = img1[i + 3] / 255,
115-
a2 = img2[j + 3] / 255,
114+
function colorDelta(img1, img2, k, m, yOnly) {
115+
var a1 = img1[k + 3] / 255,
116+
a2 = img2[m + 3] / 255,
116117

117-
r1 = img1[i + 0] * a1,
118-
g1 = img1[i + 1] * a1,
119-
b1 = img1[i + 2] * a1,
118+
r1 = blend(img1[k + 0], a1),
119+
g1 = blend(img1[k + 1], a1),
120+
b1 = blend(img1[k + 2], a1),
120121

121-
r2 = img2[j + 0] * a2,
122-
g2 = img2[j + 1] * a2,
123-
b2 = img2[j + 2] * a2,
122+
r2 = blend(img2[m + 0], a2),
123+
g2 = blend(img2[m + 1], a2),
124+
b2 = blend(img2[m + 2], a2),
124125

125-
y1 = 0.299 * r1 + 0.587 * g1 + 0.114 * b1,
126-
y2 = 0.299 * r2 + 0.587 * g2 + 0.114 * b2,
126+
y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
127127

128-
yd = y1 - y2;
128+
if (yOnly) return y; // brightness difference only
129129

130-
if (yOnly) return yd;
130+
var i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2),
131+
q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
131132

132-
var ud = 0.492 * (b1 - y1) - 0.492 * (b2 - y2),
133-
vd = 0.877 * (r1 - y1) - 0.877 * (r2 - y2);
133+
return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
134+
}
135+
136+
function rgb2y(r, g, b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; }
137+
function rgb2i(r, g, b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; }
138+
function rgb2q(r, g, b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; }
134139

135-
return (yd * yd) + (ud * ud) + (vd * vd);
140+
// blend semi-transparent color with white
141+
function blend(c, a) {
142+
return 255 + (c - 255) * a;
136143
}
137144

138145
function drawPixel(output, pos, r, g, b) {
@@ -142,8 +149,10 @@ function drawPixel(output, pos, r, g, b) {
142149
output[pos + 3] = 255;
143150
}
144151

145-
function grayPixel(img, pos) {
146-
return 0.30 * img[pos + 0] +
147-
0.59 * img[pos + 1] +
148-
0.11 * img[pos + 2];
152+
function grayPixel(img, i) {
153+
var a = img[i + 3] / 255,
154+
r = blend(img[i + 0], a),
155+
g = blend(img[i + 1], a),
156+
b = blend(img[i + 2], a);
157+
return rgb2y(r, g, b);
149158
}

test/fixtures/1diff.png

-74 Bytes
Loading

test/fixtures/2diff.png

210 Bytes
Loading

test/fixtures/3diff.png

236 Bytes
Loading

test/fixtures/4diff.png

73 Bytes
Loading

test/test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ var PNG = require('pngjs2').PNG,
66
path = require('path'),
77
match = require('../.');
88

9-
diffTest('1a', '1b', '1diff', 0.03, false, 144);
10-
diffTest('2a', '2b', '2diff', 0.03, false, 12785);
11-
diffTest('3a', '3b', '3diff', 0.03, false, 212);
12-
diffTest('4a', '4b', '4diff', 0.03, false, 36383);
9+
diffTest('1a', '1b', '1diff', 0.05, false, 143);
10+
diffTest('2a', '2b', '2diff', 0.05, false, 12439);
11+
diffTest('3a', '3b', '3diff', 0.05, false, 212);
12+
diffTest('4a', '4b', '4diff', 0.05, false, 36089);
1313

1414
function diffTest(imgPath1, imgPath2, diffPath, threshold, includeAA, expectedMismatch) {
1515
var name = 'comparing ' + imgPath1 + ' to ' + imgPath2 +

0 commit comments

Comments
 (0)