Skip to main content

agent_image_diff/
dilate.rs

1/// Morphological dilation of a boolean mask.
2///
3/// Expands true pixels by `radius` in all directions using separable
4/// box dilation (horizontal pass then vertical pass). O(n) per pass
5/// regardless of radius.
6///
7/// The dilated mask is used for clustering only — original mask is
8/// preserved for accurate pixel statistics.
9pub fn dilate(mask: &[bool], width: u32, height: u32, radius: u32) -> Vec<bool> {
10    if radius == 0 {
11        return mask.to_vec();
12    }
13
14    let w = width as usize;
15    let h = height as usize;
16    let r = radius as usize;
17
18    // Horizontal pass: for each row, dilate along x
19    let mut horiz = vec![false; w * h];
20    for y in 0..h {
21        let row_start = y * w;
22        // Count of true pixels in the sliding window
23        // Window covers [x - r, x + r]
24        let mut count = 0usize;
25
26        // Initialize window for x=0: covers [0, min(r, w-1)]
27        for wx in 0..=(r.min(w - 1)) {
28            if mask[row_start + wx] {
29                count += 1;
30            }
31        }
32
33        for x in 0..w {
34            if count > 0 {
35                horiz[row_start + x] = true;
36            }
37            // Slide window: add pixel entering on the right, remove pixel leaving on the left
38            let add_x = x + r + 1;
39            if add_x < w && mask[row_start + add_x] {
40                count += 1;
41            }
42            if x >= r {
43                let remove_x = x - r;
44                if mask[row_start + remove_x] {
45                    count -= 1;
46                }
47            }
48        }
49    }
50
51    // Vertical pass: for each column, dilate along y using horiz as input
52    let mut result = vec![false; w * h];
53    for x in 0..w {
54        let mut count = 0usize;
55
56        // Initialize window for y=0
57        for wy in 0..=(r.min(h - 1)) {
58            if horiz[wy * w + x] {
59                count += 1;
60            }
61        }
62
63        for y in 0..h {
64            if count > 0 {
65                result[y * w + x] = true;
66            }
67            let add_y = y + r + 1;
68            if add_y < h && horiz[add_y * w + x] {
69                count += 1;
70            }
71            if y >= r {
72                let remove_y = y - r;
73                if horiz[remove_y * w + x] {
74                    count -= 1;
75                }
76            }
77        }
78    }
79
80    result
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn radius_zero_is_noop() {
89        let mask = vec![false, true, false, false];
90        let result = dilate(&mask, 2, 2, 0);
91        assert_eq!(result, mask);
92    }
93
94    #[test]
95    fn single_pixel_expands() {
96        // 5x5 grid with one true pixel at center (2,2)
97        let mut mask = vec![false; 25];
98        mask[2 * 5 + 2] = true;
99
100        let result = dilate(&mask, 5, 5, 1);
101
102        // Should expand to a cross/diamond shape with radius 1
103        // Center and all 4-neighbors should be true
104        assert!(result[2 * 5 + 2]); // center
105        assert!(result[1 * 5 + 2]); // up
106        assert!(result[3 * 5 + 2]); // down
107        assert!(result[2 * 5 + 1]); // left
108        assert!(result[2 * 5 + 3]); // right
109        // Corners at distance 2 should also be true (box dilation, not diamond)
110        assert!(result[1 * 5 + 1]); // up-left
111        assert!(result[1 * 5 + 3]); // up-right
112        assert!(result[3 * 5 + 1]); // down-left
113        assert!(result[3 * 5 + 3]); // down-right
114    }
115
116    #[test]
117    fn nearby_pixels_merge() {
118        // Two pixels 3 apart: (0,0) and (0,4) in a 5x1 grid
119        let mask = vec![true, false, false, false, true];
120        let result = dilate(&mask, 5, 1, 2);
121        // With radius 2, they should merge into one contiguous block
122        assert!(result.iter().all(|&v| v));
123    }
124
125    #[test]
126    fn distant_pixels_stay_separate() {
127        // Two pixels far apart in a 10x1 grid
128        let mut mask = vec![false; 10];
129        mask[0] = true;
130        mask[9] = true;
131        let result = dilate(&mask, 10, 1, 2);
132        // Pixel 0 expands to [0,2], pixel 9 expands to [7,9]
133        // Middle pixels [3,6] should remain false
134        assert!(!result[4]);
135        assert!(!result[5]);
136    }
137}