1#[derive(Clone, Debug)]
19pub struct Contour {
20 pub points: Vec<(i32, i32)>,
22 pub is_hole: bool,
25}
26
27const NEI: [(i32, i32); 8] = [
29 (0, 1),
30 (1, 1),
31 (1, 0),
32 (1, -1),
33 (0, -1),
34 (-1, -1),
35 (-1, 0),
36 (-1, 1),
37];
38
39fn dir_index(dr: i32, dc: i32) -> usize {
40 NEI.iter().position(|&(r, c)| r == dr && c == dc).unwrap()
41}
42
43#[inline]
44fn get(img: &[i32], w: i32, h: i32, r: i32, c: i32) -> i32 {
45 if r < 0 || c < 0 || r >= h || c >= w {
46 0
47 } else {
48 img[(r * w + c) as usize]
49 }
50}
51
52pub fn find_contours(bin: &[u8], width: usize, height: usize) -> Vec<Contour> {
54 assert_eq!(bin.len(), width * height, "bin length must be width*height");
55 let (w, h) = (width as i32, height as i32);
56 let mut img: Vec<i32> = bin.iter().map(|&v| (v != 0) as i32).collect();
57
58 let mut contours = Vec::new();
59 let mut nbd = 1i32;
64
65 for i in 0..h {
66 for j in 0..w {
67 let fij = img[(i * w + j) as usize];
68 if fij == 0 {
69 continue;
70 }
71
72 let from;
73 let is_hole;
74 if fij == 1 && get(&img, w, h, i, j - 1) == 0 {
75 nbd += 1;
77 from = (i, j - 1);
78 is_hole = false;
79 } else if fij >= 1 && get(&img, w, h, i, j + 1) == 0 {
80 nbd += 1;
82 from = (i, j + 1);
83 is_hole = true;
84 } else {
85 continue;
86 }
87
88 let mut points = Vec::new();
89 border_follow(&mut img, w, h, (i, j), from, nbd, &mut points);
90 contours.push(Contour { points, is_hole });
91 }
92 }
93 contours
94}
95
96fn border_follow(
99 img: &mut [i32],
100 w: i32,
101 h: i32,
102 start: (i32, i32),
103 from: (i32, i32),
104 nbd: i32,
105 points: &mut Vec<(i32, i32)>,
106) {
107 let (i, j) = start;
108 let idx = |r: i32, c: i32| (r * w + c) as usize;
109
110 let from_dir = dir_index(from.0 - i, from.1 - j);
112 let mut found = None;
113 for k in 0..8 {
114 let d = (from_dir + k) & 7;
115 let (r, c) = (i + NEI[d].0, j + NEI[d].1);
116 if get(img, w, h, r, c) != 0 {
117 found = Some((r, c));
118 break;
119 }
120 }
121 let (i1, j1) = match found {
122 None => {
124 img[idx(i, j)] = -nbd;
125 points.push((j, i));
126 return;
127 }
128 Some(p) => p,
129 };
130
131 let (mut i2, mut j2) = (i1, j1);
132 let (mut i3, mut j3) = (i, j);
133 loop {
134 let d2 = dir_index(i2 - i3, j2 - j3);
136 let mut examined_east_zero = false;
137 let mut p4 = None;
138 for k in 1..=8 {
139 let d = (d2 + 8 - k) & 7;
140 let (r, c) = (i3 + NEI[d].0, j3 + NEI[d].1);
141 let val = get(img, w, h, r, c);
142 if d == 0 && val == 0 {
143 examined_east_zero = true;
145 }
146 if val != 0 {
147 p4 = Some((r, c));
148 break;
149 }
150 }
151 let (i4, j4) = p4.expect("border closes on itself");
152
153 if examined_east_zero {
155 img[idx(i3, j3)] = -nbd;
156 } else if img[idx(i3, j3)] == 1 {
157 img[idx(i3, j3)] = nbd;
158 }
159 points.push((j3, i3));
160
161 if (i4, j4) == (i, j) && (i3, j3) == (i1, j1) {
163 break;
164 }
165 (i2, j2) = (i3, j3);
166 (i3, j3) = (i4, j4);
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use std::collections::BTreeSet;
174
175 fn img(rows: &[&str]) -> (Vec<u8>, usize, usize) {
176 let h = rows.len();
177 let w = rows[0].len();
178 let mut data = vec![0u8; w * h];
179 for (r, row) in rows.iter().enumerate() {
180 for (c, ch) in row.chars().enumerate() {
181 data[r * w + c] = if ch == '#' { 255 } else { 0 };
182 }
183 }
184 (data, w, h)
185 }
186
187 fn pointset(c: &Contour) -> BTreeSet<(i32, i32)> {
188 c.points.iter().copied().collect()
189 }
190
191 #[test]
192 fn single_pixel() {
193 let (data, w, h) = img(&[".....", ".....", "..#..", ".....", "....."]);
194 let cs = find_contours(&data, w, h);
195 assert_eq!(cs.len(), 1);
196 assert_eq!(cs[0].points, vec![(2, 2)]);
197 assert!(!cs[0].is_hole);
198 }
199
200 #[test]
201 fn solid_square_outer_border() {
202 let (data, w, h) = img(&[".....", ".###.", ".###.", ".###.", "....."]);
203 let cs = find_contours(&data, w, h);
204 assert_eq!(cs.len(), 1);
205 assert!(!cs[0].is_hole);
206 let expected: BTreeSet<(i32, i32)> = [
208 (1, 1),
209 (2, 1),
210 (3, 1),
211 (1, 2),
212 (3, 2),
213 (1, 3),
214 (2, 3),
215 (3, 3),
216 ]
217 .into_iter()
218 .collect();
219 assert_eq!(pointset(&cs[0]), expected);
220 }
221
222 #[test]
223 fn ring_has_outer_and_hole_borders() {
224 let (data, w, h) = img(&[
226 ".......", ".#####.", ".#####.", ".##.##.", ".#####.", ".#####.", ".......",
227 ]);
228 let cs = find_contours(&data, w, h);
229 assert_eq!(cs.len(), 2, "expected one outer and one hole border");
230 assert!(!cs[0].is_hole, "first found border should be outer");
231 assert!(cs[1].is_hole, "second should be the hole border");
232 assert_eq!(
234 cs[0].points.iter().copied().collect::<BTreeSet<_>>().len(),
235 16
236 );
237 let hole = pointset(&cs[1]);
239 assert!(hole.contains(&(2, 3)) && hole.contains(&(4, 3)));
240 assert!(!hole.contains(&(3, 3)));
241 }
242
243 #[test]
244 fn two_separate_squares() {
245 let (data, w, h) = img(&[".......", ".##.##.", ".##.##.", "......."]);
246 let cs = find_contours(&data, w, h);
247 assert_eq!(cs.len(), 2);
248 assert!(cs.iter().all(|c| !c.is_hole));
249 }
250}