oar_ocr_core/processors/geometry.rs
1//! Geometric utilities for OCR processing.
2//!
3//! This module provides geometric primitives and algorithms commonly used in OCR systems,
4//! such as point representations, bounding boxes, and algorithms for calculating areas,
5//! perimeters, convex hulls, and minimum area rectangles.
6
7use imageproc::contours::Contour;
8use imageproc::point::Point as ImageProcPoint;
9use itertools::Itertools;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12
13use std::f32::consts::PI;
14
15/// A 2D point with floating-point coordinates.
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
17pub struct Point {
18 /// X-coordinate of the point.
19 pub x: f32,
20 /// Y-coordinate of the point.
21 pub y: f32,
22}
23
24impl Point {
25 /// Creates a new point with the given coordinates.
26 ///
27 /// # Arguments
28 ///
29 /// * `x` - The x-coordinate of the point.
30 /// * `y` - The y-coordinate of the point.
31 ///
32 /// # Returns
33 ///
34 /// A new `Point` instance.
35 #[inline]
36 pub fn new(x: f32, y: f32) -> Self {
37 Self { x, y }
38 }
39
40 /// Creates a point from an imageproc point with integer coordinates.
41 ///
42 /// # Arguments
43 ///
44 /// * `p` - An imageproc point with integer coordinates.
45 ///
46 /// # Returns
47 ///
48 /// A new `Point` instance with floating-point coordinates.
49 pub fn from_imageproc_point(p: ImageProcPoint<i32>) -> Self {
50 Self {
51 x: p.x as f32,
52 y: p.y as f32,
53 }
54 }
55
56 /// Converts this point to an imageproc point with integer coordinates.
57 ///
58 /// # Returns
59 ///
60 /// An imageproc point with coordinates rounded down to integers.
61 pub fn to_imageproc_point(&self) -> ImageProcPoint<i32> {
62 ImageProcPoint::new(self.x as i32, self.y as i32)
63 }
64}
65
66/// A bounding box represented by a collection of points.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct BoundingBox {
69 /// The points that define the bounding box.
70 pub points: Vec<Point>,
71}
72
73impl BoundingBox {
74 /// Creates a new bounding box from a vector of points.
75 ///
76 /// # Arguments
77 ///
78 /// * `points` - A vector of points that define the bounding box.
79 ///
80 /// # Returns
81 ///
82 /// A new `BoundingBox` instance.
83 pub fn new(points: Vec<Point>) -> Self {
84 Self { points }
85 }
86
87 /// Creates a bounding box from coordinates.
88 ///
89 /// # Arguments
90 ///
91 /// * `x1` - The x-coordinate of the top-left corner.
92 /// * `y1` - The y-coordinate of the top-left corner.
93 /// * `x2` - The x-coordinate of the bottom-right corner.
94 /// * `y2` - The y-coordinate of the bottom-right corner.
95 ///
96 /// # Returns
97 ///
98 /// A new `BoundingBox` instance representing a rectangle.
99 pub fn from_coords(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
100 let points = vec![
101 Point::new(x1, y1),
102 Point::new(x2, y1),
103 Point::new(x2, y2),
104 Point::new(x1, y2),
105 ];
106 Self { points }
107 }
108
109 /// Returns a new bounding box translated by `(dx, dy)`.
110 pub fn translate(&self, dx: f32, dy: f32) -> Self {
111 Self::new(
112 self.points
113 .iter()
114 .map(|p| Point::new(p.x + dx, p.y + dy))
115 .collect(),
116 )
117 }
118
119 /// Creates a bounding box from a contour.
120 ///
121 /// # Arguments
122 ///
123 /// * `contour` - A reference to a contour from imageproc.
124 ///
125 /// # Returns
126 ///
127 /// A new `BoundingBox` instance with points converted from the contour.
128 pub fn from_contour(contour: &Contour<u32>) -> Self {
129 let points = contour
130 .points
131 .iter()
132 .map(|p| Point::new(p.x as f32, p.y as f32))
133 .collect();
134 Self { points }
135 }
136
137 /// Calculates the area of the bounding box using the shoelace formula.
138 ///
139 /// # Returns
140 ///
141 /// The area of the bounding box. Returns 0.0 if the bounding box has fewer than 3 points.
142 pub fn area(&self) -> f32 {
143 if self.points.len() < 3 {
144 return 0.0;
145 }
146
147 let mut area = 0.0;
148 let n = self.points.len();
149 for i in 0..n {
150 let j = (i + 1) % n;
151 area += self.points[i].x * self.points[j].y;
152 area -= self.points[j].x * self.points[i].y;
153 }
154 area.abs() / 2.0
155 }
156
157 /// Calculates the perimeter of the bounding box.
158 ///
159 /// # Returns
160 ///
161 /// The perimeter of the bounding box.
162 pub fn perimeter(&self) -> f32 {
163 let mut perimeter = 0.0;
164 let n = self.points.len();
165 for i in 0..n {
166 let j = (i + 1) % n;
167 let dx = self.points[j].x - self.points[i].x;
168 let dy = self.points[j].y - self.points[i].y;
169 perimeter += (dx * dx + dy * dy).sqrt();
170 }
171 perimeter
172 }
173
174 /// Gets the minimum x-coordinate of all points in the bounding box.
175 ///
176 /// # Returns
177 ///
178 /// The minimum x-coordinate, or 0.0 if there are no points.
179 pub fn x_min(&self) -> f32 {
180 if self.points.is_empty() {
181 return 0.0;
182 }
183 self.points
184 .iter()
185 .map(|p| p.x)
186 .fold(f32::INFINITY, f32::min)
187 }
188
189 /// Gets the minimum y-coordinate of all points in the bounding box.
190 ///
191 /// # Returns
192 ///
193 /// The minimum y-coordinate, or 0.0 if there are no points.
194 pub fn y_min(&self) -> f32 {
195 if self.points.is_empty() {
196 return 0.0;
197 }
198 self.points
199 .iter()
200 .map(|p| p.y)
201 .fold(f32::INFINITY, f32::min)
202 }
203
204 /// Computes the convex hull of the bounding box using Graham's scan algorithm.
205 ///
206 /// # Returns
207 ///
208 /// A new `BoundingBox` representing the convex hull. If the bounding box has fewer than 3 points,
209 /// returns a clone of the original bounding box.
210 fn convex_hull(&self) -> BoundingBox {
211 if self.points.len() < 3 {
212 return self.clone();
213 }
214
215 let mut points = self.points.clone();
216
217 // Find the point with the lowest y-coordinate (and leftmost if tied)
218 let mut start_idx = 0;
219 for i in 1..points.len() {
220 if points[i].y < points[start_idx].y
221 || (points[i].y == points[start_idx].y && points[i].x < points[start_idx].x)
222 {
223 start_idx = i;
224 }
225 }
226 points.swap(0, start_idx);
227 let start_point = points[0];
228
229 // Sort points by polar angle with respect to the start point
230 points[1..].sort_by(|a, b| {
231 let cross = Self::cross_product(&start_point, a, b);
232 if cross == 0.0 {
233 // If points are collinear, sort by distance from start point
234 let dist_a = (a.x - start_point.x).powi(2) + (a.y - start_point.y).powi(2);
235 let dist_b = (b.x - start_point.x).powi(2) + (b.y - start_point.y).powi(2);
236 dist_a
237 .partial_cmp(&dist_b)
238 .unwrap_or(std::cmp::Ordering::Equal)
239 } else if cross > 0.0 {
240 // Counter-clockwise turn
241 std::cmp::Ordering::Less
242 } else {
243 // Clockwise turn
244 std::cmp::Ordering::Greater
245 }
246 });
247
248 // Build the convex hull using a stack
249 let mut hull = Vec::new();
250 for point in points {
251 // Remove points that make clockwise turns
252 while hull.len() > 1
253 && Self::cross_product(&hull[hull.len() - 2], &hull[hull.len() - 1], &point) <= 0.0
254 {
255 hull.pop();
256 }
257 hull.push(point);
258 }
259
260 BoundingBox::new(hull)
261 }
262
263 /// Computes the cross product of three points.
264 ///
265 /// # Arguments
266 ///
267 /// * `p1` - The first point.
268 /// * `p2` - The second point.
269 /// * `p3` - The third point.
270 ///
271 /// # Returns
272 ///
273 /// The cross product value. A positive value indicates a counter-clockwise turn,
274 /// a negative value indicates a clockwise turn, and zero indicates collinearity.
275 fn cross_product(p1: &Point, p2: &Point, p3: &Point) -> f32 {
276 (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
277 }
278
279 /// Computes the minimum area rectangle that encloses the bounding box.
280 ///
281 /// This method uses the rotating calipers algorithm on the convex hull of the bounding box
282 /// to find the minimum area rectangle.
283 ///
284 /// # Returns
285 ///
286 /// A `MinAreaRect` representing the minimum area rectangle. If the bounding box has fewer than
287 /// 3 points, returns a rectangle with zero dimensions.
288 pub fn get_min_area_rect(&self) -> MinAreaRect {
289 if self.points.len() < 3 {
290 return MinAreaRect {
291 center: Point::new(0.0, 0.0),
292 width: 0.0,
293 height: 0.0,
294 angle: 0.0,
295 };
296 }
297
298 // Get the convex hull of the bounding box
299 let hull = self.convex_hull();
300 let hull_points = &hull.points;
301
302 // Handle degenerate cases
303 if hull_points.len() < 3 {
304 let (min_x, max_x) = match self.points.iter().map(|p| p.x).minmax().into_option() {
305 Some((min, max)) => (min, max),
306 None => {
307 return MinAreaRect {
308 center: Point::new(0.0, 0.0),
309 width: 0.0,
310 height: 0.0,
311 angle: 0.0,
312 };
313 }
314 };
315
316 let (min_y, max_y) = match self.points.iter().map(|p| p.y).minmax().into_option() {
317 Some((min, max)) => (min, max),
318 None => {
319 return MinAreaRect {
320 center: Point::new(0.0, 0.0),
321 width: 0.0,
322 height: 0.0,
323 angle: 0.0,
324 };
325 }
326 };
327
328 let center = Point::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0);
329 let width = max_x - min_x;
330 let height = max_y - min_y;
331
332 return MinAreaRect {
333 center,
334 width,
335 height,
336 angle: 0.0,
337 };
338 }
339
340 // Find the minimum area rectangle using rotating calipers
341 let mut min_area = f32::MAX;
342 let mut min_rect = MinAreaRect {
343 center: Point::new(0.0, 0.0),
344 width: 0.0,
345 height: 0.0,
346 angle: 0.0,
347 };
348
349 let n = hull_points.len();
350 for i in 0..n {
351 let j = (i + 1) % n;
352
353 // Calculate the edge vector
354 let edge_x = hull_points[j].x - hull_points[i].x;
355 let edge_y = hull_points[j].y - hull_points[i].y;
356 let edge_length = (edge_x * edge_x + edge_y * edge_y).sqrt();
357
358 // Skip degenerate edges
359 if edge_length < f32::EPSILON {
360 continue;
361 }
362
363 // Normalize the edge vector
364 let nx = edge_x / edge_length;
365 let ny = edge_y / edge_length;
366
367 // Calculate the perpendicular vector
368 let px = -ny;
369 let py = nx;
370
371 // Project all points onto the edge and perpendicular vectors
372 let mut min_n = f32::MAX;
373 let mut max_n = f32::MIN;
374 let mut min_p = f32::MAX;
375 let mut max_p = f32::MIN;
376
377 for k in 0..n {
378 let point = &hull_points[k];
379
380 let proj_n = nx * (point.x - hull_points[i].x) + ny * (point.y - hull_points[i].y);
381 min_n = min_n.min(proj_n);
382 max_n = max_n.max(proj_n);
383
384 let proj_p = px * (point.x - hull_points[i].x) + py * (point.y - hull_points[i].y);
385 min_p = min_p.min(proj_p);
386 max_p = max_p.max(proj_p);
387 }
388
389 // Calculate the width, height, and area of the rectangle
390 let width = max_n - min_n;
391 let height = max_p - min_p;
392 let area = width * height;
393
394 // Update the minimum area rectangle if this one is smaller
395 if area < min_area {
396 min_area = area;
397
398 let center_n = (min_n + max_n) / 2.0;
399 let center_p = (min_p + max_p) / 2.0;
400
401 let center_x = hull_points[i].x + center_n * nx + center_p * px;
402 let center_y = hull_points[i].y + center_n * ny + center_p * py;
403
404 let angle_rad = f32::atan2(ny, nx);
405 let angle_deg = angle_rad * 180.0 / PI;
406
407 min_rect = MinAreaRect {
408 center: Point::new(center_x, center_y),
409 width,
410 height,
411 angle: angle_deg,
412 };
413 }
414 }
415
416 min_rect
417 }
418
419 /// Approximates a polygon using the Douglas-Peucker algorithm.
420 ///
421 /// # Arguments
422 ///
423 /// * `epsilon` - The maximum distance between the original curve and the simplified curve.
424 ///
425 /// # Returns
426 ///
427 /// A new `BoundingBox` with simplified points. If the bounding box has 2 or fewer points,
428 /// returns a clone of the original bounding box.
429 pub fn approx_poly_dp(&self, epsilon: f32) -> BoundingBox {
430 if self.points.len() <= 2 {
431 return self.clone();
432 }
433
434 let mut simplified = Vec::new();
435 self.douglas_peucker(&self.points, epsilon, &mut simplified);
436
437 BoundingBox::new(simplified)
438 }
439
440 /// Implements the Douglas-Peucker algorithm for curve simplification.
441 ///
442 /// # Arguments
443 ///
444 /// * `points` - The points to simplify.
445 /// * `epsilon` - The maximum distance between the original curve and the simplified curve.
446 /// * `result` - A mutable reference to a vector where the simplified points will be stored.
447 fn douglas_peucker(&self, points: &[Point], epsilon: f32, result: &mut Vec<Point>) {
448 if points.len() <= 2 {
449 result.extend_from_slice(points);
450 return;
451 }
452
453 // Initialize a stack for iterative implementation
454 let mut stack = Vec::new();
455 stack.push((0, points.len() - 1));
456
457 // Track which points to keep
458 let mut keep = vec![false; points.len()];
459 keep[0] = true;
460 keep[points.len() - 1] = true;
461
462 // Process the stack
463 const MAX_ITERATIONS: usize = 10000;
464 let mut iterations = 0;
465
466 while let Some((start, end)) = stack.pop() {
467 iterations += 1;
468 // Prevent infinite loops
469 if iterations > MAX_ITERATIONS {
470 keep.iter_mut()
471 .take(end + 1)
472 .skip(start)
473 .for_each(|k| *k = true);
474 break;
475 }
476
477 // Skip segments with only 2 points
478 if end - start <= 1 {
479 continue;
480 }
481
482 // Find the point with maximum distance from the line segment
483 let mut max_dist = 0.0;
484 let mut max_index = start;
485
486 for i in (start + 1)..end {
487 let dist = self.point_to_line_distance(&points[i], &points[start], &points[end]);
488 if dist > max_dist {
489 max_dist = dist;
490 max_index = i;
491 }
492 }
493
494 // If the maximum distance exceeds epsilon, split the segment
495 if max_dist > epsilon {
496 keep[max_index] = true;
497
498 if max_index - start > 1 {
499 stack.push((start, max_index));
500 }
501 if end - max_index > 1 {
502 stack.push((max_index, end));
503 }
504 }
505 }
506
507 // Collect the points to keep
508 for (i, &should_keep) in keep.iter().enumerate() {
509 if should_keep {
510 result.push(points[i]);
511 }
512 }
513 }
514
515 /// Calculates the perpendicular distance from a point to a line segment.
516 ///
517 /// # Arguments
518 ///
519 /// * `point` - The point to calculate the distance for.
520 /// * `line_start` - The start point of the line segment.
521 /// * `line_end` - The end point of the line segment.
522 ///
523 /// # Returns
524 ///
525 /// The perpendicular distance from the point to the line segment.
526 fn point_to_line_distance(&self, point: &Point, line_start: &Point, line_end: &Point) -> f32 {
527 let a = line_end.y - line_start.y;
528 let b = line_start.x - line_end.x;
529 let c = line_end.x * line_start.y - line_start.x * line_end.y;
530
531 let denominator = (a * a + b * b).sqrt();
532 if denominator == 0.0 {
533 return 0.0;
534 }
535
536 (a * point.x + b * point.y + c).abs() / denominator
537 }
538
539 /// Gets the maximum x-coordinate of all points in the bounding box.
540 ///
541 /// # Returns
542 ///
543 /// The maximum x-coordinate, or 0.0 if there are no points.
544 pub fn x_max(&self) -> f32 {
545 if self.points.is_empty() {
546 return 0.0;
547 }
548 self.points
549 .iter()
550 .map(|p| p.x)
551 .fold(f32::NEG_INFINITY, f32::max)
552 }
553
554 /// Gets the maximum y-coordinate of all points in the bounding box.
555 ///
556 /// # Returns
557 ///
558 /// The maximum y-coordinate, or 0.0 if there are no points.
559 pub fn y_max(&self) -> f32 {
560 if self.points.is_empty() {
561 return 0.0;
562 }
563 self.points
564 .iter()
565 .map(|p| p.y)
566 .fold(f32::NEG_INFINITY, f32::max)
567 }
568
569 /// Gets the geometric center (centroid) of the bounding box.
570 ///
571 /// # Returns
572 ///
573 /// The center point of the bounding box.
574 pub fn center(&self) -> Point {
575 if self.points.is_empty() {
576 return Point::new(0.0, 0.0);
577 }
578 let sum_x: f32 = self.points.iter().map(|p| p.x).sum();
579 let sum_y: f32 = self.points.iter().map(|p| p.y).sum();
580 let count = self.points.len() as f32;
581 Point::new(sum_x / count, sum_y / count)
582 }
583
584 /// Computes the area of intersection between this bounding box and another.
585 ///
586 /// # Arguments
587 ///
588 /// * `other` - The other bounding box.
589 ///
590 /// # Returns
591 ///
592 /// The area of the intersection. Returns 0.0 if there is no intersection.
593 pub fn intersection_area(&self, other: &BoundingBox) -> f32 {
594 let x1_min = self.x_min();
595 let y1_min = self.y_min();
596 let x1_max = self.x_max();
597 let y1_max = self.y_max();
598
599 let x2_min = other.x_min();
600 let y2_min = other.y_min();
601 let x2_max = other.x_max();
602 let y2_max = other.y_max();
603
604 // Compute intersection rectangle
605 let inter_x_min = x1_min.max(x2_min);
606 let inter_y_min = y1_min.max(y2_min);
607 let inter_x_max = x1_max.min(x2_max);
608 let inter_y_max = y1_max.min(y2_max);
609
610 // Check if there is no intersection
611 if inter_x_min >= inter_x_max || inter_y_min >= inter_y_max {
612 return 0.0;
613 }
614
615 // Compute intersection area
616 (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
617 }
618
619 /// Computes the Intersection over Union (IoU) between this bounding box and another.
620 ///
621 /// # Arguments
622 ///
623 /// * `other` - The other bounding box to compute IoU with.
624 ///
625 /// # Returns
626 ///
627 /// The IoU value between 0.0 and 1.0. Returns 0.0 if there is no intersection.
628 pub fn iou(&self, other: &BoundingBox) -> f32 {
629 let inter_area = self.intersection_area(other);
630
631 if inter_area <= 0.0 {
632 return 0.0;
633 }
634
635 // Compute areas of both bounding boxes
636 // Note: Using simple rectangle areas for union calculation might be slightly inaccurate
637 // if boxes are rotated polygons, but intersection is calculated as AABB intersection.
638 // For consistency, we should probably use AABB area for IOU if we use AABB intersection.
639 // However, existing IOU implementation used (x_max-x_min)*(y_max-y_min) which is AABB area.
640 // So let's stick to AABB area for consistency with previous implementation.
641
642 let x1_min = self.x_min();
643 let y1_min = self.y_min();
644 let x1_max = self.x_max();
645 let y1_max = self.y_max();
646 let aabb_area1 = (x1_max - x1_min) * (y1_max - y1_min);
647
648 let x2_min = other.x_min();
649 let y2_min = other.y_min();
650 let x2_max = other.x_max();
651 let y2_max = other.y_max();
652 let aabb_area2 = (x2_max - x2_min) * (y2_max - y2_min);
653
654 let union_area = aabb_area1 + aabb_area2 - inter_area;
655
656 if union_area <= 0.0 {
657 return 0.0;
658 }
659
660 inter_area / union_area
661 }
662
663 /// Computes the Intersection over Area (IoA) of this bounding box with another.
664 ///
665 /// IoA = intersection_area / self_area
666 ///
667 /// This is useful for determining what fraction of this box is inside another box.
668 /// For example, to check if a text box is mostly inside a table region.
669 ///
670 /// # Arguments
671 ///
672 /// * `other` - The other bounding box to compute IoA with.
673 ///
674 /// # Returns
675 ///
676 /// The IoA value between 0.0 and 1.0. Returns 0.0 if self has zero area or no intersection.
677 pub fn ioa(&self, other: &BoundingBox) -> f32 {
678 let inter_area = self.intersection_area(other);
679
680 if inter_area <= 0.0 {
681 return 0.0;
682 }
683
684 let x_min = self.x_min();
685 let y_min = self.y_min();
686 let x_max = self.x_max();
687 let y_max = self.y_max();
688 let self_area = (x_max - x_min) * (y_max - y_min);
689
690 if self_area <= 0.0 {
691 return 0.0;
692 }
693
694 inter_area / self_area
695 }
696
697 /// Computes the union (minimum bounding box) of this bounding box and another.
698 ///
699 /// # Arguments
700 ///
701 /// * `other` - The other bounding box to compute the union with.
702 ///
703 /// # Returns
704 ///
705 /// A new `BoundingBox` that encloses both input bounding boxes.
706 pub fn union(&self, other: &Self) -> Self {
707 let new_x_min = self.x_min().min(other.x_min());
708 let new_y_min = self.y_min().min(other.y_min());
709 let new_x_max = self.x_max().max(other.x_max());
710 let new_y_max = self.y_max().max(other.y_max());
711 BoundingBox::from_coords(new_x_min, new_y_min, new_x_max, new_y_max)
712 }
713
714 /// Checks if this bounding box is fully inside another bounding box.
715 ///
716 /// # Arguments
717 ///
718 /// * `container` - The bounding box to check if this box is inside.
719 /// * `tolerance` - Optional tolerance in pixels for boundary checks (default: 0.0).
720 ///
721 /// # Returns
722 ///
723 /// `true` if this bounding box is fully contained within the container, `false` otherwise.
724 pub fn is_fully_inside(&self, container: &BoundingBox, tolerance: f32) -> bool {
725 let self_x_min = self.x_min();
726 let self_y_min = self.y_min();
727 let self_x_max = self.x_max();
728 let self_y_max = self.y_max();
729
730 let cont_x_min = container.x_min();
731 let cont_y_min = container.y_min();
732 let cont_x_max = container.x_max();
733 let cont_y_max = container.y_max();
734
735 self_x_min + tolerance >= cont_x_min
736 && self_y_min + tolerance >= cont_y_min
737 && self_x_max - tolerance <= cont_x_max
738 && self_y_max - tolerance <= cont_y_max
739 }
740
741 /// Checks if this bounding box overlaps with another bounding box.
742 ///
743 /// Two boxes are considered overlapping if their intersection has both width and height
744 /// greater than the specified threshold.
745 ///
746 /// This follows standard approach for checking box overlap.
747 ///
748 /// # Arguments
749 ///
750 /// * `other` - The other bounding box to check overlap with.
751 /// * `threshold` - Minimum intersection dimension (default: 3.0 pixels).
752 ///
753 /// # Returns
754 ///
755 /// `true` if the boxes overlap significantly, `false` otherwise.
756 pub fn overlaps_with(&self, other: &BoundingBox, threshold: f32) -> bool {
757 let x1_min = self.x_min();
758 let y1_min = self.y_min();
759 let x1_max = self.x_max();
760 let y1_max = self.y_max();
761
762 let x2_min = other.x_min();
763 let y2_min = other.y_min();
764 let x2_max = other.x_max();
765 let y2_max = other.y_max();
766
767 // Compute intersection rectangle
768 let inter_x_min = x1_min.max(x2_min);
769 let inter_y_min = y1_min.max(y2_min);
770 let inter_x_max = x1_max.min(x2_max);
771 let inter_y_max = y1_max.min(y2_max);
772
773 // Compute intersection dimensions
774 let inter_width = inter_x_max - inter_x_min;
775 let inter_height = inter_y_max - inter_y_min;
776
777 // Check if intersection is significant
778 inter_width > threshold && inter_height > threshold
779 }
780
781 /// Rotates this bounding box to compensate for document orientation correction.
782 ///
783 /// When a document is rotated during preprocessing (e.g., 90°, 180°, 270°),
784 /// detection boxes are in the rotated image's coordinate system. This method
785 /// transforms boxes back to the original image's coordinate system.
786 ///
787 /// # Arguments
788 ///
789 /// * `rotation_angle` - The rotation angle that was applied to correct the image (0°, 90°, 180°, 270°)
790 /// * `rotated_width` - Width of the image after rotation (i.e., the corrected image width)
791 /// * `rotated_height` - Height of the image after rotation (i.e., the corrected image height)
792 ///
793 /// # Returns
794 ///
795 /// A new `BoundingBox` with points transformed back to the original coordinate system.
796 ///
797 /// # Note
798 ///
799 /// The rotation transformations are:
800 /// - 90° correction: boxes rotated 90° clockwise (original was 90° counter-clockwise)
801 /// - 180° correction: boxes rotated 180°
802 /// - 270° correction: boxes rotated 270° clockwise (original was 270° counter-clockwise)
803 pub fn rotate_back_to_original(
804 &self,
805 rotation_angle: f32,
806 rotated_width: u32,
807 rotated_height: u32,
808 ) -> BoundingBox {
809 let angle = rotation_angle as i32;
810
811 let transformed_points: Vec<Point> = self
812 .points
813 .iter()
814 .map(|p| match angle {
815 90 => {
816 // Image was rotated 270° counter-clockwise (or 90° clockwise) to correct
817 // Inverse: rotate box 90° clockwise
818 // (x, y) in rotated → (rotated_height - 1 - y, x) in original
819 Point::new(rotated_height as f32 - 1.0 - p.y, p.x)
820 }
821 180 => {
822 // Image was rotated 180° to correct
823 // Inverse: rotate box 180°
824 // (x, y) in rotated → (rotated_width - 1 - x, rotated_height - 1 - y) in original
825 Point::new(
826 rotated_width as f32 - 1.0 - p.x,
827 rotated_height as f32 - 1.0 - p.y,
828 )
829 }
830 270 => {
831 // Image was rotated 90° counter-clockwise (or 270° clockwise) to correct
832 // Inverse: rotate box 270° clockwise (or 90° counter-clockwise)
833 // (x, y) in rotated → (y, rotated_width - 1 - x) in original
834 Point::new(p.y, rotated_width as f32 - 1.0 - p.x)
835 }
836 _ => {
837 // No rotation (0° or unknown)
838 *p
839 }
840 })
841 .collect();
842
843 BoundingBox::new(transformed_points)
844 }
845}
846
847/// A rectangle with minimum area that encloses a shape.
848#[derive(Debug, Clone, Serialize, Deserialize)]
849pub struct MinAreaRect {
850 /// The center point of the rectangle.
851 pub center: Point,
852 /// The width of the rectangle.
853 pub width: f32,
854 /// The height of the rectangle.
855 pub height: f32,
856 /// The rotation angle of the rectangle in degrees.
857 pub angle: f32,
858}
859
860impl MinAreaRect {
861 /// Gets the four corner points of the rectangle.
862 ///
863 /// # Returns
864 ///
865 /// A vector containing the four corner points of the rectangle ordered as:
866 /// top-left, top-right, bottom-right, bottom-left in the final image coordinate system.
867 pub fn get_box_points(&self) -> Vec<Point> {
868 let cos_a = (self.angle * PI / 180.0).cos();
869 let sin_a = (self.angle * PI / 180.0).sin();
870
871 let w_2 = self.width / 2.0;
872 let h_2 = self.height / 2.0;
873
874 let corners = [(-w_2, -h_2), (w_2, -h_2), (w_2, h_2), (-w_2, h_2)];
875
876 let mut points: Vec<Point> = corners
877 .iter()
878 .map(|(x, y)| {
879 let rotated_x = x * cos_a - y * sin_a + self.center.x;
880 let rotated_y = x * sin_a + y * cos_a + self.center.y;
881 Point::new(rotated_x, rotated_y)
882 })
883 .collect();
884
885 // Sort points to ensure consistent ordering: top-left, top-right, bottom-right, bottom-left
886 Self::sort_box_points(&mut points);
887 points
888 }
889
890 /// Sorts four points to ensure consistent ordering for OCR bounding boxes.
891 ///
892 /// Orders points as: top-left, top-right, bottom-right, bottom-left
893 /// based on their actual coordinates in the image space.
894 ///
895 /// This algorithm works by:
896 /// 1. Finding the centroid of the four points
897 /// 2. Classifying each point based on its position relative to the centroid
898 /// 3. Assigning points to corners based on their quadrant
899 ///
900 /// # Arguments
901 ///
902 /// * `points` - A mutable reference to a vector of exactly 4 points
903 fn sort_box_points(points: &mut [Point]) {
904 if points.len() != 4 {
905 return;
906 }
907
908 // Calculate the centroid of the four points
909 let center_x = points.iter().map(|p| p.x).sum::<f32>() / 4.0;
910 let center_y = points.iter().map(|p| p.y).sum::<f32>() / 4.0;
911
912 // Create a vector to store points with their classifications
913 let mut classified_points = Vec::with_capacity(4);
914
915 for point in points.iter() {
916 let is_left = point.x < center_x;
917 let is_top = point.y < center_y;
918
919 let corner_type = match (is_left, is_top) {
920 (true, true) => 0, // top-left
921 (false, true) => 1, // top-right
922 (false, false) => 2, // bottom-right
923 (true, false) => 3, // bottom-left
924 };
925
926 classified_points.push((corner_type, *point));
927 }
928
929 // Sort by corner type to get the desired order
930 classified_points.sort_by_key(|&(corner_type, _)| corner_type);
931
932 // Handle the case where multiple points might be classified as the same corner
933 // This can happen with very thin or rotated rectangles
934 let mut corner_types = HashSet::new();
935 for (corner_type, _) in &classified_points {
936 corner_types.insert(*corner_type);
937 }
938
939 if corner_types.len() < 4 {
940 // Fallback to a more robust method using angles from centroid
941 Self::sort_box_points_by_angle(points, center_x, center_y);
942 } else {
943 // Update the original points vector with the sorted points
944 for (i, (_, point)) in classified_points.iter().enumerate() {
945 points[i] = *point;
946 }
947 }
948 }
949
950 /// Fallback sorting method using polar angles from the centroid.
951 ///
952 /// # Arguments
953 ///
954 /// * `points` - A mutable reference to a vector of exactly 4 points
955 /// * `center_x` - X coordinate of the centroid
956 /// * `center_y` - Y coordinate of the centroid
957 fn sort_box_points_by_angle(points: &mut [Point], center_x: f32, center_y: f32) {
958 // Calculate angle from centroid to each point
959 let mut points_with_angles: Vec<(f32, Point)> = points
960 .iter()
961 .map(|p| {
962 let angle = f32::atan2(p.y - center_y, p.x - center_x);
963 // Normalize angle to [0, 2π) and adjust so that top-left is first
964 let normalized_angle = if angle < -PI / 2.0 {
965 angle + 2.0 * PI
966 } else {
967 angle
968 };
969 (normalized_angle, *p)
970 })
971 .collect();
972
973 // Sort by angle (starting from top-left, going clockwise)
974 points_with_angles
975 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
976
977 // Find the starting point (closest to top-left quadrant)
978 let mut start_idx = 0;
979 let mut min_top_left_score = f32::MAX;
980
981 for (i, (_, point)) in points_with_angles.iter().enumerate() {
982 // Score based on distance from theoretical top-left position
983 let top_left_score =
984 (point.x - center_x + 100.0).powi(2) + (point.y - center_y + 100.0).powi(2);
985 if top_left_score < min_top_left_score {
986 min_top_left_score = top_left_score;
987 start_idx = i;
988 }
989 }
990
991 // Reorder starting from the identified top-left point
992 for (i, point) in points.iter_mut().enumerate().take(4) {
993 let src_idx = (start_idx + i) % 4;
994 *point = points_with_angles[src_idx].1;
995 }
996 }
997
998 /// Gets the length of the shorter side of the rectangle.
999 ///
1000 /// # Returns
1001 ///
1002 /// The length of the shorter side.
1003 pub fn min_side(&self) -> f32 {
1004 self.width.min(self.height)
1005 }
1006}
1007
1008/// A buffer for processing scanlines in polygon rasterization.
1009pub(crate) struct ScanlineBuffer {
1010 /// Intersections of the scanline with polygon edges.
1011 pub(crate) intersections: Vec<f32>,
1012}
1013
1014impl ScanlineBuffer {
1015 /// Creates a new scanline buffer with the specified capacity.
1016 ///
1017 /// # Arguments
1018 ///
1019 /// * `max_polygon_points` - The maximum number of polygon points, used to pre-allocate memory.
1020 ///
1021 /// # Returns
1022 ///
1023 /// A new `ScanlineBuffer` instance.
1024 pub(crate) fn new(max_polygon_points: usize) -> Self {
1025 Self {
1026 intersections: Vec::with_capacity(max_polygon_points),
1027 }
1028 }
1029
1030 /// Processes a scanline by finding intersections with polygon edges and accumulating scores.
1031 ///
1032 /// # Arguments
1033 ///
1034 /// * `y` - The y-coordinate of the scanline.
1035 /// * `bbox` - The bounding box representing the polygon.
1036 /// * `start_x` - The starting x-coordinate for processing.
1037 /// * `end_x` - The ending x-coordinate for processing.
1038 /// * `pred` - A 2D array of prediction scores.
1039 ///
1040 /// # Returns
1041 ///
1042 /// A tuple containing:
1043 /// * The accumulated line score
1044 /// * The number of pixels processed
1045 pub(crate) fn process_scanline(
1046 &mut self,
1047 y: f32,
1048 bbox: &BoundingBox,
1049 start_x: usize,
1050 end_x: usize,
1051 pred: &ndarray::ArrayView2<f32>,
1052 ) -> (f32, usize) {
1053 // Clear previous intersections
1054 self.intersections.clear();
1055
1056 // Find intersections of the scanline with polygon edges
1057 let n = bbox.points.len();
1058 for i in 0..n {
1059 let j = (i + 1) % n;
1060 let p1 = &bbox.points[i];
1061 let p2 = &bbox.points[j];
1062
1063 // Check if the edge crosses the scanline
1064 if ((p1.y <= y && y < p2.y) || (p2.y <= y && y < p1.y))
1065 && (p2.y - p1.y).abs() > f32::EPSILON
1066 {
1067 let x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
1068 self.intersections.push(x);
1069 }
1070 }
1071
1072 // Sort intersections by x-coordinate
1073 self.intersections
1074 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1075
1076 let mut line_score = 0.0;
1077 let mut line_pixels = 0;
1078
1079 // Process pairs of intersections (segments of the scanline inside the polygon)
1080 for chunk in self.intersections.chunks(2) {
1081 if chunk.len() == 2 {
1082 let x1 = chunk[0].max(start_x as f32) as usize;
1083 let x2 = chunk[1].min(end_x as f32) as usize;
1084
1085 // Accumulate scores for pixels within the segment
1086 if x1 < x2 && x1 >= start_x && x2 <= end_x {
1087 for x in x1..x2 {
1088 if (y as usize) < pred.shape()[0] && x < pred.shape()[1] {
1089 line_score += pred[[y as usize, x]];
1090 line_pixels += 1;
1091 }
1092 }
1093 }
1094 }
1095 }
1096
1097 (line_score, line_pixels)
1098 }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use super::*;
1104
1105 #[test]
1106 fn test_bounding_box_x_max_y_max() {
1107 let bbox = BoundingBox::from_coords(10.0, 20.0, 100.0, 80.0);
1108 assert_eq!(bbox.x_min(), 10.0);
1109 assert_eq!(bbox.y_min(), 20.0);
1110 assert_eq!(bbox.x_max(), 100.0);
1111 assert_eq!(bbox.y_max(), 80.0);
1112 }
1113
1114 #[test]
1115 fn test_bounding_box_iou() {
1116 // Two overlapping boxes
1117 let bbox1 = BoundingBox::from_coords(0.0, 0.0, 10.0, 10.0);
1118 let bbox2 = BoundingBox::from_coords(5.0, 5.0, 15.0, 15.0);
1119
1120 // Intersection area: 5x5 = 25
1121 // Union area: 100 + 100 - 25 = 175
1122 // IoU: 25/175 ≈ 0.1428
1123 let iou = bbox1.iou(&bbox2);
1124 assert!((iou - 0.1428).abs() < 0.01, "IoU: {}", iou);
1125
1126 // Same box should have IoU of 1.0
1127 let iou_same = bbox1.iou(&bbox1);
1128 assert!((iou_same - 1.0).abs() < 0.001, "IoU same: {}", iou_same);
1129
1130 // Non-overlapping boxes should have IoU of 0.0
1131 let bbox3 = BoundingBox::from_coords(20.0, 20.0, 30.0, 30.0);
1132 let iou_none = bbox1.iou(&bbox3);
1133 assert_eq!(iou_none, 0.0, "IoU non-overlapping: {}", iou_none);
1134 }
1135
1136 #[test]
1137 fn test_bounding_box_is_fully_inside() {
1138 let container = BoundingBox::from_coords(0.0, 0.0, 100.0, 100.0);
1139 let inner = BoundingBox::from_coords(10.0, 10.0, 50.0, 50.0);
1140 let partial = BoundingBox::from_coords(80.0, 80.0, 120.0, 120.0);
1141 let outside = BoundingBox::from_coords(110.0, 110.0, 150.0, 150.0);
1142
1143 // Inner box should be fully inside
1144 assert!(inner.is_fully_inside(&container, 0.0));
1145
1146 // Partial overlap should not be fully inside
1147 assert!(!partial.is_fully_inside(&container, 0.0));
1148
1149 // Outside box should not be fully inside
1150 assert!(!outside.is_fully_inside(&container, 0.0));
1151
1152 // Test with tolerance
1153 let almost_inside = BoundingBox::from_coords(1.0, 1.0, 99.0, 99.0);
1154 assert!(almost_inside.is_fully_inside(&container, 0.0));
1155 assert!(almost_inside.is_fully_inside(&container, 2.0));
1156 }
1157
1158 #[test]
1159 fn test_bounding_box_iou_with_table_region() {
1160 // Simulate a table region and cell detections
1161 let table_region = BoundingBox::from_coords(50.0, 50.0, 200.0, 200.0);
1162
1163 // Cell fully inside table
1164 let cell_inside = BoundingBox::from_coords(60.0, 60.0, 100.0, 100.0);
1165 assert!(cell_inside.is_fully_inside(&table_region, 0.0));
1166 assert!(cell_inside.iou(&table_region) > 0.0);
1167
1168 // Cell with significant overlap (IoU > 0.5)
1169 let cell_overlap = BoundingBox::from_coords(40.0, 40.0, 150.0, 150.0);
1170 let iou_overlap = cell_overlap.iou(&table_region);
1171 // This cell should have reasonable overlap
1172 assert!(iou_overlap > 0.3, "IoU: {}", iou_overlap);
1173
1174 // Cell outside table
1175 let cell_outside = BoundingBox::from_coords(250.0, 250.0, 300.0, 300.0);
1176 assert!(!cell_outside.is_fully_inside(&table_region, 0.0));
1177 assert_eq!(cell_outside.iou(&table_region), 0.0);
1178 }
1179
1180 #[test]
1181 fn test_bounding_box_overlaps_with() {
1182 // Two boxes with significant overlap
1183 let box1 = BoundingBox::from_coords(0.0, 0.0, 100.0, 100.0);
1184 let box2 = BoundingBox::from_coords(50.0, 50.0, 150.0, 150.0);
1185
1186 // Overlap width and height are both 50, which is > 3
1187 assert!(box1.overlaps_with(&box2, 3.0));
1188 assert!(box2.overlaps_with(&box1, 3.0));
1189
1190 // Boxes with minimal overlap (< 3 pixels)
1191 let box3 = BoundingBox::from_coords(99.0, 99.0, 150.0, 150.0);
1192 assert!(!box1.overlaps_with(&box3, 3.0));
1193
1194 // Non-overlapping boxes
1195 let box4 = BoundingBox::from_coords(200.0, 200.0, 300.0, 300.0);
1196 assert!(!box1.overlaps_with(&box4, 3.0));
1197
1198 // Adjacent boxes (touching but not overlapping)
1199 let box5 = BoundingBox::from_coords(100.0, 0.0, 200.0, 100.0);
1200 assert!(!box1.overlaps_with(&box5, 3.0));
1201 }
1202
1203 #[test]
1204 fn test_bounding_box_rotate_back_to_original_0_degrees_is_identity() {
1205 let bbox = BoundingBox::from_coords(0.0, 1.0, 2.0, 3.0);
1206 let rotated = bbox.rotate_back_to_original(0.0, 10, 20);
1207 assert_eq!(rotated.points, bbox.points);
1208 }
1209
1210 #[test]
1211 fn test_bounding_box_rotate_back_to_original_90_degrees() {
1212 // Rotated image dimensions (after correction rotation): width=3, height=4.
1213 let rotated_width = 3;
1214 let rotated_height = 4;
1215 let bbox = BoundingBox::from_coords(0.0, 0.0, 1.0, 1.0);
1216 let rotated = bbox.rotate_back_to_original(90.0, rotated_width, rotated_height);
1217
1218 // angle=90 inverse mapping: (x, y) -> (rotated_height-1-y, x)
1219 let expected = BoundingBox::new(vec![
1220 Point::new(3.0, 0.0),
1221 Point::new(3.0, 1.0),
1222 Point::new(2.0, 1.0),
1223 Point::new(2.0, 0.0),
1224 ]);
1225 assert_eq!(rotated.points, expected.points);
1226 }
1227
1228 #[test]
1229 fn test_bounding_box_rotate_back_to_original_180_degrees() {
1230 let rotated_width = 4;
1231 let rotated_height = 3;
1232 let bbox = BoundingBox::from_coords(1.0, 1.0, 2.0, 2.0);
1233 let rotated = bbox.rotate_back_to_original(180.0, rotated_width, rotated_height);
1234
1235 // angle=180 inverse mapping: (x, y) -> (rotated_width-1-x, rotated_height-1-y)
1236 let expected = BoundingBox::new(vec![
1237 Point::new(2.0, 1.0),
1238 Point::new(1.0, 1.0),
1239 Point::new(1.0, 0.0),
1240 Point::new(2.0, 0.0),
1241 ]);
1242 assert_eq!(rotated.points, expected.points);
1243 }
1244
1245 #[test]
1246 fn test_bounding_box_rotate_back_to_original_270_degrees() {
1247 // Rotated image dimensions (after correction rotation): width=3, height=4.
1248 let rotated_width = 3;
1249 let rotated_height = 4;
1250 let bbox = BoundingBox::from_coords(0.0, 0.0, 1.0, 1.0);
1251 let rotated = bbox.rotate_back_to_original(270.0, rotated_width, rotated_height);
1252
1253 // angle=270 inverse mapping: (x, y) -> (y, rotated_width-1-x)
1254 let expected = BoundingBox::new(vec![
1255 Point::new(0.0, 2.0),
1256 Point::new(0.0, 1.0),
1257 Point::new(1.0, 1.0),
1258 Point::new(1.0, 2.0),
1259 ]);
1260 assert_eq!(rotated.points, expected.points);
1261 }
1262}