Skip to main content

blazediff_interpret/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4#[serde(rename_all = "camelCase")]
5pub struct InterpretResult {
6    pub summary: String,
7    pub diff_count: u32,
8    pub total_regions: usize,
9    pub regions: Vec<ChangeRegion>,
10    pub severity: ChangeSeverity,
11    pub diff_percentage: f64,
12    pub width: u32,
13    pub height: u32,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "camelCase")]
18pub struct ChangeRegion {
19    pub bbox: BoundingBox,
20    pub pixel_count: u32,
21    pub percentage: f64,
22    pub position: SpatialPosition,
23    pub shape: ChangeShape,
24    pub shape_stats: ShapeStats,
25    pub change_type: ChangeType,
26    pub signals: ClassificationSignals,
27    pub confidence: f32,
28    pub color_delta: ColorDeltaStats,
29    pub gradient: GradientStats,
30    pub chroma: ChromaStats,
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "kebab-case")]
35pub enum ChangeType {
36    RenderingNoise,
37    ContentChange,
38    Addition,
39    Deletion,
40    Shift,
41    ColorChange,
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
45#[serde(rename_all = "camelCase")]
46pub struct ClassificationSignals {
47    pub blends_with_bg_in_img1: bool,
48    pub blends_with_bg_in_img2: bool,
49    pub low_color_delta: bool,
50    pub low_edge_change: bool,
51    pub dense_fill: bool,
52    pub sparse_fill: bool,
53    pub tiny_region: bool,
54    pub edges_correlated: bool,
55    /// Normalized cross-correlation of luminance across the changed pixels.
56    /// High values (~1.0) mean the structural pattern is preserved between
57    /// img1 and img2; near-zero values mean the structure was replaced.
58    pub luminance_ncc: f32,
59    /// Signed asymmetry between img2 and img1 edge density. Positive values
60    /// indicate img2 gained structure (additive), negative values indicate
61    /// img1 lost structure (deletion).
62    pub structure_asymmetry: f32,
63    /// Normalized RGB distance of changed pixels from the local background in
64    /// img1. Low values mean the pixels blended with the background there
65    /// (content was absent before the change).
66    pub bg_distance_img1: f32,
67    /// Same measurement against img2.
68    pub bg_distance_img2: f32,
69    pub confidence: f32,
70}
71
72/// Chroma-plane statistics of the changed pixels, all normalized to 255.
73/// Separates recolors (coherent, smooth chroma movement) from content
74/// replacements (scattered chroma) on photographic edits where luminance
75/// correlation alone saturates.
76#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
77#[serde(rename_all = "camelCase")]
78pub struct ChromaStats {
79    /// Mean |ΔY| (luminance delta magnitude).
80    pub mean_abs_dy: f32,
81    /// Signed mean ΔY — a recolor often pushes luminance consistently in one
82    /// direction, while a replacement balances out.
83    pub mean_dy: f32,
84    /// Mean |ΔI|.
85    pub mean_abs_di: f32,
86    /// Mean |ΔQ|.
87    pub mean_abs_dq: f32,
88    /// Mean chroma-delta magnitude √(ΔI²+ΔQ²).
89    pub mean_abs_dc: f32,
90    /// Cosine between the chroma vectors of img1 and img2, weighted by their
91    /// magnitudes. Near 1 = same hues, negative = coherent hue rotation.
92    pub chroma_cos: f32,
93    /// Mean chroma magnitude (saturation) in img1.
94    pub sat1: f32,
95    /// Mean chroma magnitude in img2.
96    pub sat2: f32,
97    /// Roughness of the chroma-delta field: mean |∇|Δc|| between adjacent
98    /// changed pixels over the std of |Δc|. Low = smooth recolor, high =
99    /// patchy replacement.
100    pub chroma_rough: f32,
101}
102
103#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
104#[serde(rename_all = "camelCase")]
105pub struct ColorDeltaStats {
106    pub mean_delta: f32,
107    pub max_delta: f32,
108    /// Standard deviation of per-pixel color deltas (normalized).
109    /// Low = uniform shift (ColorChange), high = patchy (ContentChange).
110    pub delta_stddev: f32,
111}
112
113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
114#[serde(rename_all = "camelCase")]
115pub struct GradientStats {
116    pub edge_score: f32,
117    pub edge_score_img2: f32,
118    pub edge_correlation: f32,
119}
120
121#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct ShapeStats {
124    pub fill_ratio: f64,
125    pub border_ratio: f64,
126    pub inner_fill_ratio: f64,
127    pub center_density: f64,
128    pub row_occupancy: f64,
129    pub col_occupancy: f64,
130}
131
132#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(rename_all = "kebab-case")]
134pub enum ChangeShape {
135    SolidRegion,
136    ContourFrame,
137    SparseDistributed,
138    EdgeDominated,
139    MixedRegion,
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
143pub struct BoundingBox {
144    pub x: u32,
145    pub y: u32,
146    pub width: u32,
147    pub height: u32,
148}
149
150impl BoundingBox {
151    /// Expand the bounding box to include pixel (px, py).
152    pub fn expand(&mut self, px: u32, py: u32) {
153        let right = self.x + self.width;
154        let bottom = self.y + self.height;
155        if px < self.x {
156            self.width += self.x - px;
157            self.x = px;
158        }
159        if py < self.y {
160            self.height += self.y - py;
161            self.y = py;
162        }
163        if px + 1 > right {
164            self.width = px + 1 - self.x;
165        }
166        if py + 1 > bottom {
167            self.height = py + 1 - self.y;
168        }
169    }
170}
171
172#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
173#[serde(rename_all = "kebab-case")]
174pub enum ChangeSeverity {
175    Low,
176    Medium,
177    High,
178}
179
180#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
181#[serde(rename_all = "kebab-case")]
182pub enum SpatialPosition {
183    TopLeft,
184    Top,
185    TopRight,
186    Left,
187    Center,
188    Right,
189    BottomLeft,
190    Bottom,
191    BottomRight,
192}
193
194impl std::fmt::Display for SpatialPosition {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        match self {
197            Self::TopLeft => write!(f, "top-left"),
198            Self::Top => write!(f, "top"),
199            Self::TopRight => write!(f, "top-right"),
200            Self::Left => write!(f, "left"),
201            Self::Center => write!(f, "center"),
202            Self::Right => write!(f, "right"),
203            Self::BottomLeft => write!(f, "bottom-left"),
204            Self::Bottom => write!(f, "bottom"),
205            Self::BottomRight => write!(f, "bottom-right"),
206        }
207    }
208}
209
210impl std::fmt::Display for ChangeShape {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        match self {
213            Self::SolidRegion => write!(f, "solid-region"),
214            Self::ContourFrame => write!(f, "contour-frame"),
215            Self::SparseDistributed => write!(f, "sparse-distributed"),
216            Self::EdgeDominated => write!(f, "edge-dominated"),
217            Self::MixedRegion => write!(f, "mixed-region"),
218        }
219    }
220}
221
222impl std::fmt::Display for ChangeSeverity {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            Self::Low => write!(f, "low"),
226            Self::Medium => write!(f, "medium"),
227            Self::High => write!(f, "high"),
228        }
229    }
230}
231
232impl std::fmt::Display for ChangeType {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            Self::RenderingNoise => write!(f, "rendering-noise"),
236            Self::ContentChange => write!(f, "content-change"),
237            Self::Addition => write!(f, "addition"),
238            Self::Deletion => write!(f, "deletion"),
239            Self::Shift => write!(f, "shift"),
240            Self::ColorChange => write!(f, "color-change"),
241        }
242    }
243}