Skip to main content

oar_ocr_core/models/rectification/
uvdoc.rs

1//! UVDoc Document Rectification Model
2//!
3//! This module provides a pure implementation of the UVDoc model for document rectification.
4//! The model takes distorted document images and outputs rectified (flattened) versions.
5
6use crate::core::OCRError;
7use crate::core::inference::{OrtInfer, TensorInput};
8use crate::processors::{NormalizeImage, TensorLayout, UVDocPostProcess};
9use image::{DynamicImage, RgbImage, imageops::FilterType};
10use std::borrow::Cow;
11
12type PreprocessResult = Result<(ndarray::Array4<f32>, Vec<(u32, u32)>), OCRError>;
13
14/// Configuration for UVDoc model preprocessing.
15#[derive(Debug, Clone)]
16pub struct UVDocPreprocessConfig {
17    /// Input shape [channels, height, width]
18    pub rec_image_shape: [usize; 3],
19}
20
21impl Default for UVDocPreprocessConfig {
22    fn default() -> Self {
23        Self {
24            rec_image_shape: [3, 512, 512],
25        }
26    }
27}
28
29/// Output from UVDoc model.
30#[derive(Debug, Clone)]
31pub struct UVDocModelOutput {
32    /// Rectified images
33    pub images: Vec<RgbImage>,
34}
35
36/// Pure UVDoc model implementation.
37///
38/// This model performs document rectification (unwarping) on distorted document images.
39#[derive(Debug)]
40pub struct UVDocModel {
41    /// ONNX Runtime inference engine
42    inference: OrtInfer,
43    /// Image normalizer for preprocessing
44    normalizer: NormalizeImage,
45    /// UVDoc postprocessor for converting tensor to images
46    postprocessor: UVDocPostProcess,
47    /// Input shape [channels, height, width]
48    rec_image_shape: [usize; 3],
49}
50
51impl UVDocModel {
52    /// Creates a new UVDoc model.
53    pub fn new(
54        inference: OrtInfer,
55        normalizer: NormalizeImage,
56        postprocessor: UVDocPostProcess,
57        rec_image_shape: [usize; 3],
58    ) -> Self {
59        Self {
60            inference,
61            normalizer,
62            postprocessor,
63            rec_image_shape,
64        }
65    }
66
67    /// Preprocesses images for rectification.
68    ///
69    /// # Arguments
70    ///
71    /// * `images` - Input images to preprocess
72    ///
73    /// # Returns
74    ///
75    /// A tuple of (batch_tensor, original_sizes)
76    pub fn preprocess(&self, images: Vec<RgbImage>) -> PreprocessResult {
77        let image_refs: Vec<_> = images.iter().collect();
78        self.preprocess_refs(&image_refs)
79    }
80
81    /// Preprocesses borrowed images without cloning full-resolution source pages.
82    pub fn preprocess_refs(&self, images: &[&RgbImage]) -> PreprocessResult {
83        let mut original_sizes = Vec::with_capacity(images.len());
84        let mut processed_images = Vec::with_capacity(images.len());
85
86        let target_height = self.rec_image_shape[1] as u32;
87        let target_width = self.rec_image_shape[2] as u32;
88        let should_resize = target_height > 0 && target_width > 0;
89
90        for &img in images {
91            let original_size = (img.width(), img.height());
92            original_sizes.push(original_size);
93
94            if should_resize && (img.width() != target_width || img.height() != target_height) {
95                // Use cv2.INTER_LINEAR for UVDoc resize.
96                let resized =
97                    image::imageops::resize(img, target_width, target_height, FilterType::Triangle);
98                processed_images.push(Cow::Owned(resized));
99            } else {
100                processed_images.push(Cow::Borrowed(img));
101            }
102        }
103
104        // Normalize and convert to tensor
105        let processed_refs: Vec<_> = processed_images.iter().map(Cow::as_ref).collect();
106        let batch_tensor = self.normalizer.normalize_batch_refs(&processed_refs)?;
107
108        Ok((batch_tensor, original_sizes))
109    }
110
111    /// Runs inference on the preprocessed batch.
112    ///
113    /// # Arguments
114    ///
115    /// * `batch_tensor` - Preprocessed batch tensor
116    ///
117    /// # Returns
118    ///
119    /// Model predictions as a 4D tensor
120    pub fn infer(
121        &self,
122        batch_tensor: &ndarray::Array4<f32>,
123    ) -> Result<ndarray::Array4<f32>, OCRError> {
124        let input_name = self.inference.input_name();
125        let inputs = vec![(input_name, TensorInput::Array4(batch_tensor))];
126
127        let outputs = self
128            .inference
129            .infer(&inputs)
130            .map_err(|e| OCRError::Inference {
131                model_name: "UVDoc".to_string(),
132                context: format!(
133                    "failed to run inference on batch with shape {:?}",
134                    batch_tensor.shape()
135                ),
136                source: Box::new(e),
137            })?;
138
139        let output = outputs
140            .into_iter()
141            .next()
142            .ok_or_else(|| OCRError::InvalidInput {
143                message: "UVDoc: no output returned from inference".to_string(),
144            })?;
145
146        output
147            .1
148            .try_into_array4_f32()
149            .map_err(|e| OCRError::Inference {
150                model_name: "UVDoc".to_string(),
151                context: "failed to convert output to 4D array".to_string(),
152                source: Box::new(e),
153            })
154    }
155
156    /// Postprocesses model predictions to rectified images.
157    ///
158    /// # Arguments
159    ///
160    /// * `predictions` - Model predictions
161    /// * `original_sizes` - Original image sizes (width, height)
162    ///
163    /// # Returns
164    ///
165    /// Rectified images resized to original dimensions
166    pub fn postprocess(
167        &self,
168        predictions: &ndarray::Array4<f32>,
169        original_sizes: &[(u32, u32)],
170    ) -> Result<Vec<RgbImage>, OCRError> {
171        // Use UVDocPostProcess to convert tensor to images
172        let mut images =
173            self.postprocessor
174                .apply_batch(predictions)
175                .map_err(|e| OCRError::ConfigError {
176                    message: format!("Failed to postprocess rectification output: {}", e),
177                })?;
178
179        if images.len() != original_sizes.len() {
180            return Err(OCRError::InvalidInput {
181                message: format!(
182                    "Mismatched rectification batch sizes: predictions={}, originals={}",
183                    images.len(),
184                    original_sizes.len()
185                ),
186            });
187        }
188
189        // Resize back to original dimensions
190        for (img, &(orig_w, orig_h)) in images.iter_mut().zip(original_sizes) {
191            if orig_w == 0 || orig_h == 0 {
192                continue;
193            }
194
195            if img.width() != orig_w || img.height() != orig_h {
196                // Use cv2.INTER_LINEAR for resizing outputs back to original size.
197                let resized = DynamicImage::ImageRgb8(std::mem::take(img)).resize_exact(
198                    orig_w,
199                    orig_h,
200                    FilterType::Triangle,
201                );
202                *img = resized.into_rgb8();
203            }
204        }
205
206        Ok(images)
207    }
208
209    /// Performs complete forward pass: preprocess -> infer -> postprocess.
210    ///
211    /// # Arguments
212    ///
213    /// * `images` - Input images to rectify
214    ///
215    /// # Returns
216    ///
217    /// UVDocModelOutput containing rectified images
218    pub fn forward(&self, images: Vec<RgbImage>) -> Result<UVDocModelOutput, OCRError> {
219        let image_refs: Vec<_> = images.iter().collect();
220        self.forward_refs(&image_refs)
221    }
222
223    /// Runs rectification on borrowed images without cloning shared page buffers.
224    pub fn forward_refs(&self, images: &[&RgbImage]) -> Result<UVDocModelOutput, OCRError> {
225        let (batch_tensor, original_sizes) = self.preprocess_refs(images)?;
226        let predictions = self.infer(&batch_tensor)?;
227        let rectified_images = self.postprocess(&predictions, &original_sizes)?;
228
229        Ok(UVDocModelOutput {
230            images: rectified_images,
231        })
232    }
233}
234
235/// Builder for UVDoc model.
236#[derive(Debug, Default)]
237pub struct UVDocModelBuilder {
238    /// Preprocessing configuration
239    preprocess_config: UVDocPreprocessConfig,
240    /// ONNX Runtime session configuration
241    ort_config: Option<crate::core::config::OrtSessionConfig>,
242}
243
244impl UVDocModelBuilder {
245    /// Creates a new UVDoc model builder.
246    pub fn new() -> Self {
247        Self {
248            preprocess_config: UVDocPreprocessConfig::default(),
249            ort_config: None,
250        }
251    }
252
253    /// Sets the preprocessing configuration.
254    pub fn preprocess_config(mut self, config: UVDocPreprocessConfig) -> Self {
255        self.preprocess_config = config;
256        self
257    }
258
259    /// Sets the input image shape.
260    pub fn rec_image_shape(mut self, shape: [usize; 3]) -> Self {
261        self.preprocess_config.rec_image_shape = shape;
262        self
263    }
264
265    /// Sets the ONNX Runtime session configuration.
266    pub fn with_ort_config(mut self, config: crate::core::config::OrtSessionConfig) -> Self {
267        self.ort_config = Some(config);
268        self
269    }
270
271    /// Builds the UVDoc model.
272    ///
273    /// # Arguments
274    ///
275    /// * `model_source` - Path to the ONNX model file
276    ///
277    /// # Returns
278    ///
279    /// A configured UVDoc model instance
280    pub fn build(
281        self,
282        model_source: impl Into<crate::core::ModelSource>,
283    ) -> Result<UVDocModel, OCRError> {
284        // Create ONNX inference engine
285        let inference = if self.ort_config.is_some() {
286            use crate::core::config::ModelInferenceConfig;
287            let common_config = ModelInferenceConfig {
288                ort_session: self.ort_config,
289                ..Default::default()
290            };
291            OrtInfer::from_config(&common_config, model_source, Some("image"))?
292        } else {
293            OrtInfer::new(model_source, Some("image"))?
294        };
295
296        // Create normalizer (scale to [0, 1] without mean shift).
297        let normalizer = NormalizeImage::with_color_order(
298            Some(1.0 / 255.0),
299            Some(vec![0.0, 0.0, 0.0]),
300            Some(vec![1.0, 1.0, 1.0]),
301            Some(TensorLayout::CHW),
302            Some(crate::processors::types::ColorOrder::BGR),
303        )?;
304
305        // Create postprocessor
306        let postprocessor = UVDocPostProcess::new(255.0);
307
308        Ok(UVDocModel::new(
309            inference,
310            normalizer,
311            postprocessor,
312            self.preprocess_config.rec_image_shape,
313        ))
314    }
315}