Skip to main content

ppocr_rs/
ocr_utils.rs

1use crate::{
2    ocr_error::OcrError,
3    ocr_result::{Point, TextBox},
4};
5use image::imageops;
6use imageproc::geometric_transformations::{Interpolation, Projection};
7use ndarray::{Array, Array4};
8
9pub struct OcrUtils;
10
11impl OcrUtils {
12    pub fn substract_mean_normalize(
13        img_src: &image::RgbImage,
14        mean_vals: &[f32],
15        norm_vals: &[f32],
16    ) -> Array4<f32> {
17        let cols = img_src.width();
18        let rows = img_src.height();
19        let channels = 3;
20
21        let mut input_tensor = Array::zeros((1, channels as usize, rows as usize, cols as usize));
22
23        // 获取图像数据
24        unsafe {
25            for r in 0..rows {
26                for c in 0..cols {
27                    for ch in 0..channels {
28                        let idx = (r * cols * channels + c * channels + ch) as usize;
29                        let value = img_src.get_unchecked(idx).to_owned();
30                        let data = value as f32 * norm_vals[ch as usize]
31                            - mean_vals[ch as usize] * norm_vals[ch as usize];
32                        input_tensor[[0, ch as usize, r as usize, c as usize]] = data;
33                    }
34                }
35            }
36        }
37
38        input_tensor
39    }
40
41    pub fn make_padding(
42        img_src: &image::RgbImage,
43        padding: u32,
44    ) -> Result<image::RgbImage, OcrError> {
45        if padding == 0 {
46            return Ok(img_src.clone());
47        }
48
49        let width = img_src.width();
50        let height = img_src.height();
51
52        let mut padding_src = image::RgbImage::new(width + 2 * padding, height + 2 * padding);
53        imageproc::drawing::draw_filled_rect_mut(
54            &mut padding_src,
55            imageproc::rect::Rect::at(0, 0).of_size(width + 2 * padding, height + 2 * padding),
56            image::Rgb([255, 255, 255]),
57        );
58
59        image::imageops::replace(&mut padding_src, img_src, padding as i64, padding as i64);
60
61        Ok(padding_src)
62    }
63
64    pub fn get_part_images(
65        img_src: &image::RgbImage,
66        text_boxes: &[TextBox],
67    ) -> Vec<image::RgbImage> {
68        text_boxes
69            .iter()
70            .map(|text_box| Self::get_rotate_crop_image(img_src, &text_box.points))
71            .collect()
72    }
73
74    pub fn get_rotate_crop_image(
75        img_src: &image::RgbImage,
76        box_points: &[Point],
77    ) -> image::RgbImage {
78        let mut points = box_points.to_vec();
79
80        // 计算边界框
81        let (min_x, min_y, max_x, max_y) = points.iter().fold(
82            (u32::MAX, u32::MAX, 0u32, 0u32),
83            |(min_x, min_y, max_x, max_y), point| {
84                (
85                    min_x.min(point.x),
86                    min_y.min(point.y),
87                    max_x.max(point.x),
88                    max_y.max(point.y),
89                )
90            },
91        );
92
93        // 裁剪图像
94        let img_crop =
95            imageops::crop_imm(img_src, min_x, min_y, max_x - min_x, max_y - min_y).to_image();
96
97        for point in &mut points {
98            point.x -= min_x;
99            point.y -= min_y;
100        }
101
102        let img_crop_width = ((points[0].x as i32 - points[1].x as i32).pow(2) as f32
103            + (points[0].y as i32 - points[1].y as i32).pow(2) as f32)
104            .sqrt() as u32;
105        let img_crop_height = ((points[0].x as i32 - points[3].x as i32).pow(2) as f32
106            + (points[0].y as i32 - points[3].y as i32).pow(2) as f32)
107            .sqrt() as u32;
108
109        let src_points = [
110            (points[0].x as f32, points[0].y as f32),
111            (points[1].x as f32, points[1].y as f32),
112            (points[2].x as f32, points[2].y as f32),
113            (points[3].x as f32, points[3].y as f32),
114        ];
115
116        let dst_points = [
117            (0.0, 0.0),
118            (img_crop_width as f32, 0.0),
119            (img_crop_width as f32, img_crop_height as f32),
120            (0.0, img_crop_height as f32),
121        ];
122
123        let projection = Projection::from_control_points(src_points, dst_points)
124            .expect("Failed to create projection transformation");
125
126        let mut part_img = image::RgbImage::new(img_crop_width, img_crop_height);
127        imageproc::geometric_transformations::warp_into(
128            &img_crop,
129            &projection,
130            Interpolation::Bilinear,
131            image::Rgb([255, 255, 255]),
132            &mut part_img,
133        );
134
135        // 根据需要旋转图像
136        if part_img.height() >= part_img.width() * 3 / 2 {
137            let mut rotated = image::RgbImage::new(part_img.height(), part_img.width());
138
139            for (x, y, pixel) in part_img.enumerate_pixels() {
140                rotated.put_pixel(y, part_img.width() - 1 - x, *pixel);
141            }
142
143            rotated
144        } else {
145            part_img
146        }
147    }
148
149    pub fn mat_rotate_clock_wise_180(src: &mut image::RgbImage) {
150        imageops::rotate180_in_place(src);
151    }
152
153    pub fn calculate_mean_with_mask(
154        img: &image::ImageBuffer<image::Luma<f32>, Vec<f32>>,
155        mask: &image::ImageBuffer<image::Luma<u8>, Vec<u8>>,
156    ) -> f32 {
157        let mut sum: f32 = 0.0;
158        let mut mask_count = 0;
159
160        assert_eq!(img.width(), mask.width());
161        assert_eq!(img.height(), mask.height());
162
163        for y in 0..img.height() {
164            for x in 0..img.width() {
165                let mask_value = mask.get_pixel(x, y)[0];
166                if mask_value > 0 {
167                    let pixel = img.get_pixel(x, y);
168                    sum += pixel[0];
169                    mask_count += 1;
170                }
171            }
172        }
173
174        if mask_count == 0 {
175            return 0.0;
176        }
177
178        sum / mask_count as f32
179    }
180
181    /// Centroide (x, y) di un poligono. Usato per associare text-block a
182    /// layout-box (containment via centroid + nearest-neighbor su orphan).
183    /// Ritorna `(0, 0)` se il poligono è vuoto.
184    pub fn polygon_centroid(points: &[Point]) -> (u32, u32) {
185        if points.is_empty() { return (0, 0); }
186        let n = points.len() as u32;
187        let sx: u32 = points.iter().map(|p| p.x).sum();
188        let sy: u32 = points.iter().map(|p| p.y).sum();
189        (sx / n, sy / n)
190    }
191
192    /// Inversa della trasformazione applicata da `get_rotate_crop_image`:
193    /// dato il polygon DBNet della linea (4 corner nello spazio
194    /// dell'immagine originale) e un rettangolo `(crop_w, crop_h)` (il
195    /// rettangolo target del crop+warp), trasforma 4 punti dal CROP-SPACE
196    /// (rettangolo) all'IMAGE-SPACE (polygon).
197    ///
198    /// Usato per riportare i word-box dal CRNN-cropped-line space allo
199    /// spazio dell'immagine originale.
200    ///
201    /// `quad_in_crop` è array di 4 `(x, y)` in `[0..crop_w] × [0..crop_h]`.
202    /// Ritorna 4 `Point` clampati a `u32`. Se la `Projection::invert()`
203    /// fallisce (raro: polygon degenerato), ritorna `None`.
204    ///
205    /// **Nota**: questa funzione NON gestisce la rotazione 90° applicata
206    /// quando `crop_h >= crop_w * 3/2` (vedi `get_rotate_crop_image`).
207    /// Per quelle linee, il chiamante deve saltare il word-level (testo
208    /// verticale, edge case non supportato).
209    pub fn inverse_warp_quad(
210        line_polygon: &[Point; 4],
211        crop_size: (u32, u32),
212        quad_in_crop: &[(f32, f32); 4],
213    ) -> Option<[Point; 4]> {
214        // Replica la stessa logica di get_rotate_crop_image: shift al
215        // (min_x, min_y) del polygon e lavora in coordinate relative.
216        let (min_x, min_y, _, _) = line_polygon.iter().fold(
217            (u32::MAX, u32::MAX, 0u32, 0u32),
218            |(mn_x, mn_y, mx_x, mx_y), p| (mn_x.min(p.x), mn_y.min(p.y), mx_x.max(p.x), mx_y.max(p.y)),
219        );
220        let src_points: [(f32, f32); 4] = [
221            ((line_polygon[0].x - min_x) as f32, (line_polygon[0].y - min_y) as f32),
222            ((line_polygon[1].x - min_x) as f32, (line_polygon[1].y - min_y) as f32),
223            ((line_polygon[2].x - min_x) as f32, (line_polygon[2].y - min_y) as f32),
224            ((line_polygon[3].x - min_x) as f32, (line_polygon[3].y - min_y) as f32),
225        ];
226        let (cw, ch) = (crop_size.0 as f32, crop_size.1 as f32);
227        let dst_points: [(f32, f32); 4] = [
228            (0.0, 0.0),
229            (cw,  0.0),
230            (cw,  ch),
231            (0.0, ch),
232        ];
233
234        // get_rotate_crop_image usa Projection::from_control_points(src, dst)
235        // per andare polygon→rect. Per andare rect→polygon serviamo l'inversa.
236        let proj = imageproc::geometric_transformations::Projection::from_control_points(
237            src_points, dst_points,
238        )?.invert();
239
240        let mut out = [Point { x: 0, y: 0 }; 4];
241        for (i, &(qx, qy)) in quad_in_crop.iter().enumerate() {
242            let (mapped_x, mapped_y) = proj * (qx, qy);
243            // Aggiungi back l'offset (min_x, min_y) per ritornare al sistema
244            // dell'immagine originale.
245            let abs_x = (mapped_x.max(0.0) as u32).saturating_add(min_x);
246            let abs_y = (mapped_y.max(0.0) as u32).saturating_add(min_y);
247            out[i] = Point { x: abs_x, y: abs_y };
248        }
249        Some(out)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn inverse_warp_axis_aligned_roundtrip() {
259        // Polygon axis-aligned 100×30 a (50, 200).
260        let poly = [
261            Point { x: 50,  y: 200 },
262            Point { x: 150, y: 200 },
263            Point { x: 150, y: 230 },
264            Point { x: 50,  y: 230 },
265        ];
266        let crop_size = (200u32, 60u32); // mappato su rect 200×60
267
268        // Quad: il word "Hello" copre x=[40, 80] del crop.
269        let word_quad = [(40.0, 0.0), (80.0, 0.0), (80.0, 60.0), (40.0, 60.0)];
270        let result = OcrUtils::inverse_warp_quad(&poly, crop_size, &word_quad).unwrap();
271
272        // Atteso: x = 50 + (40/200)*100 = 70, x = 50 + (80/200)*100 = 90, y full poly height.
273        assert_eq!(result[0].x, 70);
274        assert_eq!(result[0].y, 200);
275        assert_eq!(result[1].x, 90);
276        assert_eq!(result[1].y, 200);
277        assert_eq!(result[2].x, 90);
278        assert_eq!(result[2].y, 230);
279        assert_eq!(result[3].x, 70);
280        assert_eq!(result[3].y, 230);
281    }
282
283    #[test]
284    fn inverse_warp_corners_match_polygon() {
285        // Quad = full crop → deve mappare ai 4 corner del polygon.
286        let poly = [
287            Point { x: 100, y: 50 },
288            Point { x: 300, y: 60 },
289            Point { x: 295, y: 90 },
290            Point { x: 95,  y: 80 },
291        ];
292        let crop_size = (200u32, 30u32);
293        let full_crop = [(0.0, 0.0), (200.0, 0.0), (200.0, 30.0), (0.0, 30.0)];
294        let result = OcrUtils::inverse_warp_quad(&poly, crop_size, &full_crop).unwrap();
295
296        // Tolerance ±2 px per arrotondamenti f32→u32.
297        for (i, p) in result.iter().enumerate() {
298            let dx = (p.x as i32 - poly[i].x as i32).abs();
299            let dy = (p.y as i32 - poly[i].y as i32).abs();
300            assert!(dx <= 2, "corner {i} x: got {} expected {} (Δ={})", p.x, poly[i].x, dx);
301            assert!(dy <= 2, "corner {i} y: got {} expected {} (Δ={})", p.y, poly[i].y, dy);
302        }
303    }
304}