Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 645304x 645304x 148008x 148008x 148008x 148008x 8x 8x 8x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 645304x 645304x 645304x 645304x 645304x 645304x 19936x 19936x 19936x 19936x 19936x 19936x 19936x 625368x 625368x 625368x 625368x 625368x 625368x 625368x 645304x 645304x 8x 8x 8x 8x 8x 8x 8x 8x 1x 1x 1x 1x 1x 40x 40x 40x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 645304x 645304x 645304x 645304x 645304x 645304x 1x 1x 1x 1x 1x 1x 1x 1x 625368x 625368x 625368x 625368x 625368x 625368x 625368x 625368x 625368x 1x 1x 1x 1x 1x 1x 1x 1x 1x 16x 16x 16x 16x 16x 16x 16x | import { colorDeltaImgPosition, validAlgorithms } from './color-delta.js' import { rgb2y } from './color-metrics.js' import { colorOrFallbackColorToRGBA } from './html-color-to-rgba.js' /** @import {Algorithm} from './color-delta.js' */ export const fallbackAAColor = 'yellow' export const fallbackDiffColor = 'red' /** * @param {object} params - function parameters * @param {Uint8Array | Uint8ClampedArray} params.img1 - original image * @param {Uint8Array | Uint8ClampedArray} params.img2 - image to compare * @param {number} params.width - images width * @param {number} params.height - images height * @param {Algorithm} [params.algorithm] - output image * @returns {{identical: boolean, diffMap: Uint8Array}} number of different pixels */ export function getNormalizedDiffs ({ img1, img2, width, height, algorithm = 'CIEDE2000' }) { validateImagePreconditions({ img1, img2, width, height }) if (!validAlgorithms.includes(algorithm)) { throw new Error(`Invalid algorithm ${algorithm}, expected algorithms: ${validAlgorithms.join(', ')}.`) } const len = width * height const diffMap = new Uint8Array(len) const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len) const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len) let identical = true for (let pixelPos = 0; pixelPos < len; pixelPos++) { if (a32[pixelPos] === b32[pixelPos]) { continue } // Fast way to check if pixels are identical const pos = pixelPos * 4 const { delta, maxDelta } = colorDeltaImgPosition(img1, img2, pos, pos, algorithm) diffMap[pixelPos] = Math.ceil(Math.abs(delta) * 100 / maxDelta) identical = false } return { identical, diffMap } } /** * Gets antialias map for non-identical pixels * * The result is an byte array, where 3 out of 8 bits are used ( e.g. 1100001 ) * first bit just tells if it was worked on, useful when calculating partial antialias map and recalculate again * second bit just tells if it was calculated or ignored, no antialias is calculated on identical pixels * last bit tells if the pixel is an antialias pixel * * @param {object} params - function parameters * @param {Uint8Array | Uint8ClampedArray} params.img1 - original image * @param {Uint8Array | Uint8ClampedArray} params.img2 - image to compare * @param {number} params.width - images width * @param {number} params.height - images height * @returns {Uint8Array} anti alias map */ export function getAntiAliasMap (params) { validateImagePreconditions(params) const { img1, img2, width, height } = params const len = width * height const antiAliasMap = new Uint8Array(len) const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len) const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pos = y * width + x if (a32[pos] === b32[pos]) { // Fast way to check if pixels are identical antiAliasMap[pos] = 0b1000_0000 } else if (antialiased(img1, x, y, width, height, img2) || antialiased(img2, x, y, width, height, img1)) { antiAliasMap[pos] = 0b1100_0001 } else { antiAliasMap[pos] = 0b1100_0000 } } } return antiAliasMap } /** * @param {object} params - function parameters * @param {Uint8Array | Uint8ClampedArray} params.img1 - original image * @param {Uint8Array | Uint8ClampedArray} params.img2 - image to compare * @param {number} params.width - images width * @param {number} params.height - images height * @param {Uint8Array | Uint8ClampedArray | null} [params.output] - output image * @param {Uint8Array | Uint8ClampedArray | null} [params.diffMapOutput] - output for difference map, * useful if you need a separate image for antialias or diff * @param {number} [params.threshold] - threshold value, integer number between 0 and 100, both * inclusive, if the difference is below or equal the threshold, it is considered similar * pixel, and will not count as a different pixel * @param {boolean} [params.antialias] - whether to include anti-aliasing detection * @param {string} [params.aaColor] - color of anti-aliased pixels in diff output * @param {string} [params.diffColor] - color of different pixels in diff output * @param {boolean} [params.diffMask] - whether to include anti-aliasing detection * @param {number} [params.alpha] - // opacity of original image in diff output * @returns {{diffPixelAmount: number, aaPixelAmount: number}} number of different pixels */ export function calculateDiff ({ img1, img2, output, diffMapOutput, width, height, threshold = 10, antialias = false, aaColor = fallbackAAColor, diffColor = fallbackDiffColor, diffMask = false, alpha = 0.1, }) { validateImagePreconditions({ img1, img2, width, height, output }) if (diffMapOutput && diffMapOutput.length !== width * height) { throw new Error('diffMapOutput data size does not match width/height.') } if (img1.length !== img2.length || (output && output.length !== img1.length)) { throw new Error('Image sizes do not match.') } const { identical, diffMap } = getNormalizedDiffs({ img1, img2, width, height, algorithm: 'CIEDE2000' }) if (identical) { // fast path if identical if (output && !diffMask) { for (let i = 0, len = width * height; i < len; i++) drawGrayPixel(img1, 4 * i, alpha, output) } return { diffPixelAmount: 0, aaPixelAmount: 0, } } const antiAliasMap = antialias ? getAntiAliasMap({ img1, img2, width, height }) : null const [aaR, aaG, aaB] = colorOrFallbackColorToRGBA(aaColor, fallbackAAColor) const [diffR, diffG, diffB] = colorOrFallbackColorToRGBA(diffColor, fallbackDiffColor) let diffPixelAmount = 0 let aaPixelAmount = 0 // compare each pixel of one image against the other one for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pos = (y * width + x) * 4 const normalizedDelta = diffMap[y * width + x] // the color difference is above the threshold if (normalizedDelta > threshold) { // check it's a real rendering difference or just anti-aliasing if (antiAliasMap && (antiAliasMap[y * width + x] & 1) === 1) { // one of the pixels is anti-aliasing; draw as yellow and do not count as difference if (output) { drawPixel(output, pos, aaR, aaG, aaB) } if (diffMapOutput) { diffMapOutput[y * width + x] = 0b0011 } aaPixelAmount++ } else { // found substantial difference not caused by anti-aliasing; draw it as such if (output) { drawPixel(output, pos, diffR, diffG, diffB) } if (diffMapOutput) { diffMapOutput[y * width + x] = 0b0001 } diffPixelAmount++ } } else { if (output && !diffMask) { // pixels are similar; draw background as grayscale image blended with white drawGrayPixel(img1, pos, alpha, output) } if (diffMapOutput) { diffMapOutput[y * width + x] = 0 } } } } // return the number of different pixels return { diffPixelAmount, aaPixelAmount, } } /** * Checks if `arr` is an 8-bit unsigned integer typed array * @param {ArrayBufferView} arr - target object */ function isPixelData (arr) { return arr instanceof Uint8Array || arr instanceof Uint8ClampedArray } /** * check if a pixel is likely a part of anti-aliasing; * based on "Anti-aliased Pixel and Intensity Slope Detector" paper by V. Vysniauskas, 2009 * @param {Uint8Array | Uint8ClampedArray} img - original image * @param {number} x1 - pixel horizontal position * @param {number} y1 - pixel vertical position * @param {number} width - images width * @param {number} height - images height * @param {Uint8Array | Uint8ClampedArray} img2 - image to compare */ function antialiased (img, x1, y1, width, height, img2) { const x0 = Math.max(x1 - 1, 0) const y0 = Math.max(y1 - 1, 0) const x2 = Math.min(x1 + 1, width - 1) const y2 = Math.min(y1 + 1, height - 1) const pos = (y1 * width + x1) * 4 let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0 let min = 0 let max = 0 let minX = width let minY = height let maxX = 0 let maxY = 0 // go through 8 adjacent pixels for (let x = x0; x <= x2; x++) { for (let y = y0; y <= y2; y++) { if (x === x1 && y === y1) continue // brightness delta between the center pixel and adjacent one const { delta } = colorDeltaImgPosition(img, img, pos, (y * width + x) * 4, 'Brightness') // count the number of equal, darker and brighter adjacent pixels if (delta === 0) { zeroes++ // if found more than 2 equal siblings, it's definitely not anti-aliasing if (zeroes > 2) return false // remember the darkest pixel } else if (delta < min) { min = delta minX = x minY = y // remember the brightest pixel } else if (delta > max) { max = delta maxX = x maxY = y } } } // if there are no both darker and brighter pixels among siblings, it's not anti-aliasing if (min === 0 || max === 0) return false // if either the darkest or the brightest pixel has 3+ equal siblings in both images // (definitely not anti-aliased), this pixel is anti-aliased return (hasManySiblings(img, minX, minY, width, height) && hasManySiblings(img2, minX, minY, width, height)) || (hasManySiblings(img, maxX, maxY, width, height) && hasManySiblings(img2, maxX, maxY, width, height)) } // check if a pixel has 3+ adjacent pixels of the same color. /** * * @param {Uint8Array | Uint8ClampedArray} img - original image * @param {number} x1 - pixel horizontal position * @param {number} y1 - pixel vertical position * @param {number} width - images width * @param {number} height - images height */ function hasManySiblings (img, x1, y1, width, height) { const x0 = Math.max(x1 - 1, 0) const y0 = Math.max(y1 - 1, 0) const x2 = Math.min(x1 + 1, width - 1) const y2 = Math.min(y1 + 1, height - 1) const pos = (y1 * width + x1) * 4 let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0 // go through 8 adjacent pixels for (let x = x0; x <= x2; x++) { for (let y = y0; y <= y2; y++) { if (x === x1 && y === y1) continue const pos2 = (y * width + x) * 4 if (img[pos] === img[pos2] && img[pos + 1] === img[pos2 + 1] && img[pos + 2] === img[pos2 + 2] && img[pos + 3] === img[pos2 + 3]) zeroes++ if (zeroes > 2) return true } } return false } /** * * @param {Uint8Array | Uint8ClampedArray } output - output image * @param {number} pos - image position * @param {number} r - rgb red value * @param {number} g - rgb green value * @param {number} b - rgb blue value */ function drawPixel (output, pos, r, g, b) { output[pos + 0] = r output[pos + 1] = g output[pos + 2] = b output[pos + 3] = 255 } /** * * @param {Uint8Array | Uint8ClampedArray} img - original image * @param {number} pos - image position * @param {number} alpha - alpha value * @param {Uint8Array | Uint8ClampedArray } output - output image */ function drawGrayPixel (img, pos, alpha, output) { const r = img[pos + 0] const g = img[pos + 1] const b = img[pos + 2] const a = alpha * img[pos + 3] / 255 const brightness = rgb2y(r, g, b) const val = 255 + (brightness - 255) * a drawPixel(output, pos, val, val, val) } /** * @param {object} params - function parameters * @param {Uint8Array | Uint8ClampedArray} params.img1 - original image * @param {Uint8Array | Uint8ClampedArray} params.img2 - image to compare * @param {Uint8Array | Uint8ClampedArray | null} [params.output] - output image * @param {number} params.width - images width * @param {number} params.height - images height */ function validateImagePreconditions ({ img1, img2, width, height, output }) { if (!isPixelData(img1) || !isPixelData(img2) || (output && !isPixelData(output))) { throw new Error('Image data: Uint8Array or Uint8ClampedArray expected.') } if (img1.length !== img2.length) { throw new Error('Image sizes do not match.') } if (img1.length !== width * height * 4) { throw new Error('Image data size does not match width/height.') } } |