Skip to main content

nv_perception/
track.rs

1//! Track types — tracked objects across frames.
2
3use nv_core::{BBox, DetectionId, MonotonicTs, TrackId, TypedMetadata};
4
5/// Lifecycle state of a tracked object.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TrackState {
8    /// Track has been initialized but not yet confirmed by repeated observations.
9    Tentative,
10    /// Track has been confirmed by multiple consistent observations.
11    Confirmed,
12    /// No observation this frame — position is predicted (coasted).
13    Coasted,
14    /// Coasted too long — pending deletion by the temporal store.
15    Lost,
16}
17
18/// One observation of a track in a single frame.
19///
20/// Records the spatial and temporal state at the moment of observation.
21///
22/// # Per-observation metadata
23///
24/// The [`metadata`](Self::metadata) field allows stages to attach
25/// arbitrary per-observation data: embeddings, model-specific scores,
26/// attention weights, or feature vectors. This is especially useful
27/// for **joint detection+tracking models** that produce tracks directly
28/// (without an intermediate `DetectionSet`) and need somewhere to store
29/// per-observation features.
30///
31/// When metadata is unused (the common case for classical trackers),
32/// the field is zero-cost — `TypedMetadata::new()` does not allocate.
33/// If storing large data (e.g., a full feature map), wrap it in
34/// `Arc<T>` to keep clone costs low.
35#[derive(Clone, Debug)]
36pub struct TrackObservation {
37    /// Timestamp of this observation.
38    pub ts: MonotonicTs,
39    /// Bounding box in normalized coordinates.
40    pub bbox: BBox,
41    /// Confidence score for this observation.
42    pub confidence: f32,
43    /// Track state at time of observation.
44    pub state: TrackState,
45    /// The detection that was associated with this track, if any.
46    /// `None` when the track is coasting (no matching detection), or
47    /// when the track was produced by a joint model that does not
48    /// generate intermediate detections.
49    pub detection_id: Option<DetectionId>,
50    /// Extensible per-observation metadata.
51    ///
52    /// Stages can store embeddings, features, model-specific scores,
53    /// or any `Clone + Send + Sync + 'static` data here. The field is
54    /// zero-cost when empty (no heap allocation until first insert).
55    pub metadata: TypedMetadata,
56}
57
58impl TrackObservation {
59    /// Create a new observation with optional detection association.
60    ///
61    /// Metadata starts empty. Attach per-observation data (embeddings,
62    /// features, etc.) by setting the public `metadata` field after
63    /// construction.
64    #[must_use]
65    pub fn new(
66        ts: MonotonicTs,
67        bbox: BBox,
68        confidence: f32,
69        state: TrackState,
70        detection_id: Option<DetectionId>,
71    ) -> Self {
72        Self {
73            ts,
74            bbox,
75            confidence,
76            state,
77            detection_id,
78            metadata: TypedMetadata::new(),
79        }
80    }
81}
82
83/// A live tracked object.
84///
85/// Produced by tracker stages. The `current` field holds the latest observation.
86/// Historical observations are managed by the temporal store.
87#[derive(Clone, Debug)]
88pub struct Track {
89    /// Unique track identifier within this feed session.
90    pub id: TrackId,
91    /// Numeric class identifier (from the associated detections).
92    pub class_id: u32,
93    /// Current lifecycle state.
94    pub state: TrackState,
95    /// Most recent observation.
96    pub current: TrackObservation,
97    /// Extensible metadata (re-id features, custom scores, etc.).
98    pub metadata: TypedMetadata,
99}
100
101impl Track {
102    /// Create a new track with the given identity and current observation.
103    ///
104    /// Metadata starts empty — use the builder or set `metadata` directly
105    /// to attach custom data.
106    #[must_use]
107    pub fn new(id: TrackId, class_id: u32, state: TrackState, current: TrackObservation) -> Self {
108        Self {
109            id,
110            class_id,
111            state,
112            current,
113            metadata: TypedMetadata::new(),
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use nv_core::BBox;
122
123    #[test]
124    fn track_observation_new() {
125        let obs = TrackObservation::new(
126            MonotonicTs::from_nanos(1_000_000),
127            BBox::new(0.1, 0.2, 0.3, 0.4),
128            0.95,
129            TrackState::Confirmed,
130            Some(DetectionId::new(7)),
131        );
132        assert_eq!(obs.ts, MonotonicTs::from_nanos(1_000_000));
133        assert!((obs.confidence - 0.95).abs() < f32::EPSILON);
134        assert_eq!(obs.state, TrackState::Confirmed);
135        assert_eq!(obs.detection_id, Some(DetectionId::new(7)));
136        assert!(obs.metadata.is_empty());
137    }
138
139    #[test]
140    fn track_observation_with_metadata() {
141        #[derive(Clone, Debug, PartialEq)]
142        struct Embedding(Vec<f32>);
143
144        let mut obs = TrackObservation::new(
145            MonotonicTs::from_nanos(1_000_000),
146            BBox::new(0.1, 0.2, 0.3, 0.4),
147            0.95,
148            TrackState::Confirmed,
149            None,
150        );
151        obs.metadata.insert(Embedding(vec![0.1, 0.2, 0.3]));
152        assert_eq!(
153            obs.metadata.get::<Embedding>(),
154            Some(&Embedding(vec![0.1, 0.2, 0.3]))
155        );
156    }
157
158    #[test]
159    fn track_new_has_empty_metadata() {
160        let obs = TrackObservation::new(
161            MonotonicTs::from_nanos(0),
162            BBox::new(0.0, 0.0, 0.5, 0.5),
163            0.9,
164            TrackState::Tentative,
165            None,
166        );
167        let track = Track::new(TrackId::new(1), 0, TrackState::Tentative, obs);
168        assert_eq!(track.id, TrackId::new(1));
169        assert_eq!(track.class_id, 0);
170        assert!(track.metadata.is_empty());
171    }
172}