Skip to main content

oar_ocr_core/domain/tasks/
text_detection.rs

1//! Concrete task implementations for text detection.
2//!
3//! This module provides the text detection task that locates text regions in images.
4
5use super::validation::ensure_non_empty_images;
6use crate::ConfigValidator;
7use crate::core::OCRError;
8use crate::core::traits::TaskDefinition;
9use crate::core::traits::task::{ImageTaskInput, Task, TaskType};
10use crate::processors::{BoundingBox, LimitType};
11use crate::utils::ScoreValidator;
12use serde::{Deserialize, Serialize};
13
14/// A single text detection result with bounding box and confidence score.
15#[derive(Debug, Clone)]
16pub struct Detection {
17    /// The bounding box polygon coordinates
18    pub bbox: BoundingBox,
19    /// Confidence score for this detection (0.0 to 1.0)
20    pub score: f32,
21}
22
23impl Detection {
24    /// Creates a new detection.
25    pub fn new(bbox: BoundingBox, score: f32) -> Self {
26        Self { bbox, score }
27    }
28}
29
30/// Configuration for text detection task.
31///
32/// Default values are aligned with PP-StructureV3.
33#[derive(Debug, Clone, Serialize, Deserialize, ConfigValidator)]
34pub struct TextDetectionConfig {
35    /// Score threshold for detection (default: 0.3)
36    #[validate(range(min = 0.0, max = 1.0))]
37    pub score_threshold: f32,
38    /// Box threshold for filtering (default: 0.6)
39    #[validate(range(min = 0.0, max = 1.0))]
40    pub box_threshold: f32,
41    /// Unclip ratio for expanding detected regions (default: 1.5)
42    #[validate(min = 0.0)]
43    pub unclip_ratio: f32,
44    /// Maximum candidates to consider (default: 1000)
45    #[validate(min = 1)]
46    pub max_candidates: usize,
47    /// Target side length for image resizing (optional)
48    pub limit_side_len: Option<u32>,
49    /// Limit type for resizing (optional)
50    pub limit_type: Option<LimitType>,
51    /// Maximum side length to prevent OOM (optional)
52    pub max_side_len: Option<u32>,
53}
54
55impl Default for TextDetectionConfig {
56    fn default() -> Self {
57        Self {
58            score_threshold: 0.3,
59            box_threshold: 0.6,
60            unclip_ratio: 1.5,
61            max_candidates: 1000,
62            limit_side_len: None,
63            limit_type: None,
64            max_side_len: None,
65        }
66    }
67}
68
69/// Output from text detection task.
70#[derive(Debug, Clone)]
71pub struct TextDetectionOutput {
72    /// Detected text regions per image
73    pub detections: Vec<Vec<Detection>>,
74}
75
76impl TextDetectionOutput {
77    /// Creates an empty text detection output.
78    pub fn empty() -> Self {
79        Self {
80            detections: Vec::new(),
81        }
82    }
83
84    /// Creates a text detection output with the given capacity.
85    pub fn with_capacity(capacity: usize) -> Self {
86        Self {
87            detections: Vec::with_capacity(capacity),
88        }
89    }
90}
91
92impl TaskDefinition for TextDetectionOutput {
93    const TASK_NAME: &'static str = "text_detection";
94    const TASK_DOC: &'static str = "Text detection - locating text regions in images";
95
96    fn empty() -> Self {
97        TextDetectionOutput::empty()
98    }
99}
100
101/// Text detection task implementation.
102#[derive(Debug, Default)]
103pub struct TextDetectionTask {
104    _config: TextDetectionConfig,
105}
106
107impl TextDetectionTask {
108    /// Creates a new text detection task.
109    pub fn new(config: TextDetectionConfig) -> Self {
110        Self { _config: config }
111    }
112}
113
114impl Task for TextDetectionTask {
115    type Config = TextDetectionConfig;
116    type Input = ImageTaskInput;
117    type Output = TextDetectionOutput;
118
119    fn task_type(&self) -> TaskType {
120        TaskType::TextDetection
121    }
122
123    fn validate_input(&self, input: &Self::Input) -> Result<(), OCRError> {
124        ensure_non_empty_images(&input.images, "No images provided for text detection")?;
125
126        Ok(())
127    }
128
129    fn validate_output(&self, output: &Self::Output) -> Result<(), OCRError> {
130        let validator = ScoreValidator::new_unit_range("score");
131
132        // Validate each image's detections
133        for (idx, detections) in output.detections.iter().enumerate() {
134            let scores: Vec<f32> = detections.iter().map(|d| d.score).collect();
135            validator.validate_scores_with(&scores, |det_idx| {
136                format!("Image {}, detection {}", idx, det_idx)
137            })?;
138        }
139
140        Ok(())
141    }
142
143    fn empty_output(&self) -> Self::Output {
144        TextDetectionOutput::empty()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::processors::Point;
152    use image::RgbImage;
153
154    #[test]
155    fn test_text_detection_task_creation() {
156        let task = TextDetectionTask::default();
157        assert_eq!(task.task_type(), TaskType::TextDetection);
158    }
159
160    #[test]
161    fn test_input_validation() {
162        let task = TextDetectionTask::default();
163
164        // Empty images should fail
165        let empty_input = ImageTaskInput::new(vec![]);
166        assert!(task.validate_input(&empty_input).is_err());
167
168        // Valid images should pass
169        let valid_input = ImageTaskInput::new(vec![RgbImage::new(100, 100)]);
170        assert!(task.validate_input(&valid_input).is_ok());
171    }
172
173    #[test]
174    fn test_output_validation() {
175        let task = TextDetectionTask::default();
176
177        // Valid detection should pass
178        let box1 = BoundingBox::new(vec![
179            Point::new(0.0, 0.0),
180            Point::new(10.0, 0.0),
181            Point::new(10.0, 10.0),
182            Point::new(0.0, 10.0),
183        ]);
184        let detection1 = Detection::new(box1, 0.95);
185        let output = TextDetectionOutput {
186            detections: vec![vec![detection1]],
187        };
188        assert!(task.validate_output(&output).is_ok());
189
190        // Invalid score should fail
191        let box2 = BoundingBox::new(vec![
192            Point::new(0.0, 0.0),
193            Point::new(10.0, 0.0),
194            Point::new(10.0, 10.0),
195            Point::new(0.0, 10.0),
196        ]);
197        let detection2 = Detection::new(box2, 1.5); // Invalid score > 1.0
198        let bad_output = TextDetectionOutput {
199            detections: vec![vec![detection2]],
200        };
201        assert!(task.validate_output(&bad_output).is_err());
202    }
203}