visual_cortex_ocr_onnx/
engine.rs1use std::path::Path;
2
3use ndarray::Array4;
4use ort::session::Session;
5use ort::value::TensorRef;
6use visual_cortex_capture::{FrameView, PxRect};
7use visual_cortex_vision::{DetectorError, OcrEngine, TextSpan};
8
9use crate::det::{det_target_size, extract_boxes, preprocess_det, view_to_rgb};
10use crate::error::OcrError;
11use crate::models::ensure_all_cached;
12use crate::rec::{build_charset, ctc_decode, preprocess_rec};
13
14const DET_THRESHOLD: f32 = 0.2;
19const BOX_PAD: u32 = 5;
23const MIN_CONFIDENCE: f32 = 0.3;
25
26pub struct PaddleOcr {
28 det: Session,
29 rec: Session,
30 charset: Vec<String>,
31}
32
33impl PaddleOcr {
34 pub async fn new() -> Result<Self, OcrError> {
37 let (det, rec, dict) = ensure_all_cached().await?;
38 Self::from_paths(&det, &rec, &dict)
39 }
40
41 pub fn from_paths(det: &Path, rec: &Path, dict: &Path) -> Result<Self, OcrError> {
43 let det = Session::builder()
44 .and_then(|mut b| b.commit_from_file(det))
45 .map_err(|e| OcrError::ModelLoad(format!("det: {e}")))?;
46 let rec = Session::builder()
47 .and_then(|mut b| b.commit_from_file(rec))
48 .map_err(|e| OcrError::ModelLoad(format!("rec: {e}")))?;
49 let dict =
50 std::fs::read_to_string(dict).map_err(|e| OcrError::ModelLoad(format!("dict: {e}")))?;
51 Ok(Self {
52 det,
53 rec,
54 charset: build_charset(&dict),
55 })
56 }
57
58 fn run_det(&mut self, input: Array4<f32>, tw: u32, th: u32) -> Result<Vec<f32>, OcrError> {
59 let outputs = self
60 .det
61 .run(ort::inputs!["x" => TensorRef::from_array_view(input.view())
62 .map_err(|e| OcrError::Inference(format!("det input: {e}")))?])
63 .map_err(|e| OcrError::Inference(format!("det run: {e}")))?;
64 let map = outputs["sigmoid_0.tmp_0"]
65 .try_extract_array::<f32>()
66 .map_err(|e| OcrError::Inference(format!("det output: {e}")))?;
67 let flat: Vec<f32> = map.iter().copied().collect();
68 let expected = tw as usize * th as usize;
69 if flat.len() != expected {
70 return Err(OcrError::Inference(format!(
71 "det output has {} elements, expected {tw}x{th} = {expected}",
72 flat.len()
73 )));
74 }
75 Ok(flat)
76 }
77
78 fn run_rec(&mut self, input: Array4<f32>) -> Result<(String, f32), OcrError> {
79 let outputs = self
80 .rec
81 .run(ort::inputs!["x" => TensorRef::from_array_view(input.view())
82 .map_err(|e| OcrError::Inference(format!("rec input: {e}")))?])
83 .map_err(|e| OcrError::Inference(format!("rec run: {e}")))?;
84 let probs = outputs["softmax_2.tmp_0"]
85 .try_extract_array::<f32>()
86 .map_err(|e| OcrError::Inference(format!("rec output: {e}")))?;
87 let shape = probs.shape().to_vec(); if shape.len() != 3 || shape[2] != self.charset.len() {
89 return Err(OcrError::Inference(format!(
90 "rec output shape {shape:?} does not match charset len {}",
91 self.charset.len()
92 )));
93 }
94 let flat: Vec<f32> = probs.iter().copied().collect();
95 Ok(ctc_decode(&flat, shape[1], shape[2], &self.charset))
96 }
97}
98
99impl OcrEngine for PaddleOcr {
100 fn recognize(&mut self, view: &FrameView<'_>) -> Result<Vec<TextSpan>, DetectorError> {
101 let rgb = view_to_rgb(view);
102 let (vw, vh) = (rgb.width(), rgb.height());
103 let (tw, th) = det_target_size(vw, vh);
104 let det_input = preprocess_det(&rgb, tw, th);
105 let prob = self
106 .run_det(det_input, tw, th)
107 .map_err(|e| DetectorError::Ocr(e.to_string()))?;
108 let boxes = extract_boxes(&prob, tw as usize, th as usize, DET_THRESHOLD, BOX_PAD);
109
110 let (sx, sy) = (vw as f32 / tw as f32, vh as f32 / th as f32);
112 let mut spans = Vec::new();
113 for b in boxes {
114 let x = ((b.x as f32 * sx) as u32).min(vw - 1);
115 let y = ((b.y as f32 * sy) as u32).min(vh - 1);
116 let w = ((b.w as f32 * sx).ceil() as u32).clamp(1, vw - x);
117 let h = ((b.h as f32 * sy).ceil() as u32).clamp(1, vh - y);
118 let crop = image::imageops::crop_imm(&rgb, x, y, w, h).to_image();
119 let (text, confidence) = self
120 .run_rec(preprocess_rec(&crop))
121 .map_err(|e| DetectorError::Ocr(e.to_string()))?;
122 if !text.is_empty() && confidence >= MIN_CONFIDENCE {
123 spans.push(TextSpan {
124 text,
125 confidence,
126 bbox: PxRect { x, y, w, h },
127 });
128 }
129 }
130 Ok(spans)
131 }
132}