Skip to main content

pagesift_ocr/det/
preprocess.rs

1//! 检测的预处理:按 `limit_side_len` / `limit_type` 定尺寸(对齐到 32 的倍数),双线性缩放,
2//! 缩放到 0–1、按通道减均值除标准差,最后排成 BGR 通道顺序。
3//! 语义照 transformers 的 `PPOCRV5ServerDetImageProcessor`(Apache-2.0)。
4
5use pagesift_infer::Tensor;
6
7use crate::Image;
8
9/// 尺寸限制的方式(`limit_type`)。
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LimitType {
12    /// 长边超过 `limit_side_len` 才缩小到它。
13    Max,
14    /// 短边不足 `limit_side_len` 才放大到它。
15    Min,
16    /// 长边总是缩放到 `limit_side_len`。
17    ResizeLong,
18}
19
20/// 预处理参数(preprocessor_config.json)。
21#[derive(Debug, Clone)]
22pub struct Preprocess {
23    pub limit_side_len: usize,
24    pub limit_type: LimitType,
25    pub max_side_limit: Option<usize>,
26    pub rescale: f32,
27    /// 均值、标准差按输入图的 RGB 通道顺序排。
28    pub mean: [f32; 3],
29    pub std: [f32; 3],
30}
31
32impl Preprocess {
33    pub fn from_json(v: &serde_json::Value) -> Result<Preprocess, String> {
34        let limit_type = match v["limit_type"].as_str().unwrap_or("max") {
35            "max" => LimitType::Max,
36            "min" => LimitType::Min,
37            "resize_long" => LimitType::ResizeLong,
38            t => return Err(format!("unsupported limit_type: {t}")),
39        };
40        let triple = |key: &str, def: [f32; 3]| -> Result<[f32; 3], String> {
41            match v[key].as_array() {
42                None => Ok(def),
43                Some(a) if a.len() == 3 => {
44                    let mut out = [0f32; 3];
45                    for (o, x) in out.iter_mut().zip(a) {
46                        *o = x.as_f64().ok_or(format!("{key} must be a number"))? as f32;
47                    }
48                    Ok(out)
49                }
50                Some(_) => Err(format!("{key} must have 3 numbers")),
51            }
52        };
53        if v["do_resize"].as_bool() == Some(false)
54            || v["do_rescale"].as_bool() == Some(false)
55            || v["do_normalize"].as_bool() == Some(false)
56        {
57            return Err(
58                "only preprocessing with resize, rescale and normalize all on is supported".into(),
59            );
60        }
61        // [协议] 缺省值取 PPOCRV5ServerDetImageProcessor 的类属性:limit_side_len 960、
62        // max_side_limit 4000、image_mean/std 如下、rescale 1/255
63        Ok(Preprocess {
64            limit_side_len: v["limit_side_len"].as_u64().unwrap_or(960) as usize,
65            limit_type,
66            max_side_limit: match &v["max_side_limit"] {
67                serde_json::Value::Null => Some(4000),
68                x => x.as_u64().map(|n| n as usize),
69            },
70            rescale: v["rescale_factor"].as_f64().unwrap_or(1.0 / 255.0) as f32,
71            mean: triple("image_mean", [0.406, 0.456, 0.485])?,
72            std: triple("image_std", [0.225, 0.224, 0.229])?,
73        })
74    }
75
76    /// 网络输入的 (高, 宽),对应 `get_image_size`。
77    pub fn target_size(&self, h: usize, w: usize) -> (usize, usize) {
78        let (hf, wf) = (h as f64, w as f64);
79        let lim = self.limit_side_len as f64;
80        let ratio = match self.limit_type {
81            LimitType::Max if hf.max(wf) > lim => lim / hf.max(wf),
82            // [策略] 参考实现只放大短边不足的图;这里大图也缩到短边 = limit_side_len:
83            // 300 dpi 整页扫描按原尺寸检测会漏行(手写页实测),且慢十几倍;识别从原图切行,不受影响
84            LimitType::Min => lim / hf.min(wf),
85            LimitType::ResizeLong => lim / hf.max(wf),
86            _ => 1.0,
87        };
88        // 参考实现用 int() 截断
89        let mut rh = (hf * ratio) as usize;
90        let mut rw = (wf * ratio) as usize;
91        if let Some(m) = self.max_side_limit
92            && rh.max(rw) > m
93        {
94            let r = m as f64 / rh.max(rw) as f64;
95            rh = (rh as f64 * r) as usize;
96            rw = (rw as f64 * r) as usize;
97        }
98        // [协议] 对齐到 32 的倍数(四舍五入、至少 32):骨干总步长 32
99        let align = |n: usize| (((n as f64 / 32.0).round_ties_even() as usize) * 32).max(32);
100        (align(rh), align(rw))
101    }
102
103    /// 整页图 → `[3, H', W']` 的网络输入(通道顺序 B、G、R)。
104    pub fn run(&self, img: &Image) -> Tensor {
105        let (th, tw) = self.target_size(img.height, img.width);
106        // 先把 8 位图转成 RGB 浮点平面(灰度图三个通道相同,即 do_convert_rgb)
107        let plane = img.width * img.height;
108        let mut rgb = vec![0f32; 3 * plane];
109        for y in 0..img.height {
110            for x in 0..img.width {
111                for c in 0..3 {
112                    rgb[c * plane + y * img.width + x] = img.at(x, y, c) as f32;
113                }
114            }
115        }
116        let resized = resize_bilinear_aa(&rgb, 3, img.height, img.width, th, tw);
117        let out_plane = th * tw;
118        let mut out = vec![0f32; 3 * out_plane];
119        for c in 0..3 {
120            // [协议] 在 RGB 顺序上归一化后取 `[:, [2, 1, 0]]` 换成 BGR
121            let dst = 2 - c;
122            let (s, m, d) = (self.rescale, self.mean[c], self.std[c]);
123            for (o, v) in out[dst * out_plane..(dst + 1) * out_plane]
124                .iter_mut()
125                .zip(&resized[c * out_plane..(c + 1) * out_plane])
126            {
127                *o = (v * s - m) / d;
128            }
129        }
130        Tensor::new(vec![3, th, tw], out)
131    }
132}
133
134/// 一维重采样的权重表:每个输出位置的起始下标和各输入的权重。
135struct Taps {
136    start: Vec<usize>,
137    weights: Vec<Vec<f32>>,
138}
139
140/// 抗锯齿双线性(PIL / torchvision `antialias=True` 的做法):缩小时三角核按比例放宽,
141/// 放大时就是半像素对齐的普通双线性。
142fn taps(inp: usize, out: usize) -> Taps {
143    let scale = inp as f64 / out as f64;
144    // [协议] 双线性核半径 1,缩小时乘以 scale
145    let support = scale.max(1.0);
146    let inv = 1.0 / scale.max(1.0);
147    let mut start = Vec::with_capacity(out);
148    let mut weights = Vec::with_capacity(out);
149    for i in 0..out {
150        let center = scale * (i as f64 + 0.5);
151        let lo = ((center - support + 0.5).floor().max(0.0)) as usize;
152        let hi = ((center + support + 0.5).floor() as usize).min(inp);
153        let mut w: Vec<f64> = (lo..hi)
154            .map(|j| (1.0 - ((j as f64 - center + 0.5) * inv).abs()).max(0.0))
155            .collect();
156        let sum: f64 = w.iter().sum();
157        if sum > 0.0 {
158            for v in &mut w {
159                *v /= sum;
160            }
161        }
162        start.push(lo);
163        weights.push(w.into_iter().map(|v| v as f32).collect());
164    }
165    Taps { start, weights }
166}
167
168/// 把 `c` 个 `h × w` 平面缩放到 `oh × ow`(先横后纵,可分离)。
169pub(crate) fn resize_bilinear_aa(
170    src: &[f32],
171    c: usize,
172    h: usize,
173    w: usize,
174    oh: usize,
175    ow: usize,
176) -> Vec<f32> {
177    if (h, w) == (oh, ow) {
178        return src.to_vec();
179    }
180    let tx = taps(w, ow);
181    let ty = taps(h, oh);
182    let mut mid = vec![0f32; c * h * ow];
183    for (srow, drow) in src.chunks_exact(w).zip(mid.chunks_exact_mut(ow)) {
184        for (x, d) in drow.iter_mut().enumerate() {
185            let s0 = tx.start[x];
186            *d = tx.weights[x]
187                .iter()
188                .enumerate()
189                .map(|(k, wt)| srow[s0 + k] * wt)
190                .sum();
191        }
192    }
193    let mut out = vec![0f32; c * oh * ow];
194    for ch in 0..c {
195        let m = &mid[ch * h * ow..(ch + 1) * h * ow];
196        for y in 0..oh {
197            let drow = &mut out[(ch * oh + y) * ow..(ch * oh + y + 1) * ow];
198            for (k, wt) in ty.weights[y].iter().enumerate() {
199                let srow = &m[(ty.start[y] + k) * ow..(ty.start[y] + k + 1) * ow];
200                for (d, s) in drow.iter_mut().zip(srow) {
201                    *d += s * wt;
202                }
203            }
204        }
205    }
206    out
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn pre(limit_type: LimitType) -> Preprocess {
214        Preprocess {
215            limit_side_len: 736,
216            limit_type,
217            max_side_limit: Some(4000),
218            rescale: 1.0 / 255.0,
219            mean: [0.406, 0.456, 0.485],
220            std: [0.225, 0.224, 0.229],
221        }
222    }
223
224    #[test]
225    fn 尺寸规则() {
226        let p = pre(LimitType::Min);
227        // 短边超过 736:缩到 736(与参考实现不同,见 target_size)
228        assert_eq!(p.target_size(2902, 2244), (960, 736));
229        // 短边不足:放大到 736
230        assert_eq!(p.target_size(500, 300), (1216, 736));
231        // 长边超过 4000 再缩回
232        assert_eq!(p.target_size(9000, 1000), (4000, 448));
233        assert_eq!(pre(LimitType::Max).target_size(1000, 500), (736, 384));
234    }
235
236    #[test]
237    fn 缩放保常数且放大是双线性() {
238        let src = vec![5.0f32; 6 * 4];
239        assert!(
240            resize_bilinear_aa(&src, 1, 6, 4, 3, 2)
241                .iter()
242                .all(|v| (v - 5.0).abs() < 1e-5)
243        );
244        // 1×2 放大到 1×4:半像素对齐的双线性 → 0, 0.25, 0.75, 1
245        let up = resize_bilinear_aa(&[0.0, 1.0], 1, 1, 2, 1, 4);
246        let want = [0.0, 0.25, 0.75, 1.0];
247        for (a, b) in up.iter().zip(want) {
248            assert!((a - b).abs() < 1e-6, "{up:?}");
249        }
250    }
251
252    #[test]
253    fn 通道换成_bgr() {
254        let img = Image {
255            width: 32,
256            height: 32,
257            channels: 3,
258            data: [255u8, 0, 0].repeat(32 * 32),
259        };
260        let t = pre(LimitType::Max).run(&img);
261        let plane = 32 * 32;
262        // 第 0 通道是 B(原图 0),第 2 通道是 R(原图 255)
263        assert!((t.data[0] - (0.0 - 0.485) / 0.229).abs() < 1e-5);
264        assert!((t.data[2 * plane] - (1.0 - 0.406) / 0.225).abs() < 1e-5);
265    }
266}