1pub mod shape_analysis;
2
3pub use shape_analysis::{RotatedRect, ShapeAnalysis};
4
5use crate::core::types::Point;
6use crate::error::Result;
7use crate::image::Image;
8use burn::tensor::backend::Backend;
9
10type HierarchyEntry = [i32; 4];
11type ContourResult = (Vec<Vec<Point<usize>>>, Vec<HierarchyEntry>);
12
13#[derive(Clone, Debug, Default, PartialEq)]
15pub struct Contour {
16 pub points: Vec<Point<usize>>,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq)]
21pub struct Moments {
22 pub m00: f64,
23 pub m10: f64,
24 pub m01: f64,
25 pub m20: f64,
26 pub m02: f64,
27 pub m11: f64,
28 pub m30: f64,
29 pub m03: f64,
30 pub m21: f64,
31 pub m12: f64,
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
36pub struct ConvexityDefect {
37 pub start: Point<f64>,
38 pub end: Point<f64>,
39 pub far_point: Point<f64>,
40 pub depth: f64,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum RetrievalMode {
46 External,
48 List,
50 CComp,
53 Tree,
55 FloodFill,
57}
58
59impl Moments {
60 #[must_use]
62 pub fn centroid(&self) -> Option<Point<f64>> {
63 if self.m00.abs() < 1e-9 {
64 None
65 } else {
66 Some(Point::new(self.m10 / self.m00, self.m01 / self.m00))
67 }
68 }
69}
70
71impl Contour {
72 #[must_use]
74 pub fn new(points: Vec<Point<usize>>) -> Self {
75 Self { points }
76 }
77
78 pub fn convexity_defects(contour: &[Point<f64>], hull: &[Point<f64>]) -> Vec<ConvexityDefect> {
85 if contour.len() < 3 || hull.len() < 3 {
86 return Vec::new();
87 }
88
89 let mut defects = Vec::new();
90
91 for hi in 0..hull.len() {
92 let a = hull[hi];
93 let b = hull[(hi + 1) % hull.len()];
94
95 let abx = b.x - a.x;
96 let aby = b.y - a.y;
97 let ab_len2 = abx * abx + aby * aby;
98 if ab_len2 < 1e-12 {
99 continue;
100 }
101
102 let mut max_depth = 0.0;
103 let mut far_point = a;
104
105 for &p in contour {
106 let apx = p.x - a.x;
107 let apy = p.y - a.y;
108
109 let t = ((apx * abx + apy * aby) / ab_len2).clamp(0.0, 1.0);
111 let proj_x = a.x + t * abx;
112 let proj_y = a.y + t * aby;
113
114 let dx = p.x - proj_x;
115 let dy = p.y - proj_y;
116 let depth = (dx * dx + dy * dy).sqrt();
117
118 if depth > max_depth {
119 max_depth = depth;
120 far_point = p;
121 }
122 }
123
124 if max_depth > 1e-6 {
125 defects.push(ConvexityDefect {
126 start: a,
127 end: b,
128 far_point,
129 depth: max_depth,
130 });
131 }
132 }
133
134 defects
135 }
136
137 #[must_use]
139 pub fn convex_hull(&self) -> Self {
140 let mut pts = self.points.clone();
141 if pts.len() <= 3 {
142 return Self::new(pts);
143 }
144
145 pts.sort_by(|a, b| {
147 if a.x == b.x {
148 a.y.cmp(&b.y)
149 } else {
150 a.x.cmp(&b.x)
151 }
152 });
153
154 fn cross(o: &Point<usize>, a: &Point<usize>, b: &Point<usize>) -> isize {
155 (a.x as isize - o.x as isize) * (b.y as isize - o.y as isize)
156 - (a.y as isize - o.y as isize) * (b.x as isize - o.x as isize)
157 }
158
159 let mut lower = Vec::new();
160 for p in &pts {
161 while lower.len() >= 2
162 && cross(&lower[lower.len() - 2], &lower[lower.len() - 1], p) <= 0
163 {
164 lower.pop();
165 }
166 lower.push(*p);
167 }
168
169 let mut upper = Vec::new();
170 for p in pts.iter().rev() {
171 while upper.len() >= 2
172 && cross(&upper[upper.len() - 2], &upper[upper.len() - 1], p) <= 0
173 {
174 upper.pop();
175 }
176 upper.push(*p);
177 }
178
179 lower.pop();
180 upper.pop();
181 lower.extend(upper);
182
183 Self::new(lower)
184 }
185
186 #[must_use]
188 pub fn moments(&self) -> Moments {
189 let pts = &self.points;
190 let n = pts.len();
191 if n < 3 {
192 return Moments::default();
193 }
194
195 let mut m00 = 0.0;
196 let mut m10 = 0.0;
197 let mut m01 = 0.0;
198 let mut m20 = 0.0;
199 let mut m02 = 0.0;
200 let mut m11 = 0.0;
201 let mut m30 = 0.0;
202 let mut m03 = 0.0;
203 let mut m21 = 0.0;
204 let mut m12 = 0.0;
205
206 for i in 0..n {
207 let p0 = pts[i];
208 let p1 = pts[(i + 1) % n];
209
210 let xi = p0.x as f64;
211 let yi = p0.y as f64;
212 let xi1 = p1.x as f64;
213 let yi1 = p1.y as f64;
214
215 let cross = xi * yi1 - xi1 * yi;
216 m00 += cross;
217 m10 += (xi + xi1) * cross;
218 m01 += (yi + yi1) * cross;
219 m20 += (xi * xi + xi * xi1 + xi1 * xi1) * cross;
220 m02 += (yi * yi + yi * yi1 + yi1 * yi1) * cross;
221 m11 += (2.0 * xi * yi + xi * yi1 + xi1 * yi + 2.0 * xi1 * yi1) * cross;
222 m30 += (xi * xi * xi + xi * xi * xi1 + xi * xi1 * xi1 + xi1 * xi1 * xi1) * cross;
223 m03 += (yi * yi * yi + yi * yi * yi1 + yi * yi1 * yi1 + yi1 * yi1 * yi1) * cross;
224 m21 += (2.0 * xi * xi * yi
225 + xi * xi * yi1
226 + 2.0 * xi * xi1 * yi
227 + xi * xi1 * yi1
228 + xi1 * xi1 * yi
229 + 2.0 * xi1 * xi1 * yi1)
230 * cross;
231 m12 += (2.0 * yi * yi * xi
232 + yi * yi * xi1
233 + 2.0 * yi * yi1 * xi
234 + yi * yi1 * xi1
235 + yi1 * yi1 * xi
236 + 2.0 * yi1 * yi1 * xi1)
237 * cross;
238 }
239
240 m00 /= 2.0;
241 m10 /= 6.0;
242 m01 /= 6.0;
243 m20 /= 12.0;
244 m02 /= 12.0;
245 m11 /= 24.0;
246 m30 /= 20.0;
247 m03 /= 20.0;
248 m21 /= 60.0;
249 m12 /= 60.0;
250
251 Moments {
252 m00: m00.abs(),
253 m10: m10.abs(),
254 m01: m01.abs(),
255 m20: m20.abs(),
256 m02: m02.abs(),
257 m11: m11.abs(),
258 m30: m30.abs(),
259 m03: m03.abs(),
260 m21: m21.abs(),
261 m12: m12.abs(),
262 }
263 }
264}
265
266impl<B: Backend> Image<B> {
267 pub fn find_contours(&self) -> Result<Vec<Contour>> {
270 let gray = self.grayscale()?;
271 let dims = gray.tensor.dims();
272 let h = dims[1];
273 let w = dims[2];
274
275 let tensor_data = gray.tensor.clone().into_data();
276 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
277
278 let mut visited = vec![false; h * w];
279 let mut contours = Vec::new();
280
281 let dx = [1, 1, 0, -1, -1, -1, 0, 1];
283 let dy = [0, 1, 1, 1, 0, -1, -1, -1];
284
285 for y in 1..(h - 1) {
286 for x in 1..(w - 1) {
287 let idx = y * w + x;
288 if flat_vals[idx] > 0.5 && !visited[idx] {
289 let mut pts = Vec::new();
291 let mut cx = x;
292 let mut cy = y;
293 let mut dir = 0;
294
295 pts.push(Point::new(cx, cy));
296 visited[idx] = true;
297
298 let mut loop_count = 0;
300 loop {
301 let mut found = false;
302 for i in 0..8 {
303 let ndir = (dir + i) % 8;
304 let nx = cx as isize + dx[ndir];
305 let ny = cy as isize + dy[ndir];
306
307 if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
308 let nidx = (ny as usize) * w + (nx as usize);
309 if flat_vals[nidx] > 0.5 {
310 cx = nx as usize;
311 cy = ny as usize;
312 visited[nidx] = true;
313 pts.push(Point::new(cx, cy));
314 dir = (ndir + 5) % 8; found = true;
316 break;
317 }
318 }
319 }
320
321 if !found || (cx == x && cy == y) || loop_count > 10000 {
322 break;
323 }
324 loop_count += 1;
325 }
326
327 if pts.len() >= 3 {
328 contours.push(Contour::new(pts));
329 }
330 }
331 }
332 }
333
334 Ok(contours)
335 }
336
337 pub fn find_contours_with_hierarchy(&self, mode: RetrievalMode) -> Result<ContourResult> {
344 let gray = self.grayscale()?;
345 let dims = gray.tensor.dims();
346 let h = dims[1];
347 let w = dims[2];
348
349 let tensor_data = gray.tensor.clone().into_data();
350 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
351
352 let mut binary = vec![false; h * w];
354 for i in 0..(h * w) {
355 binary[i] = flat_vals[i] > 0.5;
356 }
357
358 let mut labels = vec![0i32; h * w];
360 let mut label_count: i32 = 0;
361
362 let dx4 = [1, 0, -1, 0];
363 let dy4 = [0, 1, 0, -1];
364
365 for y in 0..h {
366 for x in 0..w {
367 let idx = y * w + x;
368 if binary[idx] && labels[idx] == 0 {
369 label_count += 1;
370 let mut stack = vec![(x, y)];
372 labels[idx] = label_count;
373 while let Some((cx, cy)) = stack.pop() {
374 for d in 0..4 {
375 let nx = cx as isize + dx4[d];
376 let ny = cy as isize + dy4[d];
377 if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
378 let nidx = ny as usize * w + nx as usize;
379 if binary[nidx] && labels[nidx] == 0 {
380 labels[nidx] = label_count;
381 stack.push((nx as usize, ny as usize));
382 }
383 }
384 }
385 }
386 }
387 }
388 }
389
390 let dx8 = [1, 1, 0, -1, -1, -1, 0, 1];
392 let dy8 = [0, 1, 1, 1, 0, -1, -1, -1];
393
394 let mut all_contours: Vec<Vec<Point<usize>>> = Vec::new();
395 let mut visited = vec![false; h * w];
396
397 for y in 1..(h - 1) {
398 for x in 1..(w - 1) {
399 let idx = y * w + x;
400 if !binary[idx] || visited[idx] {
401 continue;
402 }
403
404 let mut is_boundary = false;
406 for d in 0..8 {
407 let nx = x as isize + dx8[d];
408 let ny = y as isize + dy8[d];
409 if nx >= 0
410 && nx < w as isize
411 && ny >= 0
412 && ny < h as isize
413 && !binary[ny as usize * w + nx as usize]
414 {
415 is_boundary = true;
416 break;
417 }
418 }
419 if !is_boundary {
420 visited[idx] = true;
421 continue;
422 }
423
424 let mut pts = Vec::new();
426 let mut cx = x;
427 let mut cy = y;
428 let mut dir = 0;
429
430 pts.push(Point::new(cx, cy));
431 visited[idx] = true;
432
433 let mut loop_count = 0;
434 loop {
435 let mut found = false;
436 for i in 0..8 {
437 let ndir = (dir + i) % 8;
438 let nx = cx as isize + dx8[ndir];
439 let ny = cy as isize + dy8[ndir];
440
441 if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
442 let nidx = ny as usize * w + nx as usize;
443 if binary[nidx] {
444 cx = nx as usize;
445 cy = ny as usize;
446 visited[nidx] = true;
447 pts.push(Point::new(cx, cy));
448 dir = (ndir + 5) % 8;
449 found = true;
450 break;
451 }
452 }
453 }
454
455 if !found || (cx == x && cy == y) || loop_count > 10000 {
456 break;
457 }
458 loop_count += 1;
459 }
460
461 if pts.len() >= 3 {
462 all_contours.push(pts);
463 }
464 }
465 }
466
467 let n = all_contours.len();
469
470 let mut parent_map: Vec<Option<usize>> = vec![None; n];
473
474 for i in 0..n {
475 let ci = &all_contours[i];
476 let center_i = {
477 let sx: f64 = ci.iter().map(|p| p.x as f64).sum::<f64>() / ci.len() as f64;
478 let sy: f64 = ci.iter().map(|p| p.y as f64).sum::<f64>() / ci.len() as f64;
479 Point::new(sx, sy)
480 };
481
482 let mut best_parent: Option<usize> = None;
483 let mut best_area = f64::MAX;
484
485 for j in 0..n {
486 if i == j {
487 continue;
488 }
489 let cj = &all_contours[j];
491 if Self::point_inside_polygon(center_i, cj) {
492 let area = Self::polygon_area(cj);
493 if area < best_area {
494 best_area = area;
495 best_parent = Some(j);
496 }
497 }
498 }
499 parent_map[i] = best_parent;
500 }
501
502 let include = |idx: usize, mode: RetrievalMode| -> bool {
504 match mode {
505 RetrievalMode::External => parent_map[idx].is_none(),
506 RetrievalMode::List | RetrievalMode::CComp | RetrievalMode::Tree => true,
507 RetrievalMode::FloodFill => true,
508 }
509 };
510
511 let filtered_indices: Vec<usize> = (0..n).filter(|&i| include(i, mode)).collect();
513 let mut index_map: Vec<i32> = vec![-1; n];
514 for (new_idx, &old_idx) in filtered_indices.iter().enumerate() {
515 index_map[old_idx] = new_idx as i32;
516 }
517
518 let filtered_n = filtered_indices.len();
519 let mut filtered_hierarchy = vec![[-1i32; 4]; filtered_n];
520
521 for (new_idx, &old_idx) in filtered_indices.iter().enumerate() {
522 let next_sibling = {
524 let mut found = -1i32;
525 if let Some(parent) = parent_map[old_idx] {
526 for k in (old_idx + 1)..n {
527 if parent_map[k] == Some(parent) && index_map[k] >= 0 {
528 found = index_map[k];
529 break;
530 }
531 }
532 }
533 found
534 };
535
536 let prev_sibling = {
538 let mut found = -1i32;
539 if let Some(parent) = parent_map[old_idx] {
540 for k in (0..old_idx).rev() {
541 if parent_map[k] == Some(parent) && index_map[k] >= 0 {
542 found = index_map[k];
543 break;
544 }
545 }
546 }
547 found
548 };
549
550 let first_child = {
552 let mut found = -1i32;
553 for k in 0..n {
554 if parent_map[k] == Some(old_idx) && index_map[k] >= 0 {
555 found = index_map[k];
556 break;
557 }
558 }
559 found
560 };
561
562 let parent = parent_map[old_idx].map(|p| index_map[p]).unwrap_or(-1);
563
564 filtered_hierarchy[new_idx] = [next_sibling, prev_sibling, first_child, parent];
565 }
566
567 let contours_out: Vec<Vec<Point<usize>>> = filtered_indices
568 .into_iter()
569 .map(|i| all_contours[i].clone())
570 .collect();
571
572 Ok((contours_out, filtered_hierarchy))
573 }
574
575 fn point_inside_polygon(point: Point<f64>, polygon: &[Point<usize>]) -> bool {
577 let n = polygon.len();
578 if n < 3 {
579 return false;
580 }
581 let mut inside = false;
582 let mut j = n - 1;
583 for i in 0..n {
584 let yi = polygon[i].y as f64;
585 let yj = polygon[j].y as f64;
586 let xi = polygon[i].x as f64;
587 let xj = polygon[j].x as f64;
588
589 if ((yi > point.y) != (yj > point.y))
590 && (point.x < (xj - xi) * (point.y - yi) / (yj - yi + 1e-9) + xi)
591 {
592 inside = !inside;
593 }
594 j = i;
595 }
596 inside
597 }
598
599 fn polygon_area(polygon: &[Point<usize>]) -> f64 {
601 let n = polygon.len();
602 if n < 3 {
603 return 0.0;
604 }
605 let mut area = 0.0;
606 for i in 0..n {
607 let j = (i + 1) % n;
608 area += polygon[i].x as f64 * polygon[j].y as f64;
609 area -= polygon[j].x as f64 * polygon[i].y as f64;
610 }
611 area.abs() / 2.0
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use crate::test_helpers::{TestBackend, test_device};
619 use burn::tensor::{Tensor, TensorData};
620
621 #[test]
622 fn test_contours_and_moments() {
623 let pts = vec![
624 Point::new(0, 0),
625 Point::new(10, 0),
626 Point::new(10, 10),
627 Point::new(0, 10),
628 ];
629 let contour = Contour::new(pts);
630 let hull = contour.convex_hull();
631 assert_eq!(hull.points.len(), 4);
632
633 let m = contour.moments();
634 assert!(m.m00 > 0.0);
635 let centroid = m.centroid().unwrap();
636 assert!(centroid.x > 0.0);
637
638 let device = test_device();
639 let mut flat_data = vec![0.0f32; 10 * 10];
641 for y in 2..5 {
643 for x in 2..5 {
644 flat_data[y * 10 + x] = 1.0;
645 }
646 }
647 let tensor =
648 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 10, 10]), &device);
649 let img = Image::new(tensor);
650 let found = img.find_contours().unwrap();
651 assert!(!found.is_empty());
652 }
653
654 #[test]
655 fn test_convexity_defects() {
656 let contour = vec![
659 Point::new(0.0, 0.0),
660 Point::new(10.0, 0.0),
661 Point::new(10.0, 5.0),
662 Point::new(7.0, 3.0), Point::new(5.0, 5.0), Point::new(5.0, 10.0),
665 Point::new(0.0, 10.0),
666 ];
667
668 let hull = vec![
670 Point::new(0.0, 0.0),
671 Point::new(10.0, 0.0),
672 Point::new(10.0, 5.0),
673 Point::new(5.0, 10.0),
674 Point::new(0.0, 10.0),
675 ];
676
677 let defects = Contour::convexity_defects(&contour, &hull);
678 assert!(!defects.is_empty());
680 for d in &defects {
681 assert!(d.depth > 0.0);
682 }
683 }
684
685 #[test]
686 fn test_find_contours_with_hierarchy() {
687 let device = test_device();
688 let mut flat_data = vec![0.0f32; 20 * 20];
690 for y in 2..5 {
692 for x in 2..5 {
693 flat_data[y * 20 + x] = 1.0;
694 }
695 }
696 for y in 12..15 {
698 for x in 12..15 {
699 flat_data[y * 20 + x] = 1.0;
700 }
701 }
702 let tensor =
703 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 20, 20]), &device);
704 let img = Image::new(tensor);
705
706 let (contours, hierarchy) = img
707 .find_contours_with_hierarchy(RetrievalMode::External)
708 .unwrap();
709 assert!(!contours.is_empty());
710 assert_eq!(contours.len(), hierarchy.len());
711 for h in &hierarchy {
713 assert_eq!(h.len(), 4);
714 }
715 }
716}