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 outputssim: 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
Comparisondecodes 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 benchruns 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 callssim_breakdown.rs: decode/pad vs core SSIM vs API callcombined_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_countis cheaper thandiffwhen 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)
| File | Size | Uploaded | |
|---|---|---|---|
| pixelhog-1.3.0.tar.gz | 46.5 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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}
|