1use ndarray::ArrayView2;
9use num_traits::Float;
10
11use crate::error::{FitsCubeError, Result};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct BoundingBox {
19 pub xmin: usize,
21 pub xmax: usize,
23 pub ymin: usize,
25 pub ymax: usize,
27 pub original_shape: (usize, usize),
29 pub y_span: usize,
31 pub x_span: usize,
33}
34
35impl BoundingBox {
36 fn new(
37 xmin: usize,
38 xmax: usize,
39 ymin: usize,
40 ymax: usize,
41 original_shape: (usize, usize),
42 ) -> Self {
43 Self {
44 xmin,
45 xmax,
46 ymin,
47 ymax,
48 original_shape,
49 y_span: ymax - ymin,
50 x_span: xmax - xmin,
51 }
52 }
53}
54
55pub fn create_bound_box_plane<T: Float>(image: &ArrayView2<T>) -> Option<BoundingBox> {
61 let (nrows, ncols) = image.dim();
62
63 let mut xmin: Option<usize> = None;
64 let mut xmax = 0usize;
65 let mut ymin: Option<usize> = None;
66 let mut ymax = 0usize;
67 let mut any_valid = false;
68
69 for r in 0..nrows {
70 for c in 0..ncols {
71 if image[(r, c)].is_finite() {
72 any_valid = true;
73 if xmin.is_none() {
74 xmin = Some(r);
75 }
76 xmax = r;
77 if ymin.is_none_or(|y| c < y) {
78 ymin = Some(c);
79 }
80 if c > ymax {
81 ymax = c;
82 }
83 }
84 }
85 }
86
87 if !any_valid {
88 tracing::info!("No pixels to create bounding box for");
89 return None;
90 }
91
92 let xmin = xmin.expect("any_valid implies xmin set");
96 let ymin = ymin.expect("any_valid implies ymin set");
97 Some(BoundingBox::new(
98 xmin,
99 xmax + 1,
100 ymin,
101 ymax + 1,
102 (nrows, ncols),
103 ))
104}
105
106pub fn extract_common_bounding_box(boxes: &[Option<BoundingBox>]) -> Result<BoundingBox> {
112 let valid: Vec<&BoundingBox> = boxes.iter().filter_map(|b| b.as_ref()).collect();
113
114 if valid.is_empty() {
115 return Err(FitsCubeError::Other(
116 "No valid input boxes to consider".to_string(),
117 ));
118 }
119
120 let shape0 = valid[0].original_shape;
121 if !valid.iter().all(|b| b.original_shape == shape0) {
122 return Err(FitsCubeError::Other(
123 "Different shapes, and not sure this is really supported or meaningful".to_string(),
124 ));
125 }
126
127 let xmin = valid.iter().map(|b| b.xmin).min().unwrap();
128 let xmax = valid.iter().map(|b| b.xmax).max().unwrap();
129 let ymin = valid.iter().map(|b| b.ymin).min().unwrap();
130 let ymax = valid.iter().map(|b| b.ymax).max().unwrap();
131
132 Ok(BoundingBox::new(xmin, xmax, ymin, ymax, shape0))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use ndarray::Array2;
139
140 fn nan() -> f64 {
141 f64::NAN
142 }
143
144 #[test]
145 fn full_image_is_bounded_by_itself() {
146 let img = Array2::<f64>::ones((4, 5));
147 let bb = create_bound_box_plane(&img.view()).unwrap();
148 assert_eq!(bb.xmin, 0);
149 assert_eq!(bb.xmax, 4);
150 assert_eq!(bb.ymin, 0);
151 assert_eq!(bb.ymax, 5);
152 assert_eq!(bb.x_span, 4);
153 assert_eq!(bb.y_span, 5);
154 assert_eq!(bb.original_shape, (4, 5));
155 }
156
157 #[test]
158 fn nan_border_is_trimmed() {
159 let mut img = Array2::<f64>::from_elem((5, 5), nan());
160 for r in 1..=3 {
162 for c in 2..=3 {
163 img[(r, c)] = 1.0;
164 }
165 }
166 let bb = create_bound_box_plane(&img.view()).unwrap();
167 assert_eq!((bb.xmin, bb.xmax), (1, 4));
168 assert_eq!((bb.ymin, bb.ymax), (2, 4));
169 assert_eq!(bb.x_span, 3);
170 assert_eq!(bb.y_span, 2);
171 }
172
173 #[test]
174 fn all_nan_returns_none() {
175 let img = Array2::<f64>::from_elem((3, 3), nan());
176 assert!(create_bound_box_plane(&img.view()).is_none());
177 }
178
179 #[test]
180 fn common_box_is_union() {
181 let a = create_bound_box_plane(
182 &{
183 let mut m = Array2::<f64>::from_elem((6, 6), nan());
184 m[(1, 1)] = 1.0;
185 m
186 }
187 .view(),
188 )
189 .unwrap();
190 let b = create_bound_box_plane(
191 &{
192 let mut m = Array2::<f64>::from_elem((6, 6), nan());
193 m[(4, 4)] = 1.0;
194 m
195 }
196 .view(),
197 )
198 .unwrap();
199 let common = extract_common_bounding_box(&[Some(a), None, Some(b)]).unwrap();
200 assert_eq!((common.xmin, common.xmax), (1, 5));
201 assert_eq!((common.ymin, common.ymax), (1, 5));
202 }
203
204 #[test]
205 fn no_valid_boxes_errors() {
206 assert!(extract_common_bounding_box(&[None, None]).is_err());
207 }
208
209 #[test]
210 fn shape_mismatch_errors() {
211 let a = BoundingBox::new(0, 1, 0, 1, (4, 4));
212 let b = BoundingBox::new(0, 1, 0, 1, (5, 5));
213 assert!(extract_common_bounding_box(&[Some(a), Some(b)]).is_err());
214 }
215}