oar_ocr/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)]
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    /// Creates a bounding box from a contour.
110    ///
111    /// # Arguments
112    ///
113    /// * `contour` - A reference to a contour from imageproc.
114    ///
115    /// # Returns
116    ///
117    /// A new `BoundingBox` instance with points converted from the contour.
118    pub fn from_contour(contour: &Contour<u32>) -> Self {
119        let points = contour
120            .points
121            .iter()
122            .map(|p| Point::new(p.x as f32, p.y as f32))
123            .collect();
124        Self { points }
125    }
126
127    /// Calculates the area of the bounding box using the shoelace formula.
128    ///
129    /// # Returns
130    ///
131    /// The area of the bounding box. Returns 0.0 if the bounding box has fewer than 3 points.
132    pub fn area(&self) -> f32 {
133        if self.points.len() < 3 {
134            return 0.0;
135        }
136
137        let mut area = 0.0;
138        let n = self.points.len();
139        for i in 0..n {
140            let j = (i + 1) % n;
141            area += self.points[i].x * self.points[j].y;
142            area -= self.points[j].x * self.points[i].y;
143        }
144        area.abs() / 2.0
145    }
146
147    /// Calculates the perimeter of the bounding box.
148    ///
149    /// # Returns
150    ///
151    /// The perimeter of the bounding box.
152    pub fn perimeter(&self) -> f32 {
153        let mut perimeter = 0.0;
154        let n = self.points.len();
155        for i in 0..n {
156            let j = (i + 1) % n;
157            let dx = self.points[j].x - self.points[i].x;
158            let dy = self.points[j].y - self.points[i].y;
159            perimeter += (dx * dx + dy * dy).sqrt();
160        }
161        perimeter
162    }
163
164    /// Gets the minimum x-coordinate of all points in the bounding box.
165    ///
166    /// # Returns
167    ///
168    /// The minimum x-coordinate, or 0.0 if there are no points.
169    pub fn x_min(&self) -> f32 {
170        if self.points.is_empty() {
171            return 0.0;
172        }
173        self.points
174            .iter()
175            .map(|p| p.x)
176            .fold(f32::INFINITY, f32::min)
177    }
178
179    /// Gets the minimum y-coordinate of all points in the bounding box.
180    ///
181    /// # Returns
182    ///
183    /// The minimum y-coordinate, or 0.0 if there are no points.
184    pub fn y_min(&self) -> f32 {
185        if self.points.is_empty() {
186            return 0.0;
187        }
188        self.points
189            .iter()
190            .map(|p| p.y)
191            .fold(f32::INFINITY, f32::min)
192    }
193
194    /// Computes the convex hull of the bounding box using Graham's scan algorithm.
195    ///
196    /// # Returns
197    ///
198    /// A new `BoundingBox` representing the convex hull. If the bounding box has fewer than 3 points,
199    /// returns a clone of the original bounding box.
200    fn convex_hull(&self) -> BoundingBox {
201        if self.points.len() < 3 {
202            return self.clone();
203        }
204
205        let mut points = self.points.clone();
206
207        // Find the point with the lowest y-coordinate (and leftmost if tied)
208        let mut start_idx = 0;
209        for i in 1..points.len() {
210            if points[i].y < points[start_idx].y
211                || (points[i].y == points[start_idx].y && points[i].x < points[start_idx].x)
212            {
213                start_idx = i;
214            }
215        }
216        points.swap(0, start_idx);
217        let start_point = points[0];
218
219        // Sort points by polar angle with respect to the start point
220        points[1..].sort_by(|a, b| {
221            let cross = Self::cross_product(&start_point, a, b);
222            if cross == 0.0 {
223                // If points are collinear, sort by distance from start point
224                let dist_a = (a.x - start_point.x).powi(2) + (a.y - start_point.y).powi(2);
225                let dist_b = (b.x - start_point.x).powi(2) + (b.y - start_point.y).powi(2);
226                dist_a
227                    .partial_cmp(&dist_b)
228                    .unwrap_or(std::cmp::Ordering::Equal)
229            } else if cross > 0.0 {
230                // Counter-clockwise turn
231                std::cmp::Ordering::Less
232            } else {
233                // Clockwise turn
234                std::cmp::Ordering::Greater
235            }
236        });
237
238        // Build the convex hull using a stack
239        let mut hull = Vec::new();
240        for point in points {
241            // Remove points that make clockwise turns
242            while hull.len() > 1
243                && Self::cross_product(&hull[hull.len() - 2], &hull[hull.len() - 1], &point) <= 0.0
244            {
245                hull.pop();
246            }
247            hull.push(point);
248        }
249
250        BoundingBox::new(hull)
251    }
252
253    /// Computes the cross product of three points.
254    ///
255    /// # Arguments
256    ///
257    /// * `p1` - The first point.
258    /// * `p2` - The second point.
259    /// * `p3` - The third point.
260    ///
261    /// # Returns
262    ///
263    /// The cross product value. A positive value indicates a counter-clockwise turn,
264    /// a negative value indicates a clockwise turn, and zero indicates collinearity.
265    fn cross_product(p1: &Point, p2: &Point, p3: &Point) -> f32 {
266        (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
267    }
268
269    /// Computes the minimum area rectangle that encloses the bounding box.
270    ///
271    /// This method uses the rotating calipers algorithm on the convex hull of the bounding box
272    /// to find the minimum area rectangle.
273    ///
274    /// # Returns
275    ///
276    /// A `MinAreaRect` representing the minimum area rectangle. If the bounding box has fewer than
277    /// 3 points, returns a rectangle with zero dimensions.
278    pub fn get_min_area_rect(&self) -> MinAreaRect {
279        if self.points.len() < 3 {
280            return MinAreaRect {
281                center: Point::new(0.0, 0.0),
282                width: 0.0,
283                height: 0.0,
284                angle: 0.0,
285            };
286        }
287
288        // Get the convex hull of the bounding box
289        let hull = self.convex_hull();
290        let hull_points = &hull.points;
291
292        // Handle degenerate cases
293        if hull_points.len() < 3 {
294            let (min_x, max_x) = match self.points.iter().map(|p| p.x).minmax().into_option() {
295                Some((min, max)) => (min, max),
296                None => {
297                    return MinAreaRect {
298                        center: Point::new(0.0, 0.0),
299                        width: 0.0,
300                        height: 0.0,
301                        angle: 0.0,
302                    };
303                }
304            };
305
306            let (min_y, max_y) = match self.points.iter().map(|p| p.y).minmax().into_option() {
307                Some((min, max)) => (min, max),
308                None => {
309                    return MinAreaRect {
310                        center: Point::new(0.0, 0.0),
311                        width: 0.0,
312                        height: 0.0,
313                        angle: 0.0,
314                    };
315                }
316            };
317
318            let center = Point::new((min_x + max_x) / 2.0, (min_y + max_y) / 2.0);
319            let width = max_x - min_x;
320            let height = max_y - min_y;
321
322            return MinAreaRect {
323                center,
324                width,
325                height,
326                angle: 0.0,
327            };
328        }
329
330        // Find the minimum area rectangle using rotating calipers
331        let mut min_area = f32::MAX;
332        let mut min_rect = MinAreaRect {
333            center: Point::new(0.0, 0.0),
334            width: 0.0,
335            height: 0.0,
336            angle: 0.0,
337        };
338
339        let n = hull_points.len();
340        for i in 0..n {
341            let j = (i + 1) % n;
342
343            // Calculate the edge vector
344            let edge_x = hull_points[j].x - hull_points[i].x;
345            let edge_y = hull_points[j].y - hull_points[i].y;
346            let edge_length = (edge_x * edge_x + edge_y * edge_y).sqrt();
347
348            // Skip degenerate edges
349            if edge_length < f32::EPSILON {
350                continue;
351            }
352
353            // Normalize the edge vector
354            let nx = edge_x / edge_length;
355            let ny = edge_y / edge_length;
356
357            // Calculate the perpendicular vector
358            let px = -ny;
359            let py = nx;
360
361            // Project all points onto the edge and perpendicular vectors
362            let mut min_n = f32::MAX;
363            let mut max_n = f32::MIN;
364            let mut min_p = f32::MAX;
365            let mut max_p = f32::MIN;
366
367            for k in 0..n {
368                let point = &hull_points[k];
369
370                let proj_n = nx * (point.x - hull_points[i].x) + ny * (point.y - hull_points[i].y);
371                min_n = min_n.min(proj_n);
372                max_n = max_n.max(proj_n);
373
374                let proj_p = px * (point.x - hull_points[i].x) + py * (point.y - hull_points[i].y);
375                min_p = min_p.min(proj_p);
376                max_p = max_p.max(proj_p);
377            }
378
379            // Calculate the width, height, and area of the rectangle
380            let width = max_n - min_n;
381            let height = max_p - min_p;
382            let area = width * height;
383
384            // Update the minimum area rectangle if this one is smaller
385            if area < min_area {
386                min_area = area;
387
388                let center_n = (min_n + max_n) / 2.0;
389                let center_p = (min_p + max_p) / 2.0;
390
391                let center_x = hull_points[i].x + center_n * nx + center_p * px;
392                let center_y = hull_points[i].y + center_n * ny + center_p * py;
393
394                let angle_rad = f32::atan2(ny, nx);
395                let angle_deg = angle_rad * 180.0 / PI;
396
397                min_rect = MinAreaRect {
398                    center: Point::new(center_x, center_y),
399                    width,
400                    height,
401                    angle: angle_deg,
402                };
403            }
404        }
405
406        min_rect
407    }
408
409    /// Approximates a polygon using the Douglas-Peucker algorithm.
410    ///
411    /// # Arguments
412    ///
413    /// * `epsilon` - The maximum distance between the original curve and the simplified curve.
414    ///
415    /// # Returns
416    ///
417    /// A new `BoundingBox` with simplified points. If the bounding box has 2 or fewer points,
418    /// returns a clone of the original bounding box.
419    pub fn approx_poly_dp(&self, epsilon: f32) -> BoundingBox {
420        if self.points.len() <= 2 {
421            return self.clone();
422        }
423
424        let mut simplified = Vec::new();
425        self.douglas_peucker(&self.points, epsilon, &mut simplified);
426
427        BoundingBox::new(simplified)
428    }
429
430    /// Implements the Douglas-Peucker algorithm for curve simplification.
431    ///
432    /// # Arguments
433    ///
434    /// * `points` - The points to simplify.
435    /// * `epsilon` - The maximum distance between the original curve and the simplified curve.
436    /// * `result` - A mutable reference to a vector where the simplified points will be stored.
437    fn douglas_peucker(&self, points: &[Point], epsilon: f32, result: &mut Vec<Point>) {
438        if points.len() <= 2 {
439            result.extend_from_slice(points);
440            return;
441        }
442
443        // Initialize a stack for iterative implementation
444        let mut stack = Vec::new();
445        stack.push((0, points.len() - 1));
446
447        // Track which points to keep
448        let mut keep = vec![false; points.len()];
449        keep[0] = true;
450        keep[points.len() - 1] = true;
451
452        // Process the stack
453        const MAX_ITERATIONS: usize = 10000;
454        let mut iterations = 0;
455
456        while let Some((start, end)) = stack.pop() {
457            iterations += 1;
458            // Prevent infinite loops
459            if iterations > MAX_ITERATIONS {
460                keep.iter_mut()
461                    .take(end + 1)
462                    .skip(start)
463                    .for_each(|k| *k = true);
464                break;
465            }
466
467            // Skip segments with only 2 points
468            if end - start <= 1 {
469                continue;
470            }
471
472            // Find the point with maximum distance from the line segment
473            let mut max_dist = 0.0;
474            let mut max_index = start;
475
476            for i in (start + 1)..end {
477                let dist = self.point_to_line_distance(&points[i], &points[start], &points[end]);
478                if dist > max_dist {
479                    max_dist = dist;
480                    max_index = i;
481                }
482            }
483
484            // If the maximum distance exceeds epsilon, split the segment
485            if max_dist > epsilon {
486                keep[max_index] = true;
487
488                if max_index - start > 1 {
489                    stack.push((start, max_index));
490                }
491                if end - max_index > 1 {
492                    stack.push((max_index, end));
493                }
494            }
495        }
496
497        // Collect the points to keep
498        for (i, &should_keep) in keep.iter().enumerate() {
499            if should_keep {
500                result.push(points[i]);
501            }
502        }
503    }
504
505    /// Calculates the perpendicular distance from a point to a line segment.
506    ///
507    /// # Arguments
508    ///
509    /// * `point` - The point to calculate the distance for.
510    /// * `line_start` - The start point of the line segment.
511    /// * `line_end` - The end point of the line segment.
512    ///
513    /// # Returns
514    ///
515    /// The perpendicular distance from the point to the line segment.
516    fn point_to_line_distance(&self, point: &Point, line_start: &Point, line_end: &Point) -> f32 {
517        let a = line_end.y - line_start.y;
518        let b = line_start.x - line_end.x;
519        let c = line_end.x * line_start.y - line_start.x * line_end.y;
520
521        let denominator = (a * a + b * b).sqrt();
522        if denominator == 0.0 {
523            return 0.0;
524        }
525
526        (a * point.x + b * point.y + c).abs() / denominator
527    }
528}
529
530/// A rectangle with minimum area that encloses a shape.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct MinAreaRect {
533    /// The center point of the rectangle.
534    pub center: Point,
535    /// The width of the rectangle.
536    pub width: f32,
537    /// The height of the rectangle.
538    pub height: f32,
539    /// The rotation angle of the rectangle in degrees.
540    pub angle: f32,
541}
542
543impl MinAreaRect {
544    /// Gets the four corner points of the rectangle.
545    ///
546    /// # Returns
547    ///
548    /// A vector containing the four corner points of the rectangle ordered as:
549    /// top-left, top-right, bottom-right, bottom-left in the final image coordinate system.
550    pub fn get_box_points(&self) -> Vec<Point> {
551        let cos_a = (self.angle * PI / 180.0).cos();
552        let sin_a = (self.angle * PI / 180.0).sin();
553
554        let w_2 = self.width / 2.0;
555        let h_2 = self.height / 2.0;
556
557        let corners = [(-w_2, -h_2), (w_2, -h_2), (w_2, h_2), (-w_2, h_2)];
558
559        let mut points: Vec<Point> = corners
560            .iter()
561            .map(|(x, y)| {
562                let rotated_x = x * cos_a - y * sin_a + self.center.x;
563                let rotated_y = x * sin_a + y * cos_a + self.center.y;
564                Point::new(rotated_x, rotated_y)
565            })
566            .collect();
567
568        // Sort points to ensure consistent ordering: top-left, top-right, bottom-right, bottom-left
569        Self::sort_box_points(&mut points);
570        points
571    }
572
573    /// Sorts four points to ensure consistent ordering for OCR bounding boxes.
574    ///
575    /// Orders points as: top-left, top-right, bottom-right, bottom-left
576    /// based on their actual coordinates in the image space.
577    ///
578    /// This algorithm works by:
579    /// 1. Finding the centroid of the four points
580    /// 2. Classifying each point based on its position relative to the centroid
581    /// 3. Assigning points to corners based on their quadrant
582    ///
583    /// # Arguments
584    ///
585    /// * `points` - A mutable reference to a vector of exactly 4 points
586    fn sort_box_points(points: &mut [Point]) {
587        if points.len() != 4 {
588            return;
589        }
590
591        // Calculate the centroid of the four points
592        let center_x = points.iter().map(|p| p.x).sum::<f32>() / 4.0;
593        let center_y = points.iter().map(|p| p.y).sum::<f32>() / 4.0;
594
595        // Create a vector to store points with their classifications
596        let mut classified_points = Vec::with_capacity(4);
597
598        for point in points.iter() {
599            let is_left = point.x < center_x;
600            let is_top = point.y < center_y;
601
602            let corner_type = match (is_left, is_top) {
603                (true, true) => 0,   // top-left
604                (false, true) => 1,  // top-right
605                (false, false) => 2, // bottom-right
606                (true, false) => 3,  // bottom-left
607            };
608
609            classified_points.push((corner_type, *point));
610        }
611
612        // Sort by corner type to get the desired order
613        classified_points.sort_by_key(|&(corner_type, _)| corner_type);
614
615        // Handle the case where multiple points might be classified as the same corner
616        // This can happen with very thin or rotated rectangles
617        let mut corner_types = HashSet::new();
618        for (corner_type, _) in &classified_points {
619            corner_types.insert(*corner_type);
620        }
621
622        if corner_types.len() < 4 {
623            // Fallback to a more robust method using angles from centroid
624            Self::sort_box_points_by_angle(points, center_x, center_y);
625        } else {
626            // Update the original points vector with the sorted points
627            for (i, (_, point)) in classified_points.iter().enumerate() {
628                points[i] = *point;
629            }
630        }
631    }
632
633    /// Fallback sorting method using polar angles from the centroid.
634    ///
635    /// # Arguments
636    ///
637    /// * `points` - A mutable reference to a vector of exactly 4 points
638    /// * `center_x` - X coordinate of the centroid
639    /// * `center_y` - Y coordinate of the centroid
640    fn sort_box_points_by_angle(points: &mut [Point], center_x: f32, center_y: f32) {
641        // Calculate angle from centroid to each point
642        let mut points_with_angles: Vec<(f32, Point)> = points
643            .iter()
644            .map(|p| {
645                let angle = f32::atan2(p.y - center_y, p.x - center_x);
646                // Normalize angle to [0, 2π) and adjust so that top-left is first
647                let normalized_angle = if angle < -PI / 2.0 {
648                    angle + 2.0 * PI
649                } else {
650                    angle
651                };
652                (normalized_angle, *p)
653            })
654            .collect();
655
656        // Sort by angle (starting from top-left, going clockwise)
657        points_with_angles
658            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
659
660        // Find the starting point (closest to top-left quadrant)
661        let mut start_idx = 0;
662        let mut min_top_left_score = f32::MAX;
663
664        for (i, (_, point)) in points_with_angles.iter().enumerate() {
665            // Score based on distance from theoretical top-left position
666            let top_left_score =
667                (point.x - center_x + 100.0).powi(2) + (point.y - center_y + 100.0).powi(2);
668            if top_left_score < min_top_left_score {
669                min_top_left_score = top_left_score;
670                start_idx = i;
671            }
672        }
673
674        // Reorder starting from the identified top-left point
675        for (i, point) in points.iter_mut().enumerate().take(4) {
676            let src_idx = (start_idx + i) % 4;
677            *point = points_with_angles[src_idx].1;
678        }
679    }
680
681    /// Gets the length of the shorter side of the rectangle.
682    ///
683    /// # Returns
684    ///
685    /// The length of the shorter side.
686    pub fn min_side(&self) -> f32 {
687        self.width.min(self.height)
688    }
689}
690
691/// A buffer for processing scanlines in polygon rasterization.
692pub(crate) struct ScanlineBuffer {
693    /// Intersections of the scanline with polygon edges.
694    pub(crate) intersections: Vec<f32>,
695}
696
697impl ScanlineBuffer {
698    /// Creates a new scanline buffer with the specified capacity.
699    ///
700    /// # Arguments
701    ///
702    /// * `max_polygon_points` - The maximum number of polygon points, used to pre-allocate memory.
703    ///
704    /// # Returns
705    ///
706    /// A new `ScanlineBuffer` instance.
707    pub(crate) fn new(max_polygon_points: usize) -> Self {
708        Self {
709            intersections: Vec::with_capacity(max_polygon_points),
710        }
711    }
712
713    /// Processes a scanline by finding intersections with polygon edges and accumulating scores.
714    ///
715    /// # Arguments
716    ///
717    /// * `y` - The y-coordinate of the scanline.
718    /// * `bbox` - The bounding box representing the polygon.
719    /// * `start_x` - The starting x-coordinate for processing.
720    /// * `end_x` - The ending x-coordinate for processing.
721    /// * `pred` - A 2D array of prediction scores.
722    ///
723    /// # Returns
724    ///
725    /// A tuple containing:
726    /// * The accumulated line score
727    /// * The number of pixels processed
728    pub(crate) fn process_scanline(
729        &mut self,
730        y: f32,
731        bbox: &BoundingBox,
732        start_x: usize,
733        end_x: usize,
734        pred: &ndarray::Array2<f32>,
735    ) -> (f32, usize) {
736        // Clear previous intersections
737        self.intersections.clear();
738
739        // Find intersections of the scanline with polygon edges
740        let n = bbox.points.len();
741        for i in 0..n {
742            let j = (i + 1) % n;
743            let p1 = &bbox.points[i];
744            let p2 = &bbox.points[j];
745
746            // Check if the edge crosses the scanline
747            if ((p1.y <= y && y < p2.y) || (p2.y <= y && y < p1.y))
748                && (p2.y - p1.y).abs() > f32::EPSILON
749            {
750                let x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
751                self.intersections.push(x);
752            }
753        }
754
755        // Sort intersections by x-coordinate
756        self.intersections
757            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
758
759        let mut line_score = 0.0;
760        let mut line_pixels = 0;
761
762        // Process pairs of intersections (segments of the scanline inside the polygon)
763        for chunk in self.intersections.chunks(2) {
764            if chunk.len() == 2 {
765                let x1 = chunk[0].max(start_x as f32) as usize;
766                let x2 = chunk[1].min(end_x as f32) as usize;
767
768                // Accumulate scores for pixels within the segment
769                if x1 < x2 && x1 >= start_x && x2 <= end_x {
770                    for x in x1..x2 {
771                        if (y as usize) < pred.shape()[0] && x < pred.shape()[1] {
772                            line_score += pred[[y as usize, x]];
773                            line_pixels += 1;
774                        }
775                    }
776                }
777            }
778        }
779
780        (line_score, line_pixels)
781    }
782}