Skip to content

Commit b417567

Browse files
committed
optimization pass (8% faster)
1 parent 4094e6b commit b417567

1 file changed

Lines changed: 75 additions & 43 deletions

File tree

index.js

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function pixelmatch(img1, img2, output, width, height, options =
4646
}
4747
if (identical) { // fast path if identical
4848
if (output && !diffMask) {
49-
for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, alpha, output);
49+
for (let i = 0, pos = 0; i < len; i++, pos += 4) drawGrayPixel(img1, pos, alpha, output);
5050
}
5151
return 0;
5252
}
@@ -60,40 +60,36 @@ export default function pixelmatch(img1, img2, output, width, height, options =
6060
let diff = 0;
6161

6262
// compare each pixel of one image against the other one
63-
for (let y = 0; y < height; y++) {
64-
for (let x = 0; x < width; x++) {
65-
66-
const i = y * width + x;
67-
const pos = i * 4;
68-
69-
// squared YUV distance between colors at this pixel position, negative if the img2 pixel is darker
70-
const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, false);
71-
72-
// the color difference is above the threshold
73-
if (Math.abs(delta) > maxDelta) {
74-
// check it's a real rendering difference or just anti-aliasing
75-
const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32));
76-
if (isExcludedAA) {
77-
// one of the pixels is anti-aliasing; draw as yellow and do not count as difference
78-
// note that we do not include such pixels in a mask
79-
if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);
80-
81-
} else {
82-
// found substantial difference not caused by anti-aliasing; draw it as such
83-
if (output) {
84-
if (delta < 0) {
85-
drawPixel(output, pos, altR, altG, altB);
86-
} else {
87-
drawPixel(output, pos, diffR, diffG, diffB);
88-
}
63+
for (let i = 0, pos = 0; i < len; i++, pos += 4) {
64+
// squared YUV distance between colors at this pixel position, negative if the img2 pixel is darker
65+
const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos);
66+
67+
// the color difference is above the threshold
68+
if (Math.abs(delta) > maxDelta) {
69+
const x = i % width;
70+
const y = (i / width) | 0;
71+
// check it's a real rendering difference or just anti-aliasing
72+
const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32));
73+
if (isExcludedAA) {
74+
// one of the pixels is anti-aliasing; draw as yellow and do not count as difference
75+
// note that we do not include such pixels in a mask
76+
if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);
77+
78+
} else {
79+
// found substantial difference not caused by anti-aliasing; draw it as such
80+
if (output) {
81+
if (delta < 0) {
82+
drawPixel(output, pos, altR, altG, altB);
83+
} else {
84+
drawPixel(output, pos, diffR, diffG, diffB);
8985
}
90-
diff++;
9186
}
92-
93-
} else if (output && !diffMask) {
94-
// pixels are similar; draw background as grayscale image blended with white
95-
drawGrayPixel(img1, pos, alpha, output);
87+
diff++;
9688
}
89+
90+
} else if (output && !diffMask) {
91+
// pixels are similar; draw background as grayscale image blended with white
92+
drawGrayPixel(img1, pos, alpha, output);
9793
}
9894
}
9995

@@ -123,7 +119,12 @@ function antialiased(img, x1, y1, width, height, a32, b32) {
123119
const y0 = Math.max(y1 - 1, 0);
124120
const x2 = Math.min(x1 + 1, width - 1);
125121
const y2 = Math.min(y1 + 1, height - 1);
126-
const pos = y1 * width + x1;
122+
const pos4 = (y1 * width + x1) * 4;
123+
// cache the center pixel's RGBA once instead of re-reading it on every neighbor comparison
124+
const cr = img[pos4];
125+
const cg = img[pos4 + 1];
126+
const cb = img[pos4 + 2];
127+
const ca = img[pos4 + 3];
127128
let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
128129
let min = 0;
129130
let max = 0;
@@ -138,7 +139,7 @@ function antialiased(img, x1, y1, width, height, a32, b32) {
138139
if (x === x1 && y === y1) continue;
139140

140141
// brightness delta between the center pixel and adjacent one
141-
const delta = colorDelta(img, img, pos * 4, (y * width + x) * 4, true);
142+
const delta = brightnessDelta(img, pos4, (y * width + x) * 4, cr, cg, cb, ca);
142143

143144
// count the number of equal, darker and brighter adjacent pixels
144145
if (delta === 0) {
@@ -199,14 +200,14 @@ function hasManySiblings(img, x1, y1, width, height) {
199200

200201
/**
201202
* Calculate color difference according to the paper "Measuring perceived color difference
202-
* using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos
203+
* using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos.
204+
* Caller guarantees the two pixels differ, so the early-zero check is omitted.
203205
* @param {Uint8Array | Uint8ClampedArray} img1
204206
* @param {Uint8Array | Uint8ClampedArray} img2
205207
* @param {number} k
206208
* @param {number} m
207-
* @param {boolean} yOnly
208209
*/
209-
function colorDelta(img1, img2, k, m, yOnly) {
210+
function colorDelta(img1, img2, k, m) {
210211
const r1 = img1[k];
211212
const g1 = img1[k + 1];
212213
const b1 = img1[k + 2];
@@ -221,8 +222,6 @@ function colorDelta(img1, img2, k, m, yOnly) {
221222
let db = b1 - b2;
222223
const da = a1 - a2;
223224

224-
if (!dr && !dg && !db && !da) return 0;
225-
226225
if (a1 < 255 || a2 < 255) { // blend pixels with background
227226
const rb = 48 + 159 * (k % 2);
228227
const gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
@@ -233,9 +232,6 @@ function colorDelta(img1, img2, k, m, yOnly) {
233232
}
234233

235234
const y = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
236-
237-
if (yOnly) return y; // brightness difference only
238-
239235
const i = dr * 0.59597799 - dg * 0.27417610 - db * 0.32180189;
240236
const q = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
241237

@@ -245,6 +241,42 @@ function colorDelta(img1, img2, k, m, yOnly) {
245241
return y > 0 ? -delta : delta;
246242
}
247243

244+
/**
245+
* Specialized brightness-only color delta for the anti-aliasing detector,
246+
* with the center pixel's RGBA hoisted out of the neighbor loop.
247+
* @param {Uint8Array | Uint8ClampedArray} img
248+
* @param {number} k center pixel offset
249+
* @param {number} m neighbor pixel offset
250+
* @param {number} r1
251+
* @param {number} g1
252+
* @param {number} b1
253+
* @param {number} a1
254+
*/
255+
function brightnessDelta(img, k, m, r1, g1, b1, a1) {
256+
const r2 = img[m];
257+
const g2 = img[m + 1];
258+
const b2 = img[m + 2];
259+
const a2 = img[m + 3];
260+
261+
let dr = r1 - r2;
262+
let dg = g1 - g2;
263+
let db = b1 - b2;
264+
const da = a1 - a2;
265+
266+
if (!dr && !dg && !db && !da) return 0;
267+
268+
if (a1 < 255 || a2 < 255) {
269+
const rb = 48 + 159 * (k % 2);
270+
const gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
271+
const bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
272+
dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
273+
dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
274+
db = (b1 * a1 - b2 * a2 - bb * da) / 255;
275+
}
276+
277+
return dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
278+
}
279+
248280
/**
249281
* @param {Uint8Array | Uint8ClampedArray} output
250282
* @param {number} pos
@@ -253,7 +285,7 @@ function colorDelta(img1, img2, k, m, yOnly) {
253285
* @param {number} b
254286
*/
255287
function drawPixel(output, pos, r, g, b) {
256-
output[pos + 0] = r;
288+
output[pos] = r;
257289
output[pos + 1] = g;
258290
output[pos + 2] = b;
259291
output[pos + 3] = 255;

0 commit comments

Comments
 (0)