pineapple_core/im/
boxes.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use std::fs::File;
5use std::io::{BufWriter, Read};
6use std::path::Path;
7
8use serde::Serialize;
9use serde_json::Value;
10
11use crate::constant::BOUNDING_BOX_JSON_VALID_KEYS;
12use crate::error::PineappleError;
13
14/// A bounding box container for storing locations of detected objects
15///
16/// The bounding boxes are stored in xyxy format. Any input set of
17/// bounding boxes that has a box with a non-positive area will
18/// return an error.
19///
20/// # Examples
21///
22/// ```
23/// use pineapple_core::im::BoundingBoxes;
24///
25/// let data: Vec<[f32; 4]> = vec![[0., 0., 1., 1.], [3., 4., 5., 7.]];
26/// let boxes = BoundingBoxes::new(data);
27/// assert!(boxes.is_ok());
28///
29/// let data: Vec<[f32; 4]> = vec![[2., 2., 1., 1.], [3., 4., 5., 7.]];
30/// let boxes = BoundingBoxes::new(data);
31/// assert!(boxes.is_err());
32/// ```
33#[derive(Debug, Clone)]
34pub struct BoundingBoxes {
35    data: Vec<[f32; 4]>,
36}
37
38impl BoundingBoxes {
39    /// Initialize a new bounding boxes container
40    ///
41    /// # Arguments
42    ///
43    /// * `data` - Bounding boxes in xyxy format
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use pineapple_core::im::BoundingBoxes;
49    ///
50    /// let data: Vec<[f32; 4]> = vec![[0., 0., 1., 1.], [3., 4., 5., 7.]];
51    /// let boxes = BoundingBoxes::new(data);
52    /// ```
53    pub fn new(data: Vec<[f32; 4]>) -> Result<Self, PineappleError> {
54        let n = data.len();
55
56        let data: Vec<[f32; 4]> = data
57            .into_iter()
58            .filter(|[min_x, min_y, max_x, max_y]| max_x >= min_x && max_y >= min_y)
59            .collect();
60
61        if data.len() != n {
62            return Err(PineappleError::BoxesSizeError);
63        }
64
65        Ok(Self { data })
66    }
67}
68
69// >>> I/O METHODS
70
71impl BoundingBoxes {
72    /// Open bounding boxes from the provided path
73    ///
74    /// # Arguments
75    ///
76    /// * `path` - A path to bounding boxes with a valid extension
77    ///
78    /// # Examples
79    ///
80    /// ```no_run
81    /// use pineapple_core::im::BoundingBoxes;
82    /// let bounding_boxes = BoundingBoxes::open("boxes.json");
83    /// ```
84    pub fn open<P: AsRef<Path>>(path: P) -> Result<BoundingBoxes, PineappleError> {
85        let extension = path
86            .as_ref()
87            .extension()
88            .and_then(|s| s.to_str())
89            .map(|s| s.to_lowercase());
90
91        if let Some(ext) = extension && ext == "json" {
92            return read_boxes_json(path);
93        }
94
95        Err(PineappleError::BoxesReadError)
96    }
97
98    /// Save bounding boxes at the provided path
99    ///
100    /// # Arguments
101    ///
102    /// * `path` - Path to save bounding boxes
103    ///
104    /// # Examples
105    ///
106    /// ```no_run
107    /// use pineapple_core::im::BoundingBoxes;
108    /// let boxes = BoundingBoxes::open("boxes.json").unwrap();
109    /// boxes.save("boxes.json").unwrap();
110    /// ```
111    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), PineappleError> {
112        let extension = path
113            .as_ref()
114            .extension()
115            .and_then(|s| s.to_str())
116            .map(|s| s.to_lowercase());
117
118        if let Some(ext) = extension && ext == "json" {
119            return write_boxes_json(path, &self.data);
120        }
121
122        Err(PineappleError::BoxesWriteError)
123    }
124}
125
126// <<< I/O METHODS
127
128// >>> PROPERTY METHODS
129
130impl BoundingBoxes {
131    /// Number of bounding boxes
132    pub fn len(&self) -> usize {
133        self.data.len()
134    }
135
136    /// Check if bounding boxes are empty
137    pub fn is_empty(&self) -> bool {
138        self.data.len() == 0
139    }
140}
141
142// <<< PROPERTY METHODS
143
144// >>> CONVERSION METHODS
145
146impl BoundingBoxes {
147    /// Return a reference to underlying bounding boxes data
148    pub fn as_xyxy(&self) -> &Vec<[f32; 4]> {
149        &self.data
150    }
151
152    /// Return the underlying bounding boxes data
153    pub fn to_xyxy(self) -> Vec<[f32; 4]> {
154        self.data
155    }
156
157    /// Return the bounding box data in xywh format
158    pub fn to_xywh(self) -> Vec<[f32; 4]> {
159        self.data
160            .into_iter()
161            .map(|[min_x, min_y, max_x, max_y]| [min_x, min_y, max_x - min_x, max_y - min_y])
162            .collect()
163    }
164}
165
166// <<< CONVERSION METHODS
167
168// >>> TRANSFORM METHODS
169
170impl BoundingBoxes {
171    /// Remove bounding boxes based on an array of pre-sorted (ascending) indices
172    pub fn remove(&mut self, indices: &[usize]) {
173        if indices.is_empty() {
174            return;
175        }
176
177        let mut data: Vec<[f32; 4]> = Vec::with_capacity(self.len() - indices.len());
178        let mut indices_iter = indices.iter().peekable();
179        let mut next_remove = indices_iter.next().copied();
180
181        for (idx, bounding_box) in self.data.iter().enumerate() {
182            if Some(idx) == next_remove {
183                next_remove = indices_iter.next().copied();
184            } else {
185                data.push(*bounding_box)
186            }
187        }
188
189        self.data = data;
190    }
191}
192
193// <<< TRANSFORM METHODS
194
195/// Read bounding boxes stored as json format
196pub fn read_boxes_json<P: AsRef<Path>>(path: P) -> Result<BoundingBoxes, PineappleError> {
197    let mut contents = String::new();
198
199    File::open(path)
200        .map_err(|err| PineappleError::NoFileError(err.to_string()))?
201        .read_to_string(&mut contents)
202        .map_err(|err| PineappleError::NoFileError(err.to_string()))?;
203
204    let data: Value = serde_json::from_str(&contents).map_err(|_| PineappleError::BoxesReadError)?;
205
206    fn to_f32(value: &Value) -> Result<f32, PineappleError> {
207        if let Some(n) = value.as_f64() {
208            Ok(n as f32)
209        } else if let Some(n) = value.as_u64() {
210            Ok(n as f32)
211        } else if let Some(n) = value.as_i64() {
212            Ok(n as f32)
213        } else {
214            Err(PineappleError::BoxesReadError)
215        }
216    }
217
218    for key in &BOUNDING_BOX_JSON_VALID_KEYS {
219        if let Some(boxes) = data.get(key).and_then(|v| v.as_array()) {
220            let boxes: Result<Vec<[f32; 4]>, _> = boxes
221                .iter()
222                .map(|item| {
223                    item.as_array()
224                        .ok_or(PineappleError::BoxesReadError)
225                        .and_then(|b| {
226                            if b.len() == 4 {
227                                let min_x = to_f32(&b[0])?;
228                                let min_y = to_f32(&b[1])?;
229                                let max_x = to_f32(&b[2])?;
230                                let max_y = to_f32(&b[3])?;
231                                Ok([min_x, min_y, max_x, max_y])
232                            } else {
233                                Err(PineappleError::BoxesReadError)
234                            }
235                        })
236                })
237                .collect();
238
239            if let Ok(boxes) = boxes {
240                return BoundingBoxes::new(boxes);
241            }
242        };
243    }
244
245    Err(PineappleError::BoxesReadError)
246}
247
248/// Write bounding boxes to a json file
249pub fn write_boxes_json<P, T>(path: P, boxes: &Vec<[T; 4]>) -> Result<(), PineappleError>
250where
251    P: AsRef<Path>,
252    T: Serialize,
253{
254    let file = File::create(path).map_err(|_| PineappleError::BoxesWriteError)?;
255    let writer = BufWriter::new(file);
256
257    serde_json::to_writer(writer, &serde_json::json!({ "bounding_boxes": boxes }))
258        .map_err(|_| PineappleError::BoxesWriteError)?;
259
260    Ok(())
261}
262
263#[cfg(test)]
264mod test {
265
266    use super::*;
267
268    const TEST_DATA_JSON: &str = "../data/tests/test_boxes.json";
269
270    #[test]
271    pub fn test_open_json_success() {
272        let bounding_boxes = BoundingBoxes::open(TEST_DATA_JSON);
273        assert!(bounding_boxes.is_ok());
274    }
275
276    #[test]
277    pub fn test_open_json_failure() {
278        let bounding_boxes = BoundingBoxes::open("does_not_exist/");
279        assert!(bounding_boxes.is_err())
280    }
281
282    #[test]
283    pub fn test_open_json_format() {
284        let bounding_boxes = BoundingBoxes::open(TEST_DATA_JSON).unwrap();
285
286        for (i, bounding_box) in bounding_boxes.as_xyxy().iter().enumerate() {
287            assert_eq!(*bounding_box, [0_f32, 0_f32, i as f32, i as f32]);
288        }
289    }
290
291    #[test]
292    pub fn test_write_json() {
293        const OUTPUT: &str = "TEST_BOX_WRITE.json";
294
295        let bounding_boxes = BoundingBoxes::open(TEST_DATA_JSON).unwrap();
296
297        bounding_boxes.save(OUTPUT).unwrap();
298
299        let reloaded_boxes = BoundingBoxes::open(OUTPUT).unwrap();
300
301        assert_eq!(bounding_boxes.as_xyxy(), reloaded_boxes.as_xyxy());
302
303        std::fs::remove_file(OUTPUT).unwrap();
304    }
305}