Skip to main content

agent_image_diff/
compare.rs

1use image::RgbaImage;
2use rayon::prelude::*;
3
4use crate::antialias;
5use crate::color;
6
7pub struct CompareResult {
8    pub diff_mask: Vec<bool>,
9    pub delta_map: Vec<f64>,
10    pub width: u32,
11    pub height: u32,
12    pub aa_pixel_count: u64,
13}
14
15pub struct CompareOptions {
16    pub threshold: f64,
17    pub detect_antialias: bool,
18}
19
20/// Compare two images pixel by pixel.
21///
22/// If images have different dimensions, the comparison area is the
23/// minimum of both dimensions. Pixels outside the intersection in
24/// the larger image are marked as different.
25pub fn compare(img1: &RgbaImage, img2: &RgbaImage, options: &CompareOptions) -> CompareResult {
26    let width = img1.width().min(img2.width());
27    let height = img1.height().min(img2.height());
28    let max_width = img1.width().max(img2.width());
29    let max_height = img1.height().max(img2.height());
30
31    let total = max_width as usize * max_height as usize;
32    let mut diff_mask = vec![false; total];
33    let mut delta_map = vec![0.0f64; total];
34
35    // Mark pixels outside the intersection as different
36    if max_width > width || max_height > height {
37        for y in 0..max_height {
38            for x in 0..max_width {
39                if x >= width || y >= height {
40                    let idx = (y * max_width + x) as usize;
41                    diff_mask[idx] = true;
42                    delta_map[idx] = 1.0;
43                }
44            }
45        }
46    }
47
48    // Compare the intersection region in parallel across rows
49    let rows: Vec<(Vec<bool>, Vec<f64>, u64)> = (0..height)
50        .into_par_iter()
51        .map(|y| {
52            let mut row_mask = vec![false; width as usize];
53            let mut row_delta = vec![0.0f64; width as usize];
54            let mut row_aa = 0u64;
55
56            for x in 0..width {
57                let px1 = img1.get_pixel(x, y).0;
58                let px2 = img2.get_pixel(x, y).0;
59
60                // Fast path: identical pixels
61                if px1 == px2 {
62                    continue;
63                }
64
65                let delta = color::color_delta(px1, px2, false);
66                row_delta[x as usize] = delta;
67
68                if delta > options.threshold {
69                    if options.detect_antialias
70                        && (antialias::is_antialiased(img1, img2, x, y)
71                            || antialias::is_antialiased(img2, img1, x, y))
72                    {
73                        row_aa += 1;
74                    } else {
75                        row_mask[x as usize] = true;
76                    }
77                }
78            }
79
80            (row_mask, row_delta, row_aa)
81        })
82        .collect();
83
84    // Flatten rows into the output arrays
85    let mut total_aa = 0u64;
86    for (y, (row_mask, row_delta, row_aa)) in rows.into_iter().enumerate() {
87        let row_start = y * max_width as usize;
88        for (x, (mask, delta)) in row_mask.into_iter().zip(row_delta).enumerate() {
89            diff_mask[row_start + x] = mask;
90            delta_map[row_start + x] = delta;
91        }
92        total_aa += row_aa;
93    }
94
95    CompareResult {
96        diff_mask,
97        delta_map,
98        width: max_width,
99        height: max_height,
100        aa_pixel_count: total_aa,
101    }
102}