Skip to main content

agent_image_diff/
region.rs

1use serde::Serialize;
2use std::collections::HashMap;
3
4#[derive(Debug, Serialize)]
5pub struct DiffResult {
6    pub dimensions: Dimensions,
7    pub stats: DiffStats,
8    #[serde(rename = "match")]
9    pub is_match: bool,
10    pub regions: Vec<Region>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub dimension_mismatch: Option<DimensionMismatch>,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct Dimensions {
17    pub width: u32,
18    pub height: u32,
19}
20
21#[derive(Debug, Serialize)]
22pub struct DiffStats {
23    pub changed_pixels: u64,
24    pub total_pixels: u64,
25    pub diff_percentage: f64,
26    pub region_count: usize,
27    pub antialiased_pixels: u64,
28}
29
30#[derive(Debug, Serialize)]
31pub struct Region {
32    pub id: u32,
33    pub bounding_box: BoundingBox,
34    pub pixel_count: u32,
35    pub avg_delta: f64,
36    pub max_delta: f64,
37    pub label: String,
38    /// Component label IDs from clustering that belong to this region.
39    /// Used internally for mapping pixels to regions in the image renderer.
40    #[serde(skip)]
41    pub component_ids: Vec<u32>,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct BoundingBox {
46    pub x: u32,
47    pub y: u32,
48    pub width: u32,
49    pub height: u32,
50}
51
52#[derive(Debug, Clone, Serialize)]
53pub struct DimensionMismatch {
54    pub baseline: Dimensions,
55    pub candidate: Dimensions,
56}
57
58struct RegionAccumulator {
59    min_x: u32,
60    min_y: u32,
61    max_x: u32,
62    max_y: u32,
63    pixel_count: u32,
64    delta_sum: f64,
65    max_delta: f64,
66}
67
68pub fn extract_regions(
69    labels: &[u32],
70    delta_map: &[f64],
71    width: u32,
72    height: u32,
73    min_region_size: u32,
74) -> Vec<Region> {
75    let mut accumulators: HashMap<u32, RegionAccumulator> = HashMap::new();
76
77    for y in 0..height {
78        for x in 0..width {
79            let idx = (y * width + x) as usize;
80            let label = labels[idx];
81            if label == 0 {
82                continue;
83            }
84            let delta = delta_map[idx];
85            accumulators
86                .entry(label)
87                .and_modify(|acc| {
88                    acc.min_x = acc.min_x.min(x);
89                    acc.min_y = acc.min_y.min(y);
90                    acc.max_x = acc.max_x.max(x);
91                    acc.max_y = acc.max_y.max(y);
92                    acc.pixel_count += 1;
93                    acc.delta_sum += delta;
94                    acc.max_delta = acc.max_delta.max(delta);
95                })
96                .or_insert(RegionAccumulator {
97                    min_x: x,
98                    min_y: y,
99                    max_x: x,
100                    max_y: y,
101                    pixel_count: 1,
102                    delta_sum: delta,
103                    max_delta: delta,
104                });
105        }
106    }
107
108    let mut regions: Vec<Region> = accumulators
109        .into_iter()
110        .filter(|(_, acc)| acc.pixel_count >= min_region_size)
111        .map(|(component_id, acc)| Region {
112            id: 0, // assigned after sorting
113            bounding_box: BoundingBox {
114                x: acc.min_x,
115                y: acc.min_y,
116                width: acc.max_x - acc.min_x + 1,
117                height: acc.max_y - acc.min_y + 1,
118            },
119            pixel_count: acc.pixel_count,
120            avg_delta: acc.delta_sum / acc.pixel_count as f64,
121            max_delta: acc.max_delta,
122            label: String::new(),
123            component_ids: vec![component_id],
124        })
125        .collect();
126
127    // Sort by pixel count descending (largest region first)
128    regions.sort_by(|a, b| b.pixel_count.cmp(&a.pixel_count));
129
130    // Assign contiguous IDs
131    for (i, region) in regions.iter_mut().enumerate() {
132        region.id = (i + 1) as u32;
133    }
134
135    regions
136}