1use crate::domain::tasks::MergeBboxMode;
7use crate::processors::{BoundingBox, ImageScaleInfo, Point};
8use ndarray::{ArrayView3, Axis};
9use rayon::prelude::*;
10use std::borrow::Cow;
11use std::collections::HashMap;
12
13type LayoutPostprocessOutput = (Vec<Vec<BoundingBox>>, Vec<Vec<usize>>, Vec<Vec<f32>>);
14type NmsResult = (Vec<BoundingBox>, Vec<usize>, Vec<f32>, Vec<(f32, f32)>);
15
16#[derive(Debug, Clone)]
21pub struct LayoutPostProcess {
22 num_classes: usize,
24 score_threshold: f32,
26 nms_threshold: f32,
28 max_detections: usize,
30 model_type: String,
32}
33
34impl LayoutPostProcess {
35 pub fn new(
37 num_classes: usize,
38 score_threshold: f32,
39 nms_threshold: f32,
40 max_detections: usize,
41 model_type: String,
42 ) -> Self {
43 Self {
44 num_classes,
45 score_threshold,
46 nms_threshold,
47 max_detections,
48 model_type,
49 }
50 }
51
52 pub fn apply(
61 &self,
62 predictions: &ndarray::Array4<f32>,
63 img_shapes: Vec<ImageScaleInfo>,
64 ) -> LayoutPostprocessOutput {
65 let batch_size = predictions.shape()[0];
66 let n = batch_size.min(img_shapes.len());
67
68 let per_image: Vec<(Vec<BoundingBox>, Vec<usize>, Vec<f32>)> = (0..n)
73 .into_par_iter()
74 .map(|batch_idx| {
75 let img_shape = &img_shapes[batch_idx];
76 let pred = predictions.index_axis(Axis(0), batch_idx);
77 match self.model_type.as_str() {
78 "picodet" => self.process_picodet(pred, img_shape),
79 "rtdetr" => self.process_rtdetr(pred, img_shape),
80 "pp-doclayout" => self.process_pp_doclayout(pred, img_shape),
81 _ => self.process_standard(pred, img_shape),
82 }
83 })
84 .collect();
85
86 let mut all_boxes = Vec::with_capacity(n);
87 let mut all_classes = Vec::with_capacity(n);
88 let mut all_scores = Vec::with_capacity(n);
89 for (b, c, s) in per_image {
90 all_boxes.push(b);
91 all_classes.push(c);
92 all_scores.push(s);
93 }
94
95 (all_boxes, all_classes, all_scores)
96 }
97
98 fn process_picodet(
100 &self,
101 predictions: ArrayView3<f32>,
102 img_shape: &ImageScaleInfo,
103 ) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
104 let mut boxes = Vec::new();
105 let mut classes = Vec::new();
106 let mut scores = Vec::new();
107
108 let orig_width = img_shape.src_w;
109 let orig_height = img_shape.src_h;
110 let shape = predictions.shape();
111 if shape.len() != 3 || shape[2] == 0 {
112 return (boxes, classes, scores);
113 }
114
115 let total_boxes = shape[0] * shape[1];
116 if total_boxes == 0 {
117 return (boxes, classes, scores);
118 }
119
120 let feature_dim = shape[2];
121 let data: Cow<'_, [f32]> = match predictions.as_slice() {
122 Some(slice) => Cow::Borrowed(slice),
123 None => {
124 let (mut vec, offset) = predictions.to_owned().into_raw_vec_and_offset();
125 if let Some(offset) = offset
126 && offset != 0
127 {
128 vec.drain(0..offset);
129 }
130 Cow::Owned(vec)
131 }
132 };
133
134 for box_idx in 0..total_boxes {
135 let start = box_idx * feature_dim;
136 let end = start + feature_dim;
137
138 if end > data.len() {
139 break;
140 }
141
142 let row = &data[start..end];
143 if feature_dim == 4 + self.num_classes {
144 let (max_class, max_score) = row[4..].iter().enumerate().fold(
146 (0usize, f32::NEG_INFINITY),
147 |(best_cls, best_score), (cls_idx, &score)| {
148 if score > best_score {
149 (cls_idx, score)
150 } else {
151 (best_cls, best_score)
152 }
153 },
154 );
155
156 if max_score < self.score_threshold {
157 continue;
158 }
159
160 let (sx1, sy1, sx2, sy2) = self.convert_bbox_coords(
161 row[0],
162 row[1],
163 row[2],
164 row[3],
165 orig_width,
166 orig_height,
167 );
168
169 if !Self::is_valid_box(sx1, sy1, sx2, sy2) {
170 continue;
171 }
172
173 let bbox = BoundingBox::new(vec![
174 Point::new(sx1, sy1),
175 Point::new(sx2, sy1),
176 Point::new(sx2, sy2),
177 Point::new(sx1, sy2),
178 ]);
179
180 boxes.push(bbox);
181 classes.push(max_class);
182 scores.push(max_score);
183 } else if feature_dim >= 6
184 && let Some((class_id, score, x1, y1, x2, y2)) = self.parse_compact_prediction(row)
185 {
186 if score < self.score_threshold || class_id >= self.num_classes {
187 continue;
188 }
189
190 let (sx1, sy1, sx2, sy2) =
191 self.convert_bbox_coords(x1, y1, x2, y2, orig_width, orig_height);
192
193 if !Self::is_valid_box(sx1, sy1, sx2, sy2) {
194 continue;
195 }
196
197 let bbox = BoundingBox::new(vec![
198 Point::new(sx1, sy1),
199 Point::new(sx2, sy1),
200 Point::new(sx2, sy2),
201 Point::new(sx1, sy2),
202 ]);
203
204 boxes.push(bbox);
205 classes.push(class_id);
206 scores.push(score);
207 }
208 }
209
210 self.apply_nms(boxes, classes, scores)
211 }
212
213 fn process_rtdetr(
215 &self,
216 predictions: ArrayView3<f32>,
217 img_shape: &ImageScaleInfo,
218 ) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
219 self.process_picodet(predictions, img_shape)
221 }
222
223 fn process_pp_doclayout(
233 &self,
234 predictions: ArrayView3<f32>,
235 img_shape: &ImageScaleInfo,
236 ) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
237 let shape = predictions.shape();
240
241 let mut boxes = Vec::new();
242 let mut classes = Vec::new();
243 let mut scores = Vec::new();
244 let mut reading_orders: Vec<(f32, f32)> = Vec::new();
245
246 if shape.len() != 3 || shape[1] == 0 || shape[2] < 6 {
250 return (boxes, classes, scores);
251 }
252
253 let num_boxes = shape[0];
254 let feature_dim = shape[2];
255
256 let orig_width = img_shape.src_w;
257 let orig_height = img_shape.src_h;
258
259 let has_reading_order = feature_dim == 8;
260
261 for box_idx in 0..num_boxes {
263 let class_id = predictions[[box_idx, 0, 0]] as i32;
265 let score = predictions[[box_idx, 0, 1]];
266 let x1 = predictions[[box_idx, 0, 2]];
267 let y1 = predictions[[box_idx, 0, 3]];
268 let x2 = predictions[[box_idx, 0, 4]];
269 let y2 = predictions[[box_idx, 0, 5]];
270
271 let reading_order = if has_reading_order {
274 (predictions[[box_idx, 0, 6]], predictions[[box_idx, 0, 7]])
275 } else {
276 (0.0, box_idx as f32)
277 };
278
279 if score < self.score_threshold
281 || class_id < 0
282 || (class_id as usize) >= self.num_classes
283 {
284 continue;
285 }
286
287 let (sx1, sy1, sx2, sy2) =
290 self.convert_bbox_coords(x1, y1, x2, y2, orig_width, orig_height);
291 if !Self::is_valid_box(sx1, sy1, sx2, sy2) {
292 continue;
293 }
294
295 let bbox = BoundingBox::new(vec![
296 Point::new(sx1, sy1),
297 Point::new(sx2, sy1),
298 Point::new(sx2, sy2),
299 Point::new(sx1, sy2),
300 ]);
301
302 boxes.push(bbox);
303 classes.push(class_id as usize);
304 scores.push(score);
305 reading_orders.push(reading_order);
306 }
307
308 let (filtered_boxes, filtered_classes, filtered_scores, filtered_reading_orders) =
310 self.apply_nms_with_reading_order(boxes, classes, scores, reading_orders);
311
312 if has_reading_order && !filtered_boxes.is_empty() {
314 let mut indices: Vec<usize> = (0..filtered_boxes.len()).collect();
315 indices.sort_by(|&i, &j| {
316 let (col_i, row_i) = filtered_reading_orders[i];
317 let (col_j, row_j) = filtered_reading_orders[j];
318 col_i
321 .total_cmp(&col_j)
322 .then_with(|| row_i.total_cmp(&row_j))
323 });
324
325 let sorted_boxes = indices.iter().map(|&i| filtered_boxes[i].clone()).collect();
326 let sorted_classes = indices.iter().map(|&i| filtered_classes[i]).collect();
327 let sorted_scores = indices.iter().map(|&i| filtered_scores[i]).collect();
328
329 (sorted_boxes, sorted_classes, sorted_scores)
330 } else {
331 (filtered_boxes, filtered_classes, filtered_scores)
332 }
333 }
334
335 fn apply_nms_with_reading_order(
337 &self,
338 boxes: Vec<BoundingBox>,
339 classes: Vec<usize>,
340 scores: Vec<f32>,
341 reading_orders: Vec<(f32, f32)>,
342 ) -> NmsResult {
343 if boxes.is_empty() {
344 return (boxes, classes, scores, reading_orders);
345 }
346
347 let keep = self.compute_nms_keep_indices(&boxes, &classes, &scores);
348
349 let filtered_boxes: Vec<BoundingBox> = keep.iter().map(|&i| boxes[i].clone()).collect();
350 let filtered_classes: Vec<usize> = keep.iter().map(|&i| classes[i]).collect();
351 let filtered_scores: Vec<f32> = keep.iter().map(|&i| scores[i]).collect();
352 let filtered_reading_orders: Vec<(f32, f32)> =
353 keep.iter().map(|&i| reading_orders[i]).collect();
354
355 (
356 filtered_boxes,
357 filtered_classes,
358 filtered_scores,
359 filtered_reading_orders,
360 )
361 }
362
363 fn process_standard(
365 &self,
366 predictions: ArrayView3<f32>,
367 img_shape: &ImageScaleInfo,
368 ) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
369 self.process_picodet(predictions, img_shape)
370 }
371
372 fn parse_compact_prediction(&self, row: &[f32]) -> Option<(usize, f32, f32, f32, f32, f32)> {
373 if row.len() < 6 {
374 return None;
375 }
376
377 let score_is_valid = if self.model_type == "rtdetr" {
379 row[1].is_finite()
380 } else {
381 Self::is_valid_score(row[1])
382 };
383
384 if score_is_valid && Self::is_valid_class(row[0], self.num_classes) {
385 let class_id = row[0].round() as i32;
386 if class_id >= 0 {
387 let score = self.adjust_score(row[1]);
388 return Some((class_id as usize, score, row[2], row[3], row[4], row[5]));
389 }
390 }
391
392 let score_is_valid = if self.model_type == "rtdetr" {
394 row[4].is_finite()
395 } else {
396 Self::is_valid_score(row[4])
397 };
398 if score_is_valid && Self::is_valid_class(row[5], self.num_classes) {
399 let class_id = row[5].round() as i32;
400 if class_id >= 0 {
401 let score = self.adjust_score(row[4]);
402 return Some((class_id as usize, score, row[0], row[1], row[2], row[3]));
403 }
404 }
405
406 let score_is_valid = if self.model_type == "rtdetr" {
408 row[0].is_finite()
409 } else {
410 Self::is_valid_score(row[0])
411 };
412 if score_is_valid && Self::is_valid_class(row[1], self.num_classes) {
413 let class_id = row[1].round() as i32;
414 if class_id >= 0 {
415 let score = self.adjust_score(row[0]);
416 return Some((class_id as usize, score, row[2], row[3], row[4], row[5]));
417 }
418 }
419
420 None
421 }
422
423 fn convert_bbox_coords(
424 &self,
425 x1: f32,
426 y1: f32,
427 x2: f32,
428 y2: f32,
429 orig_width: f32,
430 orig_height: f32,
431 ) -> (f32, f32, f32, f32) {
432 let normalized = x2 <= 1.05
433 && y2 <= 1.05
434 && x1 >= -0.05
435 && y1 >= -0.05
436 && orig_width > 0.0
437 && orig_height > 0.0;
438
439 if normalized {
440 (
441 x1.clamp(0.0, 1.0) * orig_width,
442 y1.clamp(0.0, 1.0) * orig_height,
443 x2.clamp(0.0, 1.0) * orig_width,
444 y2.clamp(0.0, 1.0) * orig_height,
445 )
446 } else {
447 (
448 x1.clamp(0.0, orig_width),
449 y1.clamp(0.0, orig_height),
450 x2.clamp(0.0, orig_width),
451 y2.clamp(0.0, orig_height),
452 )
453 }
454 }
455
456 fn is_valid_box(x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
457 x2 > x1 && y2 > y1 && x1.is_finite() && y1.is_finite() && x2.is_finite() && y2.is_finite()
458 }
459
460 fn is_valid_score(score: f32) -> bool {
461 score.is_finite() && (0.0..=1.0 + f32::EPSILON).contains(&score)
462 }
463
464 fn is_valid_class(raw: f32, num_classes: usize) -> bool {
465 if !raw.is_finite() {
466 return false;
467 }
468 let class_id = raw.round() as i32;
469 class_id >= 0 && (class_id as usize) < num_classes + 5
470 }
471
472 fn adjust_score(&self, raw_score: f32) -> f32 {
473 if self.model_type == "rtdetr" {
474 raw_score.clamp(0.0, 1.0)
475 } else {
476 raw_score
477 }
478 }
479
480 fn compute_nms_keep_indices(
483 &self,
484 boxes: &[BoundingBox],
485 classes: &[usize],
486 scores: &[f32],
487 ) -> Vec<usize> {
488 let mut indices: Vec<usize> = (0..boxes.len()).collect();
490 indices.sort_by(|&a, &b| {
491 scores[b]
492 .partial_cmp(&scores[a])
493 .unwrap_or(std::cmp::Ordering::Equal)
494 });
495
496 let bounds: Vec<(f32, f32, f32, f32)> = boxes.iter().map(|b| b.aabb()).collect();
499
500 let mut keep = Vec::new();
501 let mut suppressed = vec![false; boxes.len()];
502
503 for pos in 0..indices.len() {
504 let i = indices[pos];
505 if suppressed[i] {
506 continue;
507 }
508
509 keep.push(i);
510 if keep.len() >= self.max_detections {
511 break;
512 }
513
514 let (ix1, iy1, ix2, iy2) = bounds[i];
515 let ic = classes[i];
516 let area_i = (ix2 - ix1) * (iy2 - iy1);
517
518 for &j in &indices[pos + 1..] {
523 if suppressed[j] || classes[j] != ic {
524 continue;
525 }
526 let (jx1, jy1, jx2, jy2) = bounds[j];
527 let inter_x_min = ix1.max(jx1);
528 let inter_y_min = iy1.max(jy1);
529 let inter_x_max = ix2.min(jx2);
530 let inter_y_max = iy2.min(jy2);
531 if inter_x_min >= inter_x_max || inter_y_min >= inter_y_max {
532 continue;
533 }
534 let inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min);
535 let area_j = (jx2 - jx1) * (jy2 - jy1);
536 let union_area = area_i + area_j - inter_area;
537 if union_area > 0.0 {
538 let iou = inter_area / union_area;
539 if iou > self.nms_threshold {
540 suppressed[j] = true;
541 }
542 }
543 }
544 }
545
546 keep
547 }
548
549 fn apply_nms(
551 &self,
552 boxes: Vec<BoundingBox>,
553 classes: Vec<usize>,
554 scores: Vec<f32>,
555 ) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
556 if boxes.is_empty() {
557 return (boxes, classes, scores);
558 }
559
560 let keep = self.compute_nms_keep_indices(&boxes, &classes, &scores);
561
562 let filtered_boxes: Vec<BoundingBox> = keep.iter().map(|&i| boxes[i].clone()).collect();
563 let filtered_classes: Vec<usize> = keep.iter().map(|&i| classes[i]).collect();
564 let filtered_scores: Vec<f32> = keep.iter().map(|&i| scores[i]).collect();
565
566 (filtered_boxes, filtered_classes, filtered_scores)
567 }
568
569 #[allow(dead_code)]
571 fn calculate_iou(&self, box1: &BoundingBox, box2: &BoundingBox) -> f32 {
572 let (x1_min, y1_min, x1_max, y1_max) = self.get_bbox_bounds(box1);
574
575 let (x2_min, y2_min, x2_max, y2_max) = self.get_bbox_bounds(box2);
577
578 let x_min = x1_min.max(x2_min);
580 let y_min = y1_min.max(y2_min);
581 let x_max = x1_max.min(x2_max);
582 let y_max = y1_max.min(y2_max);
583
584 if x_max <= x_min || y_max <= y_min {
585 return 0.0;
586 }
587
588 let intersection = (x_max - x_min) * (y_max - y_min);
589 let area1 = (x1_max - x1_min) * (y1_max - y1_min);
590 let area2 = (x2_max - x2_min) * (y2_max - y2_min);
591 let union = area1 + area2 - intersection;
592
593 if union > 0.0 {
594 intersection / union
595 } else {
596 0.0
597 }
598 }
599
600 #[allow(dead_code)]
602 fn get_bbox_bounds(&self, bbox: &BoundingBox) -> (f32, f32, f32, f32) {
603 if bbox.points.is_empty() {
604 return (0.0, 0.0, 0.0, 0.0);
605 }
606
607 let mut x_min = f32::INFINITY;
608 let mut y_min = f32::INFINITY;
609 let mut x_max = f32::NEG_INFINITY;
610 let mut y_max = f32::NEG_INFINITY;
611
612 for point in &bbox.points {
613 x_min = x_min.min(point.x);
614 y_min = y_min.min(point.y);
615 x_max = x_max.max(point.x);
616 y_max = y_max.max(point.y);
617 }
618
619 (x_min, y_min, x_max, y_max)
620 }
621}
622
623pub fn unclip_boxes(
637 boxes: &[BoundingBox],
638 classes: &[usize],
639 width_ratio: f32,
640 height_ratio: f32,
641 per_class_ratios: Option<&std::collections::HashMap<usize, (f32, f32)>>,
642) -> Vec<BoundingBox> {
643 boxes
644 .iter()
645 .zip(classes.iter())
646 .map(|(bbox, &class_id)| {
647 let (w_ratio, h_ratio) = per_class_ratios
649 .and_then(|ratios| ratios.get(&class_id).copied())
650 .unwrap_or((width_ratio, height_ratio));
651
652 if (w_ratio - 1.0).abs() < 1e-6 && (h_ratio - 1.0).abs() < 1e-6 {
654 return bbox.clone();
655 }
656
657 let (x_min, y_min, x_max, y_max) = bbox.aabb();
659
660 let width = x_max - x_min;
662 let height = y_max - y_min;
663 let center_x = x_min + width * 0.5;
664 let center_y = y_min + height * 0.5;
665
666 let new_width = width * w_ratio;
668 let new_height = height * h_ratio;
669 let half_new_w = new_width * 0.5;
670 let half_new_h = new_height * 0.5;
671
672 let new_x_min = center_x - half_new_w;
674 let new_y_min = center_y - half_new_h;
675 let new_x_max = center_x + half_new_w;
676 let new_y_max = center_y + half_new_h;
677
678 BoundingBox::from_coords(new_x_min, new_y_min, new_x_max, new_y_max)
679 })
680 .collect()
681}
682
683pub fn merge_boxes(box1: &BoundingBox, box2: &BoundingBox, mode: MergeBboxMode) -> BoundingBox {
693 let (x1_min, y1_min, x1_max, y1_max) = box1.aabb();
694 let (x2_min, y2_min, x2_max, y2_max) = box2.aabb();
695
696 let area1 = (x1_max - x1_min) * (y1_max - y1_min);
697 let area2 = (x2_max - x2_min) * (y2_max - y2_min);
698
699 match mode {
700 MergeBboxMode::Large => {
701 if area1 >= area2 {
703 box1.clone()
704 } else {
705 box2.clone()
706 }
707 }
708 MergeBboxMode::Small => {
709 if area1 <= area2 {
711 box1.clone()
712 } else {
713 box2.clone()
714 }
715 }
716 MergeBboxMode::Union => {
717 let union_x_min = x1_min.min(x2_min);
719 let union_y_min = y1_min.min(y2_min);
720 let union_x_max = x1_max.max(x2_max);
721 let union_y_max = y1_max.max(y2_max);
722 BoundingBox::from_coords(union_x_min, union_y_min, union_x_max, union_y_max)
723 }
724 }
725}
726
727pub fn apply_nms_with_merge(
744 boxes: Vec<BoundingBox>,
745 classes: Vec<usize>,
746 scores: Vec<f32>,
747 class_labels: &HashMap<usize, String>,
748 class_merge_modes: &HashMap<String, MergeBboxMode>,
749 nms_threshold: f32,
750 max_detections: usize,
751) -> (Vec<BoundingBox>, Vec<usize>, Vec<f32>) {
752 if boxes.is_empty() {
753 return (boxes, classes, scores);
754 }
755
756 let mut indices: Vec<usize> = (0..boxes.len()).collect();
758 indices.sort_by(|&a, &b| {
759 scores[b]
760 .partial_cmp(&scores[a])
761 .unwrap_or(std::cmp::Ordering::Equal)
762 });
763
764 let mut result_boxes = Vec::new();
765 let mut result_classes = Vec::new();
766 let mut result_scores = Vec::new();
767 let mut result_order_indices = Vec::new();
768 let mut processed = vec![false; boxes.len()];
769
770 for &i in &indices {
771 if processed[i] {
772 continue;
773 }
774
775 processed[i] = true;
776
777 let class_label = class_labels
779 .get(&classes[i])
780 .map(|s| s.as_str())
781 .unwrap_or("unknown");
782 let merge_mode = class_merge_modes
783 .get(class_label)
784 .copied()
785 .unwrap_or(MergeBboxMode::Large);
786
787 let mut merged_box = boxes[i].clone();
788 let mut best_score = scores[i];
789 let mut order_idx = i;
790
791 for &j in &indices {
793 if i != j && !processed[j] && classes[i] == classes[j] {
794 let iou = calculate_iou_static(&merged_box, &boxes[j]);
795 if iou > nms_threshold {
796 merged_box = merge_boxes(&merged_box, &boxes[j], merge_mode);
798 best_score = best_score.max(scores[j]);
799 order_idx = order_idx.min(j);
800 processed[j] = true;
801 }
802 }
803 }
804
805 result_boxes.push(merged_box);
806 result_classes.push(classes[i]);
807 result_scores.push(best_score);
808 result_order_indices.push(order_idx);
809 }
810
811 let take_count = max_detections.min(result_boxes.len());
815
816 let mut merged: Vec<(usize, BoundingBox, usize, f32)> = result_order_indices
820 .into_iter()
821 .zip(result_boxes)
822 .zip(result_classes)
823 .zip(result_scores)
824 .map(|(((order, bbox), class_id), score)| (order, bbox, class_id, score))
825 .take(take_count) .collect();
827
828 merged.sort_by_key(|(a, _, _, _)| *a);
829
830 let mut final_boxes = Vec::new();
831 let mut final_classes = Vec::new();
832 let mut final_scores = Vec::new();
833
834 for (_, bbox, class_id, score) in merged {
835 final_boxes.push(bbox);
836 final_classes.push(class_id);
837 final_scores.push(score);
838 }
839
840 (final_boxes, final_classes, final_scores)
841}
842
843fn calculate_iou_static(box1: &BoundingBox, box2: &BoundingBox) -> f32 {
845 let (x1_min, y1_min, x1_max, y1_max) = box1.aabb();
846 let (x2_min, y2_min, x2_max, y2_max) = box2.aabb();
847
848 let x_min = x1_min.max(x2_min);
850 let y_min = y1_min.max(y2_min);
851 let x_max = x1_max.min(x2_max);
852 let y_max = y1_max.min(y2_max);
853
854 if x_max <= x_min || y_max <= y_min {
855 return 0.0;
856 }
857
858 let intersection = (x_max - x_min) * (y_max - y_min);
859 let area1 = (x1_max - x1_min) * (y1_max - y1_min);
860 let area2 = (x2_max - x2_min) * (y2_max - y2_min);
861 let union = area1 + area2 - intersection;
862
863 if union > 0.0 {
864 intersection / union
865 } else {
866 0.0
867 }
868}
869
870impl Default for LayoutPostProcess {
871 fn default() -> Self {
872 Self {
873 num_classes: 5, score_threshold: 0.5,
875 nms_threshold: 0.5,
876 max_detections: 100,
877 model_type: "picodet".to_string(),
878 }
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885
886 #[test]
887 fn test_layout_postprocess_creation() {
888 let processor = LayoutPostProcess::default();
889 assert_eq!(processor.num_classes, 5);
890 assert_eq!(processor.score_threshold, 0.5);
891 }
892
893 #[test]
894 fn test_iou_calculation() {
895 let processor = LayoutPostProcess::default();
896
897 let box1 = BoundingBox::new(vec![
899 Point::new(0.0, 0.0),
900 Point::new(100.0, 0.0),
901 Point::new(100.0, 100.0),
902 Point::new(0.0, 100.0),
903 ]);
904 let box2 = box1.clone();
905
906 assert_eq!(processor.calculate_iou(&box1, &box2), 1.0);
907
908 let box3 = BoundingBox::new(vec![
910 Point::new(200.0, 200.0),
911 Point::new(300.0, 200.0),
912 Point::new(300.0, 300.0),
913 Point::new(200.0, 300.0),
914 ]);
915
916 assert_eq!(processor.calculate_iou(&box1, &box3), 0.0);
917 }
918
919 #[test]
920 fn test_pp_doclayout_under_width_output_does_not_panic() {
921 let processor = LayoutPostProcess::new(17, 0.5, 0.5, 100, "pp-doclayout".to_string());
925 let img_shape = ImageScaleInfo::new(100.0, 100.0, 1.0, 1.0);
926
927 let preds = ndarray::Array3::<f32>::zeros((3, 1, 4));
929 let (boxes, classes, scores) = processor.process_pp_doclayout(preds.view(), &img_shape);
930 assert!(boxes.is_empty() && classes.is_empty() && scores.is_empty());
931
932 let preds = ndarray::Array3::<f32>::zeros((3, 0, 8));
934 let (boxes, _, _) = processor.process_pp_doclayout(preds.view(), &img_shape);
935 assert!(boxes.is_empty());
936 }
937
938 #[test]
939 fn test_picodet_argmax_handles_non_positive_scores() {
940 let processor = LayoutPostProcess::new(3, -1.0, 1.0, 100, "picodet".to_string());
944 let img_shape = ImageScaleInfo::new(100.0, 100.0, 1.0, 1.0);
945
946 let preds = ndarray::Array3::<f32>::from_shape_vec(
949 (1, 1, 4 + 3),
950 vec![10.0, 10.0, 50.0, 50.0, -0.9, -0.2, -0.5],
951 )
952 .unwrap();
953 let (boxes, classes, scores) = processor.process_picodet(preds.view(), &img_shape);
954 assert_eq!(boxes.len(), 1);
955 assert_eq!(classes[0], 1);
956 assert!((scores[0] - (-0.2)).abs() < 1e-6);
957 }
958}