Skip to content

Commit

Permalink
Merge pull request #6 from happo/yic
Browse files Browse the repository at this point in the history
Replace euclidean distance with YIC NTSC
  • Loading branch information
lencioni authored Nov 14, 2019
2 parents ddf3454 + 901907e commit f5b80cb
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 31 deletions.
Binary file modified snapshots/logo/diff.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 snapshots/long-example/diff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions src/__tests__/colorDelta-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const colorDelta = require('../colorDelta');

it('is large when comparing black and white', () => {
expect(colorDelta([0, 0, 0, 255], [255, 255, 255, 255]))
.toBeGreaterThan(0.92);
});

it('is small when comparing black and very dark grey', () => {
expect(colorDelta([0, 0, 0, 255], [10, 10, 10, 255]))
.toBeLessThan(0.02);
});

it('is medium when comparing black and medium grey', () => {
const delta = colorDelta([0, 0, 0, 255], [127, 127, 127, 255]);
expect(delta).toBeGreaterThan(0.21);
expect(delta).toBeLessThan(0.24);
});

it('is medium when comparing red and blue', () => {
const delta = colorDelta([255, 0, 0, 255], [0, 0, 255, 255]);
expect(delta).toBeGreaterThan(0.5);
expect(delta).toBeLessThan(0.51);
});

it('is zero when comparing transparent and white', () => {
expect(colorDelta([0, 0, 0, 0], [255, 255, 255, 255]))
.toEqual(0);
});

it('is large when comparing transparent and black', () => {
expect(colorDelta([0, 0, 0, 0], [0, 0, 0, 255]))
.toBeGreaterThan(0.92);
});
6 changes: 3 additions & 3 deletions src/__tests__/createDiffImage-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ beforeEach(async () => {
});

it('has a total diff value and a max diff', async () => {
const { maxDiff, diff } = await subject();
expect(maxDiff).toEqual(0.027169424432452977);
expect(diff).toEqual(0.00043751263781383705);
const { diff, maxDiff } = await subject();
expect(diff).toEqual(0.000013924627638992437);
expect(maxDiff).toEqual(0.0009183359547574563);
});
6 changes: 3 additions & 3 deletions src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ it('produces a trace svg', () => {
});

it('has meta-data', () => {
const img = subject();
expect(img.diff).toEqual(0.1991598705880487);
expect(img.maxDiff).toEqual(1);
const { diff, maxDiff } = subject();
expect(diff).toEqual(0.049155430620799745);
expect(maxDiff).toEqual(0.7809273602519097);
});
7 changes: 7 additions & 0 deletions src/__tests__/snapshots-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,30 @@ describe('snapshot tests', () => {
Jimp.read(path.resolve('snapshots', snapshot, 'after.png')),
]);
console.log('Images ready', snapshot);

const diffImage = imageDiff(image1.bitmap, image2.bitmap, {
hashFunction,
});

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

// To update diff images when making changes, delete the existing diff.png
// files and run this test again.
//
// find snapshots -name diff.png | xargs rm
if (!fs.existsSync(pathToDiff)) {
console.log(
`No previous diff image for ${snapshot} found -- saving diff.png.`,
);
const newDiffImage = await new Jimp(diffImage);
await newDiffImage.write(pathToDiff);
}

const expectedDiffImage = (await Jimp.read(pathToDiff)).bitmap;
const diffHash = hashFunction(diffImage.data);
const expectedHash = hashFunction(expectedDiffImage.data);

if (diffHash !== expectedHash) {
console.log(
`Diff image did not match existing diff image. Remove this image and run again to re-generate:\n${pathToDiff}`,
Expand Down
52 changes: 52 additions & 0 deletions src/colorDelta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const MAX_YIQ_DIFFERENCE = 35215;

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.2741761 - b * 0.32180189;
}

function rgb2q(r, g, b) {
return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
}

// blend semi-transparent color with white
function blend(color, alpha) {
return 255 + (color - 255) * alpha;
}

// 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
//
// Modified from https://github.com/mapbox/pixelmatch
module.exports = function colorDelta(previousPixel, currentPixel) {
let [r1, g1, b1, a1] = previousPixel;
let [r2, g2, b2, a2] = currentPixel;

if (r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2) {
return 0;
}

if (a1 < 255) {
a1 /= 255;
r1 = blend(r1, a1);
g1 = blend(g1, a1);
b1 = blend(b1, a1);
}

if (a2 < 255) {
a2 /= 255;
r2 = blend(r2, a2);
g2 = blend(g2, a2);
b2 = blend(b2, a2);
}

const y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
const i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
const q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);

return (0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q) / MAX_YIQ_DIFFERENCE;
};
14 changes: 0 additions & 14 deletions src/euclideanDistance.js

This file was deleted.

13 changes: 2 additions & 11 deletions src/getDiffPixel.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
const compose = require('./compose');
const euclideanDistance = require('./euclideanDistance');
const colorDelta = require('./colorDelta');

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

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

0 comments on commit f5b80cb

Please sign in to comment.