Skip to Content
New: blazediff-png - a from-scratch Rust PNG codec, byte-exact to libspng and faster on every fixture. Read more β†’
DocsMetrics ExplainedChoosing a Metric

Choosing a Metric

Start with pixel diffing. Move to a structural metric only when exact matching is too strict. Pixel diffing is the fastest and the only one that tells you exactly what changed. SSIM and GMSD trade that detail for tolerance: they score how different two images look, so they shrug off compression artifacts and rendering noise that pixel counting flags as failures.

The five options

MetricAnswersOutputDirectionRelative costBest for
Pixel diffHow many pixels changedCount and percentLower is betterFastestThe default. CI gates, exact rendering
GMSDDid the edges changeScore, 0 to ~1Lower is betterLowLayout and shape changes, compressed images
SSIMDoes it look differentScore, 0 to 1Higher is betterMediumPerceived quality, matching published results
MS-SSIMDoes it look different at any sizeScore, 0 to 1Higher is betterHighImages viewed at several resolutions
Hitchhiker’s SSIMSame as SSIM, fasterScore, 0 to 1Higher is betterLowLarge batches where SSIM is the bottleneck

Read the details: what SSIM measures Β· how GMSD works.

Pick by problem

Your problemUse
Standard CI screenshot gatePixel diff (core-native) with threshold: 0.1
Failures from anti-aliasing on text and edgesPixel diff with antialiasing: true first, then GMSD
Screenshots re-encoded as JPEG somewhere in the pipelineGMSD, gate around 0.15
Same page rendered on macOS locally and Linux in CIPixel diff with a percentage threshold, then GMSD
A β€œdid the design get worse” score for a reportSSIM or MS-SSIM
Thousands of images per run and SSIM is too slowHitchhiker’s SSIM
A brand color changedPixel diff. SSIM and GMSD are near-blind to it
You need to know what changed, not just how muchInterpret mode

Both structural metrics are weak on color. SSIM and GMSD work on luminance. Two colors with the same brightness can swap and barely move either score. If color correctness matters, keep a pixel diff in the pipeline.

Structural metrics instead of RGB thresholds

A raw RGB threshold asks one question per pixel: is this channel more than N away from the baseline. That has no idea whether the pixel sits on the edge of a letter, inside a gradient, or in the middle of a flat background. So the noise you want to ignore and the change you want to catch look the same to it, and the only knob you have is β€œbe less strict everywhere”.

Structural metrics score a neighborhood instead of a pixel. SSIM compares the brightness, contrast, and structure of a window; GMSD compares the edge strength around each pixel. Both stay stable when a render is subtly noisy and both react when the shape of something actually changes - which is the distinction an RGB threshold cannot express.

The practical setup for a noisy pipeline:

import { compare } from "@blazediff/core-native"; import gmsd from "@blazediff/gmsd"; // 1. Cheap exact gate. Most runs stop here. const result = await compare("baseline.png", "current.png", "diff.png", { threshold: 0.1, antialiasing: true, }); if (result.match) return "pass"; // 2. Something changed. Is it perceptible? const score = gmsd(baselinePixels, currentPixels, undefined, width, height); if (score < 0.05) return "pass-with-noise"; return "fail";

Setting a threshold

Do not copy a number from a table. Run your own baselines twice with no code change and see what score the noise alone produces, then set the gate above it.

ComparisonKnobSane starting point
Pixel diffthreshold (per-pixel color distance, 0-1)0.1
Pixel difffailureThreshold + failureThresholdType0.1 with 'percent'
GMSDGate on the returned score0.05, or 0.15 if compressed
SSIM / MS-SSIMGate on the returned score0.98 for full-page shots

Percentage thresholds travel better than pixel counts, because a pixel budget tuned on a 1280px viewport becomes far too strict at 4K.

Using them from a test

Every metric is available through the same matcher:

import "@blazediff/vitest"; // or @blazediff/jest, @blazediff/bun await expect(screenshot).toMatchImageSnapshot({ method: "core-native" }); await expect(screenshot).toMatchImageSnapshot({ method: "gmsd" }); await expect(screenshot).toMatchImageSnapshot({ method: "ssim" }); await expect(screenshot).toMatchImageSnapshot({ method: "hitchhikers-ssim" });

And from the CLI:

blazediff-cli baseline.png current.png diff.png # core-native, the default blazediff-cli gmsd baseline.png current.png blazediff-cli ssim baseline.png current.png blazediff-cli hitchhikers-ssim baseline.png current.png

Measured cost

Numbers from the pixel benchmarks and structural benchmarks, M1 Max, Node 22:

  • Pixel diff, 4K pair, native Rust with image IO included: 215-269ms
  • Pixel diff, 4K pair, pure JS with IO excluded: 97-136ms
  • Hitchhiker’s SSIM: about 4x faster than standard SSIM
  • GMSD: a single pass with two 3x3 convolutions, cheaper than SSIM

Next: What is SSIM β†’ Β· How GMSD works β†’ Β· Structural comparison examples β†’

Last updated on