use crate::ocr_error::OcrError;
use ndarray::{Array, Array4};
use ort::{
inputs,
session::{builder::GraphOptimizationLevel, Session},
value::Tensor,
};
use std::path::Path;
pub const SLANEXT_INPUT_SIZE: u32 = 512;
const EN_DICT_TOKENS: &[&str] = &[
"<thead>",
"<tr>",
"<td>",
"</td>",
"</tr>",
"</thead>",
"<tbody>",
"</tbody>",
"<td",
r#" colspan="5""#,
r#" colspan="2""#,
r#" colspan="3""#,
r#" rowspan="2""#,
r#" colspan="4""#,
r#" colspan="6""#,
r#" rowspan="3""#,
r#" colspan="9""#,
r#" colspan="10""#,
r#" colspan="7""#,
r#" rowspan="4""#,
r#" rowspan="5""#,
r#" rowspan="9""#,
r#" colspan="8""#,
r#" rowspan="8""#,
r#" rowspan="6""#,
r#" rowspan="7""#,
r#" rowspan="10""#,
">",
"<td></td>",
];
#[derive(Debug, Clone)]
pub struct TableCellBox {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
}
impl TableCellBox {
pub fn width(&self) -> f32 { (self.x2 - self.x1).max(0.0) }
pub fn height(&self) -> f32 { (self.y2 - self.y1).max(0.0) }
pub fn cx(&self) -> f32 { (self.x1 + self.x2) * 0.5 }
pub fn cy(&self) -> f32 { (self.y1 + self.y2) * 0.5 }
}
#[derive(Debug, Clone)]
pub struct TableStructure {
pub html_tokens: String,
pub score: f32,
pub cell_boxes: Vec<TableCellBox>,
}
pub struct TableStructureRecognizer {
session: Session,
token_dict: Vec<String>,
end_idx: usize,
input_size: u32,
}
impl TableStructureRecognizer {
pub fn from_path(model_path: impl AsRef<Path>) -> Result<Self, OcrError> {
Self::from_path_with_dict(model_path, None)
}
pub fn from_path_with_dict(
model_path: impl AsRef<Path>,
dict_path: Option<&Path>,
) -> Result<Self, OcrError> {
let session = Session::builder()?
.with_optimization_level(GraphOptimizationLevel::Level3)?
.commit_from_file(model_path)?;
let token_dict = build_token_dict(dict_path)?;
let end_idx = find_end_idx(&token_dict);
Ok(Self { session, token_dict, end_idx, input_size: SLANEXT_INPUT_SIZE })
}
pub fn with_input_size(mut self, size: u32) -> Self {
self.input_size = size;
self
}
pub fn recognize(
&self,
image: &image::RgbImage,
) -> Result<TableStructure, OcrError> {
let orig_w = image.width() as f32;
let orig_h = image.height() as f32;
let (blob, ratio_w, ratio_h) = preprocess_slanext(image, self.input_size);
let input_name = self.session.inputs[0].name.clone();
let outputs = self.session.run(
inputs![input_name => Tensor::from_array(blob)?]?
)?;
let vocab = self.token_dict.len() as i64;
let mut all_out: Vec<(String, Vec<i64>, Vec<f32>)> = Vec::new();
for (name, val) in &outputs {
let (sh, d) = crate::compat::tensor_extract_with_shape_f32(&val)?;
all_out.push((name.to_lowercase(), sh, d));
}
let mut sp_idx: Option<usize> = None;
let mut lp_idx: Option<usize> = None;
for (i, (n, _, _)) in all_out.iter().enumerate() {
if sp_idx.is_none() && (n.contains("structure") || n.contains("prob")) {
sp_idx = Some(i);
} else if lp_idx.is_none() && (n.contains("loc") || n.contains("bbox") || n.contains("pred")) {
lp_idx = Some(i);
}
}
if sp_idx.is_none() || lp_idx.is_none() {
for (i, (_, sh, _)) in all_out.iter().enumerate() {
if Some(i) == sp_idx || Some(i) == lp_idx { continue; }
let last = sh.last().copied().unwrap_or(0);
if sp_idx.is_none() && last == vocab {
sp_idx = Some(i);
} else if lp_idx.is_none() {
lp_idx = Some(i);
}
}
}
if sp_idx.is_none() {
sp_idx = all_out.iter().enumerate()
.find(|(i, _)| Some(*i) != lp_idx)
.map(|(i, _)| i);
}
if lp_idx.is_none() {
lp_idx = all_out.iter().enumerate()
.find(|(i, _)| Some(*i) != sp_idx)
.map(|(i, _)| i);
}
let mut sp_shape: Vec<i64> = Vec::new();
let mut sp_data: Vec<f32> = Vec::new();
let mut lp_shape: Vec<i64> = Vec::new();
let mut lp_data: Vec<f32> = Vec::new();
if let Some(i) = sp_idx {
sp_shape = all_out[i].1.clone();
sp_data = all_out[i].2.clone();
}
if let Some(i) = lp_idx {
lp_shape = all_out[i].1.clone();
lp_data = all_out[i].2.clone();
}
if sp_shape.len() < 3 {
return Err(OcrError::ModelOutput(format!(
"SLANeXt: structure_probs shape atteso [1,T,V], trovato {sp_shape:?}"
)));
}
let t_len = sp_shape[1] as usize;
let v_len = sp_shape[2] as usize;
let lp_cols = lp_shape.get(2).copied().unwrap_or(4) as usize;
let structural = |t: &str| matches!(t,
"<thead>" | "</thead>" | "<tbody>" | "</tbody>" | "<tr>" | "</tr>" | "</td>" | ">"
);
let td_opener = |t: &str| !structural(t) && !t.is_empty() && t != "<eos>";
let mut html = String::with_capacity(256);
let mut cell_boxes: Vec<TableCellBox> = Vec::new();
let mut scores: Vec<f32> = Vec::new();
for step in 0..t_len {
let base = step * v_len;
let (char_idx, prob) = argmax_slice(&sp_data[base..base + v_len.min(sp_data.len().saturating_sub(base))]);
if step > 0 && char_idx == self.end_idx { break; }
if char_idx == 0 { continue; }
let token = self.token_dict.get(char_idx)
.map(|s| s.as_str())
.unwrap_or("");
if token.is_empty() || token == "<eos>" { break; }
html.push_str(token);
scores.push(prob);
if td_opener(token) && lp_cols >= 4 {
let lp_base = step * lp_cols;
let needed = if lp_cols >= 6 { lp_base + 5 } else { lp_base + 3 };
if needed < lp_data.len() {
let pad_sz = self.input_size as f32;
let (xi2, yi2) = if lp_cols >= 6 { (lp_base + 4, lp_base + 5) }
else { (lp_base + 2, lp_base + 3) };
let x1 = decode_coord(lp_data[lp_base], pad_sz, ratio_w, orig_w);
let y1 = decode_coord(lp_data[lp_base + 1], pad_sz, ratio_h, orig_h);
let x2 = decode_coord(lp_data[xi2], pad_sz, ratio_w, orig_w);
let y2 = decode_coord(lp_data[yi2], pad_sz, ratio_h, orig_h);
if x2 > x1 && y2 > y1 {
cell_boxes.push(TableCellBox { x1, y1, x2, y2 });
}
}
}
}
let score = if scores.is_empty() { 0.0 } else {
scores.iter().sum::<f32>() / scores.len() as f32
};
Ok(TableStructure { html_tokens: html, score, cell_boxes })
}
pub fn vocab_size(&self) -> usize { self.token_dict.len() }
}
fn build_token_dict(dict_path: Option<&Path>) -> Result<Vec<String>, OcrError> {
if let Some(p) = dict_path {
let content = std::fs::read_to_string(p)?;
let mut dict = vec!["<pad>".to_string()];
for line in content.lines() {
let t = line.trim_end_matches('\r').to_string();
if !t.is_empty() {
dict.push(t);
}
}
if !dict.iter().any(|t| t == "<eos>") {
dict.push("<eos>".to_string());
}
Ok(dict)
} else {
let mut dict = vec!["<pad>".to_string()];
dict.extend(EN_DICT_TOKENS.iter().map(|s| s.to_string()));
dict.push("<eos>".to_string());
Ok(dict)
}
}
fn find_end_idx(dict: &[String]) -> usize {
dict.iter().position(|t| t == "</tbody>")
.or_else(|| dict.iter().position(|t| t == "<eos>"))
.unwrap_or(dict.len().saturating_sub(1))
}
fn preprocess_slanext(
image: &image::RgbImage,
target: u32,
) -> (Array4<f32>, f32, f32) {
let (orig_w, orig_h) = (image.width(), image.height());
let scale = (target as f32 / orig_w as f32).min(target as f32 / orig_h as f32);
let new_w = ((orig_w as f32 * scale).round() as u32).max(1);
let new_h = ((orig_h as f32 * scale).round() as u32).max(1);
let ratio_w = new_w as f32 / orig_w as f32;
let ratio_h = new_h as f32 / orig_h as f32;
let resized = image::imageops::resize(image, new_w, new_h, image::imageops::FilterType::Triangle);
let mean = [0.485f32, 0.456, 0.406];
let std = [0.229f32, 0.224, 0.225];
let t = target as usize;
let mut blob: Array4<f32> = Array::zeros((1, 3, t, t));
for y in 0..new_h as usize {
for x in 0..new_w as usize {
let p = resized.get_pixel(x as u32, y as u32);
for c in 0..3usize {
blob[[0, c, y, x]] = (p[c] as f32 / 255.0 - mean[c]) / std[c];
}
}
}
(blob, ratio_w, ratio_h)
}
#[inline]
fn decode_coord(norm: f32, pad_size: f32, ratio: f32, orig_max: f32) -> f32 {
(norm * pad_size / ratio).clamp(0.0, orig_max)
}
fn argmax_slice(s: &[f32]) -> (usize, f32) {
s.iter().copied().enumerate()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, 0.0))
}