1use crate::error::{NdimageError, NdimageResult};
11use scirs2_core::ndarray::Array2;
12use std::collections::VecDeque;
13
14#[derive(Debug, Clone)]
20pub struct StructuringElement {
21 pub mask: Array2<bool>,
23}
24
25impl StructuringElement {
26 pub fn disk(radius: usize) -> Self {
28 let side = 2 * radius + 1;
29 let c = radius as f64;
30 let mask = Array2::from_shape_fn((side, side), |(r, col)| {
31 let dr = r as f64 - c;
32 let dc = col as f64 - c;
33 dr * dr + dc * dc <= (radius as f64).powi(2) + 1e-9
34 });
35 StructuringElement { mask }
36 }
37
38 pub fn square(half: usize) -> Self {
40 let side = 2 * half + 1;
41 let mask = Array2::from_elem((side, side), true);
42 StructuringElement { mask }
43 }
44
45 pub fn cross(radius: usize) -> Self {
47 let side = 2 * radius + 1;
48 let cr = radius;
49 let mask = Array2::from_shape_fn((side, side), |(r, c)| r == cr || c == cr);
50 StructuringElement { mask }
51 }
52
53 pub fn rows(&self) -> usize {
55 self.mask.nrows()
56 }
57
58 pub fn cols(&self) -> usize {
60 self.mask.ncols()
61 }
62
63 pub fn anchor_row(&self) -> usize {
65 self.rows() / 2
66 }
67
68 pub fn anchor_col(&self) -> usize {
70 self.cols() / 2
71 }
72}
73
74pub fn erode(image: &Array2<f64>, se: &StructuringElement) -> NdimageResult<Array2<f64>> {
78 let rows = image.nrows();
79 let cols = image.ncols();
80 if rows == 0 || cols == 0 {
81 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
82 }
83 let ar = se.anchor_row() as isize;
84 let ac = se.anchor_col() as isize;
85 let mut out = Array2::<f64>::from_elem((rows, cols), f64::INFINITY);
86 for r in 0..rows {
87 for c in 0..cols {
88 let mut min_val = f64::INFINITY;
89 for sr in 0..se.rows() {
90 for sc in 0..se.cols() {
91 if !se.mask[[sr, sc]] {
92 continue;
93 }
94 let nr = r as isize + sr as isize - ar;
95 let nc = c as isize + sc as isize - ac;
96 if nr < 0 || nc < 0 || nr >= rows as isize || nc >= cols as isize {
97 let nr = nr.max(0).min(rows as isize - 1) as usize;
99 let nc = nc.max(0).min(cols as isize - 1) as usize;
100 let v = image[[nr, nc]];
101 if v < min_val {
102 min_val = v;
103 }
104 } else {
105 let v = image[[nr as usize, nc as usize]];
106 if v < min_val {
107 min_val = v;
108 }
109 }
110 }
111 }
112 out[[r, c]] = min_val;
113 }
114 }
115 Ok(out)
116}
117
118pub fn dilate(image: &Array2<f64>, se: &StructuringElement) -> NdimageResult<Array2<f64>> {
120 let rows = image.nrows();
121 let cols = image.ncols();
122 if rows == 0 || cols == 0 {
123 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
124 }
125 let ar = se.anchor_row() as isize;
126 let ac = se.anchor_col() as isize;
127 let mut out = Array2::<f64>::from_elem((rows, cols), f64::NEG_INFINITY);
128 for r in 0..rows {
129 for c in 0..cols {
130 let mut max_val = f64::NEG_INFINITY;
131 for sr in 0..se.rows() {
132 for sc in 0..se.cols() {
133 if !se.mask[[sr, sc]] {
134 continue;
135 }
136 let nr = r as isize + sr as isize - ar;
137 let nc = c as isize + sc as isize - ac;
138 let nr = nr.max(0).min(rows as isize - 1) as usize;
139 let nc = nc.max(0).min(cols as isize - 1) as usize;
140 let v = image[[nr, nc]];
141 if v > max_val {
142 max_val = v;
143 }
144 }
145 }
146 out[[r, c]] = max_val;
147 }
148 }
149 Ok(out)
150}
151
152pub fn rolling_ball_background(image: &Array2<f64>, radius: f64) -> NdimageResult<Array2<f64>> {
169 if radius <= 0.0 {
170 return Err(NdimageError::InvalidInput("radius must be positive".into()));
171 }
172 let rows = image.nrows();
173 let cols = image.ncols();
174 if rows == 0 || cols == 0 {
175 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
176 }
177
178 let r_int = radius.ceil() as usize;
179 let se = StructuringElement::disk(r_int);
180
181 let eroded = erode(image, &se)?;
183 let background = dilate(&eroded, &se)?;
184 Ok(background)
185}
186
187pub fn top_hat(image: &Array2<f64>, se: &StructuringElement) -> NdimageResult<Array2<f64>> {
197 let eroded = erode(image, se)?;
199 let opened = dilate(&eroded, se)?;
200 let rows = image.nrows();
201 let cols = image.ncols();
202 let mut result = Array2::<f64>::zeros((rows, cols));
203 for r in 0..rows {
204 for c in 0..cols {
205 result[[r, c]] = (image[[r, c]] - opened[[r, c]]).max(0.0);
206 }
207 }
208 Ok(result)
209}
210
211pub fn black_hat(image: &Array2<f64>, se: &StructuringElement) -> NdimageResult<Array2<f64>> {
219 let dilated = dilate(image, se)?;
221 let closed = erode(&dilated, se)?;
222 let rows = image.nrows();
223 let cols = image.ncols();
224 let mut result = Array2::<f64>::zeros((rows, cols));
225 for r in 0..rows {
226 for c in 0..cols {
227 result[[r, c]] = (closed[[r, c]] - image[[r, c]]).max(0.0);
228 }
229 }
230 Ok(result)
231}
232
233pub fn morphological_gradient(
243 image: &Array2<f64>,
244 se: &StructuringElement,
245) -> NdimageResult<Array2<f64>> {
246 let dil = dilate(image, se)?;
247 let ero = erode(image, se)?;
248 let rows = image.nrows();
249 let cols = image.ncols();
250 let mut result = Array2::<f64>::zeros((rows, cols));
251 for r in 0..rows {
252 for c in 0..cols {
253 result[[r, c]] = dil[[r, c]] - ero[[r, c]];
254 }
255 }
256 Ok(result)
257}
258
259pub fn toggle_contrast(
272 image: &Array2<f64>,
273 se_inner: &StructuringElement,
274 se_outer: &StructuringElement,
275) -> NdimageResult<Array2<f64>> {
276 let dil = dilate(image, se_outer)?;
277 let ero = erode(image, se_inner)?;
278 let rows = image.nrows();
279 let cols = image.ncols();
280 let mut result = Array2::<f64>::zeros((rows, cols));
281 for r in 0..rows {
282 for c in 0..cols {
283 let v = image[[r, c]];
284 let d = dil[[r, c]];
285 let e = ero[[r, c]];
286 result[[r, c]] = if (v - d).abs() <= (v - e).abs() { d } else { e };
287 }
288 }
289 Ok(result)
290}
291
292pub fn hit_or_miss(
312 image: &Array2<f64>,
313 fg_se: &StructuringElement,
314 bg_se: &StructuringElement,
315) -> NdimageResult<Array2<bool>> {
316 let rows = image.nrows();
317 let cols = image.ncols();
318 if rows == 0 || cols == 0 {
319 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
320 }
321
322 let ero_fg = erode(image, fg_se)?;
323 let dil_bg = dilate(image, bg_se)?;
324
325 let mut result = Array2::<bool>::from_elem((rows, cols), false);
326 for r in 0..rows {
327 for c in 0..cols {
328 result[[r, c]] = ero_fg[[r, c]] > dil_bg[[r, c]];
329 }
330 }
331 Ok(result)
332}
333
334#[cfg(test)]
337mod tests {
338 use super::*;
339 use scirs2_core::ndarray::Array2;
340
341 fn bright_spot_image(rows: usize, cols: usize) -> Array2<f64> {
343 let cr = rows / 2;
344 let cc = cols / 2;
345 Array2::from_shape_fn((rows, cols), |(r, c)| {
346 let dr = r as f64 - cr as f64;
347 let dc = c as f64 - cc as f64;
348 if dr * dr + dc * dc < 4.0 {
349 1.0
350 } else {
351 0.1
352 }
353 })
354 }
355
356 fn step_image(rows: usize, cols: usize) -> Array2<f64> {
357 Array2::from_shape_fn((rows, cols), |(_, c)| if c < cols / 2 { 0.0 } else { 1.0 })
358 }
359
360 #[test]
363 fn test_disk_se_centre_true() {
364 let se = StructuringElement::disk(2);
365 let r = se.anchor_row();
366 let c = se.anchor_col();
367 assert!(se.mask[[r, c]]);
368 }
369
370 #[test]
371 fn test_square_se_all_true() {
372 let se = StructuringElement::square(1);
373 assert_eq!(se.rows(), 3);
374 assert_eq!(se.cols(), 3);
375 assert!(se.mask.iter().all(|&v| v));
376 }
377
378 #[test]
381 fn test_rolling_ball_background_shape() {
382 let img = bright_spot_image(16, 16);
383 let bg = rolling_ball_background(&img, 3.0).expect("rolling ball failed");
384 assert_eq!(bg.shape(), img.shape());
385 }
386
387 #[test]
388 fn test_rolling_ball_background_lte_image() {
389 let img = bright_spot_image(12, 12);
391 let bg = rolling_ball_background(&img, 2.0).expect("rolling ball");
392 for r in 0..img.nrows() {
393 for c in 0..img.ncols() {
394 assert!(
395 bg[[r, c]] <= img[[r, c]] + 1e-9,
396 "bg > img at ({r},{c}): {} > {}",
397 bg[[r, c]],
398 img[[r, c]]
399 );
400 }
401 }
402 }
403
404 #[test]
405 fn test_rolling_ball_invalid_radius() {
406 let img = Array2::<f64>::zeros((4, 4));
407 assert!(rolling_ball_background(&img, 0.0).is_err());
408 assert!(rolling_ball_background(&img, -1.0).is_err());
409 }
410
411 #[test]
414 fn test_top_hat_bright_spot_detected() {
415 let img = bright_spot_image(16, 16);
416 let se = StructuringElement::disk(3);
417 let th = top_hat(&img, &se).expect("top hat failed");
418 assert_eq!(th.shape(), img.shape());
419 let cr = img.nrows() / 2;
421 let cc = img.ncols() / 2;
422 assert!(th[[cr, cc]] > 0.0, "Centre should be > 0 after top-hat");
423 }
424
425 #[test]
426 fn test_top_hat_uniform_image_zero() {
427 let img = Array2::<f64>::from_elem((8, 8), 0.5);
428 let se = StructuringElement::square(1);
429 let th = top_hat(&img, &se).expect("top hat uniform");
430 assert!(th.iter().all(|&v| v.abs() < 1e-10));
432 }
433
434 #[test]
437 fn test_black_hat_dark_hole() {
438 let img = Array2::from_shape_fn((16, 16), |(r, c)| {
440 let cr = 8usize;
441 let cc = 8usize;
442 let dr = r as f64 - cr as f64;
443 let dc = c as f64 - cc as f64;
444 if dr * dr + dc * dc < 4.0 {
445 0.0
446 } else {
447 0.9
448 }
449 });
450 let se = StructuringElement::disk(3);
451 let bh = black_hat(&img, &se).expect("black hat failed");
452 assert_eq!(bh.shape(), img.shape());
453 let cr = img.nrows() / 2;
454 let cc = img.ncols() / 2;
455 assert!(
456 bh[[cr, cc]] > 0.0,
457 "Dark hole should be detected by black-hat"
458 );
459 }
460
461 #[test]
462 fn test_black_hat_uniform_zero() {
463 let img = Array2::<f64>::from_elem((8, 8), 0.5);
464 let se = StructuringElement::square(1);
465 let bh = black_hat(&img, &se).expect("black hat uniform");
466 assert!(bh.iter().all(|&v| v.abs() < 1e-10));
467 }
468
469 #[test]
472 fn test_morphological_gradient_step_edge() {
473 let img = step_image(8, 8);
474 let se = StructuringElement::square(1);
475 let grad = morphological_gradient(&img, &se).expect("morphological gradient failed");
476 assert_eq!(grad.shape(), img.shape());
477 let col = 4; assert!(grad[[4, col]] > 0.0 || grad[[4, col - 1]] > 0.0);
480 }
481
482 #[test]
483 fn test_morphological_gradient_uniform_zero() {
484 let img = Array2::<f64>::from_elem((8, 8), 0.5);
485 let se = StructuringElement::square(1);
486 let grad = morphological_gradient(&img, &se).expect("gradient uniform");
487 assert!(grad.iter().all(|&v| v.abs() < 1e-10));
489 }
490
491 #[test]
494 fn test_toggle_contrast_shape() {
495 let img = bright_spot_image(12, 12);
496 let se_inner = StructuringElement::cross(1);
497 let se_outer = StructuringElement::disk(2);
498 let tc = toggle_contrast(&img, &se_inner, &se_outer).expect("toggle contrast failed");
499 assert_eq!(tc.shape(), img.shape());
500 }
501
502 #[test]
503 fn test_toggle_contrast_extreme_values() {
504 let img = Array2::<f64>::from_elem((6, 6), 0.5);
506 let se = StructuringElement::square(1);
507 let tc = toggle_contrast(&img, &se, &se).expect("toggle contrast const");
508 assert!(tc.iter().all(|&v| v.is_finite()));
509 }
510
511 #[test]
514 fn test_hit_or_miss_bright_peak() {
515 let mut img = Array2::<f64>::from_elem((8, 8), 0.0);
517 img[[4, 4]] = 1.0;
518 let fg_se = StructuringElement::disk(0); let bg_se = StructuringElement::disk(0); let fg_se = StructuringElement {
522 mask: {
523 let mut m = Array2::from_elem((3, 3), false);
524 m[[1, 1]] = true;
525 m
526 },
527 };
528 let bg_se = StructuringElement {
529 mask: {
530 let mut m = Array2::from_elem((3, 3), false);
531 m[[0, 1]] = true;
532 m[[2, 1]] = true;
533 m[[1, 0]] = true;
534 m[[1, 2]] = true;
535 m
536 },
537 };
538 let hom = hit_or_miss(&img, &fg_se, &bg_se).expect("hit or miss failed");
539 assert_eq!(hom.shape(), img.shape());
540 assert!(hom[[4, 4]], "Bright isolated pixel should produce a hit");
542 }
543
544 #[test]
545 fn test_hit_or_miss_no_hit_uniform() {
546 let img = Array2::<f64>::from_elem((8, 8), 0.5);
548 let se = StructuringElement::square(1);
549 let hom = hit_or_miss(&img, &se, &se).expect("hit or miss uniform");
550 assert!(!hom.iter().any(|&v| v), "Uniform image should have no hits");
552 }
553}