Skip to main content

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}