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