mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 22:22:53 +03:00
6d82460a02
This patch implements a new image comparison function, codenamed "ssim-cie94". The goal of the new comparison function is to cancel out browser non-determenistic rendering. To use the new comparison function: ```ts await expect(page).toHaveScreenshot({ comparator: 'ssim-cie94', }); ``` As of Nov 30, 2022, we identified the following sources of non-determenistic rendering for Chromium: - Anti-aliasing for certain shapes might be different due to the way skia rasterizes certain shapes. - Color blending might be different on `x86` and `aarch64` architectures. The new function employs a few heuristics to fight these differences. Consider two non-equal image pixels `(r1, g1, b1)` and `(r2, g2, b2)`: 1. If the [CIE94] metric is less then 1.0, then we consider these pixels **EQUAL**. (The value `1.0` is the [just-noticeable difference] for [CIE94].). Otherwise, proceed to next step. 1. If all the 8 neighbors of the first pixel match its color, or if the 8 neighbors of the second pixel match its color, then these pixels are **DIFFERENT**. (In case of anti-aliasing, some of the direct neighbors have to be blended up or down.) Otherwise, proceed to next step. 1. If SSIM in some locality around the different pixels is more than 0.99, then consider this pixels to be **EQUAL**. Otherwise, mark them as **DIFFERENT**. (Local SSIM for anti-aliased pixels turns out to be very close to 1.0). [CIE94]: https://en.wikipedia.org/wiki/Color_difference#CIE94 [just-noticeable difference]: https://en.wikipedia.org/wiki/Just-noticeable_difference
120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
/**
|
|
* Copyright (c) Microsoft Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { test } from '../playwright-test/stable-test-runner';
|
|
import { ssim, FastStats } from 'playwright-core/lib/image_tools/stats';
|
|
import { ImageChannel } from 'playwright-core/lib/image_tools/imageChannel';
|
|
import { srgb2xyz, xyz2lab, colorDeltaE94 } from 'playwright-core/lib/image_tools/colorUtils';
|
|
import referenceSSIM from 'ssim.js';
|
|
import { randomPNG, assertEqual, grayChannel } from './utils';
|
|
|
|
test('srgb to lab conversion should work', async () => {
|
|
const srgb = [123, 81, 252];
|
|
const [x, y, z] = srgb2xyz(srgb);
|
|
// Values obtained with http://colormine.org/convert/rgb-to-xyz
|
|
assertEqual(x, 0.28681495837305815);
|
|
assertEqual(y, 0.17124087944445404);
|
|
assertEqual(z, 0.938890585081072);
|
|
const [l, a, b] = xyz2lab([x, y, z]);
|
|
// Values obtained with http://colormine.org/convert/rgb-to-lab
|
|
assertEqual(l, 48.416007793699535);
|
|
assertEqual(a, 57.71275605467668);
|
|
assertEqual(b, -79.29993619401066);
|
|
});
|
|
|
|
test('colorDeltaE94 should work', async () => {
|
|
const rgb1 = [123, 81, 252];
|
|
const rgb2 = [43, 201, 100];
|
|
// Value obtained with http://colormine.org/delta-e-calculator/cie94
|
|
assertEqual(colorDeltaE94(rgb1, rgb2), 71.2159);
|
|
});
|
|
|
|
test('fast stats and naive computation should match', async () => {
|
|
const N = 13, M = 17;
|
|
const png1 = randomPNG(N, M, 239);
|
|
const png2 = randomPNG(N, M, 261);
|
|
const [r1] = ImageChannel.intoRGB(png1.width, png1.height, png1.data);
|
|
const [r2] = ImageChannel.intoRGB(png2.width, png2.height, png2.data);
|
|
const fastStats = new FastStats(r1, r2);
|
|
|
|
for (let x1 = 0; x1 < png1.width; ++x1) {
|
|
for (let y1 = 0; y1 < png1.height; ++y1) {
|
|
for (let x2 = x1; x2 < png1.width; ++x2) {
|
|
for (let y2 = y1; y2 < png1.height; ++y2) {
|
|
assertEqual(fastStats.meanC1(x1, y1, x2, y2), computeMean(r1, x1, y1, x2, y2));
|
|
assertEqual(fastStats.varianceC1(x1, y1, x2, y2), computeVariance(r1, x1, y1, x2, y2));
|
|
assertEqual(fastStats.covariance(x1, y1, x2, y2), computeCovariance(r1, r2, x1, y1, x2, y2));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('ssim + fastStats should match "weber" algorithm from ssim.js', async () => {
|
|
const N = 200;
|
|
const png1 = randomPNG(N, N, 239);
|
|
const png2 = randomPNG(N, N, 261);
|
|
const windowRadius = 5;
|
|
const refSSIM = referenceSSIM(png1 as any, png2 as any, {
|
|
downsample: false,
|
|
ssim: 'weber',
|
|
windowSize: windowRadius * 2 + 1,
|
|
});
|
|
const gray1 = grayChannel(png1);
|
|
const gray2 = grayChannel(png2);
|
|
const fastStats = new FastStats(gray1, gray2);
|
|
for (let y = windowRadius; y < N - windowRadius; ++y) {
|
|
for (let x = windowRadius; x < N - windowRadius; ++x) {
|
|
const customSSIM = ssim(fastStats, x - windowRadius, y - windowRadius, x + windowRadius, y + windowRadius);
|
|
const reference = refSSIM.ssim_map.data[(y - windowRadius) * refSSIM.ssim_map.width + x - windowRadius];
|
|
assertEqual(customSSIM, reference);
|
|
}
|
|
}
|
|
});
|
|
|
|
function computeMean(c: ImageChannel, x1: number, y1: number, x2: number, y2: number) {
|
|
let result = 0;
|
|
const N = (x2 - x1 + 1) * (y2 - y1 + 1);
|
|
for (let y = y1; y <= y2; ++y) {
|
|
for (let x = x1; x <= x2; ++x)
|
|
result += c.get(x, y);
|
|
}
|
|
return result / N;
|
|
}
|
|
|
|
function computeVariance(c: ImageChannel, x1: number, y1: number, x2: number, y2: number) {
|
|
let result = 0;
|
|
const mean = computeMean(c, x1, y1, x2, y2);
|
|
const N = (x2 - x1 + 1) * (y2 - y1 + 1);
|
|
for (let y = y1; y <= y2; ++y) {
|
|
for (let x = x1; x <= x2; ++x)
|
|
result += (c.get(x, y) - mean) ** 2;
|
|
}
|
|
return result / N;
|
|
}
|
|
|
|
function computeCovariance(c1: ImageChannel, c2: ImageChannel, x1: number, y1: number, x2: number, y2: number) {
|
|
const N = (x2 - x1 + 1) * (y2 - y1 + 1);
|
|
const mean1 = computeMean(c1, x1, y1, x2, y2);
|
|
const mean2 = computeMean(c2, x1, y1, x2, y2);
|
|
let result = 0;
|
|
for (let y = y1; y <= y2; ++y) {
|
|
for (let x = x1; x <= x2; ++x)
|
|
result += (c1.get(x, y) - mean1) * (c2.get(x, y) - mean2);
|
|
}
|
|
return result / N;
|
|
}
|