Skip to main content

oxigdal_algorithms/raster/
morphology.rs

1//! Morphological operations for rasters
2//!
3//! This module provides mathematical morphology operations:
4//! - Dilation (expands bright regions)
5//! - Erosion (shrinks bright regions)
6//! - Opening (erosion followed by dilation)
7//! - Closing (dilation followed by erosion)
8//! - Morphological gradient (difference between dilation and erosion)
9//! - Top-hat and black-hat transforms
10
11use crate::error::{AlgorithmError, Result};
12use oxigdal_core::buffer::RasterBuffer;
13
14#[cfg(not(feature = "std"))]
15use alloc::vec::Vec;
16
17/// Structuring element shape
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum StructuringElement {
20    /// Square/rectangular element
21    Square {
22        /// Size of the square
23        size: usize,
24    },
25    /// Cross-shaped element
26    Cross {
27        /// Size of the cross
28        size: usize,
29    },
30    /// Disk-shaped element (approximated)
31    Disk {
32        /// Radius of the disk
33        radius: usize,
34    },
35    /// Horizontal line
36    HorizontalLine {
37        /// Length of the line
38        length: usize,
39    },
40    /// Vertical line
41    VerticalLine {
42        /// Length of the line
43        length: usize,
44    },
45}
46
47impl StructuringElement {
48    /// Generates the structuring element as a 2D mask
49    fn generate(&self) -> Result<Vec<Vec<bool>>> {
50        use oxigdal_core::OxiGdalError;
51
52        match self {
53            Self::Square { size } => {
54                if *size == 0 {
55                    return Err(OxiGdalError::invalid_parameter_builder(
56                        "size",
57                        "must be positive, got 0",
58                    )
59                    .with_parameter("value", "0")
60                    .with_parameter("min", "1")
61                    .with_operation("morphology_square")
62                    .with_suggestion("Use positive size. Common values: 3, 5, 7 for standard morphological operations")
63                    .build()
64                    .into());
65                }
66                Ok(vec![vec![true; *size]; *size])
67            }
68            Self::Cross { size } => {
69                if *size == 0 || *size % 2 == 0 {
70                    let suggested = if *size == 0 { 3 } else { size + 1 };
71                    return Err(OxiGdalError::invalid_parameter_builder(
72                        "size",
73                        format!("must be odd and positive, got {}", size),
74                    )
75                    .with_parameter("value", size.to_string())
76                    .with_parameter("min", "1")
77                    .with_operation("morphology_cross")
78                    .with_suggestion(format!(
79                        "Use odd positive size. Try {} instead. Common values: 3, 5, 7",
80                        suggested
81                    ))
82                    .build()
83                    .into());
84                }
85
86                let center = size / 2;
87                let mut mask = vec![vec![false; *size]; *size];
88
89                for i in 0..*size {
90                    mask[center][i] = true;
91                    mask[i][center] = true;
92                }
93
94                Ok(mask)
95            }
96            Self::Disk { radius } => {
97                if *radius == 0 {
98                    return Err(OxiGdalError::invalid_parameter_builder(
99                        "radius",
100                        "must be positive, got 0",
101                    )
102                    .with_parameter("value", "0")
103                    .with_parameter("min", "1")
104                    .with_operation("morphology_disk")
105                    .with_suggestion("Use positive radius. Common values: 1, 2, 3, 5 (in pixels)")
106                    .build()
107                    .into());
108                }
109
110                let size = 2 * radius + 1;
111                let center = *radius as i32;
112                let mut mask = vec![vec![false; size]; size];
113                let r_sq = (radius * radius) as i32;
114
115                for y in 0..size {
116                    for x in 0..size {
117                        let dx = x as i32 - center;
118                        let dy = y as i32 - center;
119                        if dx * dx + dy * dy <= r_sq {
120                            mask[y][x] = true;
121                        }
122                    }
123                }
124
125                Ok(mask)
126            }
127            Self::HorizontalLine { length } => {
128                if *length == 0 {
129                    return Err(OxiGdalError::invalid_parameter_builder(
130                        "length",
131                        "must be positive, got 0",
132                    )
133                    .with_parameter("value", "0")
134                    .with_parameter("min", "1")
135                    .with_operation("morphology_horizontal_line")
136                    .with_suggestion(
137                        "Use positive length. Common values: 3, 5, 7, 9 for edge detection",
138                    )
139                    .build()
140                    .into());
141                }
142
143                Ok(vec![vec![true; *length]])
144            }
145            Self::VerticalLine { length } => {
146                if *length == 0 {
147                    return Err(OxiGdalError::invalid_parameter_builder(
148                        "length",
149                        "must be positive, got 0",
150                    )
151                    .with_parameter("value", "0")
152                    .with_parameter("min", "1")
153                    .with_operation("morphology_vertical_line")
154                    .with_suggestion(
155                        "Use positive length. Common values: 3, 5, 7, 9 for edge detection",
156                    )
157                    .build()
158                    .into());
159                }
160
161                Ok(vec![vec![true]; *length])
162            }
163        }
164    }
165
166    /// Returns the size (width, height) of the structuring element
167    fn size(&self) -> (usize, usize) {
168        match self {
169            Self::Square { size } => (*size, *size),
170            Self::Cross { size } => (*size, *size),
171            Self::Disk { radius } => (2 * radius + 1, 2 * radius + 1),
172            Self::HorizontalLine { length } => (*length, 1),
173            Self::VerticalLine { length } => (1, *length),
174        }
175    }
176}
177
178/// Applies morphological dilation
179///
180/// Expands bright regions in the image
181///
182/// # Arguments
183///
184/// * `src` - Source raster
185/// * `element` - Structuring element
186///
187/// # Errors
188///
189/// Returns an error if operation fails
190pub fn dilate(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
191    let mask = element.generate()?;
192    let (se_width, se_height) = element.size();
193
194    let width = src.width();
195    let height = src.height();
196    let mut dst = RasterBuffer::zeros(width, height, src.data_type());
197
198    let offset_x = (se_width / 2) as i64;
199    let offset_y = (se_height / 2) as i64;
200
201    for y in 0..height {
202        for x in 0..width {
203            let mut max_val = f64::NEG_INFINITY;
204
205            // Apply structuring element
206            for (se_y, row) in mask.iter().enumerate() {
207                for (se_x, &active) in row.iter().enumerate() {
208                    if !active {
209                        continue;
210                    }
211
212                    let px = x as i64 + se_x as i64 - offset_x;
213                    let py = y as i64 + se_y as i64 - offset_y;
214
215                    if px >= 0 && px < width as i64 && py >= 0 && py < height as i64 {
216                        let val = src
217                            .get_pixel(px as u64, py as u64)
218                            .map_err(AlgorithmError::Core)?;
219
220                        if val.is_finite() && !src.is_nodata(val) && val > max_val {
221                            max_val = val;
222                        }
223                    }
224                }
225            }
226
227            if max_val.is_finite() {
228                dst.set_pixel(x, y, max_val).map_err(AlgorithmError::Core)?;
229            }
230        }
231    }
232
233    Ok(dst)
234}
235
236/// Applies morphological erosion
237///
238/// Shrinks bright regions in the image
239///
240/// # Arguments
241///
242/// * `src` - Source raster
243/// * `element` - Structuring element
244///
245/// # Errors
246///
247/// Returns an error if operation fails
248pub fn erode(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
249    let mask = element.generate()?;
250    let (se_width, se_height) = element.size();
251
252    let width = src.width();
253    let height = src.height();
254    let mut dst = RasterBuffer::zeros(width, height, src.data_type());
255
256    let offset_x = (se_width / 2) as i64;
257    let offset_y = (se_height / 2) as i64;
258
259    for y in 0..height {
260        for x in 0..width {
261            let mut min_val = f64::INFINITY;
262
263            // Apply structuring element
264            for (se_y, row) in mask.iter().enumerate() {
265                for (se_x, &active) in row.iter().enumerate() {
266                    if !active {
267                        continue;
268                    }
269
270                    let px = x as i64 + se_x as i64 - offset_x;
271                    let py = y as i64 + se_y as i64 - offset_y;
272
273                    if px >= 0 && px < width as i64 && py >= 0 && py < height as i64 {
274                        let val = src
275                            .get_pixel(px as u64, py as u64)
276                            .map_err(AlgorithmError::Core)?;
277
278                        if val.is_finite() && !src.is_nodata(val) && val < min_val {
279                            min_val = val;
280                        }
281                    }
282                }
283            }
284
285            if min_val.is_finite() {
286                dst.set_pixel(x, y, min_val).map_err(AlgorithmError::Core)?;
287            }
288        }
289    }
290
291    Ok(dst)
292}
293
294/// Applies morphological opening
295///
296/// Opening = Erosion followed by Dilation
297/// Removes small bright features while preserving larger ones
298///
299/// # Arguments
300///
301/// * `src` - Source raster
302/// * `element` - Structuring element
303///
304/// # Errors
305///
306/// Returns an error if operation fails
307pub fn open(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
308    let eroded = erode(src, element)?;
309    dilate(&eroded, element)
310}
311
312/// Applies morphological closing
313///
314/// Closing = Dilation followed by Erosion
315/// Fills small dark features while preserving larger ones
316///
317/// # Arguments
318///
319/// * `src` - Source raster
320/// * `element` - Structuring element
321///
322/// # Errors
323///
324/// Returns an error if operation fails
325pub fn close(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
326    let dilated = dilate(src, element)?;
327    erode(&dilated, element)
328}
329
330/// Computes morphological gradient
331///
332/// Gradient = Dilation - Erosion
333/// Highlights edges and boundaries
334///
335/// # Arguments
336///
337/// * `src` - Source raster
338/// * `element` - Structuring element
339///
340/// # Errors
341///
342/// Returns an error if operation fails
343pub fn morphological_gradient(
344    src: &RasterBuffer,
345    element: StructuringElement,
346) -> Result<RasterBuffer> {
347    let dilated = dilate(src, element)?;
348    let eroded = erode(src, element)?;
349
350    let width = src.width();
351    let height = src.height();
352    let mut gradient = RasterBuffer::zeros(width, height, src.data_type());
353
354    for y in 0..height {
355        for x in 0..width {
356            let dil_val = dilated.get_pixel(x, y).map_err(AlgorithmError::Core)?;
357            let ero_val = eroded.get_pixel(x, y).map_err(AlgorithmError::Core)?;
358            let grad = dil_val - ero_val;
359
360            gradient
361                .set_pixel(x, y, grad)
362                .map_err(AlgorithmError::Core)?;
363        }
364    }
365
366    Ok(gradient)
367}
368
369/// Computes top-hat transform
370///
371/// Top-hat = Original - Opening
372/// Extracts small bright features
373///
374/// # Arguments
375///
376/// * `src` - Source raster
377/// * `element` - Structuring element
378///
379/// # Errors
380///
381/// Returns an error if operation fails
382pub fn top_hat(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
383    let opened = open(src, element)?;
384
385    let width = src.width();
386    let height = src.height();
387    let mut result = RasterBuffer::zeros(width, height, src.data_type());
388
389    for y in 0..height {
390        for x in 0..width {
391            let orig = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
392            let open_val = opened.get_pixel(x, y).map_err(AlgorithmError::Core)?;
393            let top = orig - open_val;
394
395            result.set_pixel(x, y, top).map_err(AlgorithmError::Core)?;
396        }
397    }
398
399    Ok(result)
400}
401
402/// Computes black-hat transform
403///
404/// Black-hat = Closing - Original
405/// Extracts small dark features
406///
407/// # Arguments
408///
409/// * `src` - Source raster
410/// * `element` - Structuring element
411///
412/// # Errors
413///
414/// Returns an error if operation fails
415pub fn black_hat(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
416    let closed = close(src, element)?;
417
418    let width = src.width();
419    let height = src.height();
420    let mut result = RasterBuffer::zeros(width, height, src.data_type());
421
422    for y in 0..height {
423        for x in 0..width {
424            let close_val = closed.get_pixel(x, y).map_err(AlgorithmError::Core)?;
425            let orig = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
426            let black = close_val - orig;
427
428            result
429                .set_pixel(x, y, black)
430                .map_err(AlgorithmError::Core)?;
431        }
432    }
433
434    Ok(result)
435}
436
437/// Computes internal gradient
438///
439/// Internal gradient = Original - Erosion
440///
441/// # Arguments
442///
443/// * `src` - Source raster
444/// * `element` - Structuring element
445///
446/// # Errors
447///
448/// Returns an error if operation fails
449pub fn internal_gradient(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
450    let eroded = erode(src, element)?;
451
452    let width = src.width();
453    let height = src.height();
454    let mut gradient = RasterBuffer::zeros(width, height, src.data_type());
455
456    for y in 0..height {
457        for x in 0..width {
458            let orig = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
459            let ero_val = eroded.get_pixel(x, y).map_err(AlgorithmError::Core)?;
460            let grad = orig - ero_val;
461
462            gradient
463                .set_pixel(x, y, grad)
464                .map_err(AlgorithmError::Core)?;
465        }
466    }
467
468    Ok(gradient)
469}
470
471/// Computes external gradient
472///
473/// External gradient = Dilation - Original
474///
475/// # Arguments
476///
477/// * `src` - Source raster
478/// * `element` - Structuring element
479///
480/// # Errors
481///
482/// Returns an error if operation fails
483pub fn external_gradient(src: &RasterBuffer, element: StructuringElement) -> Result<RasterBuffer> {
484    let dilated = dilate(src, element)?;
485
486    let width = src.width();
487    let height = src.height();
488    let mut gradient = RasterBuffer::zeros(width, height, src.data_type());
489
490    for y in 0..height {
491        for x in 0..width {
492            let dil_val = dilated.get_pixel(x, y).map_err(AlgorithmError::Core)?;
493            let orig = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
494            let grad = dil_val - orig;
495
496            gradient
497                .set_pixel(x, y, grad)
498                .map_err(AlgorithmError::Core)?;
499        }
500    }
501
502    Ok(gradient)
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use oxigdal_core::types::RasterDataType;
509
510    #[test]
511    fn test_structuring_elements() {
512        let square = StructuringElement::Square { size: 3 };
513        let mask = square.generate();
514        assert!(mask.is_ok());
515        let m = mask.expect("Should generate");
516        assert_eq!(m.len(), 3);
517        assert_eq!(m[0].len(), 3);
518
519        let cross = StructuringElement::Cross { size: 3 };
520        let mask = cross.generate();
521        assert!(mask.is_ok());
522
523        let disk = StructuringElement::Disk { radius: 2 };
524        let mask = disk.generate();
525        assert!(mask.is_ok());
526    }
527
528    #[test]
529    fn test_dilate() {
530        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
531
532        // Set center pixel
533        src.set_pixel(5, 5, 100.0).ok();
534
535        let element = StructuringElement::Square { size: 3 };
536        let result = dilate(&src, element);
537        assert!(result.is_ok());
538
539        let dilated = result.expect("Should succeed");
540
541        // Check that dilation expanded the bright region
542        let val = dilated.get_pixel(4, 4).expect("Should get pixel");
543        assert!((val - 100.0).abs() < f64::EPSILON);
544    }
545
546    #[test]
547    fn test_erode() {
548        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
549
550        // Fill a 5x5 region
551        for y in 3..8 {
552            for x in 3..8 {
553                src.set_pixel(x, y, 100.0).ok();
554            }
555        }
556
557        let element = StructuringElement::Square { size: 3 };
558        let result = erode(&src, element);
559        assert!(result.is_ok());
560    }
561
562    #[test]
563    fn test_opening() {
564        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
565
566        for y in 0..10 {
567            for x in 0..10 {
568                src.set_pixel(x, y, 50.0).ok();
569            }
570        }
571
572        // Add small bright spot
573        src.set_pixel(5, 5, 100.0).ok();
574
575        let element = StructuringElement::Square { size: 3 };
576        let result = open(&src, element);
577        assert!(result.is_ok());
578    }
579
580    #[test]
581    fn test_closing() {
582        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
583
584        for y in 0..10 {
585            for x in 0..10 {
586                src.set_pixel(x, y, 100.0).ok();
587            }
588        }
589
590        // Add small dark spot
591        src.set_pixel(5, 5, 0.0).ok();
592
593        let element = StructuringElement::Square { size: 3 };
594        let result = close(&src, element);
595        assert!(result.is_ok());
596    }
597
598    #[test]
599    fn test_morphological_gradient() {
600        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
601
602        // Create a rectangle
603        for y in 3..7 {
604            for x in 3..7 {
605                src.set_pixel(x, y, 100.0).ok();
606            }
607        }
608
609        let element = StructuringElement::Square { size: 3 };
610        let result = morphological_gradient(&src, element);
611        assert!(result.is_ok());
612    }
613
614    #[test]
615    fn test_top_hat() {
616        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
617
618        for y in 0..10 {
619            for x in 0..10 {
620                src.set_pixel(x, y, 50.0).ok();
621            }
622        }
623
624        src.set_pixel(5, 5, 100.0).ok();
625
626        let element = StructuringElement::Square { size: 3 };
627        let result = top_hat(&src, element);
628        assert!(result.is_ok());
629    }
630
631    // ========== Additional Morphological Operations ==========
632
633    #[test]
634    fn test_black_hat() {
635        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
636
637        for y in 0..10 {
638            for x in 0..10 {
639                src.set_pixel(x, y, 100.0).ok();
640            }
641        }
642
643        // Add small dark spot
644        src.set_pixel(5, 5, 0.0).ok();
645
646        let element = StructuringElement::Square { size: 3 };
647        let result = black_hat(&src, element);
648        assert!(result.is_ok());
649    }
650
651    #[test]
652    fn test_internal_gradient() {
653        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
654
655        // Create a filled circle
656        for y in 0..10 {
657            for x in 0..10 {
658                let dx = x as f64 - 5.0;
659                let dy = y as f64 - 5.0;
660                if (dx * dx + dy * dy).sqrt() < 3.0 {
661                    src.set_pixel(x, y, 100.0).ok();
662                }
663            }
664        }
665
666        let element = StructuringElement::Square { size: 3 };
667        let result = internal_gradient(&src, element);
668        assert!(result.is_ok());
669    }
670
671    #[test]
672    fn test_external_gradient() {
673        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
674
675        // Create a filled square
676        for y in 3..7 {
677            for x in 3..7 {
678                src.set_pixel(x, y, 100.0).ok();
679            }
680        }
681
682        let element = StructuringElement::Square { size: 3 };
683        let result = external_gradient(&src, element);
684        assert!(result.is_ok());
685    }
686
687    // ========== Different Structuring Elements ==========
688
689    #[test]
690    fn test_disk_element() {
691        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
692        src.set_pixel(5, 5, 100.0).ok();
693
694        let element = StructuringElement::Disk { radius: 2 };
695        let result = dilate(&src, element);
696        assert!(result.is_ok());
697    }
698
699    #[test]
700    fn test_cross_element() {
701        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
702        src.set_pixel(5, 5, 100.0).ok();
703
704        let element = StructuringElement::Cross { size: 5 };
705        let result = dilate(&src, element);
706        assert!(result.is_ok());
707    }
708
709    #[test]
710    fn test_horizontal_line_element() {
711        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
712        src.set_pixel(5, 5, 100.0).ok();
713
714        let element = StructuringElement::HorizontalLine { length: 5 };
715        let result = dilate(&src, element);
716        assert!(result.is_ok());
717    }
718
719    #[test]
720    fn test_vertical_line_element() {
721        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
722        src.set_pixel(5, 5, 100.0).ok();
723
724        let element = StructuringElement::VerticalLine { length: 5 };
725        let result = dilate(&src, element);
726        assert!(result.is_ok());
727    }
728
729    // ========== Edge Cases ==========
730
731    #[test]
732    fn test_structuring_element_zero_size() {
733        let element = StructuringElement::Square { size: 0 };
734        let result = element.generate();
735        assert!(result.is_err());
736    }
737
738    #[test]
739    fn test_cross_even_size() {
740        let element = StructuringElement::Cross { size: 4 };
741        let result = element.generate();
742        assert!(result.is_err());
743    }
744
745    #[test]
746    fn test_disk_zero_radius() {
747        let element = StructuringElement::Disk { radius: 0 };
748        let result = element.generate();
749        assert!(result.is_err());
750    }
751
752    #[test]
753    fn test_dilate_single_pixel() {
754        let mut src = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
755        src.set_pixel(0, 0, 100.0).ok();
756
757        let element = StructuringElement::Square { size: 1 };
758        let result = dilate(&src, element);
759        assert!(result.is_ok());
760        let dilated = result.expect("Should succeed");
761        let val = dilated.get_pixel(0, 0).expect("Should get pixel");
762        assert!((val - 100.0).abs() < f64::EPSILON);
763    }
764
765    #[test]
766    fn test_erode_single_pixel() {
767        let mut src = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
768        src.set_pixel(0, 0, 100.0).ok();
769
770        let element = StructuringElement::Square { size: 1 };
771        let result = erode(&src, element);
772        assert!(result.is_ok());
773    }
774
775    // ========== Complex Patterns ==========
776
777    #[test]
778    fn test_opening_closing_inverse() {
779        let mut src = RasterBuffer::zeros(15, 15, RasterDataType::Float32);
780
781        // Create pattern with small bright and dark features
782        for y in 0..15 {
783            for x in 0..15 {
784                src.set_pixel(x, y, 50.0).ok();
785            }
786        }
787
788        // Add small bright spot
789        src.set_pixel(5, 5, 100.0).ok();
790        // Add small dark spot
791        src.set_pixel(10, 10, 0.0).ok();
792
793        let element = StructuringElement::Square { size: 3 };
794
795        let opened = open(&src, element);
796        assert!(opened.is_ok());
797
798        let closed = close(&src, element);
799        assert!(closed.is_ok());
800    }
801
802    #[test]
803    fn test_gradient_types_comparison() {
804        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
805
806        // Create a square
807        for y in 3..7 {
808            for x in 3..7 {
809                src.set_pixel(x, y, 100.0).ok();
810            }
811        }
812
813        let element = StructuringElement::Square { size: 3 };
814
815        let morph_grad = morphological_gradient(&src, element);
816        assert!(morph_grad.is_ok());
817
818        let int_grad = internal_gradient(&src, element);
819        assert!(int_grad.is_ok());
820
821        let ext_grad = external_gradient(&src, element);
822        assert!(ext_grad.is_ok());
823    }
824
825    #[test]
826    fn test_dilation_erosion_properties() {
827        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
828
829        for y in 4..6 {
830            for x in 4..6 {
831                src.set_pixel(x, y, 100.0).ok();
832            }
833        }
834
835        let element = StructuringElement::Square { size: 3 };
836
837        let dilated = dilate(&src, element);
838        assert!(dilated.is_ok());
839        let dil = dilated.expect("Should succeed");
840
841        // Dilated region should be larger
842        let val = dil.get_pixel(3, 3).expect("Should get pixel");
843        assert!((val - 100.0).abs() < f64::EPSILON);
844
845        let eroded = erode(&src, element);
846        assert!(eroded.is_ok());
847        let ero = eroded.expect("Should succeed");
848
849        // Eroded region should be smaller or gone
850        let val_ero = ero.get_pixel(4, 4).expect("Should get pixel");
851        assert!(val_ero <= 100.0);
852    }
853
854    #[test]
855    fn test_top_hat_black_hat_duality() {
856        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
857
858        for y in 0..10 {
859            for x in 0..10 {
860                src.set_pixel(x, y, 50.0).ok();
861            }
862        }
863
864        // Add both bright and dark features
865        src.set_pixel(3, 3, 100.0).ok();
866        src.set_pixel(7, 7, 0.0).ok();
867
868        let element = StructuringElement::Square { size: 3 };
869
870        let top = top_hat(&src, element);
871        assert!(top.is_ok());
872
873        let black = black_hat(&src, element);
874        assert!(black.is_ok());
875    }
876
877    // ========== Larger Elements ==========
878
879    #[test]
880    fn test_large_disk_element() {
881        let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
882        src.set_pixel(10, 10, 100.0).ok();
883
884        let element = StructuringElement::Disk { radius: 5 };
885        let result = dilate(&src, element);
886        assert!(result.is_ok());
887    }
888
889    #[test]
890    fn test_large_square_element() {
891        let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
892
893        for y in 8..12 {
894            for x in 8..12 {
895                src.set_pixel(x, y, 100.0).ok();
896            }
897        }
898
899        let element = StructuringElement::Square { size: 7 };
900        let result = erode(&src, element);
901        assert!(result.is_ok());
902    }
903
904    // ========== Real-world Applications ==========
905
906    #[test]
907    fn test_binary_image_cleanup() {
908        let mut src = RasterBuffer::zeros(15, 15, RasterDataType::Float32);
909
910        // Create noisy binary image
911        for y in 0..15 {
912            for x in 0..15 {
913                if (x > 3 && x < 12) && (y > 3 && y < 12) {
914                    src.set_pixel(x, y, 1.0).ok();
915                } else {
916                    src.set_pixel(x, y, 0.0).ok();
917                }
918            }
919        }
920
921        // Add some noise
922        src.set_pixel(1, 1, 1.0).ok(); // Salt
923        src.set_pixel(7, 7, 0.0).ok(); // Pepper
924
925        let element = StructuringElement::Square { size: 3 };
926
927        // Opening removes small bright features
928        let opened = open(&src, element);
929        assert!(opened.is_ok());
930
931        // Closing removes small dark features
932        let closed = close(&src, element);
933        assert!(closed.is_ok());
934    }
935
936    #[test]
937    fn test_boundary_extraction() {
938        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
939
940        // Create a filled square
941        for y in 3..7 {
942            for x in 3..7 {
943                src.set_pixel(x, y, 100.0).ok();
944            }
945        }
946
947        let element = StructuringElement::Square { size: 3 };
948
949        // Internal gradient should extract the inner boundary
950        let boundary = internal_gradient(&src, element);
951        assert!(boundary.is_ok());
952        let bound = boundary.expect("Should succeed");
953
954        // Check that boundary pixels are non-zero
955        let val = bound.get_pixel(3, 3).expect("Should get pixel");
956        assert!(val > 0.0);
957    }
958}