1use image::{DynamicImage, GenericImageView, Rgb, RgbImage};
6use imageproc::geometric_transformations::{warp_into, Interpolation, Projection};
7use imageproc::point::Point;
8use ndarray::ArrayD;
9use std::path::Path;
10
11use crate::error::{OcrError, OcrResult};
12use crate::mnn::{InferenceConfig, InferenceEngine};
13use crate::postprocess::{extract_boxes_with_unclip, TextBox};
14use crate::preprocess::{preprocess_for_det, NormalizeParams};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum DetPrecisionMode {
19 #[default]
21 Fast,
22}
23
24#[derive(Debug, Clone)]
26pub struct DetOptions {
27 pub max_side_len: u32,
29 pub box_threshold: f32,
31 pub unclip_ratio: f32,
33 pub score_threshold: f32,
35 pub min_area: u32,
37 pub box_border: u32,
39 pub merge_boxes: bool,
41 pub merge_threshold: i32,
43 pub precision_mode: DetPrecisionMode,
45 pub multi_scales: Vec<f32>,
47 pub block_size: u32,
49 pub block_overlap: u32,
51 pub nms_threshold: f32,
53}
54
55impl Default for DetOptions {
56 fn default() -> Self {
57 Self {
58 max_side_len: 960,
59 box_threshold: 0.5,
60 unclip_ratio: 1.5,
61 score_threshold: 0.3,
62 min_area: 16,
63 box_border: 5,
64 merge_boxes: false,
65 merge_threshold: 10,
66 precision_mode: DetPrecisionMode::Fast,
67 multi_scales: vec![0.5, 1.0, 1.5],
68 block_size: 640,
69 block_overlap: 100,
70 nms_threshold: 0.3,
71 }
72 }
73}
74
75impl DetOptions {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn with_max_side_len(mut self, len: u32) -> Self {
83 self.max_side_len = len;
84 self
85 }
86
87 pub fn with_box_threshold(mut self, threshold: f32) -> Self {
89 self.box_threshold = threshold;
90 self
91 }
92
93 pub fn with_score_threshold(mut self, threshold: f32) -> Self {
95 self.score_threshold = threshold;
96 self
97 }
98
99 pub fn with_min_area(mut self, area: u32) -> Self {
101 self.min_area = area;
102 self
103 }
104
105 pub fn with_box_border(mut self, border: u32) -> Self {
107 self.box_border = border;
108 self
109 }
110
111 pub fn with_merge_boxes(mut self, merge: bool) -> Self {
113 self.merge_boxes = merge;
114 self
115 }
116
117 pub fn with_merge_threshold(mut self, threshold: i32) -> Self {
119 self.merge_threshold = threshold;
120 self
121 }
122
123 pub fn with_precision_mode(mut self, mode: DetPrecisionMode) -> Self {
125 self.precision_mode = mode;
126 self
127 }
128
129 pub fn with_multi_scales(mut self, scales: Vec<f32>) -> Self {
131 self.multi_scales = scales;
132 self
133 }
134
135 pub fn with_block_size(mut self, size: u32) -> Self {
137 self.block_size = size;
138 self
139 }
140
141 pub fn fast() -> Self {
143 Self {
144 max_side_len: 960,
145 precision_mode: DetPrecisionMode::Fast,
146 ..Default::default()
147 }
148 }
149}
150
151pub struct DetModel {
153 engine: InferenceEngine,
154 options: DetOptions,
155 normalize_params: NormalizeParams,
156}
157
158impl DetModel {
159 pub fn from_file(
165 model_path: impl AsRef<Path>,
166 config: Option<InferenceConfig>,
167 ) -> OcrResult<Self> {
168 let engine = InferenceEngine::from_file(model_path, config)?;
169 Ok(Self {
170 engine,
171 options: DetOptions::default(),
172 normalize_params: NormalizeParams::paddle_det(),
173 })
174 }
175
176 pub fn from_bytes(model_bytes: &[u8], config: Option<InferenceConfig>) -> OcrResult<Self> {
178 let engine = InferenceEngine::from_buffer(model_bytes, config)?;
179 Ok(Self {
180 engine,
181 options: DetOptions::default(),
182 normalize_params: NormalizeParams::paddle_det(),
183 })
184 }
185
186 pub fn with_options(mut self, options: DetOptions) -> Self {
188 self.options = options;
189 self
190 }
191
192 pub fn options(&self) -> &DetOptions {
194 &self.options
195 }
196
197 pub fn options_mut(&mut self) -> &mut DetOptions {
199 &mut self.options
200 }
201
202 pub fn detect(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
210 self.detect_fast(image)
211 }
212
213 pub fn detect_and_crop(&self, image: &DynamicImage) -> OcrResult<Vec<(DynamicImage, TextBox)>> {
221 let boxes = self.detect(image)?;
222 let (width, height) = image.dimensions();
223
224 let mut results = Vec::with_capacity(boxes.len());
225
226 for text_box in boxes {
227 let expanded = text_box.expand(self.options.box_border, width, height);
229
230 let cropped = crop_text_region(image, &expanded);
232
233 results.push((cropped, expanded));
234 }
235
236 Ok(results)
237 }
238
239 fn detect_fast(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
241 let (original_width, original_height) = image.dimensions();
242
243 let scaled = self.scale_image(image);
245 let (scaled_width, scaled_height) = scaled.dimensions();
246
247 let input = preprocess_for_det(&scaled, &self.normalize_params)?;
249
250 let output = self.engine.run_dynamic(input.view().into_dyn())?;
252
253 let output_shape = output.shape();
255 let out_w = output_shape[3] as u32;
256 let out_h = output_shape[2] as u32;
257
258 let boxes = self.postprocess_output(
259 &output,
260 out_w,
261 out_h,
262 scaled_width,
263 scaled_height,
264 original_width,
265 original_height,
266 )?;
267
268 Ok(boxes)
269 }
270
271 fn scale_image(&self, image: &DynamicImage) -> DynamicImage {
274 let (w, h) = image.dimensions();
275 let max_dim = w.max(h);
276
277 if max_dim <= self.options.max_side_len {
278 return image.clone();
279 }
280
281 let scale = self.options.max_side_len as f64 / max_dim as f64;
282 let new_w = (w as f64 * scale).round() as u32;
283 let new_h = (h as f64 * scale).round() as u32;
284
285 image.resize_exact(new_w, new_h, image::imageops::FilterType::Lanczos3)
286 }
287
288 fn postprocess_output(
290 &self,
291 output: &ArrayD<f32>,
292 out_w: u32,
293 out_h: u32,
294 scaled_width: u32,
295 scaled_height: u32,
296 original_width: u32,
297 original_height: u32,
298 ) -> OcrResult<Vec<TextBox>> {
299 let output_shape = output.shape();
301 if output_shape.len() < 3 {
302 return Err(OcrError::PostprocessError(
303 "Detection model output shape invalid".to_string(),
304 ));
305 }
306
307 let mask_data: Vec<f32> = output.iter().cloned().collect();
309
310 let binary_mask: Vec<u8> = mask_data
312 .iter()
313 .map(|&v| {
314 if v > self.options.score_threshold {
315 255u8
316 } else {
317 0u8
318 }
319 })
320 .collect();
321
322 let boxes = extract_boxes_with_unclip(
325 &binary_mask,
326 out_w,
327 out_h,
328 scaled_width,
329 scaled_height,
330 original_width,
331 original_height,
332 self.options.min_area,
333 self.options.unclip_ratio,
334 );
335
336 Ok(boxes)
337 }
338}
339
340fn crop_text_region(image: &DynamicImage, text_box: &TextBox) -> DynamicImage {
341 if let Some(points) = text_box.points {
342 if let Some(cropped) = crop_rotated_region(image, points) {
343 return cropped;
344 }
345 }
346
347 crop_axis_aligned_region(image, text_box)
348}
349
350fn crop_axis_aligned_region(image: &DynamicImage, text_box: &TextBox) -> DynamicImage {
351 let (image_width, image_height) = image.dimensions();
352 let x = text_box.rect.left().max(0) as u32;
353 let y = text_box.rect.top().max(0) as u32;
354 let width = text_box
355 .rect
356 .width()
357 .min(image_width.saturating_sub(x))
358 .max(1);
359 let height = text_box
360 .rect
361 .height()
362 .min(image_height.saturating_sub(y))
363 .max(1);
364
365 image.crop_imm(x, y, width, height)
366}
367
368fn crop_rotated_region(image: &DynamicImage, points: [Point<f32>; 4]) -> Option<DynamicImage> {
369 let crop_width = distance(points[0], points[1])
370 .max(distance(points[3], points[2]))
371 .round()
372 .max(1.0) as u32;
373 let crop_height = distance(points[0], points[3])
374 .max(distance(points[1], points[2]))
375 .round()
376 .max(1.0) as u32;
377
378 if crop_width <= 1 || crop_height <= 1 {
379 return None;
380 }
381
382 let source_points = points.map(|point| (point.x, point.y));
383 let target_points = [
384 (0.0, 0.0),
385 (crop_width.saturating_sub(1) as f32, 0.0),
386 (
387 crop_width.saturating_sub(1) as f32,
388 crop_height.saturating_sub(1) as f32,
389 ),
390 (0.0, crop_height.saturating_sub(1) as f32),
391 ];
392
393 let projection = Projection::from_control_points(source_points, target_points)?;
394 let source = image.to_rgb8();
395 let mut output = RgbImage::new(crop_width, crop_height);
396 warp_into(
397 &source,
398 &projection,
399 Interpolation::Bilinear,
400 Rgb([255, 255, 255]),
401 &mut output,
402 );
403
404 Some(DynamicImage::ImageRgb8(output))
405}
406
407fn distance(a: Point<f32>, b: Point<f32>) -> f32 {
408 let dx = a.x - b.x;
409 let dy = a.y - b.y;
410 (dx * dx + dy * dy).sqrt()
411}
412
413impl DetModel {
415 pub fn run_raw(&self, input: ndarray::ArrayViewD<f32>) -> OcrResult<ArrayD<f32>> {
425 Ok(self.engine.run_dynamic(input)?)
426 }
427
428 pub fn input_shape(&self) -> &[usize] {
430 self.engine.input_shape()
431 }
432
433 pub fn output_shape(&self) -> &[usize] {
435 self.engine.output_shape()
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_det_options_default() {
445 let opts = DetOptions::default();
446 assert_eq!(opts.max_side_len, 960);
447 assert_eq!(opts.box_threshold, 0.5);
448 assert_eq!(opts.unclip_ratio, 1.5);
449 assert_eq!(opts.score_threshold, 0.3);
450 assert_eq!(opts.min_area, 16);
451 assert_eq!(opts.box_border, 5);
452 assert!(!opts.merge_boxes);
453 assert_eq!(opts.merge_threshold, 10);
454 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
455 assert_eq!(opts.nms_threshold, 0.3);
456 }
457
458 #[test]
459 fn test_det_options_fast() {
460 let opts = DetOptions::fast();
461 assert_eq!(opts.max_side_len, 960);
462 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
463 }
464
465 #[test]
466 fn test_det_options_builder() {
467 let opts = DetOptions::new()
468 .with_max_side_len(1280)
469 .with_box_threshold(0.6)
470 .with_score_threshold(0.4)
471 .with_min_area(32)
472 .with_box_border(10)
473 .with_merge_boxes(true)
474 .with_merge_threshold(20)
475 .with_precision_mode(DetPrecisionMode::Fast)
476 .with_multi_scales(vec![0.5, 1.0, 1.5])
477 .with_block_size(800);
478
479 assert_eq!(opts.max_side_len, 1280);
480 assert_eq!(opts.box_threshold, 0.6);
481 assert_eq!(opts.score_threshold, 0.4);
482 assert_eq!(opts.min_area, 32);
483 assert_eq!(opts.box_border, 10);
484 assert!(opts.merge_boxes);
485 assert_eq!(opts.merge_threshold, 20);
486 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
487 assert_eq!(opts.multi_scales, vec![0.5, 1.0, 1.5]);
488 assert_eq!(opts.block_size, 800);
489 }
490
491 #[test]
492 fn test_det_precision_mode_default() {
493 let mode = DetPrecisionMode::default();
494 assert_eq!(mode, DetPrecisionMode::Fast);
495 }
496
497 #[test]
498 fn test_det_precision_mode_equality() {
499 assert_eq!(DetPrecisionMode::Fast, DetPrecisionMode::Fast);
500 }
501
502 #[test]
503 fn test_det_options_chaining() {
504 let opts = DetOptions::new()
506 .with_max_side_len(1000)
507 .with_box_threshold(0.7);
508
509 assert_eq!(opts.max_side_len, 1000);
510 assert_eq!(opts.box_threshold, 0.7);
511 assert_eq!(opts.score_threshold, 0.3);
513 }
514
515 #[test]
516 fn test_det_options_presets_are_valid() {
517 let fast = DetOptions::fast();
519 assert!(fast.box_threshold >= 0.0 && fast.box_threshold <= 1.0);
520 assert!(fast.score_threshold >= 0.0 && fast.score_threshold <= 1.0);
521 assert!(fast.nms_threshold >= 0.0 && fast.nms_threshold <= 1.0);
522 }
523}