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 changed content, shift left out
.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 changed content
    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 content below moved, in current-image coordinates, as the diff image draws it. A deleted band is the seam row the removed rows left behind. Band rows can differ from the counts when a region was replaced.

The cluster mask holds the changed content only. Shift bands are the other half of the answer, so read alignment.bands to decide whether to absorb a shift or flag it.

A region replaced by content of a different height, such as a tall chart swapped for a short one, keeps blank rows that both versions share. The Myers diff matches those blank rows inside the region, so the fields above report the change as separate inserted and deleted rows. aligned_diff_image(), aligned_clusters() and bands show such a region as one changed region instead, followed by the rows one side has over the other. The counts and residual_count do not change, so thresholds that read them behave as before. 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.1

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.1
File Size Uploaded
pixelhog-1.3.1.tar.gz 49.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for pixelhog 1.3.1
File
pixelhog-1.3.1-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
pixelhog-1.3.1-cp312-abi3-musllinux_1_2_x86_64.whl CPython 3.12 abi3 Linux musl 1.2+ x86-64 Details
pixelhog-1.3.1-cp312-abi3-musllinux_1_2_aarch64.whl CPython 3.12 abi3 Linux musl 1.2+ ARM64 Details
pixelhog-1.3.1-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.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.12 abi3 Linux glibc 2.17+ ARM64 Details
pixelhog-1.3.1-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
pixelhog-1.3.1-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.1.tar.gz

Download URL pixelhog-1.3.1.tar.gz
Size 49.0 kB
Tags Source
SHA-256 checksum
How to use checksums
4fc2bdc21a5b89e5cf102530c9ace07f27ad16a55d92778f459000c00a00f415
BLAKE2b-256 checksum
How to use checksums
2a50b734dbdd51b18560d0907f480a42f47d15481469df63fec5b87ca723949a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-win_amd64.whl

Download URL pixelhog-1.3.1-cp312-abi3-win_amd64.whl
Size 629.3 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
7e33affaf4ec24058016d5022de738c14c1a702bdd31aaeb33f7f1bb93b67be9
BLAKE2b-256 checksum
How to use checksums
1e97cc1be926aa3220d679d8261ee645a1ffc42d7d88ce002ab2b68d5f71ad76
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-musllinux_1_2_x86_64.whl

Download URL pixelhog-1.3.1-cp312-abi3-musllinux_1_2_x86_64.whl
Size 943.2 kB
Tags CPython 3.12 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
819c702512f33009e8badd403956803c4f1ea767155f8226c79e1414cc755251
BLAKE2b-256 checksum
How to use checksums
08ed9b31f347f72eb27fd63fffc4fb0ca24192b21f2e1c312118b982a67f6c54
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-musllinux_1_2_aarch64.whl

Download URL pixelhog-1.3.1-cp312-abi3-musllinux_1_2_aarch64.whl
Size 880.7 kB
Tags CPython 3.12 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
58a6e030e02426f2ea619dcbd1e5bea105a13c6fbba1c5aec7f221d882f5f0e5
BLAKE2b-256 checksum
How to use checksums
97916a9daf0f7f84da24c38143dc3e8dda04c16e8d53e32612ceff46be4c7353
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL pixelhog-1.3.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 748.5 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
325b80dc6e7c9ec9e974e3be5823d8e85a48174201eb6bbe06c30649cf7de336
BLAKE2b-256 checksum
How to use checksums
aa9ae28b70f8d6f92a3a25695727de8a988ba123bb5dcc4452f4416bc5bbbdb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL pixelhog-1.3.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 702.9 kB
Tags CPython 3.12 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
091e3b64c8706ab4e062164639ca3ccaa2ab676474b6ef2d61c165cec1928815
BLAKE2b-256 checksum
How to use checksums
682c63d168314b9e5f4c641e8329d5b6aafafc7bc3226952d21fa7d78b423981
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-macosx_11_0_arm64.whl

Download URL pixelhog-1.3.1-cp312-abi3-macosx_11_0_arm64.whl
Size 669.1 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b503f711d8d879991d2e171b1439c094052ad6fcb714b7b049eee7856527ef5b
BLAKE2b-256 checksum
How to use checksums
2f4098faf38d601092873e074fda9ca869ca1c454654d1f881f00a8855e34e11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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.1-cp312-abi3-macosx_10_12_x86_64.whl

Download URL pixelhog-1.3.1-cp312-abi3-macosx_10_12_x86_64.whl
Size 680.7 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
6ac936ca360730a4daf8793806fb4b3aedfe3ddec7928d854094bc14df89a663
BLAKE2b-256 checksum
How to use checksums
466fbd7937bc86fd3827c2ccf3728e1b9f48439409e27a173df783d399554e60
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","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

This release

1.3.1 This release

8 release files

1.3.0

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