Skip to main content

oar_ocr/oarocr/
processors.rs

1//! Data processors for task graph edges.
2//!
3//! This module provides processors that transform data between task nodes in the graph.
4//! For example, cropping and perspective transformation between detection and recognition.
5
6use image::{Rgb, RgbImage};
7use imageproc::geometric_transformations::{Interpolation, rotate_about_center};
8use oar_ocr_core::core::OCRError;
9use oar_ocr_core::processors::BoundingBox;
10use oar_ocr_core::utils::BBoxCrop;
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::sync::Arc;
14
15/// Trait for processors that transform data between task nodes.
16pub trait EdgeProcessor: Debug + Send + Sync {
17    /// Input type for this processor
18    type Input;
19
20    /// Output type for this processor
21    type Output;
22
23    /// Process the input data and produce output
24    fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError>;
25
26    /// Get the processor name for debugging
27    fn name(&self) -> &str;
28}
29
30/// Configuration for edge processors in the task graph.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum EdgeProcessorConfig {
34    /// Crop text regions from image based on bounding boxes
35    TextCropping {
36        /// Whether to handle rotated bounding boxes
37        #[serde(default = "default_true")]
38        handle_rotation: bool,
39    },
40
41    /// Apply perspective transformation to correct text orientation
42    PerspectiveTransform {
43        /// Target width for transformed images
44        target_width: Option<u32>,
45        /// Target height for transformed images
46        target_height: Option<u32>,
47    },
48
49    /// Rotate images based on orientation angles
50    ImageRotation {
51        /// Whether to rotate based on detected angles
52        #[serde(default = "default_true")]
53        auto_rotate: bool,
54    },
55
56    /// Resize images to specific dimensions
57    ImageResize {
58        /// Target width
59        width: u32,
60        /// Target height
61        height: u32,
62        /// Whether to maintain aspect ratio
63        #[serde(default)]
64        maintain_aspect_ratio: bool,
65    },
66
67    /// Chain multiple processors
68    Chain {
69        /// List of processors to apply in sequence
70        processors: Vec<EdgeProcessorConfig>,
71    },
72}
73
74fn default_true() -> bool {
75    true
76}
77
78/// Processor that crops text regions from an image based on bounding boxes.
79#[derive(Debug)]
80pub struct TextCroppingProcessor {
81    pub(crate) handle_rotation: bool,
82}
83
84impl TextCroppingProcessor {
85    pub fn new(handle_rotation: bool) -> Self {
86        Self { handle_rotation }
87    }
88
89    /// Crop a single bounding box from an image
90    fn crop_single(&self, image: &RgbImage, bbox: &BoundingBox) -> Result<RgbImage, OCRError> {
91        if self.handle_rotation && bbox.points.len() == 4 {
92            // Rotated bounding box (quadrilateral) - use perspective transform
93            BBoxCrop::crop_rotated_bounding_box(image, bbox)
94        } else {
95            // Regular axis-aligned bounding box
96            BBoxCrop::crop_bounding_box(image, bbox)
97        }
98    }
99}
100
101impl EdgeProcessor for TextCroppingProcessor {
102    type Input = (Arc<RgbImage>, Vec<BoundingBox>);
103    type Output = Vec<Option<Arc<RgbImage>>>;
104
105    fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError> {
106        let (image, bboxes) = input;
107
108        let cropped_images: Vec<Option<Arc<RgbImage>>> = bboxes
109            .iter()
110            .map(|bbox| {
111                self.crop_single(&image, bbox)
112                    .map(|img| Some(Arc::new(img)))
113                    .unwrap_or_else(|_e| {
114                        // Failed to crop, return None
115                        None
116                    })
117            })
118            .collect();
119
120        Ok(cropped_images)
121    }
122
123    fn name(&self) -> &str {
124        "TextCropping"
125    }
126}
127
128/// Processor that rotates images based on orientation angles.
129#[derive(Debug)]
130pub struct ImageRotationProcessor {
131    auto_rotate: bool,
132}
133
134impl ImageRotationProcessor {
135    pub fn new(auto_rotate: bool) -> Self {
136        Self { auto_rotate }
137    }
138}
139
140impl EdgeProcessor for ImageRotationProcessor {
141    type Input = (Vec<Option<Arc<RgbImage>>>, Vec<Option<f32>>);
142    type Output = Vec<Option<Arc<RgbImage>>>;
143
144    fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError> {
145        let (images, angles) = input;
146
147        if !self.auto_rotate {
148            return Ok(images);
149        }
150
151        let rotated_images: Vec<Option<Arc<RgbImage>>> = images
152            .into_iter()
153            .zip(angles.iter())
154            .map(|(img_opt, angle_opt)| {
155                match (img_opt, angle_opt) {
156                    (Some(img), Some(angle)) if angle.abs() > 0.1 => {
157                        // Rotate image by the detected angle
158                        // Convert angle from degrees to radians (imageproc expects radians)
159                        let angle_radians = -angle.to_radians(); // Negative for clockwise rotation
160
161                        // Use bilinear interpolation for smooth rotation
162                        let rotated = rotate_about_center(
163                            &img,
164                            angle_radians,
165                            Interpolation::Bilinear,
166                            Rgb([255u8, 255u8, 255u8]), // White background for padding
167                        );
168
169                        Some(Arc::new(rotated))
170                    }
171                    (img_opt, _) => img_opt,
172                }
173            })
174            .collect();
175
176        Ok(rotated_images)
177    }
178
179    fn name(&self) -> &str {
180        "ImageRotation"
181    }
182}
183
184/// Processor that chains multiple processors together.
185///
186/// All processors in the chain must have the same input and output types,
187/// allowing the output of each processor to be fed as input to the next.
188#[derive(Debug)]
189pub struct ChainProcessor<T> {
190    processors: Vec<Box<dyn EdgeProcessor<Input = T, Output = T>>>,
191}
192
193impl<T> ChainProcessor<T> {
194    /// Creates a new chain processor with the given processors.
195    pub fn new(processors: Vec<Box<dyn EdgeProcessor<Input = T, Output = T>>>) -> Self {
196        Self { processors }
197    }
198}
199
200impl<T> EdgeProcessor for ChainProcessor<T>
201where
202    T: Debug + Send + Sync,
203{
204    type Input = T;
205    type Output = T;
206
207    fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError> {
208        if self.processors.is_empty() {
209            return Err(OCRError::ConfigError {
210                message: "Empty processor chain".to_string(),
211            });
212        }
213
214        // Apply all processors in sequence, threading the output of each as input to the next
215        let mut current = input;
216
217        for processor in &self.processors {
218            current = processor.process(current)?;
219        }
220
221        Ok(current)
222    }
223
224    fn name(&self) -> &str {
225        "Chain"
226    }
227}
228
229/// Type alias for text cropping processor output
230type TextCroppingOutput = Box<
231    dyn EdgeProcessor<
232            Input = (Arc<RgbImage>, Vec<BoundingBox>),
233            Output = Vec<Option<Arc<RgbImage>>>,
234        >,
235>;
236
237/// Type alias for image rotation processor output
238type ImageRotationOutput = Box<
239    dyn EdgeProcessor<
240            Input = (Vec<Option<Arc<RgbImage>>>, Vec<Option<f32>>),
241            Output = Vec<Option<Arc<RgbImage>>>,
242        >,
243>;
244
245/// Factory for creating edge processors from configuration.
246pub struct EdgeProcessorFactory;
247
248impl EdgeProcessorFactory {
249    /// Create a text cropping processor
250    pub fn create_text_cropping(handle_rotation: bool) -> TextCroppingOutput {
251        Box::new(TextCroppingProcessor::new(handle_rotation))
252    }
253
254    /// Create an image rotation processor
255    pub fn create_image_rotation(auto_rotate: bool) -> ImageRotationOutput {
256        Box::new(ImageRotationProcessor::new(auto_rotate))
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_text_cropping_processor_creation() {
266        let processor = TextCroppingProcessor::new(true);
267        assert_eq!(processor.name(), "TextCropping");
268    }
269
270    #[test]
271    fn test_image_rotation_processor_creation() {
272        let processor = ImageRotationProcessor::new(true);
273        assert_eq!(processor.name(), "ImageRotation");
274    }
275
276    #[test]
277    fn test_edge_processor_config_serialization() -> Result<(), Box<dyn std::error::Error>> {
278        let config = EdgeProcessorConfig::TextCropping {
279            handle_rotation: true,
280        };
281
282        let json = serde_json::to_string(&config)?;
283        assert!(json.contains("TextCropping"));
284
285        let deserialized: EdgeProcessorConfig = serde_json::from_str(&json)?;
286        if let EdgeProcessorConfig::TextCropping { handle_rotation } = deserialized {
287            assert!(handle_rotation);
288        } else {
289            panic!("Wrong variant");
290        }
291        Ok(())
292    }
293
294    #[test]
295    fn test_image_rotation_processor_rotates_images() -> Result<(), OCRError> {
296        let processor = ImageRotationProcessor::new(true);
297
298        // Create a simple test image (10x10 white image)
299        let img = Arc::new(RgbImage::from_pixel(10, 10, Rgb([255u8, 255u8, 255u8])));
300
301        // Test with rotation angle
302        let images = vec![Some(img.clone())];
303        let angles = vec![Some(45.0)]; // 45 degree rotation
304
305        let result = processor.process((images, angles))?;
306
307        // Should have one rotated image
308        assert_eq!(result.len(), 1);
309        assert!(result[0].is_some());
310
311        // The rotated image should have different dimensions due to rotation
312        let Some(rotated) = result[0].as_ref() else {
313            panic!("expected rotated image to be Some");
314        };
315        // After rotation, the image will be larger to accommodate the rotated content
316        assert!(rotated.width() >= 10 || rotated.height() >= 10);
317        Ok(())
318    }
319
320    #[test]
321    fn test_image_rotation_processor_skips_small_angles() -> Result<(), OCRError> {
322        let processor = ImageRotationProcessor::new(true);
323
324        let img = Arc::new(RgbImage::from_pixel(10, 10, Rgb([255u8, 255u8, 255u8])));
325        let images = vec![Some(img.clone())];
326        let angles = vec![Some(0.05)]; // Very small angle, should be skipped
327
328        let result = processor.process((images, angles))?;
329
330        // Should return the original image unchanged
331        assert_eq!(result.len(), 1);
332        assert!(result[0].is_some());
333        let Some(output) = result[0].as_ref() else {
334            panic!("expected output image to be Some");
335        };
336        assert_eq!(output.dimensions(), img.dimensions());
337        Ok(())
338    }
339
340    #[test]
341    fn test_image_rotation_processor_disabled() -> Result<(), OCRError> {
342        let processor = ImageRotationProcessor::new(false); // auto_rotate disabled
343
344        let img = Arc::new(RgbImage::from_pixel(10, 10, Rgb([255u8, 255u8, 255u8])));
345        let images = vec![Some(img.clone())];
346        let angles = vec![Some(45.0)];
347
348        let result = processor.process((images, angles))?;
349
350        // Should return the original image unchanged
351        assert_eq!(result.len(), 1);
352        assert!(result[0].is_some());
353        let Some(output) = result[0].as_ref() else {
354            panic!("expected output image to be Some");
355        };
356        assert_eq!(output.dimensions(), img.dimensions());
357        Ok(())
358    }
359
360    // Test processor that adds a value to an integer
361    #[derive(Debug)]
362    struct AddProcessor {
363        value: i32,
364    }
365
366    impl EdgeProcessor for AddProcessor {
367        type Input = i32;
368        type Output = i32;
369
370        fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError> {
371            Ok(input + self.value)
372        }
373
374        fn name(&self) -> &str {
375            "Add"
376        }
377    }
378
379    // Test processor that multiplies an integer by a value
380    #[derive(Debug)]
381    struct MultiplyProcessor {
382        value: i32,
383    }
384
385    impl EdgeProcessor for MultiplyProcessor {
386        type Input = i32;
387        type Output = i32;
388
389        fn process(&self, input: Self::Input) -> Result<Self::Output, OCRError> {
390            Ok(input * self.value)
391        }
392
393        fn name(&self) -> &str {
394            "Multiply"
395        }
396    }
397
398    #[test]
399    fn test_chain_processor_single_processor() -> Result<(), OCRError> {
400        let processors: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> =
401            vec![Box::new(AddProcessor { value: 5 })];
402
403        let chain = ChainProcessor::new(processors);
404        let result = chain.process(10)?;
405
406        // 10 + 5 = 15
407        assert_eq!(result, 15);
408        Ok(())
409    }
410
411    #[test]
412    fn test_chain_processor_multiple_processors() -> Result<(), OCRError> {
413        let processors: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> = vec![
414            Box::new(AddProcessor { value: 5 }),      // 10 + 5 = 15
415            Box::new(MultiplyProcessor { value: 2 }), // 15 * 2 = 30
416            Box::new(AddProcessor { value: 10 }),     // 30 + 10 = 40
417        ];
418
419        let chain = ChainProcessor::new(processors);
420        let result = chain.process(10)?;
421
422        // (10 + 5) * 2 + 10 = 40
423        assert_eq!(result, 40);
424        Ok(())
425    }
426
427    #[test]
428    fn test_chain_processor_empty_chain() {
429        let processors: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> = vec![];
430
431        let chain = ChainProcessor::new(processors);
432        let result = chain.process(10);
433
434        // Should return an error for empty chain
435        assert!(result.is_err());
436        if let Err(OCRError::ConfigError { message }) = result {
437            assert_eq!(message, "Empty processor chain");
438        } else {
439            panic!("Expected ConfigError");
440        }
441    }
442
443    #[test]
444    fn test_chain_processor_name() {
445        let processors: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> =
446            vec![Box::new(AddProcessor { value: 5 })];
447
448        let chain = ChainProcessor::new(processors);
449        assert_eq!(chain.name(), "Chain");
450    }
451
452    #[test]
453    fn test_chain_processor_order_matters() -> Result<(), OCRError> {
454        // Test that processors are applied in order
455        let processors1: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> = vec![
456            Box::new(AddProcessor { value: 5 }),      // 10 + 5 = 15
457            Box::new(MultiplyProcessor { value: 2 }), // 15 * 2 = 30
458        ];
459
460        let processors2: Vec<Box<dyn EdgeProcessor<Input = i32, Output = i32>>> = vec![
461            Box::new(MultiplyProcessor { value: 2 }), // 10 * 2 = 20
462            Box::new(AddProcessor { value: 5 }),      // 20 + 5 = 25
463        ];
464
465        let chain1 = ChainProcessor::new(processors1);
466        let chain2 = ChainProcessor::new(processors2);
467
468        let result1 = chain1.process(10)?;
469        let result2 = chain2.process(10)?;
470
471        // (10 + 5) * 2 = 30
472        assert_eq!(result1, 30);
473        // (10 * 2) + 5 = 25
474        assert_eq!(result2, 25);
475        // Results should be different
476        assert_ne!(result1, result2);
477        Ok(())
478    }
479}