data_contracts/
capture.rs

1use 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}