use crate::core::OCRError;
use crate::processors::BoundingBox;
use crate::utils::transform::get_rotate_crop_image;
use image::{RgbImage, imageops};
pub struct BBoxCrop;
impl BBoxCrop {
pub fn crop_bounding_box(image: &RgbImage, bbox: &BoundingBox) -> Result<RgbImage, OCRError> {
if bbox.points.is_empty() {
return Err(OCRError::image_processing_error("Empty bounding box"));
}
let min_x = bbox
.points
.iter()
.map(|p| p.x)
.fold(f32::INFINITY, f32::min)
.max(0.0);
let max_x = bbox
.points
.iter()
.map(|p| p.x)
.fold(f32::NEG_INFINITY, f32::max);
let min_y = bbox
.points
.iter()
.map(|p| p.y)
.fold(f32::INFINITY, f32::min)
.max(0.0);
let max_y = bbox
.points
.iter()
.map(|p| p.y)
.fold(f32::NEG_INFINITY, f32::max);
let x1 = (min_x as u32).min(image.width().saturating_sub(1));
let y1 = (min_y as u32).min(image.height().saturating_sub(1));
let x2 = (max_x as u32).min(image.width());
let y2 = (max_y as u32).min(image.height());
if x2 <= x1 || y2 <= y1 {
return Err(OCRError::image_processing_error(format!(
"Invalid crop region: ({x1}, {y1}) to ({x2}, {y2})"
)));
}
let coords = (x1, y1, x2, y2);
Ok(Self::slice_rgb_image(image, coords))
}
fn slice_rgb_image(img: &RgbImage, coords: (u32, u32, u32, u32)) -> RgbImage {
let (x1, y1, x2, y2) = coords;
let width = x2 - x1;
let height = y2 - y1;
imageops::crop_imm(img, x1, y1, width, height).to_image()
}
pub fn batch_crop_bounding_boxes(
image: &RgbImage,
bboxes: &[BoundingBox],
) -> Vec<Result<RgbImage, OCRError>> {
bboxes
.iter()
.map(|bbox| Self::crop_bounding_box(image, bbox))
.collect()
}
pub fn batch_crop_rotated_bounding_boxes(
image: &RgbImage,
bboxes: &[BoundingBox],
) -> Vec<Result<RgbImage, OCRError>> {
bboxes
.iter()
.map(|bbox| get_rotate_crop_image(image, &bbox.points))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::processors::Point;
use image::{ImageBuffer, Rgb};
fn create_test_image(width: u32, height: u32) -> RgbImage {
let mut img = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let r = (x * 255 / width.max(1)) as u8;
let g = (y * 255 / height.max(1)) as u8;
let b = 128;
img.put_pixel(x, y, Rgb([r, g, b]));
}
}
img
}
#[test]
fn test_crop_bounding_box_valid_rectangle() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 10.0, y: 10.0 },
Point { x: 50.0, y: 10.0 },
Point { x: 50.0, y: 40.0 },
Point { x: 10.0, y: 40.0 },
],
};
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_ok());
let cropped = match result {
Ok(cropped) => cropped,
Err(err) => panic!("expected crop to succeed: {err}"),
};
assert_eq!(cropped.width(), 40); assert_eq!(cropped.height(), 30); }
#[test]
fn test_crop_bounding_box_empty_points() {
let img = create_test_image(100, 100);
let bbox = BoundingBox { points: vec![] };
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Empty bounding box"));
}
#[test]
fn test_crop_bounding_box_single_point() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![Point { x: 50.0, y: 50.0 }],
};
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Invalid crop region"));
}
#[test]
fn test_crop_bounding_box_negative_coordinates() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: -10.0, y: -5.0 },
Point { x: 30.0, y: -5.0 },
Point { x: 30.0, y: 25.0 },
Point { x: -10.0, y: 25.0 },
],
};
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_ok());
let cropped = match result {
Ok(cropped) => cropped,
Err(err) => panic!("expected crop to succeed: {err}"),
};
assert_eq!(cropped.width(), 30); assert_eq!(cropped.height(), 25); }
#[test]
fn test_crop_bounding_box_out_of_bounds() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 80.0, y: 80.0 },
Point { x: 150.0, y: 80.0 }, Point { x: 150.0, y: 120.0 }, Point { x: 80.0, y: 120.0 },
],
};
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_ok());
let cropped = match result {
Ok(cropped) => cropped,
Err(err) => panic!("expected crop to succeed: {err}"),
};
assert_eq!(cropped.width(), 20); assert_eq!(cropped.height(), 20); }
#[test]
fn test_crop_bounding_box_irregular_polygon() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 20.0, y: 30.0 },
Point { x: 60.0, y: 10.0 },
Point { x: 80.0, y: 50.0 },
Point { x: 40.0, y: 70.0 },
Point { x: 10.0, y: 40.0 },
],
};
let result = BBoxCrop::crop_bounding_box(&img, &bbox);
assert!(result.is_ok());
let cropped = match result {
Ok(cropped) => cropped,
Err(err) => panic!("expected crop to succeed: {err}"),
};
assert_eq!(cropped.width(), 70); assert_eq!(cropped.height(), 60); }
#[test]
fn test_batch_crop_rotated_bounding_boxes_valid() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 20.0, y: 20.0 },
Point { x: 60.0, y: 20.0 },
Point { x: 60.0, y: 40.0 },
Point { x: 20.0, y: 40.0 },
],
};
let mut results = BBoxCrop::batch_crop_rotated_bounding_boxes(&img, &[bbox]);
assert_eq!(results.len(), 1);
let result = results.remove(0);
let cropped = match result {
Ok(cropped) => cropped,
Err(err) => panic!("expected crop to succeed: {err}"),
};
assert!(cropped.width() > 0);
assert!(cropped.height() > 0);
}
#[test]
fn test_batch_crop_rotated_bounding_boxes_wrong_point_count() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 20.0, y: 20.0 },
Point { x: 60.0, y: 20.0 },
Point { x: 60.0, y: 40.0 },
], };
let mut results = BBoxCrop::batch_crop_rotated_bounding_boxes(&img, &[bbox]);
assert_eq!(results.len(), 1);
let result = results.remove(0);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Box must contain exactly 4 points"));
}
#[test]
fn test_get_rotate_crop_image_axis_aligned_wide_box() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 10.0, y: 20.0 },
Point { x: 60.0, y: 20.0 },
Point { x: 60.0, y: 50.0 },
Point { x: 10.0, y: 50.0 },
],
};
let cropped_fast = match get_rotate_crop_image(&img, &bbox.points) {
Ok(cropped_fast) => cropped_fast,
Err(err) => panic!("expected rotated crop to succeed: {err}"),
};
assert_eq!(cropped_fast.dimensions(), (50, 30));
}
#[test]
fn test_get_rotate_crop_image_axis_aligned_tall_box_rotates() {
let img = create_test_image(100, 100);
let bbox = BoundingBox {
points: vec![
Point { x: 10.0, y: 20.0 },
Point { x: 30.0, y: 20.0 },
Point { x: 30.0, y: 80.0 },
Point { x: 10.0, y: 80.0 },
],
};
let cropped = match get_rotate_crop_image(&img, &bbox.points) {
Ok(cropped) => cropped,
Err(err) => panic!("expected rotated crop to succeed: {err}"),
};
assert_eq!(cropped.dimensions(), (60, 20));
}
#[test]
fn test_slice_rgb_image() {
let img = create_test_image(100, 100);
let coords = (10, 20, 50, 60);
let sliced = BBoxCrop::slice_rgb_image(&img, coords);
assert_eq!(sliced.width(), 40); assert_eq!(sliced.height(), 40);
let original_pixel = img.get_pixel(10, 20);
let sliced_pixel = sliced.get_pixel(0, 0);
assert_eq!(original_pixel, sliced_pixel);
}
}