Metadata-Version: 2.4
Name: blazediff-interpret
Version: 6.3.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Multimedia :: Graphics
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Summary: Structured region analysis for image diffs: classify what changed, not just where
Keywords: image,diff,classification,visual-regression,screenshot
Author: Teimur Gasanov
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://blazediff.dev
Project-URL: Repository, https://github.com/teimurjan/blazediff

# 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:

| Producer | `ChangeSource` | How it finds regions |
| --- | --- | --- |
| [`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

| Item | Purpose |
| --- | --- |
| `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

