1use crate::core::OCRError;
4use crate::processors::BoundingBox;
5use crate::utils::transform::{Point2f, get_rotate_crop_image};
6use image::{RgbImage, imageops};
7
8pub struct ImageProcessor;
10
11impl ImageProcessor {
12 pub fn crop_bounding_box(image: &RgbImage, bbox: &BoundingBox) -> Result<RgbImage, OCRError> {
27 if bbox.points.is_empty() {
29 return Err(OCRError::image_processing_error("Empty bounding box"));
30 }
31
32 let min_x = bbox
34 .points
35 .iter()
36 .map(|p| p.x)
37 .fold(f32::INFINITY, f32::min)
38 .max(0.0);
39 let max_x = bbox
40 .points
41 .iter()
42 .map(|p| p.x)
43 .fold(f32::NEG_INFINITY, f32::max);
44 let min_y = bbox
45 .points
46 .iter()
47 .map(|p| p.y)
48 .fold(f32::INFINITY, f32::min)
49 .max(0.0);
50 let max_y = bbox
51 .points
52 .iter()
53 .map(|p| p.y)
54 .fold(f32::NEG_INFINITY, f32::max);
55
56 let x1 = (min_x as u32).min(image.width().saturating_sub(1));
58 let y1 = (min_y as u32).min(image.height().saturating_sub(1));
59 let x2 = (max_x as u32).min(image.width());
60 let y2 = (max_y as u32).min(image.height());
61
62 if x2 <= x1 || y2 <= y1 {
64 return Err(OCRError::image_processing_error(format!(
65 "Invalid crop region: ({x1}, {y1}) to ({x2}, {y2})"
66 )));
67 }
68
69 let coords = (x1, y1, x2, y2);
70 Ok(Self::slice_rgb_image(image, coords))
71 }
72
73 fn slice_rgb_image(img: &RgbImage, coords: (u32, u32, u32, u32)) -> RgbImage {
88 let (x1, y1, x2, y2) = coords;
89 let width = x2 - x1;
90 let height = y2 - y1;
91 imageops::crop_imm(img, x1, y1, width, height).to_image()
93 }
94
95 pub fn batch_crop_bounding_boxes(
111 image: &RgbImage,
112 bboxes: &[BoundingBox],
113 ) -> Vec<Result<RgbImage, OCRError>> {
114 bboxes
115 .iter()
116 .map(|bbox| Self::crop_bounding_box(image, bbox))
117 .collect()
118 }
119
120 #[allow(dead_code)]
134 pub fn batch_crop_rotated_bounding_boxes(
135 image: &RgbImage,
136 bboxes: &[BoundingBox],
137 ) -> Vec<Result<RgbImage, OCRError>> {
138 bboxes
139 .iter()
140 .map(|bbox| Self::crop_rotated_bounding_box(image, bbox))
141 .collect()
142 }
143
144 pub fn crop_rotated_bounding_box(
160 image: &RgbImage,
161 bbox: &BoundingBox,
162 ) -> Result<RgbImage, OCRError> {
163 if bbox.points.len() != 4 {
165 return Err(OCRError::image_processing_error(format!(
166 "Bounding box must have exactly 4 points, got {}",
167 bbox.points.len()
168 )));
169 }
170
171 let box_points: Vec<Point2f> = bbox.points.iter().map(|p| Point2f::new(p.x, p.y)).collect();
173
174 if let [p0, p1, p2, p3] = &box_points[..] {
176 let is_axis_aligned = (p0.y == p1.y && p2.y == p3.y && p0.x == p3.x && p1.x == p2.x)
177 || (p0.x == p1.x && p2.x == p3.x && p0.y == p3.y && p1.y == p2.y);
178 if is_axis_aligned {
179 let min_x = p0.x.min(p1.x).min(p2.x).min(p3.x).max(0.0) as u32;
180 let min_y = p0.y.min(p1.y).min(p2.y).min(p3.y).max(0.0) as u32;
181 let max_x = p0.x.max(p1.x).max(p2.x).max(p3.x).min(image.width() as f32) as u32;
182 let max_y =
183 p0.y.max(p1.y)
184 .max(p2.y)
185 .max(p3.y)
186 .min(image.height() as f32) as u32;
187 if max_x > min_x && max_y > min_y {
188 use image::imageops;
189 let w = max_x - min_x;
190 let h = max_y - min_y;
191 return Ok(imageops::crop_imm(image, min_x, min_y, w, h).to_image());
192 }
193 }
194 }
195
196 get_rotate_crop_image(image, &box_points)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::processors::Point;
205 use image::{ImageBuffer, Rgb};
206
207 fn create_test_image(width: u32, height: u32) -> RgbImage {
208 let mut img = ImageBuffer::new(width, height);
209 for y in 0..height {
210 for x in 0..width {
211 let r = (x * 255 / width.max(1)) as u8;
213 let g = (y * 255 / height.max(1)) as u8;
214 let b = 128;
215 img.put_pixel(x, y, Rgb([r, g, b]));
216 }
217 }
218 img
219 }
220
221 #[test]
222 fn test_crop_bounding_box_valid_rectangle() {
223 let img = create_test_image(100, 100);
224 let bbox = BoundingBox {
225 points: vec![
226 Point { x: 10.0, y: 10.0 },
227 Point { x: 50.0, y: 10.0 },
228 Point { x: 50.0, y: 40.0 },
229 Point { x: 10.0, y: 40.0 },
230 ],
231 };
232
233 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
234 assert!(result.is_ok());
235
236 let cropped = result.unwrap();
237 assert_eq!(cropped.width(), 40); assert_eq!(cropped.height(), 30); }
240
241 #[test]
242 fn test_crop_bounding_box_empty_points() {
243 let img = create_test_image(100, 100);
244 let bbox = BoundingBox { points: vec![] };
245
246 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
247 assert!(result.is_err());
248
249 let error_msg = result.unwrap_err().to_string();
250 assert!(error_msg.contains("Empty bounding box"));
251 }
252
253 #[test]
254 fn test_crop_bounding_box_single_point() {
255 let img = create_test_image(100, 100);
256 let bbox = BoundingBox {
257 points: vec![Point { x: 50.0, y: 50.0 }],
258 };
259
260 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
261 assert!(result.is_err());
262
263 let error_msg = result.unwrap_err().to_string();
264 assert!(error_msg.contains("Invalid crop region"));
265 }
266
267 #[test]
268 fn test_crop_bounding_box_negative_coordinates() {
269 let img = create_test_image(100, 100);
270 let bbox = BoundingBox {
271 points: vec![
272 Point { x: -10.0, y: -5.0 },
273 Point { x: 30.0, y: -5.0 },
274 Point { x: 30.0, y: 25.0 },
275 Point { x: -10.0, y: 25.0 },
276 ],
277 };
278
279 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
280 assert!(result.is_ok());
281
282 let cropped = result.unwrap();
283 assert_eq!(cropped.width(), 30); assert_eq!(cropped.height(), 25); }
287
288 #[test]
289 fn test_crop_bounding_box_out_of_bounds() {
290 let img = create_test_image(100, 100);
291 let bbox = BoundingBox {
292 points: vec![
293 Point { x: 80.0, y: 80.0 },
294 Point { x: 150.0, y: 80.0 }, Point { x: 150.0, y: 120.0 }, Point { x: 80.0, y: 120.0 },
297 ],
298 };
299
300 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
301 assert!(result.is_ok());
302
303 let cropped = result.unwrap();
304 assert_eq!(cropped.width(), 20); assert_eq!(cropped.height(), 20); }
308
309 #[test]
310 fn test_crop_bounding_box_irregular_polygon() {
311 let img = create_test_image(100, 100);
312 let bbox = BoundingBox {
313 points: vec![
314 Point { x: 20.0, y: 30.0 },
315 Point { x: 60.0, y: 10.0 },
316 Point { x: 80.0, y: 50.0 },
317 Point { x: 40.0, y: 70.0 },
318 Point { x: 10.0, y: 40.0 },
319 ],
320 };
321
322 let result = ImageProcessor::crop_bounding_box(&img, &bbox);
323 assert!(result.is_ok());
324
325 let cropped = result.unwrap();
326 assert_eq!(cropped.width(), 70); assert_eq!(cropped.height(), 60); }
330
331 #[test]
332 fn test_crop_rotated_bounding_box_valid() {
333 let img = create_test_image(100, 100);
334 let bbox = BoundingBox {
335 points: vec![
336 Point { x: 20.0, y: 20.0 },
337 Point { x: 60.0, y: 20.0 },
338 Point { x: 60.0, y: 40.0 },
339 Point { x: 20.0, y: 40.0 },
340 ],
341 };
342
343 let result = ImageProcessor::crop_rotated_bounding_box(&img, &bbox);
344 assert!(result.is_ok());
345
346 let cropped = result.unwrap();
347 assert!(cropped.width() > 0);
348 assert!(cropped.height() > 0);
349 }
350
351 #[test]
352 fn test_crop_rotated_bounding_box_wrong_point_count() {
353 let img = create_test_image(100, 100);
354 let bbox = BoundingBox {
355 points: vec![
356 Point { x: 20.0, y: 20.0 },
357 Point { x: 60.0, y: 20.0 },
358 Point { x: 60.0, y: 40.0 },
359 ], };
361
362 let result = ImageProcessor::crop_rotated_bounding_box(&img, &bbox);
363 assert!(result.is_err());
364
365 let error_msg = result.unwrap_err().to_string();
366 assert!(error_msg.contains("must have exactly 4 points"));
367 }
368
369 #[test]
370 fn test_crop_rotated_bounding_box_axis_aligned_fast_path() {
371 let img = create_test_image(100, 100);
372 let bbox = BoundingBox {
374 points: vec![
375 Point { x: 10.0, y: 20.0 },
376 Point { x: 60.0, y: 20.0 },
377 Point { x: 60.0, y: 50.0 },
378 Point { x: 10.0, y: 50.0 },
379 ],
380 };
381 let cropped_fast = ImageProcessor::crop_rotated_bounding_box(&img, &bbox).unwrap();
382 let expected = imageops::crop_imm(&img, 10, 20, 50, 30).to_image();
384 assert_eq!(cropped_fast.dimensions(), expected.dimensions());
385 assert_eq!(cropped_fast.get_pixel(0, 0), expected.get_pixel(0, 0));
387 assert_eq!(cropped_fast.get_pixel(49, 29), expected.get_pixel(49, 29));
388 }
389
390 #[test]
391 fn test_slice_rgb_image() {
392 let img = create_test_image(100, 100);
393 let coords = (10, 20, 50, 60);
394
395 let sliced = ImageProcessor::slice_rgb_image(&img, coords);
396 assert_eq!(sliced.width(), 40); assert_eq!(sliced.height(), 40); let original_pixel = img.get_pixel(10, 20);
401 let sliced_pixel = sliced.get_pixel(0, 0);
402 assert_eq!(original_pixel, sliced_pixel);
403 }
404}