|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +function pixelmatch(img1, img2, output, width, height, options) { |
| 4 | + |
| 5 | + if (!options) options = {}; |
| 6 | + |
| 7 | + var threshold = options.threshold === undefined ? 0.1 : options.threshold; |
| 8 | + |
| 9 | + // maximum acceptable square distance between two colors; |
| 10 | + // 35215 is the maximum possible value for the YIQ difference metric |
| 11 | + var maxDelta = 35215 * threshold * threshold, |
| 12 | + diff = 0; |
| 13 | + |
| 14 | + // compare each pixel of one image against the other one |
| 15 | + for (var y = 0; y < height; y++) { |
| 16 | + for (var x = 0; x < width; x++) { |
| 17 | + |
| 18 | + var pos = (y * width + x) * 4; |
| 19 | + |
| 20 | + // squared YUV distance between colors at this pixel position |
| 21 | + var delta = colorDelta(img1, img2, pos, pos); |
| 22 | + |
| 23 | + // the color difference is above the threshold |
| 24 | + if (delta > maxDelta) { |
| 25 | + // check it's a real rendering difference or just anti-aliasing |
| 26 | + if (!options.includeAA && (antialiased(img1, x, y, width, height, img2) || |
| 27 | + antialiased(img2, x, y, width, height, img1))) { |
| 28 | + // one of the pixels is anti-aliasing; draw as yellow and do not count as difference |
| 29 | + if (output) drawPixel(output, pos, 255, 255, 0); |
| 30 | + |
| 31 | + } else { |
| 32 | + // found substantial difference not caused by anti-aliasing; draw it as red |
| 33 | + if (output) drawPixel(output, pos, 255, 0, 0); |
| 34 | + diff++; |
| 35 | + } |
| 36 | + |
| 37 | + } else if (output) { |
| 38 | + // pixels are similar; draw background as grayscale image blended with white |
| 39 | + var val = blend(grayPixel(img1, pos), 0.1); |
| 40 | + drawPixel(output, pos, val, val, val); |
| 41 | + } |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + // return the number of different pixels |
| 46 | + return diff; |
| 47 | +} |
| 48 | + |
| 49 | +// check if a pixel is likely a part of anti-aliasing; |
| 50 | +// based on "Anti-aliased Pixel and Intensity Slope Detector" paper by V. Vysniauskas, 2009 |
| 51 | + |
| 52 | +function antialiased(img, x1, y1, width, height, img2) { |
| 53 | + var x0 = Math.max(x1 - 1, 0), |
| 54 | + y0 = Math.max(y1 - 1, 0), |
| 55 | + x2 = Math.min(x1 + 1, width - 1), |
| 56 | + y2 = Math.min(y1 + 1, height - 1), |
| 57 | + pos = (y1 * width + x1) * 4, |
| 58 | + zeroes = 0, |
| 59 | + positives = 0, |
| 60 | + negatives = 0, |
| 61 | + min = 0, |
| 62 | + max = 0, |
| 63 | + minX, minY, maxX, maxY; |
| 64 | + |
| 65 | + // go through 8 adjacent pixels |
| 66 | + for (var x = x0; x <= x2; x++) { |
| 67 | + for (var y = y0; y <= y2; y++) { |
| 68 | + if (x === x1 && y === y1) continue; |
| 69 | + |
| 70 | + // brightness delta between the center pixel and adjacent one |
| 71 | + var delta = colorDelta(img, img, pos, (y * width + x) * 4, true); |
| 72 | + |
| 73 | + // count the number of equal, darker and brighter adjacent pixels |
| 74 | + if (delta === 0) zeroes++; |
| 75 | + else if (delta < 0) negatives++; |
| 76 | + else if (delta > 0) positives++; |
| 77 | + |
| 78 | + // if found more than 2 equal siblings, it's definitely not anti-aliasing |
| 79 | + if (zeroes > 2) return false; |
| 80 | + |
| 81 | + if (!img2) continue; |
| 82 | + |
| 83 | + // remember the darkest pixel |
| 84 | + if (delta < min) { |
| 85 | + min = delta; |
| 86 | + minX = x; |
| 87 | + minY = y; |
| 88 | + } |
| 89 | + // remember the brightest pixel |
| 90 | + if (delta > max) { |
| 91 | + max = delta; |
| 92 | + maxX = x; |
| 93 | + maxY = y; |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + if (!img2) return true; |
| 99 | + |
| 100 | + // if there are no both darker and brighter pixels among siblings, it's not anti-aliasing |
| 101 | + if (negatives === 0 || positives === 0) return false; |
| 102 | + |
| 103 | + // if either the darkest or the brightest pixel has more than 2 equal siblings in both images |
| 104 | + // (definitely not anti-aliased), this pixel is anti-aliased |
| 105 | + return (!antialiased(img, minX, minY, width, height) && !antialiased(img2, minX, minY, width, height)) || |
| 106 | + (!antialiased(img, maxX, maxY, width, height) && !antialiased(img2, maxX, maxY, width, height)); |
| 107 | +} |
| 108 | + |
| 109 | +// calculate color difference according to the paper "Measuring perceived color difference |
| 110 | +// using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos |
| 111 | + |
| 112 | +function colorDelta(img1, img2, k, m, yOnly) { |
| 113 | + var a1 = img1[k + 3] / 255, |
| 114 | + a2 = img2[m + 3] / 255, |
| 115 | + |
| 116 | + r1 = blend(img1[k + 0], a1), |
| 117 | + g1 = blend(img1[k + 1], a1), |
| 118 | + b1 = blend(img1[k + 2], a1), |
| 119 | + |
| 120 | + r2 = blend(img2[m + 0], a2), |
| 121 | + g2 = blend(img2[m + 1], a2), |
| 122 | + b2 = blend(img2[m + 2], a2), |
| 123 | + |
| 124 | + y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2); |
| 125 | + |
| 126 | + if (yOnly) return y; // brightness difference only |
| 127 | + |
| 128 | + var i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2), |
| 129 | + q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2); |
| 130 | + |
| 131 | + return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q; |
| 132 | +} |
| 133 | + |
| 134 | +function rgb2y(r, g, b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; } |
| 135 | +function rgb2i(r, g, b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; } |
| 136 | +function rgb2q(r, g, b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; } |
| 137 | + |
| 138 | +// blend semi-transparent color with white |
| 139 | +function blend(c, a) { |
| 140 | + return 255 + (c - 255) * a; |
| 141 | +} |
| 142 | + |
| 143 | +function drawPixel(output, pos, r, g, b) { |
| 144 | + output[pos + 0] = r; |
| 145 | + output[pos + 1] = g; |
| 146 | + output[pos + 2] = b; |
| 147 | + output[pos + 3] = 255; |
| 148 | +} |
| 149 | + |
| 150 | +function grayPixel(img, i) { |
| 151 | + var a = img[i + 3] / 255, |
| 152 | + r = blend(img[i + 0], a), |
| 153 | + g = blend(img[i + 1], a), |
| 154 | + b = blend(img[i + 2], a); |
| 155 | + return rgb2y(r, g, b); |
| 156 | +} |
0 commit comments