1use image::{DynamicImage, GenericImageView, GrayImage, Luma};
4
5use crate::types::{Rect, VisionResult, VisualDiff};
6
7const DIFF_THRESHOLD: u8 = 30;
9
10const MIN_REGION_SIZE: u32 = 10;
12
13pub fn compute_diff(
15 before_id: u64,
16 after_id: u64,
17 img_a: &DynamicImage,
18 img_b: &DynamicImage,
19) -> VisionResult<VisualDiff> {
20 let (w_a, h_a) = img_a.dimensions();
21 let (w_b, h_b) = img_b.dimensions();
22
23 let target_w = w_a.min(w_b);
25 let target_h = h_a.min(h_b);
26
27 let gray_a = img_a
28 .resize_exact(target_w, target_h, image::imageops::FilterType::Nearest)
29 .to_luma8();
30 let gray_b = img_b
31 .resize_exact(target_w, target_h, image::imageops::FilterType::Nearest)
32 .to_luma8();
33
34 let mut diff_img = GrayImage::new(target_w, target_h);
36 let mut changed_pixels = 0u64;
37 let total_pixels = (target_w as u64) * (target_h as u64);
38
39 for y in 0..target_h {
40 for x in 0..target_w {
41 let a = gray_a.get_pixel(x, y).0[0];
42 let b = gray_b.get_pixel(x, y).0[0];
43 let d = (a as i16 - b as i16).unsigned_abs() as u8;
44 diff_img.put_pixel(x, y, Luma([d]));
45 if d > DIFF_THRESHOLD {
46 changed_pixels += 1;
47 }
48 }
49 }
50
51 let pixel_diff_ratio = if total_pixels > 0 {
52 changed_pixels as f32 / total_pixels as f32
53 } else {
54 0.0
55 };
56
57 let similarity = 1.0 - pixel_diff_ratio;
58 let changed_regions = find_changed_regions(&diff_img, DIFF_THRESHOLD);
59
60 Ok(VisualDiff {
61 before_id,
62 after_id,
63 similarity,
64 changed_regions,
65 pixel_diff_ratio,
66 })
67}
68
69fn find_changed_regions(diff_img: &GrayImage, threshold: u8) -> Vec<Rect> {
71 let (w, h) = diff_img.dimensions();
72 if w == 0 || h == 0 {
73 return Vec::new();
74 }
75
76 let cell_w = (w / 8).max(1);
78 let cell_h = (h / 8).max(1);
79 let mut regions = Vec::new();
80
81 for gy in 0..(h / cell_h).max(1) {
82 for gx in 0..(w / cell_w).max(1) {
83 let x0 = gx * cell_w;
84 let y0 = gy * cell_h;
85 let x1 = ((gx + 1) * cell_w).min(w);
86 let y1 = ((gy + 1) * cell_h).min(h);
87
88 let mut changed = 0u32;
89 let total = (x1 - x0) * (y1 - y0);
90
91 for y in y0..y1 {
92 for x in x0..x1 {
93 if diff_img.get_pixel(x, y).0[0] > threshold {
94 changed += 1;
95 }
96 }
97 }
98
99 if total > 0 && changed > total / 10 && (x1 - x0) >= MIN_REGION_SIZE {
101 regions.push(Rect {
102 x: x0,
103 y: y0,
104 w: x1 - x0,
105 h: y1 - y0,
106 });
107 }
108 }
109 }
110
111 merge_adjacent_regions(&mut regions);
113 regions
114}
115
116fn merge_adjacent_regions(regions: &mut Vec<Rect>) {
118 if regions.len() < 2 {
119 return;
120 }
121
122 let mut merged = true;
123 while merged {
124 merged = false;
125 let mut i = 0;
126 while i < regions.len() {
127 let mut j = i + 1;
128 while j < regions.len() {
129 if rects_adjacent(®ions[i], ®ions[j]) {
130 let a = regions[i];
131 let b = regions.remove(j);
132 regions[i] = merge_rects(&a, &b);
133 merged = true;
134 } else {
135 j += 1;
136 }
137 }
138 i += 1;
139 }
140 }
141}
142
143fn rects_adjacent(a: &Rect, b: &Rect) -> bool {
144 let a_right = a.x + a.w;
145 let a_bottom = a.y + a.h;
146 let b_right = b.x + b.w;
147 let b_bottom = b.y + b.h;
148
149 !(a_right < b.x || b_right < a.x || a_bottom < b.y || b_bottom < a.y)
151}
152
153fn merge_rects(a: &Rect, b: &Rect) -> Rect {
154 let x = a.x.min(b.x);
155 let y = a.y.min(b.y);
156 let right = (a.x + a.w).max(b.x + b.w);
157 let bottom = (a.y + a.h).max(b.y + b.h);
158 Rect {
159 x,
160 y,
161 w: right - x,
162 h: bottom - y,
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_identical_images() {
172 let img = DynamicImage::new_rgb8(100, 100);
173 let diff = compute_diff(1, 2, &img, &img).unwrap();
174 assert!((diff.similarity - 1.0).abs() < 0.01);
175 assert!(diff.pixel_diff_ratio < 0.01);
176 }
177
178 #[test]
179 fn test_different_images() {
180 let mut img_a = DynamicImage::new_rgb8(100, 100);
181 let img_b = DynamicImage::new_rgba8(100, 100);
182 if let Some(rgb) = img_a.as_mut_rgb8() {
184 for pixel in rgb.pixels_mut() {
185 *pixel = image::Rgb([255, 255, 255]);
186 }
187 }
188 let diff = compute_diff(1, 2, &img_a, &img_b).unwrap();
189 assert!(diff.similarity < 1.0);
190 }
191}