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}