Skip to main content

ocr_rs/
preprocess.rs

1//! Image Preprocessing Utilities
2//!
3//! Provides various image preprocessing functions required for OCR
4
5use image::{DynamicImage, GenericImageView, RgbImage};
6use ndarray::{Array4, ArrayBase, Dim, OwnedRepr};
7
8use crate::error::{OcrError, OcrResult};
9
10/// Image normalization parameters
11#[derive(Debug, Clone)]
12pub struct NormalizeParams {
13    /// RGB channel means
14    pub mean: [f32; 3],
15    /// RGB channel standard deviations
16    pub std: [f32; 3],
17}
18
19impl Default for NormalizeParams {
20    fn default() -> Self {
21        // ImageNet normalization parameters
22        Self {
23            mean: [0.485, 0.456, 0.406],
24            std: [0.229, 0.224, 0.225],
25        }
26    }
27}
28
29impl NormalizeParams {
30    /// Normalization parameters for PaddleOCR detection model
31    pub fn paddle_det() -> Self {
32        Self {
33            mean: [0.485, 0.456, 0.406],
34            std: [0.229, 0.224, 0.225],
35        }
36    }
37
38    /// Normalization parameters for PaddleOCR recognition model
39    pub fn paddle_rec() -> Self {
40        Self {
41            mean: [0.5, 0.5, 0.5],
42            std: [0.5, 0.5, 0.5],
43        }
44    }
45}
46
47/// Calculate size to pad to (multiple of 32)
48#[inline]
49pub fn get_padded_size(size: u32) -> u32 {
50    ((size + 31) / 32) * 32
51}
52
53/// Scale image to specified maximum side length
54///
55/// Maintains aspect ratio, scales longest side to max_side_len
56pub fn resize_to_max_side(img: &DynamicImage, max_side_len: u32) -> OcrResult<DynamicImage> {
57    let (w, h) = img.dimensions();
58    let max_dim = w.max(h);
59
60    if max_dim <= max_side_len {
61        return Ok(img.clone());
62    }
63
64    let scale = max_side_len as f64 / max_dim as f64;
65    let new_w = (w as f64 * scale).round() as u32;
66    let new_h = (h as f64 * scale).round() as u32;
67
68    fast_resize(img, new_w, new_h)
69}
70
71/// Scale image to specified height (for recognition model)
72///
73/// Scales maintaining aspect ratio
74pub fn resize_to_height(img: &DynamicImage, target_height: u32) -> OcrResult<DynamicImage> {
75    let (w, h) = img.dimensions();
76
77    if h == target_height {
78        return Ok(img.clone());
79    }
80
81    let scale = target_height as f64 / h as f64;
82    let new_w = (w as f64 * scale).round() as u32;
83
84    fast_resize(img, new_w, target_height)
85}
86
87/// Fast image resizing using fast_image_resize
88/// Can pass DynamicImage directly when "image" feature is enabled
89fn fast_resize(img: &DynamicImage, new_w: u32, new_h: u32) -> OcrResult<DynamicImage> {
90    use fast_image_resize::{images::Image, IntoImageView, PixelType, Resizer};
91
92    // Only U8x3 (RGB) and U8x4 (RGBA) are handled end-to-end.
93    // Grayscale (U8), 16-bit, and other formats must be converted to RGB first;
94    // otherwise the output buffer byte count would not match the expected channel count.
95    let converted: DynamicImage;
96    let (src, pixel_type) = match img.pixel_type() {
97        Some(PixelType::U8x3) => (img, PixelType::U8x3),
98        Some(PixelType::U8x4) => (img, PixelType::U8x4),
99        _ => {
100            converted = DynamicImage::ImageRgb8(img.to_rgb8());
101            (&converted, PixelType::U8x3)
102        }
103    };
104
105    // Create destination image container
106    let mut dst_image = Image::new(new_w, new_h, pixel_type);
107
108    // Resize using Resizer
109    let mut resizer = Resizer::new();
110    resizer
111        .resize(src, &mut dst_image, None)
112        .map_err(|e| OcrError::PreprocessError(format!("Image resize failed: {e}")))?;
113
114    // Convert result back to DynamicImage
115    match pixel_type {
116        PixelType::U8x3 => RgbImage::from_raw(new_w, new_h, dst_image.into_vec())
117            .map(DynamicImage::ImageRgb8)
118            .ok_or_else(|| {
119                OcrError::PreprocessError("RGB buffer size mismatch after resize".into())
120            }),
121        PixelType::U8x4 => image::RgbaImage::from_raw(new_w, new_h, dst_image.into_vec())
122            .map(DynamicImage::ImageRgba8)
123            .ok_or_else(|| {
124                OcrError::PreprocessError("RGBA buffer size mismatch after resize".into())
125            }),
126        _ => unreachable!("pixel_type is constrained to U8x3 or U8x4 above"),
127    }
128}
129
130/// Convert image to detection model input tensor
131///
132/// Output format: [1, 3, H, W] (NCHW)
133pub fn preprocess_for_det(
134    img: &DynamicImage,
135    params: &NormalizeParams,
136) -> OcrResult<ArrayBase<OwnedRepr<f32>, Dim<[usize; 4]>>> {
137    let (w, h) = img.dimensions();
138    let pad_w = get_padded_size(w) as usize;
139    let pad_h = get_padded_size(h) as usize;
140    let (w, h) = (w as usize, h as usize);
141
142    let mut input = Array4::<f32>::zeros((1, 3, pad_h, pad_w));
143    let rgb_img = img.to_rgb8();
144    let rgb = rgb_img.as_raw();
145
146    let plane_size = pad_h * pad_w;
147    let data = input
148        .as_slice_mut()
149        .expect("Array4 created by zeros should be contiguous");
150    let scales = [
151        1.0 / (255.0 * params.std[0]),
152        1.0 / (255.0 * params.std[1]),
153        1.0 / (255.0 * params.std[2]),
154    ];
155    let offsets = [
156        -params.mean[0] / params.std[0],
157        -params.mean[1] / params.std[1],
158        -params.mean[2] / params.std[2],
159    ];
160
161    // Normalize and pad
162    for y in 0..h {
163        let src_row = &rgb[y * w * 3..(y + 1) * w * 3];
164        let dst_row = y * pad_w;
165
166        for (x, pixel) in src_row.chunks_exact(3).enumerate() {
167            let dst = dst_row + x;
168            data[dst] = pixel[0] as f32 * scales[0] + offsets[0];
169            data[plane_size + dst] = pixel[1] as f32 * scales[1] + offsets[1];
170            data[plane_size * 2 + dst] = pixel[2] as f32 * scales[2] + offsets[2];
171        }
172    }
173
174    Ok(input)
175}
176
177/// Convert image to recognition model input tensor
178///
179/// Output format: [1, 3, H, W] (NCHW)
180/// Height is fixed at 48 (or specified value), width scaled proportionally
181pub fn preprocess_for_rec(
182    img: &DynamicImage,
183    target_height: u32,
184    params: &NormalizeParams,
185) -> OcrResult<ArrayBase<OwnedRepr<f32>, Dim<[usize; 4]>>> {
186    let (_, h) = img.dimensions();
187
188    let resized = if h == target_height {
189        img.clone()
190    } else {
191        resize_to_height(img, target_height)?
192    };
193
194    let rgb_img = resized.to_rgb8();
195    let rgb = rgb_img.as_raw();
196    let (w, h) = (rgb_img.width() as usize, rgb_img.height() as usize);
197
198    let mut input = Array4::<f32>::zeros((1, 3, h, w));
199    let plane_size = h * w;
200    let data = input
201        .as_slice_mut()
202        .expect("Array4 created by zeros should be contiguous");
203    let scales = [
204        1.0 / (255.0 * params.std[0]),
205        1.0 / (255.0 * params.std[1]),
206        1.0 / (255.0 * params.std[2]),
207    ];
208    let offsets = [
209        -params.mean[0] / params.std[0],
210        -params.mean[1] / params.std[1],
211        -params.mean[2] / params.std[2],
212    ];
213
214    for y in 0..h {
215        let src_row = &rgb[y * w * 3..(y + 1) * w * 3];
216        let dst_row = y * w;
217
218        for (x, pixel) in src_row.chunks_exact(3).enumerate() {
219            let dst = dst_row + x;
220            data[dst] = pixel[0] as f32 * scales[0] + offsets[0];
221            data[plane_size + dst] = pixel[1] as f32 * scales[1] + offsets[1];
222            data[plane_size * 2 + dst] = pixel[2] as f32 * scales[2] + offsets[2];
223        }
224    }
225
226    Ok(input)
227}
228
229/// Batch preprocess recognition images
230///
231/// Process multiple images into batch tensor, all images padded to same width
232pub fn preprocess_batch_for_rec(
233    images: &[DynamicImage],
234    target_height: u32,
235    params: &NormalizeParams,
236) -> OcrResult<ArrayBase<OwnedRepr<f32>, Dim<[usize; 4]>>> {
237    if images.is_empty() {
238        return Ok(Array4::<f32>::zeros((0, 3, target_height as usize, 0)));
239    }
240
241    // Calculate scaled width for all images
242    let widths: Vec<u32> = images
243        .iter()
244        .map(|img| {
245            let (w, h) = img.dimensions();
246            let scale = target_height as f64 / h as f64;
247            (w as f64 * scale).round() as u32
248        })
249        .collect();
250
251    // widths is non-empty because images is non-empty (checked above)
252    let max_width = *widths.iter().max().unwrap() as usize;
253    let batch_size = images.len();
254
255    let mut batch = Array4::<f32>::zeros((batch_size, 3, target_height as usize, max_width));
256    let sample_size = 3 * target_height as usize * max_width;
257    let plane_size = target_height as usize * max_width;
258    let data = batch
259        .as_slice_mut()
260        .expect("Array4 created by zeros should be contiguous");
261    let scales = [
262        1.0 / (255.0 * params.std[0]),
263        1.0 / (255.0 * params.std[1]),
264        1.0 / (255.0 * params.std[2]),
265    ];
266    let offsets = [
267        -params.mean[0] / params.std[0],
268        -params.mean[1] / params.std[1],
269        -params.mean[2] / params.std[2],
270    ];
271
272    for (i, img) in images.iter().enumerate() {
273        let resized = resize_to_height(img, target_height)?;
274        let rgb_img = resized.to_rgb8();
275        let rgb = rgb_img.as_raw();
276        let w = rgb_img.width() as usize;
277        let h = target_height as usize;
278        let sample_offset = i * sample_size;
279
280        for y in 0..h {
281            let src_row = &rgb[y * w * 3..(y + 1) * w * 3];
282            let dst_row = y * max_width;
283
284            for (x, pixel) in src_row.chunks_exact(3).enumerate() {
285                let dst = sample_offset + dst_row + x;
286                data[dst] = pixel[0] as f32 * scales[0] + offsets[0];
287                data[sample_offset + plane_size + dst_row + x] =
288                    pixel[1] as f32 * scales[1] + offsets[1];
289                data[sample_offset + plane_size * 2 + dst_row + x] =
290                    pixel[2] as f32 * scales[2] + offsets[2];
291            }
292        }
293    }
294
295    Ok(batch)
296}
297
298/// Crop image region
299pub fn crop_image(img: &DynamicImage, x: u32, y: u32, width: u32, height: u32) -> DynamicImage {
300    img.crop_imm(x, y, width, height)
301}
302
303/// Split image into blocks (for high precision mode)
304///
305/// # Parameters
306/// - `img`: Input image
307/// - `block_size`: Block size
308/// - `overlap`: Overlap region size
309///
310/// # Returns
311/// List of block images and their positions in original image (x, y)
312pub fn split_into_blocks(
313    img: &DynamicImage,
314    block_size: u32,
315    overlap: u32,
316) -> Vec<(DynamicImage, u32, u32)> {
317    let (width, height) = img.dimensions();
318    let mut blocks = Vec::new();
319
320    let step = block_size - overlap;
321
322    let mut y = 0u32;
323    while y < height {
324        let mut x = 0u32;
325        while x < width {
326            let block_w = (block_size).min(width - x);
327            let block_h = (block_size).min(height - y);
328
329            let block = img.crop_imm(x, y, block_w, block_h);
330            blocks.push((block, x, y));
331
332            x += step;
333            if x + overlap >= width && x < width {
334                break;
335            }
336        }
337
338        y += step;
339        if y + overlap >= height && y < height {
340            break;
341        }
342    }
343
344    blocks
345}
346
347/// Convert grayscale mask to binary mask
348pub fn threshold_mask(mask: &[f32], threshold: f32) -> Vec<u8> {
349    mask.iter()
350        .map(|&v| if v > threshold { 255u8 } else { 0u8 })
351        .collect()
352}
353
354/// Create grayscale image
355pub fn create_gray_image(data: &[u8], width: u32, height: u32) -> image::GrayImage {
356    image::GrayImage::from_raw(width, height, data.to_vec())
357        .unwrap_or_else(|| image::GrayImage::new(width, height))
358}
359
360/// Convert image to RGB
361pub fn to_rgb(img: &DynamicImage) -> RgbImage {
362    img.to_rgb8()
363}
364
365/// Create image from RGB data
366pub fn rgb_to_image(data: &[u8], width: u32, height: u32) -> DynamicImage {
367    let rgb = RgbImage::from_raw(width, height, data.to_vec())
368        .unwrap_or_else(|| RgbImage::new(width, height));
369    DynamicImage::ImageRgb8(rgb)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_padded_size() {
378        assert_eq!(get_padded_size(100), 128);
379        assert_eq!(get_padded_size(32), 32);
380        assert_eq!(get_padded_size(33), 64);
381        assert_eq!(get_padded_size(0), 0);
382        assert_eq!(get_padded_size(1), 32);
383        assert_eq!(get_padded_size(31), 32);
384        assert_eq!(get_padded_size(64), 64);
385        assert_eq!(get_padded_size(65), 96);
386    }
387
388    #[test]
389    fn test_normalize_params() {
390        let params = NormalizeParams::default();
391        assert_eq!(params.mean[0], 0.485);
392
393        let paddle = NormalizeParams::paddle_det();
394        assert_eq!(paddle.mean[0], 0.485);
395        assert_eq!(paddle.std[0], 0.229);
396    }
397
398    #[test]
399    fn test_normalize_params_paddle_rec() {
400        let params = NormalizeParams::paddle_rec();
401        assert_eq!(params.mean[0], 0.5);
402        assert_eq!(params.mean[1], 0.5);
403        assert_eq!(params.mean[2], 0.5);
404        assert_eq!(params.std[0], 0.5);
405        assert_eq!(params.std[1], 0.5);
406        assert_eq!(params.std[2], 0.5);
407    }
408
409    #[test]
410    fn test_resize_to_max_side_no_resize() {
411        let img = DynamicImage::new_rgb8(100, 50);
412        let resized = resize_to_max_side(&img, 200).unwrap();
413
414        // 图像已经小于最大边,不应该缩放
415        assert_eq!(resized.width(), 100);
416        assert_eq!(resized.height(), 50);
417    }
418
419    #[test]
420    fn test_resize_to_max_side_width_limited() {
421        let img = DynamicImage::new_rgb8(1000, 500);
422        let resized = resize_to_max_side(&img, 500).unwrap();
423
424        // 宽度是最大边,应该缩放到 500
425        assert_eq!(resized.width(), 500);
426        assert_eq!(resized.height(), 250);
427    }
428
429    #[test]
430    fn test_resize_to_max_side_height_limited() {
431        let img = DynamicImage::new_rgb8(500, 1000);
432        let resized = resize_to_max_side(&img, 500).unwrap();
433
434        // 高度是最大边,应该缩放到 500
435        assert_eq!(resized.width(), 250);
436        assert_eq!(resized.height(), 500);
437    }
438
439    #[test]
440    fn test_resize_to_height() {
441        let img = DynamicImage::new_rgb8(200, 100);
442        let resized = resize_to_height(&img, 48).unwrap();
443
444        assert_eq!(resized.height(), 48);
445        // 宽度应该按比例缩放: 200 * 48/100 = 96
446        assert_eq!(resized.width(), 96);
447    }
448
449    #[test]
450    fn test_resize_to_height_no_resize() {
451        let img = DynamicImage::new_rgb8(200, 48);
452        let resized = resize_to_height(&img, 48).unwrap();
453
454        // 高度已经是目标高度,不应该缩放
455        assert_eq!(resized.height(), 48);
456        assert_eq!(resized.width(), 200);
457    }
458
459    #[test]
460    fn test_preprocess_for_det_shape() {
461        let img = DynamicImage::new_rgb8(100, 50);
462        let params = NormalizeParams::paddle_det();
463        let tensor = preprocess_for_det(&img, &params).unwrap();
464
465        // 输出形状应该是 [1, 3, H, W],H 和 W 是 32 的倍数
466        assert_eq!(tensor.shape()[0], 1);
467        assert_eq!(tensor.shape()[1], 3);
468        assert_eq!(tensor.shape()[2], 64); // 50 向上取整到 64
469        assert_eq!(tensor.shape()[3], 128); // 100 向上取整到 128
470    }
471
472    #[test]
473    fn test_preprocess_for_rec_shape() {
474        let img = DynamicImage::new_rgb8(200, 100);
475        let params = NormalizeParams::paddle_rec();
476        let tensor = preprocess_for_rec(&img, 48, &params).unwrap();
477
478        // 输出高度应该是 48
479        assert_eq!(tensor.shape()[0], 1);
480        assert_eq!(tensor.shape()[1], 3);
481        assert_eq!(tensor.shape()[2], 48);
482        // 宽度应该按比例缩放: 200 * 48/100 = 96
483        assert_eq!(tensor.shape()[3], 96);
484    }
485
486    #[test]
487    fn test_preprocess_batch_for_rec_empty() {
488        let images: Vec<DynamicImage> = vec![];
489        let params = NormalizeParams::paddle_rec();
490        let tensor = preprocess_batch_for_rec(&images, 48, &params).unwrap();
491
492        assert_eq!(tensor.shape()[0], 0);
493    }
494
495    #[test]
496    fn test_preprocess_batch_for_rec_single() {
497        let images = vec![DynamicImage::new_rgb8(200, 100)];
498        let params = NormalizeParams::paddle_rec();
499        let tensor = preprocess_batch_for_rec(&images, 48, &params).unwrap();
500
501        assert_eq!(tensor.shape()[0], 1);
502        assert_eq!(tensor.shape()[1], 3);
503        assert_eq!(tensor.shape()[2], 48);
504    }
505
506    #[test]
507    fn test_preprocess_batch_for_rec_multiple() {
508        let images = vec![
509            DynamicImage::new_rgb8(200, 100),
510            DynamicImage::new_rgb8(300, 100),
511        ];
512        let params = NormalizeParams::paddle_rec();
513        let tensor = preprocess_batch_for_rec(&images, 48, &params).unwrap();
514
515        assert_eq!(tensor.shape()[0], 2);
516        assert_eq!(tensor.shape()[1], 3);
517        assert_eq!(tensor.shape()[2], 48);
518        // 宽度应该是最大宽度: max(96, 144) = 144
519        assert_eq!(tensor.shape()[3], 144);
520    }
521
522    #[test]
523    fn test_crop_image() {
524        let img = DynamicImage::new_rgb8(200, 100);
525        let cropped = crop_image(&img, 50, 25, 100, 50);
526
527        assert_eq!(cropped.width(), 100);
528        assert_eq!(cropped.height(), 50);
529    }
530
531    #[test]
532    fn test_split_into_blocks() {
533        let img = DynamicImage::new_rgb8(500, 500);
534        let blocks = split_into_blocks(&img, 200, 50);
535
536        // 应该有多个块
537        assert!(!blocks.is_empty());
538
539        // 每个块的位置应该记录正确
540        for (block, x, y) in &blocks {
541            assert!(block.width() <= 200);
542            assert!(block.height() <= 200);
543            assert!(*x < 500);
544            assert!(*y < 500);
545        }
546    }
547
548    #[test]
549    fn test_split_into_blocks_small_image() {
550        let img = DynamicImage::new_rgb8(100, 100);
551        let blocks = split_into_blocks(&img, 200, 50);
552
553        // 图像小于块大小,应该只有一个块
554        assert_eq!(blocks.len(), 1);
555        assert_eq!(blocks[0].1, 0); // x offset
556        assert_eq!(blocks[0].2, 0); // y offset
557    }
558
559    #[test]
560    fn test_threshold_mask() {
561        let mask = vec![0.1, 0.3, 0.5, 0.7, 0.9];
562        let binary = threshold_mask(&mask, 0.5);
563
564        assert_eq!(binary, vec![0, 0, 0, 255, 255]);
565    }
566
567    #[test]
568    fn test_threshold_mask_all_below() {
569        let mask = vec![0.1, 0.2, 0.3, 0.4];
570        let binary = threshold_mask(&mask, 0.5);
571
572        assert_eq!(binary, vec![0, 0, 0, 0]);
573    }
574
575    #[test]
576    fn test_threshold_mask_all_above() {
577        let mask = vec![0.6, 0.7, 0.8, 0.9];
578        let binary = threshold_mask(&mask, 0.5);
579
580        assert_eq!(binary, vec![255, 255, 255, 255]);
581    }
582
583    #[test]
584    fn test_create_gray_image() {
585        let data = vec![128u8; 100];
586        let gray = create_gray_image(&data, 10, 10);
587
588        assert_eq!(gray.width(), 10);
589        assert_eq!(gray.height(), 10);
590    }
591
592    #[test]
593    fn test_to_rgb() {
594        let img = DynamicImage::new_rgb8(100, 50);
595        let rgb = to_rgb(&img);
596
597        assert_eq!(rgb.width(), 100);
598        assert_eq!(rgb.height(), 50);
599    }
600
601    #[test]
602    fn test_rgb_to_image() {
603        let data = vec![128u8; 300]; // 10x10 RGB
604        let img = rgb_to_image(&data, 10, 10);
605
606        assert_eq!(img.width(), 10);
607        assert_eq!(img.height(), 10);
608    }
609}