1pub type Pt = (i32, i32);
14
15const DIRS: [Pt; 8] = [
17 (1, 0),
18 (1, -1),
19 (0, -1),
20 (-1, -1),
21 (-1, 0),
22 (-1, 1),
23 (0, 1),
24 (1, 1),
25];
26
27fn dir_of(a: Pt, b: Pt) -> usize {
29 let d = (b.0 - a.0, b.1 - a.1);
30 DIRS.iter().position(|&x| x == d).expect("两点不相邻")
31}
32
33pub struct Bitmap<'a> {
35 pub data: &'a [bool],
36 pub width: usize,
37 pub height: usize,
38}
39
40impl Bitmap<'_> {
41 #[inline]
42 fn fg(&self, p: Pt) -> bool {
43 p.0 >= 0
44 && p.1 >= 0
45 && (p.0 as usize) < self.width
46 && (p.1 as usize) < self.height
47 && self.data[p.1 as usize * self.width + p.0 as usize]
48 }
49}
50
51fn component_starts(bm: &Bitmap) -> Vec<Pt> {
53 let (w, h) = (bm.width, bm.height);
54 let mut seen = vec![false; w * h];
55 let mut starts = Vec::new();
56 let mut stack = Vec::new();
57 for i in 0..w * h {
58 if !bm.data[i] || seen[i] {
59 continue;
60 }
61 starts.push(((i % w) as i32, (i / w) as i32));
62 seen[i] = true;
63 stack.push(i);
64 while let Some(j) = stack.pop() {
65 let p = ((j % w) as i32, (j / w) as i32);
66 for d in DIRS {
67 let q = (p.0 + d.0, p.1 + d.1);
68 if bm.fg(q) {
69 let k = q.1 as usize * w + q.0 as usize;
70 if !seen[k] {
71 seen[k] = true;
72 stack.push(k);
73 }
74 }
75 }
76 }
77 }
78 starts
79}
80
81fn trace_outer(bm: &Bitmap, start: Pt) -> Vec<Pt> {
85 let west = 4;
87 let Some(d1) = (0..8)
88 .map(|k| (west + 8 - k) % 8)
89 .find(|&d| bm.fg((start.0 + DIRS[d].0, start.1 + DIRS[d].1)))
90 else {
91 return vec![start]; };
93 let p1 = (start.0 + DIRS[d1].0, start.1 + DIRS[d1].1);
94 let (mut p2, mut p3) = (p1, start);
95 let mut out = Vec::new();
96 loop {
97 out.push(p3);
98 let d2 = dir_of(p3, p2);
100 let p4 = (1..=8)
101 .map(|k| (d2 + k) % 8)
102 .map(|d| (p3.0 + DIRS[d].0, p3.1 + DIRS[d].1))
103 .find(|&q| bm.fg(q))
104 .expect("p2 本身是前景,必能找到");
105 if p4 == start && p3 == p1 {
107 break;
108 }
109 p2 = p3;
110 p3 = p4;
111 }
112 out
113}
114
115fn compress(path: Vec<Pt>) -> Vec<Pt> {
117 let n = path.len();
118 if n < 3 {
119 return path;
120 }
121 let step = |a: Pt, b: Pt| (b.0 - a.0, b.1 - a.1);
122 (0..n)
123 .filter(|&i| {
124 let prev = path[(i + n - 1) % n];
125 let next = path[(i + 1) % n];
126 step(prev, path[i]) != step(path[i], next)
127 })
128 .map(|i| path[i])
129 .collect()
130}
131
132pub fn outer_contours(bm: &Bitmap) -> Vec<Vec<Pt>> {
134 component_starts(bm)
135 .into_iter()
136 .map(|s| compress(trace_outer(bm, s)))
137 .collect()
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn bitmap(rows: &[&str]) -> (Vec<bool>, usize, usize) {
145 let w = rows[0].len();
146 let data = rows
147 .iter()
148 .flat_map(|r| r.bytes().map(|b| b == b'#'))
149 .collect();
150 (data, w, rows.len())
151 }
152
153 fn contours(rows: &[&str]) -> Vec<Vec<Pt>> {
154 let (data, width, height) = bitmap(rows);
155 outer_contours(&Bitmap {
156 data: &data,
157 width,
158 height,
159 })
160 }
161
162 #[test]
163 fn 矩形只留四个角() {
164 let c = contours(&["......", ".####.", ".####.", ".####.", "......"]);
165 assert_eq!(c.len(), 1);
166 assert_eq!(c[0], vec![(1, 1), (1, 3), (4, 3), (4, 1)]);
168 }
169
170 #[test]
171 fn 八邻接连通且贴边也行() {
172 let c = contours(&["#..", ".#.", "..#"]);
174 assert_eq!(c.len(), 1);
175 let mut pts = c[0].clone();
176 pts.sort();
177 pts.dedup();
178 assert_eq!(pts, vec![(0, 0), (2, 2)]);
179 }
180
181 #[test]
182 fn 多个连通域和孤立点() {
183 let c = contours(&["##..#", "##...", ".....", "..###"]);
184 assert_eq!(c.len(), 3);
185 assert_eq!(c[0], vec![(0, 0), (0, 1), (1, 1), (1, 0)]);
186 assert_eq!(c[1], vec![(4, 0)]);
187 assert_eq!(c[2], vec![(2, 3), (4, 3)]);
189 }
190
191 #[test]
192 fn 带洞的环只给外轮廓() {
193 let c = contours(&["#####", "#...#", "#...#", "#####"]);
194 assert_eq!(c.len(), 1);
195 assert_eq!(c[0], vec![(0, 0), (0, 3), (4, 3), (4, 0)]);
196 }
197
198 #[test]
199 fn 凹形轮廓经过每个外边界像素() {
200 let rows = ["#.#", "#.#", "###"];
201 let (data, width, height) = bitmap(&rows);
202 let bm = Bitmap {
203 data: &data,
204 width,
205 height,
206 };
207 let path = trace_outer(&bm, (0, 0));
208 let mut pts = path.clone();
209 pts.sort();
210 pts.dedup();
211 assert_eq!(pts.len(), 7, "{path:?}");
212 }
213}