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
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_expanded(image)?;
222 let rotated_source = if boxes.iter().any(|text_box| text_box.points.is_some()) {
223 Some(image.to_rgb8())
224 } else {
225 None
226 };
227
228 let mut results = Vec::with_capacity(boxes.len());
229
230 for text_box in boxes {
231 let cropped = crop_text_region(image, rotated_source.as_ref(), &text_box);
233
234 results.push((cropped, text_box));
235 }
236
237 Ok(results)
238 }
239
240 pub(crate) fn detect_expanded(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
241 let boxes = self.detect(image)?;
242 let (width, height) = image.dimensions();
243
244 Ok(boxes
245 .into_iter()
246 .map(|text_box| text_box.expand(self.options.box_border, width, height))
247 .collect())
248 }
249
250 fn detect_fast(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
252 let (original_width, original_height) = image.dimensions();
253
254 let scaled = self.scale_image(image)?;
256 let (scaled_width, scaled_height) = scaled.dimensions();
257
258 let input = preprocess_for_det(&scaled, &self.normalize_params)?;
260
261 let output = self.engine.run_dynamic(input.view().into_dyn())?;
263
264 let output_shape = output.shape();
266 let out_w = output_shape[3] as u32;
267 let out_h = output_shape[2] as u32;
268
269 let boxes = self.postprocess_output(
270 &output,
271 out_w,
272 out_h,
273 scaled_width,
274 scaled_height,
275 original_width,
276 original_height,
277 )?;
278
279 Ok(boxes)
280 }
281
282 fn scale_image(&self, image: &DynamicImage) -> OcrResult<DynamicImage> {
285 resize_to_max_side(image, self.options.max_side_len)
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 binary_mask: Vec<u8> = output
309 .iter()
310 .map(|&v| {
311 if v > self.options.score_threshold {
312 255u8
313 } else {
314 0u8
315 }
316 })
317 .collect();
318
319 let boxes = extract_boxes_with_unclip(
322 &binary_mask,
323 out_w,
324 out_h,
325 scaled_width,
326 scaled_height,
327 original_width,
328 original_height,
329 self.options.min_area,
330 self.options.unclip_ratio,
331 );
332
333 Ok(boxes)
334 }
335}
336
337fn crop_text_region(
338 image: &DynamicImage,
339 rotated_source: Option<&RgbImage>,
340 text_box: &TextBox,
341) -> DynamicImage {
342 if let (Some(points), Some(source)) = (text_box.points, rotated_source) {
343 if let Some(cropped) = crop_rotated_region(source, points) {
344 return cropped;
345 }
346 }
347
348 crop_axis_aligned_region(image, text_box)
349}
350
351fn crop_axis_aligned_region(image: &DynamicImage, text_box: &TextBox) -> DynamicImage {
352 let (image_width, image_height) = image.dimensions();
353 let x = text_box.rect.left().max(0) as u32;
354 let y = text_box.rect.top().max(0) as u32;
355 let width = text_box
356 .rect
357 .width()
358 .min(image_width.saturating_sub(x))
359 .max(1);
360 let height = text_box
361 .rect
362 .height()
363 .min(image_height.saturating_sub(y))
364 .max(1);
365
366 image.crop_imm(x, y, width, height)
367}
368
369fn crop_rotated_region(source: &RgbImage, points: [Point<f32>; 4]) -> Option<DynamicImage> {
370 let crop_width = distance(points[0], points[1])
371 .max(distance(points[3], points[2]))
372 .round()
373 .max(1.0) as u32;
374 let crop_height = distance(points[0], points[3])
375 .max(distance(points[1], points[2]))
376 .round()
377 .max(1.0) as u32;
378
379 if crop_width <= 1 || crop_height <= 1 {
380 return None;
381 }
382
383 let source_points = points.map(|point| (point.x, point.y));
384 let target_points = [
385 (0.0, 0.0),
386 (crop_width.saturating_sub(1) as f32, 0.0),
387 (
388 crop_width.saturating_sub(1) as f32,
389 crop_height.saturating_sub(1) as f32,
390 ),
391 (0.0, crop_height.saturating_sub(1) as f32),
392 ];
393
394 let projection = Projection::from_control_points(source_points, target_points)?;
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}