Skip to main content

oar_ocr_core/processors/
uvdoc_postprocess.rs

1//! Document transformation post-processing functionality.
2
3use crate::core::OcrResult;
4use crate::core::errors::OCRError;
5use std::str::FromStr;
6
7/// Post-processor for document transformation results.
8///
9/// The `UVDocPostProcess` struct handles the post-processing of document
10/// transformation model outputs, converting normalized coordinates back
11/// to pixel coordinates and applying various transformations.
12#[derive(Debug)]
13pub struct UVDocPostProcess {
14    /// Scale factor to convert normalized values back to pixel values.
15    pub scale: f32,
16}
17
18impl UVDocPostProcess {
19    /// Creates a new UVDocPostProcess instance.
20    ///
21    /// # Arguments
22    ///
23    /// * `scale` - Scale factor for converting normalized coordinates to pixels.
24    ///
25    /// # Examples
26    ///
27    /// ```rust,no_run
28    /// use oar_ocr_core::processors::UVDocPostProcess;
29    ///
30    /// let postprocessor = UVDocPostProcess::new(1.0);
31    /// ```
32    pub fn new(scale: f32) -> Self {
33        Self { scale }
34    }
35
36    /// Gets the current scale factor.
37    ///
38    /// # Returns
39    ///
40    /// The scale factor used for coordinate conversion.
41    pub fn scale(&self) -> f32 {
42        self.scale
43    }
44
45    /// Sets a new scale factor.
46    ///
47    /// # Arguments
48    ///
49    /// * `scale` - New scale factor.
50    pub fn set_scale(&mut self, scale: f32) {
51        self.scale = scale;
52    }
53
54    /// Converts normalized coordinates to pixel coordinates.
55    ///
56    /// # Arguments
57    ///
58    /// * `normalized_coords` - Vector of normalized coordinates (0.0 to 1.0).
59    ///
60    /// # Returns
61    ///
62    /// * `Vec<f32>` - Vector of pixel coordinates.
63    ///
64    /// # Examples
65    ///
66    /// ```rust,no_run
67    /// use oar_ocr_core::processors::UVDocPostProcess;
68    ///
69    /// let postprocessor = UVDocPostProcess::new(100.0);
70    /// let normalized = vec![0.1, 0.2, 0.8, 0.9];
71    /// let pixels = postprocessor.denormalize_coordinates(&normalized);
72    /// assert_eq!(pixels, vec![10.0, 20.0, 80.0, 90.0]);
73    /// ```
74    pub fn denormalize_coordinates(&self, normalized_coords: &[f32]) -> Vec<f32> {
75        normalized_coords
76            .iter()
77            .map(|&coord| coord * self.scale)
78            .collect()
79    }
80
81    /// Converts pixel coordinates to normalized coordinates.
82    ///
83    /// # Arguments
84    ///
85    /// * `pixel_coords` - Vector of pixel coordinates.
86    ///
87    /// # Returns
88    ///
89    /// * `Vec<f32>` - Vector of normalized coordinates (0.0 to 1.0).
90    ///
91    /// # Examples
92    ///
93    /// ```rust,no_run
94    /// use oar_ocr_core::processors::UVDocPostProcess;
95    ///
96    /// let postprocessor = UVDocPostProcess::new(100.0);
97    /// let pixels = vec![10.0, 20.0, 80.0, 90.0];
98    /// let normalized = postprocessor.normalize_coordinates(&pixels);
99    /// assert_eq!(normalized, vec![0.1, 0.2, 0.8, 0.9]);
100    /// ```
101    pub fn normalize_coordinates(&self, pixel_coords: &[f32]) -> Vec<f32> {
102        if self.scale == 0.0 {
103            return vec![0.0; pixel_coords.len()];
104        }
105        pixel_coords
106            .iter()
107            .map(|&coord| coord / self.scale)
108            .collect()
109    }
110
111    /// Processes a bounding box from normalized to pixel coordinates.
112    ///
113    /// # Arguments
114    ///
115    /// * `bbox` - Bounding box as [x1, y1, x2, y2] in normalized coordinates.
116    ///
117    /// # Returns
118    ///
119    /// * `[f32; 4]` - Bounding box in pixel coordinates.
120    ///
121    /// # Examples
122    ///
123    /// ```rust,no_run
124    /// use oar_ocr_core::processors::UVDocPostProcess;
125    ///
126    /// let postprocessor = UVDocPostProcess::new(100.0);
127    /// let normalized_bbox = [0.1, 0.2, 0.8, 0.9];
128    /// let pixel_bbox = postprocessor.process_bbox(&normalized_bbox);
129    /// assert_eq!(pixel_bbox, [10.0, 20.0, 80.0, 90.0]);
130    /// ```
131    pub fn process_bbox(&self, bbox: &[f32; 4]) -> [f32; 4] {
132        [
133            bbox[0] * self.scale,
134            bbox[1] * self.scale,
135            bbox[2] * self.scale,
136            bbox[3] * self.scale,
137        ]
138    }
139
140    /// Processes multiple bounding boxes.
141    ///
142    /// # Arguments
143    ///
144    /// * `bboxes` - Vector of bounding boxes in normalized coordinates.
145    ///
146    /// # Returns
147    ///
148    /// * `Vec<[f32; 4]>` - Vector of bounding boxes in pixel coordinates.
149    pub fn process_bboxes(&self, bboxes: &[[f32; 4]]) -> Vec<[f32; 4]> {
150        bboxes.iter().map(|bbox| self.process_bbox(bbox)).collect()
151    }
152
153    /// Processes a polygon from normalized to pixel coordinates.
154    ///
155    /// # Arguments
156    ///
157    /// * `polygon` - Vector of points as [x, y] pairs in normalized coordinates.
158    ///
159    /// # Returns
160    ///
161    /// * `Vec<[f32; 2]>` - Vector of points in pixel coordinates.
162    ///
163    /// # Examples
164    ///
165    /// ```rust,no_run
166    /// use oar_ocr_core::processors::UVDocPostProcess;
167    ///
168    /// let postprocessor = UVDocPostProcess::new(100.0);
169    /// let normalized_polygon = vec![[0.1, 0.2], [0.8, 0.2], [0.8, 0.9], [0.1, 0.9]];
170    /// let pixel_polygon = postprocessor.process_polygon(&normalized_polygon);
171    /// assert_eq!(pixel_polygon[0], [10.0, 20.0]);
172    /// ```
173    pub fn process_polygon(&self, polygon: &[[f32; 2]]) -> Vec<[f32; 2]> {
174        polygon
175            .iter()
176            .map(|&[x, y]| [x * self.scale, y * self.scale])
177            .collect()
178    }
179
180    /// Clamps coordinates to valid ranges.
181    ///
182    /// # Arguments
183    ///
184    /// * `coords` - Vector of coordinates to clamp.
185    /// * `min_val` - Minimum allowed value.
186    /// * `max_val` - Maximum allowed value.
187    ///
188    /// # Returns
189    ///
190    /// * `Vec<f32>` - Vector of clamped coordinates.
191    pub fn clamp_coordinates(&self, coords: &[f32], min_val: f32, max_val: f32) -> Vec<f32> {
192        coords
193            .iter()
194            .map(|&coord| coord.clamp(min_val, max_val))
195            .collect()
196    }
197
198    /// Validates that coordinates are within expected ranges.
199    ///
200    /// # Arguments
201    ///
202    /// * `coords` - Vector of coordinates to validate.
203    /// * `min_val` - Minimum expected value.
204    /// * `max_val` - Maximum expected value.
205    ///
206    /// # Returns
207    ///
208    /// * `true` - If all coordinates are within range.
209    /// * `false` - If any coordinate is out of range.
210    pub fn validate_coordinates(&self, coords: &[f32], min_val: f32, max_val: f32) -> bool {
211        coords
212            .iter()
213            .all(|&coord| coord >= min_val && coord <= max_val)
214    }
215
216    /// Rounds coordinates to integer values.
217    ///
218    /// # Arguments
219    ///
220    /// * `coords` - Vector of coordinates to round.
221    ///
222    /// # Returns
223    ///
224    /// * `Vec<i32>` - Vector of rounded integer coordinates.
225    pub fn round_coordinates(&self, coords: &[f32]) -> Vec<i32> {
226        coords.iter().map(|&coord| coord.round() as i32).collect()
227    }
228
229    /// Applies batch processing to tensor output to produce rectified images.
230    ///
231    /// # Arguments
232    ///
233    /// * `output` - 4D tensor output from the model [batch, channels, height, width].
234    ///
235    /// # Returns
236    ///
237    /// * `OcrResult<Vec<image::RgbImage>>` - Vector of rectified images or error.
238    pub fn apply_batch(&self, output: &ndarray::Array4<f32>) -> OcrResult<Vec<image::RgbImage>> {
239        use image::{Rgb, RgbImage};
240
241        let shape = output.shape();
242        if shape.len() != 4 {
243            return Err(OCRError::InvalidInput {
244                message: "Expected 4D tensor [batch, channels, height, width]".to_string(),
245            });
246        }
247
248        let batch_size = shape[0];
249        let channels = shape[1];
250        let height = shape[2];
251        let width = shape[3];
252
253        if channels != 3 {
254            return Err(OCRError::InvalidInput {
255                message: "Expected 3 channels (RGB)".to_string(),
256            });
257        }
258
259        let mut images = Vec::with_capacity(batch_size);
260
261        let scale = self.scale;
262        let plane = height * width;
263
264        for b in 0..batch_size {
265            let mut img = RgbImage::new(width as u32, height as u32);
266
267            // Model outputs are in BGR order; convert back to RGB. For the
268            // common standard-layout tensor the three channel planes are
269            // contiguous, so the SIMD kernel scales + clamps them straight into
270            // the image buffer (no per-pixel `put_pixel`/strided indexing).
271            // Fall back to indexed access for non-standard layouts.
272            match output.as_slice() {
273                Some(buf) => {
274                    let base = b * 3 * plane;
275                    crate::processors::simd::scale_clamp_bgr_planes_to_rgb(
276                        &buf[base..base + plane],
277                        &buf[base + plane..base + 2 * plane],
278                        &buf[base + 2 * plane..base + 3 * plane],
279                        scale,
280                        img.as_mut(),
281                    );
282                }
283                None => {
284                    for y in 0..height {
285                        for x in 0..width {
286                            let b_val = (output[[b, 0, y, x]] * scale).clamp(0.0, 255.0) as u8;
287                            let g_val = (output[[b, 1, y, x]] * scale).clamp(0.0, 255.0) as u8;
288                            let r_val = (output[[b, 2, y, x]] * scale).clamp(0.0, 255.0) as u8;
289                            img.put_pixel(x as u32, y as u32, Rgb([r_val, g_val, b_val]));
290                        }
291                    }
292                }
293            }
294
295            images.push(img);
296        }
297
298        Ok(images)
299    }
300}
301
302impl Default for UVDocPostProcess {
303    /// Creates a default UVDocPostProcess with scale factor 1.0.
304    fn default() -> Self {
305        Self::new(1.0)
306    }
307}
308
309impl FromStr for UVDocPostProcess {
310    type Err = std::num::ParseFloatError;
311
312    /// Creates a UVDocPostProcess from a string representation of the scale factor.
313    ///
314    /// # Arguments
315    ///
316    /// * `s` - String representation of the scale factor.
317    ///
318    /// # Returns
319    ///
320    /// * `Ok(UVDocPostProcess)` - If the string can be parsed as a float.
321    /// * `Err(ParseFloatError)` - If the string cannot be parsed.
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        let scale = s.parse::<f32>()?;
324        Ok(Self::new(scale))
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_denormalize_coordinates() {
334        let postprocessor = UVDocPostProcess::new(100.0);
335        let normalized = vec![0.1, 0.2, 0.8, 0.9];
336        let pixels = postprocessor.denormalize_coordinates(&normalized);
337        assert_eq!(pixels, vec![10.0, 20.0, 80.0, 90.0]);
338    }
339
340    #[test]
341    fn test_normalize_coordinates() {
342        let postprocessor = UVDocPostProcess::new(100.0);
343        let pixels = vec![10.0, 20.0, 80.0, 90.0];
344        let normalized = postprocessor.normalize_coordinates(&pixels);
345        assert_eq!(normalized, vec![0.1, 0.2, 0.8, 0.9]);
346    }
347
348    #[test]
349    fn test_process_bbox() {
350        let postprocessor = UVDocPostProcess::new(100.0);
351        let normalized_bbox = [0.1, 0.2, 0.8, 0.9];
352        let pixel_bbox = postprocessor.process_bbox(&normalized_bbox);
353        assert_eq!(pixel_bbox, [10.0, 20.0, 80.0, 90.0]);
354    }
355
356    #[test]
357    fn test_process_polygon() {
358        let postprocessor = UVDocPostProcess::new(100.0);
359        let normalized_polygon = vec![[0.1, 0.2], [0.8, 0.2], [0.8, 0.9], [0.1, 0.9]];
360        let pixel_polygon = postprocessor.process_polygon(&normalized_polygon);
361        assert_eq!(pixel_polygon[0], [10.0, 20.0]);
362        assert_eq!(pixel_polygon[1], [80.0, 20.0]);
363    }
364
365    #[test]
366    fn test_clamp_coordinates() {
367        let postprocessor = UVDocPostProcess::new(1.0);
368        let coords = vec![-10.0, 50.0, 150.0];
369        let clamped = postprocessor.clamp_coordinates(&coords, 0.0, 100.0);
370        assert_eq!(clamped, vec![0.0, 50.0, 100.0]);
371    }
372
373    #[test]
374    fn test_validate_coordinates() {
375        let postprocessor = UVDocPostProcess::new(1.0);
376        let valid_coords = vec![10.0, 50.0, 90.0];
377        let invalid_coords = vec![10.0, 150.0, 90.0];
378
379        assert!(postprocessor.validate_coordinates(&valid_coords, 0.0, 100.0));
380        assert!(!postprocessor.validate_coordinates(&invalid_coords, 0.0, 100.0));
381    }
382
383    #[test]
384    fn test_round_coordinates() {
385        let postprocessor = UVDocPostProcess::new(1.0);
386        let coords = vec![10.3, 20.7, 30.5];
387        let rounded = postprocessor.round_coordinates(&coords);
388        assert_eq!(rounded, vec![10, 21, 31]);
389    }
390
391    #[test]
392    fn test_from_str() -> Result<(), std::num::ParseFloatError> {
393        let postprocessor: UVDocPostProcess = "2.5".parse()?;
394        assert_eq!(postprocessor.scale(), 2.5);
395
396        assert!("invalid".parse::<UVDocPostProcess>().is_err());
397        Ok(())
398    }
399
400    #[test]
401    fn test_zero_scale_normalize() {
402        let postprocessor = UVDocPostProcess::new(0.0);
403        let pixels = vec![10.0, 20.0];
404        let normalized = postprocessor.normalize_coordinates(&pixels);
405        assert_eq!(normalized, vec![0.0, 0.0]);
406    }
407}