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