Skip to main content

nv_perception/
artifact.rs

1//! Accumulated perception artifacts and stage output merging.
2
3use crate::detection::DetectionSet;
4use crate::scene::SceneFeature;
5use crate::signal::DerivedSignal;
6use crate::track::Track;
7use nv_core::TypedMetadata;
8
9/// Accumulated outputs of all stages that have run so far for this frame.
10///
11/// Built up incrementally by the pipeline executor. Stage N+1 sees the
12/// accumulated result of stages 0..N through [`StageContext::artifacts`](super::StageContext).
13///
14/// ## Merge semantics
15///
16/// | Field | Behavior |
17/// |---|---|
18/// | `detections` | **Replace** — latest `Some(DetectionSet)` wins |
19/// | `tracks` | **Replace** — latest `Some(Vec<Track>)` wins |
20/// | `signals` | **Append** — all signals accumulate |
21/// | `scene_features` | **Append** — all scene features accumulate |
22/// | `stage_artifacts` | **Merge** — last-writer-wins per `TypeId` |
23///
24/// ## Extension seam: `stage_artifacts`
25///
26/// The [`stage_artifacts`](Self::stage_artifacts) field is the primary
27/// inter-stage communication channel for data that does not fit the
28/// built-in fields. Any `Clone + Send + Sync + 'static` value can be
29/// stored by type.
30///
31/// A pre-processing stage can assemble a sliding window of frames
32/// (e.g., `Arc<[FrameEnvelope]>`) and store it as a typed artifact for a
33/// downstream temporal or clip-based model to consume, without any changes
34/// to the core pipeline execution model.
35#[derive(Clone, Debug, Default)]
36pub struct PerceptionArtifacts {
37    /// Current detection set (replaced by each stage that returns detections).
38    pub detections: DetectionSet,
39    /// Current track set (replaced by each stage that returns tracks).
40    ///
41    /// When [`tracks_authoritative`](Self::tracks_authoritative) is `true`,
42    /// this is the complete set of active tracks for the frame. Tracks
43    /// previously known to the temporal store but absent here are considered
44    /// normally ended (`TrackEnded`).
45    pub tracks: Vec<Track>,
46    /// Whether any stage produced authoritative track output this frame.
47    ///
48    /// Set to `true` when at least one stage returns `Some(tracks)` in its
49    /// [`StageOutput`](super::StageOutput). When `false`, no stage claimed
50    /// ownership of the track set, and the executor must **not** infer
51    /// track endings from the (default-empty) `tracks` field.
52    pub tracks_authoritative: bool,
53    /// Accumulated signals from all stages.
54    pub signals: Vec<DerivedSignal>,
55    /// Scene-level features accumulated from all stages.
56    pub scene_features: Vec<SceneFeature>,
57    /// Typed artifacts from stages — keyed by `TypeId`, last-writer-wins.
58    ///
59    /// This is the extension seam for arbitrary inter-stage data: feature
60    /// maps, prepared input tensors, multi-frame windows, calibration
61    /// metadata, or any domain-specific payload. Downstream stages access
62    /// stored values via `StageContext::artifacts.stage_artifacts.get::<T>()`.
63    pub stage_artifacts: TypedMetadata,
64}
65
66impl PerceptionArtifacts {
67    /// Create empty perception artifacts.
68    #[must_use]
69    pub fn empty() -> Self {
70        Self::default()
71    }
72
73    /// Merge a [`StageOutput`](super::StageOutput) into the accumulator.
74    ///
75    /// Applies the merge semantics documented above.
76    pub fn merge(&mut self, output: super::StageOutput) {
77        if let Some(detections) = output.detections {
78            self.detections = detections;
79        }
80        if let Some(tracks) = output.tracks {
81            self.tracks = tracks;
82            self.tracks_authoritative = true;
83        }
84        self.signals.extend(output.signals);
85        self.scene_features.extend(output.scene_features);
86        self.stage_artifacts.merge(output.artifacts);
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::detection::Detection;
94    use crate::signal::{DerivedSignal, SignalValue};
95    use crate::stage::StageOutput;
96    use crate::track::Track;
97    use nv_core::id::{DetectionId, TrackId};
98    use nv_core::{BBox, MonotonicTs, TypedMetadata};
99
100    fn make_detection(id: u64) -> Detection {
101        Detection::builder(DetectionId::new(id), 0, 0.9, BBox::new(0.1, 0.2, 0.3, 0.4)).build()
102    }
103
104    fn make_track(id: u64) -> Track {
105        use crate::track::{TrackObservation, TrackState};
106        let obs = TrackObservation::new(
107            MonotonicTs::from_nanos(0),
108            BBox::new(0.1, 0.2, 0.3, 0.4),
109            0.9,
110            TrackState::Confirmed,
111            None,
112        );
113        Track::new(TrackId::new(id), 0, TrackState::Confirmed, obs)
114    }
115
116    fn make_signal(name: &'static str) -> DerivedSignal {
117        DerivedSignal {
118            name,
119            value: SignalValue::Scalar(1.0),
120            ts: MonotonicTs::from_nanos(0),
121        }
122    }
123
124    #[test]
125    fn merge_empty_output_is_noop() {
126        let mut arts = PerceptionArtifacts::empty();
127        arts.merge(StageOutput::empty());
128
129        assert!(arts.detections.is_empty());
130        assert!(arts.tracks.is_empty());
131        assert!(!arts.tracks_authoritative);
132        assert!(arts.signals.is_empty());
133        assert!(arts.scene_features.is_empty());
134    }
135
136    #[test]
137    fn merge_detections_replace() {
138        let mut arts = PerceptionArtifacts::empty();
139
140        // First stage sets detections.
141        let dets1 = DetectionSet::from(vec![make_detection(1), make_detection(2)]);
142        arts.merge(StageOutput::with_detections(dets1));
143        assert_eq!(arts.detections.len(), 2);
144
145        // Second stage replaces with a single detection.
146        let dets2 = DetectionSet::from(vec![make_detection(3)]);
147        arts.merge(StageOutput::with_detections(dets2));
148        assert_eq!(arts.detections.len(), 1);
149        assert_eq!(arts.detections.detections[0].id, DetectionId::new(3));
150    }
151
152    #[test]
153    fn merge_none_detections_preserves_existing() {
154        let mut arts = PerceptionArtifacts::empty();
155
156        let dets = DetectionSet::from(vec![make_detection(1)]);
157        arts.merge(StageOutput::with_detections(dets));
158        assert_eq!(arts.detections.len(), 1);
159
160        // Merge output with no detections — previous set preserved.
161        arts.merge(StageOutput::empty());
162        assert_eq!(arts.detections.len(), 1);
163    }
164
165    #[test]
166    fn merge_tracks_replace_and_set_authoritative() {
167        let mut arts = PerceptionArtifacts::empty();
168        assert!(!arts.tracks_authoritative);
169
170        let tracks = vec![make_track(1), make_track(2)];
171        arts.merge(StageOutput::with_tracks(tracks));
172        assert_eq!(arts.tracks.len(), 2);
173        assert!(arts.tracks_authoritative);
174
175        // Replace with fewer tracks.
176        let tracks2 = vec![make_track(3)];
177        arts.merge(StageOutput::with_tracks(tracks2));
178        assert_eq!(arts.tracks.len(), 1);
179        assert_eq!(arts.tracks[0].id, TrackId::new(3));
180    }
181
182    #[test]
183    fn merge_signals_append() {
184        let mut arts = PerceptionArtifacts::empty();
185
186        arts.merge(StageOutput::with_signal(make_signal("sig_a")));
187        assert_eq!(arts.signals.len(), 1);
188
189        arts.merge(StageOutput::with_signal(make_signal("sig_b")));
190        assert_eq!(arts.signals.len(), 2);
191        assert_eq!(arts.signals[0].name, "sig_a");
192        assert_eq!(arts.signals[1].name, "sig_b");
193    }
194
195    #[test]
196    fn merge_stage_artifacts_last_writer_wins() {
197        #[derive(Clone, Debug, PartialEq)]
198        struct MyData(u32);
199
200        let mut arts = PerceptionArtifacts::empty();
201
202        let mut meta1 = TypedMetadata::new();
203        meta1.insert(MyData(1));
204        arts.merge(StageOutput {
205            artifacts: meta1,
206            ..StageOutput::default()
207        });
208        assert_eq!(arts.stage_artifacts.get::<MyData>(), Some(&MyData(1)));
209
210        // Second merge overwrites.
211        let mut meta2 = TypedMetadata::new();
212        meta2.insert(MyData(42));
213        arts.merge(StageOutput {
214            artifacts: meta2,
215            ..StageOutput::default()
216        });
217        assert_eq!(arts.stage_artifacts.get::<MyData>(), Some(&MyData(42)));
218    }
219}