Skip to content

Commit f5b80cb

Browse files
authored
Merge pull request #6 from happo/yic
Replace euclidean distance with YIC NTSC
2 parents ddf3454 + 901907e commit f5b80cb

File tree

9 files changed

+100
-31
lines changed

9 files changed

+100
-31
lines changed

snapshots/logo/diff.png

137 Bytes
Loading

snapshots/long-example/diff.png

-193 KB
Loading

src/__tests__/colorDelta-test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const colorDelta = require('../colorDelta');
2+
3+
it('is large when comparing black and white', () => {
4+
expect(colorDelta([0, 0, 0, 255], [255, 255, 255, 255]))
5+
.toBeGreaterThan(0.92);
6+
});
7+
8+
it('is small when comparing black and very dark grey', () => {
9+
expect(colorDelta([0, 0, 0, 255], [10, 10, 10, 255]))
10+
.toBeLessThan(0.02);
11+
});
12+
13+
it('is medium when comparing black and medium grey', () => {
14+
const delta = colorDelta([0, 0, 0, 255], [127, 127, 127, 255]);
15+
expect(delta).toBeGreaterThan(0.21);
16+
expect(delta).toBeLessThan(0.24);
17+
});
18+
19+
it('is medium when comparing red and blue', () => {
20+
const delta = colorDelta([255, 0, 0, 255], [0, 0, 255, 255]);
21+
expect(delta).toBeGreaterThan(0.5);
22+
expect(delta).toBeLessThan(0.51);
23+
});
24+
25+
it('is zero when comparing transparent and white', () => {
26+
expect(colorDelta([0, 0, 0, 0], [255, 255, 255, 255]))
27+
.toEqual(0);
28+
});
29+
30+
it('is large when comparing transparent and black', () => {
31+
expect(colorDelta([0, 0, 0, 0], [0, 0, 0, 255]))
32+
.toBeGreaterThan(0.92);
33+
});

src/__tests__/createDiffImage-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ beforeEach(async () => {
2727
});
2828

2929
it('has a total diff value and a max diff', async () => {
30-
const { maxDiff, diff } = await subject();
31-
expect(maxDiff).toEqual(0.027169424432452977);
32-
expect(diff).toEqual(0.00043751263781383705);
30+
const { diff, maxDiff } = await subject();
31+
expect(diff).toEqual(0.000013924627638992437);
32+
expect(maxDiff).toEqual(0.0009183359547574563);
3333
});

src/__tests__/index-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ it('produces a trace svg', () => {
3636
});
3737

3838
it('has meta-data', () => {
39-
const img = subject();
40-
expect(img.diff).toEqual(0.1991598705880487);
41-
expect(img.maxDiff).toEqual(1);
39+
const { diff, maxDiff } = subject();
40+
expect(diff).toEqual(0.049155430620799745);
41+
expect(maxDiff).toEqual(0.7809273602519097);
4242
});

src/__tests__/snapshots-test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,30 @@ describe('snapshot tests', () => {
3030
Jimp.read(path.resolve('snapshots', snapshot, 'after.png')),
3131
]);
3232
console.log('Images ready', snapshot);
33+
3334
const diffImage = imageDiff(image1.bitmap, image2.bitmap, {
3435
hashFunction,
3536
});
3637

3738
console.log('Created diff image', snapshot);
3839
const pathToDiff = path.resolve('snapshots', snapshot, 'diff.png');
3940

41+
// To update diff images when making changes, delete the existing diff.png
42+
// files and run this test again.
43+
//
44+
// find snapshots -name diff.png | xargs rm
4045
if (!fs.existsSync(pathToDiff)) {
4146
console.log(
4247
`No previous diff image for ${snapshot} found -- saving diff.png.`,
4348
);
4449
const newDiffImage = await new Jimp(diffImage);
4550
await newDiffImage.write(pathToDiff);
4651
}
52+
4753
const expectedDiffImage = (await Jimp.read(pathToDiff)).bitmap;
4854
const diffHash = hashFunction(diffImage.data);
4955
const expectedHash = hashFunction(expectedDiffImage.data);
56+
5057
if (diffHash !== expectedHash) {
5158
console.log(
5259
`Diff image did not match existing diff image. Remove this image and run again to re-generate:\n${pathToDiff}`,

src/colorDelta.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const MAX_YIQ_DIFFERENCE = 35215;
2+
3+
function rgb2y(r, g, b) {
4+
return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
5+
}
6+
7+
function rgb2i(r, g, b) {
8+
return r * 0.59597799 - g * 0.2741761 - b * 0.32180189;
9+
}
10+
11+
function rgb2q(r, g, b) {
12+
return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
13+
}
14+
15+
// blend semi-transparent color with white
16+
function blend(color, alpha) {
17+
return 255 + (color - 255) * alpha;
18+
}
19+
20+
// calculate color difference according to the paper "Measuring perceived color
21+
// difference using YIQ NTSC transmission color space in mobile applications" by
22+
// Y. Kotsarenko and F. Ramos
23+
//
24+
// Modified from https://github.com/mapbox/pixelmatch
25+
module.exports = function colorDelta(previousPixel, currentPixel) {
26+
let [r1, g1, b1, a1] = previousPixel;
27+
let [r2, g2, b2, a2] = currentPixel;
28+
29+
if (r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2) {
30+
return 0;
31+
}
32+
33+
if (a1 < 255) {
34+
a1 /= 255;
35+
r1 = blend(r1, a1);
36+
g1 = blend(g1, a1);
37+
b1 = blend(b1, a1);
38+
}
39+
40+
if (a2 < 255) {
41+
a2 /= 255;
42+
r2 = blend(r2, a2);
43+
g2 = blend(g2, a2);
44+
b2 = blend(b2, a2);
45+
}
46+
47+
const y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
48+
const i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
49+
const q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
50+
51+
return (0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q) / MAX_YIQ_DIFFERENCE;
52+
};

src/euclideanDistance.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/getDiffPixel.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
const compose = require('./compose');
2-
const euclideanDistance = require('./euclideanDistance');
2+
const colorDelta = require('./colorDelta');
33

44
const TRANSPARENT = [0, 0, 0, 0];
55

66
module.exports = function getDiffPixel(previousPixel, currentPixel) {
77
// Compute a score that represents the difference between 2 pixels
8-
//
9-
// This method simply takes the Euclidean distance between the RGBA channels
10-
// of 2 colors over the maximum possible Euclidean distance. This gives us a
11-
// percentage of how different the two colors are.
12-
//
13-
// Although it would be more perceptually accurate to calculate a proper
14-
// Delta E in Lab colorspace, we probably don't need perceptual accuracy for
15-
// this application, and it is nice to avoid the overhead of converting RGBA
16-
// to Lab.
17-
const diff = euclideanDistance(previousPixel, currentPixel);
8+
const diff = colorDelta(previousPixel, currentPixel);
189
if (diff === 0) {
1910
if (currentPixel[3] === 0) {
2011
return {

0 commit comments

Comments
 (0)