1use crate::error::{Result, VisionError};
30
31#[derive(Clone, Debug, PartialEq)]
41pub struct BoundingBox {
42 pub x1: f64,
44 pub y1: f64,
46 pub x2: f64,
48 pub y2: f64,
50 pub score: f64,
52 pub class_id: usize,
54}
55
56impl BoundingBox {
57 pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, score: f64, class_id: usize) -> Self {
65 Self {
66 x1: x1.min(x2),
67 y1: y1.min(y2),
68 x2: x1.max(x2),
69 y2: y1.max(y2),
70 score,
71 class_id,
72 }
73 }
74
75 pub fn from_center(cx: f64, cy: f64, w: f64, h: f64, score: f64, class_id: usize) -> Self {
77 let hw = w.abs() * 0.5;
78 let hh = h.abs() * 0.5;
79 Self::new(cx - hw, cy - hh, cx + hw, cy + hh, score, class_id)
80 }
81
82 #[inline]
84 pub fn width(&self) -> f64 {
85 (self.x2 - self.x1).max(0.0)
86 }
87
88 #[inline]
90 pub fn height(&self) -> f64 {
91 (self.y2 - self.y1).max(0.0)
92 }
93
94 #[inline]
96 pub fn area(&self) -> f64 {
97 self.width() * self.height()
98 }
99
100 #[inline]
102 pub fn center(&self) -> (f64, f64) {
103 ((self.x1 + self.x2) * 0.5, (self.y1 + self.y2) * 0.5)
104 }
105
106 pub fn scale(&self, factor: f64) -> Self {
110 let (cx, cy) = self.center();
111 let hw = self.width() * 0.5 * factor;
112 let hh = self.height() * 0.5 * factor;
113 Self::new(
114 cx - hw,
115 cy - hh,
116 cx + hw,
117 cy + hh,
118 self.score,
119 self.class_id,
120 )
121 }
122
123 pub fn clip(&self, img_w: f64, img_h: f64) -> Self {
125 Self::new(
126 self.x1.max(0.0),
127 self.y1.max(0.0),
128 self.x2.min(img_w),
129 self.y2.min(img_h),
130 self.score,
131 self.class_id,
132 )
133 }
134}
135
136pub fn compute_iou(a: &BoundingBox, b: &BoundingBox) -> f64 {
153 let ix1 = a.x1.max(b.x1);
154 let iy1 = a.y1.max(b.y1);
155 let ix2 = a.x2.min(b.x2);
156 let iy2 = a.y2.min(b.y2);
157
158 let inter_w = (ix2 - ix1).max(0.0);
159 let inter_h = (iy2 - iy1).max(0.0);
160 let inter = inter_w * inter_h;
161
162 let union = a.area() + b.area() - inter;
163 if union < 1e-12 {
164 0.0
165 } else {
166 inter / union
167 }
168}
169
170pub fn nms(boxes: &[BoundingBox], iou_threshold: f64) -> Vec<BoundingBox> {
186 if boxes.is_empty() {
187 return Vec::new();
188 }
189
190 let mut sorted: Vec<&BoundingBox> = boxes.iter().collect();
192 sorted.sort_by(|a, b| {
193 b.score
194 .partial_cmp(&a.score)
195 .unwrap_or(std::cmp::Ordering::Equal)
196 });
197
198 let n = sorted.len();
199 let mut suppressed = vec![false; n];
200 let mut kept = Vec::new();
201
202 for i in 0..n {
203 if suppressed[i] {
204 continue;
205 }
206 kept.push(sorted[i].clone());
207 for j in (i + 1)..n {
208 if suppressed[j] {
209 continue;
210 }
211 if sorted[i].class_id == sorted[j].class_id
213 && compute_iou(sorted[i], sorted[j]) > iou_threshold
214 {
215 suppressed[j] = true;
216 }
217 }
218 }
219 kept
220}
221
222#[derive(Clone, Debug, Copy, PartialEq)]
228pub enum SoftNmsMethod {
229 Linear,
231 Gaussian {
233 sigma: f64,
235 },
236}
237
238pub fn soft_nms(
253 boxes: &[BoundingBox],
254 iou_threshold: f64,
255 score_threshold: f64,
256 method: SoftNmsMethod,
257) -> Vec<BoundingBox> {
258 if boxes.is_empty() {
259 return Vec::new();
260 }
261
262 let mut candidates: Vec<BoundingBox> = boxes.to_vec();
263 let mut kept: Vec<BoundingBox> = Vec::with_capacity(candidates.len());
264
265 while !candidates.is_empty() {
266 let best_idx = candidates
268 .iter()
269 .enumerate()
270 .max_by(|(_, a), (_, b)| {
271 a.score
272 .partial_cmp(&b.score)
273 .unwrap_or(std::cmp::Ordering::Equal)
274 })
275 .map(|(i, _)| i)
276 .unwrap_or(0);
277
278 let best = candidates.swap_remove(best_idx);
279
280 for candidate in candidates.iter_mut() {
282 let iou = compute_iou(&best, candidate);
283 if iou > iou_threshold {
284 match method {
285 SoftNmsMethod::Linear => {
286 candidate.score *= 1.0 - iou;
287 }
288 SoftNmsMethod::Gaussian { sigma } => {
289 candidate.score *= (-iou * iou / (sigma * sigma)).exp();
290 }
291 }
292 }
293 }
294
295 kept.push(best);
296 candidates.retain(|b| b.score >= score_threshold);
298 }
299
300 kept.sort_by(|a, b| {
302 b.score
303 .partial_cmp(&a.score)
304 .unwrap_or(std::cmp::Ordering::Equal)
305 });
306 kept
307}
308
309#[derive(Clone, Debug, PartialEq)]
315pub struct WindowSpec {
316 pub x: usize,
318 pub y: usize,
320 pub width: usize,
322 pub height: usize,
324 pub scale: f64,
326}
327
328pub fn sliding_window(
347 img_width: usize,
348 img_height: usize,
349 win_width: usize,
350 win_height: usize,
351 stride: usize,
352 scale_factor: f64,
353 num_scales: usize,
354) -> Result<Vec<WindowSpec>> {
355 if win_width == 0 || win_height == 0 {
356 return Err(VisionError::InvalidInput(
357 "sliding_window: window dimensions must be > 0".to_string(),
358 ));
359 }
360 if stride == 0 {
361 return Err(VisionError::InvalidInput(
362 "sliding_window: stride must be > 0".to_string(),
363 ));
364 }
365 if scale_factor <= 0.0 {
366 return Err(VisionError::InvalidInput(
367 "sliding_window: scale_factor must be positive".to_string(),
368 ));
369 }
370
371 let mut windows = Vec::new();
372
373 for scale_idx in 0..num_scales {
374 let scale = scale_factor.powi(scale_idx as i32);
375 let w = ((win_width as f64) * scale).round() as usize;
376 let h = ((win_height as f64) * scale).round() as usize;
377
378 if w == 0 || h == 0 || w > img_width || h > img_height {
379 continue;
381 }
382
383 let step = ((stride as f64) * scale).round().max(1.0) as usize;
384
385 let mut y = 0usize;
386 while y + h <= img_height {
387 let mut x = 0usize;
388 while x + w <= img_width {
389 windows.push(WindowSpec {
390 x,
391 y,
392 width: w,
393 height: h,
394 scale,
395 });
396 x += step;
397 }
398 y += step;
399 }
400 }
401
402 Ok(windows)
403}
404
405#[derive(Clone, Debug)]
411pub struct AnchorConfig {
412 pub base_sizes: Vec<f64>,
414 pub aspect_ratios: Vec<f64>,
416 pub scales: Vec<f64>,
418 pub img_width: usize,
420 pub img_height: usize,
422 pub feat_width: usize,
424 pub feat_height: usize,
426}
427
428impl Default for AnchorConfig {
429 fn default() -> Self {
430 Self {
431 base_sizes: vec![32.0, 64.0, 128.0, 256.0, 512.0],
432 aspect_ratios: vec![0.5, 1.0, 2.0],
433 scales: vec![1.0, 2.0f64.sqrt()],
434 img_width: 512,
435 img_height: 512,
436 feat_width: 16,
437 feat_height: 16,
438 }
439 }
440}
441
442pub fn anchor_boxes(config: &AnchorConfig) -> Result<Vec<BoundingBox>> {
451 if config.feat_width == 0 || config.feat_height == 0 {
452 return Err(VisionError::InvalidInput(
453 "anchor_boxes: feature map dimensions must be > 0".to_string(),
454 ));
455 }
456 if config.img_width == 0 || config.img_height == 0 {
457 return Err(VisionError::InvalidInput(
458 "anchor_boxes: image dimensions must be > 0".to_string(),
459 ));
460 }
461 if config.base_sizes.is_empty() || config.aspect_ratios.is_empty() || config.scales.is_empty() {
462 return Err(VisionError::InvalidInput(
463 "anchor_boxes: base_sizes, aspect_ratios, and scales must be non-empty".to_string(),
464 ));
465 }
466
467 let stride_x = config.img_width as f64 / config.feat_width as f64;
468 let stride_y = config.img_height as f64 / config.feat_height as f64;
469
470 let mut anchors = Vec::new();
471
472 for row in 0..config.feat_height {
473 let cy = (row as f64 + 0.5) * stride_y;
474 for col in 0..config.feat_width {
475 let cx = (col as f64 + 0.5) * stride_x;
476
477 for &base_size in &config.base_sizes {
478 for &ratio in &config.aspect_ratios {
479 for &scale in &config.scales {
480 let area = (base_size * scale).powi(2);
482 let w = (area * ratio).sqrt();
484 let h = area / w;
485
486 anchors.push(BoundingBox::new(
487 cx - w * 0.5,
488 cy - h * 0.5,
489 cx + w * 0.5,
490 cy + h * 0.5,
491 1.0,
492 0,
493 ));
494 }
495 }
496 }
497 }
498 }
499
500 Ok(anchors)
501}
502
503#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_bounding_box_geometry() {
513 let b = BoundingBox::new(10.0, 20.0, 50.0, 80.0, 0.9, 1);
514 assert!((b.width() - 40.0).abs() < 1e-10);
515 assert!((b.height() - 60.0).abs() < 1e-10);
516 assert!((b.area() - 2400.0).abs() < 1e-10);
517 let (cx, cy) = b.center();
518 assert!((cx - 30.0).abs() < 1e-10);
519 assert!((cy - 50.0).abs() < 1e-10);
520 }
521
522 #[test]
523 fn test_compute_iou_identical() {
524 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0, 1.0, 0);
525 assert!((compute_iou(&a, &a) - 1.0).abs() < 1e-10);
526 }
527
528 #[test]
529 fn test_compute_iou_disjoint() {
530 let a = BoundingBox::new(0.0, 0.0, 5.0, 5.0, 1.0, 0);
531 let b = BoundingBox::new(10.0, 10.0, 15.0, 15.0, 1.0, 0);
532 assert!((compute_iou(&a, &b)).abs() < 1e-10);
533 }
534
535 #[test]
536 fn test_compute_iou_partial() {
537 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0, 1.0, 0);
538 let b = BoundingBox::new(5.0, 5.0, 15.0, 15.0, 1.0, 0);
539 let iou = compute_iou(&a, &b);
540 assert!((iou - 25.0 / 175.0).abs() < 1e-10);
542 }
543
544 #[test]
545 fn test_nms_removes_overlapping() {
546 let boxes = vec![
547 BoundingBox::new(0.0, 0.0, 10.0, 10.0, 0.9, 0),
548 BoundingBox::new(1.0, 1.0, 11.0, 11.0, 0.7, 0), BoundingBox::new(50.0, 50.0, 60.0, 60.0, 0.8, 1), ];
551 let kept = nms(&boxes, 0.5);
552 assert_eq!(kept.len(), 2);
553 assert!((kept[0].score - 0.9).abs() < 1e-10);
554 }
555
556 #[test]
557 fn test_nms_different_classes_kept() {
558 let boxes = vec![
560 BoundingBox::new(0.0, 0.0, 10.0, 10.0, 0.9, 0),
561 BoundingBox::new(0.0, 0.0, 10.0, 10.0, 0.8, 1),
562 ];
563 let kept = nms(&boxes, 0.5);
564 assert_eq!(kept.len(), 2);
565 }
566
567 #[test]
568 fn test_soft_nms_linear() {
569 let boxes = vec![
570 BoundingBox::new(0.0, 0.0, 10.0, 10.0, 0.9, 0),
571 BoundingBox::new(1.0, 1.0, 11.0, 11.0, 0.8, 0),
572 BoundingBox::new(50.0, 50.0, 60.0, 60.0, 0.7, 0),
573 ];
574 let kept = soft_nms(&boxes, 0.3, 0.3, SoftNmsMethod::Linear);
575 assert!(!kept.is_empty());
577 for b in &kept {
579 assert!(b.score >= 0.3);
580 }
581 }
582
583 #[test]
584 fn test_soft_nms_gaussian() {
585 let boxes = vec![
586 BoundingBox::new(0.0, 0.0, 10.0, 10.0, 0.9, 0),
587 BoundingBox::new(0.5, 0.5, 10.5, 10.5, 0.8, 0),
588 ];
589 let kept = soft_nms(&boxes, 0.3, 0.1, SoftNmsMethod::Gaussian { sigma: 0.5 });
590 assert!(!kept.is_empty());
591 }
592
593 #[test]
594 fn test_sliding_window_basic() {
595 let windows =
596 sliding_window(100, 100, 20, 20, 10, 1.0, 1).expect("sliding_window should succeed");
597 assert_eq!(windows.len(), 81);
599 for w in &windows {
600 assert!(w.x + w.width <= 100);
601 assert!(w.y + w.height <= 100);
602 }
603 }
604
605 #[test]
606 fn test_sliding_window_error_zero_dims() {
607 assert!(sliding_window(100, 100, 0, 20, 10, 1.0, 1).is_err());
608 }
609
610 #[test]
611 fn test_anchor_boxes_count() {
612 let config = AnchorConfig {
613 base_sizes: vec![32.0],
614 aspect_ratios: vec![1.0],
615 scales: vec![1.0],
616 img_width: 256,
617 img_height: 256,
618 feat_width: 4,
619 feat_height: 4,
620 };
621 let anchors = anchor_boxes(&config).expect("anchor_boxes should succeed");
622 assert_eq!(anchors.len(), 16);
624 }
625
626 #[test]
627 fn test_anchor_boxes_multi() {
628 let config = AnchorConfig {
629 base_sizes: vec![32.0, 64.0],
630 aspect_ratios: vec![0.5, 1.0, 2.0],
631 scales: vec![1.0, 2.0f64.sqrt()],
632 img_width: 512,
633 img_height: 512,
634 feat_width: 8,
635 feat_height: 8,
636 };
637 let anchors = anchor_boxes(&config).expect("anchor_boxes should succeed");
638 assert_eq!(anchors.len(), 768);
640 }
641
642 #[test]
643 fn test_from_center() {
644 let b = BoundingBox::from_center(10.0, 10.0, 4.0, 6.0, 0.9, 0);
645 assert!((b.x1 - 8.0).abs() < 1e-10);
646 assert!((b.y1 - 7.0).abs() < 1e-10);
647 assert!((b.x2 - 12.0).abs() < 1e-10);
648 assert!((b.y2 - 13.0).abs() < 1e-10);
649 }
650}