1use image::GrayImage;
6use imageproc::contours::{find_contours, Contour};
7use imageproc::point::Point;
8use imageproc::rect::Rect;
9
10#[derive(Debug, Clone)]
12pub struct TextBox {
13 pub rect: Rect,
15 pub score: f32,
17 pub points: Option<[Point<f32>; 4]>,
19}
20
21impl TextBox {
22 pub fn new(rect: Rect, score: f32) -> Self {
24 Self {
25 rect,
26 score,
27 points: None,
28 }
29 }
30
31 pub fn with_points(rect: Rect, score: f32, points: [Point<f32>; 4]) -> Self {
33 Self {
34 rect,
35 score,
36 points: Some(points),
37 }
38 }
39
40 pub fn area(&self) -> u32 {
42 self.rect.width() * self.rect.height()
43 }
44
45 pub fn expand(&self, border: u32, max_width: u32, max_height: u32) -> Self {
47 let x = (self.rect.left() - border as i32).max(0) as u32;
48 let y = (self.rect.top() - border as i32).max(0) as u32;
49 let right = ((self.rect.left() as u32 + self.rect.width()) + border).min(max_width);
50 let bottom = ((self.rect.top() as u32 + self.rect.height()) + border).min(max_height);
51
52 let width = if right > x { right - x } else { 1 };
54 let height = if bottom > y { bottom - y } else { 1 };
55
56 Self {
57 rect: Rect::at(x as i32, y as i32).of_size(width, height),
58 score: self.score,
59 points: self
60 .points
61 .map(|points| expand_ordered_points(points, border as f32, max_width, max_height)),
62 }
63 }
64}
65
66pub fn extract_boxes_from_mask(
77 mask: &[u8],
78 width: u32,
79 height: u32,
80 original_width: u32,
81 original_height: u32,
82 min_area: u32,
83 _box_threshold: f32,
84) -> Vec<TextBox> {
85 extract_boxes_from_mask_with_padding(
86 mask,
87 width,
88 height,
89 width,
90 height,
91 original_width,
92 original_height,
93 min_area,
94 _box_threshold,
95 )
96}
97
98#[allow(clippy::too_many_arguments)]
111pub fn extract_boxes_from_mask_with_padding(
112 mask: &[u8],
113 mask_width: u32,
114 mask_height: u32,
115 valid_width: u32,
116 valid_height: u32,
117 original_width: u32,
118 original_height: u32,
119 min_area: u32,
120 _box_threshold: f32,
121) -> Vec<TextBox> {
122 extract_boxes_with_unclip(
123 mask,
124 mask_width,
125 mask_height,
126 valid_width,
127 valid_height,
128 original_width,
129 original_height,
130 min_area,
131 1.5, )
133}
134
135#[allow(clippy::too_many_arguments)]
140pub fn extract_boxes_with_unclip(
141 mask: &[u8],
142 mask_width: u32,
143 mask_height: u32,
144 valid_width: u32,
145 valid_height: u32,
146 original_width: u32,
147 original_height: u32,
148 min_area: u32,
149 unclip_ratio: f32,
150) -> Vec<TextBox> {
151 let gray_image = GrayImage::from_raw(mask_width, mask_height, mask.to_vec())
153 .unwrap_or_else(|| GrayImage::new(mask_width, mask_height));
154
155 let contours = find_contours::<i32>(&gray_image);
157
158 let scale_x = original_width as f32 / valid_width as f32;
160 let scale_y = original_height as f32 / valid_height as f32;
161
162 let mut boxes = Vec::new();
163
164 for contour in contours {
165 if contour.parent.is_some() {
168 continue;
169 }
170
171 if contour.points.len() < 4 {
172 continue;
173 }
174
175 let contour_points = contour_points_in_valid_region(&contour, valid_width, valid_height);
176 if contour_points.len() < 4 {
177 continue;
178 }
179
180 let Some(rotated_box) = minimum_area_rect(&contour_points) else {
181 continue;
182 };
183
184 if rotated_box.area() < min_area as f32 {
185 continue;
186 }
187
188 let expanded_points = rotated_box
189 .expand(unclip_ratio)
190 .clamped_points(valid_width, valid_height);
191 let scaled_points = scale_and_order_points(
192 expanded_points,
193 scale_x,
194 scale_y,
195 original_width,
196 original_height,
197 );
198
199 if let Some(rect) =
200 rect_from_ordered_points(&scaled_points, original_width, original_height)
201 {
202 boxes.push(TextBox::with_points(rect, 1.0, scaled_points));
203 }
204 }
205
206 boxes
207}
208
209fn contour_points_in_valid_region(
210 contour: &Contour<i32>,
211 valid_width: u32,
212 valid_height: u32,
213) -> Vec<Point<f32>> {
214 let max_x = valid_width.saturating_sub(1) as f32;
215 let max_y = valid_height.saturating_sub(1) as f32;
216
217 contour
218 .points
219 .iter()
220 .filter(|point| point.x >= 0 && point.y >= 0)
221 .filter(|point| point.x < valid_width as i32 && point.y < valid_height as i32)
222 .map(|point| Point::new((point.x as f32).min(max_x), (point.y as f32).min(max_y)))
223 .collect()
224}
225
226#[derive(Debug, Clone, Copy)]
227struct RotatedBox {
228 center: Point<f32>,
229 width: f32,
230 height: f32,
231 angle: f32,
232}
233
234impl RotatedBox {
235 fn area(&self) -> f32 {
236 self.width * self.height
237 }
238
239 fn perimeter(&self) -> f32 {
240 2.0 * (self.width + self.height)
241 }
242
243 fn expand(self, unclip_ratio: f32) -> Self {
244 let distance = (self.area() * unclip_ratio / self.perimeter()).max(1.0);
245 Self {
246 width: self.width + distance * 2.0,
247 height: self.height + distance * 2.0,
248 ..self
249 }
250 }
251
252 fn clamped_points(&self, valid_width: u32, valid_height: u32) -> [Point<f32>; 4] {
253 let cos = self.angle.cos();
254 let sin = self.angle.sin();
255 let half_w = self.width * 0.5;
256 let half_h = self.height * 0.5;
257 let corners = [
258 (-half_w, -half_h),
259 (half_w, -half_h),
260 (half_w, half_h),
261 (-half_w, half_h),
262 ];
263 let max_x = valid_width.saturating_sub(1) as f32;
264 let max_y = valid_height.saturating_sub(1) as f32;
265
266 let points = corners.map(|(x, y)| {
267 Point::new(
268 (self.center.x + x * cos - y * sin).clamp(0.0, max_x),
269 (self.center.y + x * sin + y * cos).clamp(0.0, max_y),
270 )
271 });
272
273 order_points(points)
274 }
275}
276
277fn minimum_area_rect(points: &[Point<f32>]) -> Option<RotatedBox> {
278 let hull = convex_hull(points);
279 if hull.len() < 3 {
280 return None;
281 }
282
283 let mut best: Option<RotatedBox> = None;
284 let mut best_area = f32::INFINITY;
285
286 for i in 0..hull.len() {
287 let p1 = hull[i];
288 let p2 = hull[(i + 1) % hull.len()];
289 let dx = p2.x - p1.x;
290 let dy = p2.y - p1.y;
291 if dx.abs() < f32::EPSILON && dy.abs() < f32::EPSILON {
292 continue;
293 }
294
295 let angle = dy.atan2(dx);
296 let cos = angle.cos();
297 let sin = angle.sin();
298
299 let mut min_x = f32::INFINITY;
300 let mut max_x = f32::NEG_INFINITY;
301 let mut min_y = f32::INFINITY;
302 let mut max_y = f32::NEG_INFINITY;
303
304 for point in &hull {
305 let x = point.x * cos + point.y * sin;
306 let y = -point.x * sin + point.y * cos;
307 min_x = min_x.min(x);
308 max_x = max_x.max(x);
309 min_y = min_y.min(y);
310 max_y = max_y.max(y);
311 }
312
313 let width = max_x - min_x;
314 let height = max_y - min_y;
315 let area = width * height;
316 if width <= 0.0 || height <= 0.0 || area >= best_area {
317 continue;
318 }
319
320 let center_x = (min_x + max_x) * 0.5;
321 let center_y = (min_y + max_y) * 0.5;
322 let center = Point::new(
323 center_x * cos - center_y * sin,
324 center_x * sin + center_y * cos,
325 );
326
327 best_area = area;
328 best = Some(RotatedBox {
329 center,
330 width,
331 height,
332 angle,
333 });
334 }
335
336 best
337}
338
339fn convex_hull(points: &[Point<f32>]) -> Vec<Point<f32>> {
340 let mut sorted = points.to_vec();
341 sorted.sort_by(|a, b| {
342 a.x.partial_cmp(&b.x)
343 .unwrap_or(std::cmp::Ordering::Equal)
344 .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
345 });
346 sorted.dedup_by(|a, b| (a.x - b.x).abs() < f32::EPSILON && (a.y - b.y).abs() < f32::EPSILON);
347
348 if sorted.len() <= 2 {
349 return sorted;
350 }
351
352 let mut lower = Vec::new();
353 for point in &sorted {
354 while lower.len() >= 2
355 && cross(lower[lower.len() - 2], lower[lower.len() - 1], *point) <= 0.0
356 {
357 lower.pop();
358 }
359 lower.push(*point);
360 }
361
362 let mut upper = Vec::new();
363 for point in sorted.iter().rev() {
364 while upper.len() >= 2
365 && cross(upper[upper.len() - 2], upper[upper.len() - 1], *point) <= 0.0
366 {
367 upper.pop();
368 }
369 upper.push(*point);
370 }
371
372 lower.pop();
373 upper.pop();
374 lower.extend(upper);
375 lower
376}
377
378fn cross(origin: Point<f32>, a: Point<f32>, b: Point<f32>) -> f32 {
379 (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x)
380}
381
382fn scale_and_order_points(
383 points: [Point<f32>; 4],
384 scale_x: f32,
385 scale_y: f32,
386 original_width: u32,
387 original_height: u32,
388) -> [Point<f32>; 4] {
389 let max_x = original_width.saturating_sub(1) as f32;
390 let max_y = original_height.saturating_sub(1) as f32;
391 order_points(points.map(|point| {
392 Point::new(
393 (point.x * scale_x).clamp(0.0, max_x),
394 (point.y * scale_y).clamp(0.0, max_y),
395 )
396 }))
397}
398
399fn order_points(points: [Point<f32>; 4]) -> [Point<f32>; 4] {
400 let mut top_left = points[0];
401 let mut top_right = points[0];
402 let mut bottom_right = points[0];
403 let mut bottom_left = points[0];
404
405 for point in points {
406 let sum = point.x + point.y;
407 let diff = point.x - point.y;
408
409 if sum < top_left.x + top_left.y {
410 top_left = point;
411 }
412 if sum > bottom_right.x + bottom_right.y {
413 bottom_right = point;
414 }
415 if diff > top_right.x - top_right.y {
416 top_right = point;
417 }
418 if diff < bottom_left.x - bottom_left.y {
419 bottom_left = point;
420 }
421 }
422
423 [top_left, top_right, bottom_right, bottom_left]
424}
425
426fn rect_from_ordered_points(
427 points: &[Point<f32>; 4],
428 original_width: u32,
429 original_height: u32,
430) -> Option<Rect> {
431 let min_x = points
432 .iter()
433 .map(|point| point.x)
434 .fold(f32::INFINITY, f32::min)
435 .floor()
436 .max(0.0) as u32;
437 let min_y = points
438 .iter()
439 .map(|point| point.y)
440 .fold(f32::INFINITY, f32::min)
441 .floor()
442 .max(0.0) as u32;
443 let max_x = points
444 .iter()
445 .map(|point| point.x)
446 .fold(f32::NEG_INFINITY, f32::max)
447 .ceil()
448 .min(original_width as f32) as u32;
449 let max_y = points
450 .iter()
451 .map(|point| point.y)
452 .fold(f32::NEG_INFINITY, f32::max)
453 .ceil()
454 .min(original_height as f32) as u32;
455
456 if max_x <= min_x || max_y <= min_y {
457 return None;
458 }
459
460 Some(Rect::at(min_x as i32, min_y as i32).of_size(max_x - min_x, max_y - min_y))
461}
462
463fn expand_ordered_points(
464 points: [Point<f32>; 4],
465 border: f32,
466 max_width: u32,
467 max_height: u32,
468) -> [Point<f32>; 4] {
469 if border <= 0.0 {
470 return points;
471 }
472
473 let center = Point::new(
474 points.iter().map(|p| p.x).sum::<f32>() / 4.0,
475 points.iter().map(|p| p.y).sum::<f32>() / 4.0,
476 );
477 let max_x = max_width.saturating_sub(1) as f32;
478 let max_y = max_height.saturating_sub(1) as f32;
479
480 order_points(points.map(|point| {
481 let dx = point.x - center.x;
482 let dy = point.y - center.y;
483 let len = (dx * dx + dy * dy).sqrt();
484 if len <= f32::EPSILON {
485 return point;
486 }
487
488 Point::new(
489 (point.x + dx / len * border).clamp(0.0, max_x),
490 (point.y + dy / len * border).clamp(0.0, max_y),
491 )
492 }))
493}
494
495fn get_contour_bounds(contour: &Contour<i32>) -> (i32, i32, i32, i32) {
497 let mut min_x = i32::MAX;
498 let mut min_y = i32::MAX;
499 let mut max_x = i32::MIN;
500 let mut max_y = i32::MIN;
501
502 for point in &contour.points {
503 min_x = min_x.min(point.x);
504 min_y = min_y.min(point.y);
505 max_x = max_x.max(point.x);
506 max_y = max_y.max(point.y);
507 }
508
509 (min_x, min_y, max_x, max_y)
510}
511
512fn compute_containment_ratio(inner: &Rect, outer: &Rect) -> f32 {
514 let x1 = inner.left().max(outer.left());
515 let y1 = inner.top().max(outer.top());
516 let x2 = (inner.left() + inner.width() as i32).min(outer.left() + outer.width() as i32);
517 let y2 = (inner.top() + inner.height() as i32).min(outer.top() + outer.height() as i32);
518
519 if x2 <= x1 || y2 <= y1 {
520 return 0.0;
521 }
522
523 let intersection = (x2 - x1) as f32 * (y2 - y1) as f32;
524 let inner_area = inner.width() as f32 * inner.height() as f32;
525
526 if inner_area <= 0.0 {
527 0.0
528 } else {
529 intersection / inner_area
530 }
531}
532
533pub fn nms(boxes: &[TextBox], iou_threshold: f32) -> Vec<TextBox> {
542 if boxes.is_empty() {
543 return Vec::new();
544 }
545
546 let mut indices: Vec<usize> = (0..boxes.len()).collect();
548 indices.sort_by(|&a, &b| {
549 let score_cmp = boxes[b]
551 .score
552 .partial_cmp(&boxes[a].score)
553 .unwrap_or(std::cmp::Ordering::Equal);
554 if score_cmp != std::cmp::Ordering::Equal {
555 return score_cmp;
556 }
557 boxes[b].area().cmp(&boxes[a].area())
559 });
560
561 let mut keep = Vec::new();
562 let mut suppressed = vec![false; boxes.len()];
563
564 for (pos, &i) in indices.iter().enumerate() {
565 if suppressed[i] {
566 continue;
567 }
568
569 keep.push(boxes[i].clone());
570
571 for &j in indices.iter().skip(pos + 1) {
573 if suppressed[j] {
574 continue;
575 }
576
577 let iou = compute_iou(&boxes[i].rect, &boxes[j].rect);
579 if iou > iou_threshold {
580 suppressed[j] = true;
581 continue;
582 }
583
584 let containment_j_in_i = compute_containment_ratio(&boxes[j].rect, &boxes[i].rect);
586 if containment_j_in_i > 0.5 {
587 suppressed[j] = true;
588 continue;
589 }
590
591 let containment_i_in_j = compute_containment_ratio(&boxes[i].rect, &boxes[j].rect);
594 if containment_i_in_j > 0.7 {
595 suppressed[j] = true;
596 continue;
597 }
598 }
599 }
600
601 keep
602}
603
604pub fn compute_iou(a: &Rect, b: &Rect) -> f32 {
606 let x1 = a.left().max(b.left());
607 let y1 = a.top().max(b.top());
608 let x2 = (a.left() + a.width() as i32).min(b.left() + b.width() as i32);
609 let y2 = (a.top() + a.height() as i32).min(b.top() + b.height() as i32);
610
611 if x2 <= x1 || y2 <= y1 {
612 return 0.0;
613 }
614
615 let intersection = (x2 - x1) as f32 * (y2 - y1) as f32;
616 let area_a = a.width() as f32 * a.height() as f32;
617 let area_b = b.width() as f32 * b.height() as f32;
618 let union = area_a + area_b - intersection;
619
620 if union <= 0.0 {
621 0.0
622 } else {
623 intersection / union
624 }
625}
626
627pub fn merge_adjacent_boxes(boxes: &[TextBox], distance_threshold: i32) -> Vec<TextBox> {
635 if boxes.is_empty() {
636 return Vec::new();
637 }
638
639 let mut merged = Vec::new();
640 let mut used = vec![false; boxes.len()];
641
642 for i in 0..boxes.len() {
643 if used[i] {
644 continue;
645 }
646
647 let mut current = boxes[i].rect;
648 let mut group_score = boxes[i].score;
649 let mut count = 1;
650 used[i] = true;
651
652 loop {
654 let mut found = false;
655
656 for j in 0..boxes.len() {
657 if used[j] {
658 continue;
659 }
660
661 if can_merge(¤t, &boxes[j].rect, distance_threshold) {
662 current = merge_rects(¤t, &boxes[j].rect);
663 group_score += boxes[j].score;
664 count += 1;
665 used[j] = true;
666 found = true;
667 }
668 }
669
670 if !found {
671 break;
672 }
673 }
674
675 merged.push(TextBox::new(current, group_score / count as f32));
676 }
677
678 merged
679}
680
681fn can_merge(a: &Rect, b: &Rect, threshold: i32) -> bool {
683 let a_bottom = a.top() + a.height() as i32;
685 let b_bottom = b.top() + b.height() as i32;
686
687 let _vertical_dist = if a.top() > b_bottom {
688 a.top() - b_bottom
689 } else if b.top() > a_bottom {
690 b.top() - a_bottom
691 } else {
692 0 };
694
695 let a_right = a.left() + a.width() as i32;
697 let b_right = b.left() + b.width() as i32;
698
699 let horizontal_dist = if a.left() > b_right {
700 a.left() - b_right
701 } else if b.left() > a_right {
702 b.left() - a_right
703 } else {
704 0 };
706
707 let vertical_overlap = !(a.top() > b_bottom || b.top() > a_bottom);
709
710 vertical_overlap && horizontal_dist <= threshold
711}
712
713fn merge_rects(a: &Rect, b: &Rect) -> Rect {
715 let x1 = a.left().min(b.left());
716 let y1 = a.top().min(b.top());
717 let x2 = (a.left() + a.width() as i32).max(b.left() + b.width() as i32);
718 let y2 = (a.top() + a.height() as i32).max(b.top() + b.height() as i32);
719
720 Rect::at(x1, y1).of_size((x2 - x1) as u32, (y2 - y1) as u32)
721}
722
723pub fn sort_boxes_by_reading_order(boxes: &mut [TextBox]) {
725 boxes.sort_by(|a, b| {
726 let y_cmp = a.rect.top().cmp(&b.rect.top());
728 if y_cmp != std::cmp::Ordering::Equal {
729 return y_cmp;
730 }
731 a.rect.left().cmp(&b.rect.left())
733 });
734}
735
736pub fn group_boxes_by_line(boxes: &[TextBox], line_threshold: i32) -> Vec<Vec<TextBox>> {
740 if boxes.is_empty() {
741 return Vec::new();
742 }
743
744 let mut sorted_boxes = boxes.to_vec();
745 sorted_boxes.sort_by_key(|b| b.rect.top());
746
747 let mut lines: Vec<Vec<TextBox>> = Vec::new();
748 let mut current_line: Vec<TextBox> = vec![sorted_boxes[0].clone()];
749 let mut current_y = sorted_boxes[0].rect.top();
750
751 for box_item in sorted_boxes.iter().skip(1) {
752 if (box_item.rect.top() - current_y).abs() <= line_threshold {
753 current_line.push(box_item.clone());
754 } else {
755 current_line.sort_by_key(|b| b.rect.left());
757 lines.push(current_line);
758 current_line = vec![box_item.clone()];
759 current_y = box_item.rect.top();
760 }
761 }
762
763 if !current_line.is_empty() {
765 current_line.sort_by_key(|b| b.rect.left());
766 lines.push(current_line);
767 }
768
769 lines
770}
771
772pub fn merge_multi_scale_results(
778 results: &[(Vec<TextBox>, u32, u32, f32)],
779 iou_threshold: f32,
780) -> Vec<TextBox> {
781 let mut all_boxes = Vec::new();
782
783 for (boxes, offset_x, offset_y, scale) in results {
784 for box_item in boxes {
785 let scaled_x = (box_item.rect.left() as f32 / scale) as i32 + *offset_x as i32;
787 let scaled_y = (box_item.rect.top() as f32 / scale) as i32 + *offset_y as i32;
788 let scaled_w = (box_item.rect.width() as f32 / scale) as u32;
789 let scaled_h = (box_item.rect.height() as f32 / scale) as u32;
790
791 let rect = Rect::at(scaled_x, scaled_y).of_size(scaled_w, scaled_h);
792 all_boxes.push(TextBox::new(rect, box_item.score));
793 }
794 }
795
796 nms(&all_boxes, iou_threshold)
798}
799
800pub fn detect_text_traditional(
814 gray_image: &GrayImage,
815 min_area: u32,
816 expand_ratio: f32,
817) -> Vec<TextBox> {
818 let (width, height) = gray_image.dimensions();
819
820 let threshold = otsu_threshold(gray_image);
822
823 let binary: Vec<u8> = gray_image
825 .pixels()
826 .map(|p| if p.0[0] < threshold { 255 } else { 0 })
827 .collect();
828
829 let binary_image =
831 GrayImage::from_raw(width, height, binary).unwrap_or_else(|| GrayImage::new(width, height));
832 let contours = find_contours::<i32>(&binary_image);
833
834 let mut boxes = Vec::new();
836 for contour in contours {
837 if contour.points.len() < 4 {
838 continue;
839 }
840
841 let (min_x, min_y, max_x, max_y) = get_contour_bounds(&contour);
842 let box_width = (max_x - min_x) as u32;
843 let box_height = (max_y - min_y) as u32;
844
845 if box_width * box_height < min_area {
846 continue;
847 }
848
849 let expand_w = (box_width as f32 * expand_ratio * 0.5) as i32;
851 let expand_h = (box_height as f32 * expand_ratio * 0.5) as i32;
852
853 let final_x = (min_x - expand_w).max(0) as u32;
854 let final_y = (min_y - expand_h).max(0) as u32;
855 let final_w = ((max_x + expand_w) as u32)
856 .min(width)
857 .saturating_sub(final_x);
858 let final_h = ((max_y + expand_h) as u32)
859 .min(height)
860 .saturating_sub(final_y);
861
862 if final_w > 0 && final_h > 0 {
863 let rect = Rect::at(final_x as i32, final_y as i32).of_size(final_w, final_h);
864 boxes.push(TextBox::new(rect, 1.0));
865 }
866 }
867
868 merge_into_text_lines(&boxes, 10)
870}
871
872fn otsu_threshold(image: &GrayImage) -> u8 {
874 let mut histogram = [0u32; 256];
876 for pixel in image.pixels() {
877 histogram[pixel.0[0] as usize] += 1;
878 }
879
880 let total = image.pixels().count() as f64;
881 let mut sum = 0.0;
882 for (i, &count) in histogram.iter().enumerate() {
883 sum += i as f64 * count as f64;
884 }
885
886 let mut sum_b = 0.0;
887 let mut w_b = 0.0;
888 let mut max_variance = 0.0;
889 let mut threshold = 0u8;
890
891 for (t, &count) in histogram.iter().enumerate() {
892 w_b += count as f64;
893 if w_b == 0.0 {
894 continue;
895 }
896
897 let w_f = total - w_b;
898 if w_f == 0.0 {
899 break;
900 }
901
902 sum_b += t as f64 * count as f64;
903 let m_b = sum_b / w_b;
904 let m_f = (sum - sum_b) / w_f;
905
906 let variance = w_b * w_f * (m_b - m_f).powi(2);
907 if variance > max_variance {
908 max_variance = variance;
909 threshold = t as u8;
910 }
911 }
912
913 threshold
914}
915
916fn merge_into_text_lines(boxes: &[TextBox], gap_threshold: i32) -> Vec<TextBox> {
918 if boxes.is_empty() {
919 return Vec::new();
920 }
921
922 let mut sorted_boxes: Vec<_> = boxes.iter().collect();
924 sorted_boxes.sort_by_key(|b| b.rect.top());
925
926 let mut lines: Vec<TextBox> = Vec::new();
927
928 for bbox in sorted_boxes {
929 let mut merged = false;
930
931 for line in &mut lines {
933 let line_center_y = line.rect.top() + line.rect.height() as i32 / 2;
934 let box_center_y = bbox.rect.top() + bbox.rect.height() as i32 / 2;
935
936 if (line_center_y - box_center_y).abs() < line.rect.height() as i32 / 2 {
938 let line_right = line.rect.left() + line.rect.width() as i32;
939 let box_left = bbox.rect.left();
940
941 if (box_left - line_right).abs() < gap_threshold * 3 {
942 let new_left = line.rect.left().min(bbox.rect.left());
944 let new_top = line.rect.top().min(bbox.rect.top());
945 let new_right = (line.rect.left() + line.rect.width() as i32)
946 .max(bbox.rect.left() + bbox.rect.width() as i32);
947 let new_bottom = (line.rect.top() + line.rect.height() as i32)
948 .max(bbox.rect.top() + bbox.rect.height() as i32);
949
950 line.rect = Rect::at(new_left, new_top)
951 .of_size((new_right - new_left) as u32, (new_bottom - new_top) as u32);
952 merged = true;
953 break;
954 }
955 }
956 }
957
958 if !merged {
959 lines.push(bbox.clone());
960 }
961 }
962
963 lines
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn test_textbox_new() {
972 let rect = Rect::at(10, 20).of_size(100, 50);
973 let tb = TextBox::new(rect, 0.95);
974
975 assert_eq!(tb.rect.left(), 10);
976 assert_eq!(tb.rect.top(), 20);
977 assert_eq!(tb.rect.width(), 100);
978 assert_eq!(tb.rect.height(), 50);
979 assert_eq!(tb.score, 0.95);
980 assert!(tb.points.is_none());
981 }
982
983 #[test]
984 fn test_textbox_with_points() {
985 let rect = Rect::at(0, 0).of_size(100, 50);
986 let points = [
987 Point::new(0.0, 0.0),
988 Point::new(100.0, 0.0),
989 Point::new(100.0, 50.0),
990 Point::new(0.0, 50.0),
991 ];
992 let tb = TextBox::with_points(rect, 0.9, points);
993
994 assert!(tb.points.is_some());
995 let pts = tb.points.unwrap();
996 assert_eq!(pts[0].x, 0.0);
997 assert_eq!(pts[1].x, 100.0);
998 }
999
1000 #[test]
1001 fn test_textbox_area() {
1002 let tb = TextBox::new(Rect::at(0, 0).of_size(100, 50), 0.9);
1003 assert_eq!(tb.area(), 5000);
1004 }
1005
1006 #[test]
1007 fn test_textbox_expand() {
1008 let tb = TextBox::new(Rect::at(50, 50).of_size(100, 100), 0.9);
1009 let expanded = tb.expand(10, 500, 500);
1010
1011 assert_eq!(expanded.rect.left(), 40);
1012 assert_eq!(expanded.rect.top(), 40);
1013 assert_eq!(expanded.rect.width(), 120);
1014 assert_eq!(expanded.rect.height(), 120);
1015 }
1016
1017 #[test]
1018 fn test_textbox_expand_clamp() {
1019 let tb = TextBox::new(Rect::at(5, 5).of_size(100, 100), 0.9);
1021 let expanded = tb.expand(10, 200, 200);
1022
1023 assert_eq!(expanded.rect.left(), 0);
1025 assert_eq!(expanded.rect.top(), 0);
1026 }
1027
1028 #[test]
1029 fn test_textbox_expand_keeps_rotated_points() {
1030 let rect = Rect::at(10, 10).of_size(100, 40);
1031 let points = [
1032 Point::new(12.0, 20.0),
1033 Point::new(105.0, 12.0),
1034 Point::new(108.0, 48.0),
1035 Point::new(15.0, 56.0),
1036 ];
1037 let expanded = TextBox::with_points(rect, 0.9, points).expand(5, 200, 200);
1038
1039 assert!(expanded.points.is_some());
1040 let expanded_points = expanded.points.unwrap();
1041 assert_ne!(expanded_points[0], points[0]);
1042 assert_ne!(expanded_points[1], points[1]);
1043 }
1044
1045 #[test]
1046 fn test_extract_boxes_returns_rotated_points() {
1047 let width = 100;
1048 let height = 80;
1049 let quad = [
1050 Point::new(20.0, 30.0),
1051 Point::new(80.0, 20.0),
1052 Point::new(85.0, 40.0),
1053 Point::new(25.0, 50.0),
1054 ];
1055 let mut mask = vec![0u8; width * height];
1056 for y in 0..height {
1057 for x in 0..width {
1058 if point_in_quad(x as f32 + 0.5, y as f32 + 0.5, &quad) {
1059 mask[y * width + x] = 255;
1060 }
1061 }
1062 }
1063
1064 let boxes = extract_boxes_with_unclip(
1065 &mask,
1066 width as u32,
1067 height as u32,
1068 width as u32,
1069 height as u32,
1070 width as u32,
1071 height as u32,
1072 16,
1073 1.0,
1074 );
1075
1076 let text_box = boxes
1077 .iter()
1078 .find(|text_box| text_box.points.is_some())
1079 .expect("expected a rotated text box");
1080 let points = text_box.points.unwrap();
1081 assert!(
1082 (points[0].y - points[1].y).abs() > 2.0,
1083 "top edge should preserve rotation: {points:?}"
1084 );
1085 }
1086
1087 fn point_in_quad(x: f32, y: f32, points: &[Point<f32>; 4]) -> bool {
1088 let mut inside = false;
1089 let mut prev = points.len() - 1;
1090 for current in 0..points.len() {
1091 let pi = points[current];
1092 let pj = points[prev];
1093 if (pi.y > y) != (pj.y > y) && x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x {
1094 inside = !inside;
1095 }
1096 prev = current;
1097 }
1098 inside
1099 }
1100
1101 #[test]
1102 fn test_compute_iou() {
1103 let a = Rect::at(0, 0).of_size(10, 10);
1104 let b = Rect::at(5, 5).of_size(10, 10);
1105
1106 let iou = compute_iou(&a, &b);
1107 assert!(iou > 0.0 && iou < 1.0);
1108
1109 let c = Rect::at(100, 100).of_size(10, 10);
1111 assert_eq!(compute_iou(&a, &c), 0.0);
1112
1113 assert_eq!(compute_iou(&a, &a), 1.0);
1115 }
1116
1117 #[test]
1118 fn test_compute_iou_partial_overlap() {
1119 let a = Rect::at(0, 0).of_size(10, 10);
1121 let b = Rect::at(5, 0).of_size(10, 10);
1122
1123 let iou = compute_iou(&a, &b);
1124 assert!((iou - 0.333).abs() < 0.01);
1128 }
1129
1130 #[test]
1131 fn test_nms() {
1132 let boxes = vec![
1134 TextBox::new(Rect::at(0, 0).of_size(10, 10), 0.9),
1135 TextBox::new(Rect::at(1, 1).of_size(10, 10), 0.8), TextBox::new(Rect::at(100, 100).of_size(10, 10), 0.7),
1137 ];
1138
1139 let result = nms(&boxes, 0.3); assert!(
1142 result.len() >= 2,
1143 "至少应该保留2个框,实际: {}",
1144 result.len()
1145 );
1146 }
1147
1148 #[test]
1149 fn test_nms_empty() {
1150 let boxes: Vec<TextBox> = vec![];
1151 let result = nms(&boxes, 0.5);
1152 assert!(result.is_empty());
1153 }
1154
1155 #[test]
1156 fn test_nms_single() {
1157 let boxes = vec![TextBox::new(Rect::at(0, 0).of_size(10, 10), 0.9)];
1158 let result = nms(&boxes, 0.5);
1159 assert_eq!(result.len(), 1);
1160 }
1161
1162 #[test]
1163 fn test_nms_no_overlap() {
1164 let boxes = vec![
1165 TextBox::new(Rect::at(0, 0).of_size(10, 10), 0.9),
1166 TextBox::new(Rect::at(50, 50).of_size(10, 10), 0.8),
1167 TextBox::new(Rect::at(100, 100).of_size(10, 10), 0.7),
1168 ];
1169
1170 let result = nms(&boxes, 0.5);
1171 assert_eq!(result.len(), 3); }
1173
1174 #[test]
1175 fn test_merge_adjacent() {
1176 let boxes = vec![
1177 TextBox::new(Rect::at(0, 0).of_size(10, 10), 1.0),
1178 TextBox::new(Rect::at(12, 0).of_size(10, 10), 1.0), TextBox::new(Rect::at(100, 100).of_size(10, 10), 1.0),
1180 ];
1181
1182 let result = merge_adjacent_boxes(&boxes, 5);
1183 assert_eq!(result.len(), 2); }
1185
1186 #[test]
1187 fn test_merge_adjacent_empty() {
1188 let boxes: Vec<TextBox> = vec![];
1189 let result = merge_adjacent_boxes(&boxes, 5);
1190 assert!(result.is_empty());
1191 }
1192
1193 #[test]
1194 fn test_sort_boxes_by_reading_order() {
1195 let mut boxes = vec![
1196 TextBox::new(Rect::at(100, 0).of_size(10, 10), 0.9), TextBox::new(Rect::at(0, 0).of_size(10, 10), 0.9), TextBox::new(Rect::at(0, 50).of_size(10, 10), 0.9), ];
1200
1201 sort_boxes_by_reading_order(&mut boxes);
1202
1203 assert_eq!(boxes[0].rect.left(), 0);
1205 assert_eq!(boxes[0].rect.top(), 0);
1206 }
1207
1208 #[test]
1209 fn test_group_boxes_by_line() {
1210 let boxes = vec![
1211 TextBox::new(Rect::at(0, 0).of_size(50, 20), 0.9),
1212 TextBox::new(Rect::at(60, 0).of_size(50, 20), 0.9),
1213 TextBox::new(Rect::at(0, 50).of_size(50, 20), 0.9),
1214 ];
1215
1216 let lines = group_boxes_by_line(&boxes, 10);
1217
1218 assert_eq!(lines.len(), 2);
1220 }
1221}