ppocr_rs/
cell_detection.rs1use crate::ocr_error::OcrError;
27use ndarray::{Array, Array2, Array4};
28use ort::{
29 inputs,
30 session::{builder::GraphOptimizationLevel, Session},
31 value::Tensor,
32};
33use std::path::Path;
34
35pub const CELL_DETECT_INPUT_SIZE: u32 = 640;
37
38#[derive(Debug, Clone)]
39pub struct CellBbox {
40 pub left: i32,
43 pub top: i32,
44 pub right: i32,
45 pub bottom: i32,
46 pub score: f32,
48}
49
50impl CellBbox {
51 pub fn width(&self) -> i32 { (self.right - self.left).max(0) }
52 pub fn height(&self) -> i32 { (self.bottom - self.top).max(0) }
53 pub fn cx(&self) -> i32 { (self.left + self.right) / 2 }
54 pub fn cy(&self) -> i32 { (self.top + self.bottom) / 2 }
55}
56
57pub struct CellDetector {
58 session: Session,
59 pub conf_thresh: f32,
60 pub nms_iou: f32,
61}
62
63impl CellDetector {
64 pub fn from_path(model_path: impl AsRef<Path>) -> Result<Self, OcrError> {
65 let session = Session::builder()?
66 .with_optimization_level(GraphOptimizationLevel::Level3)?
67 .commit_from_file(model_path)?;
68 Ok(Self { session, conf_thresh: 0.3, nms_iou: 0.5 })
69 }
70
71 pub fn from_session(session: Session) -> Self {
72 Self { session, conf_thresh: 0.3, nms_iou: 0.5 }
73 }
74
75 pub fn detect(&mut self, image: &image::RgbImage) -> Result<Vec<CellBbox>, OcrError> {
78 let (input_blob, scale) = preprocess(image, CELL_DETECT_INPUT_SIZE);
80
81 let im_shape: Array2<f32> = ndarray::arr2(&[[
82 CELL_DETECT_INPUT_SIZE as f32, CELL_DETECT_INPUT_SIZE as f32,
83 ]]);
84 let scale_factor: Array2<f32> = ndarray::arr2(&[[scale, scale]]);
85
86 let inputs_meta: Vec<String> = self.session.inputs.iter()
89 .map(|i| i.name.clone()).collect();
90 if inputs_meta.len() < 3 {
91 return Err(OcrError::ModelInput(format!(
92 "RT-DETR-L cell det si aspetta ≥3 input, trovati {} ({:?})",
93 inputs_meta.len(), inputs_meta,
94 )));
95 }
96 let mut name_im_shape = inputs_meta[0].clone();
97 let mut name_image = inputs_meta[1].clone();
98 let mut name_scale_factor = inputs_meta[2].clone();
99 for n in &inputs_meta {
100 if n == "im_shape" { name_im_shape = n.clone(); }
101 if n == "image" { name_image = n.clone(); }
102 if n == "scale_factor" { name_scale_factor = n.clone(); }
103 }
104
105 let outputs = self.session.run(inputs![
106 name_im_shape => Tensor::from_array(im_shape)?,
107 name_image => Tensor::from_array(input_blob)?,
108 name_scale_factor => Tensor::from_array(scale_factor)?,
109 ]?)?;
110
111 let (_, primary) = outputs.iter().next()
114 .ok_or_else(|| OcrError::ModelOutput("RT-DETR-L cell det no output".into()))?;
115 let (shape_vec, raw) = crate::compat::tensor_extract_with_shape_f32(&primary)?;
116 let n = shape_vec[0] as usize;
117 let cols = if shape_vec.len() > 1 { shape_vec[1] as usize } else { 0 };
118 if cols < 6 {
119 return Err(OcrError::ModelOutput(format!(
120 "RT-DETR-L cell det output cols={cols} (atteso ≥6)",
121 )));
122 }
123 let (img_w, img_h) = (image.width() as i32, image.height() as i32);
124 let mut cells: Vec<CellBbox> = Vec::with_capacity(n);
125 for i in 0..n {
126 let off = i * cols;
127 let class_id = raw[off] as i32;
128 let score = raw[off + 1];
129 if score < self.conf_thresh { continue; }
130 if class_id < 0 { continue; }
131 let x1 = raw[off + 2];
132 let y1 = raw[off + 3];
133 let x2 = raw[off + 4];
134 let y2 = raw[off + 5];
135 let left = (x1.max(0.0) as i32).min(img_w);
136 let top = (y1.max(0.0) as i32).min(img_h);
137 let right = (x2.max(0.0) as i32).min(img_w);
138 let bottom = (y2.max(0.0) as i32).min(img_h);
139 if right <= left || bottom <= top { continue; }
140 cells.push(CellBbox { left, top, right, bottom, score });
141 }
142 nms(&mut cells, self.nms_iou);
144 Ok(cells)
145 }
146}
147
148fn preprocess(image: &image::RgbImage, target_size: u32) -> (Array4<f32>, f32) {
152 let (orig_w, orig_h) = (image.width(), image.height());
153 let scale = (target_size as f32 / orig_h as f32).min(target_size as f32 / orig_w as f32);
154 let new_w = (orig_w as f32 * scale).round() as u32;
155 let new_h = (orig_h as f32 * scale).round() as u32;
156 let resized = image::imageops::resize(
157 image, new_w, new_h, image::imageops::FilterType::Triangle,
158 );
159 let mean = [0.485f32, 0.456, 0.406];
160 let std = [0.229f32, 0.224, 0.225];
161 let target = target_size as usize;
162 let mut blob: Array4<f32> = Array::zeros((1, 3, target, target));
163 for y in 0..new_h as usize {
164 for x in 0..new_w as usize {
165 let pixel = resized.get_pixel(x as u32, y as u32);
166 for c in 0..3 {
167 let v = pixel[c] as f32 / 255.0;
168 blob[[0, c, y, x]] = (v - mean[c]) / std[c];
169 }
170 }
171 }
172 (blob, scale)
173}
174
175fn nms(cells: &mut Vec<CellBbox>, thresh: f32) {
177 cells.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
178 let mut keep = vec![true; cells.len()];
179 for i in 0..cells.len() {
180 if !keep[i] { continue; }
181 for j in (i + 1)..cells.len() {
182 if !keep[j] { continue; }
183 if iou(&cells[i], &cells[j]) > thresh { keep[j] = false; }
184 }
185 }
186 let mut idx = 0;
187 cells.retain(|_| { let k = keep[idx]; idx += 1; k });
188}
189
190fn iou(a: &CellBbox, b: &CellBbox) -> f32 {
191 let x_min = a.left.max(b.left);
192 let y_min = a.top.max(b.top);
193 let x_max = a.right.min(b.right);
194 let y_max = a.bottom.min(b.bottom);
195 let w = (x_max - x_min).max(0);
196 let h = (y_max - y_min).max(0);
197 let inter = (w * h) as f32;
198 let area_a = (a.width() * a.height()) as f32;
199 let area_b = (b.width() * b.height()) as f32;
200 let union = area_a + area_b - inter;
201 if union <= 0.0 { 0.0 } else { inter / union }
202}
203
204pub fn derive_grid(cells: Vec<CellBbox>) -> Vec<Vec<CellBbox>> {
240 if cells.is_empty() { return Vec::new(); }
241 let mut sorted = cells;
242 sorted.sort_by_key(|c| c.cy());
243
244 let mut hs: Vec<i32> = sorted.iter().map(|c| c.height()).collect();
246 hs.sort();
247 let median_h = hs.get(hs.len() / 2).copied().unwrap_or(1).max(1);
248 let row_threshold = ((median_h as f32) * 0.6).max(8.0) as i32;
249
250 let mut rows: Vec<Vec<CellBbox>> = Vec::new();
251 let mut row_centroids: Vec<i32> = Vec::new();
252 for cell in sorted {
253 let cy = cell.cy();
254 let mut placed_idx: Option<usize> = None;
255 for (i, &rc) in row_centroids.iter().enumerate() {
256 if (cy - rc).abs() < row_threshold {
257 placed_idx = Some(i);
258 break;
259 }
260 }
261 match placed_idx {
262 Some(i) => {
263 let n = rows[i].len() as i32;
264 rows[i].push(cell);
265 row_centroids[i] = (row_centroids[i] * n + cy) / (n + 1);
267 }
268 None => {
269 rows.push(vec![cell]);
270 row_centroids.push(cy);
271 }
272 }
273 }
274
275 let mut indexed: Vec<(Vec<CellBbox>, i32)> = rows.into_iter()
277 .zip(row_centroids.into_iter())
278 .collect();
279 indexed.sort_by_key(|x| x.1);
280 let mut result: Vec<Vec<CellBbox>> = indexed.into_iter().map(|(r, _)| r).collect();
281 for row in result.iter_mut() {
283 row.sort_by_key(|c| c.left);
284 }
285 result
286}
287
288pub fn grid_to_gfm<F>(grid: &[Vec<CellBbox>], mut cell_text: F) -> String
301where
302 F: FnMut(usize, usize) -> String,
303{
304 if grid.is_empty() { return String::new(); }
305 let max_cols = grid.iter().map(|r| r.len()).max().unwrap_or(0);
306 if max_cols == 0 { return String::new(); }
307 let mut out = String::new();
308 for (row_idx, row) in grid.iter().enumerate() {
309 out.push('|');
310 for col_idx in 0..max_cols {
311 let txt = if col_idx < row.len() {
312 let raw = cell_text(row_idx, col_idx);
313 sanitize_cell(&raw)
314 } else { String::new() };
315 out.push(' ');
316 out.push_str(&txt);
317 out.push(' ');
318 out.push('|');
319 }
320 out.push('\n');
321 if row_idx == 0 {
323 out.push('|');
324 for _ in 0..max_cols { out.push_str(" --- |"); }
325 out.push('\n');
326 }
327 }
328 out
329}
330
331fn sanitize_cell(s: &str) -> String {
336 let mut out = String::with_capacity(s.len());
337 let mut prev_space = true; for ch in s.chars() {
339 match ch {
340 '|' => { out.push('\\'); out.push('|'); prev_space = false; }
341 '\n' | '\r' | '\t' => {
342 if !prev_space { out.push(' '); prev_space = true; }
343 }
344 ' ' => {
345 if !prev_space { out.push(' '); prev_space = true; }
346 }
347 c => { out.push(c); prev_space = false; }
348 }
349 }
350 while out.ends_with(' ') { out.pop(); }
352 out
353}