NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #3137 most downloaded on PyPI
Rust-accelerated pixelmatch and SSIM for PNG bytes
Last release 18 days ago
16 Sep 2026
Ships unpredictably
gaps range from 8 days to 4 months
Nearly every release is documented
notes for 5 of 5 stable releases
Nothing withdrawn
no release was ever pulled
5 months old
5 releases · first in 2026
One column per month.
fix(pixelhog): draw replaced regions as one change in the aligned diff by @webjunkie in #9
Full Changelog: v1.3.0...v1.3.1
Replaced regions in the aligned diff image. A region replaced by content of a different height
keeps the blank rows both versions share, and the row diff matched those rows inside the region. The
aligned diff image then showed solid bands and seams across a chart that had changed, and no change
inside it. aligned_diff_image(), aligned_clusters() and bands now show edits that only blank
rows separate as one changed region, followed by the rows one side has over the other. Moved content
keeps its bands. inserted_rows, deleted_rows, changed_rows, residual_count and
aligned_ssim() do not change, so callers that threshold on them behave as before.
feat(pixelhog): align rows to separate vertical shifts from real changes by @webjunkie in #8
Full Changelog: v1.2.0...v1.3.0
Row alignment. New row_alignment() tells a vertical shift from a real change. It hashes
every pixel row and runs a budgeted Myers O(ND) diff over the hashes. Adjacent delete and insert
runs pair back into a content change, so anti-aliasing jitter on a text row is not reported as a
shift. The result gives inserted_rows, deleted_rows, changed_rows, and residual_count —
the pixels that still differ once the shift is taken out. A screenshot pair that grew by one row
reads as 1 inserted row and a handful of anti-aliased residual pixels instead of a few percent of
the page.
Shift bands. RowAlignment.bands lists inserted and deleted row bands in current-image
coordinates, so they overlay the current screenshot directly. Bands are deliberately kept out of
the cluster mask — a one-row band would not survive min_side, so callers read bands.
Budget bail-out. max_edit_ratio (default 0.25) and max_edit_rows (default 2048) cap the
edit distance. When a pair is too different to align (a re-layout, a different page), the result
comes back with aligned = False and empty fields rather than burning time on an O(ND) walk.
Alignment is vertical only, so a width change is unalignable for the same reason. Every
aligned_* method raises on an unaligned result, and on an alignment computed for another image
pair.
Aligned diff image, clusters, and SSIM. aligned_diff_image() renders the diff in
current-image coordinates: grayed rows for matched content, pixelmatch coloring for rows that
really changed, filled bands where rows were added, and a seam row (in diff_color_alt when set)
where rows were removed. aligned_clusters() clusters the residual only. aligned_ssim() scores
the matched rows only, so a one-row insert no longer drags the score down.
feat(pixelhog): spatial clustering, perf fast paths, Comparison API by @webjunkie in #6
Full Changelog: v1.1.0...v1.2.0
Spatial clustering. New clusters() method returns connected-component regions of
differing pixels with bounding boxes, pixel counts, and centroids. Uses dilation (default 4px)
to merge nearby fragments into UI-level regions, then two-pass CCL with 8-connectivity and
union-find. Configurable via dilation, min_pixels (default 16), and min_side params.
Results sorted by pixel_count descending for triage UIs. ~15% overhead over count-only.
Aligned-bbox cluster merge. Post-CCL pass that collapses clusters sharing axis alignment
into regional groups. Catches the common "list reorder" pattern where every row becomes its
own cluster despite being a single semantic change. Enabled via merge_gap (max perpendicular
distance, default 0 = off) and merge_overlap (min axis overlap ratio, default 0.5). Merged
clusters expose merged_from count. ClustersResult.truncated only reflects max_clusters
cap-hit — merge-collapse does not set it. total_clusters reports pre-cap count.
Performance: threshold=0 fast path. When threshold is 0 and AA detection is off, the
count-only and mask paths skip color_delta entirely and just count u32 mismatches. ~25%
faster across all image sizes for this scenario.
Performance: early exit. New diff_count_capped(max_diffs) stops processing once enough
diffs are found. Sub-100µs for a quick-fail check regardless of image size.
Comparison object API. New Comparison class decodes PNGs once at construction, then
exposes individual methods: diff_count(), ssim(), clusters(), diff_image(),
current_thumbnail(), baseline_thumbnail(), diff_count_capped(). Also available via
Comparison.from_rgba() and Comparison.batch(). Exposes size_mismatch, baseline_size,
and current_size properties so callers can detect padding artifacts. Exposed as frozen
#[pyclass] with proper BoundingBox and Cluster result types.
Breaking: standalone paired-image functions removed. diff, diff_count, ssim,
compare, and their _rgba variants are gone. Use Comparison(a, b).method(...) instead —
same functionality, no redundant decode. Batch APIs (diff_batch, compare_batch, etc.) and
thumbnail remain as standalone functions.
Smarter thumbnails for extreme aspect ratios. current_thumbnail() and
baseline_thumbnail() accept min_width / min_height floors. When proportional scaling
would produce a result smaller than the floor (e.g. a 1065×30 tab bar → 200×6), the original
is top-left cropped instead of scaled down. No upscaling. Existing callers are unaffected —
floors default to None.
Expanded benchmarks. 9 groups covering 2.1M / 2.5M / 18M pixel images across count-only, diff image, SSIM, compare, clusters, early exit, identical, and small-diff scenarios.
feat(pixelhog): shrink diff PNGs 97%, add WebP thumbnails by @webjunkie in #5
Full Changelog: v1.0.0...v1.1.0
Diff PNG output is now 97% smaller. Encoding switched from Fast + NoFilter + RGBA
to Default + Adaptive + RGB. The alpha channel was always 255 (fully opaque) so stripping
it is lossless. For a typical 1059×674 screenshot diff: 1,405 KB → 49 KB, with ~5ms
additional encode time.
Thumbnail generation. New thumbnail() function produces lossless WebP thumbnails with
Lanczos3 downscaling. Supports width-only and width+height (top-crop) modes for grid layouts.
Also available as an opt-in on compare() / compare_rgba() / compare_batch() via
thumbnail_width / thumbnail_height params — generates the thumbnail from the
already-decoded current image buffer at zero extra decode cost.
Breaking: compare, compare_rgba, and compare_batch now return a 6-tuple instead of
5-tuple. The new 6th element is Optional[bytes] containing the WebP thumbnail when
thumbnail_width is set, None otherwise.
fix(ci): replace deprecated macos-13 with macos-15-intel, build wheels on main by @webjunkie in #2
Full Changelog: https://github.com/PostHog/pixelhog/commits/v1.0.0
Initial stable release.
compare call that runs diff + SSIM in a single decode passYour coding agent can read these notes before it upgrades. Set up the MCP server →