1use crate::ocr_error::OcrError;
34use ndarray::{Array, Array4};
35use ort::{
36 inputs,
37 session::{builder::GraphOptimizationLevel, Session},
38 value::Tensor,
39};
40use std::path::Path;
41
42pub const SLANEXT_INPUT_SIZE: u32 = 512;
44
45const EN_DICT_TOKENS: &[&str] = &[
51 "<thead>",
52 "<tr>",
53 "<td>",
54 "</td>",
55 "</tr>",
56 "</thead>",
57 "<tbody>",
58 "</tbody>",
59 "<td",
60 r#" colspan="5""#,
61 r#" colspan="2""#,
62 r#" colspan="3""#,
63 r#" rowspan="2""#,
64 r#" colspan="4""#,
65 r#" colspan="6""#,
66 r#" rowspan="3""#,
67 r#" colspan="9""#,
68 r#" colspan="10""#,
69 r#" colspan="7""#,
70 r#" rowspan="4""#,
71 r#" rowspan="5""#,
72 r#" rowspan="9""#,
73 r#" colspan="8""#,
74 r#" rowspan="8""#,
75 r#" rowspan="6""#,
76 r#" rowspan="7""#,
77 r#" rowspan="10""#,
78 ">",
79 "<td></td>",
80];
81
82#[derive(Debug, Clone)]
86pub struct TableCellBox {
87 pub x1: f32,
88 pub y1: f32,
89 pub x2: f32,
90 pub y2: f32,
91}
92
93impl TableCellBox {
94 pub fn width(&self) -> f32 { (self.x2 - self.x1).max(0.0) }
95 pub fn height(&self) -> f32 { (self.y2 - self.y1).max(0.0) }
96 pub fn cx(&self) -> f32 { (self.x1 + self.x2) * 0.5 }
97 pub fn cy(&self) -> f32 { (self.y1 + self.y2) * 0.5 }
98}
99
100#[derive(Debug, Clone)]
102pub struct TableStructure {
103 pub html_tokens: String,
106 pub score: f32,
108 pub cell_boxes: Vec<TableCellBox>,
111}
112
113pub struct TableStructureRecognizer {
116 session: Session,
117 token_dict: Vec<String>,
120 end_idx: usize,
122 input_size: u32,
124}
125
126impl TableStructureRecognizer {
127 pub fn from_path(model_path: impl AsRef<Path>) -> Result<Self, OcrError> {
129 Self::from_path_with_dict(model_path, None)
130 }
131
132 pub fn from_path_with_dict(
138 model_path: impl AsRef<Path>,
139 dict_path: Option<&Path>,
140 ) -> Result<Self, OcrError> {
141 let session = Session::builder()?
142 .with_optimization_level(GraphOptimizationLevel::Level3)?
143 .commit_from_file(model_path)?;
144
145 let token_dict = build_token_dict(dict_path)?;
146 let end_idx = find_end_idx(&token_dict);
147
148 Ok(Self { session, token_dict, end_idx, input_size: SLANEXT_INPUT_SIZE })
149 }
150
151 pub fn with_input_size(mut self, size: u32) -> Self {
153 self.input_size = size;
154 self
155 }
156
157 pub fn recognize(
163 &self,
164 image: &image::RgbImage,
165 ) -> Result<TableStructure, OcrError> {
166 let orig_w = image.width() as f32;
167 let orig_h = image.height() as f32;
168
169 let (blob, ratio_w, ratio_h) = preprocess_slanext(image, self.input_size);
171
172 let input_name = self.session.inputs[0].name.clone();
174 let outputs = self.session.run(
175 inputs![input_name => Tensor::from_array(blob)?]?
176 )?;
177
178 let vocab = self.token_dict.len() as i64;
184
185 let mut all_out: Vec<(String, Vec<i64>, Vec<f32>)> = Vec::new();
187 for (name, val) in &outputs {
188 let (sh, d) = crate::compat::tensor_extract_with_shape_f32(&val)?;
189 all_out.push((name.to_lowercase(), sh, d));
190 }
191
192 let mut sp_idx: Option<usize> = None;
193 let mut lp_idx: Option<usize> = None;
194
195 for (i, (n, _, _)) in all_out.iter().enumerate() {
197 if sp_idx.is_none() && (n.contains("structure") || n.contains("prob")) {
198 sp_idx = Some(i);
199 } else if lp_idx.is_none() && (n.contains("loc") || n.contains("bbox") || n.contains("pred")) {
200 lp_idx = Some(i);
201 }
202 }
203 if sp_idx.is_none() || lp_idx.is_none() {
205 for (i, (_, sh, _)) in all_out.iter().enumerate() {
206 if Some(i) == sp_idx || Some(i) == lp_idx { continue; }
207 let last = sh.last().copied().unwrap_or(0);
208 if sp_idx.is_none() && last == vocab {
209 sp_idx = Some(i);
210 } else if lp_idx.is_none() {
211 lp_idx = Some(i);
212 }
213 }
214 }
215 if sp_idx.is_none() {
217 sp_idx = all_out.iter().enumerate()
218 .find(|(i, _)| Some(*i) != lp_idx)
219 .map(|(i, _)| i);
220 }
221 if lp_idx.is_none() {
222 lp_idx = all_out.iter().enumerate()
223 .find(|(i, _)| Some(*i) != sp_idx)
224 .map(|(i, _)| i);
225 }
226
227 let mut sp_shape: Vec<i64> = Vec::new();
228 let mut sp_data: Vec<f32> = Vec::new();
229 let mut lp_shape: Vec<i64> = Vec::new();
230 let mut lp_data: Vec<f32> = Vec::new();
231
232 if let Some(i) = sp_idx {
233 sp_shape = all_out[i].1.clone();
234 sp_data = all_out[i].2.clone();
235 }
236 if let Some(i) = lp_idx {
237 lp_shape = all_out[i].1.clone();
238 lp_data = all_out[i].2.clone();
239 }
240
241 if sp_shape.len() < 3 {
242 return Err(OcrError::ModelOutput(format!(
243 "SLANeXt: structure_probs shape atteso [1,T,V], trovato {sp_shape:?}"
244 )));
245 }
246
247 let t_len = sp_shape[1] as usize;
248 let v_len = sp_shape[2] as usize;
249 let lp_cols = lp_shape.get(2).copied().unwrap_or(4) as usize;
250 let structural = |t: &str| matches!(t,
256 "<thead>" | "</thead>" | "<tbody>" | "</tbody>" | "<tr>" | "</tr>" | "</td>" | ">"
257 );
258 let td_opener = |t: &str| !structural(t) && !t.is_empty() && t != "<eos>";
259
260 let mut html = String::with_capacity(256);
261 let mut cell_boxes: Vec<TableCellBox> = Vec::new();
262 let mut scores: Vec<f32> = Vec::new();
263
264 for step in 0..t_len {
265 let base = step * v_len;
267 let (char_idx, prob) = argmax_slice(&sp_data[base..base + v_len.min(sp_data.len().saturating_sub(base))]);
268
269 if step > 0 && char_idx == self.end_idx { break; }
270 if char_idx == 0 { continue; } let token = self.token_dict.get(char_idx)
273 .map(|s| s.as_str())
274 .unwrap_or("");
275 if token.is_empty() || token == "<eos>" { break; }
276
277 html.push_str(token);
278 scores.push(prob);
279
280 if td_opener(token) && lp_cols >= 4 {
281 let lp_base = step * lp_cols;
282 let needed = if lp_cols >= 6 { lp_base + 5 } else { lp_base + 3 };
283 if needed < lp_data.len() {
284 let pad_sz = self.input_size as f32;
285 let (xi2, yi2) = if lp_cols >= 6 { (lp_base + 4, lp_base + 5) }
289 else { (lp_base + 2, lp_base + 3) };
290 let x1 = decode_coord(lp_data[lp_base], pad_sz, ratio_w, orig_w);
291 let y1 = decode_coord(lp_data[lp_base + 1], pad_sz, ratio_h, orig_h);
292 let x2 = decode_coord(lp_data[xi2], pad_sz, ratio_w, orig_w);
293 let y2 = decode_coord(lp_data[yi2], pad_sz, ratio_h, orig_h);
294 if x2 > x1 && y2 > y1 {
295 cell_boxes.push(TableCellBox { x1, y1, x2, y2 });
296 }
297 }
298 }
299 }
300
301 let score = if scores.is_empty() { 0.0 } else {
302 scores.iter().sum::<f32>() / scores.len() as f32
303 };
304
305 Ok(TableStructure { html_tokens: html, score, cell_boxes })
306 }
307
308 pub fn vocab_size(&self) -> usize { self.token_dict.len() }
310}
311
312fn build_token_dict(dict_path: Option<&Path>) -> Result<Vec<String>, OcrError> {
315 if let Some(p) = dict_path {
316 let content = std::fs::read_to_string(p)?;
317 let mut dict = vec!["<pad>".to_string()];
319 for line in content.lines() {
320 let t = line.trim_end_matches('\r').to_string();
321 if !t.is_empty() {
322 dict.push(t);
323 }
324 }
325 if !dict.iter().any(|t| t == "<eos>") {
327 dict.push("<eos>".to_string());
328 }
329 Ok(dict)
330 } else {
331 let mut dict = vec!["<pad>".to_string()];
333 dict.extend(EN_DICT_TOKENS.iter().map(|s| s.to_string()));
334 dict.push("<eos>".to_string());
335 Ok(dict)
336 }
337}
338
339fn find_end_idx(dict: &[String]) -> usize {
340 dict.iter().position(|t| t == "</tbody>")
342 .or_else(|| dict.iter().position(|t| t == "<eos>"))
343 .unwrap_or(dict.len().saturating_sub(1))
344}
345
346fn preprocess_slanext(
353 image: &image::RgbImage,
354 target: u32,
355) -> (Array4<f32>, f32, f32) {
356 let (orig_w, orig_h) = (image.width(), image.height());
357 let scale = (target as f32 / orig_w as f32).min(target as f32 / orig_h as f32);
359 let new_w = ((orig_w as f32 * scale).round() as u32).max(1);
360 let new_h = ((orig_h as f32 * scale).round() as u32).max(1);
361 let ratio_w = new_w as f32 / orig_w as f32;
362 let ratio_h = new_h as f32 / orig_h as f32;
363
364 let resized = image::imageops::resize(image, new_w, new_h, image::imageops::FilterType::Triangle);
365 let mean = [0.485f32, 0.456, 0.406];
366 let std = [0.229f32, 0.224, 0.225];
367 let t = target as usize;
368 let mut blob: Array4<f32> = Array::zeros((1, 3, t, t));
370 for y in 0..new_h as usize {
371 for x in 0..new_w as usize {
372 let p = resized.get_pixel(x as u32, y as u32);
373 for c in 0..3usize {
374 blob[[0, c, y, x]] = (p[c] as f32 / 255.0 - mean[c]) / std[c];
375 }
376 }
377 }
378 (blob, ratio_w, ratio_h)
379}
380
381#[inline]
387fn decode_coord(norm: f32, pad_size: f32, ratio: f32, orig_max: f32) -> f32 {
388 (norm * pad_size / ratio).clamp(0.0, orig_max)
389}
390
391fn argmax_slice(s: &[f32]) -> (usize, f32) {
394 s.iter().copied().enumerate()
395 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
396 .unwrap_or((0, 0.0))
397}