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