1use crate::core::OCRError;
7use crate::processors::Point;
8use image::{Rgb, RgbImage, imageops};
9use nalgebra::{Matrix3, Vector3};
10use rayon::prelude::*;
11use tracing::debug;
12
13fn distance(p1: &Point, p2: &Point) -> f32 {
24 (p1.x - p2.x).hypot(p1.y - p2.y)
25}
26
27pub fn get_rotate_crop_image(
51 src_image: &RgbImage,
52 box_points: &[Point],
53) -> Result<RgbImage, OCRError> {
54 if box_points.len() != 4 {
56 return Err(OCRError::InvalidInput {
57 message: "Box must contain exactly 4 points".to_string(),
58 });
59 }
60
61 let mut min_x = f32::INFINITY;
63 let mut max_x = f32::NEG_INFINITY;
64 let mut min_y = f32::INFINITY;
65 let mut max_y = f32::NEG_INFINITY;
66
67 for p in box_points {
68 min_x = min_x.min(p.x);
69 max_x = max_x.max(p.x);
70 min_y = min_y.min(p.y);
71 max_y = max_y.max(p.y);
72 }
73
74 let left = min_x.max(0.0) as u32;
76 let top = min_y.max(0.0) as u32;
77 let right = max_x.min(src_image.width() as f32) as u32;
78 let bottom = max_y.min(src_image.height() as f32) as u32;
79
80 if right <= left || bottom <= top {
82 return Err(OCRError::InvalidInput {
83 message: "Invalid crop region".to_string(),
84 });
85 }
86
87 let crop_width = right - left;
89 let crop_height = bottom - top;
90 let img_crop = imageops::crop_imm(src_image, left, top, crop_width, crop_height).to_image();
91
92 let points: Vec<Point> = box_points
94 .iter()
95 .map(|p| Point::new(p.x - left as f32, p.y - top as f32))
96 .collect();
97
98 let mut sorted = points.clone();
101 sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
102 let (mut index_a, mut index_d) = (0usize, 1usize);
103 if sorted[1].y < sorted[0].y {
104 index_a = 1;
105 index_d = 0;
106 }
107 let (mut index_b, mut index_c) = (2usize, 3usize);
108 if sorted[3].y < sorted[2].y {
109 index_b = 3;
110 index_c = 2;
111 }
112 let ordered = [
113 sorted[index_a],
114 sorted[index_b],
115 sorted[index_c],
116 sorted[index_d],
117 ];
118
119 let width1 = distance(&ordered[0], &ordered[1]);
121 let width2 = distance(&ordered[2], &ordered[3]);
122 let img_crop_width = width1.max(width2).round() as u32;
123
124 let height1 = distance(&ordered[0], &ordered[3]);
125 let height2 = distance(&ordered[1], &ordered[2]);
126 let img_crop_height = height1.max(height2).round() as u32;
127
128 if img_crop_width == 0 || img_crop_height == 0 {
130 return Err(OCRError::InvalidInput {
131 message: "Invalid crop dimensions".to_string(),
132 });
133 }
134
135 let pts_std = [
137 Point::new(0.0, 0.0),
138 Point::new(img_crop_width as f32, 0.0),
139 Point::new(img_crop_width as f32, img_crop_height as f32),
140 Point::new(0.0, img_crop_height as f32),
141 ];
142
143 let transform_matrix = get_perspective_transform(&ordered, &pts_std)?;
145
146 let dst_img = warp_perspective(
148 &img_crop,
149 &transform_matrix,
150 img_crop_width,
151 img_crop_height,
152 )?;
153
154 if dst_img.height() as f32 >= dst_img.width() as f32 * 1.5 {
156 debug!(
157 "Rotating image due to aspect ratio: {}x{}",
158 dst_img.width(),
159 dst_img.height()
160 );
161
162 Ok(imageops::rotate270(&dst_img))
163 } else {
164 Ok(dst_img)
165 }
166}
167
168fn get_perspective_transform(
188 src_points: &[Point],
189 dst_points: &[Point],
190) -> Result<Matrix3<f32>, OCRError> {
191 if src_points.len() != 4 || dst_points.len() != 4 {
193 return Err(OCRError::InvalidInput {
194 message: "Need exactly 4 points for perspective transformation".to_string(),
195 });
196 }
197
198 let mut a = nalgebra::DMatrix::<f32>::zeros(8, 8);
200 let mut b = nalgebra::DVector::<f32>::zeros(8);
201
202 for i in 0..4 {
204 let src = &src_points[i];
205 let dst = &dst_points[i];
206
207 a.set_row(
209 i * 2,
210 &nalgebra::RowDVector::from_row_slice(&[
211 src.x,
212 src.y,
213 1.0,
214 0.0,
215 0.0,
216 0.0,
217 -src.x * dst.x,
218 -src.y * dst.x,
219 ]),
220 );
221 b[i * 2] = dst.x;
222
223 a.set_row(
225 i * 2 + 1,
226 &nalgebra::RowDVector::from_row_slice(&[
227 0.0,
228 0.0,
229 0.0,
230 src.x,
231 src.y,
232 1.0,
233 -src.x * dst.y,
234 -src.y * dst.y,
235 ]),
236 );
237 b[i * 2 + 1] = dst.y;
238 }
239
240 let decomp = a.lu();
242 let solution = decomp.solve(&b).ok_or_else(|| OCRError::InvalidInput {
243 message: "Cannot solve perspective transformation".to_string(),
244 })?;
245
246 Ok(Matrix3::new(
248 solution[0],
249 solution[1],
250 solution[2],
251 solution[3],
252 solution[4],
253 solution[5],
254 solution[6],
255 solution[7],
256 1.0,
257 ))
258}
259
260fn warp_perspective(
281 src_image: &RgbImage,
282 transform_matrix: &Matrix3<f32>,
283 dst_width: u32,
284 dst_height: u32,
285) -> Result<RgbImage, OCRError> {
286 let inv_matrix = transform_matrix
288 .try_inverse()
289 .ok_or_else(|| OCRError::InvalidInput {
290 message: "Cannot invert transformation matrix".to_string(),
291 })?;
292
293 let mut dst_image = RgbImage::new(dst_width, dst_height);
295 let buffer: &mut [u8] = dst_image.as_mut();
296
297 if dst_height <= 1 {
303 let row_buffer = &mut buffer[0..(dst_width * 3) as usize];
304 warp_row(&inv_matrix, src_image, 0, dst_width, row_buffer);
305 } else {
306 buffer
307 .par_chunks_mut((dst_width * 3) as usize)
308 .enumerate()
309 .for_each(|(dst_y, row_buffer)| {
310 warp_row(&inv_matrix, src_image, dst_y as u32, dst_width, row_buffer);
311 });
312 }
313
314 Ok(dst_image)
315}
316
317#[inline]
328fn warp_row(
329 inv_matrix: &Matrix3<f32>,
330 src_image: &RgbImage,
331 dst_y: u32,
332 dst_width: u32,
333 row_buffer: &mut [u8],
334) {
335 for dst_x in 0..dst_width {
336 let src_point = inv_matrix * Vector3::new(dst_x as f32, dst_y as f32, 1.0);
337 let final_pixel = if src_point.z.abs() > f32::EPSILON {
338 let src_x = src_point.x / src_point.z;
339 let src_y = src_point.y / src_point.z;
340 bicubic_interpolate(src_image, src_x, src_y)
342 } else {
343 *src_image.get_pixel(0, 0)
345 };
346 let index = (dst_x * 3) as usize;
347 row_buffer[index..index + 3].copy_from_slice(&final_pixel.0);
348 }
349}
350
351#[cfg(test)]
369#[inline]
370fn get_pixel_replicate(image: &RgbImage, x: i32, y: i32) -> Rgb<u8> {
371 let clamped_x = x.clamp(0, image.width() as i32 - 1) as u32;
372 let clamped_y = y.clamp(0, image.height() as i32 - 1) as u32;
373 *image.get_pixel(clamped_x, clamped_y)
374}
375
376#[inline]
386fn cubic_kernel(t: f32) -> f32 {
387 const A: f32 = -0.5; let t_abs = t.abs();
389
390 if t_abs <= 1.0 {
391 (A + 2.0) * t_abs * t_abs * t_abs - (A + 3.0) * t_abs * t_abs + 1.0
392 } else if t_abs < 2.0 {
393 A * t_abs * t_abs * t_abs - 5.0 * A * t_abs * t_abs + 8.0 * A * t_abs - 4.0 * A
394 } else {
395 0.0
396 }
397}
398
399fn bicubic_interpolate(image: &RgbImage, x: f32, y: f32) -> Rgb<u8> {
415 let x_int = x.floor() as i32;
416 let y_int = y.floor() as i32;
417 let dx = x - x_int as f32;
418 let dy = y - y_int as f32;
419
420 let wx = [
422 cubic_kernel(dx + 1.0),
423 cubic_kernel(dx),
424 cubic_kernel(dx - 1.0),
425 cubic_kernel(dx - 2.0),
426 ];
427
428 let wy = [
430 cubic_kernel(dy + 1.0),
431 cubic_kernel(dy),
432 cubic_kernel(dy - 1.0),
433 cubic_kernel(dy - 2.0),
434 ];
435
436 let w = image.width() as i32;
443 let h = image.height() as i32;
444 let raw = image.as_raw();
445 let stride = (w as usize) * 3;
446 let cx = [
447 (x_int - 1).clamp(0, w - 1) as usize * 3,
448 x_int.clamp(0, w - 1) as usize * 3,
449 (x_int + 1).clamp(0, w - 1) as usize * 3,
450 (x_int + 2).clamp(0, w - 1) as usize * 3,
451 ];
452 let cy = [
453 (y_int - 1).clamp(0, h - 1) as usize * stride,
454 y_int.clamp(0, h - 1) as usize * stride,
455 (y_int + 1).clamp(0, h - 1) as usize * stride,
456 (y_int + 2).clamp(0, h - 1) as usize * stride,
457 ];
458
459 let mut result = [0.0f32; 3];
460 for (j, &weight_y) in wy.iter().enumerate() {
461 let row = cy[j];
462 for (i, &weight_x) in wx.iter().enumerate() {
463 let weight = weight_x * weight_y;
464 let idx = row + cx[i];
465 result[0] += weight * raw[idx] as f32;
466 result[1] += weight * raw[idx + 1] as f32;
467 result[2] += weight * raw[idx + 2] as f32;
468 }
469 }
470
471 Rgb([
473 result[0].round().clamp(0.0, 255.0) as u8,
474 result[1].round().clamp(0.0, 255.0) as u8,
475 result[2].round().clamp(0.0, 255.0) as u8,
476 ])
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn bilinear_interpolate(image: &RgbImage, x: f32, y: f32) -> Rgb<u8> {
489 let x_int = x.floor() as i32;
490 let y_int = y.floor() as i32;
491
492 let dx = x - x_int as f32;
494 let dy = y - y_int as f32;
495
496 let p11 = get_pixel_replicate(image, x_int, y_int);
498 let p12 = get_pixel_replicate(image, x_int, y_int + 1);
499 let p21 = get_pixel_replicate(image, x_int + 1, y_int);
500 let p22 = get_pixel_replicate(image, x_int + 1, y_int + 1);
501
502 let mut result = [0u8; 3];
504 for (i, result_channel) in result.iter_mut().enumerate() {
505 let val = (1.0 - dx) * (1.0 - dy) * p11.0[i] as f32
506 + dx * (1.0 - dy) * p21.0[i] as f32
507 + (1.0 - dx) * dy * p12.0[i] as f32
508 + dx * dy * p22.0[i] as f32;
509 *result_channel = val.round().clamp(0.0, 255.0) as u8;
510 }
511
512 Rgb(result)
513 }
514
515 fn bicubic_reference(image: &RgbImage, x: f32, y: f32) -> Rgb<u8> {
519 let x_int = x.floor() as i32;
520 let y_int = y.floor() as i32;
521 let dx = x - x_int as f32;
522 let dy = y - y_int as f32;
523 let wx = [
524 cubic_kernel(dx + 1.0),
525 cubic_kernel(dx),
526 cubic_kernel(dx - 1.0),
527 cubic_kernel(dx - 2.0),
528 ];
529 let wy = [
530 cubic_kernel(dy + 1.0),
531 cubic_kernel(dy),
532 cubic_kernel(dy - 1.0),
533 cubic_kernel(dy - 2.0),
534 ];
535 let mut result = [0.0f32; 3];
536 for (j, &weight_y) in wy.iter().enumerate() {
537 let sample_y = y_int - 1 + j as i32;
538 for (i, &weight_x) in wx.iter().enumerate() {
539 let sample_x = x_int - 1 + i as i32;
540 let weight = weight_x * weight_y;
541 let pixel = get_pixel_replicate(image, sample_x, sample_y);
542 for (c, result_c) in result.iter_mut().enumerate().take(3) {
543 *result_c += weight * pixel.0[c] as f32;
544 }
545 }
546 }
547 Rgb([
548 result[0].round().clamp(0.0, 255.0) as u8,
549 result[1].round().clamp(0.0, 255.0) as u8,
550 result[2].round().clamp(0.0, 255.0) as u8,
551 ])
552 }
553
554 #[test]
555 fn bicubic_raw_buffer_matches_reference_bit_exact() {
556 let (w, h) = (17u32, 11u32);
558 let img = RgbImage::from_fn(w, h, |x, y| {
559 let i = y * w + x;
560 Rgb([
561 (i.wrapping_mul(37).wrapping_add(11) % 256) as u8,
562 (i.wrapping_mul(59).wrapping_add(7) % 256) as u8,
563 (i.wrapping_mul(101).wrapping_add(3) % 256) as u8,
564 ])
565 });
566 for yi in -3..(h as i32 + 3) {
569 for xi in -3..(w as i32 + 3) {
570 for &fx in &[0.0f32, 0.25, 0.5, 0.75] {
571 for &fy in &[0.0f32, 0.33, 0.66] {
572 let x = xi as f32 + fx;
573 let y = yi as f32 + fy;
574 assert_eq!(
575 bicubic_interpolate(&img, x, y),
576 bicubic_reference(&img, x, y),
577 "mismatch at ({x}, {y})"
578 );
579 }
580 }
581 }
582 }
583 }
584
585 #[test]
586 fn test_distance() {
587 let p1 = Point::new(0.0, 0.0);
588 let p2 = Point::new(3.0, 4.0);
589 let dist = distance(&p1, &p2);
590 assert_eq!(dist, 5.0);
591 }
592
593 #[test]
594 fn test_get_perspective_transform() -> Result<(), OCRError> {
595 let src_points = [
597 Point::new(0.0, 0.0),
598 Point::new(1.0, 0.0),
599 Point::new(1.0, 1.0),
600 Point::new(0.0, 1.0),
601 ];
602
603 let dst_points = [
604 Point::new(0.0, 0.0),
605 Point::new(2.0, 0.0),
606 Point::new(2.0, 2.0),
607 Point::new(0.0, 2.0),
608 ];
609
610 let transform = get_perspective_transform(&src_points, &dst_points)?;
611
612 assert!(transform.iter().all(|&x| x.is_finite()));
614 Ok(())
615 }
616
617 #[test]
618 fn test_get_perspective_transform_invalid_input() {
619 let src_points = [Point::new(0.0, 0.0), Point::new(1.0, 0.0)];
621
622 let dst_points = [
623 Point::new(0.0, 0.0),
624 Point::new(2.0, 0.0),
625 Point::new(2.0, 2.0),
626 Point::new(0.0, 2.0),
627 ];
628
629 let result = get_perspective_transform(&src_points, &dst_points);
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn test_get_rotate_crop_image_invalid_points() {
635 let image = RgbImage::new(4, 4);
637
638 let points = vec![Point::new(0.0, 0.0), Point::new(1.0, 0.0)];
640
641 let result = get_rotate_crop_image(&image, &points);
642 assert!(result.is_err());
643 }
644
645 #[test]
646 fn test_get_rotate_crop_image_success() -> Result<(), OCRError> {
647 let mut image = RgbImage::new(4, 4);
649 for y in 0..4 {
650 for x in 0..4 {
651 let r = (x * 64) as u8;
653 let g = (y * 64) as u8;
654 let b = ((x + y) * 32) as u8;
655 image.put_pixel(x, y, Rgb([r, g, b]));
656 }
657 }
658
659 let points = vec![
661 Point::new(1.0, 1.0),
662 Point::new(3.0, 1.0),
663 Point::new(3.0, 3.0),
664 Point::new(1.0, 3.0),
665 ];
666
667 let cropped_image = get_rotate_crop_image(&image, &points)?;
668 assert!(cropped_image.width() > 0);
670 assert!(cropped_image.height() > 0);
671 Ok(())
672 }
673
674 #[test]
675 fn test_warp_perspective_invalid_matrix() {
676 let image = RgbImage::new(2, 2);
678
679 let matrix = Matrix3::new(1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0);
681
682 let result = warp_perspective(&image, &matrix, 2, 2);
683 assert!(result.is_err());
684 }
685
686 #[test]
687 fn test_bilinear_interpolate() {
688 let mut image = RgbImage::new(2, 2);
690 image.put_pixel(0, 0, Rgb([255, 0, 0])); image.put_pixel(1, 0, Rgb([0, 255, 0])); image.put_pixel(0, 1, Rgb([0, 0, 255])); image.put_pixel(1, 1, Rgb([255, 255, 0])); let pixel = bilinear_interpolate(&image, 0.5, 0.5);
697 assert_eq!(pixel.0[0], 128);
701 assert_eq!(pixel.0[1], 128);
702 assert_eq!(pixel.0[2], 64);
703 }
704}