Skip to main content

pixelhog

Fast visual regression primitives for Python, implemented in Rust.

pixelhog compares screenshots in two complementary ways:

  • diff: exact pixel-level differences (with anti-alias handling), optional diff image output
  • ssim: perceptual similarity score in [0.0, 1.0]

It also provides spatial clustering (where on the page did things change), early-exit checks, and WebP thumbnail generation — all accessible through a stateful Comparison object that decodes images once and exposes methods on demand.

Install (local dev)

uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install -U pip maturin
maturin develop --release

Quickstart

from pixelhog import Comparison, thumbnail

cmp = Comparison(baseline_png, current_png)

count = cmp.diff_count()                     # pixel mismatch count
score = cmp.ssim()                           # perceptual similarity
png   = cmp.diff_image()                     # diff visualization (PNG bytes)
thumb = cmp.current_thumbnail(width=200)     # lossless WebP thumbnail

# Spatial clustering — where did things change?
result = cmp.clusters(dilation=8, merge_gap=60)
for cluster in result.clusters:
    print(cluster.bbox.x, cluster.bbox.y, cluster.bbox.width, cluster.bbox.height)

# Early exit — fail fast if too many diffs
capped = cmp.diff_count_capped(max_diffs=1000)

# Standalone thumbnail (lossless WebP, Lanczos3 downscale, top-crop)
thumb = thumbnail(current_png, width=200, height=150)

API at a glance

Comparison

Method Returns Notes
Comparison(baseline_png, current_png) Comparison Decode pair once, call methods on demand
Comparison.from_rgba(...) Comparison Pre-decoded RGBA buffers
Comparison.batch(pairs) list[Comparison] Parallel decode
.diff_count(threshold, include_aa) int Pixel mismatch count
.diff_count_capped(max_diffs, ...) int Early-exit count
.ssim() float Structural similarity
.clusters(dilation, merge_gap, ...) ClustersResult Spatial regions of change
.row_alignment(...) RowAlignment Separate a vertical shift from real changes
.aligned_clusters(alignment, ...) ClustersResult Clusters of the residual only
.aligned_ssim(alignment) float SSIM over the matched rows only
.aligned_diff_image(alignment, ...) bytes (PNG) Shift-aware diff visualization
.diff_image(...) bytes (PNG) Diff visualization
.current_thumbnail(width, height, ...) bytes (WebP) Thumbnail of current image
.baseline_thumbnail(width, height, ...) bytes (WebP) Thumbnail of baseline image
.size_mismatch bool Whether images had different dimensions

Utilities and batch

Function Input Output Use when
thumbnail PNG bytes bytes (WebP) Single-image thumbnail (no pair needed)
diff_batch list[(baseline, current)] list[DiffResult] Parallel diff across many pairs
diff_count_batch list[(baseline, current)] list[DiffCountResult] Parallel count-only
ssim_batch list[(baseline, current)] list[float] Parallel SSIM
compare_batch list[(baseline, current)] list[CompareResult] Parallel combined metrics

Row alignment (vertical shifts)

A panel grows by a pixel, a banner is inserted, a list gains a row — everything below moves down. A top-aligned pixel diff then flags most of the page and SSIM reports large dissimilarity, even though nothing else changed. row_alignment() hashes each pixel row and runs a budgeted Myers diff over the hashes.

cmp = Comparison(baseline_png, current_png)
alignment = cmp.row_alignment()

if alignment.aligned:
    print(alignment.inserted_rows, alignment.deleted_rows, alignment.residual_count)
    for band in alignment.bands:
        print(band.kind, band.y, band.rows)      # "inserted" / "deleted", current-image rows

    clusters = cmp.aligned_clusters(alignment)   # clusters of the residual only
    score = cmp.aligned_ssim(alignment)          # SSIM over the matched rows only
    png = cmp.aligned_diff_image(alignment)      # diff image in current-image coordinates
Field Meaning
aligned False when the pair could not be aligned: too different, or a width change (alignment is vertical only). Every other field is then zero or empty, and every aligned_* method raises.
inserted_rows / deleted_rows Rows the current image gained or lost — the shift itself.
changed_rows Rows present in both images whose content differs.
residual_count Differing pixels inside those changed rows. This is the number to threshold on: it excludes the shift.
bands Where the shift happened, in current-image coordinates. A deleted band is the seam row the removed rows left behind.

The cluster mask holds the residual only. Shift bands are the other half of the answer, so read alignment.bands to decide whether to absorb a shift or flag it. An alignment belongs to the pair it was computed from; passing it to another Comparison raises. Tune the bail-out with max_edit_ratio (default 0.25) and max_edit_rows (default 2048).

Behavior

  • Comparison decodes PNG bytes once at construction; methods compute on demand.
  • Comparison.from_rgba(...) accepts pre-decoded RGBA buffers (zero-copy).
  • Smaller images are padded to the larger dimensions with transparent pixels.
  • SSIM uses 11×11 uniform windows with reflect padding; falls back to global for tiny images.
  • Clustering uses morphological dilation + two-pass CCL with optional aligned-bbox merge.
  • Row alignment hashes rows and runs a budgeted Myers diff over the hashes. It is vertical only: a width change makes a pair unalignable.

Correctness and tests

The test suite is designed to validate both algorithm fidelity and practical product behavior.

  • Rust unit/integration tests cover:
    • identical/completely different/partial-diff images
    • threshold behavior
    • different-size padding behavior
    • SSIM behavior (identical, slight change, large change, small-image fallback)
  • Canonical pixelmatch fixture tests use the official Mapbox test set:
    • 8 fixture pairs with exact expected mismatch counts
    • expected diff image comparison against golden outputs
    • decoded RGBA byte equality checks to ensure pixel-perfect output matching
  • Python integration tests cover:
    • high-level API contracts and error behavior
    • tall-page and subtle-change scenarios
    • cross-validation against a pure-Python reference implementation
      • pixel diff counts must match exactly
      • SSIM must stay within tolerance

Run the full correctness suite:

# Rust core only
cargo test -p pixelhog

# Full suite including Python integration tests
cargo test
uv run --python 3.12 --with maturin --with pytest --with pillow bash -lc \
  "maturin develop --release && pytest -q"

Benchmarks

The repo includes both Criterion benches and pipeline breakdown tools.

  • cargo bench runs Criterion API benchmarks (PNG-bytes entry points).
  • Breakdown binaries in examples/ measure where time goes:
    • breakdown.rs: decode vs core diff vs encode vs API call
    • ssim_breakdown.rs: decode/pad vs core SSIM vs API call
    • combined_estimate.rs: separate calls vs combined single-decode flow

Run:

cargo bench -p pixelhog
cargo run -p pixelhog --release --example breakdown
cargo run -p pixelhog --release --example ssim_breakdown
cargo run -p pixelhog --release --example combined_estimate

For screenshot-style workloads, the practical guidance is:

  • diff_count is cheaper than diff when you do not need an artifact.
  • compare(..., return_diff=False) avoids duplicate decode work when you need both diff-count and SSIM.

Development

# Rust tests (includes canonical Mapbox fixture tests)
cargo test

# Python extension + tests
uv run --python 3.12 --with maturin --with pytest --with pillow bash -lc \
  "maturin develop --release && pytest -q"

# Lint/format/type-check
uv run --python 3.12 --with ruff ruff format --check .
uv run --python 3.12 --with ruff ruff check .
uv run --python 3.12 --with ty --with pytest --with pillow ty check . --python .venv

License

This repository is MIT licensed. See LICENSE.

Algorithm attribution for pixelmatch is documented in THIRD_PARTY_NOTICES.md.

Release files for pixelhog 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pixelhog 1.3.0
File Size Uploaded
pixelhog-1.3.0.tar.gz 46.5 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for pixelhog 1.3.0
File
pixelhog-1.3.0-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
pixelhog-1.3.0-cp312-abi3-musllinux_1_2_x86_64.whl CPython 3.12 abi3 Linux musl 1.2+ x86-64 Details
pixelhog-1.3.0-cp312-abi3-musllinux_1_2_aarch64.whl CPython 3.12 abi3 Linux musl 1.2+ ARM64 Details
pixelhog-1.3.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 abi3 Linux glibc 2.17+ x86-64 Details
pixelhog-1.3.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.12 abi3 Linux glibc 2.17+ ARM64 Details
pixelhog-1.3.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
pixelhog-1.3.0-cp312-abi3-macosx_10_12_x86_64.whl CPython 3.12 abi3 macOS 10.12+ x86-64 Details

Total release size: 5.3 MB

Release files / pixelhog-1.3.0.tar.gz

Download URL pixelhog-1.3.0.tar.gz
Size 46.5 kB
Tags Source
SHA-256 checksum
How to use checksums
dda5a6b0ac57b21d5045ac79ce29a5e71905c89465f48674a8392629133c20ae
BLAKE2b-256 checksum
How to use checksums
2ed927e623a5d417b2142f45c44f22a7cee254eaa326601a2298df084e309616
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-win_amd64.whl

Download URL pixelhog-1.3.0-cp312-abi3-win_amd64.whl
Size 637.5 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
f76882d96890f6764f7f2d94e61403a595781be9839e41d009b03bacefe9764c
BLAKE2b-256 checksum
How to use checksums
04d62665bc86c5850e2c8348f2566e7434e05d31149c2004849d0c5342f24815
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-musllinux_1_2_x86_64.whl

Download URL pixelhog-1.3.0-cp312-abi3-musllinux_1_2_x86_64.whl
Size 947.0 kB
Tags CPython 3.12 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
6b67fc52acce409c7600dd1958e796bef2efd37d8b044af40bb4127e18c60558
BLAKE2b-256 checksum
How to use checksums
ce4863c1616c3c1648505499cbdfea1c93c5d14083b2a9653ff2b4377f9741c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-musllinux_1_2_aarch64.whl

Download URL pixelhog-1.3.0-cp312-abi3-musllinux_1_2_aarch64.whl
Size 881.4 kB
Tags CPython 3.12 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
2cb7dd216e3b6d859f199db1a4507eab45d94a8fc1c346998909a34511906f6b
BLAKE2b-256 checksum
How to use checksums
defe5eb670983f8b2e7b81dc8163b54873ed4e46077bafc8ac653365a66725f6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL pixelhog-1.3.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 752.6 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
295705dc7d229cb2e5e8357fa73a6b1274a2d7b79ea82932eb5b98d251b04491
BLAKE2b-256 checksum
How to use checksums
05f88680f0abd297423b13b89facedb41cb47bef470657233af76919f3ff1820
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL pixelhog-1.3.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 703.7 kB
Tags CPython 3.12 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
0b9c0a13ba74dd9c2bf997805b8b4f5363a388485e644314dc19ed867111d66d
BLAKE2b-256 checksum
How to use checksums
30c15267264269c7bea81396cab580a971095a65a2900e3f865a77ef6f587a37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL pixelhog-1.3.0-cp312-abi3-macosx_11_0_arm64.whl
Size 670.5 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
92c2991132f1400b98d588c9c87db4b789af6e77e5ec4e79ea295cadc5baea96
BLAKE2b-256 checksum
How to use checksums
2e6e40e854d6fb86273077d047559e04befc6ca5d39fb67c65a8fd1755264618
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pixelhog-1.3.0-cp312-abi3-macosx_10_12_x86_64.whl

Download URL pixelhog-1.3.0-cp312-abi3-macosx_10_12_x86_64.whl
Size 682.6 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
fb7bf5ec5dd836ee629a552c760edd077799cd5db5dcfc1b684915eaaa056ecd
BLAKE2b-256 checksum
How to use checksums
00878227f1aa5d4b25923410868ce3328fd69789e7a521fb0ac2398833d6a9f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

1.3.1

8 release files

This release

1.3.0 This release

8 release files

1.2.0

8 release files

1.1.0

8 release files

1.0.0

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page