Skip to main content

pagesift_ocr/det/
postprocess.rs

1//! DB 后处理:概率图 → 二值图 → 连通域外轮廓 → 最小外接矩形 → 框内平均分 → unclip 外扩 →
2//! 再取最小外接矩形 → 映射回原图坐标。语义照 transformers 的
3//! `PPOCRV5ServerDetImageProcessor.post_process_object_detection`(Apache-2.0)。
4
5use super::contour::{self, Bitmap};
6use super::geometry::{self, P};
7use crate::Quad;
8
9/// DB 后处理参数。
10#[derive(Debug, Clone)]
11pub struct DbParams {
12    /// 概率高于它的像素算文字。
13    pub thresh: f32,
14    /// 框内平均概率不低于它才保留。
15    pub box_thresh: f32,
16    /// 最多处理的轮廓数。
17    pub max_candidates: usize,
18    /// 外扩前短边至少这么长(概率图像素);外扩后至少再长 2。
19    pub min_size: f64,
20    /// 外扩比例。
21    pub unclip_ratio: f64,
22}
23
24impl Default for DbParams {
25    fn default() -> Self {
26        // [协议] post_process_object_detection 的缺省:threshold 0.3、box_threshold 0.6、
27        // max_candidates 1000、min_size 3、unclip_ratio 1.5
28        DbParams {
29            thresh: 0.3,
30            box_thresh: 0.6,
31            max_candidates: 1000,
32            min_size: 3.0,
33            unclip_ratio: 1.5,
34        }
35    }
36}
37
38/// 一张概率图(按行存放,`width × height`)。
39pub struct ProbMap<'a> {
40    pub data: &'a [f32],
41    pub width: usize,
42    pub height: usize,
43}
44
45/// 框内平均概率(参考实现的 `_get_box_score`,fast 模式):取框的轴对齐外接矩形,
46/// 在其中把框(顶点截断成整数)填满作掩码,求掩码内概率的平均。
47pub fn box_score_fast(prob: &ProbMap, quad: &[P; 4]) -> f32 {
48    let (w, h) = (prob.width as i64, prob.height as i64);
49    let xs = quad.iter().map(|p| p.0);
50    let ys = quad.iter().map(|p| p.1);
51    let clamp = |v: f64, hi: i64| (v as i64).clamp(0, hi - 1);
52    let xmin = clamp(xs.clone().fold(f64::MAX, f64::min).floor(), w);
53    let xmax = clamp(xs.fold(f64::MIN, f64::max).ceil(), w);
54    let ymin = clamp(ys.clone().fold(f64::MAX, f64::min).floor(), h);
55    let ymax = clamp(ys.fold(f64::MIN, f64::max).ceil(), h);
56    // [协议] 参考实现先减去外接矩形左上角,再 astype(int32)(向零截断)
57    let poly: Vec<(i32, i32)> = quad
58        .iter()
59        .map(|p| ((p.0 - xmin as f64) as i32, (p.1 - ymin as f64) as i32))
60        .collect();
61    let bw = (xmax - xmin + 1) as usize;
62    let bh = (ymax - ymin + 1) as usize;
63    let mut sum = 0f64;
64    let mut n = 0usize;
65    for (y, x0, x1) in geometry::fill_convex_rows(&poly, bw, bh) {
66        let row = (ymin as usize + y) * prob.width + xmin as usize;
67        sum += prob.data[row + x0..=row + x1]
68            .iter()
69            .map(|&v| v as f64)
70            .sum::<f64>();
71        n += x1 - x0 + 1;
72    }
73    // cv2.mean 在掩码为空时给 0
74    if n == 0 { 0.0 } else { (sum / n as f64) as f32 }
75}
76
77/// 概率图 → 原图坐标下的文字框和分数。`dest_w`、`dest_h` 是原图尺寸。
78pub fn boxes_from_prob(
79    prob: &ProbMap,
80    dest_w: usize,
81    dest_h: usize,
82    p: &DbParams,
83) -> Vec<(Quad, f32)> {
84    let bits: Vec<bool> = prob.data.iter().map(|&v| v > p.thresh).collect();
85    let contours = contour::outer_contours(&Bitmap {
86        data: &bits,
87        width: prob.width,
88        height: prob.height,
89    });
90    let sx = dest_w as f64 / prob.width as f64;
91    let sy = dest_h as f64 / prob.height as f64;
92    let mut out = Vec::new();
93    for c in contours.iter().take(p.max_candidates) {
94        let pts: Vec<P> = c.iter().map(|&(x, y)| (x as f64, y as f64)).collect();
95        let (quad, short) = geometry::mini_box(&pts);
96        if short < p.min_size {
97            continue;
98        }
99        let score = box_score_fast(prob, &quad);
100        if score < p.box_thresh {
101            continue;
102        }
103        let Some(expanded) = geometry::unclip(&quad, p.unclip_ratio) else {
104            continue;
105        };
106        let (quad, short) = geometry::mini_box(&expanded);
107        // [协议] 参考实现外扩后要求短边 ≥ min_size + 2
108        if short < p.min_size + 2.0 {
109            continue;
110        }
111        // [协议] 参考实现按比例放回原图后 round(numpy 的四舍六入五成双)并裁到 [0, 原图宽高]
112        let map = |(x, y): P| {
113            (
114                (x * sx).round_ties_even().clamp(0.0, dest_w as f64) as f32,
115                (y * sy).round_ties_even().clamp(0.0, dest_h as f64) as f32,
116            )
117        };
118        out.push((quad.map(map), score));
119    }
120    out
121}
122
123/// 把文字框排成阅读顺序:先按左上角 y、再按 x 排;之后相邻两框左上角的 y 相差不到
124/// `line_tol` 像素时视为同一行,按 x 从左到右调整(同 PaddleOCR 的 `sorted_boxes`)。
125pub fn sort_reading_order(boxes: &mut [(Quad, f32)], line_tol: f32) {
126    boxes.sort_by(|a, b| {
127        let (pa, pb) = (a.0[0], b.0[0]);
128        pa.1.total_cmp(&pb.1).then(pa.0.total_cmp(&pb.0))
129    });
130    for i in 0..boxes.len().saturating_sub(1) {
131        for j in (0..=i).rev() {
132            let (a, b) = (boxes[j].0[0], boxes[j + 1].0[0]);
133            if (b.1 - a.1).abs() < line_tol && b.0 < a.0 {
134                boxes.swap(j, j + 1);
135            } else {
136                break;
137            }
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn 框内平均分() {
148        // 4 × 4 概率图,左上 2 × 2 是 1,其余 0
149        let mut data = vec![0f32; 16];
150        for (x, y) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
151            data[y * 4 + x] = 1.0;
152        }
153        let pm = ProbMap {
154            data: &data,
155            width: 4,
156            height: 4,
157        };
158        let q = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
159        assert!((box_score_fast(&pm, &q) - 1.0).abs() < 1e-6);
160        let q = [(0.0, 0.0), (3.0, 0.0), (3.0, 1.0), (0.0, 1.0)];
161        assert!((box_score_fast(&pm, &q) - 0.5).abs() < 1e-6);
162    }
163
164    #[test]
165    fn 一个矩形块变成外扩后的框() {
166        // 40 × 20 的概率图,(5..=24, 6..=11) 是一块 20 × 6 的文字
167        let (w, h) = (40, 20);
168        let mut data = vec![0f32; w * h];
169        for y in 6..=11 {
170            for x in 5..=24 {
171                data[y * w + x] = 0.9;
172            }
173        }
174        let pm = ProbMap {
175            data: &data,
176            width: w,
177            height: h,
178        };
179        // 原图是概率图的 2 倍
180        let r = boxes_from_prob(&pm, 2 * w, 2 * h, &DbParams::default());
181        assert_eq!(r.len(), 1);
182        let (q, s) = r[0];
183        assert!((s - 0.9).abs() < 1e-6);
184        // 像素中心构成 19 × 5 的矩形:d = 95 × 1.5 / 48 ≈ 2.97
185        let d = 95.0 * 1.5 / 48.0;
186        let want = [
187            (5.0 - d, 6.0 - d),
188            (24.0 + d, 6.0 - d),
189            (24.0 + d, 11.0 + d),
190            (5.0 - d, 11.0 + d),
191        ];
192        for (got, (x, y)) in q.iter().zip(want) {
193            let (ex, ey) = (
194                (2.0f64 * x).round_ties_even(),
195                (2.0f64 * y).round_ties_even(),
196            );
197            assert_eq!((got.0 as f64, got.1 as f64), (ex, ey), "{q:?}");
198        }
199    }
200
201    #[test]
202    fn 太小或分低的块不要() {
203        let (w, h) = (20, 10);
204        let mut data = vec![0f32; w * h];
205        // 2 像素高:短边 1 < 3
206        for x in 2..15 {
207            data[2 * w + x] = 0.9;
208            data[3 * w + x] = 0.9;
209        }
210        // 分数 0.4 < 0.6
211        for y in 5..10 {
212            for x in 2..15 {
213                data[y * w + x] = 0.4;
214            }
215        }
216        let pm = ProbMap {
217            data: &data,
218            width: w,
219            height: h,
220        };
221        assert!(boxes_from_prob(&pm, w, h, &DbParams::default()).is_empty());
222    }
223
224    #[test]
225    fn 阅读顺序() {
226        let b = |x: f32, y: f32| -> (Quad, f32) {
227            (
228                [(x, y), (x + 5.0, y), (x + 5.0, y + 3.0), (x, y + 3.0)],
229                1.0,
230            )
231        };
232        let mut v = vec![b(50.0, 100.0), b(10.0, 104.0), b(10.0, 10.0), b(60.0, 8.0)];
233        sort_reading_order(&mut v, 10.0);
234        let xs: Vec<(f32, f32)> = v.iter().map(|q| q.0[0]).collect();
235        assert_eq!(
236            xs,
237            vec![(10.0, 10.0), (60.0, 8.0), (10.0, 104.0), (50.0, 100.0)]
238        );
239    }
240}