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