Skip to main content

pagesift_ocr/det/
contour.rs

1//! 二值图的连通域与外轮廓,取代参考实现里的 `cv2.findContours`。
2//!
3//! - 前景按 8 邻接连通(与 OpenCV 对前景的约定相同),图外一律当背景;
4//! - 每个连通域从它在光栅顺序里的第一个像素出发,用 Suzuki–Abe(1985)的外边界跟踪
5//!   绕一圈,得到外轮廓上的像素中心坐标;
6//! - 只要外轮廓:参考实现用 `RETR_LIST`,还会把连通域里的洞的边界当成轮廓,那些多半是
7//!   行内的重复小框,这里不要;
8//! - 输出像 `CHAIN_APPROX_SIMPLE` 一样,把同方向的连续步并成一段、只留拐点。
9//!
10//! 后面只用到轮廓的凸包(最小外接矩形),所以拐点压缩不改变结果。
11
12/// 一个像素坐标 (x, y)。
13pub type Pt = (i32, i32);
14
15/// [协议] 8 个方向,按屏幕上的逆时针排(y 向下):东、东北、北、西北、西、西南、南、东南。
16const 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
27/// 从 `a` 指向相邻像素 `b` 的方向下标。
28fn 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
33/// 二值图(按行存放)。
34pub 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
51/// 给 8 邻接连通域编号,返回每个连通域在光栅顺序里的第一个像素(也就是它最上一行最左的像素)。
52fn 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
81/// 从外边界的起点 `start`(左邻是背景)出发,沿外边界逆时针(屏幕上)跟踪一圈。
82/// Suzuki–Abe 的步骤 3.1–3.5:先顺时针找到最后一个边界点 p1,再反复以当前点为中心、
83/// 从上一点的下一个方向起逆时针找下一点,回到起点且当前点是 p1 时结束。
84fn trace_outer(bm: &Bitmap, start: Pt) -> Vec<Pt> {
85    // 3.1:从西邻起顺时针找第一个前景邻点
86    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]; // 孤立像素
92    };
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        // 3.3:从 p2 之后的方向起逆时针找
99        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        // 3.5
106        if p4 == start && p3 == p1 {
107            break;
108        }
109        p2 = p3;
110        p3 = p4;
111    }
112    out
113}
114
115/// 把同方向的连续步合并,只留拐点(`CHAIN_APPROX_SIMPLE`)。
116fn 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
132/// 二值图里所有连通域的外轮廓(拐点),按连通域的光栅顺序排。
133pub 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        // 屏幕上逆时针:左上 → 左下 → 右下 → 右上
167        assert_eq!(c[0], vec![(1, 1), (1, 3), (4, 3), (4, 1)]);
168    }
169
170    #[test]
171    fn 八邻接连通且贴边也行() {
172        // 对角相连算一个连通域;贴着图边
173        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        // 单行的连通域:来回各走一遍,压缩后剩两端
188        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}