Skip to main content

pagesift_ocr/layout/
preprocess.rs

1//! 预处理:整页图拉伸到固定尺寸(不保长宽比),双三次插值(不抗锯齿),取整回 8 位,再缩放到 0–1、
2//! 减均值除标准差。语义照 transformers 的 `PPDocLayoutV3ImageProcessor`(Apache-2.0):torchvision 的
3//! `resize(antialias=False)` 走 `F.interpolate(mode="bicubic", align_corners=False)`。
4
5use pagesift_infer::Tensor;
6
7use crate::Image;
8
9/// [协议] 网络输入是 RGB 三通道
10const CHANNELS: usize = 3;
11/// [协议] `PPDocLayoutV3ImageProcessor.size` 的缺省值
12const DEFAULT_SIZE: u64 = 800;
13/// [协议] 8 位像素的最大值
14const MAX_U8: f32 = 255.0;
15/// [协议] 双三次插值每个方向 4 个抽头
16const TAPS: usize = 4;
17
18/// 预处理参数(preprocessor_config.json)。
19#[derive(Debug, Clone)]
20pub struct Preprocess {
21    pub height: usize,
22    pub width: usize,
23    pub rescale: f32,
24    pub mean: [f32; CHANNELS],
25    pub std: [f32; CHANNELS],
26}
27
28/// [协议] PIL 的重采样编号:3 是 BICUBIC
29const RESAMPLE_BICUBIC: u64 = 3;
30
31impl Preprocess {
32    pub fn from_json(v: &serde_json::Value) -> Result<Preprocess, String> {
33        if v["do_resize"].as_bool() == Some(false)
34            || v["do_rescale"].as_bool() == Some(false)
35            || v["do_normalize"].as_bool() == Some(false)
36        {
37            return Err(
38                "only preprocessing with resize, rescale and normalize all on is supported".into(),
39            );
40        }
41        if v["resample"].as_u64().unwrap_or(RESAMPLE_BICUBIC) != RESAMPLE_BICUBIC {
42            return Err("only bicubic resampling is supported".into());
43        }
44        let triple = |key: &str, def: f32| -> Result<[f32; CHANNELS], String> {
45            match v[key].as_array() {
46                None => Ok([def; CHANNELS]),
47                Some(a) if a.len() == CHANNELS => {
48                    let mut out = [0f32; CHANNELS];
49                    for (o, x) in out.iter_mut().zip(a) {
50                        *o = x.as_f64().ok_or(format!("{key} must be a number"))? as f32;
51                    }
52                    Ok(out)
53                }
54                Some(_) => Err(format!("{key} must have {CHANNELS} numbers")),
55            }
56        };
57        // 缺省值取 PPDocLayoutV3ImageProcessor 的类属性:均值 0、标准差 1、1/255
58        Ok(Preprocess {
59            height: v["size"]["height"].as_u64().unwrap_or(DEFAULT_SIZE) as usize,
60            width: v["size"]["width"].as_u64().unwrap_or(DEFAULT_SIZE) as usize,
61            rescale: v["rescale_factor"]
62                .as_f64()
63                .unwrap_or(1.0 / f64::from(MAX_U8)) as f32,
64            mean: triple("image_mean", 0.0)?,
65            std: triple("image_std", 1.0)?,
66        })
67    }
68
69    /// 整页图 → `[3, H, W]`,RGB 顺序;灰度图三个通道相同。
70    pub fn run(&self, img: &Image) -> Tensor {
71        let planes = if img.channels == 1 { 1 } else { CHANNELS };
72        let (h, w) = (self.height, self.width);
73        let mut out = vec![0f32; CHANNELS * h * w];
74        for c in 0..planes {
75            let src: Vec<f32> = (0..img.height * img.width)
76                .map(|i| f32::from(img.data[i * img.channels + c]))
77                .collect();
78            let r = resize_bicubic(&src, img.height, img.width, h, w);
79            let dst = &mut out[c * h * w..(c + 1) * h * w];
80            for (d, v) in dst.iter_mut().zip(r) {
81                // torchvision 对 8 位图:插值后四舍五入(到偶)并截到 0–255,再做后面的归一化
82                let v = v.round_ties_even().clamp(0.0, MAX_U8);
83                *d = (v * self.rescale - self.mean[c]) / self.std[c];
84            }
85        }
86        if planes == 1 {
87            let (first, rest) = out.split_at_mut(h * w);
88            rest[..h * w].copy_from_slice(first);
89            rest[h * w..].copy_from_slice(first);
90        }
91        Tensor::new(vec![CHANNELS, h, w], out)
92    }
93}
94
95/// 一个输出坐标的四个抽头:起始下标(可越界,取值时截到边上)和权重。
96fn cubic_taps(input: usize, output: usize) -> Vec<(isize, [f32; TAPS])> {
97    let scale = input as f32 / output as f32;
98    (0..output)
99        .map(|o| {
100            // align_corners=False:源坐标 = (目标 + ½)·比例 − ½;双三次不把负数截到 0
101            let src = (o as f32 + 0.5) * scale - 0.5;
102            let floor = src.floor();
103            (floor as isize - 1, cubic_weights(src - floor))
104        })
105        .collect()
106}
107
108/// PyTorch `upsample_bicubic2d` 的 `get_cubic_upsample_coefficients`,t 是到左邻点的距离。
109fn cubic_weights(t: f32) -> [f32; TAPS] {
110    [
111        cubic_outer(t + 1.0),
112        cubic_inner(t),
113        cubic_inner(1.0 - t),
114        cubic_outer(2.0 - t),
115    ]
116}
117
118/// [协议] PyTorch 双三次核的参数 A = −0.75(OpenCV 的 INTER_CUBIC 也是)
119const CUBIC_A: f32 = -0.75;
120
121/// |x| ≤ 1 段:(A + 2)x³ − (A + 3)x² + 1。
122fn cubic_inner(x: f32) -> f32 {
123    // [协议] PyTorch `cubic_convolution1` 的系数
124    ((CUBIC_A + 2.0) * x - (CUBIC_A + 3.0)) * x * x + 1.0
125}
126
127/// 1 < |x| < 2 段:Ax³ − 5Ax² + 8Ax − 4A。
128fn cubic_outer(x: f32) -> f32 {
129    // [协议] PyTorch `cubic_convolution2` 的系数
130    ((CUBIC_A * x - 5.0 * CUBIC_A) * x + 8.0 * CUBIC_A) * x - 4.0 * CUBIC_A
131}
132
133/// 把一个 `h × w` 平面缩放到 `oh × ow`,先横后纵;越界的抽头取边上的像素。
134fn resize_bicubic(src: &[f32], h: usize, w: usize, oh: usize, ow: usize) -> Vec<f32> {
135    let tx = cubic_taps(w, ow);
136    let ty = cubic_taps(h, oh);
137    let at = |i: isize, n: usize| i.clamp(0, n as isize - 1) as usize;
138    // 只有被纵向抽头用到的行才做横向一遍:大图缩小时省掉大部分行
139    let mut used = vec![false; h];
140    for &(s, _) in &ty {
141        for k in 0..TAPS as isize {
142            used[at(s + k, h)] = true;
143        }
144    }
145    let mut mid = vec![0f32; h * ow];
146    for y in (0..h).filter(|&y| used[y]) {
147        let row = &src[y * w..(y + 1) * w];
148        for (d, (s, wt)) in mid[y * ow..(y + 1) * ow].iter_mut().zip(&tx) {
149            *d = (0..TAPS)
150                .map(|k| row[at(s + k as isize, w)] * wt[k])
151                .sum::<f32>();
152        }
153    }
154    let mut out = vec![0f32; oh * ow];
155    for (drow, (s, wt)) in out.chunks_exact_mut(ow).zip(&ty) {
156        for (k, wk) in wt.iter().enumerate() {
157            let y = at(s + k as isize, h);
158            let srow = &mid[y * ow..(y + 1) * ow];
159            for (d, v) in drow.iter_mut().zip(srow) {
160                *d += v * wk;
161            }
162        }
163    }
164    out
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn 权重和为一且在节点上取原值() {
173        for t in [0.0f32, 0.25, 0.5, 0.9] {
174            let s: f32 = cubic_weights(t).iter().sum();
175            assert!((s - 1.0).abs() < 1e-5, "{t}: {s}");
176        }
177        let w0 = cubic_weights(0.0);
178        assert!(w0[0].abs() < 1e-6 && (w0[1] - 1.0).abs() < 1e-6 && w0[2].abs() < 1e-6);
179    }
180
181    #[test]
182    fn 同尺寸不变_常数保持() {
183        let src: Vec<f32> = (0..16).map(|v| v as f32).collect();
184        let same = resize_bicubic(&src, 4, 4, 4, 4);
185        for (a, b) in same.iter().zip(&src) {
186            assert!((a - b).abs() < 1e-5);
187        }
188        let flat = resize_bicubic(&[7.0; 30], 5, 6, 3, 2);
189        assert!(flat.iter().all(|v| (v - 7.0).abs() < 1e-4));
190    }
191
192    #[test]
193    fn 灰度图三通道相同且缩放到一() {
194        let p = Preprocess {
195            height: 4,
196            width: 4,
197            rescale: 1.0 / 255.0,
198            mean: [0.0; 3],
199            std: [1.0; 3],
200        };
201        let img = Image {
202            width: 8,
203            height: 8,
204            channels: 1,
205            data: vec![255; 64],
206        };
207        let t = p.run(&img);
208        assert_eq!(t.shape, [3, 4, 4]);
209        assert!(t.data.iter().all(|v| (v - 1.0).abs() < 1e-6));
210    }
211
212    #[test]
213    fn 彩色图通道不混() {
214        let p = Preprocess {
215            height: 2,
216            width: 2,
217            rescale: 1.0 / 255.0,
218            mean: [0.0; 3],
219            std: [1.0; 3],
220        };
221        let img = Image {
222            width: 2,
223            height: 2,
224            channels: 3,
225            data: [255u8, 0, 51].repeat(4),
226        };
227        let t = p.run(&img);
228        assert!((t.data[0] - 1.0).abs() < 1e-6);
229        assert!(t.data[4].abs() < 1e-6);
230        assert!((t.data[8] - 0.2).abs() < 1e-6);
231    }
232}