Skip to main content

sightloom_core/
detection.rs

1//! Compact detections and caller-owned detection batches.
2
3use crate::{ClassId, CoreError, Rect, TrackId};
4
5/// A validated object detection with optional typed metadata.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct Detection {
8    bbox: Rect,
9    score: f32,
10    class_id: Option<ClassId>,
11    track_id: Option<TrackId>,
12}
13
14impl Detection {
15    /// Creates a detection when its score is finite.
16    ///
17    /// Finite scores are preserved without clamping.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`CoreError::NonFinite`] when `score` is NaN or infinite.
22    pub fn new(
23        bbox: Rect,
24        score: f32,
25        class_id: Option<ClassId>,
26        track_id: Option<TrackId>,
27    ) -> Result<Self, CoreError> {
28        if !score.is_finite() {
29            return Err(CoreError::NonFinite);
30        }
31
32        Ok(Self {
33            bbox,
34            score,
35            class_id,
36            track_id,
37        })
38    }
39
40    /// Returns the detection bounding box.
41    #[must_use]
42    pub const fn bbox(self) -> Rect {
43        self.bbox
44    }
45
46    /// Returns the model confidence score.
47    #[must_use]
48    pub const fn score(self) -> f32 {
49        self.score
50    }
51
52    /// Returns the optional class identifier.
53    #[must_use]
54    pub const fn class_id(self) -> Option<ClassId> {
55        self.class_id
56    }
57
58    /// Returns the optional external track identifier.
59    #[must_use]
60    pub const fn track_id(self) -> Option<TrackId> {
61        self.track_id
62    }
63}
64
65/// A detection batch backed by mutable storage owned by the caller.
66#[derive(Debug)]
67pub struct DetectionBatch<'a> {
68    storage: &'a mut [Detection],
69    len: usize,
70}
71
72impl<'a> DetectionBatch<'a> {
73    /// Creates an empty batch over caller-owned storage.
74    #[must_use]
75    pub fn new(storage: &'a mut [Detection]) -> Self {
76        Self { storage, len: 0 }
77    }
78
79    /// Creates a batch whose complete caller-owned slice contains valid data.
80    #[must_use]
81    pub fn from_filled(storage: &'a mut [Detection]) -> Self {
82        let len = storage.len();
83        Self { storage, len }
84    }
85
86    /// Appends a detection without reallocating or truncating existing data.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`CoreError::InsufficientCapacity`] without modifying the batch
91    /// when the caller-owned storage is full.
92    pub fn push(&mut self, detection: Detection) -> Result<(), CoreError> {
93        let slot = self
94            .storage
95            .get_mut(self.len)
96            .ok_or(CoreError::InsufficientCapacity)?;
97        *slot = detection;
98        self.len += 1;
99        Ok(())
100    }
101
102    /// Returns the valid prefix of the caller-owned storage.
103    #[must_use]
104    pub fn as_slice(&self) -> &[Detection] {
105        &self.storage[..self.len]
106    }
107}