agent_image_diff/
region.rs1use 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 #[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, 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 regions.sort_by(|a, b| b.pixel_count.cmp(&a.pixel_count));
129
130 for (i, region) in regions.iter_mut().enumerate() {
132 region.id = (i + 1) as u32;
133 }
134
135 regions
136}