1use crate::ocr_error::OcrError;
26use ndarray::{Array, Array2, Array4};
27use ort::{
28 inputs,
29 session::{builder::GraphOptimizationLevel, Session},
30 value::Tensor,
31};
32use std::path::Path;
33
34pub const LAYOUT_INPUT_SIZE: u32 = 800;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub enum LayoutClass {
41 Abstract = 0,
42 Algorithm = 1,
43 AsideText = 2,
44 Chart = 3,
45 Content = 4,
46 DisplayFormula = 5,
47 DocTitle = 6,
48 FigureTitle = 7,
49 Footer = 8,
50 FooterImage = 9,
51 Footnote = 10,
52 FormulaNumber = 11,
53 Header = 12,
54 HeaderImage = 13,
55 Image = 14,
56 InlineFormula = 15,
57 Number = 16,
58 ParagraphTitle = 17,
59 Reference = 18,
60 ReferenceContent = 19,
61 Seal = 20,
62 Table = 21,
63 Text = 22,
64 VerticalText = 23,
65 VisionFootnote = 24,
66}
67
68impl LayoutClass {
69 pub fn from_id(id: usize) -> Option<Self> {
70 Some(match id {
71 0 => Self::Abstract,
72 1 => Self::Algorithm,
73 2 => Self::AsideText,
74 3 => Self::Chart,
75 4 => Self::Content,
76 5 => Self::DisplayFormula,
77 6 => Self::DocTitle,
78 7 => Self::FigureTitle,
79 8 => Self::Footer,
80 9 => Self::FooterImage,
81 10 => Self::Footnote,
82 11 => Self::FormulaNumber,
83 12 => Self::Header,
84 13 => Self::HeaderImage,
85 14 => Self::Image,
86 15 => Self::InlineFormula,
87 16 => Self::Number,
88 17 => Self::ParagraphTitle,
89 18 => Self::Reference,
90 19 => Self::ReferenceContent,
91 20 => Self::Seal,
92 21 => Self::Table,
93 22 => Self::Text,
94 23 => Self::VerticalText,
95 24 => Self::VisionFootnote,
96 _ => return None,
97 })
98 }
99
100 pub fn semantic(self) -> SemanticClass {
105 use LayoutClass::*;
106 match self {
107 DocTitle | ParagraphTitle => SemanticClass::Title,
108 Header => SemanticClass::Header,
109 Footer => SemanticClass::Footer,
110 Reference => SemanticClass::List,
111 Chart | FooterImage | HeaderImage |
112 Image | Seal => SemanticClass::Figure,
113 Table => SemanticClass::Table,
114 DisplayFormula | InlineFormula => SemanticClass::Equation,
115 _ => SemanticClass::Text,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
126pub enum SemanticClass {
127 Text,
128 Title,
129 List,
130 Figure,
131 Table,
132 Header,
133 Footer,
134 Equation,
135}
136
137#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
140pub struct LayoutBox {
141 pub x: u32,
142 pub y: u32,
143 pub w: u32,
144 pub h: u32,
145 pub class: LayoutClass,
146 pub score: f32,
147 pub reading_order: i32,
150}
151
152impl LayoutBox {
153 pub fn xmin(&self) -> u32 { self.x }
154 pub fn ymin(&self) -> u32 { self.y }
155 pub fn xmax(&self) -> u32 { self.x + self.w }
156 pub fn ymax(&self) -> u32 { self.y + self.h }
157
158 pub fn contains(&self, px: u32, py: u32) -> bool {
161 px >= self.xmin() && px < self.xmax()
162 && py >= self.ymin() && py < self.ymax()
163 }
164
165 pub fn distance_to(&self, px: u32, py: u32) -> f32 {
168 let cx = self.x as f32 + self.w as f32 / 2.0;
169 let cy = self.y as f32 + self.h as f32 / 2.0;
170 let dx = cx - px as f32;
171 let dy = cy - py as f32;
172 (dx * dx + dy * dy).sqrt()
173 }
174}
175
176pub struct LayoutAnalyzer {
178 pub session: Session,
179 pub conf_thresh: f32,
180 pub nms_iou_thresh: f32,
181}
182
183impl LayoutAnalyzer {
184 pub fn from_path(model_path: impl AsRef<Path>) -> Result<Self, OcrError> {
186 let session = Session::builder()?
187 .with_optimization_level(GraphOptimizationLevel::Level3)?
188 .commit_from_file(model_path)?;
189 Ok(Self {
190 session,
191 conf_thresh: 0.50,
192 nms_iou_thresh: 0.50,
193 })
194 }
195
196 pub fn from_session(session: Session) -> Self {
199 Self {
200 session,
201 conf_thresh: 0.50,
202 nms_iou_thresh: 0.50,
203 }
204 }
205
206 pub fn analyze(&mut self, image: &image::RgbImage) -> Result<Vec<LayoutBox>, OcrError> {
210 let (input_blob, scale) = preprocess(image, LAYOUT_INPUT_SIZE);
212
213 let im_shape: Array2<f32> = ndarray::arr2(&[[
215 LAYOUT_INPUT_SIZE as f32, LAYOUT_INPUT_SIZE as f32,
216 ]]);
217 let scale_factor: Array2<f32> = ndarray::arr2(&[[scale, scale]]);
218
219 let inputs = self.session.inputs.iter()
224 .map(|i| i.name.clone())
225 .collect::<Vec<_>>();
226 if inputs.len() < 3 {
227 return Err(OcrError::ModelInput(format!(
228 "PP-DocLayoutV3 si aspetta ≥3 input, trovati {} ({:?})",
229 inputs.len(), inputs,
230 )));
231 }
232 let mut name_im_shape = inputs[0].clone();
235 let mut name_image = inputs[1].clone();
236 let mut name_scale_factor = inputs[2].clone();
237 for n in &inputs {
238 if n == "im_shape" { name_im_shape = n.clone(); }
239 if n == "image" { name_image = n.clone(); }
240 if n == "scale_factor" { name_scale_factor = n.clone(); }
241 }
242
243 let im_shape_t = Tensor::from_array(im_shape)?;
244 let image_t = Tensor::from_array(input_blob)?;
245 let scale_factor_t = Tensor::from_array(scale_factor)?;
246
247 let outputs = self.session.run(inputs![
248 name_im_shape => im_shape_t,
249 name_image => image_t,
250 name_scale_factor => scale_factor_t,
251 ]?)?;
252
253 let (_, primary) = outputs.iter().next()
257 .ok_or_else(|| OcrError::ModelOutput("PP-DocLayoutV3 non ha emesso output".into()))?;
258
259 let (shape_vec, raw_data) = crate::compat::tensor_extract_with_shape_f32(&primary)?;
260 let n_boxes = shape_vec[0] as usize;
261 let n_cols = if shape_vec.len() > 1 { shape_vec[1] as usize } else { 0 };
262 if n_cols < 6 {
263 return Err(OcrError::ModelOutput(format!(
264 "PP-DocLayoutV3 output cols={} (atteso ≥6)", n_cols,
265 )));
266 }
267 let has_reading_order = n_cols >= 7;
268
269 let mut boxes: Vec<LayoutBox> = Vec::with_capacity(n_boxes);
271 let (img_w, img_h) = (image.width() as i32, image.height() as i32);
272 for i in 0..n_boxes {
273 let off = i * n_cols;
274 let class_id = raw_data[off] as i32;
275 let score = raw_data[off + 1];
276 if score < self.conf_thresh { continue; }
277 if class_id < 0 { continue; }
278 let xmin = raw_data[off + 2];
279 let ymin = raw_data[off + 3];
280 let xmax = raw_data[off + 4];
281 let ymax = raw_data[off + 5];
282 let read_order = if has_reading_order { raw_data[off + 6] as i32 } else { -1 };
283
284 let xmin_c = (xmin.max(0.0) as i32).min(img_w);
286 let ymin_c = (ymin.max(0.0) as i32).min(img_h);
287 let xmax_c = (xmax.max(0.0) as i32).min(img_w);
288 let ymax_c = (ymax.max(0.0) as i32).min(img_h);
289 if xmax_c <= xmin_c || ymax_c <= ymin_c { continue; }
290
291 let class = match LayoutClass::from_id(class_id as usize) {
292 Some(c) => c,
293 None => LayoutClass::Text,
296 };
297 boxes.push(LayoutBox {
298 x: xmin_c as u32,
299 y: ymin_c as u32,
300 w: (xmax_c - xmin_c) as u32,
301 h: (ymax_c - ymin_c) as u32,
302 class,
303 score,
304 reading_order: read_order,
305 });
306 }
307
308 let kept = nms(&mut boxes, self.nms_iou_thresh);
310
311 let mut sorted = kept;
313 sorted.sort_by(|a, b| {
314 let ka = if a.reading_order < 0 { i32::MAX } else { a.reading_order };
315 let kb = if b.reading_order < 0 { i32::MAX } else { b.reading_order };
316 ka.cmp(&kb)
317 });
318
319 Ok(sorted)
320 }
321}
322
323fn preprocess(image: &image::RgbImage, target_size: u32) -> (Array4<f32>, f32) {
328 let (orig_w, orig_h) = (image.width(), image.height());
329 let scale = (target_size as f32 / orig_h as f32).min(target_size as f32 / orig_w as f32);
330 let new_w = (orig_w as f32 * scale).round() as u32;
331 let new_h = (orig_h as f32 * scale).round() as u32;
332
333 let resized = image::imageops::resize(
335 image, new_w, new_h, image::imageops::FilterType::Triangle,
336 );
337
338 let mean = [0.485f32, 0.456, 0.406];
341 let std = [0.229f32, 0.224, 0.225];
342
343 let target = target_size as usize;
344 let mut blob: Array4<f32> = Array::zeros((1, 3, target, target));
345 for y in 0..new_h as usize {
346 for x in 0..new_w as usize {
347 let pixel = resized.get_pixel(x as u32, y as u32);
348 for c in 0..3 {
349 let v = pixel[c] as f32 / 255.0;
350 blob[[0, c, y, x]] = (v - mean[c]) / std[c];
351 }
352 }
353 }
354 (blob, scale)
355}
356
357fn nms(boxes: &mut Vec<LayoutBox>, iou_thresh: f32) -> Vec<LayoutBox> {
361 boxes.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
362 let mut keep: Vec<bool> = vec![true; boxes.len()];
363 for i in 0..boxes.len() {
364 if !keep[i] { continue; }
365 for j in (i + 1)..boxes.len() {
366 if !keep[j] { continue; }
367 if iou(&boxes[i], &boxes[j]) > iou_thresh {
368 keep[j] = false;
369 }
370 }
371 }
372 boxes.iter().zip(keep.iter())
373 .filter_map(|(b, &k)| if k { Some(b.clone()) } else { None })
374 .collect()
375}
376
377fn iou(a: &LayoutBox, b: &LayoutBox) -> f32 {
378 let xa = a.xmin().max(b.xmin()) as i32;
379 let ya = a.ymin().max(b.ymin()) as i32;
380 let xb = (a.xmax().min(b.xmax())) as i32;
381 let yb = (a.ymax().min(b.ymax())) as i32;
382 let w = (xb - xa).max(0);
383 let h = (yb - ya).max(0);
384 let inter = (w * h) as f32;
385 let area_a = (a.w * a.h) as f32;
386 let area_b = (b.w * b.h) as f32;
387 let union = area_a + area_b - inter;
388 if union <= 0.0 { 0.0 } else { inter / union }
389}
390
391pub fn xy_cut_order(boxes: &[LayoutBox]) -> Vec<usize> {
407 let indices: Vec<usize> = (0..boxes.len()).collect();
408 let mut result = Vec::with_capacity(boxes.len());
409 xy_cut_rec(boxes, &indices, &mut result);
410 result
411}
412
413fn xy_cut_rec(boxes: &[LayoutBox], indices: &[usize], out: &mut Vec<usize>) {
414 match indices.len() {
415 0 => {}
416 1 => out.push(indices[0]),
417 _ => {
418 if let Some((top, bottom)) = find_cut(boxes, indices, Axis::Y) {
419 xy_cut_rec(boxes, &top, out);
420 xy_cut_rec(boxes, &bottom, out);
421 } else if let Some((left, right)) = find_cut(boxes, indices, Axis::X) {
422 xy_cut_rec(boxes, &left, out);
423 xy_cut_rec(boxes, &right, out);
424 } else {
425 let mut sorted = indices.to_vec();
427 sorted.sort_by(|&a, &b| {
428 let ay = boxes[a].y as f32 + boxes[a].h as f32 / 2.0;
429 let by = boxes[b].y as f32 + boxes[b].h as f32 / 2.0;
430 ay.partial_cmp(&by).unwrap_or(std::cmp::Ordering::Equal)
431 .then_with(|| {
432 let ax = boxes[a].x as f32 + boxes[a].w as f32 / 2.0;
433 let bx = boxes[b].x as f32 + boxes[b].w as f32 / 2.0;
434 ax.partial_cmp(&bx).unwrap_or(std::cmp::Ordering::Equal)
435 })
436 });
437 out.extend(sorted);
438 }
439 }
440 }
441}
442
443#[derive(Clone, Copy)]
444enum Axis { X, Y }
445
446fn find_cut(boxes: &[LayoutBox], indices: &[usize], axis: Axis) -> Option<(Vec<usize>, Vec<usize>)> {
449 let intervals: Vec<(u32, u32)> = indices.iter()
451 .map(|&i| match axis {
452 Axis::Y => (boxes[i].ymin(), boxes[i].ymax()),
453 Axis::X => (boxes[i].xmin(), boxes[i].xmax()),
454 })
455 .collect();
456
457 let mut events: Vec<(u32, bool)> = Vec::new(); for &(s, e) in &intervals {
461 events.push((s, true));
462 events.push((e, false));
463 }
464 events.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
467
468 let mut active = 0i32;
469 let mut cut_end: Option<u32> = None;
470 for &(v, is_start) in &events {
471 if is_start { active += 1; } else { active -= 1; }
472 if active == 0 {
473 cut_end = Some(v);
474 break;
475 }
476 }
477
478 let cut = cut_end?;
479 let next_start = events.iter()
481 .find(|&&(v, is_start)| v > cut && is_start)
482 .map(|&(v, _)| v)?;
483
484 let first: Vec<usize> = indices.iter().cloned()
485 .filter(|&i| match axis {
486 Axis::Y => boxes[i].ymax() <= cut,
487 Axis::X => boxes[i].xmax() <= cut,
488 })
489 .collect();
490 let second: Vec<usize> = indices.iter().cloned()
491 .filter(|&i| match axis {
492 Axis::Y => boxes[i].ymin() >= next_start,
493 Axis::X => boxes[i].xmin() >= next_start,
494 })
495 .collect();
496
497 if first.len() + second.len() == indices.len() {
499 Some((first, second))
500 } else {
501 None
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 fn lb(x: u32, y: u32, w: u32, h: u32, class: LayoutClass, score: f32, ro: i32) -> LayoutBox {
510 LayoutBox { x, y, w, h, class, score, reading_order: ro }
511 }
512
513 #[test]
514 fn iou_identical_is_one() {
515 let a = lb(0, 0, 100, 50, LayoutClass::Text, 0.9, 0);
516 let b = a.clone();
517 assert!((iou(&a, &b) - 1.0).abs() < 1e-6);
518 }
519
520 #[test]
521 fn iou_disjoint_is_zero() {
522 let a = lb(0, 0, 50, 50, LayoutClass::Text, 0.9, 0);
523 let b = lb(100, 0, 50, 50, LayoutClass::Text, 0.9, 0);
524 assert_eq!(iou(&a, &b), 0.0);
525 }
526
527 #[test]
528 fn nms_keeps_highest_score() {
529 let mut boxes = vec![
530 lb(0, 0, 100, 50, LayoutClass::Text, 0.85, 0),
531 lb(5, 5, 100, 50, LayoutClass::Text, 0.95, 0), lb(200, 200, 50, 50, LayoutClass::DocTitle, 0.70, 1),
533 ];
534 let kept = nms(&mut boxes, 0.5);
535 assert_eq!(kept.len(), 2);
536 assert!((kept[0].score - 0.95).abs() < 1e-6);
538 assert!((kept[1].score - 0.70).abs() < 1e-6);
539 }
540
541 #[test]
542 fn semantic_mapping() {
543 assert_eq!(LayoutClass::DocTitle.semantic(), SemanticClass::Title);
544 assert_eq!(LayoutClass::ParagraphTitle.semantic(), SemanticClass::Title);
545 assert_eq!(LayoutClass::Image.semantic(), SemanticClass::Figure);
546 assert_eq!(LayoutClass::Table.semantic(), SemanticClass::Table);
547 assert_eq!(LayoutClass::Footer.semantic(), SemanticClass::Footer);
548 assert_eq!(LayoutClass::DisplayFormula.semantic(), SemanticClass::Equation);
549 assert_eq!(LayoutClass::Text.semantic(), SemanticClass::Text);
550 assert_eq!(LayoutClass::VerticalText.semantic(), SemanticClass::Text);
551 }
552
553 #[test]
554 fn layout_box_contains_and_distance() {
555 let b = lb(100, 100, 200, 50, LayoutClass::Text, 0.9, 0);
556 assert!(b.contains(150, 120));
557 assert!(!b.contains(50, 120));
558 assert!(!b.contains(350, 120));
559 assert!(b.distance_to(200, 125) < 0.01);
561 }
562
563 #[test]
564 fn preprocess_letterbox_keeps_aspect() {
565 let img = image::RgbImage::new(600, 300);
568 let (blob, scale) = preprocess(&img, 800);
569 assert!((scale - 800.0/600.0).abs() < 1e-3);
570 assert_eq!(blob.shape(), &[1, 3, 800, 800]);
571 }
572}