use crate::core::OCRError;
use crate::core::inference::{OrtInfer, TensorInput, TensorOutput};
use crate::processors::{FormulaPreprocessParams, FormulaPreprocessor};
use image::RgbImage;
use ndarray::{ArrayBase, Axis, Data, Ix2};
#[derive(Debug, Clone)]
pub struct PPFormulaNetPreprocessConfig {
pub target_size: (u32, u32),
pub crop_threshold: u8,
pub padding_multiple: usize,
pub normalize_mean: [f32; 3],
pub normalize_std: [f32; 3],
}
impl Default for PPFormulaNetPreprocessConfig {
fn default() -> Self {
Self {
target_size: (384, 384),
crop_threshold: 200,
padding_multiple: 16,
normalize_mean: [0.7931, 0.7931, 0.7931],
normalize_std: [0.1738, 0.1738, 0.1738],
}
}
}
#[derive(Debug, Clone)]
pub struct PPFormulaNetPostprocessConfig {
pub sos_token_id: i64,
pub eos_token_id: i64,
pub vocab_size: i64,
}
impl Default for PPFormulaNetPostprocessConfig {
fn default() -> Self {
Self {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: i64::MAX,
}
}
}
#[derive(Debug, Clone)]
pub struct PPFormulaNetModelOutput {
pub token_ids: ndarray::Array2<i64>,
}
#[derive(Debug)]
pub struct PPFormulaNetModel {
inference: OrtInfer,
preprocessor: FormulaPreprocessor,
_preprocess_config: PPFormulaNetPreprocessConfig,
}
impl PPFormulaNetModel {
pub fn new(
inference: OrtInfer,
preprocess_config: PPFormulaNetPreprocessConfig,
) -> Result<Self, OCRError> {
let params = FormulaPreprocessParams {
target_size: preprocess_config.target_size,
crop_threshold: preprocess_config.crop_threshold,
padding_multiple: preprocess_config.padding_multiple,
normalize_mean: preprocess_config.normalize_mean,
normalize_std: preprocess_config.normalize_std,
};
let preprocessor = FormulaPreprocessor::new(params);
Ok(Self {
inference,
preprocessor,
_preprocess_config: preprocess_config,
})
}
pub fn preprocess(&self, images: Vec<RgbImage>) -> Result<ndarray::Array4<f32>, OCRError> {
self.preprocessor.preprocess_batch(&images)
}
pub fn infer(
&self,
batch_tensor: &ndarray::Array4<f32>,
) -> Result<ndarray::Array2<i64>, OCRError> {
let input_name = self.inference.input_name();
let inputs = vec![(input_name, TensorInput::Array4(batch_tensor))];
let outputs = self
.inference
.infer(&inputs)
.map_err(|e| OCRError::Inference {
model_name: "PP-FormulaNet".to_string(),
context: format!(
"failed to run inference on batch with shape {:?}",
batch_tensor.shape()
),
source: Box::new(e),
})?;
tracing::debug!(
"PP-FormulaNet declared output shapes: {:?}",
self.inference.output_shapes()
);
let i64_2d_count = outputs
.iter()
.filter(|(_, t)| matches!(t, TensorOutput::I64 { shape, .. } if shape.len() == 2))
.count();
if i64_2d_count != 1 {
let candidates: Vec<(String, &'static str, Vec<i64>)> = outputs
.iter()
.map(|(name, t)| (name.clone(), t.dtype_name(), t.shape().to_vec()))
.collect();
return Err(OCRError::Inference {
model_name: "PP-FormulaNet".to_string(),
context: format!(
"expected exactly one 2-D i64 output (token ids); found {} candidate(s) among outputs {:?}",
i64_2d_count, candidates
),
source: Box::new(OCRError::InvalidInput {
message: "PP-FormulaNet: ambiguous or missing token-id output".to_string(),
}),
});
}
let (name, tensor) = outputs
.into_iter()
.find(|(_, t)| matches!(t, TensorOutput::I64 { shape, .. } if shape.len() == 2))
.expect("i64_2d_count == 1 checked above");
tracing::debug!(
"PP-FormulaNet selected output '{}' dtype={} runtime_shape={:?}",
name,
tensor.dtype_name(),
tensor.shape()
);
tensor
.try_into_array2_i64()
.map_err(|e| OCRError::Inference {
model_name: "PP-FormulaNet".to_string(),
context: format!("failed to convert output '{name}' to 2-D i64 array"),
source: Box::new(e),
})
}
pub fn postprocess(
&self,
token_ids: ndarray::Array2<i64>,
_config: &PPFormulaNetPostprocessConfig,
) -> Result<PPFormulaNetModelOutput, OCRError> {
Ok(PPFormulaNetModelOutput { token_ids })
}
pub fn forward(
&self,
images: Vec<RgbImage>,
config: &PPFormulaNetPostprocessConfig,
) -> Result<PPFormulaNetModelOutput, OCRError> {
let batch_tensor = self.preprocess(images)?;
let token_ids = self.infer(&batch_tensor)?;
let output = self.postprocess(token_ids, config)?;
Ok(output)
}
pub fn filter_tokens<D>(
token_ids: &ArrayBase<D, Ix2>,
config: &PPFormulaNetPostprocessConfig,
) -> Vec<Vec<u32>>
where
D: Data<Elem = i64>,
{
let batch_size = token_ids.shape()[0];
let mut filtered_tokens = Vec::with_capacity(batch_size);
for batch_idx in 0..batch_size {
let row = token_ids.index_axis(Axis(0), batch_idx);
let tokens: Vec<u32> = row
.iter()
.copied()
.take_while(|&id| id != config.eos_token_id)
.take_while(|&id| id < 0 || id < config.vocab_size)
.filter(|&id| id >= 0 && id != config.sos_token_id)
.map(|id| id as u32)
.collect();
filtered_tokens.push(tokens);
}
filtered_tokens
}
}
#[derive(Debug, Default)]
pub struct PPFormulaNetModelBuilder {
preprocess_config: Option<PPFormulaNetPreprocessConfig>,
ort_config: Option<crate::core::config::OrtSessionConfig>,
}
impl PPFormulaNetModelBuilder {
pub fn new() -> Self {
Self {
preprocess_config: None,
ort_config: None,
}
}
pub fn preprocess_config(mut self, config: PPFormulaNetPreprocessConfig) -> Self {
self.preprocess_config = Some(config);
self
}
pub fn target_size(mut self, width: u32, height: u32) -> Self {
let mut config = self.preprocess_config.unwrap_or_default();
config.target_size = (width, height);
self.preprocess_config = Some(config);
self
}
pub fn padding_multiple(mut self, multiple: usize) -> Self {
let mut config = self.preprocess_config.unwrap_or_default();
config.padding_multiple = multiple;
self.preprocess_config = Some(config);
self
}
pub fn with_ort_config(mut self, config: crate::core::config::OrtSessionConfig) -> Self {
self.ort_config = Some(config);
self
}
fn configure_ppformulanet_ort_for_cuda(
mut config: crate::core::config::OrtSessionConfig,
) -> crate::core::config::OrtSessionConfig {
use crate::core::config::OrtExecutionProvider;
if let Some(eps) = config.execution_providers.as_mut() {
for ep in eps.iter_mut() {
if let OrtExecutionProvider::CUDA {
arena_extend_strategy,
..
} = ep
&& arena_extend_strategy.is_none()
{
*arena_extend_strategy = Some("SameAsRequested".to_string());
}
}
}
config
}
pub fn build(self, model_path: &std::path::Path) -> Result<PPFormulaNetModel, OCRError> {
let ort_config = self
.ort_config
.map(Self::configure_ppformulanet_ort_for_cuda);
let inference = if ort_config.is_some() {
use crate::core::config::ModelInferenceConfig;
let common_config = ModelInferenceConfig {
ort_session: ort_config,
model_name: Some("PP-FormulaNet".to_string()),
..Default::default()
};
OrtInfer::from_config(&common_config, model_path, None)?
} else {
OrtInfer::new(model_path, None)?
};
let mut preprocess_config = self.preprocess_config.unwrap_or_default();
if preprocess_config.target_size == (384, 384)
&& let Some(shape) = inference.primary_input_shape()
&& shape.len() >= 4
{
let height = shape[shape.len() - 2];
let width = shape[shape.len() - 1];
if height > 0 && width > 0 {
preprocess_config.target_size = (width as u32, height as u32);
}
}
PPFormulaNetModel::new(inference, preprocess_config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::arr2;
#[test]
fn filter_tokens_stops_at_vocab_sentinel() {
let token_ids = arr2(&[[0, 42, 49_999, 4_096_990_134i64, 77, 2]]);
let config = PPFormulaNetPostprocessConfig {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: 50_000,
};
let filtered = PPFormulaNetModel::filter_tokens(&token_ids, &config);
assert_eq!(filtered, vec![vec![42, 49_999]]);
}
#[test]
fn filter_tokens_still_stops_at_eos() {
let token_ids = arr2(&[[0, 42, 2, 43]]);
let config = PPFormulaNetPostprocessConfig {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: 50_000,
};
let filtered = PPFormulaNetModel::filter_tokens(&token_ids, &config);
assert_eq!(filtered, vec![vec![42]]);
}
}