1use crate::error::{VisionError, VisionResult};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct StructuringElement {
37 pub width: usize,
39 pub height: usize,
41 pub anchor_x: usize,
43 pub anchor_y: usize,
45 pub mask: Vec<bool>,
47}
48
49impl StructuringElement {
50 pub fn new(width: usize, height: usize, mask: Vec<bool>) -> VisionResult<Self> {
58 let se = Self {
59 width,
60 height,
61 anchor_x: width / 2,
62 anchor_y: height / 2,
63 mask,
64 };
65 validate_se(&se)?;
66 Ok(se)
67 }
68
69 pub fn rect(width: usize, height: usize) -> VisionResult<Self> {
75 if width == 0 || height == 0 {
76 return Err(VisionError::EmptyInput("structuring element"));
77 }
78 Ok(Self {
79 width,
80 height,
81 anchor_x: width / 2,
82 anchor_y: height / 2,
83 mask: vec![true; width * height],
84 })
85 }
86
87 pub fn square(size: usize) -> VisionResult<Self> {
92 Self::rect(size, size)
93 }
94
95 #[must_use]
100 pub fn cross(radius: usize) -> Self {
101 let size = 2 * radius + 1;
102 let mut mask = vec![false; size * size];
103 for i in 0..size {
104 mask[radius * size + i] = true;
105 mask[i * size + radius] = true;
106 }
107 Self {
108 width: size,
109 height: size,
110 anchor_x: radius,
111 anchor_y: radius,
112 mask,
113 }
114 }
115
116 fn active_offsets(&self) -> Vec<(isize, isize)> {
118 let mut offsets = Vec::new();
119 for row in 0..self.height {
120 for col in 0..self.width {
121 if self.mask[row * self.width + col] {
122 offsets.push((
123 row as isize - self.anchor_y as isize,
124 col as isize - self.anchor_x as isize,
125 ));
126 }
127 }
128 }
129 offsets
130 }
131}
132
133fn validate_se(se: &StructuringElement) -> VisionResult<()> {
135 if se.width == 0 || se.height == 0 {
136 return Err(VisionError::EmptyInput("structuring element"));
137 }
138 if se.mask.len() != se.width * se.height {
139 return Err(VisionError::DimensionMismatch {
140 expected: se.width * se.height,
141 got: se.mask.len(),
142 });
143 }
144 if se.anchor_x >= se.width || se.anchor_y >= se.height {
145 return Err(VisionError::Internal(format!(
146 "structuring-element anchor ({}, {}) out of bounds for {}×{}",
147 se.anchor_y, se.anchor_x, se.height, se.width
148 )));
149 }
150 if !se.mask.iter().any(|&m| m) {
151 return Err(VisionError::EmptyInput("structuring element"));
152 }
153 Ok(())
154}
155
156#[inline]
158fn validate_gray(img: &[f32], h: usize, w: usize) -> VisionResult<()> {
159 if h == 0 || w == 0 {
160 return Err(VisionError::InvalidImageSize {
161 height: h,
162 width: w,
163 channels: 1,
164 });
165 }
166 if img.len() != h * w {
167 return Err(VisionError::DimensionMismatch {
168 expected: h * w,
169 got: img.len(),
170 });
171 }
172 Ok(())
173}
174
175fn validate_op(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<()> {
177 validate_gray(img, h, w)?;
178 validate_se(se)?;
179 if se.height > h || se.width > w {
180 return Err(VisionError::Internal(format!(
181 "structuring element {}×{} larger than image {}×{}",
182 se.height, se.width, h, w
183 )));
184 }
185 Ok(())
186}
187
188#[inline]
190fn clamp_idx(v: isize, n: usize) -> usize {
191 if v < 0 {
192 0
193 } else if v as usize >= n {
194 n - 1
195 } else {
196 v as usize
197 }
198}
199
200pub fn erode(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
208 validate_op(img, h, w, se)?;
209 let offsets = se.active_offsets();
210 let mut out = vec![0.0_f32; h * w];
211 for y in 0..h {
212 for x in 0..w {
213 let mut acc = f32::INFINITY;
214 for &(dy, dx) in &offsets {
215 let sy = clamp_idx(y as isize + dy, h);
216 let sx = clamp_idx(x as isize + dx, w);
217 let v = img[sy * w + sx];
218 if v < acc {
219 acc = v;
220 }
221 }
222 out[y * w + x] = acc;
223 }
224 }
225 Ok(out)
226}
227
228pub fn dilate(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
237 validate_op(img, h, w, se)?;
238 let offsets = se.active_offsets();
239 let mut out = vec![0.0_f32; h * w];
240 for y in 0..h {
241 for x in 0..w {
242 let mut acc = f32::NEG_INFINITY;
243 for &(dy, dx) in &offsets {
244 let sy = clamp_idx(y as isize - dy, h);
246 let sx = clamp_idx(x as isize - dx, w);
247 let v = img[sy * w + sx];
248 if v > acc {
249 acc = v;
250 }
251 }
252 out[y * w + x] = acc;
253 }
254 }
255 Ok(out)
256}
257
258pub fn open(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
265 let eroded = erode(img, h, w, se)?;
266 dilate(&eroded, h, w, se)
267}
268
269pub fn close(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
276 let dilated = dilate(img, h, w, se)?;
277 erode(&dilated, h, w, se)
278}
279
280pub fn morphological_gradient(
286 img: &[f32],
287 h: usize,
288 w: usize,
289 se: &StructuringElement,
290) -> VisionResult<Vec<f32>> {
291 let dilated = dilate(img, h, w, se)?;
292 let eroded = erode(img, h, w, se)?;
293 let out = dilated
294 .iter()
295 .zip(eroded.iter())
296 .map(|(&d, &e)| d - e)
297 .collect();
298 Ok(out)
299}
300
301#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn single_pixel(h: usize, w: usize, y: usize, x: usize) -> Vec<f32> {
309 let mut img = vec![0.0_f32; h * w];
310 img[y * w + x] = 1.0;
311 img
312 }
313
314 fn block(h: usize, w: usize, r0: usize, r1: usize, c0: usize, c1: usize) -> Vec<f32> {
316 let mut img = vec![0.0_f32; h * w];
317 for y in r0..r1 {
318 for x in c0..c1 {
319 img[y * w + x] = 1.0;
320 }
321 }
322 img
323 }
324
325 #[test]
326 fn erode_removes_isolated_pixel() {
327 let img = single_pixel(7, 7, 3, 3);
328 let se = StructuringElement::square(3).expect("se");
329 let out = erode(&img, 7, 7, &se).expect("erode");
330 let total: f32 = out.iter().sum();
331 assert_eq!(total, 0.0, "erosion must remove an isolated pixel");
332 }
333
334 #[test]
335 fn dilate_grows_single_pixel_to_se() {
336 let img = single_pixel(5, 5, 2, 2);
337 let se = StructuringElement::square(3).expect("se");
338 let out = dilate(&img, 5, 5, &se).expect("dilate");
339 for y in 0..5 {
340 for x in 0..5 {
341 let expected = if (1..=3).contains(&y) && (1..=3).contains(&x) {
342 1.0
343 } else {
344 0.0
345 };
346 assert_eq!(
347 out[y * 5 + x],
348 expected,
349 "dilation of a point must reproduce the 3×3 element at ({y},{x})"
350 );
351 }
352 }
353 }
354
355 #[test]
356 fn dilate_constant_image_unchanged() {
357 let img = vec![0.37_f32; 6 * 6];
358 let se = StructuringElement::square(3).expect("se");
359 let out = dilate(&img, 6, 6, &se).expect("dilate");
360 assert!(out.iter().all(|&v| (v - 0.37).abs() < 1e-6));
361 }
362
363 #[test]
364 fn erode_constant_image_unchanged() {
365 let img = vec![0.42_f32; 6 * 6];
366 let se = StructuringElement::square(3).expect("se");
367 let out = erode(&img, 6, 6, &se).expect("erode");
368 assert!(out.iter().all(|&v| (v - 0.42).abs() < 1e-6));
369 }
370
371 #[test]
372 fn cross_se_dilation_shape() {
373 let img = single_pixel(5, 5, 2, 2);
374 let se = StructuringElement::cross(1);
375 let out = dilate(&img, 5, 5, &se).expect("dilate");
376 let total: f32 = out.iter().sum();
377 assert_eq!(total, 5.0, "plus-shaped element has 5 members");
378 for &(y, x) in &[(2usize, 2usize), (1, 2), (3, 2), (2, 1), (2, 3)] {
380 assert_eq!(out[y * 5 + x], 1.0);
381 }
382 assert_eq!(out[3 * 5 + 3], 0.0);
384 }
385
386 #[test]
387 fn opening_removes_noise_keeps_blob() {
388 let mut img = block(8, 8, 2, 6, 2, 6);
390 img[0] = 1.0; let se = StructuringElement::square(3).expect("se");
392 let out = open(&img, 8, 8, &se).expect("open");
393 assert_eq!(out[0], 0.0, "opening must remove the isolated noise pixel");
395 let total: f32 = out.iter().sum();
397 assert_eq!(total, 16.0, "opening must preserve the 4×4 blob");
398 for y in 2..6 {
399 for x in 2..6 {
400 assert_eq!(out[y * 8 + x], 1.0);
401 }
402 }
403 }
404
405 #[test]
406 fn closing_fills_small_hole() {
407 let mut img = block(8, 8, 2, 6, 2, 6);
409 img[3 * 8 + 3] = 0.0; let se = StructuringElement::square(3).expect("se");
411 let out = close(&img, 8, 8, &se).expect("close");
412 assert_eq!(out[3 * 8 + 3], 1.0, "closing must fill the interior hole");
413 let total: f32 = out.iter().sum();
415 assert_eq!(total, 16.0);
416 }
417
418 #[test]
419 fn opening_is_idempotent() {
420 let mut img = block(12, 12, 5, 9, 5, 9);
422 img[3 * 12 + 3] = 1.0; let se = StructuringElement::square(3).expect("se");
424 let once = open(&img, 12, 12, &se).expect("open");
425 let twice = open(&once, 12, 12, &se).expect("open");
426 for (a, b) in once.iter().zip(twice.iter()) {
427 assert!((a - b).abs() < 1e-6, "opening must be idempotent");
428 }
429 }
430
431 #[test]
432 fn closing_is_idempotent() {
433 let mut img = block(12, 12, 4, 9, 4, 9);
434 img[6 * 12 + 6] = 0.0; let se = StructuringElement::square(3).expect("se");
436 let once = close(&img, 12, 12, &se).expect("close");
437 let twice = close(&once, 12, 12, &se).expect("close");
438 for (a, b) in once.iter().zip(twice.iter()) {
439 assert!((a - b).abs() < 1e-6, "closing must be idempotent");
440 }
441 }
442
443 #[test]
444 fn morph_gradient_outlines_point() {
445 let img = single_pixel(5, 5, 2, 2);
446 let se = StructuringElement::square(3).expect("se");
447 let grad = morphological_gradient(&img, 5, 5, &se).expect("gradient");
448 let total: f32 = grad.iter().sum();
450 assert_eq!(total, 9.0);
451 assert!(grad.iter().all(|&v| v >= 0.0));
452 }
453
454 #[test]
455 fn se_larger_than_image_errors() {
456 let img = vec![0.0_f32; 4 * 4];
457 let se = StructuringElement::square(7).expect("se");
458 assert!(matches!(
459 erode(&img, 4, 4, &se),
460 Err(VisionError::Internal(_))
461 ));
462 assert!(matches!(
463 dilate(&img, 4, 4, &se),
464 Err(VisionError::Internal(_))
465 ));
466 }
467
468 #[test]
469 fn empty_se_errors() {
470 let r = StructuringElement::new(3, 3, vec![false; 9]);
472 assert!(matches!(r, Err(VisionError::EmptyInput(_))));
473 assert!(matches!(
475 StructuringElement::rect(0, 3),
476 Err(VisionError::EmptyInput(_))
477 ));
478 }
479
480 #[test]
481 fn se_mask_length_mismatch_errors() {
482 let r = StructuringElement::new(3, 3, vec![true; 8]);
483 assert!(matches!(r, Err(VisionError::DimensionMismatch { .. })));
484 }
485
486 #[test]
487 fn wrong_image_size_errors() {
488 let img = vec![0.0_f32; 10];
489 let se = StructuringElement::square(3).expect("se");
490 assert!(matches!(
491 erode(&img, 8, 8, &se),
492 Err(VisionError::DimensionMismatch { .. })
493 ));
494 }
495
496 #[test]
497 fn zero_dim_image_errors() {
498 let img: Vec<f32> = vec![];
499 let se = StructuringElement::square(3).expect("se");
500 assert!(matches!(
501 dilate(&img, 0, 8, &se),
502 Err(VisionError::InvalidImageSize { .. })
503 ));
504 }
505
506 #[test]
507 fn erode_is_anti_extensive_dilate_extensive() {
508 let img = block(8, 8, 2, 6, 1, 5);
510 let se = StructuringElement::square(3).expect("se");
511 let er = erode(&img, 8, 8, &se).expect("erode");
512 let di = dilate(&img, 8, 8, &se).expect("dilate");
513 for ((&e, &f), &d) in er.iter().zip(img.iter()).zip(di.iter()) {
514 assert!(e <= f + 1e-6, "erosion must be anti-extensive");
515 assert!(d + 1e-6 >= f, "dilation must be extensive");
516 }
517 }
518}