Skip to content

Commit aa60d78

Browse files
authored
Merge pull request #19 from gemini-testing/sipayrt.anti
Add ability to ignore image antialiasing
2 parents defcb39 + be1100e commit aa60d78

File tree

10 files changed

+202
-24
lines changed

10 files changed

+202
-24
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ looksSame('image1.png', 'image2.png', {ignoreCaret: true}, function(error, equal
5454
Both `strict` and `ignoreCaret` can be set independently of one another.
5555

5656
Some devices can have different proportion between physical and logical screen resolutions also
57-
known as `pixel ratio`. Default value for this proportion is 1.
57+
known as `pixel ratio`. Default value for this proportion is 1.
5858
This param also affects the comparison result, so it can be set manually with `pixelRatio` option.
5959

6060
```javascript
@@ -63,6 +63,14 @@ looksSame('image1.png', 'image2.png', {pixelRatio: 2}, function(error, equal) {
6363
});
6464
```
6565

66+
Some images has difference while comparing because of antialiasing. These diffs will be ignored by default. You can use `ignoreAntialiasing` option with `false` value to disable ignoring such diffs. In that way antialiased pixels will be marked as diffs. Read more about [anti-aliasing algorithm](http://www.eejournal.ktu.lt/index.php/elt/article/view/10058/5000).
67+
68+
```javascript
69+
looksSame('image1.png', 'image2.png', {ignoreAntialiasing: true}, function(error, equal) {
70+
...
71+
});
72+
```
73+
6674
## Building diff image
6775

6876
```javascript

index.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
var parseColor = require('parse-color'),
33
colorDiff = require('color-diff'),
44
png = require('./lib/png'),
5-
IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
5+
IgnoreCaretComparator = require('./lib/ignore-caret-comparator'),
6+
AntialiasingComparator = require('./lib/antialiasing-comparator');
67

78
var JND = 2.3; //Just noticable difference
89
//if ciede2000 >= JND then colors
@@ -44,7 +45,11 @@ function everyPixelPair(png1, png2, predicate, endCallback) {
4445
for (var x = 0; x < width; x++) {
4546
var color1 = png1.getPixel(x, y),
4647
color2 = png2.getPixel(x, y),
47-
result = predicate(color1, color2, x, y);
48+
result = predicate({
49+
color1, color2,
50+
x, y,
51+
width, height
52+
});
4853

4954
if (!result) {
5055
return endCallback(false);
@@ -70,35 +75,46 @@ function arePNGsLookSame(png1, png2, opts, callback) {
7075
}
7176

7277
var comparator = opts.strict? areColorsSame : makeCIEDE2000Comparator(opts.tolerance);
78+
if (opts.ignoreAntialiasing) {
79+
comparator = makeAntialiasingComparator(comparator, png1, png2);
80+
}
81+
7382
if (opts.ignoreCaret) {
7483
comparator = makeNoCaretColorComparator(comparator, opts.pixelRatio);
7584
}
7685

7786
everyPixelPair(png1, png2, comparator, callback);
7887
}
7988

89+
function makeAntialiasingComparator(comparator, png1, png2) {
90+
const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2);
91+
return (data) => antialiasingComparator.compare(data);
92+
}
93+
8094
function makeNoCaretColorComparator(comparator, pixelRatio) {
8195
const caretComparator = new IgnoreCaretComparator(comparator, pixelRatio);
82-
return (color1, color2, x, y) => caretComparator.compare(color1, color2, x, y);
96+
return (data) => caretComparator.compare(data);
8397
}
8498

8599
function makeCIEDE2000Comparator(tolerance) {
86-
return function doColorsLookSame(c1, c2) {
87-
if (areColorsSame(c1, c2)) {
100+
return function doColorsLookSame(data) {
101+
if (areColorsSame(data)) {
88102
return true;
89103
}
90104
/*jshint camelcase:false*/
91-
var lab1 = colorDiff.rgb_to_lab(c1),
92-
lab2 = colorDiff.rgb_to_lab(c2);
105+
var lab1 = colorDiff.rgb_to_lab(data.color1),
106+
lab2 = colorDiff.rgb_to_lab(data.color2);
93107

94108
return colorDiff.diff(lab1, lab2) < tolerance;
95109
};
96110
}
97111

98-
function areColorsSame(c1, c2) {
99-
return c1.R === c2.R &&
100-
c1.G === c2.G &&
101-
c1.B === c2.B;
112+
function areColorsSame(data) {
113+
const c1 = data.color1;
114+
const c2 = data.color2;
115+
return c1.R === c2.R
116+
&& c1.G === c2.G
117+
&& c1.B === c2.B;
102118
}
103119

104120
module.exports = exports = function looksSame(reference, image, opts, callback) {
@@ -109,6 +125,10 @@ module.exports = exports = function looksSame(reference, image, opts, callback)
109125

110126
opts.tolerance = getToleranceFromOpts(opts);
111127

128+
if (opts.ignoreAntialiasing === undefined) {
129+
opts.ignoreAntialiasing = true;
130+
}
131+
112132
readPair(reference, image, function(error, result) {
113133
if (error) {
114134
return callback(error, null);
@@ -136,7 +156,7 @@ function buildDiffImage(png1, png2, options, callback) {
136156
var color1 = png1.getPixel(x, y),
137157
color2 = png2.getPixel(x, y);
138158

139-
if (!options.comparator(color1, color2)) {
159+
if (!options.comparator({color1, color2})) {
140160
result.setPixel(x, y, highlightColor);
141161
} else {
142162
result.setPixel(x, y, color1);
@@ -211,5 +231,5 @@ exports.colors = function(color1, color2, opts) {
211231
opts.tolerance = JND;
212232
}
213233
var comparator = makeCIEDE2000Comparator(opts.tolerance);
214-
return comparator(color1, color2);
234+
return comparator({color1, color2});
215235
};

lib/antialiasing-comparator.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict';
2+
3+
/*
4+
* Anti-aliased pixel detector
5+
* @see http://www.eejournal.ktu.lt/index.php/elt/article/view/10058/5000
6+
*/
7+
8+
module.exports = class AntialiasingComparator {
9+
constructor(baseComparator, png1, png2) {
10+
this._baseComparator = baseComparator;
11+
this._img1 = png1;
12+
this._img2 = png2;
13+
}
14+
15+
compare(data) {
16+
return this._baseComparator(data) || this._checkIsAntialiased(data);
17+
}
18+
19+
_checkIsAntialiased(data) {
20+
return this._isAntialiased(this._img2, data.x, data.y, data, this._img1)
21+
|| this._isAntialiased(this._img1, data.x, data.y, data, this._img2);
22+
}
23+
24+
_isAntialiased(img1, x1, y1, data, img2) {
25+
const color1 = img1.getPixel(x1, y1);
26+
const width = data.width;
27+
const height = data.height;
28+
const x0 = Math.max(x1 - 1, 0);
29+
const y0 = Math.max(y1 - 1, 0);
30+
const x2 = Math.min(x1 + 1, width - 1);
31+
const y2 = Math.min(y1 + 1, height - 1);
32+
let zeroes = 0;
33+
let positives = 0;
34+
let negatives = 0;
35+
let min = 0;
36+
let max = 0;
37+
let minX, minY, maxX, maxY;
38+
39+
for (let y = y0; y <= y2; y++) {
40+
for (let x = x0; x <= x2; x++) {
41+
if (x === x1 && y === y1) continue;
42+
43+
// brightness delta between the center pixel and adjacent one
44+
const delta = this._brightnessDelta(img1.getPixel(x, y), color1);
45+
46+
// count the number of equal, darker and brighter adjacent pixels
47+
if (delta === 0) zeroes++;
48+
else if (delta < 0) negatives++;
49+
else if (delta > 0) positives++;
50+
51+
// if found more than 2 equal siblings, it's definitely not anti-aliasing
52+
if (zeroes > 2) {
53+
return false;
54+
}
55+
56+
if (!img2) continue;
57+
58+
// remember the darkest pixel
59+
if (delta < min) {
60+
min = delta;
61+
minX = x;
62+
minY = y;
63+
}
64+
// remember the brightest pixel
65+
if (delta > max) {
66+
max = delta;
67+
maxX = x;
68+
maxY = y;
69+
}
70+
}
71+
}
72+
73+
if (!img2) return true;
74+
75+
// if there are no both darker and brighter pixels among siblings, it's not anti-aliasing
76+
if (negatives === 0 || positives === 0) {
77+
return false;
78+
}
79+
80+
// if either the darkest or the brightest pixel has more than 2 equal siblings in both images
81+
// (definitely not anti-aliased), this pixel is anti-aliased
82+
return (!this._isAntialiased(img1, minX, minY, data) && !this._isAntialiased(img2, minX, minY, data)) ||
83+
(!this._isAntialiased(img1, maxX, maxY, data) && !this._isAntialiased(img2, maxX, maxY, data));
84+
}
85+
86+
_brightnessDelta(color1, color2) {
87+
// gamma-corrected luminance of a color (YIQ NTSC transmission color space)
88+
// see https://www.academia.edu/8200524/DIGITAL_IMAGE_PROCESSING_Digital_Image_Processing_PIKS_Inside_Third_Edition
89+
const rgb2y = (r, g, b) => r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
90+
91+
return rgb2y(color1.R, color1.G, color1.B) - rgb2y(color2.R, color2.G, color2.B);
92+
}
93+
};

lib/ignore-caret-comparator/index.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,17 @@ module.exports = class IgnoreCaretComparator {
3333

3434
/**
3535
* Compare pixels for current active comparator state
36-
* @param {Object} color1
37-
* @param {Object} color2
38-
* @param {Number} x coordinate
39-
* @param {Number} y coordinate
36+
* @param {Object} data
37+
* @param {Object} data.color1
38+
* @param {Object} data.color2
39+
* @param {Number} data.x coordinate
40+
* @param {Number} data.y coordinate
4041
* @returns {boolean}
4142
*/
42-
compare(color1, color2, x, y) {
43-
return this._baseComparator(color1, color2, x, y)
44-
? this._state.validateOnEqual({x, y})
45-
: this._state.validateOnDiff({x, y});
43+
compare(data) {
44+
return this._baseComparator(data)
45+
? this._state.validateOnEqual({x: data.x, y: data.y})
46+
: this._state.validateOnDiff({x: data.x, y: data.y});
4647
}
4748

4849
switchState(stateName) {

test/data/src/antialiasing-actual.png

17.5 KB
Loading

test/data/src/antialiasing-ref.png

17.5 KB
Loading

test/data/src/caret+antialiasing.png

15.2 KB
Loading
15.2 KB
Loading

test/ignore-caret-comparator.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function expectDeclined(params, pixels) {
122122
}
123123

124124
function execComparator(params, pixels) {
125-
const colorComparator = (color) => !color;
125+
const colorComparator = (data) => !data.color1;
126126
const ignoreCaretComparator = new IgnoreCaretComparator(colorComparator, params.pixelRatio);
127127
return comparePixels(pixels, ignoreCaretComparator.compare.bind(ignoreCaretComparator));
128128
}
@@ -131,7 +131,7 @@ function comparePixels(pixels, comparator) {
131131
let res = true;
132132
for(let y=0; y<pixels.length; ++y) {
133133
for(let x=0; x<pixels[y].length; ++x) {
134-
res = comparator(pixels[y][x], null, x, y);
134+
res = comparator({color1: pixels[y][x], x, y});
135135
if(!res) {
136136
break;
137137
}

test/test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,62 @@ describe('looksSame', function() {
136136
done();
137137
});
138138
});
139+
140+
it('if enabled, should return true for images with caret and antialiased pixels', function(done) {
141+
const opts = {
142+
ignoreCaret: true,
143+
ignoreAntialiasing: true
144+
};
145+
looksSame(getImage('caret+antialiasing.png'), getImage('no-caret+antialiasing.png'), opts, function(error, equal) {
146+
expect(error).to.equal(null);
147+
expect(equal).to.equal(true);
148+
done();
149+
});
150+
});
151+
152+
it('if enabled, should return false for images with 1px diff', function(done) {
153+
looksSame(getImage('no-caret.png'), getImage('1px-diff.png'), function(error, equal) {
154+
expect(error).to.equal(null);
155+
expect(equal).to.equal(false);
156+
done();
157+
});
158+
});
159+
});
160+
});
161+
162+
describe('with antialiasing', function() {
163+
forFilesAndBuffers(function(getImage) {
164+
it('should check images for antialiasing by default', function(done) {
165+
looksSame(getImage('antialiasing-ref.png'), getImage('antialiasing-actual.png'), function(error, equal) {
166+
expect(error).to.equal(null);
167+
expect(equal).to.equal(true);
168+
done();
169+
});
170+
});
171+
172+
it('if disabled, should return false for images with antialiasing', function(done) {
173+
looksSame(getImage('antialiasing-ref.png'), getImage('antialiasing-actual.png'), {ignoreAntialiasing: false}, function(error, equal) {
174+
expect(error).to.equal(null);
175+
expect(equal).to.equal(false);
176+
done();
177+
});
178+
});
179+
180+
it('if enabled, should return true for images with antialiasing', function(done) {
181+
looksSame(getImage('antialiasing-ref.png'), getImage('antialiasing-actual.png'), {ignoreAntialiasing: true}, function(error, equal) {
182+
expect(error).to.equal(null);
183+
expect(equal).to.equal(true);
184+
done();
185+
});
186+
});
187+
188+
it('should return false for images which differ even with ignore antialiasing option', function(done) {
189+
looksSame(getImage('no-caret.png'), getImage('1px-diff.png'), {ignoreAntialiasing: true}, function(error, equal) {
190+
expect(error).to.equal(null);
191+
expect(equal).to.equal(false);
192+
done();
193+
});
194+
});
139195
});
140196
});
141197
});

0 commit comments

Comments
 (0)