blazediff-interpret 6.4.0

Structured region analysis for image diffs: classify what changed, not just where
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InterpretResult {
    pub summary: String,
    pub diff_count: u32,
    pub total_regions: usize,
    pub regions: Vec<ChangeRegion>,
    pub severity: ChangeSeverity,
    pub diff_percentage: f64,
    pub width: u32,
    pub height: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ChangeRegion {
    pub bbox: BoundingBox,
    pub pixel_count: u32,
    pub percentage: f64,
    pub position: SpatialPosition,
    pub shape: ChangeShape,
    pub shape_stats: ShapeStats,
    pub change_type: ChangeType,
    pub signals: ClassificationSignals,
    pub confidence: f32,
    pub color_delta: ColorDeltaStats,
    pub gradient: GradientStats,
    pub chroma: ChromaStats,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChangeType {
    RenderingNoise,
    ContentChange,
    Addition,
    Deletion,
    Shift,
    ColorChange,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClassificationSignals {
    pub blends_with_bg_in_img1: bool,
    pub blends_with_bg_in_img2: bool,
    pub low_color_delta: bool,
    pub low_edge_change: bool,
    pub dense_fill: bool,
    pub sparse_fill: bool,
    pub tiny_region: bool,
    pub edges_correlated: bool,
    /// Normalized cross-correlation of luminance across the changed pixels.
    /// High values (~1.0) mean the structural pattern is preserved between
    /// img1 and img2; near-zero values mean the structure was replaced.
    pub luminance_ncc: f32,
    /// Signed asymmetry between img2 and img1 edge density. Positive values
    /// indicate img2 gained structure (additive), negative values indicate
    /// img1 lost structure (deletion).
    pub structure_asymmetry: f32,
    /// Normalized RGB distance of changed pixels from the local background in
    /// img1. Low values mean the pixels blended with the background there
    /// (content was absent before the change).
    pub bg_distance_img1: f32,
    /// Same measurement against img2.
    pub bg_distance_img2: f32,
    pub confidence: f32,
}

/// Chroma-plane statistics of the changed pixels, all normalized to 255.
/// Separates recolors (coherent, smooth chroma movement) from content
/// replacements (scattered chroma) on photographic edits where luminance
/// correlation alone saturates.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ChromaStats {
    /// Mean |ΔY| (luminance delta magnitude).
    pub mean_abs_dy: f32,
    /// Signed mean ΔY — a recolor often pushes luminance consistently in one
    /// direction, while a replacement balances out.
    pub mean_dy: f32,
    /// Mean |ΔI|.
    pub mean_abs_di: f32,
    /// Mean |ΔQ|.
    pub mean_abs_dq: f32,
    /// Mean chroma-delta magnitude √(ΔI²+ΔQ²).
    pub mean_abs_dc: f32,
    /// Cosine between the chroma vectors of img1 and img2, weighted by their
    /// magnitudes. Near 1 = same hues, negative = coherent hue rotation.
    pub chroma_cos: f32,
    /// Mean chroma magnitude (saturation) in img1.
    pub sat1: f32,
    /// Mean chroma magnitude in img2.
    pub sat2: f32,
    /// Roughness of the chroma-delta field: mean |∇|Δc|| between adjacent
    /// changed pixels over the std of |Δc|. Low = smooth recolor, high =
    /// patchy replacement.
    pub chroma_rough: f32,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ColorDeltaStats {
    pub mean_delta: f32,
    pub max_delta: f32,
    /// Standard deviation of per-pixel color deltas (normalized).
    /// Low = uniform shift (ColorChange), high = patchy (ContentChange).
    pub delta_stddev: f32,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GradientStats {
    pub edge_score: f32,
    pub edge_score_img2: f32,
    pub edge_correlation: f32,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ShapeStats {
    pub fill_ratio: f64,
    pub border_ratio: f64,
    pub inner_fill_ratio: f64,
    pub center_density: f64,
    pub row_occupancy: f64,
    pub col_occupancy: f64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChangeShape {
    SolidRegion,
    ContourFrame,
    SparseDistributed,
    EdgeDominated,
    MixedRegion,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct BoundingBox {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

impl BoundingBox {
    /// Expand the bounding box to include pixel (px, py).
    pub fn expand(&mut self, px: u32, py: u32) {
        let right = self.x + self.width;
        let bottom = self.y + self.height;
        if px < self.x {
            self.width += self.x - px;
            self.x = px;
        }
        if py < self.y {
            self.height += self.y - py;
            self.y = py;
        }
        if px + 1 > right {
            self.width = px + 1 - self.x;
        }
        if py + 1 > bottom {
            self.height = py + 1 - self.y;
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChangeSeverity {
    Low,
    Medium,
    High,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum SpatialPosition {
    TopLeft,
    Top,
    TopRight,
    Left,
    Center,
    Right,
    BottomLeft,
    Bottom,
    BottomRight,
}

impl std::fmt::Display for SpatialPosition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TopLeft => write!(f, "top-left"),
            Self::Top => write!(f, "top"),
            Self::TopRight => write!(f, "top-right"),
            Self::Left => write!(f, "left"),
            Self::Center => write!(f, "center"),
            Self::Right => write!(f, "right"),
            Self::BottomLeft => write!(f, "bottom-left"),
            Self::Bottom => write!(f, "bottom"),
            Self::BottomRight => write!(f, "bottom-right"),
        }
    }
}

impl std::fmt::Display for ChangeShape {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::SolidRegion => write!(f, "solid-region"),
            Self::ContourFrame => write!(f, "contour-frame"),
            Self::SparseDistributed => write!(f, "sparse-distributed"),
            Self::EdgeDominated => write!(f, "edge-dominated"),
            Self::MixedRegion => write!(f, "mixed-region"),
        }
    }
}

impl std::fmt::Display for ChangeSeverity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Low => write!(f, "low"),
            Self::Medium => write!(f, "medium"),
            Self::High => write!(f, "high"),
        }
    }
}

impl std::fmt::Display for ChangeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::RenderingNoise => write!(f, "rendering-noise"),
            Self::ContentChange => write!(f, "content-change"),
            Self::Addition => write!(f, "addition"),
            Self::Deletion => write!(f, "deletion"),
            Self::Shift => write!(f, "shift"),
            Self::ColorChange => write!(f, "color-change"),
        }
    }
}