oar_ocr_core/domain/tasks/
text_detection.rs1use 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#[derive(Debug, Clone)]
16pub struct Detection {
17 pub bbox: BoundingBox,
19 pub score: f32,
21}
22
23impl Detection {
24 pub fn new(bbox: BoundingBox, score: f32) -> Self {
26 Self { bbox, score }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, ConfigValidator)]
34pub struct TextDetectionConfig {
35 #[validate(range(min = 0.0, max = 1.0))]
37 pub score_threshold: f32,
38 #[validate(range(min = 0.0, max = 1.0))]
40 pub box_threshold: f32,
41 #[validate(min = 0.0)]
43 pub unclip_ratio: f32,
44 #[validate(min = 1)]
46 pub max_candidates: usize,
47 pub limit_side_len: Option<u32>,
49 pub limit_type: Option<LimitType>,
51 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#[derive(Debug, Clone)]
71pub struct TextDetectionOutput {
72 pub detections: Vec<Vec<Detection>>,
74}
75
76impl TextDetectionOutput {
77 pub fn empty() -> Self {
79 Self {
80 detections: Vec::new(),
81 }
82 }
83
84 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#[derive(Debug, Default)]
103pub struct TextDetectionTask {
104 _config: TextDetectionConfig,
105}
106
107impl TextDetectionTask {
108 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 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 let empty_input = ImageTaskInput::new(vec![]);
166 assert!(task.validate_input(&empty_input).is_err());
167
168 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 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 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); let bad_output = TextDetectionOutput {
199 detections: vec![vec![detection2]],
200 };
201 assert!(task.validate_output(&bad_output).is_err());
202 }
203}