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}