use std::path::Path;
use pagesift_infer::exec::{Cpu, Exec};
use pagesift_infer::{Activation, BN_EPS, BatchNorm, Conv2d, LayerNorm, Linear, Tensor, Weights};
use crate::Image;
use crate::lcnet::{Backbone, Config as BackboneConfig};
const TARGET_H: usize = 48;
const MIN_W: usize = 320;
const DEFAULT_MAX_W: usize = 3200;
const LN_EPS: f32 = 1e-6;
const DEFAULT_HEADS: usize = 8;
struct ConvAct {
conv: Conv2d,
act: Activation,
}
impl ConvAct {
fn load(w: &Weights, p: &str, depthwise: bool, act: Activation) -> Result<ConvAct, String> {
let shape = w.get(&format!("{p}.convolution.weight"))?.shape.clone();
let groups = if depthwise { shape[0] } else { 1 };
let mut conv = Conv2d::load(
w,
&format!("{p}.convolution"),
(1, 1),
(shape[2] / 2, shape[3] / 2),
(1, 1),
groups,
)?;
conv.fold(&BatchNorm::load(w, &format!("{p}.normalization"), BN_EPS)?);
Ok(ConvAct { conv, act })
}
fn forward_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
let mut y = e.conv(&self.conv, x);
e.act(self.act, &mut y);
y
}
}
struct Attention {
qkv: Linear,
proj: Linear,
heads: usize,
}
impl Attention {
fn forward_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
let qkv = e.linear(&self.qkv, x);
let o = e.attention(&qkv, self.heads);
e.linear(&self.proj, &o)
}
}
struct Block {
ln1: LayerNorm,
attn: Attention,
ln2: LayerNorm,
fc1: Linear,
fc2: Linear,
act: Activation,
}
impl Block {
fn load(w: &Weights, p: &str, heads: usize, act: Activation) -> Result<Block, String> {
Ok(Block {
ln1: LayerNorm::load(w, &format!("{p}.layer_norm1"), LN_EPS)?,
attn: Attention {
qkv: Linear::load(w, &format!("{p}.self_attn.qkv"))?,
proj: Linear::load(w, &format!("{p}.self_attn.projection"))?,
heads,
},
ln2: LayerNorm::load(w, &format!("{p}.layer_norm2"), LN_EPS)?,
fc1: Linear::load(w, &format!("{p}.mlp.fc1"))?,
fc2: Linear::load(w, &format!("{p}.mlp.fc2"))?,
act,
})
}
fn forward_on<E: Exec>(&self, e: &E, x: E::T) -> E::T {
let x = e.add(&x, &self.attn.forward_on(e, &e.layer_norm(&self.ln1, &x)));
let mut h = e.linear(&self.fc1, &e.layer_norm(&self.ln2, &x));
e.act(self.act, &mut h);
e.add(&x, &e.linear(&self.fc2, &h))
}
}
pub struct Rec {
backbone: Backbone,
skip: ConvAct,
reduce: ConvAct,
local: ConvAct,
blocks: Vec<Block>,
norm: LayerNorm,
head: Linear,
chars: Vec<String>,
max_w: usize,
}
impl Rec {
pub fn load(dir: &Path) -> Result<Rec, String> {
let cfg = crate::read_json(dir, "config.json")?;
let pre = crate::read_json(dir, "preprocessor_config.json")?;
let w = crate::read_weights(dir)?;
let backbone = Backbone::load(
&w,
"model.backbone.encoder",
&BackboneConfig::from_json(&cfg)?,
)?;
let act = match cfg["hidden_act"].as_str().unwrap_or("silu") {
"silu" | "swish" => Activation::Silu,
"gelu" => Activation::Gelu,
"relu" => Activation::Relu,
a => return Err(format!("unsupported hidden_act {a}")),
};
let heads = cfg["num_attention_heads"]
.as_u64()
.map_or(DEFAULT_HEADS, |v| v as usize);
let p = "head.encoder";
let mut blocks = Vec::new();
while w
.opt(&format!(
"{p}.svtr_block.{}.layer_norm1.weight",
blocks.len()
))
.is_some()
{
let i = blocks.len();
blocks.push(Block::load(&w, &format!("{p}.svtr_block.{i}"), heads, act)?);
}
if blocks.is_empty() {
return Err("no svtr_block in the weights".into());
}
let hidden = w.get(&format!("{p}.norm.weight"))?.shape[0];
if hidden % heads != 0 {
return Err(format!(
"hidden_size {hidden} is not divisible by {heads} heads"
));
}
let chars: Vec<String> = pre["character_list"]
.as_array()
.ok_or("preprocessor_config.json has no character_list")?
.iter()
.map(|c| {
c.as_str()
.map(str::to_owned)
.ok_or("the character list has a non-string item")
})
.collect::<Result<_, _>>()?;
let head = Linear::load(&w, "head.head")?;
if head.weight_t.shape[1] != chars.len() {
return Err(format!(
"output layer has {} classes but the character list has {} items",
head.weight_t.shape[1],
chars.len()
));
}
Ok(Rec {
backbone,
skip: ConvAct::load(&w, &format!("{p}.conv_block.0"), false, act)?,
reduce: ConvAct::load(&w, &format!("{p}.conv_block.1"), false, act)?,
local: ConvAct::load(&w, &format!("{p}.conv_block.2"), true, act)?,
blocks,
norm: LayerNorm::load(&w, &format!("{p}.norm"), LN_EPS)?,
head,
chars,
max_w: pre["max_image_width"]
.as_u64()
.map_or(DEFAULT_MAX_W, |v| v as usize),
})
}
pub fn recognize(&self, line: &Image) -> Result<(String, f32), String> {
let x = self.preprocess(line)?;
let logits = self.logits_on(&Cpu, &x);
let rows = Cpu.ctc_digest(&logits);
Ok(self.decode(&rows[0]))
}
pub fn input_channels(&self) -> usize {
3 }
pub fn input_height(&self) -> usize {
TARGET_H
}
pub fn preprocess(&self, line: &Image) -> Result<Tensor, String> {
if line.width == 0 || line.height == 0 {
return Err("empty line image".into());
}
if line.channels != 1 && line.channels != 3 {
return Err(format!(
"images with {} channels are not supported",
line.channels
));
}
Ok(preprocess(line, self.max_w))
}
pub fn logits_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
let last = self.backbone.forward_last_on(e, x);
let pooled = e.avg_pool2d(&last, (3, 2));
drop(last);
let seq = self.encode_on(e, &pooled);
e.linear(&self.head, &seq)
}
fn encode_on<E: Exec>(&self, e: &E, x: &E::T) -> E::T {
let residual = e.chw_to_seq(&self.skip.forward_on(e, x));
let r = self.reduce.forward_on(e, x);
let r = e.add(&r, &self.local.forward_on(e, &r));
let mut s = e.chw_to_seq(&r);
drop(r);
for b in &self.blocks {
s = b.forward_on(e, s);
}
e.add(&e.layer_norm(&self.norm, &s), &residual)
}
pub fn decode(&self, rows: &[(u32, f32)]) -> (String, f32) {
let mut text = String::new();
let (mut sum, mut kept) = (0f32, 0usize);
let mut prev = u32::MAX;
for &(arg, p) in rows {
if arg != prev && arg != 0 {
text.push_str(&self.chars[arg as usize]);
sum += p;
kept += 1;
}
prev = arg;
}
let score = if kept == 0 { 0.0 } else { sum / kept as f32 };
(text, score)
}
}
fn target_width(w: usize, h: usize, max_w: usize) -> usize {
let r = w as f64 / h as f64;
let ratio = r.max(MIN_W as f64 / TARGET_H as f64);
let tw = (TARGET_H as f64 * ratio) as usize;
if tw > max_w {
return max_w;
}
let exact = (TARGET_H as f64 * r).ceil() as usize;
if tw >= exact { exact.max(1) } else { tw }
}
fn preprocess(img: &Image, max_w: usize) -> Tensor {
let tw = target_width(img.width, img.height, max_w);
let (th, out_w) = (TARGET_H, tw.max(MIN_W));
let mut data = vec![0f32; 3 * th * out_w];
let xs = taps(img.width, tw);
let ys = taps(img.height, th);
for (oy, &(y0, y1, fy)) in ys.iter().enumerate() {
for (ox, &(x0, x1, fx)) in xs.iter().enumerate() {
for c in 0..3 {
let p = |x: usize, y: usize| f32::from(img.at(x, y, c));
let top = p(x0, y0) + (p(x1, y0) - p(x0, y0)) * fx;
let bot = p(x0, y1) + (p(x1, y1) - p(x0, y1)) * fx;
let v = (top + (bot - top) * fy).round_ties_even().clamp(0.0, 255.0);
let oc = 2 - c;
data[(oc * th + oy) * out_w + ox] = (v / 255.0 - 0.5) / 0.5;
}
}
}
Tensor::new(vec![3, th, out_w], data)
}
fn taps(n_in: usize, n_out: usize) -> Vec<(usize, usize, f32)> {
let scale = n_in as f32 / n_out as f32;
(0..n_out)
.map(|o| {
let src = ((o as f32 + 0.5) * scale - 0.5).max(0.0);
let i0 = (src as usize).min(n_in - 1);
let i1 = (i0 + 1).min(n_in - 1);
(i0, i1, src - i0 as f32)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_width_follows_reference() {
assert_eq!(target_width(960, 48, 3200), 960);
assert_eq!(target_width(1001, 30, 3200), 1601);
assert_eq!(target_width(100, 30, 3200), 160);
assert_eq!(target_width(101, 30, 3200), 162);
assert_eq!(target_width(10000, 30, 3200), 3200);
}
#[test]
fn taps_identity_and_half() {
let t = taps(4, 4);
assert!(
t.iter()
.enumerate()
.all(|(i, &(a, _, f))| a == i && f == 0.0)
);
let t = taps(4, 2);
assert_eq!((t[0].0, t[0].1), (0, 1));
assert!((t[0].2 - 0.5).abs() < 1e-6);
}
}