Skip to main content

nv_perception/
detection.rs

1//! Detection types — individual detections and per-frame detection sets.
2
3use nv_core::{BBox, DetectionId, TypedMetadata};
4
5/// A single object detection within one frame.
6///
7/// Detections are produced by detection stages and stored in [`DetectionSet`].
8/// They carry spatial, classification, and optional re-identification data.
9///
10/// Use [`Detection::builder()`] for ergonomic construction with optional fields.
11#[derive(Clone, Debug)]
12pub struct Detection {
13    /// Unique ID within this frame's detection set.
14    pub id: DetectionId,
15    /// Numeric class identifier (model-defined).
16    pub class_id: u32,
17    /// Detection confidence score, typically in `[0, 1]`.
18    pub confidence: f32,
19    /// Axis-aligned bounding box in normalized `[0, 1]` coordinates.
20    pub bbox: BBox,
21    /// Optional re-identification or feature embedding vector.
22    pub embedding: Option<Vec<f32>>,
23    /// Extensible metadata (domain-specific fields, additional scores, etc.).
24    pub metadata: TypedMetadata,
25}
26
27impl Detection {
28    /// Create a builder for a [`Detection`].
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use nv_core::{BBox, DetectionId};
34    /// use nv_perception::Detection;
35    ///
36    /// let det = Detection::builder(
37    ///     DetectionId::new(1),
38    ///     0,
39    ///     0.95,
40    ///     BBox::new(0.1, 0.2, 0.3, 0.4),
41    /// )
42    /// .embedding(vec![0.1, 0.2, 0.3])
43    /// .build();
44    /// ```
45    #[must_use]
46    pub fn builder(
47        id: DetectionId,
48        class_id: u32,
49        confidence: f32,
50        bbox: BBox,
51    ) -> DetectionBuilder {
52        DetectionBuilder {
53            id,
54            class_id,
55            confidence,
56            bbox,
57            embedding: None,
58            metadata: TypedMetadata::new(),
59        }
60    }
61}
62
63/// Builder for [`Detection`].
64///
65/// All required fields are set in [`Detection::builder()`]. Optional fields
66/// can be chained before calling [`build()`](DetectionBuilder::build).
67pub struct DetectionBuilder {
68    id: DetectionId,
69    class_id: u32,
70    confidence: f32,
71    bbox: BBox,
72    embedding: Option<Vec<f32>>,
73    metadata: TypedMetadata,
74}
75
76impl DetectionBuilder {
77    /// Set the re-identification / feature embedding vector.
78    #[must_use]
79    pub fn embedding(mut self, embedding: Vec<f32>) -> Self {
80        self.embedding = Some(embedding);
81        self
82    }
83
84    /// Insert a typed metadata value.
85    #[must_use]
86    pub fn meta<T: Clone + Send + Sync + 'static>(mut self, val: T) -> Self {
87        self.metadata.insert(val);
88        self
89    }
90
91    /// Build the detection. Confidence is clamped to `[0.0, 1.0]`;
92    /// NaN is treated as `0.0`.
93    #[must_use]
94    pub fn build(self) -> Detection {
95        Detection {
96            id: self.id,
97            class_id: self.class_id,
98            confidence: clamp_unit(self.confidence),
99            bbox: self.bbox,
100            embedding: self.embedding,
101            metadata: self.metadata,
102        }
103    }
104}
105
106/// Clamp a float to `[0.0, 1.0]`, treating NaN as `0.0`.
107fn clamp_unit(v: f32) -> f32 {
108    if v.is_finite() {
109        v.clamp(0.0, 1.0)
110    } else {
111        0.0
112    }
113}
114
115/// All detections for one frame.
116///
117/// This is the authoritative set for a frame at any point in the pipeline.
118/// When a stage returns `Some(DetectionSet)` in its [`StageOutput`](super::StageOutput),
119/// it **replaces** the accumulator's detection set entirely.
120#[derive(Clone, Debug, Default)]
121pub struct DetectionSet {
122    /// The detections in this set.
123    pub detections: Vec<Detection>,
124}
125
126impl DetectionSet {
127    /// Create an empty detection set.
128    #[must_use]
129    pub fn empty() -> Self {
130        Self {
131            detections: Vec::new(),
132        }
133    }
134
135    /// Number of detections.
136    #[must_use]
137    pub fn len(&self) -> usize {
138        self.detections.len()
139    }
140
141    /// Whether the set is empty.
142    #[must_use]
143    pub fn is_empty(&self) -> bool {
144        self.detections.is_empty()
145    }
146}
147
148impl From<Vec<Detection>> for DetectionSet {
149    fn from(detections: Vec<Detection>) -> Self {
150        Self { detections }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use nv_core::BBox;
158
159    #[test]
160    fn builder_required_fields_only() {
161        let det =
162            Detection::builder(DetectionId::new(1), 0, 0.95, BBox::new(0.1, 0.2, 0.3, 0.4)).build();
163
164        assert_eq!(det.id, DetectionId::new(1));
165        assert_eq!(det.class_id, 0);
166        assert!((det.confidence - 0.95).abs() < f32::EPSILON);
167        assert!(det.embedding.is_none());
168        assert!(det.metadata.is_empty());
169    }
170
171    #[test]
172    fn builder_with_embedding_and_meta() {
173        #[derive(Clone, Debug, PartialEq)]
174        struct Extra(u32);
175
176        let det = Detection::builder(DetectionId::new(2), 5, 0.8, BBox::new(0.0, 0.0, 1.0, 1.0))
177            .embedding(vec![0.1, 0.2, 0.3])
178            .meta(Extra(42))
179            .build();
180
181        assert_eq!(det.embedding.as_ref().unwrap().len(), 3);
182        assert_eq!(det.metadata.get::<Extra>(), Some(&Extra(42)));
183    }
184
185    #[test]
186    fn detection_set_from_vec() {
187        let dets = vec![
188            Detection::builder(DetectionId::new(1), 0, 0.9, BBox::new(0.0, 0.0, 0.5, 0.5)).build(),
189            Detection::builder(DetectionId::new(2), 1, 0.7, BBox::new(0.5, 0.5, 1.0, 1.0)).build(),
190        ];
191        let set: DetectionSet = dets.into();
192        assert_eq!(set.len(), 2);
193        assert!(!set.is_empty());
194    }
195}