use ndarray::{Array2, Array3, Array4};
use std::time::Instant;
#[cfg(feature = "use-opencv")]
use opencv::prelude::*;
#[cfg(not(feature = "use-opencv"))]
use crate::image_impl::Mat;
use crate::engine::{EngineError, MnnSession};
use crate::types::LayoutConfig;
#[derive(Debug, Clone)]
pub struct BBox {
pub x_min: i32,
pub y_min: i32,
pub x_max: i32,
pub y_max: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutType {
Text = 0,
Title = 1,
Figure = 2,
FigureCaption = 3,
Table = 4,
TableCaption = 5,
Header = 6,
Footer = 7,
Reference = 8,
Equation = 9,
}
impl LayoutType {
pub fn from_class(class_id: usize) -> Option<Self> {
match class_id {
0 => Some(LayoutType::Text),
1 => Some(LayoutType::Title),
2 => Some(LayoutType::Figure),
3 => Some(LayoutType::FigureCaption),
4 => Some(LayoutType::Table),
5 => Some(LayoutType::TableCaption),
6 => Some(LayoutType::Header),
7 => Some(LayoutType::Footer),
8 => Some(LayoutType::Reference),
9 => Some(LayoutType::Equation),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
LayoutType::Text => "text",
LayoutType::Title => "title",
LayoutType::Figure => "figure",
LayoutType::FigureCaption => "figure_caption",
LayoutType::Table => "table",
LayoutType::TableCaption => "table_caption",
LayoutType::Header => "header",
LayoutType::Footer => "footer",
LayoutType::Reference => "reference",
LayoutType::Equation => "equation",
}
}
}
#[derive(Debug, Clone)]
pub struct LayoutRegion {
pub bbox: BBox,
pub layout_type: LayoutType,
pub confidence: f32,
}
pub struct LayoutOutput {
pub regions: Vec<LayoutRegion>,
pub elapse: f64,
}
pub struct LayoutDetector {
session: MnnSession,
config: LayoutConfig,
}
impl LayoutDetector {
pub fn new(config: LayoutConfig) -> Result<Self, EngineError> {
let session = MnnSession::from_path(&config.model_path, &config.engine_cfg)?;
Ok(Self { session, config })
}
pub fn detect(&mut self, image: &Mat) -> Result<LayoutOutput, EngineError> {
let start = Instant::now();
#[cfg(feature = "use-opencv")]
let (orig_h, orig_w) = (image.rows(), image.cols());
#[cfg(not(feature = "use-opencv"))]
let size = image.size()?;
#[cfg(not(feature = "use-opencv"))]
let (orig_h, orig_w) = (size.height, size.width);
let (input_tensor, scale_w, scale_h) = self.preprocess(image)?;
let mut inputs = std::collections::HashMap::new();
inputs.insert("image".to_string(), input_tensor.into_dyn());
let target_size = self.config.target_size as f32;
let im_shape = Array2::from_shape_vec((1, 2), vec![target_size, target_size])
.map_err(|e| EngineError::ShapeError(e))?;
inputs.insert("im_shape".to_string(), im_shape.into_dyn());
let scale_factor = Array2::from_shape_vec((1, 2), vec![scale_h, scale_w])
.map_err(|e| EngineError::ShapeError(e))?;
inputs.insert("scale_factor".to_string(), scale_factor.into_dyn());
let outputs = self.session.run_with_inputs(inputs)?;
let regions = self.postprocess(outputs, orig_w, orig_h, scale_w, scale_h)?;
let elapse = start.elapsed().as_secs_f64();
Ok(LayoutOutput { regions, elapse })
}
fn preprocess(&self, image: &Mat) -> Result<(Array4<f32>, f32, f32), EngineError> {
#[cfg(feature = "use-opencv")]
let (h, w) = (image.rows(), image.cols());
#[cfg(not(feature = "use-opencv"))]
let size = image.size()?;
#[cfg(not(feature = "use-opencv"))]
let (h, w) = (size.height, size.width);
let target_size = self.config.target_size;
let new_h = target_size;
let new_w = target_size;
let ratio = target_size as f32 / h.max(w) as f32;
#[cfg(feature = "use-opencv")]
{
use opencv::{core, imgproc};
let mut resized = Mat::default();
imgproc::resize(
image,
&mut resized,
core::Size::new(new_w, new_h),
0.0,
0.0,
imgproc::INTER_LINEAR,
)?;
let mean = self.config.mean;
let std = self.config.std;
let mut float_img = Mat::default();
resized.convert_to(&mut float_img, opencv::core::CV_32F, 1.0 / 255.0, 0.0)?;
let channels = float_img.channels() as usize;
let rows = float_img.rows() as usize;
let cols = float_img.cols() as usize;
let mut data = vec![0.0f32; channels * rows * cols];
let mat_data = float_img.data_bytes()?;
for i in 0..rows {
for j in 0..cols {
for c in 0..channels {
let idx = (i * cols + j) * channels + c;
let val = mat_data[idx * 4] as f32 / 255.0;
data[c * rows * cols + i * cols + j] = (val - mean[c]) / std[c];
}
}
}
let arr = Array3::from_shape_vec((3, new_h as usize, new_w as usize), data)
.map_err(|e| EngineError::Preprocess(e.to_string()))?;
Ok((arr.insert_axis(ndarray::Axis(0)), ratio, ratio))
}
#[cfg(not(feature = "use-opencv"))]
{
use crate::image_impl::{resize, Size, INTER_LINEAR};
let mut resized = Mat::default();
resize(image, &mut resized, Size::new(new_w, new_h), INTER_LINEAR)?;
let mean = self.config.mean;
let std = self.config.std;
let mut data = vec![0.0f32; 3 * new_h as usize * new_w as usize];
for y in 0..new_h as usize {
for x in 0..new_w as usize {
let pixel = resized.get_pixel(x as u32, y as u32);
for c in 0..3 {
let val = pixel[c] as f32 / 255.0;
data[c * (new_h as usize * new_w as usize) + y * (new_w as usize) + x] =
(val - mean[c]) / std[c];
}
}
}
let arr = Array3::from_shape_vec((3, new_h as usize, new_w as usize), data)
.map_err(|e| EngineError::Preprocess(e.to_string()))?;
Ok((arr.insert_axis(ndarray::Axis(0)), ratio, ratio))
}
}
fn postprocess(
&self,
outputs: std::collections::HashMap<String, ndarray::ArrayD<f32>>,
_orig_w: i32,
_orig_h: i32,
scale_w: f32,
scale_h: f32,
) -> Result<Vec<LayoutRegion>, EngineError> {
let mut boxes_tensor = None;
for (_name, tensor) in &outputs {
let shape = tensor.shape();
if shape.len() == 2 && shape[1] == 6 {
boxes_tensor = Some(tensor);
break;
}
}
let boxes_tensor = match boxes_tensor {
Some(t) => t,
None => {
return Err(EngineError::OutputError(
"No suitable output tensor found".to_string(),
))
}
};
let mut raw_boxes = Vec::new();
for row in boxes_tensor.outer_iter() {
let class_id = row[0] as usize;
let score = row[1];
let x1 = row[2];
let y1 = row[3];
let x2 = row[4];
let y2 = row[5];
raw_boxes.push([class_id as f32, score, x1, y1, x2, y2]);
}
let indices = crate::geometry::nms(&raw_boxes, 0.6, 0.95);
let mut regions = Vec::new();
for idx in indices {
let row = &raw_boxes[idx];
let class_id = row[0] as usize;
let score = row[1];
let x1 = row[2];
let y1 = row[3];
let x2 = row[4];
let y2 = row[5];
let layout_type = match class_id {
0 => LayoutType::Title,
1 | 15 => LayoutType::Text,
2 => continue, 3 => LayoutType::Figure,
4 => LayoutType::FigureCaption,
5 => LayoutType::Table,
6 => LayoutType::TableCaption,
7 => LayoutType::Reference, 8 | 13 | 14 => LayoutType::Equation,
9 => LayoutType::Text, _ => LayoutType::Text, };
let x_min = ((x1 / scale_w) as i32).max(0).min(_orig_w);
let y_min = ((y1 / scale_h) as i32).max(0).min(_orig_h);
let x_max = ((x2 / scale_w) as i32).max(0).min(_orig_w);
let y_max = ((y2 / scale_h) as i32).max(0).min(_orig_h);
if x_max <= x_min || y_max <= y_min {
continue;
}
regions.push(LayoutRegion {
bbox: BBox {
x_min,
y_min,
x_max,
y_max,
},
layout_type,
confidence: score,
});
}
Ok(regions)
}
}