# blazediff-interpret
Structured region analysis for image diffs. Given two images and a set of changed regions, it
says *what* changed in each one — not just where.
```rust
use blazediff_interpret::{interpret, ChangeSource};
// From a pixel diff — what `blazediff` does.
let result = interpret(&expected, &actual, ChangeSource::Diff {
output: &diff_image.data,
diff_count: diff.diff_count,
diff_percentage: diff.diff_percentage,
})?;
// From a similarity map — what `blazediff-ssim` does.
let result = interpret(&expected, &actual, ChangeSource::ScoreMap {
map: &outcome.map,
width: outcome.map_width,
height: outcome.map_height,
floor: 0.99,
})?;
// From boxes you already have.
let result = interpret(&expected, &actual, ChangeSource::Regions(&boxes))?;
println!("{}", result.summary);
for region in &result.regions {
println!("{:?} at {} ({:.2}%)", region.change_type, region.position, region.percentage);
}
```
## Why it's a separate crate
The classifier is deliberately independent of whatever *found* the regions. Three producers feed
it today:
| [`blazediff`](https://crates.io/crates/blazediff) | `Diff` | connected components over a pixel-diff mask |
| [`blazediff-ssim`](https://crates.io/crates/blazediff-ssim) | `ScoreMap` | thresholding a local SSIM score map |
| your code | `Regions` | DOM rectangles, a JS-side diff, a crop list — anything |
All three call the same function and get identical treatment; only the description of *where*
differs. `blazediff` and `blazediff-ssim` are independent of each other, so a classifier living in
either would be unreachable from the other. It sits below both instead.
## Coarse regions are fine
A producer only has to know roughly where something changed. Before any statistic is computed,
the supplied boxes are refined against the source pixels — every pixel whose YIQ delta falls
below the noise floor is dropped — so shape, colour and gradient analysis stay per-pixel no
matter how blocky the input was. (If a claimed box refines to nothing but the content does
differ — a sub-threshold edit such as a subtle uniform recolor — the box is kept as-is so the
region still gets meaningful statistics.)
```rust
// An 8x8 change, described exactly and then quantized to a 16px grid.
let exact = interpret(&a, &b, ChangeSource::Regions(&[BoundingBox { x: 16, y: 16, width: 8, height: 8 }]))?;
let coarse = interpret(&a, &b, ChangeSource::Regions(&[BoundingBox { x: 16, y: 16, width: 16, height: 16 }]))?;
assert_eq!(coarse.diff_count, exact.diff_count); // both 64
```
That is what makes an SSIM window map a usable region source: its grid is coarse, but the
statistics derived from it are not. `diff_count` therefore means the same thing on every path —
actually-changed pixels, never windows.
## API
| `interpret` | the entry point: a `ChangeSource` in, a full `InterpretResult` out |
| `ChangeSource` | `Diff` (a pixel diff's output + counts), `ScoreMap` (a similarity map), or `Regions` |
| `classify_region` / `classify_regions` | classify against a mask you already hold |
| `detect_regions` | connected components over a boolean mask |
| `merge_overlapping_components` | fuse fragmented components whose bboxes overlap or nearly touch |
| `extract_change_mask` | recover a mask from an RGBA diff visualization |
| `detect_shifts` | the shift-relabeling pass, for producers holding an exact mask |
| `classify_severity`, `build_summary` | the pooling steps, exposed for custom pipelines |
Regions arriving from a caller are validated: a box outside the image is an
`InterpretError::RegionOutOfBounds`, not an out-of-bounds panic. That matters now that regions
cross the wasm and N-API boundaries.
### Python - `blazediff-interpret`
```bash
pip install blazediff-interpret
```
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.
```python
import blazediff_interpret as interpret
result = interpret.interpret_images("expected.png", "actual.png", "diff.png")
print(result["summary"])
for region in result["regions"]:
print(region["changeType"], region["bbox"])
# Also: interpret_buffers(bytes, bytes), interpret_ssim(base, compare,
# metric=...) and interpret_regions(base, compare, regions).
```
The result crosses as a plain dict with camelCase keys, matching the N-API
binding and the CLI's `--json`. `interpret_regions` takes `(x, y, width,
height)` tuples or mappings with those keys, so a `bbox` from a prior result
feeds straight back in.
## What it classifies
Each region gets a change type, a shape, a position, a confidence, and the statistics behind
them — colour delta, gradient/edge correlation, luminance correlation, chroma-plane movement
(hue rotation, saturation, delta smoothness), fill ratios, and the signals the classifier used.
See [INTERPRET.md](https://github.com/teimurjan/blazediff/blob/main/crates/blazediff/INTERPRET.md)
for the full algorithm: pipeline stages, formulas, and classification rules.
## License
MIT