1use crate::core::inference::{OrtInfer, TensorInput};
7use crate::core::{OCRError, validate_positive, validate_range};
8use crate::processors::{
9 BoundingBox, BoxType, DBPostProcess, DBPostProcessConfig, DetResizeForTest, ImageScaleInfo,
10 LimitType, NormalizeImage, ScoreMode, TensorLayout,
11};
12use image::{DynamicImage, GenericImageView, RgbImage};
13use std::path::Path;
14use tracing::debug;
15
16#[derive(Debug, Clone, Default)]
18pub struct DBPreprocessConfig {
19 pub limit_side_len: Option<u32>,
21 pub limit_type: Option<LimitType>,
23 pub max_side_limit: Option<u32>,
25 pub resize_long: Option<u32>,
27}
28
29#[derive(Debug, Clone)]
31pub struct DBPostprocessConfig {
32 pub score_threshold: f32,
34 pub box_threshold: f32,
36 pub unclip_ratio: f32,
38 pub max_candidates: usize,
40 pub use_dilation: bool,
42 pub score_mode: ScoreMode,
44 pub box_type: BoxType,
46}
47
48impl Default for DBPostprocessConfig {
49 fn default() -> Self {
50 Self {
51 score_threshold: 0.3,
52 box_threshold: 0.7,
53 unclip_ratio: 1.5,
54 max_candidates: 1000,
55 use_dilation: false,
56 score_mode: ScoreMode::Fast,
57 box_type: BoxType::Quad,
58 }
59 }
60}
61
62impl DBPostprocessConfig {
63 pub fn validate(&self) -> Result<(), OCRError> {
65 validate_range(self.score_threshold, 0.0, 1.0, "score_threshold")?;
67
68 validate_range(self.box_threshold, 0.0, 1.0, "box_threshold")?;
70
71 validate_positive(self.unclip_ratio, "unclip_ratio")?;
73
74 validate_positive(self.max_candidates, "max_candidates")?;
76
77 Ok(())
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct DBModelOutput {
84 pub boxes: Vec<Vec<BoundingBox>>,
86 pub scores: Vec<Vec<f32>>,
88}
89
90#[derive(Debug)]
95pub struct DBModel {
96 inference: OrtInfer,
98 resizer: DetResizeForTest,
100 normalizer: NormalizeImage,
102 postprocessor: DBPostProcess,
104}
105
106impl DBModel {
107 pub fn new(
109 inference: OrtInfer,
110 resizer: DetResizeForTest,
111 normalizer: NormalizeImage,
112 postprocessor: DBPostProcess,
113 ) -> Self {
114 Self {
115 inference,
116 resizer,
117 normalizer,
118 postprocessor,
119 }
120 }
121
122 pub fn preprocess(
124 &self,
125 images: Vec<RgbImage>,
126 ) -> Result<(ndarray::Array4<f32>, Vec<ImageScaleInfo>), OCRError> {
127 let dynamic_images: Vec<DynamicImage> =
129 images.into_iter().map(DynamicImage::ImageRgb8).collect();
130
131 let (resized_images, img_shapes) = self.resizer.apply(
133 dynamic_images,
134 None, None, None, );
138
139 debug!("After resize: {} images", resized_images.len());
140 for (i, (img, shape)) in resized_images.iter().zip(&img_shapes).enumerate() {
141 debug!(
142 " Image {}: {}x{}, shape=[src_h={:.0}, src_w={:.0}, ratio_h={:.3}, ratio_w={:.3}]",
143 i,
144 img.width(),
145 img.height(),
146 shape.src_h,
147 shape.src_w,
148 shape.ratio_h,
149 shape.ratio_w
150 );
151 }
152
153 let batch_tensor = self.normalizer.normalize_batch_to(resized_images)?;
160 debug!("Batch tensor shape: {:?}", batch_tensor.shape());
161
162 Ok((batch_tensor, img_shapes))
163 }
164
165 fn resize_images(&self, images: Vec<RgbImage>) -> Vec<(usize, DynamicImage, ImageScaleInfo)> {
166 let dynamic_images: Vec<DynamicImage> =
167 images.into_iter().map(DynamicImage::ImageRgb8).collect();
168 let (resized_images, img_shapes) = self.resizer.apply(
169 dynamic_images,
170 None, None, None, );
174
175 resized_images
176 .into_iter()
177 .zip(img_shapes)
178 .enumerate()
179 .map(|(idx, (image, shape))| (idx, image, shape))
180 .collect()
181 }
182
183 pub fn infer(
185 &self,
186 batch_tensor: &ndarray::Array4<f32>,
187 ) -> Result<ndarray::Array4<f32>, OCRError> {
188 let input_name = self.inference.input_name();
189 let inputs = vec![(input_name, TensorInput::Array4(batch_tensor))];
190
191 let outputs = self
192 .inference
193 .infer(&inputs)
194 .map_err(|e| OCRError::Inference {
195 model_name: "DB".to_string(),
196 context: format!(
197 "failed to run inference on batch with shape {:?}",
198 batch_tensor.shape()
199 ),
200 source: Box::new(e),
201 })?;
202
203 let output = outputs
204 .into_iter()
205 .next()
206 .ok_or_else(|| OCRError::InvalidInput {
207 message: "DB: no output returned from inference".to_string(),
208 })?;
209
210 output
211 .1
212 .try_into_array4_f32()
213 .map_err(|e| OCRError::Inference {
214 model_name: "DB".to_string(),
215 context: "failed to convert output to 4D array".to_string(),
216 source: Box::new(e),
217 })
218 }
219
220 pub fn postprocess(
222 &self,
223 predictions: &ndarray::Array4<f32>,
224 img_shapes: Vec<ImageScaleInfo>,
225 score_threshold: f32,
226 box_threshold: f32,
227 unclip_ratio: f32,
228 ) -> DBModelOutput {
229 let config = DBPostProcessConfig::new(score_threshold, box_threshold, unclip_ratio);
230 let (boxes, scores) = self
231 .postprocessor
232 .apply(predictions, img_shapes, Some(&config));
233 DBModelOutput { boxes, scores }
234 }
235
236 fn forward_resized_batch(
237 &self,
238 batch: Vec<(usize, DynamicImage, ImageScaleInfo)>,
239 boxes_by_image: &mut [Vec<BoundingBox>],
240 scores_by_image: &mut [Vec<f32>],
241 score_threshold: f32,
242 box_threshold: f32,
243 unclip_ratio: f32,
244 ) -> Result<(), OCRError> {
245 let mut original_indices = Vec::with_capacity(batch.len());
246 let mut resized_images = Vec::with_capacity(batch.len());
247 let mut img_shapes = Vec::with_capacity(batch.len());
248
249 for (original_idx, resized_image, img_shape) in batch {
250 original_indices.push(original_idx);
251 resized_images.push(resized_image);
252 img_shapes.push(img_shape);
253 }
254
255 let batch_tensor = self.normalizer.normalize_batch_to(resized_images)?;
256 let predictions = self.infer(&batch_tensor)?;
257 let group_output = self.postprocess(
258 &predictions,
259 img_shapes,
260 score_threshold,
261 box_threshold,
262 unclip_ratio,
263 );
264
265 for (group_idx, original_idx) in original_indices.into_iter().enumerate() {
266 boxes_by_image[original_idx] = group_output
267 .boxes
268 .get(group_idx)
269 .cloned()
270 .unwrap_or_default();
271 scores_by_image[original_idx] = group_output
272 .scores
273 .get(group_idx)
274 .cloned()
275 .unwrap_or_default();
276 }
277
278 Ok(())
279 }
280
281 pub fn forward(
283 &self,
284 images: Vec<RgbImage>,
285 score_threshold: f32,
286 box_threshold: f32,
287 unclip_ratio: f32,
288 ) -> Result<DBModelOutput, OCRError> {
289 if images.is_empty() {
290 return Ok(DBModelOutput {
291 boxes: Vec::new(),
292 scores: Vec::new(),
293 });
294 }
295
296 let image_count = images.len();
297 let resized = self.resize_images(images);
298 let mut groups: Vec<Vec<(usize, DynamicImage, ImageScaleInfo)>> = Vec::new();
299
300 for item in resized {
301 let dims = item.1.dimensions();
302 if let Some(group) = groups
303 .iter_mut()
304 .find(|group| group.first().map(|(_, img, _)| img.dimensions()) == Some(dims))
305 {
306 group.push(item);
307 } else {
308 groups.push(vec![item]);
309 }
310 }
311
312 debug!(
313 "DB forward: {} images grouped into {} shape batch(es)",
314 image_count,
315 groups.len()
316 );
317
318 let mut boxes_by_image = vec![Vec::new(); image_count];
319 let mut scores_by_image = vec![Vec::new(); image_count];
320
321 for group in groups {
322 self.forward_resized_batch(
323 group,
324 &mut boxes_by_image,
325 &mut scores_by_image,
326 score_threshold,
327 box_threshold,
328 unclip_ratio,
329 )?;
330 }
331
332 Ok(DBModelOutput {
333 boxes: boxes_by_image,
334 scores: scores_by_image,
335 })
336 }
337}
338
339pub struct DBModelBuilder {
341 preprocess_config: DBPreprocessConfig,
343 postprocess_config: DBPostprocessConfig,
345 ort_config: Option<crate::core::config::OrtSessionConfig>,
347}
348
349impl DBModelBuilder {
350 pub fn new() -> Self {
352 Self {
353 preprocess_config: DBPreprocessConfig::default(),
354 postprocess_config: DBPostprocessConfig::default(),
355 ort_config: None,
356 }
357 }
358
359 pub fn preprocess_config(mut self, config: DBPreprocessConfig) -> Self {
361 self.preprocess_config = config;
362 self
363 }
364
365 pub fn postprocess_config(mut self, config: DBPostprocessConfig) -> Self {
367 self.postprocess_config = config;
368 self
369 }
370
371 pub fn with_ort_config(mut self, config: crate::core::config::OrtSessionConfig) -> Self {
373 self.ort_config = Some(config);
374 self
375 }
376
377 pub fn build(self, model_path: &Path) -> Result<DBModel, OCRError> {
379 let inference = if self.ort_config.is_some() {
381 use crate::core::config::ModelInferenceConfig;
382 let common_config = ModelInferenceConfig {
383 ort_session: self.ort_config,
384 ..Default::default()
385 };
386 OrtInfer::from_config(&common_config, model_path, Some("x"))?
387 } else {
388 OrtInfer::new(model_path, Some("x"))?
389 };
390
391 let resizer = DetResizeForTest::new(
393 None, None, None, self.preprocess_config.limit_side_len, self.preprocess_config.limit_type, self.preprocess_config.resize_long, self.preprocess_config.max_side_limit, );
401
402 let normalizer = NormalizeImage::with_color_order(
408 Some(1.0 / 255.0), Some(vec![0.485, 0.456, 0.406]), Some(vec![0.229, 0.224, 0.225]), Some(TensorLayout::CHW), Some(crate::processors::types::ColorOrder::BGR),
413 )?;
414
415 let postprocessor = DBPostProcess::new(
417 Some(self.postprocess_config.score_threshold),
418 Some(self.postprocess_config.box_threshold),
419 Some(self.postprocess_config.max_candidates),
420 Some(self.postprocess_config.unclip_ratio),
421 Some(self.postprocess_config.use_dilation),
422 Some(self.postprocess_config.score_mode),
423 Some(self.postprocess_config.box_type),
424 );
425
426 Ok(DBModel::new(inference, resizer, normalizer, postprocessor))
427 }
428}
429
430impl Default for DBModelBuilder {
431 fn default() -> Self {
432 Self::new()
433 }
434}