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, resize_to_max_side, 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
151#[derive(Debug, Clone, Copy)]
152struct DetectionGeometry {
153 output_width: u32,
154 output_height: u32,
155 scaled_width: u32,
156 scaled_height: u32,
157 original_width: u32,
158 original_height: u32,
159}
160
161pub struct DetModel {
163 engine: InferenceEngine,
164 options: DetOptions,
165 normalize_params: NormalizeParams,
166}
167
168impl DetModel {
169 pub fn from_file(
175 model_path: impl AsRef<Path>,
176 config: Option<InferenceConfig>,
177 ) -> OcrResult<Self> {
178 let engine = InferenceEngine::from_file(model_path, config)?;
179 Ok(Self {
180 engine,
181 options: DetOptions::default(),
182 normalize_params: NormalizeParams::paddle_det(),
183 })
184 }
185
186 pub fn from_bytes(model_bytes: &[u8], config: Option<InferenceConfig>) -> OcrResult<Self> {
188 let engine = InferenceEngine::from_buffer(model_bytes, config)?;
189 Ok(Self {
190 engine,
191 options: DetOptions::default(),
192 normalize_params: NormalizeParams::paddle_det(),
193 })
194 }
195
196 pub fn with_options(mut self, options: DetOptions) -> Self {
198 self.options = options;
199 self
200 }
201
202 pub fn options(&self) -> &DetOptions {
204 &self.options
205 }
206
207 pub fn options_mut(&mut self) -> &mut DetOptions {
209 &mut self.options
210 }
211
212 pub fn detect(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
220 self.detect_fast(image)
221 }
222
223 pub fn detect_and_crop(&self, image: &DynamicImage) -> OcrResult<Vec<(DynamicImage, TextBox)>> {
231 let boxes = self.detect_expanded(image)?;
232 let rotated_source = if boxes.iter().any(|text_box| text_box.points.is_some()) {
233 Some(image.to_rgb8())
234 } else {
235 None
236 };
237
238 let mut results = Vec::with_capacity(boxes.len());
239
240 for text_box in boxes {
241 let cropped = crop_text_region(image, rotated_source.as_ref(), &text_box);
243
244 results.push((cropped, text_box));
245 }
246
247 Ok(results)
248 }
249
250 pub(crate) fn detect_expanded(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
251 let boxes = self.detect(image)?;
252 let (width, height) = image.dimensions();
253
254 Ok(boxes
255 .into_iter()
256 .map(|text_box| text_box.expand(self.options.box_border, width, height))
257 .collect())
258 }
259
260 fn detect_fast(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
262 let (original_width, original_height) = image.dimensions();
263
264 let scaled = self.scale_image(image)?;
266 let (scaled_width, scaled_height) = scaled.dimensions();
267
268 let input = preprocess_for_det(&scaled, &self.normalize_params)?;
270
271 let output = self.engine.run_dynamic(input.view().into_dyn())?;
273
274 let output_shape = output.shape();
276 let out_w = output_shape[3] as u32;
277 let out_h = output_shape[2] as u32;
278
279 let geometry = DetectionGeometry {
280 output_width: out_w,
281 output_height: out_h,
282 scaled_width,
283 scaled_height,
284 original_width,
285 original_height,
286 };
287 let boxes = self.postprocess_output(&output, geometry)?;
288
289 Ok(boxes)
290 }
291
292 fn scale_image(&self, image: &DynamicImage) -> OcrResult<DynamicImage> {
295 resize_to_max_side(image, self.options.max_side_len)
296 }
297
298 fn postprocess_output(
300 &self,
301 output: &ArrayD<f32>,
302 geometry: DetectionGeometry,
303 ) -> OcrResult<Vec<TextBox>> {
304 let output_shape = output.shape();
306 if output_shape.len() < 3 {
307 return Err(OcrError::PostprocessError(
308 "Detection model output shape invalid".to_string(),
309 ));
310 }
311
312 let binary_mask: Vec<u8> = output
314 .iter()
315 .map(|&v| {
316 if v > self.options.score_threshold {
317 255u8
318 } else {
319 0u8
320 }
321 })
322 .collect();
323
324 let boxes = extract_boxes_with_unclip(
327 &binary_mask,
328 geometry.output_width,
329 geometry.output_height,
330 geometry.scaled_width,
331 geometry.scaled_height,
332 geometry.original_width,
333 geometry.original_height,
334 self.options.min_area,
335 self.options.unclip_ratio,
336 );
337
338 Ok(boxes)
339 }
340}
341
342fn crop_text_region(
343 image: &DynamicImage,
344 rotated_source: Option<&RgbImage>,
345 text_box: &TextBox,
346) -> DynamicImage {
347 if let (Some(points), Some(source)) = (text_box.points, rotated_source) {
348 if let Some(cropped) = crop_rotated_region(source, points) {
349 return cropped;
350 }
351 }
352
353 crop_axis_aligned_region(image, text_box)
354}
355
356fn crop_axis_aligned_region(image: &DynamicImage, text_box: &TextBox) -> DynamicImage {
357 let (image_width, image_height) = image.dimensions();
358 let x = text_box.rect.left().max(0) as u32;
359 let y = text_box.rect.top().max(0) as u32;
360 let width = text_box
361 .rect
362 .width()
363 .min(image_width.saturating_sub(x))
364 .max(1);
365 let height = text_box
366 .rect
367 .height()
368 .min(image_height.saturating_sub(y))
369 .max(1);
370
371 image.crop_imm(x, y, width, height)
372}
373
374fn crop_rotated_region(source: &RgbImage, points: [Point<f32>; 4]) -> Option<DynamicImage> {
375 let crop_width = distance(points[0], points[1])
376 .max(distance(points[3], points[2]))
377 .round()
378 .max(1.0) as u32;
379 let crop_height = distance(points[0], points[3])
380 .max(distance(points[1], points[2]))
381 .round()
382 .max(1.0) as u32;
383
384 if crop_width <= 1 || crop_height <= 1 {
385 return None;
386 }
387
388 let source_points = points.map(|point| (point.x, point.y));
389 let target_points = [
390 (0.0, 0.0),
391 (crop_width.saturating_sub(1) as f32, 0.0),
392 (
393 crop_width.saturating_sub(1) as f32,
394 crop_height.saturating_sub(1) as f32,
395 ),
396 (0.0, crop_height.saturating_sub(1) as f32),
397 ];
398
399 let projection = Projection::from_control_points(source_points, target_points)?;
400 let mut output = RgbImage::new(crop_width, crop_height);
401 warp_into(
402 source,
403 &projection,
404 Interpolation::Bilinear,
405 Rgb([255, 255, 255]),
406 &mut output,
407 );
408
409 Some(DynamicImage::ImageRgb8(output))
410}
411
412fn distance(a: Point<f32>, b: Point<f32>) -> f32 {
413 let dx = a.x - b.x;
414 let dy = a.y - b.y;
415 (dx * dx + dy * dy).sqrt()
416}
417
418impl DetModel {
420 pub fn run_raw(&self, input: ndarray::ArrayViewD<f32>) -> OcrResult<ArrayD<f32>> {
430 Ok(self.engine.run_dynamic(input)?)
431 }
432
433 pub fn input_shape(&self) -> &[usize] {
435 self.engine.input_shape()
436 }
437
438 pub fn output_shape(&self) -> &[usize] {
440 self.engine.output_shape()
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_det_options_default() {
450 let opts = DetOptions::default();
451 assert_eq!(opts.max_side_len, 960);
452 assert_eq!(opts.box_threshold, 0.5);
453 assert_eq!(opts.unclip_ratio, 1.5);
454 assert_eq!(opts.score_threshold, 0.3);
455 assert_eq!(opts.min_area, 16);
456 assert_eq!(opts.box_border, 5);
457 assert!(!opts.merge_boxes);
458 assert_eq!(opts.merge_threshold, 10);
459 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
460 assert_eq!(opts.nms_threshold, 0.3);
461 }
462
463 #[test]
464 fn test_det_options_fast() {
465 let opts = DetOptions::fast();
466 assert_eq!(opts.max_side_len, 960);
467 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
468 }
469
470 #[test]
471 fn test_det_options_builder() {
472 let opts = DetOptions::new()
473 .with_max_side_len(1280)
474 .with_box_threshold(0.6)
475 .with_score_threshold(0.4)
476 .with_min_area(32)
477 .with_box_border(10)
478 .with_merge_boxes(true)
479 .with_merge_threshold(20)
480 .with_precision_mode(DetPrecisionMode::Fast)
481 .with_multi_scales(vec![0.5, 1.0, 1.5])
482 .with_block_size(800);
483
484 assert_eq!(opts.max_side_len, 1280);
485 assert_eq!(opts.box_threshold, 0.6);
486 assert_eq!(opts.score_threshold, 0.4);
487 assert_eq!(opts.min_area, 32);
488 assert_eq!(opts.box_border, 10);
489 assert!(opts.merge_boxes);
490 assert_eq!(opts.merge_threshold, 20);
491 assert_eq!(opts.precision_mode, DetPrecisionMode::Fast);
492 assert_eq!(opts.multi_scales, vec![0.5, 1.0, 1.5]);
493 assert_eq!(opts.block_size, 800);
494 }
495
496 #[test]
497 fn test_det_precision_mode_default() {
498 let mode = DetPrecisionMode::default();
499 assert_eq!(mode, DetPrecisionMode::Fast);
500 }
501
502 #[test]
503 fn test_det_precision_mode_equality() {
504 assert_eq!(DetPrecisionMode::Fast, DetPrecisionMode::Fast);
505 }
506
507 #[test]
508 fn test_det_options_chaining() {
509 let opts = DetOptions::new()
511 .with_max_side_len(1000)
512 .with_box_threshold(0.7);
513
514 assert_eq!(opts.max_side_len, 1000);
515 assert_eq!(opts.box_threshold, 0.7);
516 assert_eq!(opts.score_threshold, 0.3);
518 }
519
520 #[test]
521 fn test_det_options_presets_are_valid() {
522 let fast = DetOptions::fast();
524 assert!(fast.box_threshold >= 0.0 && fast.box_threshold <= 1.0);
525 assert!(fast.score_threshold >= 0.0 && fast.score_threshold <= 1.0);
526 assert!(fast.nms_threshold >= 0.0 && fast.nms_threshold <= 1.0);
527 }
528}