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 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
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.1
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.1.tar.gz | 49.0 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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}
|