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