Skip to main content

scirs2_ndimage/morphology/
connected.rs

1//! Connected component operations for binary and labeled arrays
2
3use scirs2_core::ndarray::{Array, Dimension, IxDyn};
4use std::collections::HashMap;
5
6use super::Connectivity;
7use crate::error::{NdimageError, NdimageResult};
8
9/// Union-Find data structure for connected component labeling
10struct UnionFind {
11    parent: Vec<usize>,
12    rank: Vec<usize>,
13}
14
15impl UnionFind {
16    fn new(size: usize) -> Self {
17        UnionFind {
18            parent: (0..size).collect(),
19            rank: vec![0; size],
20        }
21    }
22
23    fn find(&mut self, x: usize) -> usize {
24        if self.parent[x] != x {
25            self.parent[x] = self.find(self.parent[x]); // Path compression
26        }
27        self.parent[x]
28    }
29
30    fn union(&mut self, x: usize, y: usize) {
31        let root_x = self.find(x);
32        let root_y = self.find(y);
33
34        if root_x != root_y {
35            // Union by rank
36            if self.rank[root_x] < self.rank[root_y] {
37                self.parent[root_x] = root_y;
38            } else if self.rank[root_x] > self.rank[root_y] {
39                self.parent[root_y] = root_x;
40            } else {
41                self.parent[root_y] = root_x;
42                self.rank[root_x] += 1;
43            }
44        }
45    }
46
47    fn get_component_mapping(&mut self) -> HashMap<usize, usize> {
48        let mut mapping = HashMap::new();
49        let mut next_label = 1;
50
51        for i in 0..self.parent.len() {
52            let root = self.find(i);
53            if !mapping.contains_key(&root) {
54                mapping.insert(root, next_label);
55                next_label += 1;
56            }
57        }
58
59        mapping
60    }
61}
62
63/// Get neighbors for a given position based on connectivity
64#[allow(dead_code)]
65fn get_neighbors(
66    position: &[usize],
67    shape: &[usize],
68    connectivity: Connectivity,
69) -> Vec<Vec<usize>> {
70    let ndim = position.len();
71    let mut neighbors = Vec::new();
72
73    match connectivity {
74        Connectivity::Face => {
75            // Face connectivity: only share a face (4-connectivity in 2D, 6-connectivity in 3D)
76            for dim in 0..ndim {
77                // Check negative direction
78                if position[dim] > 0 {
79                    let mut neighbor = position.to_vec();
80                    neighbor[dim] -= 1;
81                    neighbors.push(neighbor);
82                }
83                // Check positive direction
84                if position[dim] + 1 < shape[dim] {
85                    let mut neighbor = position.to_vec();
86                    neighbor[dim] += 1;
87                    neighbors.push(neighbor);
88                }
89            }
90        }
91        Connectivity::FaceEdge => {
92            // Face and edge connectivity: 8-connectivity in 2D, 18-connectivity in 3D
93            let offsets = generate_face_edge_offsets(ndim);
94            for offset in offsets {
95                let mut neighbor = Vec::with_capacity(ndim);
96                let mut valid = true;
97
98                for (i, &pos) in position.iter().enumerate() {
99                    let new_pos = (pos as isize) + offset[i];
100                    if new_pos < 0 || new_pos >= shape[i] as isize {
101                        valid = false;
102                        break;
103                    }
104                    neighbor.push(new_pos as usize);
105                }
106
107                if valid && neighbor != position {
108                    neighbors.push(neighbor);
109                }
110            }
111        }
112        Connectivity::Full => {
113            // Corner connectivity: all possible neighbors (8-connectivity in 2D, 26-connectivity in 3D)
114            let offsets = generate_all_offsets(ndim);
115            for offset in offsets {
116                let mut neighbor = Vec::with_capacity(ndim);
117                let mut valid = true;
118
119                for (i, &pos) in position.iter().enumerate() {
120                    let new_pos = (pos as isize) + offset[i];
121                    if new_pos < 0 || new_pos >= shape[i] as isize {
122                        valid = false;
123                        break;
124                    }
125                    neighbor.push(new_pos as usize);
126                }
127
128                if valid && neighbor != position {
129                    neighbors.push(neighbor);
130                }
131            }
132        }
133    }
134
135    neighbors
136}
137
138/// Generate all possible offsets for corner connectivity
139#[allow(dead_code)]
140fn generate_all_offsets(ndim: usize) -> Vec<Vec<isize>> {
141    let mut offsets = Vec::new();
142    let total_combinations = 3_usize.pow(ndim as u32);
143
144    for i in 0..total_combinations {
145        let mut offset = Vec::with_capacity(ndim);
146        let mut temp = i;
147
148        for _ in 0..ndim {
149            let val = (temp % 3) as isize - 1; // -1, 0, or 1
150            offset.push(val);
151            temp /= 3;
152        }
153
154        // Skip the center point (all zeros)
155        if !offset.iter().all(|&x| x == 0) {
156            offsets.push(offset);
157        }
158    }
159
160    offsets
161}
162
163/// Generate face and edge neighbor offsets (excludes vertex neighbors in 3D+)
164#[allow(dead_code)]
165fn generate_face_edge_offsets(ndim: usize) -> Vec<Vec<isize>> {
166    let mut offsets = Vec::new();
167    let total_combinations = 3_usize.pow(ndim as u32);
168
169    for i in 0..total_combinations {
170        let mut offset = Vec::with_capacity(ndim);
171        let mut temp = i;
172        for _ in 0..ndim {
173            let val = (temp % 3) as isize - 1; // -1, 0, or 1
174            offset.push(val);
175            temp /= 3;
176        }
177
178        // Skip the center point (all zeros)
179        if offset.iter().all(|&x| x == 0) {
180            continue;
181        }
182
183        // For face and edge connectivity, exclude vertex neighbors
184        // Vertex neighbors have all non-zero components
185        let non_zero_count = offset.iter().filter(|&&x| x != 0).count();
186
187        // Include face neighbors (1 non-zero) and edge neighbors (2 non-zero in 3D+)
188        // In 2D, this gives 8-connectivity (same as full)
189        // In 3D, this gives 18-connectivity (excludes 8 vertex neighbors)
190        if non_zero_count <= 2 {
191            offsets.push(offset);
192        }
193    }
194
195    offsets
196}
197
198/// Convert multi-dimensional index to flat index
199#[allow(dead_code)]
200fn ravel_index(indices: &[usize], shape: &[usize]) -> usize {
201    let mut flat_index = 0;
202    let mut stride = 1;
203
204    for i in (0..indices.len()).rev() {
205        flat_index += indices[i] * stride;
206        stride *= shape[i];
207    }
208
209    flat_index
210}
211
212/// Convert flat index to multi-dimensional index
213#[allow(dead_code)]
214fn unravel_index(_flatindex: usize, shape: &[usize]) -> Vec<usize> {
215    let mut indices = vec![0; shape.len()];
216    let mut remaining = _flatindex;
217
218    // Process dimensions in forward order for row-major (C-order) layout
219    for i in 0..shape.len() {
220        let stride: usize = shape[(i + 1)..].iter().product();
221        indices[i] = remaining / stride;
222        remaining %= stride;
223    }
224
225    indices
226}
227
228/// Label connected components in a binary array
229///
230/// # Arguments
231///
232/// * `input` - Input binary array
233/// * `structure` - Structuring element (if None, uses a box with connectivity 1)
234/// * `connectivity` - Connectivity type (default: Face)
235/// * `background` - Whether to consider background as a feature (default: false)
236///
237/// # Returns
238///
239/// * `Result<(Array<usize, D>, usize)>` - Labeled array and number of labels
240#[allow(dead_code)]
241pub fn label<D>(
242    input: &Array<bool, D>,
243    structure: Option<&Array<bool, D>>,
244    connectivity: Option<Connectivity>,
245    background: Option<bool>,
246) -> NdimageResult<(Array<usize, D>, usize)>
247where
248    D: Dimension,
249{
250    // Validate inputs
251    if input.ndim() == 0 {
252        return Err(NdimageError::InvalidInput(
253            "Input array cannot be 0-dimensional".into(),
254        ));
255    }
256
257    let conn = connectivity.unwrap_or(Connectivity::Face);
258    let bg = background.unwrap_or(false);
259
260    // Structure must have same rank as input
261    if let Some(struct_elem) = structure {
262        if struct_elem.ndim() != input.ndim() {
263            return Err(NdimageError::DimensionError(format!(
264                "Structure must have same rank as input (got {} expected {})",
265                struct_elem.ndim(),
266                input.ndim()
267            )));
268        }
269    }
270
271    let shape = input.shape();
272    let total_elements: usize = shape.iter().product();
273
274    if total_elements == 0 {
275        let output = Array::<usize, D>::zeros(input.raw_dim());
276        return Ok((output, 0));
277    }
278
279    // Initialize Union-Find data structure
280    let mut uf = UnionFind::new(total_elements);
281
282    // Convert to dynamic array for easier indexing
283    let input_dyn = input.clone().into_dyn();
284
285    // First pass: scan all pixels and union adjacent foreground pixels
286    for flat_idx in 0..total_elements {
287        let indices = unravel_index(flat_idx, shape);
288        let current_pixel = input_dyn[IxDyn(&indices)];
289
290        // Only process foreground pixels (or background if bg=true)
291        if current_pixel == !bg {
292            // Get neighbors based on connectivity
293            let neighbors = get_neighbors(&indices, shape, conn);
294
295            for neighbor_indices in neighbors {
296                let neighbor_pixel = input_dyn[IxDyn(&neighbor_indices)];
297
298                // If neighbor is also foreground, union them
299                if neighbor_pixel == current_pixel {
300                    let neighbor_flat_idx = ravel_index(&neighbor_indices, shape);
301                    uf.union(flat_idx, neighbor_flat_idx);
302                }
303            }
304        }
305    }
306
307    // Get component mapping (root -> label)
308    let component_mapping = uf.get_component_mapping();
309
310    // Create output array
311    let mut output = Array::<usize, D>::zeros(input.raw_dim());
312    let mut num_labels = 0;
313
314    // Second pass: assign labels
315    let mut output_dyn = output.clone().into_dyn();
316    for flat_idx in 0..total_elements {
317        let indices = unravel_index(flat_idx, shape);
318        let pixel = input_dyn[IxDyn(&indices)];
319
320        if pixel == !bg {
321            let root = uf.find(flat_idx);
322            if let Some(&label) = component_mapping.get(&root) {
323                output_dyn[IxDyn(&indices)] = label;
324                num_labels = num_labels.max(label);
325            }
326        }
327    }
328
329    // Convert back to original dimension type
330    output = output_dyn.into_dimensionality::<D>().map_err(|_| {
331        NdimageError::DimensionError("Failed to convert back to original dimension type".into())
332    })?;
333
334    Ok((output, num_labels))
335}
336
337/// Find the boundaries of objects in a labeled array
338///
339/// # Arguments
340///
341/// * `input` - Input labeled array
342/// * `connectivity` - Connectivity type (default: Face)
343/// * `mode` - Mode for boundary detection: "inner", "outer", or "thick" (default: "outer")
344///
345/// # Returns
346///
347/// * `Result<Array<bool, D>>` - Binary array with boundaries
348#[allow(dead_code)]
349pub fn find_boundaries<D>(
350    input: &Array<usize, D>,
351    connectivity: Option<Connectivity>,
352    mode: Option<&str>,
353) -> NdimageResult<Array<bool, D>>
354where
355    D: Dimension,
356{
357    // Validate inputs
358    if input.ndim() == 0 {
359        return Err(NdimageError::InvalidInput(
360            "Input array cannot be 0-dimensional".into(),
361        ));
362    }
363
364    let conn = connectivity.unwrap_or(Connectivity::Face);
365    let mode_str = mode.unwrap_or("outer");
366
367    // Validate mode
368    if mode_str != "inner" && mode_str != "outer" && mode_str != "thick" {
369        return Err(NdimageError::InvalidInput(format!(
370            "Mode must be 'inner', 'outer', or 'thick', got '{}'",
371            mode_str
372        )));
373    }
374
375    let shape = input.shape();
376    let total_elements: usize = shape.iter().product();
377    let mut output = Array::<bool, D>::from_elem(input.raw_dim(), false);
378
379    if total_elements == 0 {
380        return Ok(output);
381    }
382
383    // Convert to dynamic arrays for easier indexing
384    let input_dyn = input.clone().into_dyn();
385    let mut output_dyn = output.clone().into_dyn();
386
387    // Scan all pixels to find boundaries
388    for flat_idx in 0..total_elements {
389        let indices = unravel_index(flat_idx, shape);
390        let current_label = input_dyn[IxDyn(&indices)];
391
392        // Skip background pixels for inner mode
393        if mode_str == "inner" && current_label == 0 {
394            continue;
395        }
396
397        // Get neighbors based on connectivity
398        let neighbors = get_neighbors(&indices, shape, conn);
399        let mut is_boundary = false;
400
401        for neighbor_indices in neighbors {
402            let neighbor_label = input_dyn[IxDyn(&neighbor_indices)];
403
404            match mode_str {
405                "inner"
406                    // Inner boundary: foreground pixels adjacent to background or different labels
407                    if current_label != 0
408                        && (neighbor_label == 0 || neighbor_label != current_label)
409                    => {
410                        is_boundary = true;
411                        break;
412                    }
413                "outer"
414                    // Outer boundary: background pixels adjacent to foreground
415                    if current_label == 0 && neighbor_label != 0 => {
416                        is_boundary = true;
417                        break;
418                    }
419                "thick"
420                    // Thick boundary: both inner and outer
421                    if current_label != neighbor_label => {
422                        is_boundary = true;
423                        break;
424                    }
425                _ => {} // Already validated above
426            }
427        }
428
429        if is_boundary {
430            output_dyn[IxDyn(&indices)] = true;
431        }
432    }
433
434    // Convert back to original dimension type
435    output = output_dyn.into_dimensionality::<D>().map_err(|_| {
436        NdimageError::DimensionError("Failed to convert back to original dimension type".into())
437    })?;
438
439    Ok(output)
440}
441
442/// Remove small objects from a labeled array
443///
444/// # Arguments
445///
446/// * `input` - Input binary array
447/// * `min_size` - Minimum size of objects to keep
448/// * `connectivity` - Connectivity type (default: Face)
449///
450/// # Returns
451///
452/// * `Result<Array<bool, D>>` - Binary array with small objects removed
453#[allow(dead_code)]
454pub fn remove_small_objects<D>(
455    input: &Array<bool, D>,
456    min_size: usize,
457    connectivity: Option<Connectivity>,
458) -> NdimageResult<Array<bool, D>>
459where
460    D: Dimension,
461{
462    // Validate inputs
463    if input.ndim() == 0 {
464        return Err(NdimageError::InvalidInput(
465            "Input array cannot be 0-dimensional".into(),
466        ));
467    }
468
469    if min_size == 0 {
470        return Err(NdimageError::InvalidInput(
471            "min_size must be greater than 0".into(),
472        ));
473    }
474
475    let conn = connectivity.unwrap_or(Connectivity::Face);
476
477    // Label connected components
478    let (labeled, num_labels) = label(input, None, Some(conn), None)?;
479
480    if num_labels == 0 {
481        return Ok(Array::<bool, D>::from_elem(input.raw_dim(), false));
482    }
483
484    // Count the _size of each component
485    let mut component_sizes = vec![0; num_labels + 1];
486    for &label_val in labeled.iter() {
487        if label_val > 0 {
488            component_sizes[label_val] += 1;
489        }
490    }
491
492    // Create output array, keeping only large enough components
493    let mut output = Array::<bool, D>::from_elem(input.raw_dim(), false);
494    let shape = input.shape();
495    let total_elements: usize = shape.iter().product();
496
497    // Convert to dynamic arrays for easier indexing
498    let labeled_dyn = labeled.clone().into_dyn();
499    let mut output_dyn = output.clone().into_dyn();
500
501    for flat_idx in 0..total_elements {
502        let indices = unravel_index(flat_idx, shape);
503        let label_val = labeled_dyn[IxDyn(&indices)];
504
505        if label_val > 0 && component_sizes[label_val] >= min_size {
506            output_dyn[IxDyn(&indices)] = true;
507        }
508    }
509
510    // Convert back to original dimension type
511    output = output_dyn.into_dimensionality::<D>().map_err(|_| {
512        NdimageError::DimensionError("Failed to convert back to original dimension type".into())
513    })?;
514
515    Ok(output)
516}
517
518/// Remove small holes from a labeled array
519///
520/// # Arguments
521///
522/// * `input` - Input binary array
523/// * `min_size` - Minimum size of holes to keep
524/// * `connectivity` - Connectivity type (default: Face)
525///
526/// # Returns
527///
528/// * `Result<Array<bool, D>>` - Binary array with small holes removed
529#[allow(dead_code)]
530pub fn remove_small_holes<D>(
531    input: &Array<bool, D>,
532    min_size: usize,
533    connectivity: Option<Connectivity>,
534) -> NdimageResult<Array<bool, D>>
535where
536    D: Dimension,
537{
538    // Validate inputs
539    if input.ndim() == 0 {
540        return Err(NdimageError::InvalidInput(
541            "Input array cannot be 0-dimensional".into(),
542        ));
543    }
544
545    if min_size == 0 {
546        return Err(NdimageError::InvalidInput(
547            "min_size must be greater than 0".into(),
548        ));
549    }
550
551    let conn = connectivity.unwrap_or(Connectivity::Face);
552
553    // To remove small holes, we:
554    // 1. Invert the binary image (holes become objects)
555    // 2. Remove small objects from the inverted image
556    // 3. Invert back
557
558    // Create inverted image
559    let mut inverted = input.clone();
560    for pixel in inverted.iter_mut() {
561        *pixel = !*pixel;
562    }
563
564    // Remove small objects from inverted image
565    let filtered_inverted = remove_small_objects(&inverted, min_size, Some(conn))?;
566
567    // Invert back to get result
568    let mut output = filtered_inverted;
569    for pixel in output.iter_mut() {
570        *pixel = !*pixel;
571    }
572
573    Ok(output)
574}
575
576/// 2D bounding box for a labeled region
577///
578/// Represents the bounding rectangle of a labeled object in a 2D image.
579/// All coordinates use the convention: `min` is inclusive, `max` is exclusive.
580#[derive(Debug, Clone, PartialEq, Eq)]
581pub struct BoundingBox2D {
582    /// Label value of this object
583    pub label: usize,
584    /// Minimum row (inclusive)
585    pub min_row: usize,
586    /// Maximum row (exclusive)
587    pub max_row: usize,
588    /// Minimum column (inclusive)
589    pub min_col: usize,
590    /// Maximum column (exclusive)
591    pub max_col: usize,
592}
593
594impl BoundingBox2D {
595    /// Width of the bounding box in pixels
596    pub fn width(&self) -> usize {
597        self.max_col - self.min_col
598    }
599
600    /// Height of the bounding box in pixels
601    pub fn height(&self) -> usize {
602        self.max_row - self.min_row
603    }
604
605    /// Area of the bounding box in pixels
606    pub fn area(&self) -> usize {
607        self.width() * self.height()
608    }
609}
610
611/// Optimized 2D connected component labeling using two-pass algorithm with union-find
612///
613/// This is a specialized 2D version of `label()` that avoids dynamic dimension
614/// conversions and runs significantly faster on 2D images.
615///
616/// # Arguments
617///
618/// * `input` - Input 2D binary array
619/// * `connectivity` - Connectivity type (default: Face = 4-connectivity)
620///
621/// # Returns
622///
623/// * `Result<(Array2<usize>, usize)>` - Labeled array and number of features
624///
625/// # Example
626///
627/// ```
628/// use scirs2_core::ndarray::array;
629/// use scirs2_ndimage::morphology::label_2d;
630///
631/// let input = array![
632///     [true, true, false, false],
633///     [true, true, false, false],
634///     [false, false, true, true],
635///     [false, false, true, true],
636/// ];
637///
638/// let (labeled, num_features) = label_2d(&input, None).expect("label_2d should succeed");
639/// assert_eq!(num_features, 2);
640/// ```
641pub fn label_2d(
642    input: &scirs2_core::ndarray::Array2<bool>,
643    connectivity: Option<Connectivity>,
644) -> NdimageResult<(scirs2_core::ndarray::Array2<usize>, usize)> {
645    use scirs2_core::ndarray::Array2;
646
647    let conn = connectivity.unwrap_or(Connectivity::Face);
648    let rows = input.nrows();
649    let cols = input.ncols();
650
651    if rows == 0 || cols == 0 {
652        return Ok((Array2::zeros((rows, cols)), 0));
653    }
654
655    let total = rows * cols;
656    let mut uf = UnionFind::new(total);
657
658    // Determine offsets: for 2D, Face = 4-connectivity, Full/FaceEdge = 8-connectivity
659    let use_diag = matches!(conn, Connectivity::Full | Connectivity::FaceEdge);
660
661    // First pass: scan left-to-right, top-to-bottom, checking already-visited neighbors
662    for r in 0..rows {
663        for c in 0..cols {
664            if !input[[r, c]] {
665                continue;
666            }
667
668            let idx = r * cols + c;
669
670            // Check neighbor above
671            if r > 0 && input[[r - 1, c]] {
672                uf.union(idx, (r - 1) * cols + c);
673            }
674
675            // Check neighbor to the left
676            if c > 0 && input[[r, c - 1]] {
677                uf.union(idx, r * cols + (c - 1));
678            }
679
680            if use_diag {
681                // Check upper-left diagonal
682                if r > 0 && c > 0 && input[[r - 1, c - 1]] {
683                    uf.union(idx, (r - 1) * cols + (c - 1));
684                }
685                // Check upper-right diagonal
686                if r > 0 && c + 1 < cols && input[[r - 1, c + 1]] {
687                    uf.union(idx, (r - 1) * cols + (c + 1));
688                }
689            }
690        }
691    }
692
693    // Second pass: build label mapping (only for foreground pixels)
694    let mut root_to_label: HashMap<usize, usize> = HashMap::new();
695    let mut next_label = 1usize;
696    let mut output = Array2::zeros((rows, cols));
697
698    for r in 0..rows {
699        for c in 0..cols {
700            if !input[[r, c]] {
701                continue;
702            }
703            let idx = r * cols + c;
704            let root = uf.find(idx);
705            let lbl = match root_to_label.get(&root) {
706                Some(&l) => l,
707                None => {
708                    let l = next_label;
709                    root_to_label.insert(root, l);
710                    next_label += 1;
711                    l
712                }
713            };
714            output[[r, c]] = lbl;
715        }
716    }
717
718    let num_labels = next_label - 1;
719    Ok((output, num_labels))
720}
721
722/// Find objects (bounding boxes) in a 2D labeled image
723///
724/// Returns a `BoundingBox2D` for each unique non-zero label in the input.
725/// The bounding boxes are sorted by label value.
726///
727/// # Arguments
728///
729/// * `labeled` - 2D labeled array where 0 = background
730///
731/// # Returns
732///
733/// * `Result<Vec<BoundingBox2D>>` - Bounding boxes sorted by label
734///
735/// # Example
736///
737/// ```
738/// use scirs2_core::ndarray::array;
739/// use scirs2_ndimage::morphology::{label_2d, find_objects_2d};
740///
741/// let input = array![
742///     [true, true, false, false],
743///     [true, true, false, false],
744///     [false, false, true, true],
745///     [false, false, true, true],
746/// ];
747///
748/// let (labeled, _) = label_2d(&input, None).expect("label_2d should succeed");
749/// let objects = find_objects_2d(&labeled).expect("find_objects_2d should succeed");
750/// assert_eq!(objects.len(), 2);
751/// ```
752pub fn find_objects_2d(
753    labeled: &scirs2_core::ndarray::Array2<usize>,
754) -> NdimageResult<Vec<BoundingBox2D>> {
755    let rows = labeled.nrows();
756    let cols = labeled.ncols();
757
758    if rows == 0 || cols == 0 {
759        return Ok(Vec::new());
760    }
761
762    // Collect bounding boxes in a single pass
763    let mut bbox_map: HashMap<usize, (usize, usize, usize, usize)> = HashMap::new();
764
765    for r in 0..rows {
766        for c in 0..cols {
767            let lbl = labeled[[r, c]];
768            if lbl == 0 {
769                continue;
770            }
771            let entry = bbox_map.entry(lbl).or_insert((r, r, c, c));
772            if r < entry.0 {
773                entry.0 = r;
774            }
775            if r > entry.1 {
776                entry.1 = r;
777            }
778            if c < entry.2 {
779                entry.2 = c;
780            }
781            if c > entry.3 {
782                entry.3 = c;
783            }
784        }
785    }
786
787    let mut result: Vec<BoundingBox2D> = bbox_map
788        .into_iter()
789        .map(|(lbl, (min_r, max_r, min_c, max_c))| BoundingBox2D {
790            label: lbl,
791            min_row: min_r,
792            max_row: max_r + 1, // exclusive
793            min_col: min_c,
794            max_col: max_c + 1, // exclusive
795        })
796        .collect();
797
798    result.sort_by_key(|b| b.label);
799    Ok(result)
800}
801
802/// Count pixels per label in a labeled image
803///
804/// # Arguments
805///
806/// * `labeled` - Labeled array
807///
808/// # Returns
809///
810/// * HashMap mapping label -> pixel count (excludes background label 0)
811pub fn count_labels_2d(labeled: &scirs2_core::ndarray::Array2<usize>) -> HashMap<usize, usize> {
812    let mut counts: HashMap<usize, usize> = HashMap::new();
813    for &lbl in labeled.iter() {
814        if lbl > 0 {
815            *counts.entry(lbl).or_insert(0) += 1;
816        }
817    }
818    counts
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use scirs2_core::ndarray::{array, Array2};
825
826    #[test]
827    fn test_label() {
828        let input = Array2::from_elem((3, 3), true);
829        let (result, _num_labels) = label(&input, None, None, None).expect("Operation failed");
830        assert_eq!(result.shape(), input.shape());
831    }
832
833    #[test]
834    fn test_find_boundaries() {
835        let input = Array2::from_elem((3, 3), 1);
836        let result = find_boundaries(&input, None, None).expect("Operation failed");
837        assert_eq!(result.shape(), input.shape());
838    }
839
840    #[test]
841    fn test_remove_small_objects() {
842        let input = Array2::from_elem((3, 3), true);
843        let result = remove_small_objects(&input, 1, None).expect("Operation failed");
844        assert_eq!(result.shape(), input.shape());
845    }
846
847    #[test]
848    fn test_label_2d_two_components() {
849        let input = array![
850            [true, true, false, false],
851            [true, true, false, false],
852            [false, false, true, true],
853            [false, false, true, true],
854        ];
855        let (labeled, num) = label_2d(&input, None).expect("label_2d should succeed");
856        assert_eq!(num, 2);
857        // Top-left block should have one label, bottom-right another
858        let l1 = labeled[[0, 0]];
859        let l2 = labeled[[2, 2]];
860        assert_ne!(l1, 0);
861        assert_ne!(l2, 0);
862        assert_ne!(l1, l2);
863        // All pixels in block 1 should share a label
864        assert_eq!(labeled[[0, 0]], labeled[[0, 1]]);
865        assert_eq!(labeled[[0, 0]], labeled[[1, 0]]);
866        assert_eq!(labeled[[0, 0]], labeled[[1, 1]]);
867    }
868
869    #[test]
870    fn test_label_2d_single_component_8conn() {
871        // With 8-connectivity, diagonal neighbors merge components
872        let input = array![
873            [true, false, false],
874            [false, true, false],
875            [false, false, true],
876        ];
877        let (labeled, num) =
878            label_2d(&input, Some(Connectivity::Full)).expect("label_2d 8-conn should succeed");
879        assert_eq!(num, 1);
880        assert_eq!(labeled[[0, 0]], labeled[[1, 1]]);
881        assert_eq!(labeled[[1, 1]], labeled[[2, 2]]);
882    }
883
884    #[test]
885    fn test_label_2d_multiple_components_4conn() {
886        // With 4-connectivity, diagonal pixels are separate
887        let input = array![
888            [true, false, false],
889            [false, true, false],
890            [false, false, true],
891        ];
892        let (labeled, num) =
893            label_2d(&input, Some(Connectivity::Face)).expect("label_2d 4-conn should succeed");
894        assert_eq!(num, 3);
895        // Each diagonal pixel should be a different component
896        let l0 = labeled[[0, 0]];
897        let l1 = labeled[[1, 1]];
898        let l2 = labeled[[2, 2]];
899        assert_ne!(l0, l1);
900        assert_ne!(l1, l2);
901        assert_ne!(l0, l2);
902    }
903
904    #[test]
905    fn test_label_2d_empty() {
906        let input = Array2::from_elem((3, 3), false);
907        let (labeled, num) = label_2d(&input, None).expect("empty should succeed");
908        assert_eq!(num, 0);
909        for &v in labeled.iter() {
910            assert_eq!(v, 0);
911        }
912    }
913
914    #[test]
915    fn test_label_2d_all_foreground() {
916        let input = Array2::from_elem((4, 4), true);
917        let (labeled, num) = label_2d(&input, None).expect("all-foreground should succeed");
918        assert_eq!(num, 1);
919        let expected_label = labeled[[0, 0]];
920        for &v in labeled.iter() {
921            assert_eq!(v, expected_label);
922        }
923    }
924
925    #[test]
926    fn test_find_objects_2d_basic() {
927        let input = array![
928            [true, true, false, false],
929            [true, true, false, false],
930            [false, false, true, true],
931            [false, false, true, true],
932        ];
933        let (labeled, _) = label_2d(&input, None).expect("label_2d should succeed");
934        let objects = find_objects_2d(&labeled).expect("find_objects_2d should succeed");
935        assert_eq!(objects.len(), 2);
936
937        // First object (label 1) should be in top-left
938        let obj1 = &objects[0];
939        assert_eq!(obj1.min_row, 0);
940        assert_eq!(obj1.max_row, 2);
941        assert_eq!(obj1.min_col, 0);
942        assert_eq!(obj1.max_col, 2);
943        assert_eq!(obj1.width(), 2);
944        assert_eq!(obj1.height(), 2);
945
946        // Second object (label 2) should be in bottom-right
947        let obj2 = &objects[1];
948        assert_eq!(obj2.min_row, 2);
949        assert_eq!(obj2.max_row, 4);
950        assert_eq!(obj2.min_col, 2);
951        assert_eq!(obj2.max_col, 4);
952    }
953
954    #[test]
955    fn test_find_objects_2d_no_objects() {
956        let labeled = Array2::<usize>::zeros((5, 5));
957        let objects = find_objects_2d(&labeled).expect("no objects should succeed");
958        assert!(objects.is_empty());
959    }
960
961    #[test]
962    fn test_find_objects_2d_single_pixel() {
963        let mut labeled = Array2::<usize>::zeros((5, 5));
964        labeled[[2, 3]] = 1;
965        let objects = find_objects_2d(&labeled).expect("single pixel should succeed");
966        assert_eq!(objects.len(), 1);
967        assert_eq!(objects[0].min_row, 2);
968        assert_eq!(objects[0].max_row, 3);
969        assert_eq!(objects[0].min_col, 3);
970        assert_eq!(objects[0].max_col, 4);
971        assert_eq!(objects[0].area(), 1);
972    }
973
974    #[test]
975    fn test_count_labels_2d() {
976        let labeled = array![[0, 1, 1, 0], [0, 1, 0, 2], [3, 0, 0, 2], [3, 3, 0, 0],];
977        let counts = count_labels_2d(&labeled);
978        assert_eq!(counts.get(&1), Some(&3));
979        assert_eq!(counts.get(&2), Some(&2));
980        assert_eq!(counts.get(&3), Some(&3));
981        assert_eq!(counts.get(&0), None); // background not counted
982    }
983
984    #[test]
985    fn test_label_2d_l_shape() {
986        // Test an L-shaped region that requires union-find path compression
987        let input = array![
988            [true, false, false],
989            [true, false, false],
990            [true, true, true],
991        ];
992        let (labeled, num) = label_2d(&input, None).expect("L-shape should succeed");
993        assert_eq!(num, 1);
994        let expected = labeled[[0, 0]];
995        assert_eq!(labeled[[1, 0]], expected);
996        assert_eq!(labeled[[2, 0]], expected);
997        assert_eq!(labeled[[2, 1]], expected);
998        assert_eq!(labeled[[2, 2]], expected);
999    }
1000}