image_debug_utils/rect.rs
1//! Utilities for geometric primitives and bounding boxes, often used with `imageproc::geometry`.
2
3use image::math::Rect;
4use imageproc::point::Point;
5use num_traits::{Num, ToPrimitive};
6
7/// Calculates the axis-aligned bounding box of a rotated rectangle's vertices.
8///
9/// This function is designed to work with the output of `imageproc::geometry::min_area_rect`,
10/// which is an array of four `Point<T>`. It iterates through the points to find the
11/// minimum and maximum x and y coordinates, then constructs an `image::math::Rect`
12/// that encloses all four points.
13///
14/// This version is generic over numeric types that implement `PartialOrd`, making it
15/// suitable for both integer and floating-point coordinates.
16///
17/// # Arguments
18///
19/// * `vertices` - An array of 4 `Point<T>` representing the corners of a rectangle.
20/// `T` must be a numeric type that supports partial ordering and arithmetic operations.
21///
22/// # Returns
23///
24/// An `image::math::Rect` representing the smallest possible axis-aligned rectangle
25/// that contains all the input vertices.
26///
27/// # Panics
28///
29/// This function assumes the input array is not empty, which is guaranteed by its
30/// type `&[Point<T>; 4]`.
31///
32/// # Examples
33///
34/// ```use image::math::Rect;
35/// use imageproc::point::Point;
36/// use image_debug_utils::rect::to_axis_aligned_bounding_box;
37///
38/// let rotated_rect_vertices = [
39/// Point { x: 50.0, y: 10.0 },
40/// Point { x: 90.0, y: 50.0 },
41/// Point { x: 50.0, y: 90.0 },
42/// Point { x: 10.0, y: 50.0 },
43/// ];
44///
45/// let bounding_box = to_axis_aligned_bounding_box(&rotated_rect_vertices);
46///
47/// assert_eq!(bounding_box.x, 10);
48/// assert_eq!(bounding_box.y, 10);
49/// assert_eq!(bounding_box.width, 80);
50/// assert_eq!(bounding_box.height, 80);
51/// ```
52pub fn to_axis_aligned_bounding_box<T>(vertices: &[Point<T>; 4]) -> Rect
53where
54 T: Copy + PartialOrd + Num + ToPrimitive,
55{
56 let p0 = vertices[0];
57 let mut min_x = p0.x;
58 let mut max_x = p0.x;
59 let mut min_y = p0.y;
60 let mut max_y = p0.y;
61
62 // Iterate over the remaining 3 points.
63 // Manual comparison is used here because `T` only has a `PartialOrd`.
64 // This is required to support floating-point types, which do not implement `Ord`.
65 for p in &vertices[1..] {
66 if p.x < min_x {
67 min_x = p.x;
68 }
69 if p.x > max_x {
70 max_x = p.x;
71 }
72 if p.y < min_y {
73 min_y = p.y;
74 }
75 if p.y > max_y {
76 max_y = p.y;
77 }
78 }
79
80 let x = min_x.to_u32().unwrap_or(0);
81 let y = min_y.to_u32().unwrap_or(0);
82
83 let width = max_x.to_u32().unwrap_or(0).saturating_sub(x);
84 let height = max_y.to_u32().unwrap_or(0).saturating_sub(y);
85
86 Rect {
87 x,
88 y,
89 width,
90 height,
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use imageproc::point::Point;
98
99 #[test]
100 fn test_bounding_box_for_rotated_rect() {
101 // A diamond shape, which is a rotated square.
102 // min_x=10, max_x=90, min_y=10, max_y=90
103 let vertices = [
104 Point { x: 50, y: 10 },
105 Point { x: 90, y: 50 },
106 Point { x: 50, y: 90 },
107 Point { x: 10, y: 50 },
108 ];
109 let expected = Rect {
110 x: 10,
111 y: 10,
112 width: 80,
113 height: 80,
114 };
115 assert_eq!(to_axis_aligned_bounding_box(&vertices), expected);
116 }
117
118 #[test]
119 fn test_bounding_box_for_axis_aligned_rect() {
120 // An already axis-aligned rectangle.
121 let vertices = [
122 Point { x: 20, y: 30 },
123 Point { x: 120, y: 30 },
124 Point { x: 120, y: 80 },
125 Point { x: 20, y: 80 },
126 ];
127 let expected = Rect {
128 x: 20,
129 y: 30,
130 width: 100,
131 height: 50,
132 };
133 // The order of points doesn't matter. Let's shuffle them.
134 let shuffled_vertices = [vertices[2], vertices[0], vertices[3], vertices[1]];
135 assert_eq!(to_axis_aligned_bounding_box(&vertices), expected);
136 assert_eq!(to_axis_aligned_bounding_box(&shuffled_vertices), expected);
137 }
138
139 #[test]
140 fn test_bounding_box_with_negative_coordinates() {
141 // This test now compiles and passes because the function signature
142 // uses `PartialOrd`, which is implemented for f64.
143 let vertices = [
144 Point { x: -10.0, y: -20.0 },
145 Point { x: 50.0, y: 30.0 },
146 Point { x: 50.0, y: -20.0 },
147 Point { x: -10.0, y: 30.0 },
148 ];
149
150 // After conversion to u32, negative values become 0.
151 let expected = Rect {
152 x: 0, // min_x of -10.0 becomes 0
153 y: 0, // min_y of -20.0 becomes 0
154 width: 50, // max_x of 50.0 -> 50. 50.saturating_sub(0) = 50
155 height: 30, // max_y of 30.0 -> 30. 30.saturating_sub(0) = 30
156 };
157 assert_eq!(to_axis_aligned_bounding_box(&vertices), expected);
158 }
159
160 #[test]
161 fn test_single_point_rect() {
162 // A degenerate rectangle where all points are the same.
163 let vertices = [
164 Point { x: 100, y: 100 },
165 Point { x: 100, y: 100 },
166 Point { x: 100, y: 100 },
167 Point { x: 100, y: 100 },
168 ];
169 let expected = Rect {
170 x: 100,
171 y: 100,
172 width: 0,
173 height: 0,
174 };
175 assert_eq!(to_axis_aligned_bounding_box(&vertices), expected);
176 }
177
178 #[test]
179 fn test_bounding_box_all_negative() {
180 // All points have negative coordinates.
181 // min_x = -100, max_x = -50, min_y = -80, max_y = -40
182 let vertices = [
183 Point { x: -50.0, y: -40.0 },
184 Point {
185 x: -100.0,
186 y: -40.0,
187 },
188 Point {
189 x: -100.0,
190 y: -80.0,
191 },
192 Point { x: -50.0, y: -80.0 },
193 ];
194 // Everything should become 0.
195 let expected = Rect {
196 x: 0,
197 y: 0,
198 width: 0,
199 height: 0,
200 };
201 assert_eq!(to_axis_aligned_bounding_box(&vertices), expected);
202 }
203}