From 526bc3f2528468c1ad84286ec3597b67950aa67b Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Thu, 8 Dec 2022 16:08:41 -0800 Subject: [PATCH] fix(ssim-cie94): further tune SSIM-CIE94 to fight wk artifacts (#19370) This patch adds a grid border around the image so that the SSIM resolution doesn't drop for the border pixels. We also add a test with WebKit rendering artifacts to make sure new approach helps to fight this. --- .../src/image_tools/compare.ts | 26 ++++++--- .../src/image_tools/imageChannel.ts | 50 ++++++++++++------ .../webkit-corner-2x-actual.png | Bin 0 -> 2197 bytes .../webkit-corner-2x-expected.png | Bin 0 -> 2192 bytes 4 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 tests/image_tools/fixtures/should-match/webkit-rendering-artifacts/webkit-corner-2x-actual.png create mode 100644 tests/image_tools/fixtures/should-match/webkit-rendering-artifacts/webkit-corner-2x-expected.png diff --git a/packages/playwright-core/src/image_tools/compare.ts b/packages/playwright-core/src/image_tools/compare.ts index 4b79d36eb0..e1ffdc0766 100644 --- a/packages/playwright-core/src/image_tools/compare.ts +++ b/packages/playwright-core/src/image_tools/compare.ts @@ -37,22 +37,34 @@ export function compare(actual: Buffer, expected: Buffer, diff: Buffer, width: n const { maxColorDeltaE94 } = options; - const [r1, g1, b1] = ImageChannel.intoRGB(width, height, expected); - const [r2, g2, b2] = ImageChannel.intoRGB(width, height, actual); - const drawRedPixel = (x: number, y: number) => drawPixel(width, diff, x, y, 255, 0, 0); - const drawYellowPixel = (x: number, y: number) => drawPixel(width, diff, x, y, 255, 255, 0); + const paddingSize = Math.max(VARIANCE_WINDOW_RADIUS, SSIM_WINDOW_RADIUS); + const paddingColorEven = [255, 0, 255]; + const paddingColorOdd = [0, 255, 0]; + const [r1, g1, b1] = ImageChannel.intoRGB(width, height, expected, { + paddingSize, + paddingColorEven, + paddingColorOdd, + }); + const [r2, g2, b2] = ImageChannel.intoRGB(width, height, actual, { + paddingSize, + paddingColorEven, + paddingColorOdd, + }); + + const drawRedPixel = (x: number, y: number) => drawPixel(width, diff, x - paddingSize, y - paddingSize, 255, 0, 0); + const drawYellowPixel = (x: number, y: number) => drawPixel(width, diff, x - paddingSize, y - paddingSize, 255, 255, 0); const drawGrayPixel = (x: number, y: number) => { const gray = rgb2gray(r1.get(x, y), g1.get(x, y), b1.get(x, y)); const value = blendWithWhite(gray, 0.1); - drawPixel(width, diff, x, y, value, value, value); + drawPixel(width, diff, x - paddingSize, y - paddingSize, value, value, value); }; let fastR, fastG, fastB; let diffCount = 0; - for (let y = 0; y < height; ++y){ - for (let x = 0; x < width; ++x) { + for (let y = paddingSize; y < r1.height - paddingSize; ++y){ + for (let x = paddingSize; x < r1.width - paddingSize; ++x) { // Fast-path: equal pixels. if (r1.get(x, y) === r2.get(x, y) && g1.get(x, y) === g2.get(x, y) && b1.get(x, y) === b2.get(x, y)) { drawGrayPixel(x, y); diff --git a/packages/playwright-core/src/image_tools/imageChannel.ts b/packages/playwright-core/src/image_tools/imageChannel.ts index a9c4687f8c..d21c735c43 100644 --- a/packages/playwright-core/src/image_tools/imageChannel.ts +++ b/packages/playwright-core/src/image_tools/imageChannel.ts @@ -16,29 +16,49 @@ import { blendWithWhite } from './colorUtils'; +export type PaddingOptions = { + paddingSize?: number, + paddingColorOdd?: number[], + paddingColorEven?: number[], +}; + export class ImageChannel { data: Uint8Array; width: number; height: number; - static intoRGB(width: number, height: number, data: Buffer): ImageChannel[] { - const r = new Uint8Array(width * height); - const g = new Uint8Array(width * height); - const b = new Uint8Array(width * height); - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - const index = y * width + x; - const offset = index * 4; - const alpha = data[offset + 3] === 255 ? 1 : data[offset + 3] / 255; - r[index] = blendWithWhite(data[offset], alpha); - g[index] = blendWithWhite(data[offset + 1], alpha); - b[index] = blendWithWhite(data[offset + 2], alpha); + static intoRGB(width: number, height: number, data: Buffer, options: PaddingOptions = {}): ImageChannel[] { + const { + paddingSize = 0, + paddingColorOdd = [255, 0, 255], + paddingColorEven = [0, 255, 0], + } = options; + const newWidth = width + 2 * paddingSize; + const newHeight = height + 2 * paddingSize; + const r = new Uint8Array(newWidth * newHeight); + const g = new Uint8Array(newWidth * newHeight); + const b = new Uint8Array(newWidth * newHeight); + for (let y = 0; y < newHeight; ++y) { + for (let x = 0; x < newWidth; ++x) { + const index = y * newWidth + x; + if (y >= paddingSize && y < newHeight - paddingSize && x >= paddingSize && x < newWidth - paddingSize) { + const offset = ((y - paddingSize) * width + (x - paddingSize)) * 4; + const alpha = data[offset + 3] === 255 ? 1 : data[offset + 3] / 255; + r[index] = blendWithWhite(data[offset], alpha); + g[index] = blendWithWhite(data[offset + 1], alpha); + b[index] = blendWithWhite(data[offset + 2], alpha); + } else { + const color = (y + x) % 2 === 0 ? paddingColorEven : paddingColorOdd; + r[index] = color[0]; + g[index] = color[1]; + b[index] = color[2]; + } } } return [ - new ImageChannel(width, height, r), - new ImageChannel(width, height, g), - new ImageChannel(width, height, b), + new ImageChannel(newWidth, newHeight, r), + new ImageChannel(newWidth, newHeight, g), + new ImageChannel(newWidth, newHeight, b), ]; } diff --git a/tests/image_tools/fixtures/should-match/webkit-rendering-artifacts/webkit-corner-2x-actual.png b/tests/image_tools/fixtures/should-match/webkit-rendering-artifacts/webkit-corner-2x-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..a6ab54acd3436b47e49ebefcb474cb1380db46cc GIT binary patch literal 2197 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 WRqAfBpfW}De?RHqX5V5