data_contracts/
capture.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct PolypLabel {
6    pub center_world: [f32; 3],
7    pub bbox_px: Option<[f32; 4]>,
8    pub bbox_norm: Option<[f32; 4]>,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CaptureMetadata {
13    pub frame_id: u64,
14    pub sim_time: f64,
15    pub unix_time: f64,
16    pub image: String,
17    pub image_present: bool,
18    pub camera_active: bool,
19    pub polyp_seed: u64,
20    pub polyp_labels: Vec<PolypLabel>,
21}
22
23#[derive(Debug, Error)]
24pub enum ValidationError {
25    #[error("bbox_px invalid order or negative: {0:?}")]
26    InvalidBboxPx([f32; 4]),
27    #[error("bbox_norm out of range: {0:?}")]
28    InvalidBboxNorm([f32; 4]),
29    #[error("missing image path for present frame")]
30    MissingImage,
31}
32
33impl PolypLabel {
34    pub fn validate(&self) -> Result<(), ValidationError> {
35        if let Some(px) = self.bbox_px {
36            if px[0].is_nan()
37                || px[1].is_nan()
38                || px[2].is_nan()
39                || px[3].is_nan()
40                || px[0] > px[2]
41                || px[1] > px[3]
42            {
43                return Err(ValidationError::InvalidBboxPx(px));
44            }
45        }
46        if let Some(norm) = self.bbox_norm {
47            let in_range = norm.iter().all(|v| !v.is_nan() && *v >= 0.0 && *v <= 1.0);
48            if !in_range || norm[0] > norm[2] || norm[1] > norm[3] {
49                return Err(ValidationError::InvalidBboxNorm(norm));
50            }
51        }
52        Ok(())
53    }
54}
55
56impl CaptureMetadata {
57    pub fn validate(&self) -> Result<(), ValidationError> {
58        if self.image_present && self.image.trim().is_empty() {
59            return Err(ValidationError::MissingImage);
60        }
61        for label in &self.polyp_labels {
62            label.validate()?;
63        }
64        Ok(())
65    }
66}