oar_ocr_core/models/classification/
pp_lcnet.rs1use crate::core::OCRError;
8use crate::core::inference::{OrtInfer, TensorInput};
9use crate::domain::adapters::preprocessing::rgb_to_dynamic;
10use crate::processors::{NormalizeImage, TensorLayout};
11use crate::utils::topk::Topk;
12use image::{RgbImage, imageops::FilterType};
13
14#[derive(Debug, Clone)]
16pub struct PPLCNetPreprocessConfig {
17 pub input_shape: (u32, u32),
19 pub resize_filter: FilterType,
21 pub resize_short: Option<u32>,
30 pub normalize_scale: f32,
32 pub normalize_mean: Vec<f32>,
34 pub normalize_std: Vec<f32>,
36 pub tensor_layout: TensorLayout,
38}
39
40impl Default for PPLCNetPreprocessConfig {
41 fn default() -> Self {
42 Self {
43 input_shape: (224, 224),
44 resize_filter: FilterType::Triangle,
46 resize_short: Some(256),
48 normalize_scale: 1.0 / 255.0,
49 normalize_mean: vec![0.485, 0.456, 0.406],
50 normalize_std: vec![0.229, 0.224, 0.225],
51 tensor_layout: TensorLayout::CHW,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct PPLCNetPostprocessConfig {
59 pub labels: Vec<String>,
61 pub topk: usize,
63}
64
65impl Default for PPLCNetPostprocessConfig {
66 fn default() -> Self {
67 Self {
68 labels: vec![],
69 topk: 1,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct PPLCNetModelOutput {
77 pub class_ids: Vec<Vec<usize>>,
79 pub scores: Vec<Vec<f32>>,
81 pub label_names: Option<Vec<Vec<String>>>,
83}
84
85#[derive(Debug)]
89pub struct PPLCNetModel {
90 inference: OrtInfer,
92 normalizer: NormalizeImage,
94 topk_processor: Topk,
96 input_shape: (u32, u32),
98 resize_filter: FilterType,
100 resize_short: Option<u32>,
102}
103
104impl PPLCNetModel {
105 pub fn new(
107 inference: OrtInfer,
108 normalizer: NormalizeImage,
109 topk_processor: Topk,
110 input_shape: (u32, u32),
111 resize_filter: FilterType,
112 resize_short: Option<u32>,
113 ) -> Self {
114 Self {
115 inference,
116 normalizer,
117 topk_processor,
118 input_shape,
119 resize_filter,
120 resize_short,
121 }
122 }
123
124 pub fn preprocess(&self, images: Vec<RgbImage>) -> Result<ndarray::Array4<f32>, OCRError> {
134 let image_refs: Vec<&RgbImage> = images.iter().collect();
135 self.preprocess_refs(&image_refs)
136 }
137
138 pub fn preprocess_refs(&self, images: &[&RgbImage]) -> Result<ndarray::Array4<f32>, OCRError> {
140 let (crop_h, crop_w) = self.input_shape;
141
142 let resized_rgb: Vec<RgbImage> = if let Some(resize_short) = self.resize_short {
143 images
150 .iter()
151 .filter_map(|&img| {
152 let (w, h) = (img.width(), img.height());
153 if w == 0 || h == 0 {
154 return None;
155 }
156
157 let short = w.min(h) as f32;
158 let scale = (resize_short as f32) / short;
159 let new_w = ((w as f32) * scale).round().max(crop_w as f32) as u32;
160 let new_h = ((h as f32) * scale).round().max(crop_h as f32) as u32;
161
162 let resized = image::imageops::resize(img, new_w, new_h, self.resize_filter);
163
164 let x1 = (new_w.saturating_sub(crop_w)) / 2;
166 let y1 = (new_h.saturating_sub(crop_h)) / 2;
167 let cropped =
168 image::imageops::crop_imm(&resized, x1, y1, crop_w, crop_h).to_image();
169 Some(cropped)
170 })
171 .collect()
172 } else {
173 images
177 .iter()
178 .filter_map(|&img| {
179 let (w, h) = (img.width(), img.height());
180 if w == 0 || h == 0 {
181 return None;
182 }
183 Some(image::imageops::resize(
184 img,
185 crop_w,
186 crop_h,
187 self.resize_filter,
188 ))
189 })
190 .collect()
191 };
192
193 let dynamic_images = rgb_to_dynamic(resized_rgb);
195 self.normalizer.normalize_batch_to(dynamic_images)
196 }
197
198 pub fn infer(
208 &self,
209 batch_tensor: &ndarray::Array4<f32>,
210 ) -> Result<ndarray::Array2<f32>, OCRError> {
211 let input_name = self.inference.input_name();
212 let inputs = vec![(input_name, TensorInput::Array4(batch_tensor))];
213
214 let outputs = self
215 .inference
216 .infer(&inputs)
217 .map_err(|e| OCRError::Inference {
218 model_name: "PP-LCNet".to_string(),
219 context: format!(
220 "failed to run inference on batch with shape {:?}",
221 batch_tensor.shape()
222 ),
223 source: Box::new(e),
224 })?;
225
226 let output = outputs
227 .into_iter()
228 .next()
229 .ok_or_else(|| OCRError::InvalidInput {
230 message: "PP-LCNet: no output returned from inference".to_string(),
231 })?;
232
233 output
234 .1
235 .try_into_array2_f32()
236 .map_err(|e| OCRError::Inference {
237 model_name: "PP-LCNet".to_string(),
238 context: "failed to convert output to 2D array".to_string(),
239 source: Box::new(e),
240 })
241 }
242
243 pub fn postprocess(
254 &self,
255 predictions: &ndarray::Array2<f32>,
256 config: &PPLCNetPostprocessConfig,
257 ) -> Result<PPLCNetModelOutput, OCRError> {
258 let predictions_vec: Vec<Vec<f32>> =
259 predictions.outer_iter().map(|row| row.to_vec()).collect();
260
261 let topk_result = self
262 .topk_processor
263 .process(&predictions_vec, config.topk)
264 .unwrap_or_else(|_| crate::utils::topk::TopkResult {
265 indexes: vec![],
266 scores: vec![],
267 class_names: None,
268 });
269
270 let class_ids = topk_result.indexes;
271 let scores = topk_result.scores;
272
273 let label_names = if !config.labels.is_empty() {
275 Some(
276 class_ids
277 .iter()
278 .map(|ids| {
279 ids.iter()
280 .map(|&id| {
281 config
282 .labels
283 .get(id)
284 .cloned()
285 .unwrap_or_else(|| format!("class_{}", id))
286 })
287 .collect()
288 })
289 .collect(),
290 )
291 } else {
292 topk_result.class_names
293 };
294
295 Ok(PPLCNetModelOutput {
296 class_ids,
297 scores,
298 label_names,
299 })
300 }
301
302 pub fn forward(
313 &self,
314 images: Vec<RgbImage>,
315 config: &PPLCNetPostprocessConfig,
316 ) -> Result<PPLCNetModelOutput, OCRError> {
317 let image_refs: Vec<&RgbImage> = images.iter().collect();
318 self.forward_refs(&image_refs, config)
319 }
320
321 pub fn forward_refs(
323 &self,
324 images: &[&RgbImage],
325 config: &PPLCNetPostprocessConfig,
326 ) -> Result<PPLCNetModelOutput, OCRError> {
327 let batch_tensor = self.preprocess_refs(images)?;
328 let predictions = self.infer(&batch_tensor)?;
329 self.postprocess(&predictions, config)
330 }
331}
332
333#[derive(Debug, Default)]
335pub struct PPLCNetModelBuilder {
336 preprocess_config: PPLCNetPreprocessConfig,
338 ort_config: Option<crate::core::config::OrtSessionConfig>,
340}
341
342impl PPLCNetModelBuilder {
343 pub fn new() -> Self {
345 Self {
346 preprocess_config: PPLCNetPreprocessConfig::default(),
347 ort_config: None,
348 }
349 }
350
351 pub fn preprocess_config(mut self, config: PPLCNetPreprocessConfig) -> Self {
353 self.preprocess_config = config;
354 self
355 }
356
357 pub fn input_shape(mut self, shape: (u32, u32)) -> Self {
359 self.preprocess_config.input_shape = shape;
360 self
361 }
362
363 pub fn resize_filter(mut self, filter: FilterType) -> Self {
365 self.preprocess_config.resize_filter = filter;
366 self
367 }
368
369 pub fn with_ort_config(mut self, config: crate::core::config::OrtSessionConfig) -> Self {
371 self.ort_config = Some(config);
372 self
373 }
374
375 pub fn build(
385 self,
386 model_source: impl Into<crate::core::ModelSource>,
387 ) -> Result<PPLCNetModel, OCRError> {
388 let inference = if self.ort_config.is_some() {
390 use crate::core::config::ModelInferenceConfig;
391 let common_config = ModelInferenceConfig {
392 ort_session: self.ort_config,
393 ..Default::default()
394 };
395 OrtInfer::from_config(&common_config, model_source, None)?
396 } else {
397 OrtInfer::new(model_source, None)?
398 };
399
400 let mean = self.preprocess_config.normalize_mean.clone();
405 let std = self.preprocess_config.normalize_std.clone();
406 let normalizer = NormalizeImage::with_color_order(
407 Some(self.preprocess_config.normalize_scale),
408 Some(mean),
409 Some(std),
410 Some(self.preprocess_config.tensor_layout),
411 Some(crate::processors::types::ColorOrder::RGB),
412 )?;
413
414 let topk_processor = Topk::new(None);
416
417 Ok(PPLCNetModel::new(
418 inference,
419 normalizer,
420 topk_processor,
421 self.preprocess_config.input_shape,
422 self.preprocess_config.resize_filter,
423 self.preprocess_config.resize_short,
424 ))
425 }
426}