Skip to main content

trueno_image/
morphology.rs

1//! Morphological operations: dilate and erode (NPP parity).
2//!
3//! Operates on grayscale images with flat structuring elements.
4
5use crate::error::ImageError;
6
7/// Dilate: output pixel = max of neighborhood defined by structuring element.
8///
9/// # Errors
10///
11/// Returns error if dimensions don't match or SE is invalid.
12pub fn dilate(
13    image: &[f32],
14    width: usize,
15    height: usize,
16    se: &[f32],
17    se_w: usize,
18    se_h: usize,
19) -> Result<Vec<f32>, ImageError> {
20    validate_inputs(image, width, height, se, se_w, se_h)?;
21    let mut output = vec![0.0f32; width * height];
22    for y in 0..height {
23        for x in 0..width {
24            output[y * width + x] = neighborhood_max(image, width, height, se, se_w, se_h, x, y);
25        }
26    }
27    Ok(output)
28}
29
30/// Erode: output pixel = min of neighborhood defined by structuring element.
31///
32/// # Errors
33///
34/// Returns error if dimensions don't match or SE is invalid.
35pub fn erode(
36    image: &[f32],
37    width: usize,
38    height: usize,
39    se: &[f32],
40    se_w: usize,
41    se_h: usize,
42) -> Result<Vec<f32>, ImageError> {
43    validate_inputs(image, width, height, se, se_w, se_h)?;
44    let mut output = vec![0.0f32; width * height];
45    for y in 0..height {
46        for x in 0..width {
47            output[y * width + x] = neighborhood_min(image, width, height, se, se_w, se_h, x, y);
48        }
49    }
50    Ok(output)
51}
52
53/// Compute max over SE neighborhood at pixel (px, py).
54#[allow(clippy::too_many_arguments)]
55fn neighborhood_max(
56    image: &[f32],
57    width: usize,
58    height: usize,
59    se: &[f32],
60    se_w: usize,
61    se_h: usize,
62    px: usize,
63    py: usize,
64) -> f32 {
65    let half_w = se_w / 2;
66    let half_h = se_h / 2;
67    let mut max_val = f32::NEG_INFINITY;
68    for sy in 0..se_h {
69        for sx in 0..se_w {
70            if se[sy * se_w + sx] <= 0.0 {
71                continue;
72            }
73            let iy = py as isize + sy as isize - half_h as isize;
74            let ix = px as isize + sx as isize - half_w as isize;
75            if iy >= 0 && iy < height as isize && ix >= 0 && ix < width as isize {
76                let val = image[iy as usize * width + ix as usize];
77                if val > max_val {
78                    max_val = val;
79                }
80            }
81        }
82    }
83    if max_val == f32::NEG_INFINITY {
84        0.0
85    } else {
86        max_val
87    }
88}
89
90/// Compute min over SE neighborhood at pixel (px, py).
91#[allow(clippy::too_many_arguments)]
92fn neighborhood_min(
93    image: &[f32],
94    width: usize,
95    height: usize,
96    se: &[f32],
97    se_w: usize,
98    se_h: usize,
99    px: usize,
100    py: usize,
101) -> f32 {
102    let half_w = se_w / 2;
103    let half_h = se_h / 2;
104    let mut min_val = f32::INFINITY;
105    for sy in 0..se_h {
106        for sx in 0..se_w {
107            if se[sy * se_w + sx] <= 0.0 {
108                continue;
109            }
110            let iy = py as isize + sy as isize - half_h as isize;
111            let ix = px as isize + sx as isize - half_w as isize;
112            if iy >= 0 && iy < height as isize && ix >= 0 && ix < width as isize {
113                let val = image[iy as usize * width + ix as usize];
114                if val < min_val {
115                    min_val = val;
116                }
117            }
118        }
119    }
120    if min_val == f32::INFINITY {
121        0.0
122    } else {
123        min_val
124    }
125}
126
127/// Morphological opening: erode then dilate.
128///
129/// # Errors
130///
131/// Returns error if dimensions don't match.
132pub fn opening(
133    image: &[f32],
134    width: usize,
135    height: usize,
136    se: &[f32],
137    se_w: usize,
138    se_h: usize,
139) -> Result<Vec<f32>, ImageError> {
140    let eroded = erode(image, width, height, se, se_w, se_h)?;
141    dilate(&eroded, width, height, se, se_w, se_h)
142}
143
144/// Morphological closing: dilate then erode.
145///
146/// # Errors
147///
148/// Returns error if dimensions don't match.
149pub fn closing(
150    image: &[f32],
151    width: usize,
152    height: usize,
153    se: &[f32],
154    se_w: usize,
155    se_h: usize,
156) -> Result<Vec<f32>, ImageError> {
157    let dilated = dilate(image, width, height, se, se_w, se_h)?;
158    erode(&dilated, width, height, se, se_w, se_h)
159}
160
161fn validate_inputs(
162    image: &[f32],
163    width: usize,
164    height: usize,
165    se: &[f32],
166    se_w: usize,
167    se_h: usize,
168) -> Result<(), ImageError> {
169    if image.len() != width * height {
170        return Err(ImageError::BufferLengthMismatch {
171            expected: width * height,
172            got: image.len(),
173            width,
174            height,
175        });
176    }
177    if se.len() != se_w * se_h || se_w == 0 || se_h == 0 {
178        return Err(ImageError::InvalidKernelSize { kw: se_w, kh: se_h });
179    }
180    Ok(())
181}