Skip to main content

pagesift_ocr/
rec.rs

1//! 文字识别(PP-OCRv6 small rec)。结构照 transformers 的 `modeling_pp_ocrv6_small_rec.py`
2//! 与 `image_processing_pp_ocrv6_small_rec.py`(Apache-2.0,PaddlePaddle 与 HuggingFace):
3//!
4//! 行图 → 缩放到高 48(双线性)、BGR、归一化到 [−1, 1]、不足 320 宽右边补 0 →
5//! LCNetV4 骨干取第 4 段 `[384, 3, W/4]` → avg_pool2d(3, 2) 得 `[384, 1, W/8]` →
6//! EncoderWithSVTR(两路 1×1 卷积、1×7 深度卷积残差、2 层 Transformer、LayerNorm、与第一路相加)→
7//! 全连接到字符表 → softmax → CTC 贪心解码。
8//!
9//! 卷积后的批归一化在载入时并进卷积。
10
11use std::path::Path;
12
13use pagesift_infer::exec::{Cpu, Exec};
14use pagesift_infer::{Activation, BN_EPS, BatchNorm, Conv2d, LayerNorm, Linear, Tensor, Weights};
15
16use crate::Image;
17use crate::lcnet::{Backbone, Config as BackboneConfig};
18
19/// [协议] `PPOCRV6SmallRecImageProcessor.size["height"]`:输入高 48
20const TARGET_H: usize = 48;
21/// [协议] `size["width"]` 与 `pad_size["width"]`:最小宽 320,不足时右边补 0
22const MIN_W: usize = 320;
23/// [协议] 预处理缺省 `max_image_width = 3200`(preprocessor_config.json 可覆盖)
24const DEFAULT_MAX_W: usize = 3200;
25/// [协议] `PPOCRV6SmallRecConfig.layer_norm_eps` 缺省 1e-6
26const LN_EPS: f32 = 1e-6;
27/// [协议] `PPOCRV6SmallRecConfig.num_attention_heads` 缺省 8
28const DEFAULT_HEADS: usize = 8;
29
30/// 卷积 + 批归一化(已并入)+ 激活,对应参考实现的 `PPOCRV6SmallRecConvLayer`。
31struct ConvAct {
32    conv: Conv2d,
33    act: Activation,
34}
35
36impl ConvAct {
37    /// 读 `{p}.convolution`、`{p}.normalization`;补边 (kh/2, kw/2),深度卷积时 groups = 输出通道。
38    fn load(w: &Weights, p: &str, depthwise: bool, act: Activation) -> Result<ConvAct, String> {
39        let shape = w.get(&format!("{p}.convolution.weight"))?.shape.clone();
40        let groups = if depthwise { shape[0] } else { 1 };
41        let mut conv = Conv2d::load(
42            w,
43            &format!("{p}.convolution"),
44            (1, 1),
45            (shape[2] / 2, shape[3] / 2),
46            (1, 1),
47            groups,
48        )?;
49        conv.fold(&BatchNorm::load(w, &format!("{p}.normalization"), BN_EPS)?);
50        Ok(ConvAct { conv, act })
51    }
52
53    fn forward_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
54        let mut y = e.conv(&self.conv, x);
55        e.act(self.act, &mut y);
56        y
57    }
58}
59
60/// 多头自注意力:`qkv` 一个线性层出 3·C 维,按 (3, heads, head_dim) 拆。
61struct Attention {
62    qkv: Linear,
63    proj: Linear,
64    heads: usize,
65}
66
67impl Attention {
68    fn forward_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
69        // [T, 3C],列 = s·C + h·hd + d;分数的缩放见 Exec::attention
70        let qkv = e.linear(&self.qkv, x);
71        let o = e.attention(&qkv, self.heads);
72        e.linear(&self.proj, &o)
73    }
74}
75
76/// Transformer 块(前置 LayerNorm):x + attn(ln1(x)),再 x + mlp(ln2(x))。
77struct Block {
78    ln1: LayerNorm,
79    attn: Attention,
80    ln2: LayerNorm,
81    fc1: Linear,
82    fc2: Linear,
83    act: Activation,
84}
85
86impl Block {
87    fn load(w: &Weights, p: &str, heads: usize, act: Activation) -> Result<Block, String> {
88        Ok(Block {
89            ln1: LayerNorm::load(w, &format!("{p}.layer_norm1"), LN_EPS)?,
90            attn: Attention {
91                qkv: Linear::load(w, &format!("{p}.self_attn.qkv"))?,
92                proj: Linear::load(w, &format!("{p}.self_attn.projection"))?,
93                heads,
94            },
95            ln2: LayerNorm::load(w, &format!("{p}.layer_norm2"), LN_EPS)?,
96            fc1: Linear::load(w, &format!("{p}.mlp.fc1"))?,
97            fc2: Linear::load(w, &format!("{p}.mlp.fc2"))?,
98            act,
99        })
100    }
101
102    fn forward_on<E: Exec>(&self, e: &E, x: E::T) -> E::T {
103        let x = e.add(&x, &self.attn.forward_on(e, &e.layer_norm(&self.ln1, &x)));
104        let mut h = e.linear(&self.fc1, &e.layer_norm(&self.ln2, &x));
105        e.act(self.act, &mut h);
106        e.add(&x, &e.linear(&self.fc2, &h))
107    }
108}
109
110/// 载入好的识别模型。
111pub struct Rec {
112    backbone: Backbone,
113    /// conv_block 0(skip_conv)、1(conv_reduce)、2(local_conv,深度 1×7)
114    skip: ConvAct,
115    reduce: ConvAct,
116    local: ConvAct,
117    blocks: Vec<Block>,
118    norm: LayerNorm,
119    head: Linear,
120    /// 字符表,下标 0 是 CTC 的 blank
121    chars: Vec<String>,
122    max_w: usize,
123}
124
125impl Rec {
126    /// `dir` 是模型目录:model.safetensors、config.json、preprocessor_config.json(含字符表)。
127    pub fn load(dir: &Path) -> Result<Rec, String> {
128        let cfg = crate::read_json(dir, "config.json")?;
129        let pre = crate::read_json(dir, "preprocessor_config.json")?;
130        let w = crate::read_weights(dir)?;
131        // [协议] 权重名前缀:`PPOCRV6SmallRecForTextRecognition` 的 model.backbone 与 head.encoder
132        let backbone = Backbone::load(
133            &w,
134            "model.backbone.encoder",
135            &BackboneConfig::from_json(&cfg)?,
136        )?;
137        // [协议] `hidden_act` 缺省 "silu",颈部卷积与 MLP 共用
138        let act = match cfg["hidden_act"].as_str().unwrap_or("silu") {
139            "silu" | "swish" => Activation::Silu,
140            "gelu" => Activation::Gelu,
141            "relu" => Activation::Relu,
142            a => return Err(format!("unsupported hidden_act {a}")),
143        };
144        let heads = cfg["num_attention_heads"]
145            .as_u64()
146            .map_or(DEFAULT_HEADS, |v| v as usize);
147        let p = "head.encoder";
148        let mut blocks = Vec::new();
149        while w
150            .opt(&format!(
151                "{p}.svtr_block.{}.layer_norm1.weight",
152                blocks.len()
153            ))
154            .is_some()
155        {
156            let i = blocks.len();
157            blocks.push(Block::load(&w, &format!("{p}.svtr_block.{i}"), heads, act)?);
158        }
159        if blocks.is_empty() {
160            return Err("no svtr_block in the weights".into());
161        }
162        let hidden = w.get(&format!("{p}.norm.weight"))?.shape[0];
163        if hidden % heads != 0 {
164            return Err(format!(
165                "hidden_size {hidden} is not divisible by {heads} heads"
166            ));
167        }
168        let chars: Vec<String> = pre["character_list"]
169            .as_array()
170            .ok_or("preprocessor_config.json has no character_list")?
171            .iter()
172            .map(|c| {
173                c.as_str()
174                    .map(str::to_owned)
175                    .ok_or("the character list has a non-string item")
176            })
177            .collect::<Result<_, _>>()?;
178        let head = Linear::load(&w, "head.head")?;
179        if head.weight_t.shape[1] != chars.len() {
180            return Err(format!(
181                "output layer has {} classes but the character list has {} items",
182                head.weight_t.shape[1],
183                chars.len()
184            ));
185        }
186        Ok(Rec {
187            backbone,
188            skip: ConvAct::load(&w, &format!("{p}.conv_block.0"), false, act)?,
189            reduce: ConvAct::load(&w, &format!("{p}.conv_block.1"), false, act)?,
190            local: ConvAct::load(&w, &format!("{p}.conv_block.2"), true, act)?,
191            blocks,
192            norm: LayerNorm::load(&w, &format!("{p}.norm"), LN_EPS)?,
193            head,
194            chars,
195            max_w: pre["max_image_width"]
196                .as_u64()
197                .map_or(DEFAULT_MAX_W, |v| v as usize),
198        })
199    }
200
201    /// 识别一行字的图(已裁出、拉正成水平的一行)。返回文字和置信度。
202    pub fn recognize(&self, line: &Image) -> Result<(String, f32), String> {
203        let x = self.preprocess(line)?;
204        let logits = self.logits_on(&Cpu, &x);
205        let rows = Cpu.ctc_digest(&logits);
206        Ok(self.decode(&rows[0]))
207    }
208
209    /// 网络输入的通道数。
210    pub fn input_channels(&self) -> usize {
211        3 // [协议] BGR 三通道(见 preprocess)
212    }
213
214    /// 网络输入的高。
215    pub fn input_height(&self) -> usize {
216        TARGET_H
217    }
218
219    /// 预处理一行图成网络输入 `[3, 48, W]`。
220    pub fn preprocess(&self, line: &Image) -> Result<Tensor, String> {
221        if line.width == 0 || line.height == 0 {
222            return Err("empty line image".into());
223        }
224        if line.channels != 1 && line.channels != 3 {
225            return Err(format!(
226                "images with {} channels are not supported",
227                line.channels
228            ));
229        }
230        Ok(preprocess(line, self.max_w))
231    }
232
233    /// 网络部分:`[3, 48, W]` → `[T, 字符表]` 的 logits,在执行器上算。
234    pub fn logits_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
235        let last = self.backbone.forward_last_on(e, x);
236        // [协议] `PPOCRV6SmallRecModel.forward`:F.avg_pool2d(stage4, (3, 2))
237        let pooled = e.avg_pool2d(&last, (3, 2));
238        drop(last);
239        let seq = self.encode_on(e, &pooled);
240        e.linear(&self.head, &seq)
241    }
242
243    /// EncoderWithSVTR:`[C, 1, T]` → `[T, hidden]`。
244    fn encode_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
245        let residual = e.chw_to_seq(&self.skip.forward_on(e, x));
246        let r = self.reduce.forward_on(e, x);
247        let r = e.add(&r, &self.local.forward_on(e, &r));
248        let mut s = e.chw_to_seq(&r);
249        drop(r);
250        for b in &self.blocks {
251            s = b.forward_on(e, s);
252        }
253        // 高已是 1,序列顺序即从左到右
254        e.add(&e.layer_norm(&self.norm, &s), &residual)
255    }
256
257    /// CTC 贪心解码:输入每步的(最大类,其概率)。先去相邻重复、再去 blank;
258    /// 置信度是保留下来的各字最大概率的平均。
259    pub fn decode(&self, rows: &[(u32, f32)]) -> (String, f32) {
260        let mut text = String::new();
261        let (mut sum, mut kept) = (0f32, 0usize);
262        let mut prev = u32::MAX;
263        for &(arg, p) in rows {
264            // [协议] 下标 0 是 blank(character_list[0] = "blank")
265            if arg != prev && arg != 0 {
266                text.push_str(&self.chars[arg as usize]);
267                sum += p;
268                kept += 1;
269            }
270            prev = arg;
271        }
272        // 没有字时参考实现对空张量求均值得 NaN,这里给 0
273        let score = if kept == 0 { 0.0 } else { sum / kept as f32 };
274        (text, score)
275    }
276}
277
278/// 目标宽度,照 `PPOCRV6SmallRecImageProcessor.get_target_size`(单张图):
279/// 宽高比不小于 320/48 时取 ⌊48·w/h⌋(恰为整数时就是它),超过上限取上限(此时宽被压扁);
280/// 更窄的图取 ⌈48·w/h⌉,之后再补到 320。
281fn target_width(w: usize, h: usize, max_w: usize) -> usize {
282    let r = w as f64 / h as f64;
283    let ratio = r.max(MIN_W as f64 / TARGET_H as f64);
284    let tw = (TARGET_H as f64 * ratio) as usize;
285    if tw > max_w {
286        return max_w;
287    }
288    let exact = (TARGET_H as f64 * r).ceil() as usize;
289    if tw >= exact { exact.max(1) } else { tw }
290}
291
292/// 预处理成 `[3, 48, W]`:双线性缩放(不抗锯齿,结果舍入回 8 位)→ BGR →
293/// (v/255 − 0.5)/0.5 → 不足 320 宽右边补 0(补在归一化之后)。
294fn preprocess(img: &Image, max_w: usize) -> Tensor {
295    let tw = target_width(img.width, img.height, max_w);
296    let (th, out_w) = (TARGET_H, tw.max(MIN_W));
297    // [协议] `do_pad`:宽不足 pad_size 时 tvF.pad(fill=0) 补在右、下,作用在归一化之后的张量上;
298    // 缓冲先全置 0,右边多出的列就是补边
299    let mut data = vec![0f32; 3 * th * out_w];
300    let xs = taps(img.width, tw);
301    let ys = taps(img.height, th);
302    for (oy, &(y0, y1, fy)) in ys.iter().enumerate() {
303        for (ox, &(x0, x1, fx)) in xs.iter().enumerate() {
304            for c in 0..3 {
305                let p = |x: usize, y: usize| f32::from(img.at(x, y, c));
306                let top = p(x0, y0) + (p(x1, y0) - p(x0, y0)) * fx;
307                let bot = p(x0, y1) + (p(x1, y1) - p(x0, y1)) * fx;
308                // [协议] torchvision 对 uint8 图插值后 round 再截到 0–255
309                let v = (top + (bot - top) * fy).round_ties_even().clamp(0.0, 255.0);
310                // [协议] RGB → BGR(`stacked_images[:, [2, 1, 0]]`)
311                let oc = 2 - c;
312                // [协议] rescale 1/255 后按 IMAGENET_STANDARD_MEAN/STD(0.5、0.5)归一化
313                data[(oc * th + oy) * out_w + ox] = (v / 255.0 - 0.5) / 0.5;
314            }
315        }
316    }
317    Tensor::new(vec![3, th, out_w], data)
318}
319
320/// 一维双线性的取样点(PyTorch `align_corners=False`、按尺寸缩放):
321/// 源坐标 = (目标 + 0.5)·入/出 − 0.5,负数截到 0;返回 (左, 右, 右的权重)。
322fn taps(n_in: usize, n_out: usize) -> Vec<(usize, usize, f32)> {
323    let scale = n_in as f32 / n_out as f32;
324    (0..n_out)
325        .map(|o| {
326            let src = ((o as f32 + 0.5) * scale - 0.5).max(0.0);
327            let i0 = (src as usize).min(n_in - 1);
328            let i1 = (i0 + 1).min(n_in - 1);
329            (i0, i1, src - i0 as f32)
330        })
331        .collect()
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn target_width_follows_reference() {
340        // 宽行:⌊48·w/h⌋
341        assert_eq!(target_width(960, 48, 3200), 960);
342        assert_eq!(target_width(1001, 30, 3200), 1601);
343        // 窄行:⌈48·w/h⌉,之后补到 320
344        assert_eq!(target_width(100, 30, 3200), 160);
345        assert_eq!(target_width(101, 30, 3200), 162);
346        // 超上限压到上限
347        assert_eq!(target_width(10000, 30, 3200), 3200);
348    }
349
350    #[test]
351    fn taps_identity_and_half() {
352        let t = taps(4, 4);
353        assert!(
354            t.iter()
355                .enumerate()
356                .all(|(i, &(a, _, f))| a == i && f == 0.0)
357        );
358        let t = taps(4, 2);
359        assert_eq!((t[0].0, t[0].1), (0, 1));
360        assert!((t[0].2 - 0.5).abs() < 1e-6);
361    }
362}