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