use ndarray::{Array1, Array2};
use crate::geometry::{
oriented_box_area, oriented_box_iou, polygon_centroid, Point, Position, Rect,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Detection {
pub bbox: [f32; 4],
pub confidence: f32,
pub class_id: usize,
pub tracker_id: Option<usize>,
pub mask: Option<Vec<[f32; 2]>>,
pub obb: Option<[[f32; 2]; 4]>,
}
impl Detection {
pub fn new(bbox: [f32; 4], confidence: f32, class_id: usize) -> Self {
Self {
bbox,
confidence,
class_id,
tracker_id: None,
mask: None,
obb: None,
}
}
pub fn from_obb(corners: [[f32; 2]; 4], confidence: f32, class_id: usize) -> Self {
let xs = corners.iter().map(|c| c[0]);
let ys = corners.iter().map(|c| c[1]);
let x1 = xs.clone().fold(f32::INFINITY, f32::min);
let x2 = xs.fold(f32::NEG_INFINITY, f32::max);
let y1 = ys.clone().fold(f32::INFINITY, f32::min);
let y2 = ys.fold(f32::NEG_INFINITY, f32::max);
Self {
bbox: [x1, y1, x2, y2],
confidence,
class_id,
tracker_id: None,
mask: Some(corners.to_vec()),
obb: Some(corners),
}
}
pub fn width(&self) -> f32 {
self.bbox[2] - self.bbox[0]
}
pub fn height(&self) -> f32 {
self.bbox[3] - self.bbox[1]
}
pub fn area(&self) -> f32 {
if let Some(obb) = &self.obb {
return oriented_box_area(&corners_to_points(obb));
}
self.width().max(0.0) * self.height().max(0.0)
}
pub fn centroid(&self) -> (f32, f32) {
(
(self.bbox[0] + self.bbox[2]) / 2.0,
(self.bbox[1] + self.bbox[3]) / 2.0,
)
}
pub fn iou(&self, other: &Detection) -> f32 {
if let (Some(a), Some(b)) = (&self.obb, &other.obb) {
return oriented_box_iou(&corners_to_points(a), &corners_to_points(b));
}
bbox_iou(self.bbox, other.bbox)
}
pub fn anchor_point(&self, position: Position) -> Point {
if position == Position::CenterOfMass {
if let Some(mask) = &self.mask {
if !mask.is_empty() {
let polygon: Vec<Point> = mask.iter().map(|&[x, y]| Point::new(x, y)).collect();
return polygon_centroid(&polygon);
}
}
return Rect::from_xyxy(self.bbox).center();
}
Rect::from_xyxy(self.bbox).anchor(position)
}
}
fn corners_to_points(corners: &[[f32; 2]; 4]) -> [Point; 4] {
corners.map(|[x, y]| Point::new(x, y))
}
pub(crate) fn bbox_iou(a: [f32; 4], b: [f32; 4]) -> f32 {
let [ax1, ay1, ax2, ay2] = a;
let [bx1, by1, bx2, by2] = b;
let ix1 = ax1.max(bx1);
let iy1 = ay1.max(by1);
let ix2 = ax2.min(bx2);
let iy2 = ay2.min(by2);
let intersection = (ix2 - ix1).max(0.0) * (iy2 - iy1).max(0.0);
let area_a = (ax2 - ax1).max(0.0) * (ay2 - ay1).max(0.0);
let area_b = (bx2 - bx1).max(0.0) * (by2 - by1).max(0.0);
let union = area_a + area_b - intersection;
if union <= 0.0 {
0.0
} else {
intersection / union
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Detections {
pub detections: Vec<Detection>,
}
impl Detections {
pub fn new(detections: Vec<Detection>) -> Self {
Self { detections }
}
pub fn empty() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.detections.len()
}
pub fn is_empty(&self) -> bool {
self.detections.is_empty()
}
pub fn iter(&self) -> std::slice::Iter<'_, Detection> {
self.detections.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Detection> {
self.detections.iter_mut()
}
pub fn filter_by_confidence(&self, threshold: f32) -> Detections {
self.filter(|d| d.confidence >= threshold)
}
pub fn filter_by_class(&self, class_ids: &[usize]) -> Detections {
self.filter(|d| class_ids.contains(&d.class_id))
}
pub fn filter<F>(&self, predicate: F) -> Detections
where
F: Fn(&Detection) -> bool,
{
Detections::new(
self.detections
.iter()
.filter(|d| predicate(d))
.cloned()
.collect(),
)
}
pub fn non_max_suppression(&self, iou_threshold: f32, class_agnostic: bool) -> Detections {
let order = Self::confidence_order(&self.detections);
let mut suppressed = vec![false; self.len()];
let mut kept = Vec::with_capacity(self.len());
for pos in 0..order.len() {
let i = order[pos];
if suppressed[i] {
continue;
}
kept.push(self.detections[i].clone());
for &j in &order[pos + 1..] {
if suppressed[j] {
continue;
}
let same_class =
class_agnostic || self.detections[i].class_id == self.detections[j].class_id;
if same_class && self.detections[i].iou(&self.detections[j]) > iou_threshold {
suppressed[j] = true;
}
}
}
Detections::new(kept)
}
fn confidence_order(detections: &[Detection]) -> Vec<usize> {
let mut order: Vec<usize> = (0..detections.len()).collect();
order.sort_by(|&a, &b| {
detections[b]
.confidence
.total_cmp(&detections[a].confidence)
});
order
}
#[cfg(feature = "parallel")]
pub fn non_max_suppression_parallel(
&self,
iou_threshold: f32,
class_agnostic: bool,
) -> Detections {
use rayon::prelude::*;
let order = Self::confidence_order(&self.detections);
let n = order.len();
let would_suppress: Vec<Vec<bool>> = (0..n)
.into_par_iter()
.map(|pos| {
let i = order[pos];
order[pos + 1..]
.iter()
.map(|&j| {
let same_class = class_agnostic
|| self.detections[i].class_id == self.detections[j].class_id;
same_class && self.detections[i].iou(&self.detections[j]) > iou_threshold
})
.collect()
})
.collect();
let mut suppressed = vec![false; n];
let mut kept = Vec::with_capacity(n);
for pos in 0..n {
let i = order[pos];
if suppressed[i] {
continue;
}
kept.push(self.detections[i].clone());
for (k, &j) in order[pos + 1..].iter().enumerate() {
if would_suppress[pos][k] {
suppressed[j] = true;
}
}
}
Detections::new(kept)
}
pub fn merge(sets: &[Detections]) -> Detections {
let mut merged = Vec::with_capacity(sets.iter().map(Detections::len).sum());
for set in sets {
merged.extend(set.detections.iter().cloned());
}
Detections::new(merged)
}
pub fn merge_with(&self, other: &Detections) -> Detections {
Detections::merge(&[self.clone(), other.clone()])
}
pub fn scale(&self, sx: f32, sy: f32) -> Detections {
Detections::new(
self.detections
.iter()
.map(|d| {
let mut scaled = d.clone();
let [x1, y1, x2, y2] = d.bbox;
scaled.bbox = [x1 * sx, y1 * sy, x2 * sx, y2 * sy];
scaled.mask = d
.mask
.as_ref()
.map(|polygon| polygon.iter().map(|&[x, y]| [x * sx, y * sy]).collect());
scaled.obb = d.obb.map(|corners| corners.map(|[x, y]| [x * sx, y * sy]));
scaled
})
.collect(),
)
}
pub fn xyxy(&self) -> Array2<f32> {
let mut arr = Array2::<f32>::zeros((self.len(), 4));
for (i, d) in self.detections.iter().enumerate() {
arr.row_mut(i).assign(&Array1::from_vec(d.bbox.to_vec()));
}
arr
}
pub fn confidence(&self) -> Array1<f32> {
Array1::from_iter(self.detections.iter().map(|d| d.confidence))
}
pub fn class_id(&self) -> Array1<usize> {
Array1::from_iter(self.detections.iter().map(|d| d.class_id))
}
pub fn tracker_id(&self) -> Array1<Option<usize>> {
Array1::from_iter(self.detections.iter().map(|d| d.tracker_id))
}
}
impl IntoIterator for Detections {
type Item = Detection;
type IntoIter = std::vec::IntoIter<Detection>;
fn into_iter(self) -> Self::IntoIter {
self.detections.into_iter()
}
}
impl<'a> IntoIterator for &'a Detections {
type Item = &'a Detection;
type IntoIter = std::slice::Iter<'a, Detection>;
fn into_iter(self) -> Self::IntoIter {
self.detections.iter()
}
}
impl FromIterator<Detection> for Detections {
fn from_iter<T: IntoIterator<Item = Detection>>(iter: T) -> Self {
Detections::new(iter.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det(bbox: [f32; 4], confidence: f32, class_id: usize) -> Detection {
Detection::new(bbox, confidence, class_id)
}
#[test]
fn width_height_area() {
let d = det([0.0, 0.0, 10.0, 4.0], 0.9, 0);
assert_eq!(d.width(), 10.0);
assert_eq!(d.height(), 4.0);
assert_eq!(d.area(), 40.0);
}
#[test]
fn centroid_is_bbox_center() {
let d = det([0.0, 0.0, 10.0, 10.0], 0.9, 0);
assert_eq!(d.centroid(), (5.0, 5.0));
}
#[test]
fn iou_of_identical_boxes_is_one() {
let a = det([0.0, 0.0, 10.0, 10.0], 0.9, 0);
let b = det([0.0, 0.0, 10.0, 10.0], 0.5, 1);
assert!((a.iou(&b) - 1.0).abs() < 1e-6);
}
#[test]
fn iou_of_disjoint_boxes_is_zero() {
let a = det([0.0, 0.0, 10.0, 10.0], 0.9, 0);
let b = det([20.0, 20.0, 30.0, 30.0], 0.9, 0);
assert_eq!(a.iou(&b), 0.0);
}
#[test]
fn iou_of_partial_overlap() {
let a = det([0.0, 0.0, 10.0, 10.0], 0.9, 0);
let b = det([5.0, 0.0, 15.0, 10.0], 0.9, 0);
assert!((a.iou(&b) - (50.0 / 150.0)).abs() < 1e-6);
}
#[test]
fn filter_by_confidence_keeps_only_above_threshold() {
let dets = Detections::new(vec![
det([0.0, 0.0, 1.0, 1.0], 0.9, 0),
det([0.0, 0.0, 1.0, 1.0], 0.2, 0),
]);
let filtered = dets.filter_by_confidence(0.5);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered.detections[0].confidence, 0.9);
}
#[test]
fn filter_by_class_keeps_only_matching_classes() {
let dets = Detections::new(vec![
det([0.0, 0.0, 1.0, 1.0], 0.9, 0),
det([0.0, 0.0, 1.0, 1.0], 0.9, 1),
det([0.0, 0.0, 1.0, 1.0], 0.9, 2),
]);
let filtered = dets.filter_by_class(&[0, 2]);
assert_eq!(filtered.len(), 2);
assert!(filtered.detections.iter().all(|d| d.class_id != 1));
}
#[test]
fn scale_multiplies_bbox_and_mask_coordinates() {
let mut d = det([1.0, 2.0, 3.0, 4.0], 0.9, 0);
d.mask = Some(vec![[1.0, 2.0], [3.0, 4.0]]);
let dets = Detections::new(vec![d]);
let scaled = dets.scale(2.0, 3.0);
assert_eq!(scaled.detections[0].bbox, [2.0, 6.0, 6.0, 12.0]);
assert_eq!(
scaled.detections[0].mask,
Some(vec![[2.0, 6.0], [6.0, 12.0]])
);
}
#[test]
fn merge_combines_all_detections() {
let a = Detections::new(vec![det([0.0, 0.0, 1.0, 1.0], 0.9, 0)]);
let b = Detections::new(vec![det([1.0, 1.0, 2.0, 2.0], 0.8, 1)]);
let merged = Detections::merge(&[a.clone(), b.clone()]);
assert_eq!(merged.len(), 2);
let merged2 = a.merge_with(&b);
assert_eq!(merged2.len(), 2);
}
#[test]
fn nms_suppresses_lower_confidence_overlapping_same_class_box() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9, 0),
det([1.0, 1.0, 11.0, 11.0], 0.6, 0),
]);
let kept = dets.non_max_suppression(0.5, false);
assert_eq!(kept.len(), 1);
assert_eq!(kept.detections[0].confidence, 0.9);
}
#[test]
fn nms_keeps_non_overlapping_boxes() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9, 0),
det([100.0, 100.0, 110.0, 110.0], 0.6, 0),
]);
let kept = dets.non_max_suppression(0.5, false);
assert_eq!(kept.len(), 2);
}
#[test]
fn nms_class_aware_keeps_overlapping_different_class_boxes() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9, 0),
det([1.0, 1.0, 11.0, 11.0], 0.6, 1),
]);
let kept = dets.non_max_suppression(0.5, false);
assert_eq!(kept.len(), 2);
}
#[test]
fn nms_class_agnostic_suppresses_across_classes() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9, 0),
det([1.0, 1.0, 11.0, 11.0], 0.6, 1),
]);
let kept = dets.non_max_suppression(0.5, true);
assert_eq!(kept.len(), 1);
assert_eq!(kept.detections[0].confidence, 0.9);
}
#[test]
fn xyxy_matches_bboxes() {
let dets = Detections::new(vec![
det([0.0, 1.0, 2.0, 3.0], 0.9, 0),
det([4.0, 5.0, 6.0, 7.0], 0.9, 0),
]);
let arr = dets.xyxy();
assert_eq!(arr.shape(), &[2, 4]);
assert_eq!(arr.row(0).to_vec(), vec![0.0, 1.0, 2.0, 3.0]);
assert_eq!(arr.row(1).to_vec(), vec![4.0, 5.0, 6.0, 7.0]);
}
#[test]
fn empty_detections_has_zero_length() {
let dets = Detections::empty();
assert!(dets.is_empty());
assert_eq!(dets.xyxy().shape(), &[0, 4]);
}
fn square_obb(cx: f32, cy: f32, half: f32) -> [[f32; 2]; 4] {
[
[cx - half, cy - half],
[cx + half, cy - half],
[cx + half, cy + half],
[cx - half, cy + half],
]
}
#[test]
fn from_obb_sets_bbox_to_the_axis_aligned_envelope() {
let corners = [[10.0, 0.0], [20.0, 10.0], [10.0, 20.0], [0.0, 10.0]];
let d = Detection::from_obb(corners, 0.9, 0);
assert_eq!(d.bbox, [0.0, 0.0, 20.0, 20.0]);
assert_eq!(d.obb, Some(corners));
}
#[test]
fn from_obb_also_populates_mask_for_existing_polygon_annotators() {
let corners = square_obb(10.0, 10.0, 5.0);
let d = Detection::from_obb(corners, 0.9, 0);
assert_eq!(d.mask, Some(corners.to_vec()));
}
#[test]
fn area_uses_true_rotated_area_not_the_envelope() {
let corners = [[10.0, 0.0], [20.0, 10.0], [10.0, 20.0], [0.0, 10.0]];
let d = Detection::from_obb(corners, 0.9, 0);
assert!((d.area() - 200.0).abs() < 1e-3);
}
#[test]
fn iou_prefers_obb_over_axis_aligned_bbox_when_both_have_one() {
let a = Detection::from_obb(square_obb(0.0, 0.0, 10.0), 0.9, 0);
let rotated = square_obb(0.0, 0.0, 10.0).map(|[x, y]| {
let theta: f32 = 45.0_f32.to_radians();
[
x * theta.cos() - y * theta.sin(),
x * theta.sin() + y * theta.cos(),
]
});
let b = Detection::from_obb(rotated, 0.9, 0);
assert!(a.iou(&b) < 0.8, "iou was {}", a.iou(&b));
}
#[test]
fn iou_falls_back_to_bbox_when_only_one_side_has_obb() {
let a = Detection::from_obb(square_obb(0.0, 0.0, 5.0), 0.9, 0);
let b = det([-5.0, -5.0, 5.0, 5.0], 0.9, 0);
assert!((a.iou(&b) - 1.0).abs() < 1e-6);
}
#[test]
fn scale_also_scales_obb_corners() {
let d = Detection::from_obb(square_obb(1.0, 1.0, 1.0), 0.9, 0);
let dets = Detections::new(vec![d]);
let scaled = dets.scale(2.0, 3.0);
assert_eq!(
scaled.detections[0].obb,
Some([[0.0, 0.0], [4.0, 0.0], [4.0, 6.0], [0.0, 6.0]])
);
}
#[test]
fn nms_keeps_only_the_remaining_scan_reachable_by_position() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.5, 0),
det([0.5, 0.5, 10.5, 10.5], 0.95, 0),
det([1.0, 1.0, 11.0, 11.0], 0.7, 0),
]);
let kept = dets.non_max_suppression(0.3, false);
assert_eq!(kept.len(), 1);
assert_eq!(kept.detections[0].confidence, 0.95);
}
#[cfg(feature = "parallel")]
#[test]
fn non_max_suppression_parallel_matches_the_sequential_result() {
let dets = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9, 0),
det([1.0, 1.0, 11.0, 11.0], 0.6, 0),
det([100.0, 100.0, 110.0, 110.0], 0.8, 0),
det([1.0, 1.0, 11.0, 11.0], 0.4, 1),
]);
let sequential = dets.non_max_suppression(0.5, false);
let parallel = dets.non_max_suppression_parallel(0.5, false);
assert_eq!(sequential.len(), parallel.len());
let mut seq_conf: Vec<f32> = sequential.iter().map(|d| d.confidence).collect();
let mut par_conf: Vec<f32> = parallel.iter().map(|d| d.confidence).collect();
seq_conf.sort_by(f32::total_cmp);
par_conf.sort_by(f32::total_cmp);
assert_eq!(seq_conf, par_conf);
}
}