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}