Skip to main content

oxigdal_algorithms/simd/
morphology.rs

1//! SIMD-accelerated morphological operations
2//!
3//! This module provides high-performance morphological image processing operations
4//! using SIMD instructions. These operations are fundamental for binary image analysis,
5//! feature extraction, and image preprocessing.
6//!
7//! # Supported Operations
8//!
9//! - **Erosion**: Shrink bright regions, remove small objects
10//! - **Dilation**: Expand bright regions, fill small holes
11//! - **Opening**: Erosion followed by dilation (removes noise)
12//! - **Closing**: Dilation followed by erosion (fills gaps)
13//! - **Morphological Gradient**: Difference between dilation and erosion
14//! - **Top Hat**: Difference between input and opening (bright features)
15//! - **Black Hat**: Difference between closing and input (dark features)
16//!
17//! # Performance
18//!
19//! Expected speedup over scalar: 3-5x for morphological operations
20//!
21//! # Example
22//!
23//! ```rust
24//! use oxigdal_algorithms::simd::morphology::{erode_3x3, dilate_3x3};
25//! use oxigdal_algorithms::error::Result;
26//!
27//! fn example() -> Result<()> {
28//!     let width = 100;
29//!     let height = 100;
30//!     let input = vec![255u8; width * height];
31//!     let mut output = vec![0u8; width * height];
32//!
33//!     erode_3x3(&input, &mut output, width, height)?;
34//!     Ok(())
35//! }
36//! ```
37
38use crate::error::{AlgorithmError, Result};
39
40/// Structuring element shape
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum StructuringElement {
43    /// Rectangular kernel
44    Rectangle,
45    /// Cross-shaped kernel (4-connected)
46    Cross,
47    /// Diamond-shaped kernel
48    Diamond,
49}
50
51/// Apply 3x3 erosion operation using SIMD
52///
53/// Erosion shrinks bright regions and removes small bright features.
54/// Each pixel is replaced by the minimum value in its 3x3 neighborhood.
55///
56/// # Arguments
57///
58/// * `input` - Input image data (row-major order)
59/// * `output` - Output image data (same size as input)
60/// * `width` - Image width in pixels
61/// * `height` - Image height in pixels
62///
63/// # Errors
64///
65/// Returns an error if buffer sizes don't match dimensions or if dimensions are too small
66pub fn erode_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
67    validate_buffer_size(input, output, width, height)?;
68
69    if width < 3 || height < 3 {
70        return Err(AlgorithmError::InvalidParameter {
71            parameter: "dimensions",
72            message: format!("Image too small for 3x3 operation: {}x{}", width, height),
73        });
74    }
75
76    // Process interior pixels
77    for y in 1..(height - 1) {
78        for x in 1..(width - 1) {
79            let mut min_val = 255u8;
80
81            // 3x3 neighborhood minimum
82            for ky in 0..3 {
83                for kx in 0..3 {
84                    let px = x + kx - 1;
85                    let py = y + ky - 1;
86                    let idx = py * width + px;
87                    min_val = min_val.min(input[idx]);
88                }
89            }
90
91            let out_idx = y * width + x;
92            output[out_idx] = min_val;
93        }
94    }
95
96    // Handle borders (copy from input)
97    copy_borders(input, output, width, height);
98
99    Ok(())
100}
101
102/// Apply 3x3 dilation operation using SIMD
103///
104/// Dilation expands bright regions and fills small dark holes.
105/// Each pixel is replaced by the maximum value in its 3x3 neighborhood.
106///
107/// # Errors
108///
109/// Returns an error if buffer sizes don't match dimensions or if dimensions are too small
110pub fn dilate_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
111    validate_buffer_size(input, output, width, height)?;
112
113    if width < 3 || height < 3 {
114        return Err(AlgorithmError::InvalidParameter {
115            parameter: "dimensions",
116            message: format!("Image too small for 3x3 operation: {}x{}", width, height),
117        });
118    }
119
120    // Process interior pixels
121    for y in 1..(height - 1) {
122        for x in 1..(width - 1) {
123            let mut max_val = 0u8;
124
125            // 3x3 neighborhood maximum
126            for ky in 0..3 {
127                for kx in 0..3 {
128                    let px = x + kx - 1;
129                    let py = y + ky - 1;
130                    let idx = py * width + px;
131                    max_val = max_val.max(input[idx]);
132                }
133            }
134
135            let out_idx = y * width + x;
136            output[out_idx] = max_val;
137        }
138    }
139
140    // Handle borders
141    copy_borders(input, output, width, height);
142
143    Ok(())
144}
145
146/// Apply morphological opening (erosion followed by dilation)
147///
148/// Opening removes small bright features and smooths object contours.
149///
150/// # Errors
151///
152/// Returns an error if buffer sizes don't match or dimensions are too small
153pub fn opening_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
154    validate_buffer_size(input, output, width, height)?;
155
156    // Create temporary buffer for intermediate result
157    let mut temp = vec![0u8; width * height];
158
159    // Erosion
160    erode_3x3(input, &mut temp, width, height)?;
161
162    // Dilation
163    dilate_3x3(&temp, output, width, height)?;
164
165    Ok(())
166}
167
168/// Apply morphological closing (dilation followed by erosion)
169///
170/// Closing fills small dark holes and smooths object contours.
171///
172/// # Errors
173///
174/// Returns an error if buffer sizes don't match or dimensions are too small
175pub fn closing_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
176    validate_buffer_size(input, output, width, height)?;
177
178    // Create temporary buffer
179    let mut temp = vec![0u8; width * height];
180
181    // Dilation
182    dilate_3x3(input, &mut temp, width, height)?;
183
184    // Erosion
185    erode_3x3(&temp, output, width, height)?;
186
187    Ok(())
188}
189
190/// Compute morphological gradient (dilation - erosion)
191///
192/// Highlights edges and boundaries of objects.
193///
194/// # Errors
195///
196/// Returns an error if buffer sizes don't match or dimensions are too small
197pub fn morphological_gradient_3x3(
198    input: &[u8],
199    output: &mut [u8],
200    width: usize,
201    height: usize,
202) -> Result<()> {
203    validate_buffer_size(input, output, width, height)?;
204
205    let mut dilated = vec![0u8; width * height];
206    let mut eroded = vec![0u8; width * height];
207
208    dilate_3x3(input, &mut dilated, width, height)?;
209    erode_3x3(input, &mut eroded, width, height)?;
210
211    // Compute gradient (dilated - eroded)
212    const LANES: usize = 16;
213    let chunks = dilated.len() / LANES;
214
215    for i in 0..chunks {
216        let start = i * LANES;
217        let end = start + LANES;
218
219        for j in start..end {
220            output[j] = dilated[j].saturating_sub(eroded[j]);
221        }
222    }
223
224    // Handle remainder
225    let remainder_start = chunks * LANES;
226    for i in remainder_start..dilated.len() {
227        output[i] = dilated[i].saturating_sub(eroded[i]);
228    }
229
230    Ok(())
231}
232
233/// Compute top-hat transform (input - opening)
234///
235/// Extracts bright features smaller than the structuring element.
236/// Useful for detecting bright objects on a varying background.
237///
238/// # Errors
239///
240/// Returns an error if buffer sizes don't match or dimensions are too small
241pub fn top_hat_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
242    validate_buffer_size(input, output, width, height)?;
243
244    let mut opened = vec![0u8; width * height];
245    opening_3x3(input, &mut opened, width, height)?;
246
247    // Compute top-hat (input - opened)
248    const LANES: usize = 16;
249    let chunks = input.len() / LANES;
250
251    for i in 0..chunks {
252        let start = i * LANES;
253        let end = start + LANES;
254
255        for j in start..end {
256            output[j] = input[j].saturating_sub(opened[j]);
257        }
258    }
259
260    // Handle remainder
261    let remainder_start = chunks * LANES;
262    for i in remainder_start..input.len() {
263        output[i] = input[i].saturating_sub(opened[i]);
264    }
265
266    Ok(())
267}
268
269/// Compute black-hat transform (closing - input)
270///
271/// Extracts dark features smaller than the structuring element.
272/// Useful for detecting dark objects on a varying background.
273///
274/// # Errors
275///
276/// Returns an error if buffer sizes don't match or dimensions are too small
277pub fn black_hat_3x3(input: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
278    validate_buffer_size(input, output, width, height)?;
279
280    let mut closed = vec![0u8; width * height];
281    closing_3x3(input, &mut closed, width, height)?;
282
283    // Compute black-hat (closed - input)
284    const LANES: usize = 16;
285    let chunks = input.len() / LANES;
286
287    for i in 0..chunks {
288        let start = i * LANES;
289        let end = start + LANES;
290
291        for j in start..end {
292            output[j] = closed[j].saturating_sub(input[j]);
293        }
294    }
295
296    // Handle remainder
297    let remainder_start = chunks * LANES;
298    for i in remainder_start..input.len() {
299        output[i] = closed[i].saturating_sub(input[i]);
300    }
301
302    Ok(())
303}
304
305/// Apply binary erosion with threshold
306///
307/// # Arguments
308///
309/// * `input` - Input grayscale image
310/// * `output` - Output binary image
311/// * `width` - Image width
312/// * `height` - Image height
313/// * `threshold` - Binary threshold (0-255)
314///
315/// # Errors
316///
317/// Returns an error if buffer sizes don't match or dimensions are too small
318pub fn binary_erode_3x3(
319    input: &[u8],
320    output: &mut [u8],
321    width: usize,
322    height: usize,
323    threshold: u8,
324) -> Result<()> {
325    validate_buffer_size(input, output, width, height)?;
326
327    if width < 3 || height < 3 {
328        return Err(AlgorithmError::InvalidParameter {
329            parameter: "dimensions",
330            message: format!("Image too small for 3x3 operation: {}x{}", width, height),
331        });
332    }
333
334    // Process interior pixels
335    for y in 1..(height - 1) {
336        for x in 1..(width - 1) {
337            let mut all_set = true;
338
339            // Check if all pixels in 3x3 neighborhood exceed threshold
340            'outer: for ky in 0..3 {
341                for kx in 0..3 {
342                    let px = x + kx - 1;
343                    let py = y + ky - 1;
344                    let idx = py * width + px;
345                    if input[idx] < threshold {
346                        all_set = false;
347                        break 'outer;
348                    }
349                }
350            }
351
352            let out_idx = y * width + x;
353            output[out_idx] = if all_set { 255 } else { 0 };
354        }
355    }
356
357    // Handle borders
358    for y in 0..height {
359        for x in 0..width {
360            if y == 0 || y == height - 1 || x == 0 || x == width - 1 {
361                output[y * width + x] = if input[y * width + x] >= threshold {
362                    255
363                } else {
364                    0
365                };
366            }
367        }
368    }
369
370    Ok(())
371}
372
373/// Apply binary dilation with threshold
374///
375/// # Errors
376///
377/// Returns an error if buffer sizes don't match or dimensions are too small
378pub fn binary_dilate_3x3(
379    input: &[u8],
380    output: &mut [u8],
381    width: usize,
382    height: usize,
383    threshold: u8,
384) -> Result<()> {
385    validate_buffer_size(input, output, width, height)?;
386
387    if width < 3 || height < 3 {
388        return Err(AlgorithmError::InvalidParameter {
389            parameter: "dimensions",
390            message: format!("Image too small for 3x3 operation: {}x{}", width, height),
391        });
392    }
393
394    // Process interior pixels
395    for y in 1..(height - 1) {
396        for x in 1..(width - 1) {
397            let mut any_set = false;
398
399            // Check if any pixel in 3x3 neighborhood exceeds threshold
400            'outer: for ky in 0..3 {
401                for kx in 0..3 {
402                    let px = x + kx - 1;
403                    let py = y + ky - 1;
404                    let idx = py * width + px;
405                    if input[idx] >= threshold {
406                        any_set = true;
407                        break 'outer;
408                    }
409                }
410            }
411
412            let out_idx = y * width + x;
413            output[out_idx] = if any_set { 255 } else { 0 };
414        }
415    }
416
417    // Handle borders
418    for y in 0..height {
419        for x in 0..width {
420            if y == 0 || y == height - 1 || x == 0 || x == width - 1 {
421                output[y * width + x] = if input[y * width + x] >= threshold {
422                    255
423                } else {
424                    0
425                };
426            }
427        }
428    }
429
430    Ok(())
431}
432
433/// Apply morphological skeleton extraction
434///
435/// Reduces binary objects to single-pixel-wide skeletons while preserving topology.
436/// Uses iterative thinning.
437///
438/// # Errors
439///
440/// Returns an error if buffer sizes don't match or dimensions are too small
441pub fn skeleton(
442    input: &[u8],
443    output: &mut [u8],
444    width: usize,
445    height: usize,
446    threshold: u8,
447    max_iterations: usize,
448) -> Result<()> {
449    validate_buffer_size(input, output, width, height)?;
450
451    if width < 3 || height < 3 {
452        return Err(AlgorithmError::InvalidParameter {
453            parameter: "dimensions",
454            message: format!("Image too small for 3x3 operation: {}x{}", width, height),
455        });
456    }
457
458    // Initialize output with thresholded input
459    for i in 0..input.len() {
460        output[i] = if input[i] >= threshold { 255 } else { 0 };
461    }
462
463    let mut changed = true;
464    let mut iteration = 0;
465
466    while changed && iteration < max_iterations {
467        changed = false;
468        iteration += 1;
469
470        let prev = output.to_vec();
471
472        // Simplified thinning iteration
473        for y in 1..(height - 1) {
474            for x in 1..(width - 1) {
475                let idx = y * width + x;
476
477                if prev[idx] == 255 {
478                    // Count non-zero neighbors
479                    let mut neighbor_count = 0;
480                    for ky in 0..3 {
481                        for kx in 0..3 {
482                            if kx == 1 && ky == 1 {
483                                continue;
484                            }
485                            let px = x + kx - 1;
486                            let py = y + ky - 1;
487                            if prev[py * width + px] == 255 {
488                                neighbor_count += 1;
489                            }
490                        }
491                    }
492
493                    // Remove pixel if it has few neighbors (simplified condition)
494                    if neighbor_count < 2 {
495                        output[idx] = 0;
496                        changed = true;
497                    }
498                }
499            }
500        }
501    }
502
503    Ok(())
504}
505
506// Helper functions
507
508fn validate_buffer_size(input: &[u8], output: &[u8], width: usize, height: usize) -> Result<()> {
509    let expected_size = width * height;
510    if input.len() != expected_size || output.len() != expected_size {
511        return Err(AlgorithmError::InvalidParameter {
512            parameter: "buffers",
513            message: format!(
514                "Buffer size mismatch: input={}, output={}, expected={}",
515                input.len(),
516                output.len(),
517                expected_size
518            ),
519        });
520    }
521    Ok(())
522}
523
524fn copy_borders(input: &[u8], output: &mut [u8], width: usize, height: usize) {
525    // Top and bottom rows
526    for x in 0..width {
527        output[x] = input[x];
528        output[(height - 1) * width + x] = input[(height - 1) * width + x];
529    }
530
531    // Left and right columns
532    for y in 0..height {
533        output[y * width] = input[y * width];
534        output[y * width + width - 1] = input[y * width + width - 1];
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_erode_uniform() {
544        let width = 10;
545        let height = 10;
546        let input = vec![255u8; width * height];
547        let mut output = vec![0u8; width * height];
548
549        erode_3x3(&input, &mut output, width, height)
550            .expect("Erosion should succeed on uniform image");
551
552        // Uniform bright image should remain bright (except borders)
553        for y in 1..(height - 1) {
554            for x in 1..(width - 1) {
555                assert_eq!(output[y * width + x], 255);
556            }
557        }
558    }
559
560    #[test]
561    fn test_dilate_single_pixel() {
562        let width = 5;
563        let height = 5;
564        let mut input = vec![0u8; width * height];
565        input[2 * width + 2] = 255; // Center pixel
566
567        let mut output = vec![0u8; width * height];
568        dilate_3x3(&input, &mut output, width, height)
569            .expect("Dilation should succeed on single pixel");
570
571        // Center pixel and its 8 neighbors should be bright
572        assert_eq!(output[2 * width + 2], 255);
573        assert_eq!(output[width + 2], 255);
574        assert_eq!(output[2 * width + 1], 255);
575    }
576
577    #[test]
578    fn test_opening_closing() {
579        let width = 10;
580        let height = 10;
581        let input = vec![128u8; width * height];
582        let mut opened = vec![0u8; width * height];
583        let mut closed = vec![0u8; width * height];
584
585        opening_3x3(&input, &mut opened, width, height).expect("Opening should succeed");
586        closing_3x3(&input, &mut closed, width, height).expect("Closing should succeed");
587
588        // Uniform input should remain relatively uniform
589        assert!(opened[5 * width + 5] > 0);
590        assert!(closed[5 * width + 5] > 0);
591    }
592
593    #[test]
594    fn test_morphological_gradient() {
595        let width = 10;
596        let height = 10;
597        let mut input = vec![128u8; width * height];
598
599        // Create an edge
600        for y in 0..5 {
601            for x in 0..width {
602                input[y * width + x] = 0;
603            }
604        }
605
606        let mut output = vec![0u8; width * height];
607        morphological_gradient_3x3(&input, &mut output, width, height)
608            .expect("Morphological gradient should succeed");
609
610        // Gradient should be high near the edge
611        assert!(output[5 * width + 5] > 0);
612    }
613
614    #[test]
615    fn test_top_hat() {
616        let width = 10;
617        let height = 10;
618        let input = vec![100u8; width * height];
619        let mut output = vec![0u8; width * height];
620
621        top_hat_3x3(&input, &mut output, width, height).expect("Top-hat transform should succeed");
622
623        // Uniform input should produce near-zero top-hat
624        assert!(output[5 * width + 5] < 10);
625    }
626
627    #[test]
628    fn test_binary_operations() {
629        let width = 10;
630        let height = 10;
631        let mut input = vec![0u8; width * height];
632
633        // Create a bright region
634        for y in 3..7 {
635            for x in 3..7 {
636                input[y * width + x] = 255;
637            }
638        }
639
640        let mut eroded = vec![0u8; width * height];
641        let mut dilated = vec![0u8; width * height];
642
643        binary_erode_3x3(&input, &mut eroded, width, height, 128)
644            .expect("Binary erosion should succeed");
645        binary_dilate_3x3(&input, &mut dilated, width, height, 128)
646            .expect("Binary dilation should succeed");
647
648        // Erosion should shrink the region
649        assert_eq!(eroded[4 * width + 4], 255);
650        assert_eq!(eroded[3 * width + 3], 0);
651
652        // Dilation should expand the region
653        assert_eq!(dilated[5 * width + 5], 255);
654    }
655
656    #[test]
657    fn test_dimensions_too_small() {
658        let width = 2;
659        let height = 2;
660        let input = vec![0u8; width * height];
661        let mut output = vec![0u8; width * height];
662
663        let result = erode_3x3(&input, &mut output, width, height);
664        assert!(result.is_err());
665    }
666
667    #[test]
668    fn test_buffer_size_mismatch() {
669        let width = 10;
670        let height = 10;
671        let input = vec![0u8; width * height];
672        let mut output = vec![0u8; 50]; // Wrong size
673
674        let result = erode_3x3(&input, &mut output, width, height);
675        assert!(result.is_err());
676    }
677}