data_contracts/
capture.rs1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "snake_case")]
6pub enum LabelSource {
7 SimAuto,
8 Human,
9 Model,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DetectionLabel {
14 pub center_world: [f32; 3],
15 pub bbox_px: Option<[f32; 4]>,
16 pub bbox_norm: Option<[f32; 4]>,
17 pub source: Option<LabelSource>,
18 pub source_confidence: Option<f32>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CaptureMetadata {
23 pub frame_id: u64,
24 pub sim_time: f64,
25 pub unix_time: f64,
26 pub image: String,
27 pub image_present: bool,
28 pub camera_active: bool,
29 pub label_seed: u64,
30 pub labels: Vec<DetectionLabel>,
31}
32
33#[derive(Debug, Error)]
34pub enum ValidationError {
35 #[error("bbox_px invalid order or negative: {0:?}")]
36 InvalidBboxPx([f32; 4]),
37 #[error("bbox_norm out of range: {0:?}")]
38 InvalidBboxNorm([f32; 4]),
39 #[error("source_confidence out of range: {0:?}")]
40 InvalidSourceConfidence(f32),
41 #[error("missing image path for present frame")]
42 MissingImage,
43}
44
45impl DetectionLabel {
46 pub fn validate(&self) -> Result<(), ValidationError> {
47 if let Some(px) = self.bbox_px {
48 if px[0].is_nan()
49 || px[1].is_nan()
50 || px[2].is_nan()
51 || px[3].is_nan()
52 || px[0] > px[2]
53 || px[1] > px[3]
54 {
55 return Err(ValidationError::InvalidBboxPx(px));
56 }
57 }
58 if let Some(norm) = self.bbox_norm {
59 let in_range = norm.iter().all(|v| !v.is_nan() && *v >= 0.0 && *v <= 1.0);
60 if !in_range || norm[0] > norm[2] || norm[1] > norm[3] {
61 return Err(ValidationError::InvalidBboxNorm(norm));
62 }
63 }
64 if let Some(conf) = self.source_confidence {
65 if conf.is_nan() || !(0.0..=1.0).contains(&conf) {
66 return Err(ValidationError::InvalidSourceConfidence(conf));
67 }
68 }
69 Ok(())
70 }
71}
72
73impl CaptureMetadata {
74 pub fn validate(&self) -> Result<(), ValidationError> {
75 if self.image_present && self.image.trim().is_empty() {
76 return Err(ValidationError::MissingImage);
77 }
78 for label in &self.labels {
79 label.validate()?;
80 }
81 Ok(())
82 }
83}