Skip to main content

ocr_rs/
det.rs

1//! Text Detection Model
2//!
3//! Provides text region detection functionality based on PaddleOCR detection models
4
5use 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/// Detection precision mode
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum DetPrecisionMode {
19    /// Fast mode - single detection
20    #[default]
21    Fast,
22}
23
24/// Detection options
25#[derive(Debug, Clone)]
26pub struct DetOptions {
27    /// Maximum image side length limit (will be scaled if exceeded)
28    pub max_side_len: u32,
29    /// Bounding box binarization threshold (0.0 - 1.0)
30    pub box_threshold: f32,
31    /// Text box expansion ratio
32    pub unclip_ratio: f32,
33    /// Pixel-level segmentation threshold
34    pub score_threshold: f32,
35    /// Minimum bounding box area
36    pub min_area: u32,
37    /// Bounding box border expansion
38    pub box_border: u32,
39    /// Whether to merge adjacent text boxes
40    pub merge_boxes: bool,
41    /// Merge distance threshold
42    pub merge_threshold: i32,
43    /// Precision mode
44    pub precision_mode: DetPrecisionMode,
45    /// Scale ratios for multi-scale detection (high precision mode only)
46    pub multi_scales: Vec<f32>,
47    /// Block size for block detection (high precision mode only)
48    pub block_size: u32,
49    /// Overlap area for block detection
50    pub block_overlap: u32,
51    /// NMS IoU threshold
52    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    /// Create new detection options
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Set maximum side length
82    pub fn with_max_side_len(mut self, len: u32) -> Self {
83        self.max_side_len = len;
84        self
85    }
86
87    /// Set bounding box threshold
88    pub fn with_box_threshold(mut self, threshold: f32) -> Self {
89        self.box_threshold = threshold;
90        self
91    }
92
93    /// Set segmentation threshold
94    pub fn with_score_threshold(mut self, threshold: f32) -> Self {
95        self.score_threshold = threshold;
96        self
97    }
98
99    /// Set minimum area
100    pub fn with_min_area(mut self, area: u32) -> Self {
101        self.min_area = area;
102        self
103    }
104
105    /// Set box border expansion
106    pub fn with_box_border(mut self, border: u32) -> Self {
107        self.box_border = border;
108        self
109    }
110
111    /// Enable box merging
112    pub fn with_merge_boxes(mut self, merge: bool) -> Self {
113        self.merge_boxes = merge;
114        self
115    }
116
117    /// Set merge threshold
118    pub fn with_merge_threshold(mut self, threshold: i32) -> Self {
119        self.merge_threshold = threshold;
120        self
121    }
122
123    /// Set precision mode
124    pub fn with_precision_mode(mut self, mode: DetPrecisionMode) -> Self {
125        self.precision_mode = mode;
126        self
127    }
128
129    /// Set multi-scale ratios
130    pub fn with_multi_scales(mut self, scales: Vec<f32>) -> Self {
131        self.multi_scales = scales;
132        self
133    }
134
135    /// Set block size
136    pub fn with_block_size(mut self, size: u32) -> Self {
137        self.block_size = size;
138        self
139    }
140
141    /// Fast mode preset
142    pub fn fast() -> Self {
143        Self {
144            max_side_len: 960,
145            precision_mode: DetPrecisionMode::Fast,
146            ..Default::default()
147        }
148    }
149}
150
151/// Text detection model
152pub struct DetModel {
153    engine: InferenceEngine,
154    options: DetOptions,
155    normalize_params: NormalizeParams,
156}
157
158impl DetModel {
159    /// Create detector from model file
160    ///
161    /// # Parameters
162    /// - `model_path`: Model file path (.mnn format)
163    /// - `config`: Optional inference config
164    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    /// Create detector from model bytes
177    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    /// Set detection options
187    pub fn with_options(mut self, options: DetOptions) -> Self {
188        self.options = options;
189        self
190    }
191
192    /// Get current detection options
193    pub fn options(&self) -> &DetOptions {
194        &self.options
195    }
196
197    /// Modify detection options
198    pub fn options_mut(&mut self) -> &mut DetOptions {
199        &mut self.options
200    }
201
202    /// Detect text regions in image
203    ///
204    /// # Parameters
205    /// - `image`: Input image
206    ///
207    /// # Returns
208    /// List of detected text bounding boxes
209    pub fn detect(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
210        self.detect_fast(image)
211    }
212
213    /// Detect and return cropped text images
214    ///
215    /// # Parameters
216    /// - `image`: Input image
217    ///
218    /// # Returns
219    /// List of (text image, corresponding bounding box)
220    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            // Expand bounding box
228            let expanded = text_box.expand(self.options.box_border, width, height);
229
230            // Crop image
231            let cropped = crop_text_region(image, &expanded);
232
233            results.push((cropped, expanded));
234        }
235
236        Ok(results)
237    }
238
239    /// Fast detection (single inference)
240    fn detect_fast(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
241        let (original_width, original_height) = image.dimensions();
242
243        // Scale image
244        let scaled = self.scale_image(image);
245        let (scaled_width, scaled_height) = scaled.dimensions();
246
247        // Preprocess
248        let input = preprocess_for_det(&scaled, &self.normalize_params)?;
249
250        // Inference (using dynamic shape)
251        let output = self.engine.run_dynamic(input.view().into_dyn())?;
252
253        // Post-processing - output shape matches input (including padding)
254        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    /// Balanced mode detection (multi-scale)
272    /// Scale image to maximum side length limit
273    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    /// Post-process inference output
289    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        // Retrieve output data
300        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        // Extract segmentation mask (only valid region, remove padding)
308        let mask_data: Vec<f32> = output.iter().cloned().collect();
309
310        // Binarization
311        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        // Extract bounding boxes (with unclip expansion)
323        // DB algorithm needs to expand detected contours because model output segmentation mask is usually smaller than actual text region
324        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
413/// Low-level detection API
414impl DetModel {
415    /// Raw inference interface
416    ///
417    /// Execute model inference directly without preprocessing and postprocessing
418    ///
419    /// # Parameters
420    /// - `input`: Preprocessed input tensor [1, 3, H, W]
421    ///
422    /// # Returns
423    /// Model raw output
424    pub fn run_raw(&self, input: ndarray::ArrayViewD<f32>) -> OcrResult<ArrayD<f32>> {
425        Ok(self.engine.run_dynamic(input)?)
426    }
427
428    /// Get model input shape
429    pub fn input_shape(&self) -> &[usize] {
430        self.engine.input_shape()
431    }
432
433    /// Get model output shape
434    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        // Test that chaining calls do not lose previous settings
505        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        // Other values should be default values
512        assert_eq!(opts.score_threshold, 0.3);
513    }
514
515    #[test]
516    fn test_det_options_presets_are_valid() {
517        // Ensure preset parameter values are within valid ranges
518        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}