Skip to main content

scirs2_vision/segmentation/
mod.rs

1//! Image segmentation module
2//!
3//! This module provides functionality for segmenting images into regions
4//! or partitioning images into meaningful parts.
5
6pub mod grabcut;
7pub mod kmeans_seg;
8pub mod mean_shift;
9pub mod region_growing;
10pub mod semantic;
11pub mod slic;
12pub mod unified;
13pub mod watershed;
14
15pub use grabcut::{
16    apply_foreground_mask, grabcut_mask_to_image, grabcut_rect, grabcut_with_mask, GrabCutMask,
17    GrabCutParams, GrabCutResult,
18};
19pub use kmeans_seg::{
20    kmeans_labels_to_color, kmeans_labels_to_gray, kmeans_segment, KMeansSegParams, KMeansSegResult,
21};
22pub use mean_shift::{mean_shift, MeanShiftParams};
23pub use region_growing::{
24    adaptive_region_growing, region_growing, region_labels_to_color, RegionGrowingParams, SeedPoint,
25};
26pub use semantic::*;
27pub use slic::{draw_superpixel_boundaries, slic};
28pub use unified::{segment, SegmentMethod, SegmentResult};
29pub use watershed::{
30    compute_gradient_magnitude, labels_to_color_image, watershed, watershed_markers,
31};
32
33use crate::error::{Result, VisionError};
34use crate::feature::image_to_array;
35use image::{DynamicImage, GrayImage, ImageBuffer, Luma};
36// scirs2_core::ndarray::Array2 is imported through image_to_array function
37
38/// Adaptive thresholding method
39#[derive(Debug, Clone, Copy)]
40pub enum AdaptiveMethod {
41    /// Mean of the neighborhood values
42    Mean,
43    /// Gaussian weighted mean of the neighborhood
44    Gaussian,
45}
46
47/// Threshold an image to create a binary image
48///
49/// # Arguments
50///
51/// * `img` - Input image
52/// * `threshold` - Threshold value (0.0 to 1.0)
53///
54/// # Returns
55///
56/// * Result containing a binary image
57#[allow(dead_code)]
58pub fn threshold_binary(img: &DynamicImage, threshold: f32) -> Result<GrayImage> {
59    let array = image_to_array(img)?;
60    let (height, width) = array.dim();
61
62    let mut binary = ImageBuffer::new(width as u32, height as u32);
63
64    for y in 0..height {
65        for x in 0..width {
66            let value = if array[[y, x]] >= threshold { 255 } else { 0 };
67            binary.put_pixel(x as u32, y as u32, Luma([value]));
68        }
69    }
70
71    Ok(binary)
72}
73
74/// Apply Otsu's automatic thresholding method
75///
76/// # Arguments
77///
78/// * `img` - Input image
79///
80/// # Returns
81///
82/// * Result containing a binary image and the computed threshold
83#[allow(dead_code)]
84pub fn otsu_threshold(img: &DynamicImage) -> Result<(GrayImage, f32)> {
85    let gray = img.to_luma8();
86    let (width, height) = gray.dimensions();
87    let total_pixels = (width * height) as usize;
88
89    // Calculate histogram
90    let mut histogram = [0; 256];
91    for pixel in gray.pixels() {
92        histogram[pixel[0] as usize] += 1;
93    }
94
95    // Calculate running sum and weighted sum
96    let mut sum = 0;
97    for (i, &count) in histogram.iter().enumerate() {
98        sum += i * count;
99    }
100
101    let mut sum_background = 0;
102    let mut weight_background = 0;
103    let mut max_variance = 0.0;
104    let mut threshold = 0;
105
106    for (i, &count) in histogram.iter().enumerate() {
107        // Weight is the probability of the background
108        weight_background += count;
109        if weight_background == 0 {
110            continue;
111        }
112
113        let weight_foreground = total_pixels - weight_background;
114        if weight_foreground == 0 {
115            break;
116        }
117
118        // Sum is the weighted mean of the background
119        sum_background += i * histogram[i];
120
121        // Calculate means
122        let mean_background = sum_background as f32 / weight_background as f32;
123        let mean_foreground = (sum - sum_background) as f32 / weight_foreground as f32;
124
125        // Calculate between-class variance
126        let variance = weight_background as f32
127            * weight_foreground as f32
128            * (mean_background - mean_foreground).powi(2);
129
130        // Update threshold if variance is higher
131        if variance > max_variance {
132            max_variance = variance;
133            threshold = i;
134        }
135    }
136
137    // Create binary image using the computed threshold
138    let threshold_f32 = threshold as f32 / 255.0;
139    let binary = threshold_binary(img, threshold_f32)?;
140
141    Ok((binary, threshold_f32))
142}
143
144/// Apply adaptive thresholding
145///
146/// # Arguments
147///
148/// * `img` - Input image
149/// * `block_size` - Size of the neighborhood for calculating the threshold
150/// * `c` - Constant subtracted from the mean or weighted sum
151/// * `method` - Thresholding method
152///
153/// # Returns
154///
155/// * Result containing a binary image
156#[allow(dead_code)]
157pub fn adaptive_threshold(
158    img: &DynamicImage,
159    block_size: usize,
160    c: f32,
161    method: AdaptiveMethod,
162) -> Result<GrayImage> {
163    // Check if block _size is valid
164    if block_size.is_multiple_of(2) || block_size < 3 {
165        return Err(VisionError::InvalidParameter(
166            "block_size must be odd and at least 3".to_string(),
167        ));
168    }
169
170    let array = image_to_array(img)?;
171    let (height, width) = array.dim();
172    let radius = block_size / 2;
173
174    let mut binary = ImageBuffer::new(width as u32, height as u32);
175
176    for y in 0..height {
177        for x in 0..width {
178            // Define neighborhood bounds with padding at the edges
179            let start_y = y.saturating_sub(radius);
180            let end_y = (y + radius + 1).min(height);
181            let start_x = x.saturating_sub(radius);
182            let end_x = (x + radius + 1).min(width);
183
184            // Calculate threshold based on method
185            let threshold = match method {
186                AdaptiveMethod::Mean => {
187                    // Simple mean of neighborhood
188                    let mut sum = 0.0;
189                    let mut count = 0;
190
191                    for ny in start_y..end_y {
192                        for nx in start_x..end_x {
193                            sum += array[[ny, nx]];
194                            count += 1;
195                        }
196                    }
197
198                    sum / count as f32 - c
199                }
200                AdaptiveMethod::Gaussian => {
201                    // Gaussian weighted mean
202                    let mut weighted_sum = 0.0;
203                    let mut weight_sum = 0.0;
204
205                    for ny in start_y..end_y {
206                        for nx in start_x..end_x {
207                            let dy = (ny as isize - y as isize).pow(2) as f32;
208                            let dx = (nx as isize - x as isize).pow(2) as f32;
209                            let dist = (dy + dx).sqrt();
210
211                            // Gaussian weight
212                            let sigma = radius as f32 / 2.0;
213                            let weight = (-dist * dist / (2.0 * sigma * sigma)).exp();
214
215                            weighted_sum += array[[ny, nx]] * weight;
216                            weight_sum += weight;
217                        }
218                    }
219
220                    weighted_sum / weight_sum - c
221                }
222            };
223
224            // Apply threshold
225            let value = if array[[y, x]] > threshold { 255 } else { 0 };
226            binary.put_pixel(x as u32, y as u32, Luma([value]));
227        }
228    }
229
230    Ok(binary)
231}
232
233/// Apply connected component labeling
234///
235/// # Arguments
236///
237/// * `binary` - Binary input image
238///
239/// # Returns
240///
241/// * Result containing a labeled image where each connected component has a unique label
242///
243/// Type alias for labeled image
244pub type LabeledImage = ImageBuffer<Luma<u16>, Vec<u16>>;
245
246/// Find connected components in a binary image using 8-connectivity
247///
248/// This function implements a two-pass algorithm to identify connected components
249/// in a binary image. It assigns a unique label to each connected component and
250/// returns both the labeled image and the number of labels found.
251///
252/// # Arguments
253///
254/// * `binary` - Input binary image where non-zero pixels are foreground
255///
256/// # Returns
257///
258/// * Result containing a tuple with:
259///   - Labeled image where each pixel value is the label of its component
260///   - Number of labels found (counting from 1)
261#[allow(dead_code)]
262pub fn connected_components(binary: &GrayImage) -> Result<(LabeledImage, u16)> {
263    let (width, height) = binary.dimensions();
264    let mut labels: ImageBuffer<Luma<u16>, Vec<u16>> = ImageBuffer::new(width, height);
265    let mut label_equiv = vec![0u16; 65536]; // Union-find data structure
266    let mut next_label = 1u16;
267
268    // Initialize equivalence array
269    // Fill with the index values
270    for (i, val) in label_equiv.iter_mut().enumerate() {
271        *val = i as u16;
272    }
273
274    // First pass: assign labels and record equivalences
275    for y in 0..height {
276        for x in 0..width {
277            // Skip background
278            if binary.get_pixel(x, y)[0] == 0 {
279                labels.put_pixel(x, y, Luma([0]));
280                continue;
281            }
282
283            // Check connected neighbors (4-connectivity)
284            let mut neighbors = Vec::new();
285
286            if x > 0 && binary.get_pixel(x - 1, y)[0] > 0 {
287                neighbors.push(labels.get_pixel(x - 1, y)[0]);
288            }
289
290            if y > 0 && binary.get_pixel(x, y - 1)[0] > 0 {
291                neighbors.push(labels.get_pixel(x, y - 1)[0]);
292            }
293
294            // If no labeled neighbors, create a new label
295            if neighbors.is_empty() {
296                labels.put_pixel(x, y, Luma([next_label]));
297                next_label += 1;
298
299                // Avoid overflow
300                if next_label == 0 {
301                    return Err(VisionError::OperationError(
302                        "Too many components (label overflow)".to_string(),
303                    ));
304                }
305            } else {
306                // Find minimum label among neighbors
307                let min_label = *neighbors.iter().min().expect("Operation failed");
308                labels.put_pixel(x, y, Luma([min_label]));
309
310                // Record equivalences
311                for &neighbor_label in &neighbors {
312                    if neighbor_label != min_label {
313                        union(&mut label_equiv, min_label, neighbor_label);
314                    }
315                }
316            }
317        }
318    }
319
320    // Second pass: replace labels with their equivalence classes
321    for y in 0..height {
322        for x in 0..width {
323            let label = labels.get_pixel(x, y)[0];
324            if label > 0 {
325                labels.put_pixel(x, y, Luma([find(&label_equiv, label)]));
326            }
327        }
328    }
329
330    // Count unique labels (excluding background)
331    let mut unique_labels = std::collections::HashSet::new();
332    for y in 0..height {
333        for x in 0..width {
334            let label = labels.get_pixel(x, y)[0];
335            if label > 0 {
336                unique_labels.insert(label);
337            }
338        }
339    }
340
341    Ok((labels, unique_labels.len() as u16))
342}
343
344// Union-find helper functions
345#[allow(dead_code)]
346fn find(labels: &[u16], x: u16) -> u16 {
347    let mut y = x;
348    while y != labels[y as usize] {
349        y = labels[y as usize];
350    }
351    y
352}
353
354#[allow(dead_code)]
355fn union(labels: &mut [u16], x: u16, y: u16) {
356    let root_x = find(labels, x);
357    let root_y = find(labels, y);
358    if root_x <= root_y {
359        labels[root_y as usize] = root_x;
360    } else {
361        labels[root_x as usize] = root_y;
362    }
363}