1use 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
19const TARGET_H: usize = 48;
21const MIN_W: usize = 320;
23const DEFAULT_MAX_W: usize = 3200;
25const LN_EPS: f32 = 1e-6;
27const DEFAULT_HEADS: usize = 8;
29
30struct ConvAct {
32 conv: Conv2d,
33 act: Activation,
34}
35
36impl ConvAct {
37 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
60struct 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 let qkv = e.linear(&self.qkv, x);
71 let o = e.attention(&qkv, self.heads);
72 e.linear(&self.proj, &o)
73 }
74}
75
76struct 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
110pub struct Rec {
112 backbone: Backbone,
113 skip: ConvAct,
115 reduce: ConvAct,
116 local: ConvAct,
117 blocks: Vec<Block>,
118 norm: LayerNorm,
119 head: Linear,
120 chars: Vec<String>,
122 max_w: usize,
123}
124
125impl Rec {
126 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 let backbone = Backbone::load(
133 &w,
134 "model.backbone.encoder",
135 &BackboneConfig::from_json(&cfg)?,
136 )?;
137 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 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 pub fn input_channels(&self) -> usize {
211 3 }
213
214 pub fn input_height(&self) -> usize {
216 TARGET_H
217 }
218
219 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 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 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 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 e.add(&e.layer_norm(&self.norm, &s), &residual)
255 }
256
257 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 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 let score = if kept == 0 { 0.0 } else { sum / kept as f32 };
274 (text, score)
275 }
276}
277
278fn 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
292fn 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 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 let v = (top + (bot - top) * fy).round_ties_even().clamp(0.0, 255.0);
310 let oc = 2 - c;
312 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
320fn 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 assert_eq!(target_width(960, 48, 3200), 960);
342 assert_eq!(target_width(1001, 30, 3200), 1601);
343 assert_eq!(target_width(100, 30, 3200), 160);
345 assert_eq!(target_width(101, 30, 3200), 162);
346 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}