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}
31
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "kebab-case")]
34pub enum ChangeType {
35    RenderingNoise,
36    ContentChange,
37    Addition,
38    Deletion,
39    Shift,
40    ColorChange,
41}
42
43#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct ClassificationSignals {
46    pub blends_with_bg_in_img1: bool,
47    pub blends_with_bg_in_img2: bool,
48    pub low_color_delta: bool,
49    pub low_edge_change: bool,
50    pub dense_fill: bool,
51    pub sparse_fill: bool,
52    pub tiny_region: bool,
53    pub edges_correlated: bool,
54    /// Normalized cross-correlation of luminance across the changed pixels.
55    /// High values (~1.0) mean the structural pattern is preserved between
56    /// img1 and img2; near-zero values mean the structure was replaced.
57    pub luminance_ncc: f32,
58    /// Signed asymmetry between img2 and img1 edge density. Positive values
59    /// indicate img2 gained structure (additive), negative values indicate
60    /// img1 lost structure (deletion).
61    pub structure_asymmetry: f32,
62    pub confidence: f32,
63}
64
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct ColorDeltaStats {
68    pub mean_delta: f32,
69    pub max_delta: f32,
70    /// Standard deviation of per-pixel color deltas (normalized).
71    /// Low = uniform shift (ColorChange), high = patchy (ContentChange).
72    pub delta_stddev: f32,
73}
74
75#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
76#[serde(rename_all = "camelCase")]
77pub struct GradientStats {
78    pub edge_score: f32,
79    pub edge_score_img2: f32,
80    pub edge_correlation: f32,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
84#[serde(rename_all = "camelCase")]
85pub struct ShapeStats {
86    pub fill_ratio: f64,
87    pub border_ratio: f64,
88    pub inner_fill_ratio: f64,
89    pub center_density: f64,
90    pub row_occupancy: f64,
91    pub col_occupancy: f64,
92}
93
94#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
95#[serde(rename_all = "kebab-case")]
96pub enum ChangeShape {
97    SolidRegion,
98    ContourFrame,
99    SparseDistributed,
100    EdgeDominated,
101    MixedRegion,
102}
103
104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
105pub struct BoundingBox {
106    pub x: u32,
107    pub y: u32,
108    pub width: u32,
109    pub height: u32,
110}
111
112impl BoundingBox {
113    /// Expand the bounding box to include pixel (px, py).
114    pub fn expand(&mut self, px: u32, py: u32) {
115        let right = self.x + self.width;
116        let bottom = self.y + self.height;
117        if px < self.x {
118            self.width += self.x - px;
119            self.x = px;
120        }
121        if py < self.y {
122            self.height += self.y - py;
123            self.y = py;
124        }
125        if px + 1 > right {
126            self.width = px + 1 - self.x;
127        }
128        if py + 1 > bottom {
129            self.height = py + 1 - self.y;
130        }
131    }
132}
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
135#[serde(rename_all = "kebab-case")]
136pub enum ChangeSeverity {
137    Low,
138    Medium,
139    High,
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
143#[serde(rename_all = "kebab-case")]
144pub enum SpatialPosition {
145    TopLeft,
146    Top,
147    TopRight,
148    Left,
149    Center,
150    Right,
151    BottomLeft,
152    Bottom,
153    BottomRight,
154}
155
156impl std::fmt::Display for SpatialPosition {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            Self::TopLeft => write!(f, "top-left"),
160            Self::Top => write!(f, "top"),
161            Self::TopRight => write!(f, "top-right"),
162            Self::Left => write!(f, "left"),
163            Self::Center => write!(f, "center"),
164            Self::Right => write!(f, "right"),
165            Self::BottomLeft => write!(f, "bottom-left"),
166            Self::Bottom => write!(f, "bottom"),
167            Self::BottomRight => write!(f, "bottom-right"),
168        }
169    }
170}
171
172impl std::fmt::Display for ChangeShape {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            Self::SolidRegion => write!(f, "solid-region"),
176            Self::ContourFrame => write!(f, "contour-frame"),
177            Self::SparseDistributed => write!(f, "sparse-distributed"),
178            Self::EdgeDominated => write!(f, "edge-dominated"),
179            Self::MixedRegion => write!(f, "mixed-region"),
180        }
181    }
182}
183
184impl std::fmt::Display for ChangeSeverity {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        match self {
187            Self::Low => write!(f, "low"),
188            Self::Medium => write!(f, "medium"),
189            Self::High => write!(f, "high"),
190        }
191    }
192}
193
194impl std::fmt::Display for ChangeType {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        match self {
197            Self::RenderingNoise => write!(f, "rendering-noise"),
198            Self::ContentChange => write!(f, "content-change"),
199            Self::Addition => write!(f, "addition"),
200            Self::Deletion => write!(f, "deletion"),
201            Self::Shift => write!(f, "shift"),
202            Self::ColorChange => write!(f, "color-change"),
203        }
204    }
205}