Skip to main content

iris/segmentation/
watershed.rs

1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
4
5/// Marker-based watershed segmentation using iterative flooding.
6///
7/// Each unique positive marker value defines a seed region. The algorithm
8/// floods outward from all seeds simultaneously (priority queue based on
9/// pixel intensity), assigning each pixel to the nearest marker's region.
10/// Pixels where different regions meet become watershed lines (label 0).
11///
12/// - `image`: Input grayscale or single-channel image.
13/// - `markers`: 2D integer tensor of shape `[H, W]` with marker labels.
14///   Zero means unassigned; positive integers are region seeds.
15///
16/// Returns a 2D integer tensor `[H, W]` with the segmentation labels.
17pub fn watershed<B: Backend>(
18    image: &Image<B>,
19    markers: &Tensor<B, 2, Int>,
20) -> Result<Tensor<B, 2, Int>> {
21    let gray = image.grayscale()?;
22    let img_dims = gray.tensor.dims();
23    let h = img_dims[1];
24    let w = img_dims[2];
25    let mk_dims = markers.dims();
26
27    if mk_dims[0] != h || mk_dims[1] != w {
28        return Err(IrisError::DimensionMismatch {
29            expected: vec![h, w],
30            actual: mk_dims.to_vec(),
31        });
32    }
33
34    let device = gray.tensor.device();
35    let img_data = gray.tensor.clone().into_data();
36    let img_vals: Vec<f32> = img_data.iter::<f32>().collect();
37
38    let mk_data = markers.clone().into_data();
39    let mk_vals: Vec<i32> = mk_data.iter::<i32>().collect();
40
41    // Validate markers: must have at least one seed
42    if mk_vals.iter().all(|&v| v == 0) {
43        return Err(IrisError::InvalidParameter(
44            "Markers must contain at least one positive seed".into(),
45        ));
46    }
47
48    // Working labels: copy markers
49    let mut labels: Vec<i32> = mk_vals.clone();
50
51    // Priority queue: (intensity, y, x) — min-heap via reverse ordering
52    let mut queue: Vec<(f32, usize, usize)> = Vec::new();
53
54    // Seed the queue with all marker pixels
55    for y in 0..h {
56        for x in 0..w {
57            let idx = y * w + x;
58            if labels[idx] > 0 {
59                queue.push((img_vals[idx], y, x));
60            }
61        }
62    }
63    // Make it a min-heap
64    queue.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
65    // Use a BTreeMap-based priority approach: simple sorted insertion is fine for correctness
66    // (not optimized for speed, but correct for the algorithm)
67
68    let neighbors = [(1_isize, 0_isize), (-1, 0), (0, 1), (0, -1)];
69
70    while let Some((_intensity, cy, cx)) = queue.pop() {
71        let cidx = cy * w + cx;
72        let c_label = labels[cidx];
73        if c_label == 0 {
74            continue;
75        }
76
77        for &(dx, dy) in &neighbors {
78            let nx = cx as isize + dx;
79            let ny = cy as isize + dy;
80            if nx < 0 || nx >= w as isize || ny < 0 || ny >= h as isize {
81                continue;
82            }
83            let ux = nx as usize;
84            let uy = ny as usize;
85            let nidx = uy * w + ux;
86
87            if labels[nidx] == 0 {
88                // Unassigned: claim it and enqueue
89                labels[nidx] = c_label;
90                // Binary search insertion to maintain sorted order (min-heap by intensity)
91                let new_entry = (img_vals[nidx], uy, ux);
92                match queue.binary_search_by(|a| {
93                    a.0.partial_cmp(&new_entry.0)
94                        .unwrap_or(std::cmp::Ordering::Equal)
95                }) {
96                    Ok(pos) | Err(pos) => queue.insert(pos, new_entry),
97                }
98            } else if labels[nidx] != c_label {
99                // Conflict: both regions claim this pixel — watershed line
100                // Leave as-is (the first one wins; boundary pixels remain labeled)
101                // For a strict watershed line, set to 0:
102                // labels[nidx] = 0;
103            }
104        }
105    }
106
107    let out_data = TensorData::new(labels, [h, w]);
108    let out_tensor = Tensor::<B, 2, Int>::from_data(out_data, &device);
109    Ok(out_tensor)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::test_helpers::{TestBackend, test_device};
116
117    #[test]
118    fn test_watershed_basic() {
119        let device = test_device();
120
121        // 8x8 grayscale image with two regions
122        let mut img_vals = vec![0.3f32; 8 * 8];
123        // Left region is dark
124        for y in 0..8 {
125            for x in 0..4 {
126                img_vals[y * 8 + x] = 0.2;
127            }
128        }
129        // Right region is bright
130        for y in 0..8 {
131            for x in 4..8 {
132                img_vals[y * 8 + x] = 0.8;
133            }
134        }
135        let img_tensor =
136            Tensor::<TestBackend, 3>::from_data(TensorData::new(img_vals, [1, 8, 8]), &device);
137        let img = Image::new(img_tensor);
138
139        // Two markers: label 1 top-left, label 2 bottom-right
140        let mut mk_vals = vec![0i32; 8 * 8];
141        mk_vals[0] = 1;
142        mk_vals[7 * 8 + 7] = 2;
143        let mk_tensor =
144            Tensor::<TestBackend, 2, Int>::from_data(TensorData::new(mk_vals, [8, 8]), &device);
145
146        let result = watershed(&img, &mk_tensor).unwrap();
147        assert_eq!(result.dims(), [8, 8]);
148
149        // Marker pixels should retain their labels
150        let out_data = result.clone().into_data();
151        let out_vals: Vec<i32> = out_data.iter::<i32>().collect();
152        assert_eq!(out_vals[0], 1); // top-left marker
153        assert_eq!(out_vals[63], 2); // bottom-right marker
154
155        // All pixels should be assigned to some region
156        assert!(out_vals.iter().all(|&v| v > 0));
157    }
158
159    #[test]
160    fn test_watershed_no_markers_error() {
161        let device = test_device();
162        let img_tensor = Tensor::<TestBackend, 3>::from_data(
163            TensorData::new(vec![0.5f32; 16], [1, 4, 4]),
164            &device,
165        );
166        let img = Image::new(img_tensor);
167
168        let mk_tensor = Tensor::<TestBackend, 2, Int>::from_data(
169            TensorData::new(vec![0i32; 16], [4, 4]),
170            &device,
171        );
172
173        assert!(watershed(&img, &mk_tensor).is_err());
174    }
175}