Skip to main content

scirs2_vision/preprocessing/
morphology.rs

1//! Morphological operations for image preprocessing
2//!
3//! This module provides morphological operations like erosion, dilation,
4//! opening, closing, and morphological gradient.
5
6use crate::error::{Result, VisionError};
7use image::{DynamicImage, ImageBuffer, Luma};
8
9/// Structuring element (kernel) shape
10#[derive(Debug, Clone, Copy)]
11pub enum StructuringElement {
12    /// Rectangular structuring element
13    Rectangle(usize, usize),
14    /// Elliptical/circular structuring element
15    Ellipse(usize, usize),
16    /// Cross structuring element
17    Cross(usize),
18}
19
20/// Create a structuring element (kernel) for morphological operations
21///
22/// # Arguments
23///
24/// * `shape` - Shape of the structuring element
25///
26/// # Returns
27///
28/// * Result containing a binary kernel
29#[allow(dead_code)]
30fn create_structuring_element(shape: StructuringElement) -> Result<Vec<Vec<bool>>> {
31    match shape {
32        StructuringElement::Rectangle(width, height) => {
33            if width == 0 || height == 0 {
34                return Err(VisionError::InvalidParameter(
35                    "Width and height must be positive".to_string(),
36                ));
37            }
38
39            // Create a rectangular kernel filled with true values
40            let kernel = vec![vec![true; width]; height];
41            Ok(kernel)
42        }
43        StructuringElement::Ellipse(width, height) => {
44            if width == 0 || height == 0 {
45                return Err(VisionError::InvalidParameter(
46                    "Width and height must be positive".to_string(),
47                ));
48            }
49
50            let center_x = width as f32 / 2.0;
51            let center_y = height as f32 / 2.0;
52            let radius_x = center_x;
53            let radius_y = center_y;
54
55            // Create an elliptical kernel
56            let mut kernel = vec![vec![false; width]; height];
57
58            for (y, row) in kernel.iter_mut().enumerate() {
59                for (x, cell) in row.iter_mut().enumerate() {
60                    let dx = (x as f32 - center_x + 0.5) / radius_x;
61                    let dy = (y as f32 - center_y + 0.5) / radius_y;
62
63                    // Check if point is within the ellipse: (dx/a)^2 + (dy/b)^2 <= 1
64                    if dx * dx + dy * dy <= 1.0 {
65                        *cell = true;
66                    }
67                }
68            }
69
70            Ok(kernel)
71        }
72        StructuringElement::Cross(size) => {
73            if size == 0 {
74                return Err(VisionError::InvalidParameter(
75                    "Size must be positive".to_string(),
76                ));
77            }
78
79            // Ensure size is odd
80            let size = if size % 2 == 0 { size + 1 } else { size };
81
82            // Create a cross-shaped kernel
83            let mut kernel = vec![vec![false; size]; size];
84            let center = size / 2;
85
86            for (y, row) in kernel.iter_mut().enumerate() {
87                for (x, cell) in row.iter_mut().enumerate() {
88                    if x == center || y == center {
89                        *cell = true;
90                    }
91                }
92            }
93
94            Ok(kernel)
95        }
96    }
97}
98
99/// Apply erosion to an image
100///
101/// # Arguments
102///
103/// * `img` - Input grayscale image
104/// * `kernelshape` - Shape of the structuring element
105///
106/// # Returns
107///
108/// * Result containing the eroded image
109#[allow(dead_code)]
110pub fn erode(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
111    let gray = img.to_luma8();
112    let (width, height) = gray.dimensions();
113    let kernel = create_structuring_element(kernelshape)?;
114
115    let kernel_height = kernel.len();
116    let kernel_width = kernel[0].len();
117
118    // Calculate anchor point (center of kernel)
119    let anchor_x = kernel_width / 2;
120    let anchor_y = kernel_height / 2;
121
122    let mut result = ImageBuffer::new(width, height);
123
124    for y in 0..height {
125        for x in 0..width {
126            // Initialize with maximum possible value
127            let mut min_val = 255u8;
128
129            // Apply kernel
130            for (ky, kernel_row) in kernel.iter().enumerate() {
131                for (kx, &is_active) in kernel_row.iter().enumerate() {
132                    // Skip if kernel element is not active
133                    if !is_active {
134                        continue;
135                    }
136
137                    // Calculate image coordinates with kernel anchor
138                    let img_x = x as isize + (kx as isize - anchor_x as isize);
139                    let img_y = y as isize + (ky as isize - anchor_y as isize);
140
141                    // Skip coordinates outside image bounds
142                    if img_x < 0 || img_x >= width as isize || img_y < 0 || img_y >= height as isize
143                    {
144                        continue;
145                    }
146
147                    // Update minimum value
148                    let pixel_val = gray.get_pixel(img_x as u32, img_y as u32)[0];
149                    min_val = min_val.min(pixel_val);
150                }
151            }
152
153            // Set result pixel
154            result.put_pixel(x, y, Luma([min_val]));
155        }
156    }
157
158    Ok(DynamicImage::ImageLuma8(result))
159}
160
161/// Apply dilation to an image
162///
163/// # Arguments
164///
165/// * `img` - Input grayscale image
166/// * `kernelshape` - Shape of the structuring element
167///
168/// # Returns
169///
170/// * Result containing the dilated image
171#[allow(dead_code)]
172pub fn dilate(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
173    let gray = img.to_luma8();
174    let (width, height) = gray.dimensions();
175    let kernel = create_structuring_element(kernelshape)?;
176
177    let kernel_height = kernel.len();
178    let kernel_width = kernel[0].len();
179
180    // Calculate anchor point (center of kernel)
181    let anchor_x = kernel_width / 2;
182    let anchor_y = kernel_height / 2;
183
184    let mut result = ImageBuffer::new(width, height);
185
186    for y in 0..height {
187        for x in 0..width {
188            // Initialize with minimum possible value
189            let mut max_val = 0u8;
190
191            // Apply kernel
192            for (ky, kernel_row) in kernel.iter().enumerate() {
193                for (kx, &is_active) in kernel_row.iter().enumerate() {
194                    // Skip if kernel element is not active
195                    if !is_active {
196                        continue;
197                    }
198
199                    // Calculate image coordinates with kernel anchor
200                    let img_x = x as isize - (kx as isize - anchor_x as isize);
201                    let img_y = y as isize - (ky as isize - anchor_y as isize);
202
203                    // Skip coordinates outside image bounds
204                    if img_x < 0 || img_x >= width as isize || img_y < 0 || img_y >= height as isize
205                    {
206                        continue;
207                    }
208
209                    // Update maximum value
210                    let pixel_val = gray.get_pixel(img_x as u32, img_y as u32)[0];
211                    max_val = max_val.max(pixel_val);
212                }
213            }
214
215            // Set result pixel
216            result.put_pixel(x, y, Luma([max_val]));
217        }
218    }
219
220    Ok(DynamicImage::ImageLuma8(result))
221}
222
223/// Apply morphological opening (erosion followed by dilation)
224///
225/// # Arguments
226///
227/// * `img` - Input grayscale image
228/// * `kernelshape` - Shape of the structuring element
229///
230/// # Returns
231///
232/// * Result containing the opened image
233#[allow(dead_code)]
234pub fn opening(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
235    let eroded = erode(img, kernelshape)?;
236    dilate(&eroded, kernelshape)
237}
238
239/// Apply morphological closing (dilation followed by erosion)
240///
241/// # Arguments
242///
243/// * `img` - Input grayscale image
244/// * `kernelshape` - Shape of the structuring element
245///
246/// # Returns
247///
248/// * Result containing the closed image
249#[allow(dead_code)]
250pub fn closing(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
251    let dilated = dilate(img, kernelshape)?;
252    erode(&dilated, kernelshape)
253}
254
255/// Apply morphological gradient (difference between dilation and erosion)
256///
257/// # Arguments
258///
259/// * `img` - Input grayscale image
260/// * `kernelshape` - Shape of the structuring element
261///
262/// # Returns
263///
264/// * Result containing the gradient image
265#[allow(dead_code)]
266pub fn morphological_gradient(
267    img: &DynamicImage,
268    kernelshape: StructuringElement,
269) -> Result<DynamicImage> {
270    let dilated = dilate(img, kernelshape)?;
271    let eroded = erode(img, kernelshape)?;
272
273    let dilated_gray = dilated.to_luma8();
274    let eroded_gray = eroded.to_luma8();
275    let (width, height) = dilated_gray.dimensions();
276
277    let mut result = ImageBuffer::new(width, height);
278
279    for y in 0..height {
280        for x in 0..width {
281            let dilated_val = dilated_gray.get_pixel(x, y)[0];
282            let eroded_val = eroded_gray.get_pixel(x, y)[0];
283
284            // Calculate gradient (dilation - erosion)
285            // Handle underflow with saturating subtraction
286            let gradient = dilated_val.saturating_sub(eroded_val);
287
288            // Set result pixel
289            result.put_pixel(x, y, Luma([gradient]));
290        }
291    }
292
293    Ok(DynamicImage::ImageLuma8(result))
294}
295
296/// Apply top-hat transform (original - opening)
297///
298/// # Arguments
299///
300/// * `img` - Input grayscale image
301/// * `kernelshape` - Shape of the structuring element
302///
303/// # Returns
304///
305/// * Result containing the top-hat transformed image
306#[allow(dead_code)]
307pub fn top_hat(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
308    let opened = opening(img, kernelshape)?;
309
310    let original = img.to_luma8();
311    let opened_gray = opened.to_luma8();
312    let (width, height) = original.dimensions();
313
314    let mut result = ImageBuffer::new(width, height);
315
316    for y in 0..height {
317        for x in 0..width {
318            let original_val = original.get_pixel(x, y)[0];
319            let opened_val = opened_gray.get_pixel(x, y)[0];
320
321            // Top-hat: original - opening
322            // Handle underflow with saturating subtraction
323            let top_hat_val = original_val.saturating_sub(opened_val);
324
325            // Set result pixel
326            result.put_pixel(x, y, Luma([top_hat_val]));
327        }
328    }
329
330    Ok(DynamicImage::ImageLuma8(result))
331}
332
333/// Apply black-hat transform (closing - original)
334///
335/// # Arguments
336///
337/// * `img` - Input grayscale image
338/// * `kernelshape` - Shape of the structuring element
339///
340/// # Returns
341///
342/// * Result containing the black-hat transformed image
343#[allow(dead_code)]
344pub fn black_hat(img: &DynamicImage, kernelshape: StructuringElement) -> Result<DynamicImage> {
345    let closed = closing(img, kernelshape)?;
346
347    let original = img.to_luma8();
348    let closed_gray = closed.to_luma8();
349    let (width, height) = original.dimensions();
350
351    let mut result = ImageBuffer::new(width, height);
352
353    for y in 0..height {
354        for x in 0..width {
355            let original_val = original.get_pixel(x, y)[0];
356            let closed_val = closed_gray.get_pixel(x, y)[0];
357
358            // Black-hat: closing - original
359            // Handle underflow with saturating subtraction
360            let black_hat_val = closed_val.saturating_sub(original_val);
361
362            // Set result pixel
363            result.put_pixel(x, y, Luma([black_hat_val]));
364        }
365    }
366
367    Ok(DynamicImage::ImageLuma8(result))
368}