1use scirs2_core::ndarray::{Array2, ArrayView2};
34use std::collections::VecDeque;
35
36use crate::error::{NdimageError, NdimageResult};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum RetrievalMode {
41 #[default]
43 External,
44 List,
46 CComp,
49 Tree,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum ApproximationMethod {
56 None,
58 #[default]
61 Simple,
62 TehChinL1,
64 TehChinKCos,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct ContourHierarchy {
79 pub next: i32,
81 pub previous: i32,
83 pub first_child: i32,
85 pub parent: i32,
87}
88
89impl Default for ContourHierarchy {
90 fn default() -> Self {
91 Self {
92 next: -1,
93 previous: -1,
94 first_child: -1,
95 parent: -1,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct Contour {
103 pub points: Vec<(i32, i32)>,
105 pub hierarchy: ContourHierarchy,
107 pub is_hole: bool,
109}
110
111impl Contour {
112 pub fn new(points: Vec<(i32, i32)>) -> Self {
114 Self {
115 points,
116 hierarchy: ContourHierarchy::default(),
117 is_hole: false,
118 }
119 }
120
121 pub fn area(&self) -> f64 {
123 if self.points.len() < 3 {
124 return 0.0;
125 }
126
127 let mut sum = 0i64;
128 let n = self.points.len();
129
130 for i in 0..n {
131 let j = (i + 1) % n;
132 sum += (self.points[i].0 as i64) * (self.points[j].1 as i64);
133 sum -= (self.points[j].0 as i64) * (self.points[i].1 as i64);
134 }
135
136 (sum.abs() as f64) / 2.0
137 }
138
139 pub fn perimeter(&self) -> f64 {
141 if self.points.len() < 2 {
142 return 0.0;
143 }
144
145 let mut length = 0.0;
146 let n = self.points.len();
147
148 for i in 0..n {
149 let j = (i + 1) % n;
150 let dx = (self.points[j].0 - self.points[i].0) as f64;
151 let dy = (self.points[j].1 - self.points[i].1) as f64;
152 length += (dx * dx + dy * dy).sqrt();
153 }
154
155 length
156 }
157
158 pub fn bounding_rect(&self) -> (i32, i32, i32, i32) {
160 if self.points.is_empty() {
161 return (0, 0, 0, 0);
162 }
163
164 let mut min_x = i32::MAX;
165 let mut min_y = i32::MAX;
166 let mut max_x = i32::MIN;
167 let mut max_y = i32::MIN;
168
169 for &(x, y) in &self.points {
170 min_x = min_x.min(x);
171 min_y = min_y.min(y);
172 max_x = max_x.max(x);
173 max_y = max_y.max(y);
174 }
175
176 (min_x, min_y, max_x - min_x + 1, max_y - min_y + 1)
177 }
178
179 pub fn centroid(&self) -> (f64, f64) {
181 if self.points.is_empty() {
182 return (0.0, 0.0);
183 }
184
185 let sum_x: i64 = self.points.iter().map(|p| p.0 as i64).sum();
186 let sum_y: i64 = self.points.iter().map(|p| p.1 as i64).sum();
187 let n = self.points.len() as f64;
188
189 (sum_x as f64 / n, sum_y as f64 / n)
190 }
191}
192
193const DIRECTIONS: [(i32, i32); 8] = [
196 (1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), ];
205
206pub fn find_contours(
247 image: &ArrayView2<u8>,
248 mode: RetrievalMode,
249 method: ApproximationMethod,
250) -> NdimageResult<Vec<Contour>> {
251 let (height, width) = image.dim();
252
253 if height < 2 || width < 2 {
254 return Ok(Vec::new());
255 }
256
257 let mut labels = Array2::<i32>::zeros((height + 2, width + 2));
260
261 for i in 0..height {
263 for j in 0..width {
264 if image[[i, j]] != 0 {
265 labels[[i + 1, j + 1]] = 1;
266 }
267 }
268 }
269
270 let mut contours = Vec::new();
271 let mut nbd = 1; let mut lnbd; let mut parent_stack: Vec<i32> = Vec::new();
276 let mut border_types: Vec<bool> = Vec::new(); for i in 1..=height {
279 lnbd = 1;
280
281 for j in 1..=width {
282 let fij = labels[[i, j]];
283
284 let is_outer_start = fij == 1 && labels[[i, j - 1]] == 0;
287
288 let is_hole_start = fij >= 1 && labels[[i, j + 1]] == 0;
291
292 if is_outer_start || is_hole_start {
293 let is_outer = is_outer_start;
294
295 let parent = if lnbd <= 1 {
297 -1
298 } else {
299 let lnbd_idx = (lnbd - 2) as usize;
300 if lnbd_idx < border_types.len() {
301 let lnbd_is_outer = border_types[lnbd_idx];
302 if is_outer == lnbd_is_outer {
303 if lnbd_idx < parent_stack.len() {
305 parent_stack[lnbd_idx]
306 } else {
307 -1
308 }
309 } else {
310 lnbd as i32 - 2
312 }
313 } else {
314 -1
315 }
316 };
317
318 nbd += 1;
319
320 let start_dir = if is_outer { 0 } else { 4 }; let border_points = follow_border(&mut labels, i, j, start_dir, nbd, is_outer)?;
323
324 if !border_points.is_empty() {
325 let approx_points = approximate_contour(&border_points, method);
327
328 let adjusted_points: Vec<(i32, i32)> = approx_points
330 .into_iter()
331 .map(|(x, y)| (x - 1, y - 1))
332 .collect();
333
334 let should_add = match mode {
336 RetrievalMode::External => is_outer && parent == -1,
337 RetrievalMode::List => true,
338 RetrievalMode::CComp => true,
339 RetrievalMode::Tree => true,
340 };
341
342 if should_add && !adjusted_points.is_empty() {
343 let mut contour = Contour::new(adjusted_points);
344 contour.is_hole = !is_outer;
345 contour.hierarchy.parent = parent;
346 contours.push(contour);
347 }
348 }
349
350 parent_stack.push(parent);
351 border_types.push(is_outer);
352 }
353
354 let abs_fij = labels[[i, j]].abs();
356 if abs_fij > 1 {
357 lnbd = abs_fij;
358 }
359 }
360 }
361
362 build_hierarchy(&mut contours, mode);
364
365 Ok(contours)
366}
367
368fn follow_border(
370 labels: &mut Array2<i32>,
371 start_i: usize,
372 start_j: usize,
373 start_dir: usize,
374 nbd: i32,
375 is_outer: bool,
376) -> NdimageResult<Vec<(i32, i32)>> {
377 let (height, width) = labels.dim();
378 let mut points = Vec::new();
379
380 let mut dir = start_dir;
382 let mut found = false;
383
384 for _ in 0..8 {
385 let ni = (start_i as i32 + DIRECTIONS[dir].1) as usize;
386 let nj = (start_j as i32 + DIRECTIONS[dir].0) as usize;
387
388 if ni < height && nj < width && labels[[ni, nj]] != 0 {
389 found = true;
390 break;
391 }
392 dir = (dir + 1) % 8;
393 }
394
395 if !found {
396 points.push((start_j as i32, start_i as i32));
398 labels[[start_i, start_j]] = -nbd;
399 return Ok(points);
400 }
401
402 let mut i = start_i;
403 let mut j = start_j;
404 let mut prev_dir = dir;
405
406 loop {
407 points.push((j as i32, i as i32));
408
409 let search_start = (prev_dir + if is_outer { 6 } else { 2 }) % 8;
411 let mut next_found = false;
412 let mut next_i = i;
413 let mut next_j = j;
414 let mut examined_background = false;
415
416 for k in 0..8 {
417 let d = (search_start + k) % 8;
418 let ni = (i as i32 + DIRECTIONS[d].1) as usize;
419 let nj = (j as i32 + DIRECTIONS[d].0) as usize;
420
421 if ni >= height || nj >= width {
422 examined_background = true;
423 continue;
424 }
425
426 if labels[[ni, nj]] == 0 {
427 examined_background = true;
428 } else {
429 next_i = ni;
430 next_j = nj;
431 prev_dir = d;
432 next_found = true;
433 break;
434 }
435 }
436
437 if examined_background {
439 labels[[i, j]] = -nbd;
440 } else if labels[[i, j]] == 1 {
441 labels[[i, j]] = nbd;
442 }
443
444 if !next_found || (next_i == start_i && next_j == start_j && i == start_i && j == start_j) {
445 break;
446 }
447
448 if next_i == start_i && next_j == start_j {
450 break;
452 }
453
454 i = next_i;
455 j = next_j;
456
457 if points.len() > (height * width * 4) {
459 return Err(NdimageError::ComputationError(
460 "Contour following exceeded maximum iterations".to_string(),
461 ));
462 }
463 }
464
465 Ok(points)
466}
467
468fn approximate_contour(points: &[(i32, i32)], method: ApproximationMethod) -> Vec<(i32, i32)> {
470 match method {
471 ApproximationMethod::None => points.to_vec(),
472 ApproximationMethod::Simple => approximate_simple(points),
473 ApproximationMethod::TehChinL1 => approximate_teh_chin(points, false),
474 ApproximationMethod::TehChinKCos => approximate_teh_chin(points, true),
475 }
476}
477
478fn approximate_simple(points: &[(i32, i32)]) -> Vec<(i32, i32)> {
480 if points.len() <= 2 {
481 return points.to_vec();
482 }
483
484 let mut result = Vec::new();
485 result.push(points[0]);
486
487 let mut prev_dx = 0i32;
488 let mut prev_dy = 0i32;
489
490 for i in 1..points.len() {
491 let dx = (points[i].0 - points[i - 1].0).signum();
492 let dy = (points[i].1 - points[i - 1].1).signum();
493
494 if dx != prev_dx || dy != prev_dy {
495 if i > 1 {
496 result.push(points[i - 1]);
497 }
498 prev_dx = dx;
499 prev_dy = dy;
500 }
501 }
502
503 if result.last() != points.last() {
505 result.push(*points.last().unwrap_or(&(0, 0)));
506 }
507
508 result
509}
510
511fn approximate_teh_chin(points: &[(i32, i32)], use_kcos: bool) -> Vec<(i32, i32)> {
513 if points.len() <= 4 {
514 return points.to_vec();
515 }
516
517 let n = points.len();
519 let mut curvatures = vec![0.0f64; n];
520 let region = 3; for i in 0..n {
523 let prev_idx = (i + n - region) % n;
524 let next_idx = (i + region) % n;
525
526 let p1 = points[prev_idx];
527 let p2 = points[i];
528 let p3 = points[next_idx];
529
530 if use_kcos {
531 let v1 = ((p2.0 - p1.0) as f64, (p2.1 - p1.1) as f64);
533 let v2 = ((p3.0 - p2.0) as f64, (p3.1 - p2.1) as f64);
534
535 let dot = v1.0 * v2.0 + v1.1 * v2.1;
536 let len1 = (v1.0 * v1.0 + v1.1 * v1.1).sqrt();
537 let len2 = (v2.0 * v2.0 + v2.1 * v2.1).sqrt();
538
539 if len1 > 0.0 && len2 > 0.0 {
540 curvatures[i] = 1.0 - dot / (len1 * len2);
541 }
542 } else {
543 let direct_dist = ((p3.0 - p1.0).abs() + (p3.1 - p1.1).abs()) as f64;
545 let path_dist = ((p2.0 - p1.0).abs()
546 + (p2.1 - p1.1).abs()
547 + (p3.0 - p2.0).abs()
548 + (p3.1 - p2.1).abs()) as f64;
549
550 if path_dist > 0.0 {
551 curvatures[i] = 1.0 - direct_dist / path_dist;
552 }
553 }
554 }
555
556 let threshold = 0.1;
558 let mut is_corner = vec![false; n];
559
560 for i in 0..n {
561 if curvatures[i] > threshold {
562 let prev = (i + n - 1) % n;
563 let next = (i + 1) % n;
564
565 if curvatures[i] >= curvatures[prev] && curvatures[i] >= curvatures[next] {
566 is_corner[i] = true;
567 }
568 }
569 }
570
571 let mut result: Vec<(i32, i32)> = points
573 .iter()
574 .enumerate()
575 .filter(|(i, _)| is_corner[*i])
576 .map(|(_, p)| *p)
577 .collect();
578
579 if result.is_empty() {
581 result = approximate_simple(points);
582 }
583
584 result
585}
586
587fn build_hierarchy(contours: &mut [Contour], mode: RetrievalMode) {
589 if contours.is_empty() {
590 return;
591 }
592
593 let n = contours.len();
594
595 match mode {
596 RetrievalMode::External | RetrievalMode::List => {
597 for i in 0..n {
599 contours[i].hierarchy.next = if i + 1 < n { (i + 1) as i32 } else { -1 };
600 contours[i].hierarchy.previous = if i > 0 { (i - 1) as i32 } else { -1 };
601 contours[i].hierarchy.first_child = -1;
602 contours[i].hierarchy.parent = -1;
603 }
604 }
605 RetrievalMode::CComp | RetrievalMode::Tree => {
606 for i in 0..n {
609 let parent = contours[i].hierarchy.parent;
610 if parent >= 0 && (parent as usize) < n {
611 let parent_idx = parent as usize;
612 if contours[parent_idx].hierarchy.first_child == -1 {
613 contours[parent_idx].hierarchy.first_child = i as i32;
614 }
615 }
616 }
617
618 let mut by_parent: std::collections::HashMap<i32, Vec<usize>> =
621 std::collections::HashMap::new();
622
623 for i in 0..n {
624 by_parent
625 .entry(contours[i].hierarchy.parent)
626 .or_default()
627 .push(i);
628 }
629
630 for siblings in by_parent.values() {
631 for (idx, &i) in siblings.iter().enumerate() {
632 contours[i].hierarchy.previous = if idx > 0 {
633 siblings[idx - 1] as i32
634 } else {
635 -1
636 };
637 contours[i].hierarchy.next = if idx + 1 < siblings.len() {
638 siblings[idx + 1] as i32
639 } else {
640 -1
641 };
642 }
643 }
644 }
645 }
646}
647
648pub fn draw_contours(
658 image: &mut Array2<u8>,
659 contours: &[Contour],
660 contour_idx: i32,
661 color: u8,
662 thickness: i32,
663) -> NdimageResult<()> {
664 let (height, width) = image.dim();
665
666 let indices: Vec<usize> = if contour_idx < 0 {
667 (0..contours.len()).collect()
668 } else {
669 vec![contour_idx as usize]
670 };
671
672 for &idx in &indices {
673 if idx >= contours.len() {
674 continue;
675 }
676
677 let contour = &contours[idx];
678
679 if thickness == -1 {
680 fill_contour(image, contour)?;
682 } else {
683 for i in 0..contour.points.len() {
685 let p1 = contour.points[i];
686 let p2 = contour.points[(i + 1) % contour.points.len()];
687
688 draw_line(image, p1, p2, color, thickness.max(1) as usize)?;
689 }
690 }
691 }
692
693 Ok(())
694}
695
696fn fill_contour(image: &mut Array2<u8>, contour: &Contour) -> NdimageResult<()> {
698 if contour.points.len() < 3 {
699 return Ok(());
700 }
701
702 let (height, width) = image.dim();
703 let (_, min_y, _, h) = contour.bounding_rect();
704 let max_y = min_y + h;
705
706 for y in min_y.max(0)..max_y.min(height as i32) {
707 let mut intersections = Vec::new();
708
709 for i in 0..contour.points.len() {
711 let p1 = contour.points[i];
712 let p2 = contour.points[(i + 1) % contour.points.len()];
713
714 if (p1.1 <= y && p2.1 > y) || (p2.1 <= y && p1.1 > y) {
715 let x = p1.0 + (y - p1.1) * (p2.0 - p1.0) / (p2.1 - p1.1);
716 intersections.push(x);
717 }
718 }
719
720 intersections.sort();
721
722 for pair in intersections.chunks(2) {
724 if pair.len() == 2 {
725 let x1 = pair[0].max(0) as usize;
726 let x2 = (pair[1] as usize).min(width - 1);
727 for x in x1..=x2 {
728 if y >= 0 && (y as usize) < height {
729 image[[y as usize, x]] = 255;
730 }
731 }
732 }
733 }
734 }
735
736 Ok(())
737}
738
739fn draw_line(
741 image: &mut Array2<u8>,
742 p1: (i32, i32),
743 p2: (i32, i32),
744 color: u8,
745 thickness: usize,
746) -> NdimageResult<()> {
747 let (height, width) = image.dim();
748
749 let dx = (p2.0 - p1.0).abs();
750 let dy = (p2.1 - p1.1).abs();
751 let sx = if p1.0 < p2.0 { 1 } else { -1 };
752 let sy = if p1.1 < p2.1 { 1 } else { -1 };
753 let mut err = dx - dy;
754
755 let mut x = p1.0;
756 let mut y = p1.1;
757
758 loop {
759 let half_t = (thickness / 2) as i32;
761 for dy in -half_t..=half_t {
762 for dx in -half_t..=half_t {
763 let px = x + dx;
764 let py = y + dy;
765 if px >= 0 && py >= 0 && (px as usize) < width && (py as usize) < height {
766 image[[py as usize, px as usize]] = color;
767 }
768 }
769 }
770
771 if x == p2.0 && y == p2.1 {
772 break;
773 }
774
775 let e2 = 2 * err;
776 if e2 > -dy {
777 err -= dy;
778 x += sx;
779 }
780 if e2 < dx {
781 err += dx;
782 y += sy;
783 }
784 }
785
786 Ok(())
787}
788
789pub fn point_in_contour(point: (i32, i32), contour: &Contour) -> bool {
791 if contour.points.len() < 3 {
792 return false;
793 }
794
795 let (x, y) = point;
796 let n = contour.points.len();
797 let mut inside = false;
798
799 let mut j = n - 1;
800 for i in 0..n {
801 let (xi, yi) = contour.points[i];
802 let (xj, yj) = contour.points[j];
803
804 if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
805 inside = !inside;
806 }
807 j = i;
808 }
809
810 inside
811}
812
813#[derive(Debug, Clone, Default)]
815pub struct ContourMoments {
816 pub m: [[f64; 4]; 4],
818 pub mu: [[f64; 4]; 4],
820 pub nu: [[f64; 4]; 4],
822}
823
824impl ContourMoments {
825 pub fn from_contour(contour: &Contour) -> Self {
827 let mut moments = Self::default();
828
829 if contour.points.is_empty() {
830 return moments;
831 }
832
833 for &(x, y) in &contour.points {
835 let xf = x as f64;
836 let yf = y as f64;
837
838 for i in 0..4 {
839 for j in 0..4 {
840 if i + j <= 3 {
841 moments.m[i][j] += xf.powi(i as i32) * yf.powi(j as i32);
842 }
843 }
844 }
845 }
846
847 let cx = if moments.m[0][0] != 0.0 {
849 moments.m[1][0] / moments.m[0][0]
850 } else {
851 0.0
852 };
853 let cy = if moments.m[0][0] != 0.0 {
854 moments.m[0][1] / moments.m[0][0]
855 } else {
856 0.0
857 };
858
859 for &(x, y) in &contour.points {
861 let dx = x as f64 - cx;
862 let dy = y as f64 - cy;
863
864 for i in 0..4 {
865 for j in 0..4 {
866 if i + j >= 2 && i + j <= 3 {
867 moments.mu[i][j] += dx.powi(i as i32) * dy.powi(j as i32);
868 }
869 }
870 }
871 }
872
873 let m00 = moments.m[0][0];
875 if m00 > 0.0 {
876 for i in 0..4 {
877 for j in 0..4 {
878 if i + j >= 2 && i + j <= 3 {
879 let exp = ((i + j) as f64 / 2.0) + 1.0;
880 moments.nu[i][j] = moments.mu[i][j] / m00.powf(exp);
881 }
882 }
883 }
884 }
885
886 moments
887 }
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use scirs2_core::ndarray::Array2;
894
895 #[test]
896 fn test_find_contours_simple_square() {
897 let mut image = Array2::<u8>::zeros((10, 10));
898
899 for i in 2..8 {
901 for j in 2..8 {
902 image[[i, j]] = 255;
903 }
904 }
905
906 let contours = find_contours(
907 &image.view(),
908 RetrievalMode::External,
909 ApproximationMethod::Simple,
910 )
911 .expect("find_contours should succeed for valid image");
912
913 assert!(!contours.is_empty());
914 assert!(contours[0].points.len() >= 4); }
916
917 #[test]
918 fn test_find_contours_with_hole() {
919 let mut image = Array2::<u8>::zeros((20, 20));
920
921 for i in 2..18 {
923 for j in 2..18 {
924 image[[i, j]] = 255;
925 }
926 }
927
928 for i in 6..14 {
930 for j in 6..14 {
931 image[[i, j]] = 0;
932 }
933 }
934
935 let contours = find_contours(
936 &image.view(),
937 RetrievalMode::Tree,
938 ApproximationMethod::None,
939 )
940 .expect("find_contours should succeed for image with hole");
941
942 assert!(!contours.is_empty());
944 }
945
946 #[test]
947 fn test_contour_area() {
948 let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
950 let contour = Contour::new(points);
951
952 let area = contour.area();
953 assert!((area - 100.0).abs() < 1.0);
954 }
955
956 #[test]
957 fn test_contour_perimeter() {
958 let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
960 let contour = Contour::new(points);
961
962 let perimeter = contour.perimeter();
963 assert!((perimeter - 40.0).abs() < 1.0);
964 }
965
966 #[test]
967 fn test_point_in_contour() {
968 let points = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
969 let contour = Contour::new(points);
970
971 assert!(point_in_contour((5, 5), &contour));
972 assert!(!point_in_contour((15, 15), &contour));
973 }
974
975 #[test]
976 fn test_empty_image() {
977 let image = Array2::<u8>::zeros((10, 10));
978
979 let contours = find_contours(
980 &image.view(),
981 RetrievalMode::List,
982 ApproximationMethod::Simple,
983 )
984 .expect("find_contours should succeed for empty image");
985
986 assert!(contours.is_empty());
987 }
988}