blazediff-ssim
Structural-similarity metrics in Rust: SSIM, MS-SSIM and Hitchhiker's SSIM, vectorised through one lane-generic SIMD layer and held to the reference MATLAB scripts through Octave. No dependencies, no threads, no runtime dispatch.
Why it exists
BlazeDiff compares screenshots for visual regression testing. A per-pixel diff answers "which pixels changed", which is the right question until anti-aliasing, font hinting or a codec's rounding moves a few thousand pixels by one unit and the run goes red for no reason a human would call a change.
SSIM answers the other question: how alike do these look. The catch is that "SSIM" names a family whose members disagree by more than their published formulas suggest, because the reference implementations differ in window placement, boundary handling and downsampling. So the contract here is not "we implemented the paper", it is land where MATLAB lands, to a stated tolerance, and keep landing there.
The metrics turned out to have no dependency on the diff engine at all, so they
live in their own crate. blazediff consumes it; so can you.
What it does
ssim: Gaussian-windowed single-scale SSIM in'valid'mode, with the automatic downsampling to ~256px on the short edge that MATLAB'sssim.mdoes. That downsample is why it is also the fastest of the three on large inputs.ms_ssim: SSIM pooled across a 5-octave dyadic pyramid, permsssim.m. Product or weighted-sum pooling. Needs at least 176px on the short edge for the default five scales.hitchhikers_ssim: box windows over five integral images, pooled by coefficient of variation (Venkataramanan et al. 2021). Every window sum is an O(1) summed-area-table lookup instead of ten 11-tap convolutions.perceptual_ssim: the tunable variant. CIE L*a*b*, chroma weighting, chroma subsampling and mean-absolute-deviation pooling, each an independent knob. WithPerceptualOptions::default()it reduces bit-identically toms_ssim, which is what makes it usable as an ablation study rather than a second opinion.
Every metric returns a pooled score and the local map it was pooled from;
render_map paints that map into an RGBA8 buffer as grayscale.
Usage
use blazediff_ssim::{ms_ssim, MsSsimOptions, Plane, Rgba8, SsimOptions};
let plane1 = Plane::from_rgba8(Rgba8::new(&rgba1, width, height))?;
let plane2 = Plane::from_rgba8(Rgba8::new(&rgba2, width, height))?;
let outcome = ms_ssim(
&plane1,
&plane2,
&SsimOptions::default(),
&MsSsimOptions::default(),
)?;
println!("{:.6}", outcome.score); // 1.0 means identical
Rgba8 is a borrowed view, so nothing is copied to call in. Decoding is the
caller's problem: the crate takes RGBA8 bytes and has no I/O.
Python - blazediff-ssim
pip install blazediff-ssim
PyO3 bindings shipped as abi3-py38 wheels for CPython ≥ 3.8 (macOS, Linux
manylinux, Windows; arm64 + x86_64). Built from this crate's python Cargo
feature, which pulls in PNG/JPEG/QOI decoding so paths and encoded bytes work
directly.
import blazediff_ssim as ssim
result = ssim.compare("expected.png", "actual.png", metric="ms-ssim")
print(result.score) # 1.0 means identical
# Also: compare_buffers(bytes, bytes), compare_rgba(bytes, bytes, w, h),
# render_map(map, map_w, map_h, w, h) and metrics().
Every knob the Rust API exposes is a keyword argument: min_score,
window_size, k1, k2, bit_depth, weights, method, window_stride,
cov_pooling, color, chroma_weight, chroma_subsample, pooling and
deviation_weight. Pass return_map=True to get the local scores back as
little-endian float32 bytes — numpy.frombuffer(result.map, dtype="<f4").
Performance
Wall-clock on a 4K pair, decode included (decode is ~200 ms of each):
| Metric | 4K pair | Why |
|---|---|---|
ssim |
320 ms | MATLAB's automatic downsample shrinks the plane to ~256px before any convolution runs |
hitchhikers-ssim |
380 ms | full resolution, but O(1) window sums |
ms-ssim |
480 ms | full resolution at the finest of five scales |
Against single-threaded dssim, ms_ssim
runs about 1.9× faster. Two things bought that, neither of them threads:
One fused statistics pass. A scale needs five moments (µ1, µ2, σ1², σ2²,
σ12), which the textbook pipeline computes as eleven full-size intermediates.
stats.rs streams them through a row ring buffer in a single pass instead.
It is bit-identical to the unfused path by construction, and
streaming_matches_the_unfused_pipeline_bit_for_bit is the test that keeps it
that way.
Compile-time lane selection. Five kernel shapes carry nearly all the time,
so each is written once against a SimdF32 trait and instantiated per ISA:
NEON on aarch64, SSE2 on x86_64, simd128 on wasm32, a scalar fallback
elsewhere. All are baseline for their target, so nothing dispatches inside a
hot loop and there is no runtime feature detection.
Bit-exactness is the constraint, not an outcome
Tap-by-tap accumulation order is frozen to the @blazediff/ssim TypeScript
port. That is a deliberate handcuff: the JS port is the one whose MATLAB
agreement was measured, so matching its order means this crate inherits that
agreement instead of drifting away from it by an unmeasured amount. Anything
that would reassociate the sums, including some obvious-looking vectorisations,
is out of bounds even when it is faster.
Two consequences worth knowing about:
cube_rootreplacescbrtfin the Lab conversion, and is checked against libm across the whole L*a*b* domain rather than assumed equivalent.- FMA is used inside the vector body and deliberately not in the scalar tail, because the reference does not fuse either. Un-fusing the tail is a correctness fix, not a pessimisation.
MsSsimMethod::Product returns NaN when a scale's mean contrast-structure
term goes negative. That takes globally anticorrelated content (an inverted
image) rather than ordinary degradation, and both references degenerate the same
way: the JS gives NaN, MATLAB gives a complex number. The behaviour is kept
rather than papered over. WeightedSum stays finite throughout.
Verified
| Layer | Result |
|---|---|
MATLAB ssim.m |
within 0.01% on three fixture pairs, 0.05% on the one where downsampling by 5 costs the most precision |
MATLAB msssim.m |
within 0.05 absolute. The references pool 'valid' statistics where both ports pool symmetric 'same', so the gap is algorithmic, not numerical |
| TypeScript port | all three metrics agree to within 5e-6, the only cross-port pin for hitchhikers-ssim, which has no MATLAB reference |
| Fused statistics | bit-identical to the unfused eleven-buffer pipeline |
cube_root |
exhaustive over ~67M f32 values across the Lab domain (--release --ignored) |
| Unit + integration tests | 55 + 4 |
The MATLAB half shells out to Octave. Install it (brew install octave) and:
BLAZEDIFF_REQUIRE_OCTAVE=1 cargo test -p blazediff-ssim --test matlab_parity
Without Octave those tests report a skip and pass, so the default cargo test
needs no toolchain beyond Rust. BLAZEDIFF_REQUIRE_OCTAVE=1 turns a missing
Octave into a failure, which is what CI should set so parity cannot pass
vacuously.
Caveats
All three shipped metrics reduce to luma, so a change carried entirely by chroma
or by alpha is invisible to them. perceptual_ssim with ColorSpace::Lab and a
non-zero chroma_weight sees colour;
blazediff-ssim-benchmark
measures what each knob is worth against dssim on KADID-10k.
Scores are pooled over a local map, so these metrics say how much two images differ, not where beyond the resolution of that map. For exact locations, use a pixel diff.
License
MIT. The algorithms are published research; attributions are in
licenses/.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-win_arm64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-win_arm64.whl
- Upload date:
- Size: 580.4 kB
- Tags: CPython 3.8+, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6254ada45d1a08a6a36971303af8081845acbe4deb0dccd2b6668c2605df09d2
|
|
| MD5 |
101074e8744a8ddf81fa6d74274caab5
|
|
| BLAKE2b-256 |
968d53a24ffcbc6c57736e76d05e3e183ab59de7fe1db1bab5ec7711737e5adb
|
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 604.1 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
604950a166900fe5236d9336846b2d3c9607ced603e72ee2453fc8cbd1e237b4
|
|
| MD5 |
b401d6abb9019e2de37bbe788fe28acd
|
|
| BLAKE2b-256 |
21d2d443656952d6cba31377a433c41659c6efc9b5ffaef69577dab0e87c26b0
|
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 712.2 kB
- Tags: CPython 3.8+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a821b1c67cda44f4bf911001f5e7bfd77cb76d72f51075f1582142c96062e1b0
|
|
| MD5 |
d06b15c13b00b021c3d1bce97d5628de
|
|
| BLAKE2b-256 |
35205789874a78dacc2c6b7e66498b4f24e1cbf1cd0e41c7582b589429e8922d
|
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 672.5 kB
- Tags: CPython 3.8+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22024dd0f453e4b59e16d58b50fdfb7fc464c62fe975eb66944a75843c9344b2
|
|
| MD5 |
aba0dd6d44867fd7c40ca7bdce82914d
|
|
| BLAKE2b-256 |
2779da324ebd509ea2c5def4a7c288f5a99ed3f5fd29920feb770b2c60b25556
|
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 603.6 kB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
400cdbbddafb73b69850a440cbbdba7637bf582cb3fdc100fab6ab3852969e50
|
|
| MD5 |
b77b95c448c7411bd0888dc3b12433c1
|
|
| BLAKE2b-256 |
d54e6afc825b9c052a7fb008d76cd4fd21dbd53313b50329ecdb9c5a83334c20
|
File details
Details for the file blazediff_ssim-6.1.0-cp38-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: blazediff_ssim-6.1.0-cp38-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 630.2 kB
- Tags: CPython 3.8+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
maturin/1.15.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
637abefd94d18af24579a426ff2218da9e9cc7f690f52cf7b105a3d763361f17
|
|
| MD5 |
07cfae3ccbf2d01e6710513aec1c65e4
|
|
| BLAKE2b-256 |
2e460d724b5d266ce667d8853bd5ac2f78fb40e7d32f5da7f3bcdcfb624caaf9
|