Skip to main content

oar_ocr/oarocr/
processors.rs

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