Skip to main content

agent_image_diff/
lib.rs

1pub mod antialias;
2pub mod classify;
3pub mod cli;
4pub mod cluster;
5pub mod color;
6pub mod compare;
7pub mod denoise;
8pub mod dilate;
9pub mod merge;
10pub mod output;
11pub mod region;
12
13/// High-level convenience function for library consumers.
14pub fn diff_images(
15    baseline: &image::RgbaImage,
16    candidate: &image::RgbaImage,
17    options: DiffOptions,
18) -> region::DiffResult {
19    let mut compare_result = compare::compare(
20        baseline,
21        candidate,
22        &compare::CompareOptions {
23            threshold: options.threshold,
24            detect_antialias: options.detect_antialias,
25        },
26    );
27
28    denoise::denoise(
29        &mut compare_result.diff_mask,
30        compare_result.width,
31        compare_result.height,
32        options.denoise,
33    );
34
35    let dilated_mask = dilate::dilate(
36        &compare_result.diff_mask,
37        compare_result.width,
38        compare_result.height,
39        options.dilate,
40    );
41
42    let labels = cluster::label_components(
43        &dilated_mask,
44        compare_result.width,
45        compare_result.height,
46        options.connectivity,
47    );
48
49    let mut regions = region::extract_regions(
50        &labels,
51        &compare_result.delta_map,
52        compare_result.width,
53        compare_result.height,
54        options.min_region_size,
55    );
56
57    merge::merge_regions(&mut regions, options.merge_distance);
58
59    classify::classify_regions(
60        &mut regions,
61        baseline,
62        candidate,
63        &compare_result.diff_mask,
64        compare_result.width,
65        compare_result.height,
66    );
67
68    let total_pixels = compare_result.width as u64 * compare_result.height as u64;
69    let changed_pixels = compare_result.diff_mask.iter().filter(|&&v| v).count() as u64;
70
71    let dimension_mismatch =
72        if baseline.width() != candidate.width() || baseline.height() != candidate.height() {
73            Some(region::DimensionMismatch {
74                baseline: region::Dimensions {
75                    width: baseline.width(),
76                    height: baseline.height(),
77                },
78                candidate: region::Dimensions {
79                    width: candidate.width(),
80                    height: candidate.height(),
81                },
82            })
83        } else {
84            None
85        };
86
87    region::DiffResult {
88        dimensions: region::Dimensions {
89            width: compare_result.width,
90            height: compare_result.height,
91        },
92        stats: region::DiffStats {
93            changed_pixels,
94            total_pixels,
95            diff_percentage: if total_pixels > 0 {
96                (changed_pixels as f64 / total_pixels as f64) * 100.0
97            } else {
98                0.0
99            },
100            region_count: regions.len(),
101            antialiased_pixels: compare_result.aa_pixel_count,
102        },
103        is_match: regions.is_empty(),
104        regions,
105        dimension_mismatch,
106    }
107}
108
109pub struct DiffOptions {
110    pub threshold: f64,
111    pub detect_antialias: bool,
112    pub connectivity: u8,
113    pub min_region_size: u32,
114    pub denoise: u32,
115    pub dilate: u32,
116    pub merge_distance: u32,
117}
118
119impl Default for DiffOptions {
120    fn default() -> Self {
121        Self {
122            threshold: 0.1,
123            detect_antialias: true,
124            connectivity: 8,
125            min_region_size: 25,
126            denoise: 25,
127            dilate: 0,
128            merge_distance: 50,
129        }
130    }
131}