1use crate::error::{NdimageError, NdimageResult};
18use scirs2_core::ndarray::Array2;
19use scirs2_core::numeric::{Float, NumAssign};
20use std::cmp::Ordering;
21use std::collections::{BinaryHeap, HashMap};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum WatershedNeighborhood {
30 Conn4,
32 Conn8,
34}
35
36impl Default for WatershedNeighborhood {
37 fn default() -> Self {
38 WatershedNeighborhood::Conn8
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct CompactWatershedConfig {
45 pub connectivity: WatershedNeighborhood,
47 pub compactness: f64,
50 pub watershed_line: bool,
52}
53
54impl Default for CompactWatershedConfig {
55 fn default() -> Self {
56 CompactWatershedConfig {
57 connectivity: WatershedNeighborhood::Conn8,
58 compactness: 0.0,
59 watershed_line: false,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct OversegmentationConfig {
67 pub min_region_area: usize,
69 pub h_minima: f64,
72 pub max_regions: usize,
74 pub connectivity: WatershedNeighborhood,
76}
77
78impl Default for OversegmentationConfig {
79 fn default() -> Self {
80 OversegmentationConfig {
81 min_region_area: 1,
82 h_minima: 0.0,
83 max_regions: 0,
84 connectivity: WatershedNeighborhood::Conn8,
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct DamResult {
92 pub labels: Array2<i32>,
94 pub dam_mask: Array2<bool>,
96 pub num_regions: usize,
98}
99
100const WSHED: i32 = -1;
106const IN_QUEUE: i32 = -2;
108
109fn neighbor_offsets(conn: WatershedNeighborhood) -> &'static [(isize, isize)] {
110 match conn {
111 WatershedNeighborhood::Conn4 => &[(-1, 0), (0, -1), (0, 1), (1, 0)],
112 WatershedNeighborhood::Conn8 => &[
113 (-1, -1),
114 (-1, 0),
115 (-1, 1),
116 (0, -1),
117 (0, 1),
118 (1, -1),
119 (1, 0),
120 (1, 1),
121 ],
122 }
123}
124
125#[inline]
126fn in_bounds(r: isize, c: isize, rows: usize, cols: usize) -> bool {
127 r >= 0 && r < rows as isize && c >= 0 && c < cols as isize
128}
129
130#[derive(Clone, Debug)]
132struct CompactEntry {
133 row: usize,
134 col: usize,
135 intensity: f64,
137 distance: f64,
139 priority: f64,
141 order: u64,
143}
144
145impl PartialEq for CompactEntry {
146 fn eq(&self, other: &Self) -> bool {
147 self.row == other.row && self.col == other.col
148 }
149}
150impl Eq for CompactEntry {}
151
152impl PartialOrd for CompactEntry {
153 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
154 Some(self.cmp(other))
155 }
156}
157
158impl Ord for CompactEntry {
159 fn cmp(&self, other: &Self) -> Ordering {
160 match other
162 .priority
163 .partial_cmp(&self.priority)
164 .unwrap_or(Ordering::Equal)
165 {
166 Ordering::Equal => other.order.cmp(&self.order),
167 ord => ord,
168 }
169 }
170}
171
172pub fn compact_watershed<T>(
221 image: &Array2<T>,
222 markers: &Array2<i32>,
223 config: &CompactWatershedConfig,
224) -> NdimageResult<Array2<i32>>
225where
226 T: Float + NumAssign + std::fmt::Debug + 'static,
227{
228 if image.shape() != markers.shape() {
229 return Err(NdimageError::DimensionError(
230 "Image and markers must have the same shape".to_string(),
231 ));
232 }
233
234 let rows = image.nrows();
235 let cols = image.ncols();
236
237 if rows == 0 || cols == 0 {
238 return Ok(markers.clone());
239 }
240
241 let offsets = neighbor_offsets(config.connectivity);
242 let compact = config.compactness;
243
244 let mut output = markers.clone();
246
247 let mut dist = Array2::<f64>::from_elem((rows, cols), f64::INFINITY);
249
250 let mut seed_centers: HashMap<i32, (f64, f64)> = HashMap::new();
252 let mut seed_counts: HashMap<i32, usize> = HashMap::new();
253
254 for r in 0..rows {
255 for c in 0..cols {
256 let lbl = markers[[r, c]];
257 if lbl > 0 {
258 let entry = seed_centers.entry(lbl).or_insert((0.0, 0.0));
259 entry.0 += r as f64;
260 entry.1 += c as f64;
261 *seed_counts.entry(lbl).or_insert(0) += 1;
262 }
263 }
264 }
265
266 for (lbl, center) in seed_centers.iter_mut() {
267 let cnt = *seed_counts.get(lbl).unwrap_or(&1) as f64;
268 center.0 /= cnt;
269 center.1 /= cnt;
270 }
271
272 let mut queue = BinaryHeap::new();
274 let mut insertion_order: u64 = 0;
275
276 for r in 0..rows {
277 for c in 0..cols {
278 let lbl = markers[[r, c]];
279 if lbl > 0 {
280 dist[[r, c]] = 0.0;
281
282 for &(dr, dc) in offsets {
283 let nr = r as isize + dr;
284 let nc = c as isize + dc;
285 if in_bounds(nr, nc, rows, cols) {
286 let nr = nr as usize;
287 let nc = nc as usize;
288 if output[[nr, nc]] == 0 {
289 output[[nr, nc]] = IN_QUEUE;
290 let intensity = image[[nr, nc]].to_f64().unwrap_or(f64::INFINITY);
291 let spatial_dist = if let Some(¢er) = seed_centers.get(&lbl) {
292 ((nr as f64 - center.0).powi(2) + (nc as f64 - center.1).powi(2))
293 .sqrt()
294 } else {
295 0.0
296 };
297 let priority = intensity + compact * spatial_dist;
298
299 queue.push(CompactEntry {
300 row: nr,
301 col: nc,
302 intensity,
303 distance: spatial_dist,
304 priority,
305 order: insertion_order,
306 });
307 insertion_order += 1;
308 }
309 }
310 }
311 }
312 }
313 }
314
315 while let Some(entry) = queue.pop() {
317 let r = entry.row;
318 let c = entry.col;
319
320 let mut neighbor_labels: HashMap<i32, (usize, f64)> = HashMap::new(); let mut _has_wshed_neighbor = false;
323
324 for &(dr, dc) in offsets {
325 let nr = r as isize + dr;
326 let nc = c as isize + dc;
327 if in_bounds(nr, nc, rows, cols) {
328 let nr = nr as usize;
329 let nc = nc as usize;
330 let nlbl = output[[nr, nc]];
331 if nlbl > 0 {
332 let nd = dist[[nr, nc]];
333 let e = neighbor_labels.entry(nlbl).or_insert((0, f64::INFINITY));
334 e.0 += 1;
335 if nd < e.1 {
336 e.1 = nd;
337 }
338 } else if nlbl == WSHED && config.watershed_line {
339 _has_wshed_neighbor = true;
340 }
341 }
342 }
343
344 if neighbor_labels.is_empty() {
345 output[[r, c]] = 0;
346 continue;
347 }
348
349 let distinct: Vec<i32> = neighbor_labels.keys().copied().collect();
350
351 let assigned_label;
352 if config.watershed_line && distinct.len() > 1 {
353 output[[r, c]] = WSHED;
355 assigned_label = WSHED;
356 } else {
357 let best_label = if compact > 0.0 {
359 let mut best = distinct[0];
361 let mut best_cost = f64::INFINITY;
362 for &lbl in &distinct {
363 let spatial = if let Some(¢er) = seed_centers.get(&lbl) {
364 ((r as f64 - center.0).powi(2) + (c as f64 - center.1).powi(2)).sqrt()
365 } else {
366 0.0
367 };
368 let cost = entry.intensity + compact * spatial;
369 if cost < best_cost {
370 best_cost = cost;
371 best = lbl;
372 }
373 }
374 best
375 } else {
376 neighbor_labels
378 .iter()
379 .max_by_key(|&(_, (count, _))| count)
380 .map(|(&lbl, _)| lbl)
381 .unwrap_or(0)
382 };
383
384 if best_label > 0 {
385 output[[r, c]] = best_label;
386 assigned_label = best_label;
387 let spatial = if let Some(¢er) = seed_centers.get(&best_label) {
389 ((r as f64 - center.0).powi(2) + (c as f64 - center.1).powi(2)).sqrt()
390 } else {
391 0.0
392 };
393 dist[[r, c]] = spatial;
394 } else {
395 output[[r, c]] = 0;
396 continue;
397 }
398 }
399
400 for &(dr, dc) in offsets {
402 let nr = r as isize + dr;
403 let nc = c as isize + dc;
404 if in_bounds(nr, nc, rows, cols) {
405 let nr = nr as usize;
406 let nc = nc as usize;
407 if output[[nr, nc]] == 0 {
408 output[[nr, nc]] = IN_QUEUE;
409 let intensity = image[[nr, nc]].to_f64().unwrap_or(f64::INFINITY);
410 let spatial_dist = if assigned_label > 0 {
411 if let Some(¢er) = seed_centers.get(&assigned_label) {
412 ((nr as f64 - center.0).powi(2) + (nc as f64 - center.1).powi(2)).sqrt()
413 } else {
414 0.0
415 }
416 } else {
417 0.0
418 };
419 let priority = intensity + compact * spatial_dist;
420
421 queue.push(CompactEntry {
422 row: nr,
423 col: nc,
424 intensity,
425 distance: spatial_dist,
426 priority,
427 order: insertion_order,
428 });
429 insertion_order += 1;
430 }
431 }
432 }
433 }
434
435 for val in output.iter_mut() {
437 if *val == IN_QUEUE {
438 *val = 0;
439 }
440 }
441
442 Ok(output)
443}
444
445pub fn extract_dams(
477 labels: &Array2<i32>,
478 connectivity: WatershedNeighborhood,
479) -> NdimageResult<DamResult> {
480 let rows = labels.nrows();
481 let cols = labels.ncols();
482
483 if rows == 0 || cols == 0 {
484 return Ok(DamResult {
485 labels: labels.clone(),
486 dam_mask: Array2::from_elem((rows, cols), false),
487 num_regions: 0,
488 });
489 }
490
491 let offsets = neighbor_offsets(connectivity);
492 let mut dam_mask = Array2::from_elem((rows, cols), false);
493 let mut out = labels.clone();
494 let mut unique_labels = std::collections::HashSet::new();
495
496 for r in 0..rows {
497 for c in 0..cols {
498 let lbl = labels[[r, c]];
499 if lbl <= 0 {
500 continue;
501 }
502 unique_labels.insert(lbl);
503
504 let mut is_dam = false;
505 for &(dr, dc) in offsets {
506 let nr = r as isize + dr;
507 let nc = c as isize + dc;
508 if in_bounds(nr, nc, rows, cols) {
509 let nlbl = labels[[nr as usize, nc as usize]];
510 if nlbl > 0 && nlbl != lbl {
511 is_dam = true;
512 break;
513 }
514 }
515 }
516
517 if is_dam {
518 dam_mask[[r, c]] = true;
519 out[[r, c]] = WSHED;
520 }
521 }
522 }
523
524 Ok(DamResult {
525 labels: out,
526 dam_mask,
527 num_regions: unique_labels.len(),
528 })
529}
530
531pub fn merge_small_regions<T>(
577 image: &Array2<T>,
578 labels: &Array2<i32>,
579 config: &OversegmentationConfig,
580) -> NdimageResult<Array2<i32>>
581where
582 T: Float + NumAssign + std::fmt::Debug + 'static,
583{
584 if image.shape() != labels.shape() {
585 return Err(NdimageError::DimensionError(
586 "Image and labels must have the same shape".to_string(),
587 ));
588 }
589
590 let rows = image.nrows();
591 let cols = image.ncols();
592
593 if rows == 0 || cols == 0 {
594 return Ok(labels.clone());
595 }
596
597 let mut region_area: HashMap<i32, usize> = HashMap::new();
599 let mut region_sum: HashMap<i32, f64> = HashMap::new();
600
601 for r in 0..rows {
602 for c in 0..cols {
603 let lbl = labels[[r, c]];
604 if lbl <= 0 {
605 continue;
606 }
607 *region_area.entry(lbl).or_insert(0) += 1;
608 let val = image[[r, c]].to_f64().unwrap_or(0.0);
609 *region_sum.entry(lbl).or_insert(0.0) += val;
610 }
611 }
612
613 let offsets = neighbor_offsets(config.connectivity);
615 let mut adjacency: HashMap<i32, HashMap<i32, usize>> = HashMap::new(); for r in 0..rows {
618 for c in 0..cols {
619 let lbl = labels[[r, c]];
620 if lbl <= 0 {
621 continue;
622 }
623 for &(dr, dc) in offsets {
624 let nr = r as isize + dr;
625 let nc = c as isize + dc;
626 if in_bounds(nr, nc, rows, cols) {
627 let nlbl = labels[[nr as usize, nc as usize]];
628 if nlbl > 0 && nlbl != lbl {
629 *adjacency.entry(lbl).or_default().entry(nlbl).or_insert(0) += 1;
630 }
631 }
632 }
633 }
634 }
635
636 let mut merge_map: HashMap<i32, i32> = HashMap::new();
638
639 let mut regions_by_area: Vec<(i32, usize)> =
641 region_area.iter().map(|(&k, &v)| (k, v)).collect();
642 regions_by_area.sort_by_key(|&(_, area)| area);
643
644 for &(lbl, area) in ®ions_by_area {
645 if area >= config.min_region_area {
646 continue; }
648
649 let my_mean = region_sum.get(&lbl).copied().unwrap_or(0.0) / (area.max(1) as f64);
651
652 let best_neighbor = if let Some(neighbors) = adjacency.get(&lbl) {
653 let mut best: Option<(i32, f64)> = None;
654 for (&nlbl, _) in neighbors {
655 let mut final_lbl = nlbl;
657 while let Some(&merged_to) = merge_map.get(&final_lbl) {
658 if merged_to == final_lbl {
659 break;
660 }
661 final_lbl = merged_to;
662 }
663
664 if final_lbl == lbl {
665 continue; }
667
668 let n_area = region_area.get(&final_lbl).copied().unwrap_or(1);
669 let n_mean =
670 region_sum.get(&final_lbl).copied().unwrap_or(0.0) / (n_area.max(1) as f64);
671 let diff = (my_mean - n_mean).abs();
672
673 match best {
674 None => best = Some((final_lbl, diff)),
675 Some((_, best_diff)) if diff < best_diff => {
676 best = Some((final_lbl, diff));
677 }
678 _ => {}
679 }
680 }
681 best.map(|(lbl, _)| lbl)
682 } else {
683 None
684 };
685
686 if let Some(target) = best_neighbor {
687 merge_map.insert(lbl, target);
688 let my_sum = region_sum.get(&lbl).copied().unwrap_or(0.0);
690 *region_sum.entry(target).or_insert(0.0) += my_sum;
691 *region_area.entry(target).or_insert(0) += area;
692 }
693 }
694
695 let all_labels: Vec<i32> = merge_map.keys().copied().collect();
697 for lbl in all_labels {
698 let mut target = lbl;
699 let mut visited = std::collections::HashSet::new();
700 while let Some(&next) = merge_map.get(&target) {
701 if next == target || visited.contains(&next) {
702 break;
703 }
704 visited.insert(target);
705 target = next;
706 }
707 merge_map.insert(lbl, target);
708 }
709
710 let mut output = labels.clone();
712 for r in 0..rows {
713 for c in 0..cols {
714 let lbl = output[[r, c]];
715 if lbl > 0 {
716 if let Some(&target) = merge_map.get(&lbl) {
717 output[[r, c]] = target;
718 }
719 }
720 }
721 }
722
723 if config.max_regions > 0 {
725 let mut unique: std::collections::HashSet<i32> = std::collections::HashSet::new();
726 for &v in output.iter() {
727 if v > 0 {
728 unique.insert(v);
729 }
730 }
731
732 if unique.len() > config.max_regions {
733 let mut remaining = unique.len();
735 while remaining > config.max_regions {
736 let mut areas: HashMap<i32, usize> = HashMap::new();
738 for &v in output.iter() {
739 if v > 0 {
740 *areas.entry(v).or_insert(0) += 1;
741 }
742 }
743
744 let smallest = areas
746 .iter()
747 .min_by_key(|&(_, &area)| area)
748 .map(|(&lbl, _)| lbl);
749
750 if let Some(small_lbl) = smallest {
751 let mut neighbor_boundary: HashMap<i32, usize> = HashMap::new();
753 for r in 0..rows {
754 for c in 0..cols {
755 if output[[r, c]] != small_lbl {
756 continue;
757 }
758 for &(dr, dc) in offsets {
759 let nr = r as isize + dr;
760 let nc = c as isize + dc;
761 if in_bounds(nr, nc, rows, cols) {
762 let nlbl = output[[nr as usize, nc as usize]];
763 if nlbl > 0 && nlbl != small_lbl {
764 *neighbor_boundary.entry(nlbl).or_insert(0) += 1;
765 }
766 }
767 }
768 }
769 }
770
771 let best = neighbor_boundary
773 .iter()
774 .max_by_key(|&(_, &count)| count)
775 .map(|(&lbl, _)| lbl);
776
777 if let Some(target) = best {
778 for val in output.iter_mut() {
779 if *val == small_lbl {
780 *val = target;
781 }
782 }
783 remaining -= 1;
784 } else {
785 break; }
787 } else {
788 break;
789 }
790 }
791 }
792 }
793
794 Ok(output)
795}
796
797pub fn h_minima_transform<T>(image: &Array2<T>, h: f64) -> NdimageResult<Array2<f64>>
829where
830 T: Float + NumAssign + std::fmt::Debug + 'static,
831{
832 if h < 0.0 {
833 return Err(NdimageError::InvalidInput(
834 "h must be non-negative".to_string(),
835 ));
836 }
837
838 let rows = image.nrows();
839 let cols = image.ncols();
840
841 if rows == 0 || cols == 0 {
842 return Ok(Array2::zeros((rows, cols)));
843 }
844
845 let img_f64: Array2<f64> = image.mapv(|x| x.to_f64().unwrap_or(0.0));
847
848 let marker: Array2<f64> = img_f64.mapv(|x| x - h);
850
851 let result = morphological_reconstruction_by_dilation_2d(&marker, &img_f64, 200)?;
854
855 Ok(result)
856}
857
858#[allow(dead_code)]
863fn morphological_reconstruction_by_erosion_2d(
864 marker: &Array2<f64>,
865 mask: &Array2<f64>,
866 max_iterations: usize,
867) -> NdimageResult<Array2<f64>> {
868 let rows = marker.nrows();
869 let cols = marker.ncols();
870
871 let mut result = marker.clone();
872
873 for _iter in 0..max_iterations {
875 let mut changed = false;
876
877 for r in 0..rows {
879 for c in 0..cols {
880 let mut val = result[[r, c]];
881 if r > 0 && result[[r - 1, c]] < val {
883 val = result[[r - 1, c]];
884 }
885 if c > 0 && result[[r, c - 1]] < val {
886 val = result[[r, c - 1]];
887 }
888 let mask_val = mask[[r, c]];
890 if val < mask_val {
891 val = mask_val;
892 }
893 if (val - result[[r, c]]).abs() > 1e-15 {
894 result[[r, c]] = val;
895 changed = true;
896 }
897 }
898 }
899
900 for r in (0..rows).rev() {
902 for c in (0..cols).rev() {
903 let mut val = result[[r, c]];
904 if r + 1 < rows && result[[r + 1, c]] < val {
905 val = result[[r + 1, c]];
906 }
907 if c + 1 < cols && result[[r, c + 1]] < val {
908 val = result[[r, c + 1]];
909 }
910 let mask_val = mask[[r, c]];
911 if val < mask_val {
912 val = mask_val;
913 }
914 if (val - result[[r, c]]).abs() > 1e-15 {
915 result[[r, c]] = val;
916 changed = true;
917 }
918 }
919 }
920
921 if !changed {
922 break;
923 }
924 }
925
926 Ok(result)
927}
928
929fn morphological_reconstruction_by_dilation_2d(
938 marker: &Array2<f64>,
939 mask: &Array2<f64>,
940 max_iterations: usize,
941) -> NdimageResult<Array2<f64>> {
942 let rows = marker.nrows();
943 let cols = marker.ncols();
944
945 let mut result = Array2::zeros((rows, cols));
947 for r in 0..rows {
948 for c in 0..cols {
949 result[[r, c]] = marker[[r, c]].min(mask[[r, c]]);
950 }
951 }
952
953 for _iter in 0..max_iterations {
955 let mut changed = false;
956
957 for r in 0..rows {
959 for c in 0..cols {
960 let mut val = result[[r, c]];
961 if r > 0 && result[[r - 1, c]] > val {
963 val = result[[r - 1, c]];
964 }
965 if c > 0 && result[[r, c - 1]] > val {
966 val = result[[r, c - 1]];
967 }
968 if r > 0 && c > 0 && result[[r - 1, c - 1]] > val {
970 val = result[[r - 1, c - 1]];
971 }
972 if r > 0 && c + 1 < cols && result[[r - 1, c + 1]] > val {
973 val = result[[r - 1, c + 1]];
974 }
975 let mask_val = mask[[r, c]];
977 if val > mask_val {
978 val = mask_val;
979 }
980 if (val - result[[r, c]]).abs() > 1e-15 {
981 result[[r, c]] = val;
982 changed = true;
983 }
984 }
985 }
986
987 for r in (0..rows).rev() {
989 for c in (0..cols).rev() {
990 let mut val = result[[r, c]];
991 if r + 1 < rows && result[[r + 1, c]] > val {
992 val = result[[r + 1, c]];
993 }
994 if c + 1 < cols && result[[r, c + 1]] > val {
995 val = result[[r, c + 1]];
996 }
997 if r + 1 < rows && c + 1 < cols && result[[r + 1, c + 1]] > val {
999 val = result[[r + 1, c + 1]];
1000 }
1001 if r + 1 < rows && c > 0 && result[[r + 1, c - 1]] > val {
1002 val = result[[r + 1, c - 1]];
1003 }
1004 let mask_val = mask[[r, c]];
1005 if val > mask_val {
1006 val = mask_val;
1007 }
1008 if (val - result[[r, c]]).abs() > 1e-15 {
1009 result[[r, c]] = val;
1010 changed = true;
1011 }
1012 }
1013 }
1014
1015 if !changed {
1016 break;
1017 }
1018 }
1019
1020 Ok(result)
1021}
1022
1023pub fn auto_markers<T>(
1056 image: &Array2<T>,
1057 connectivity: WatershedNeighborhood,
1058 threshold: Option<f64>,
1059) -> NdimageResult<Array2<i32>>
1060where
1061 T: Float + NumAssign + std::fmt::Debug + 'static,
1062{
1063 let rows = image.nrows();
1064 let cols = image.ncols();
1065
1066 if rows == 0 || cols == 0 {
1067 return Ok(Array2::zeros((rows, cols)));
1068 }
1069
1070 let offsets = neighbor_offsets(connectivity);
1071 let thresh = threshold.unwrap_or(0.0);
1072
1073 let mut markers = Array2::<i32>::zeros((rows, cols));
1074 let mut next_label = 1i32;
1075
1076 for r in 0..rows {
1077 for c in 0..cols {
1078 let val = image[[r, c]].to_f64().unwrap_or(f64::INFINITY);
1079
1080 let mut is_minimum = true;
1081 let mut min_neighbor = f64::INFINITY;
1082
1083 for &(dr, dc) in offsets {
1084 let nr = r as isize + dr;
1085 let nc = c as isize + dc;
1086 if in_bounds(nr, nc, rows, cols) {
1087 let nval = image[[nr as usize, nc as usize]]
1088 .to_f64()
1089 .unwrap_or(f64::INFINITY);
1090 if nval <= val {
1091 is_minimum = false;
1092 break;
1093 }
1094 if nval < min_neighbor {
1095 min_neighbor = nval;
1096 }
1097 }
1098 }
1099
1100 if is_minimum {
1101 let depth = if min_neighbor.is_finite() {
1103 min_neighbor - val
1104 } else {
1105 f64::INFINITY
1106 };
1107
1108 if depth >= thresh {
1109 markers[[r, c]] = next_label;
1110 next_label += 1;
1111 }
1112 }
1113 }
1114 }
1115
1116 Ok(markers)
1117}
1118
1119#[cfg(test)]
1124mod tests {
1125 use super::*;
1126 use scirs2_core::ndarray::array;
1127
1128 #[test]
1129 fn test_compact_watershed_basic() {
1130 let image = array![
1131 [0.1, 0.2, 0.9, 0.2, 0.1],
1132 [0.1, 0.1, 0.9, 0.1, 0.1],
1133 [0.1, 0.2, 0.9, 0.2, 0.1],
1134 ];
1135 let markers = array![[0, 0, 0, 0, 0], [1, 0, 0, 0, 2], [0, 0, 0, 0, 0],];
1136
1137 let config = CompactWatershedConfig::default();
1138 let result = compact_watershed(&image, &markers, &config).expect("should succeed");
1139
1140 assert_eq!(result[[1, 0]], 1);
1141 assert_eq!(result[[1, 4]], 2);
1142 for &v in result.iter() {
1144 assert!(v > 0);
1145 }
1146 }
1147
1148 #[test]
1149 fn test_compact_watershed_with_compactness() {
1150 let image = array![
1151 [0.1, 0.1, 0.5, 0.1, 0.1],
1152 [0.1, 0.1, 0.5, 0.1, 0.1],
1153 [0.1, 0.1, 0.5, 0.1, 0.1],
1154 ];
1155 let markers = array![[0, 0, 0, 0, 0], [1, 0, 0, 0, 2], [0, 0, 0, 0, 0],];
1156
1157 let config = CompactWatershedConfig {
1158 compactness: 1.0,
1159 ..Default::default()
1160 };
1161
1162 let result = compact_watershed(&image, &markers, &config).expect("should succeed");
1163 assert_eq!(result[[1, 0]], 1);
1164 assert_eq!(result[[1, 4]], 2);
1165 }
1166
1167 #[test]
1168 fn test_compact_watershed_shape_mismatch() {
1169 let image = array![[0.1, 0.2], [0.3, 0.4]];
1170 let markers = array![[0, 0, 0], [1, 0, 2]];
1171
1172 let config = CompactWatershedConfig::default();
1173 let result = compact_watershed(&image, &markers, &config);
1174 assert!(result.is_err());
1175 }
1176
1177 #[test]
1178 fn test_compact_watershed_single_marker() {
1179 let image = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1180 let markers = array![[1, 0, 0], [0, 0, 0], [0, 0, 0]];
1181
1182 let config = CompactWatershedConfig::default();
1183 let result = compact_watershed(&image, &markers, &config).expect("should succeed");
1184 for &v in result.iter() {
1185 assert_eq!(v, 1);
1186 }
1187 }
1188
1189 #[test]
1190 fn test_compact_watershed_watershed_line() {
1191 let image = array![
1192 [1.0, 2.0, 9.0, 2.0, 1.0],
1193 [1.0, 2.0, 9.0, 2.0, 1.0],
1194 [1.0, 2.0, 9.0, 2.0, 1.0],
1195 ];
1196 let markers = array![[1, 0, 0, 0, 2], [0, 0, 0, 0, 0], [1, 0, 0, 0, 2],];
1197
1198 let config = CompactWatershedConfig {
1199 watershed_line: true,
1200 connectivity: WatershedNeighborhood::Conn4,
1201 ..Default::default()
1202 };
1203
1204 let result = compact_watershed(&image, &markers, &config).expect("should succeed");
1205 assert_eq!(result[[0, 0]], 1);
1207 assert_eq!(result[[0, 4]], 2);
1208 }
1209
1210 #[test]
1211 fn test_compact_watershed_empty() {
1212 let image: Array2<f64> = Array2::zeros((0, 0));
1213 let markers: Array2<i32> = Array2::zeros((0, 0));
1214 let config = CompactWatershedConfig::default();
1215 let result = compact_watershed(&image, &markers, &config).expect("empty should succeed");
1216 assert_eq!(result.len(), 0);
1217 }
1218
1219 #[test]
1220 fn test_extract_dams_basic() {
1221 let labels = array![[1, 1, 2, 2], [1, 1, 2, 2], [3, 3, 4, 4], [3, 3, 4, 4],];
1222
1223 let result = extract_dams(&labels, WatershedNeighborhood::Conn4).expect("should succeed");
1224
1225 assert!(result.num_regions >= 2);
1228
1229 assert!(!result.dam_mask[[0, 0]]);
1231 assert!(!result.dam_mask[[3, 3]]);
1232 }
1233
1234 #[test]
1235 fn test_extract_dams_single_region() {
1236 let labels = Array2::from_elem((4, 4), 1i32);
1237 let result = extract_dams(&labels, WatershedNeighborhood::Conn8).expect("should succeed");
1238 for &v in result.dam_mask.iter() {
1240 assert!(!v);
1241 }
1242 assert_eq!(result.num_regions, 1);
1243 }
1244
1245 #[test]
1246 fn test_merge_small_regions_basic() {
1247 let image = array![
1248 [0.1, 0.1, 0.9, 0.9],
1249 [0.1, 0.1, 0.9, 0.9],
1250 [0.1, 0.1, 0.9, 0.9],
1251 [0.1, 0.1, 0.9, 0.9],
1252 ];
1253
1254 let labels = array![
1255 [1, 1, 2, 2],
1256 [1, 3, 2, 2], [1, 1, 2, 2],
1258 [1, 1, 2, 2],
1259 ];
1260
1261 let config = OversegmentationConfig {
1262 min_region_area: 3,
1263 ..Default::default()
1264 };
1265
1266 let result = merge_small_regions(&image, &labels, &config).expect("should succeed");
1267
1268 assert_ne!(result[[1, 1]], 3);
1270 assert_eq!(result[[1, 1]], 1);
1272 }
1273
1274 #[test]
1275 fn test_merge_small_regions_max_regions() {
1276 let image = Array2::<f64>::from_elem((6, 6), 0.5);
1277
1278 let mut labels = Array2::<i32>::zeros((6, 6));
1279 for r in 0..3 {
1281 for c in 0..3 {
1282 labels[[r, c]] = 1;
1283 }
1284 }
1285 for r in 0..3 {
1286 for c in 3..6 {
1287 labels[[r, c]] = 2;
1288 }
1289 }
1290 for r in 3..6 {
1291 for c in 0..3 {
1292 labels[[r, c]] = 3;
1293 }
1294 }
1295 for r in 3..6 {
1296 for c in 3..6 {
1297 labels[[r, c]] = 4;
1298 }
1299 }
1300
1301 let config = OversegmentationConfig {
1302 min_region_area: 1,
1303 max_regions: 2,
1304 ..Default::default()
1305 };
1306
1307 let result = merge_small_regions(&image, &labels, &config).expect("should succeed");
1308
1309 let mut unique = std::collections::HashSet::new();
1311 for &v in result.iter() {
1312 if v > 0 {
1313 unique.insert(v);
1314 }
1315 }
1316 assert!(
1317 unique.len() <= 2,
1318 "Expected <= 2 regions, got {}",
1319 unique.len()
1320 );
1321 }
1322
1323 #[test]
1324 fn test_h_minima_transform() {
1325 let image = array![
1334 [5.0, 5.0, 5.0, 5.0, 5.0],
1335 [5.0, 3.0, 5.0, 1.0, 5.0],
1336 [5.0, 5.0, 5.0, 5.0, 5.0],
1337 ];
1338 let result = h_minima_transform(&image, 1.5).expect("should succeed");
1349
1350 assert!((result[[0, 0]] - 3.5).abs() < 1e-10);
1352 assert!(result[[1, 1]] < result[[0, 0]]); assert!(result[[1, 3]] < result[[0, 0]]); let result2 = h_minima_transform(&image, 3.0).expect("should succeed");
1367
1368 let plateau_level = result2[[0, 0]];
1370 assert!((plateau_level - 2.0).abs() < 1e-10);
1371
1372 assert!(
1374 (result2[[1, 1]] - plateau_level).abs() < 1e-10,
1375 "expected (1,1) to be suppressed to plateau {}, got {}",
1376 plateau_level,
1377 result2[[1, 1]]
1378 );
1379
1380 assert!(
1383 result2[[1, 3]] < plateau_level,
1384 "expected (1,3) to still be a minimum (below plateau {}), got {}",
1385 plateau_level,
1386 result2[[1, 3]]
1387 );
1388 assert!((result2[[1, 3]] - 1.0).abs() < 1e-10);
1389 }
1390
1391 #[test]
1392 fn test_auto_markers_basic() {
1393 let image = array![
1394 [5.0, 5.0, 5.0, 5.0, 5.0],
1395 [5.0, 1.0, 5.0, 2.0, 5.0],
1396 [5.0, 5.0, 5.0, 5.0, 5.0],
1397 ];
1398
1399 let markers =
1400 auto_markers(&image, WatershedNeighborhood::Conn4, None).expect("should succeed");
1401
1402 assert!(markers[[1, 1]] > 0);
1404 assert!(markers[[1, 3]] > 0);
1405 assert_ne!(markers[[1, 1]], markers[[1, 3]]);
1406
1407 assert_eq!(markers[[0, 0]], 0);
1409 }
1410
1411 #[test]
1412 fn test_auto_markers_with_threshold() {
1413 let image = array![
1414 [5.0, 5.0, 5.0, 5.0, 5.0],
1415 [5.0, 4.5, 5.0, 1.0, 5.0],
1416 [5.0, 5.0, 5.0, 5.0, 5.0],
1417 ];
1418
1419 let markers =
1421 auto_markers(&image, WatershedNeighborhood::Conn4, Some(1.0)).expect("should succeed");
1422
1423 assert_eq!(markers[[1, 1]], 0);
1425 assert!(markers[[1, 3]] > 0);
1427 }
1428
1429 #[test]
1430 fn test_auto_markers_empty() {
1431 let image: Array2<f64> = Array2::zeros((0, 0));
1432 let markers =
1433 auto_markers(&image, WatershedNeighborhood::Conn4, None).expect("empty should succeed");
1434 assert_eq!(markers.len(), 0);
1435 }
1436
1437 #[test]
1438 fn test_compact_watershed_conn4() {
1439 let image = array![[0.1, 0.9, 0.1], [0.9, 0.9, 0.9], [0.1, 0.9, 0.1],];
1440 let markers = array![[1, 0, 2], [0, 0, 0], [3, 0, 4],];
1441
1442 let config = CompactWatershedConfig {
1443 connectivity: WatershedNeighborhood::Conn4,
1444 compactness: 0.0,
1445 watershed_line: false,
1446 };
1447
1448 let result = compact_watershed(&image, &markers, &config).expect("should succeed");
1449 assert_eq!(result[[0, 0]], 1);
1450 assert_eq!(result[[0, 2]], 2);
1451 assert_eq!(result[[2, 0]], 3);
1452 assert_eq!(result[[2, 2]], 4);
1453 }
1454}