blazediff-interpret 6.0.0

Structured region analysis for image diffs: classify what changed, not just where
Documentation

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.

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 Diff connected components over a pixel-diff mask
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:

// 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
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.

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, fill ratios, and the signals the classifier used. See INTERPRET.md for the full algorithm: pipeline stages, formulas, and classification rules.

License

MIT