1use crate::contours::{Contour, Moments};
2use crate::core::types::{Point, Rect, Size};
3
4impl Contour {
5 #[must_use]
7 pub fn arc_length(&self, closed: bool) -> f64 {
8 let pts = &self.points;
9 let n = pts.len();
10 if n < 2 {
11 return 0.0;
12 }
13
14 let mut length = 0.0;
15 let limit = if closed { n } else { n - 1 };
16
17 for i in 0..limit {
18 let p0 = pts[i];
19 let p1 = pts[(i + 1) % n];
20 let dx = p1.x as f64 - p0.x as f64;
21 let dy = p1.y as f64 - p0.y as f64;
22 length += (dx * dx + dy * dy).sqrt();
23 }
24
25 length
26 }
27
28 #[must_use]
30 pub fn contour_area(&self) -> f64 {
31 self.moments().m00
32 }
33
34 #[must_use]
36 pub fn approx_poly_dp(&self, epsilon: f64, closed: bool) -> Self {
37 let pts = &self.points;
38 if pts.len() <= 2 {
39 return self.clone();
40 }
41
42 fn find_perpendicular_distance(
43 p: &Point<usize>,
44 line_start: &Point<usize>,
45 line_end: &Point<usize>,
46 ) -> f64 {
47 let dx = line_end.x as f64 - line_start.x as f64;
48 let dy = line_end.y as f64 - line_start.y as f64;
49 let len2 = dx * dx + dy * dy;
50 if len2 == 0.0 {
51 let px = p.x as f64 - line_start.x as f64;
52 let py = p.y as f64 - line_start.y as f64;
53 return (px * px + py * py).sqrt();
54 }
55
56 let t = ((p.x as f64 - line_start.x as f64) * dx
57 + (p.y as f64 - line_start.y as f64) * dy)
58 / len2;
59 let t_clamped = t.clamp(0.0, 1.0);
60 let proj_x = line_start.x as f64 + t_clamped * dx;
61 let proj_y = line_start.y as f64 + t_clamped * dy;
62
63 let rx = p.x as f64 - proj_x;
64 let ry = p.y as f64 - proj_y;
65 (rx * rx + ry * ry).sqrt()
66 }
67
68 #[allow(clippy::needless_range_loop)]
69 fn douglas_peucker(
70 pts: &[Point<usize>],
71 start: usize,
72 end: usize,
73 epsilon: f64,
74 keep: &mut [bool],
75 ) {
76 if end <= start + 1 {
77 return;
78 }
79
80 let mut max_dist = 0.0;
81 let mut index = 0;
82 let line_start = &pts[start];
83 let line_end = &pts[end];
84
85 for i in (start + 1)..end {
86 let dist = find_perpendicular_distance(&pts[i], line_start, line_end);
87 if dist > max_dist {
88 max_dist = dist;
89 index = i;
90 }
91 }
92
93 if max_dist > epsilon {
94 keep[index] = true;
95 douglas_peucker(pts, start, index, epsilon, keep);
96 douglas_peucker(pts, index, end, epsilon, keep);
97 }
98 }
99
100 let mut keep = vec![false; pts.len()];
101 keep[0] = true;
102 keep[pts.len() - 1] = true;
103
104 if closed {
105 let start = 0;
107 let end = pts.len() - 1;
108 douglas_peucker(pts, start, end, epsilon, &mut keep);
109 } else {
110 douglas_peucker(pts, 0, pts.len() - 1, epsilon, &mut keep);
111 }
112
113 let approx_pts: Vec<Point<usize>> = pts
114 .iter()
115 .enumerate()
116 .filter(|&(idx, _)| keep[idx])
117 .map(|(_, &p)| p)
118 .collect();
119
120 Self::new(approx_pts)
121 }
122
123 #[must_use]
125 pub fn bounding_rect(&self) -> Rect<usize> {
126 if self.points.is_empty() {
127 return Rect::default();
128 }
129
130 let mut min_x = usize::MAX;
131 let mut min_y = usize::MAX;
132 let mut max_x = usize::MIN;
133 let mut max_y = usize::MIN;
134
135 for p in &self.points {
136 min_x = min_x.min(p.x);
137 min_y = min_y.min(p.y);
138 max_x = max_x.max(p.x);
139 max_y = max_y.max(p.y);
140 }
141
142 Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
143 }
144
145 #[must_use]
147 pub fn min_area_rect(&self) -> (Point<f64>, Size<f64>, f64) {
148 let hull = self.convex_hull();
149 if hull.points.len() < 3 {
150 let br = self.bounding_rect();
151 return (
152 Point::new(
153 br.x as f64 + br.width as f64 / 2.0,
154 br.y as f64 + br.height as f64 / 2.0,
155 ),
156 Size::new(br.width as f64, br.height as f64),
157 0.0,
158 );
159 }
160
161 let mut min_area = f64::MAX;
162 let mut best_center = Point::new(0.0, 0.0);
163 let mut best_size = Size::new(0.0, 0.0);
164 let mut best_angle = 0.0;
165
166 for angle_deg in 0..90 {
168 let theta = f64::from(angle_deg).to_radians();
169 let cos_t = theta.cos();
170 let sin_t = theta.sin();
171
172 let mut min_u = f64::MAX;
173 let mut max_u = f64::MIN;
174 let mut min_v = f64::MAX;
175 let mut max_v = f64::MIN;
176
177 for p in &hull.points {
178 let px = p.x as f64;
179 let py = p.y as f64;
180 let u = px * cos_t + py * sin_t;
182 let v = -px * sin_t + py * cos_t;
183
184 min_u = min_u.min(u);
185 max_u = max_u.max(u);
186 min_v = min_v.min(v);
187 max_v = max_v.max(v);
188 }
189
190 let w_rot = max_u - min_u;
191 let h_rot = max_v - min_v;
192 let area = w_rot * h_rot;
193
194 if area < min_area {
195 min_area = area;
196 let uc = f64::midpoint(min_u, max_u);
197 let vc = f64::midpoint(min_v, max_v);
198 let cx = uc * cos_t - vc * sin_t;
200 let cy = uc * sin_t + vc * cos_t;
201
202 best_center = Point::new(cx, cy);
203 best_size = Size::new(w_rot, h_rot);
204 best_angle = f64::from(angle_deg);
205 }
206 }
207
208 (best_center, best_size, best_angle)
209 }
210
211 #[must_use]
217 pub fn point_polygon_test(&self, pt: Point<f64>, measure_dist: bool) -> f64 {
218 let pts = &self.points;
219 let n = pts.len();
220 if n == 0 {
221 return -1.0;
222 }
223
224 let mut inside = false;
226 let mut min_dist2 = f64::MAX;
227
228 for i in 0..n {
229 let p0 = pts[i];
230 let p1 = pts[(i + 1) % n];
231
232 let x0 = p0.x as f64;
233 let y0 = p0.y as f64;
234 let x1 = p1.x as f64;
235 let y1 = p1.y as f64;
236
237 if ((y0 > pt.y) != (y1 > pt.y))
239 && (pt.x < (x1 - x0) * (pt.y - y0) / (y1 - y0 + 1e-9) + x0)
240 {
241 inside = !inside;
242 }
243
244 let dx = x1 - x0;
246 let dy = y1 - y0;
247 let len2 = dx * dx + dy * dy;
248 let dist2 = if len2 == 0.0 {
249 let rx = pt.x - x0;
250 let ry = pt.y - y0;
251 rx * rx + ry * ry
252 } else {
253 let t = ((pt.x - x0) * dx + (pt.y - y0) * dy) / len2;
254 let t_clamped = t.clamp(0.0, 1.0);
255 let proj_x = x0 + t_clamped * dx;
256 let proj_y = y0 + t_clamped * dy;
257 let rx = pt.x - proj_x;
258 let ry = pt.y - proj_y;
259 rx * rx + ry * ry
260 };
261
262 if dist2 < min_dist2 {
263 min_dist2 = dist2;
264 }
265 }
266
267 let dist = min_dist2.sqrt();
268 if inside {
269 if measure_dist { dist } else { 1.0 }
270 } else {
271 if measure_dist { -dist } else { -1.0 }
272 }
273 }
274}
275
276pub struct RotatedRect;
278
279impl RotatedRect {
280 #[must_use]
282 pub fn box_points(center: Point<f64>, size: Size<f64>, angle_degrees: f64) -> [Point<f64>; 4] {
283 let theta = angle_degrees.to_radians();
284 let cos_t = theta.cos();
285 let sin_t = theta.sin();
286
287 let hw = size.width / 2.0;
288 let hh = size.height / 2.0;
289
290 let local_corners = [
291 Point::new(-hw, -hh),
292 Point::new(hw, -hh),
293 Point::new(hw, hh),
294 Point::new(-hw, hh),
295 ];
296
297 let mut corners = [Point::default(); 4];
298 for i in 0..4 {
299 let lc = local_corners[i];
300 let rx = lc.x * cos_t - lc.y * sin_t;
301 let ry = lc.x * sin_t + lc.y * cos_t;
302 corners[i] = Point::new(center.x + rx, center.y + ry);
303 }
304
305 corners
306 }
307}
308
309pub struct ShapeAnalysis;
311
312impl ShapeAnalysis {
313 #[must_use]
315 pub fn hu_moments(m: &Moments) -> [f64; 7] {
316 if m.m00.abs() < 1e-9 {
317 return [0.0; 7];
318 }
319
320 let xc = m.m10 / m.m00;
321 let yc = m.m01 / m.m00;
322
323 let mu00 = m.m00;
324 let mu20 = m.m20 - xc * m.m10;
325 let mu02 = m.m02 - yc * m.m01;
326 let mu11 = m.m11 - xc * m.m01;
327 let mu30 = m.m30 - 3.0 * xc * m.m20 + 2.0 * xc * xc * m.m10;
328 let mu03 = m.m03 - 3.0 * yc * m.m02 + 2.0 * yc * yc * m.m01;
329 let mu21 = m.m21 - 2.0 * xc * m.m11 - yc * m.m20 + 2.0 * xc * xc * m.m01;
330 let mu12 = m.m12 - 2.0 * yc * m.m11 - xc * m.m02 + 2.0 * yc * yc * m.m10;
331
332 let inv = 1.0 / mu00;
333 let inv2 = inv * inv;
334 let inv3 = inv2 * inv;
335
336 let eta20 = mu20 * inv2;
337 let eta02 = mu02 * inv2;
338 let eta11 = mu11 * inv2;
339 let eta30 = mu30 * inv3;
340 let eta03 = mu03 * inv3;
341 let eta21 = mu21 * inv3;
342 let eta12 = mu12 * inv3;
343
344 let h1 = eta20 + eta02;
345 let h2 = (eta20 - eta02).powi(2) + 4.0 * eta11 * eta11;
346 let h3 = (eta30 - 3.0 * eta12).powi(2) + (3.0 * eta21 - eta03).powi(2);
347 let h4 = (eta30 + eta12).powi(2) + (eta21 + eta03).powi(2);
348 let h5 = (eta30 - 3.0 * eta12)
349 * (eta30 + eta12)
350 * ((eta30 + eta12).powi(2) - 3.0 * (eta21 + eta03).powi(2))
351 + (3.0 * eta21 - eta03)
352 * (eta21 + eta03)
353 * (3.0 * (eta30 + eta12).powi(2) - (eta21 + eta03).powi(2));
354 let h6 = (eta20 - eta02) * ((eta30 + eta12).powi(2) - (eta21 + eta03).powi(2))
355 + 4.0 * eta11 * (eta30 + eta12) * (eta21 + eta03);
356 let h7 = (3.0 * eta21 - eta03)
357 * (eta30 + eta12)
358 * ((eta30 + eta12).powi(2) - 3.0 * (eta21 + eta03).powi(2))
359 - (eta30 - 3.0 * eta12)
360 * (eta21 + eta03)
361 * (3.0 * (eta30 + eta12).powi(2) - (eta21 + eta03).powi(2));
362
363 [h1, h2, h3, h4, h5, h6, h7]
364 }
365
366 #[must_use]
368 pub fn match_shapes(m1: &Moments, m2: &Moments) -> f64 {
369 let hu1 = Self::hu_moments(m1);
370 let hu2 = Self::hu_moments(m2);
371
372 let mut diff = 0.0;
373 for i in 0..7 {
374 diff += (hu1[i] - hu2[i]).abs();
375 }
376 diff
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_shape_analysis() {
386 let pts = vec![
387 Point::new(0, 0),
388 Point::new(10, 0),
389 Point::new(10, 10),
390 Point::new(0, 10),
391 ];
392 let contour = Contour::new(pts);
393
394 let length = contour.arc_length(true);
395 assert!(length > 0.0);
396
397 let area = contour.contour_area();
398 assert!(area > 0.0);
399
400 let approx = contour.approx_poly_dp(1.0, true);
401 assert!(!approx.points.is_empty());
402
403 let br = contour.bounding_rect();
404 assert_eq!(br.width, 10);
405 assert_eq!(br.height, 10);
406
407 let (center, size, angle) = contour.min_area_rect();
408 assert!(size.width > 0.0);
409
410 let in_pt = Point::new(5.0, 5.0);
411 let out_pt = Point::new(15.0, 15.0);
412 assert!(contour.point_polygon_test(in_pt, false) > 0.0);
413 assert!(contour.point_polygon_test(out_pt, false) < 0.0);
414
415 let corners = RotatedRect::box_points(center, size, angle);
416 assert_eq!(corners.len(), 4);
417
418 let m = contour.moments();
419 let hu = ShapeAnalysis::hu_moments(&m);
420 assert!(hu[0] > 0.0);
421
422 let score = ShapeAnalysis::match_shapes(&m, &m);
423 assert!(score < 1e-9);
424 }
425}