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 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}