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}