1use imgref::{ImgRef, ImgVec};
2
3use crate::{Histogram, RgbaPixel};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7#[non_exhaustive]
8pub struct Difference {
9 histogram: Histogram,
10
11 diff_image: Option<imgref::ImgVec<RgbaPixel>>,
12}
13
14impl Difference {
15 #[must_use]
17 pub fn histogram(&self) -> Histogram {
18 self.histogram
19 }
20
21 #[must_use]
30 pub fn diff_image(&self) -> Option<ImgRef<'_, RgbaPixel>> {
31 self.diff_image.as_ref().map(imgref::ImgExt::as_ref)
32 }
33}
34
35#[must_use]
49pub fn diff(actual: ImgRef<'_, RgbaPixel>, expected: ImgRef<'_, RgbaPixel>) -> Difference {
50 if dimensions(expected) != dimensions(actual) {
51 return Difference {
52 histogram: {
54 let mut h = [0; 256];
55 h[usize::from(u8::MAX)] = expected.pixels().len().max(actual.pixels().len());
56 Histogram(h)
57 },
58 diff_image: None,
59 };
60 }
61
62 let hd1 = half_diff(expected, actual);
63 let hd2 = half_diff(actual, expected);
64
65 let raw_diff_image: ImgVec<u8> = ImgVec::new(
67 (0..hd1.height())
68 .flat_map(|y| {
69 (0..hd1.width()).map({
70 let hd1 = &hd1;
71 let hd2 = &hd2;
72 move |x| core::cmp::max(hd1[(x, y)], hd2[(x, y)])
73 })
74 })
75 .collect(),
76 hd1.width(),
77 hd1.height(),
78 );
79
80 let mut histogram: [usize; 256] = [0; 256];
82 for diff_value in raw_diff_image.pixels() {
83 histogram[usize::from(diff_value)] += 1;
84 }
85 let histogram = Histogram(histogram);
86
87 Difference {
88 histogram,
89 diff_image: Some(crate::visualize::visualize(
90 expected,
91 raw_diff_image.as_ref(),
92 &histogram,
93 )),
94 }
95}
96
97fn dimensions<T>(image: imgref::ImgRef<'_, T>) -> [usize; 2] {
98 [image.width(), image.height()]
99}
100
101fn half_diff(have: ImgRef<'_, RgbaPixel>, want: ImgRef<'_, RgbaPixel>) -> ImgVec<u8> {
109 let have_elems = have.sub_image(1, 1, have.width() - 2, have.height() - 2);
110
111 let mut buffer: Vec<u8> = vec_for_same_size_image(have);
112 for (y, have_row) in have_elems.rows().enumerate() {
113 let want_rows: [&[RgbaPixel]; 3] = {
115 let mut iter = want.rows().skip(y);
117 std::array::from_fn(|_| iter.next().unwrap_or(&[]))
118 };
119
120 buffer.extend(have_row.iter().enumerate().map(move |(x, &have_pixel)| {
121 let neighborhood = [
129 want_rows[0][x],
130 want_rows[0][x + 1],
131 want_rows[0][x + 2],
132 want_rows[1][x],
133 want_rows[1][x + 1],
134 want_rows[1][x + 2],
135 want_rows[2][x],
136 want_rows[2][x + 1],
137 want_rows[2][x + 2],
138 ];
139 let minimum_diff_in_neighborhood: u8 = neighborhood
140 .into_iter()
141 .map(|want_pixel| pixel_diff(have_pixel, want_pixel))
142 .min()
143 .expect("neighborhood is never empty");
144 minimum_diff_in_neighborhood
145 }));
146 }
147
148 ImgVec::new(buffer, have_elems.width(), have_elems.height())
149}
150
151fn pixel_diff(a: RgbaPixel, b: RgbaPixel) -> u8 {
157 let r_diff = a[0].abs_diff(b[0]);
160 let g_diff = a[1].abs_diff(b[1]);
161 let b_diff = a[2].abs_diff(b[2]);
162 let a_diff = a[3].abs_diff(b[3]);
163
164 let color_diff = crate::image::rgba_to_luma([r_diff, g_diff, b_diff, 255]);
165
166 color_diff.max(a_diff)
167}
168
169#[cfg_attr(test, mutants::skip)] fn vec_for_same_size_image<T>(image: ImgRef<'_, impl Sized>) -> Vec<T> {
171 Vec::with_capacity(image.width() * image.height())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::Threshold;
179 use crate::image::luma_to_rgba;
180 use imgref::{Img, ImgExt as _};
181
182 fn diff_vecs(
185 (width, height): (usize, usize),
186 actual_data: Vec<RgbaPixel>,
187 expected_data: Vec<RgbaPixel>,
188 border_value: RgbaPixel,
189 ) -> Difference {
190 let expected = add_border(border_value, ImgVec::new(expected_data, width, height));
191 let actual = add_border(border_value, ImgVec::new(actual_data, width, height));
192 diff(actual.as_ref(), expected.as_ref())
193 }
194
195 #[allow(clippy::needless_pass_by_value)]
196 fn add_border<P: Copy>(border_value: P, image: ImgVec<P>) -> ImgVec<P> {
197 let width = image.width();
198 let height = image.height();
199 crate::image::from_fn(width + 2, height + 2, |x, y| {
200 if (1..=width).contains(&x) && (1..=height).contains(&y) {
201 image[(x - 1, y - 1)]
202 } else {
203 border_value
204 }
205 })
206 }
207
208 #[test]
209 fn simple_equality() {
210 let image = Img::new(
211 [0, 255, 128, 0, 255, 12, 255, 13, 99].map(luma_to_rgba),
212 3,
213 3,
214 );
215 let image = image.as_ref();
216 let diff_result = dbg!(diff(image, image));
217
218 let mut expected_histogram = [0; 256];
219 expected_histogram[0] = 1;
220 assert_eq!(diff_result.histogram, Histogram(expected_histogram));
221
222 assert!(Threshold::no_bigger_than(0).allows(diff_result.histogram));
223 assert!(Threshold::no_bigger_than(5).allows(diff_result.histogram));
224 assert!(Threshold::no_bigger_than(254).allows(diff_result.histogram));
225 }
226
227 #[test]
228 fn simple_inequality_thoroughly_examined() {
229 let base_pixel_value = 100u8;
230 let delta = 55u8;
231 let dred = 11; let display_scale = 3; let mut expected_histogram = [0; 256];
235 expected_histogram[usize::from(dred)] = 1;
236
237 let result_of_negative_difference = dbg!(diff_vecs(
239 (1, 1),
240 vec![[base_pixel_value, base_pixel_value, base_pixel_value, 255]],
241 vec![[
242 base_pixel_value + delta,
243 base_pixel_value,
244 base_pixel_value,
245 255
246 ]],
247 [base_pixel_value, base_pixel_value, base_pixel_value, 255],
248 ));
249 let result_of_positive_difference = dbg!(diff_vecs(
250 (1, 1),
251 vec![[
252 base_pixel_value + delta,
253 base_pixel_value,
254 base_pixel_value,
255 255
256 ]],
257 vec![[base_pixel_value, base_pixel_value, base_pixel_value, 255]],
258 [base_pixel_value, base_pixel_value, base_pixel_value, 255],
259 ));
260
261 assert_eq!(
263 result_of_positive_difference,
264 Difference {
265 histogram: Histogram(expected_histogram),
266 diff_image: Some(ImgVec::new(
267 vec![[(base_pixel_value) / display_scale, 255, 255, 255]],
268 1,
269 1,
270 ))
271 }
272 );
273 assert_eq!(
274 result_of_negative_difference,
275 Difference {
276 histogram: Histogram(expected_histogram),
277 diff_image: Some(ImgVec::new(
278 vec![[(base_pixel_value + dred) / display_scale, 255, 255, 255]],
279 1,
280 1,
281 ))
282 }
283 );
284
285 assert_eq!(
286 (
287 Threshold::no_bigger_than(dred - 1).allows(result_of_positive_difference.histogram),
288 Threshold::no_bigger_than(dred).allows(result_of_positive_difference.histogram),
289 ),
290 (false, true)
291 );
292 }
293
294 #[test]
299 fn diff_image_size() {
300 let image1 = crate::image::from_fn(10, 10, |_, _| [1, 2, 3, 255]);
301 let image2 = crate::image::from_fn(10, 10, |_, _| [100, 200, 255, 255]);
302 let diff_result = diff(image1.as_ref(), image2.as_ref());
303
304 let diff_image = diff_result.diff_image.unwrap();
305 assert_eq!((diff_image.width(), diff_image.height()), (8, 8));
306 }
307
308 #[test]
309 fn mismatched_sizes() {
310 let expected = ImgRef::new(&[[0, 0, 0, 255u8]], 1, 1);
311 let actual = ImgRef::new(&[[0, 0, 0, 255], [0, 0, 0, 255u8]], 1, 2);
312 assert_eq!(
313 diff(actual, expected),
314 Difference {
315 histogram: {
316 let mut h = [0; 256];
317 h[255] = 2;
318 Histogram(h)
319 },
320 diff_image: None
321 }
322 );
323 }
324
325 #[test]
328 fn shape_of_neighborhood() {
329 let test_image_with_center_pixel = crate::image::from_fn(7, 7, |x, y| {
330 if (x, y) == (3, 3) {
331 [255, 255, 255, 255]
332 } else {
333 [0, 0, 0, 255]
334 }
335 });
336
337 let passes_if_pixel_is_here = crate::image::from_fn(7, 7, |place_x, place_y| {
338 let test_image_with_displaced_pixel = crate::image::from_fn(7, 7, |x, y| {
339 if (x, y) == (place_x, place_y) {
340 [255, 255, 255, 255]
341 } else {
342 [0, 0, 0, 255]
343 }
344 });
345
346 diff(
347 test_image_with_displaced_pixel.as_ref(),
348 test_image_with_center_pixel.as_ref(),
349 )
350 .histogram()
351 .max_difference()
352 == 0
353 });
354 assert_eq!(
355 passes_if_pixel_is_here.into_buf(),
356 vec![
357 false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, true, true, false, false, false, false, true, true, true, false, false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, ]
365 );
366 }
367}