Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/pixelmatch-8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@wdio/image-comparison-core": major
"@wdio/visual-service": major
---

feat: compare images with pixelmatch 8 (OKLab/HyAB color distance)

The comparison engine is now pixelmatch 8. It measures color differences in the OKLab color space with the HyAB distance instead of YIQ, which is closer to how people see colors: fewer false positives and fewer missed changes.

The threshold scale did not change (`0` to `1`, where `1` is black vs white), so the `ignore*` presets keep their values. But **mismatch percentages can differ a little** from v10 for the same images. On real screenshots the number of different pixels changed by about −3 % to +3 % (for a 1-pixel shift of a full page with `ignoreAntialiasing`: 0.002 % → 0.003 %). If a check depends on an exact mismatch percentage or a tight tolerance, check it again after the upgrade.
4 changes: 2 additions & 2 deletions packages/image-comparison-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ v10 uses [pixelmatch](https://github.com/mapbox/pixelmatch) instead of resemble.
|---|---|---|
| AA forgiveness | opt-in (`ignoreAntialiasing: true`) | on by default (`ignoreAntialiasing: true`) |
| Strict comparison | default | set `ignoreAntialiasing: false` |
| Engine | resemble RGB/brightness | pixelmatch YIQ perceptual distance |
| Engine | resemble RGB/brightness | pixelmatch perceptual distance (OKLab/HyAB since v11, YIQ in v10) |

No config change is needed if you rely on forgiving comparison behaviour.

Expand All @@ -36,7 +36,7 @@ No config change is needed if you rely on forgiving comparison behaviour.
| `ignoreColors` | resemble luma grayscale | ~16/255 (`0.063`) | no |
| `ignoreNothing` | - | `0` | no |

Thresholds are calibrated to resemble outcomes; the underlying algorithm is YIQ perceptual distance, not resemble's RGB math.
Thresholds are calibrated to resemble outcomes; the underlying algorithm is pixelmatch's perceptual color distance, not resemble's RGB math. Since v11 this is pixelmatch 8 (OKLab color space and HyAB distance; v10 used YIQ). pixelmatch 8 keeps the threshold scale (`0` to `1`, where `1` is black vs white), so the thresholds above stay the same, but mismatch numbers can differ a little from v10.

### Last-wins semantics

Expand Down
6 changes: 3 additions & 3 deletions packages/image-comparison-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
"watch:tsc": "pnpm run build:tsc -w"
},
"dependencies": {
"fast-png": "^8.0.0",
"pixelmatch": "^7.2.0",
"@wdio/logger": "^10.0.0",
"@wdio/types": "^10.0.0"
"@wdio/types": "^10.0.0",
"fast-png": "^8.0.0",
"pixelmatch": "^8.0.0"
Comment thread
plum117 marked this conversation as resolved.
},
"devDependencies": {
"webdriverio": "^10.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/image-comparison-core/src/base.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export interface BaseImageCompareOptions {
*/
ignoreColors?: boolean;
/**
* Use a relaxed RGB tolerance (~16/255 per channel in YIQ space).
* Use a relaxed color tolerance (pixelmatch threshold `0.063`).
* Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness).
* @default false
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export interface ClassOptions {
ignoreColors?: boolean;

/**
* Use a relaxed RGB tolerance (~16/255 per channel in YIQ space).
* Use a relaxed color tolerance (pixelmatch threshold `0.063`).
* Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness).
*/
ignoreLess?: boolean;
Expand Down Expand Up @@ -496,7 +496,7 @@ export interface IgnorePresetCompareOptions {
ignoreColors: boolean;

/**
* Use a relaxed RGB tolerance (~16/255 per channel in YIQ space).
* Use a relaxed color tolerance (pixelmatch threshold `0.063`).
* Preset: strict threshold, AA not forgiven (does not inherit default AA forgiveness).
*/
ignoreLess: boolean;
Expand Down
6 changes: 4 additions & 2 deletions packages/image-comparison-core/src/helpers/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,8 @@ const COMPARE_PRESET_SETTINGS: Record<ComparisonIgnoreOption, { threshold: numbe
nothing: { threshold: 0, includeAA: true },
less: { threshold: 0.063, includeAA: true },
antialiasing: {
// Resemble's ignoreAntialiasing uses 32/255 per-channel tolerance (~0.13 YIQ).
// Calibrated to resemble's ignoreAntialiasing (32/255 per channel). The scale is pixelmatch's:
// 0 to 1, where 1 is black vs white (pixelmatch 8 kept it when it moved from YIQ to OKLab/HyAB).
threshold: 0.13,
includeAA: false,
},
Expand Down Expand Up @@ -538,7 +539,8 @@ export function resolveComparePreset(ignoreList: ComparisonIgnoreOption[]): { th
return COMPARE_PRESET_SETTINGS[activePreset]
}

// Default strict tolerance: 16/255 per channel (~6.3% of max YIQ distance).
// Default strict tolerance, calibrated to resemble's 16/255 per channel. The scale is pixelmatch's:
// 0 to 1, where 1 is black vs white.
return { threshold: 0.063, includeAA: true }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest'
import compareImages from './compareImages.js'
import { DEFAULT_PIXELMATCH_OPTIONS } from '../helpers/constants.js'
import { createCanvas, encodeImage } from '../utils/imageUtils.js'

// Runs the real pixelmatch (no mock): `checkerboard` only changes how pixelmatch blends semi-transparent pixels
const SIZE = 16
const transparent = encodeImage(createCanvas(SIZE, SIZE, 0, 0, 0, 0))
const opaqueWhite = encodeImage(createCanvas(SIZE, SIZE, 255, 255, 255, 255))

function compareWithCheckerboard(checkerboard: boolean) {
return compareImages(transparent, opaqueWhite, {
pixelmatch: { ...DEFAULT_PIXELMATCH_OPTIONS, threshold: 0.063, includeAA: true, checkerboard },
})
}

describe('compareImages checkerboard', () => {
it('blends transparent pixels with white when checkerboard is false, so they match white', async () => {
const result = await compareWithCheckerboard(false)

expect(result.rawMisMatchPercentage).toBe(0)
expect(result.diffPixels).toHaveLength(0)
})

it('blends transparent pixels with a checker pattern when checkerboard is true, so they differ from white', async () => {
const result = await compareWithCheckerboard(true)

expect(result.rawMisMatchPercentage).toBeGreaterThan(0)
expect(result.diffPixels.length).toBeGreaterThan(0)
})
})
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading