Skip to main content

spatialrust_vision/
morphology.rs

1//! CPU mathematical morphology with explicit structuring elements and borders.
2
3use pulp::Arch;
4use rayon::prelude::*;
5use spatialrust_image::{Image, ImageView};
6
7use crate::border::{fetch, map_index};
8use crate::dispatch::{
9    bounded_workers, items_per_worker, should_parallelize, LARGE_PARALLEL_COMPONENTS, ROWS_PER_TILE,
10};
11use crate::{BorderMode, PixelComponent, VisionError, VisionResult};
12
13/// Built-in structuring-element geometry.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum MorphologyShape {
16    /// Every element in the bounding rectangle is active.
17    Rect,
18    /// The anchor row and column are active.
19    Cross,
20    /// A filled ellipse inscribed in the bounding rectangle.
21    Ellipse,
22    /// A filled Manhattan-distance diamond.
23    Diamond,
24}
25
26/// A validated binary neighborhood mask and anchor.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct StructuringElement {
29    width: usize,
30    height: usize,
31    anchor_x: usize,
32    anchor_y: usize,
33    mask: Vec<bool>,
34}
35
36impl StructuringElement {
37    /// Creates a built-in element anchored at its integer center.
38    pub fn try_new(shape: MorphologyShape, width: usize, height: usize) -> VisionResult<Self> {
39        Self::try_new_with_anchor(shape, width, height, width / 2, height / 2)
40    }
41
42    /// Creates a built-in element with an explicit anchor.
43    pub fn try_new_with_anchor(
44        shape: MorphologyShape,
45        width: usize,
46        height: usize,
47        anchor_x: usize,
48        anchor_y: usize,
49    ) -> VisionResult<Self> {
50        validate_dimensions(width, height, anchor_x, anchor_y)?;
51        let mut mask = vec![false; width * height];
52        match shape {
53            MorphologyShape::Rect => mask.fill(true),
54            MorphologyShape::Cross => {
55                for y in 0..height {
56                    for x in 0..width {
57                        mask[y * width + x] = x == anchor_x || y == anchor_y;
58                    }
59                }
60            }
61            MorphologyShape::Ellipse => fill_ellipse(&mut mask, width, height),
62            MorphologyShape::Diamond => {
63                let rx = anchor_x.max(width - 1 - anchor_x).max(1);
64                let ry = anchor_y.max(height - 1 - anchor_y).max(1);
65                for y in 0..height {
66                    for x in 0..width {
67                        let dx = x.abs_diff(anchor_x) as f64 / rx as f64;
68                        let dy = y.abs_diff(anchor_y) as f64 / ry as f64;
69                        mask[y * width + x] = dx + dy <= 1.0 + f64::EPSILON;
70                    }
71                }
72            }
73        }
74        Self::try_from_mask(width, height, anchor_x, anchor_y, mask)
75    }
76
77    /// Creates an element from a row-major binary mask.
78    pub fn try_from_mask(
79        width: usize,
80        height: usize,
81        anchor_x: usize,
82        anchor_y: usize,
83        mask: Vec<bool>,
84    ) -> VisionResult<Self> {
85        validate_dimensions(width, height, anchor_x, anchor_y)?;
86        let expected = width
87            .checked_mul(height)
88            .ok_or_else(|| VisionError::InvalidParameter("structuring element overflows".into()))?;
89        if mask.len() != expected {
90            return Err(VisionError::ShapeMismatch(format!(
91                "structuring element needs {expected} mask values, found {}",
92                mask.len()
93            )));
94        }
95        if !mask.iter().any(|&active| active) {
96            return Err(VisionError::InvalidParameter(
97                "structuring element must contain an active sample".into(),
98            ));
99        }
100        Ok(Self { width, height, anchor_x, anchor_y, mask })
101    }
102
103    /// Element width.
104    #[must_use]
105    pub const fn width(&self) -> usize {
106        self.width
107    }
108
109    /// Element height.
110    #[must_use]
111    pub const fn height(&self) -> usize {
112        self.height
113    }
114
115    /// Anchor coordinate `(x, y)`.
116    #[must_use]
117    pub const fn anchor(&self) -> (usize, usize) {
118        (self.anchor_x, self.anchor_y)
119    }
120
121    /// Row-major binary mask.
122    #[must_use]
123    pub fn mask(&self) -> &[bool] {
124        &self.mask
125    }
126}
127
128/// Composite morphology operation.
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
130pub enum MorphologyOperation {
131    /// Erosion followed by dilation.
132    Open,
133    /// Dilation followed by erosion.
134    Close,
135    /// Dilation minus erosion.
136    Gradient,
137    /// Input minus opening.
138    TopHat,
139    /// Closing minus input.
140    BlackHat,
141}
142
143/// Erodes each channel independently.
144pub fn erode<T: PixelComponent, const CHANNELS: usize>(
145    input: ImageView<'_, T, CHANNELS>,
146    element: &StructuringElement,
147    iterations: usize,
148    border: BorderMode<T, CHANNELS>,
149) -> VisionResult<Image<T, CHANNELS>> {
150    repeat_extreme(input, element, iterations, border, Extreme::Minimum)
151}
152
153/// Dilates each channel independently.
154pub fn dilate<T: PixelComponent, const CHANNELS: usize>(
155    input: ImageView<'_, T, CHANNELS>,
156    element: &StructuringElement,
157    iterations: usize,
158    border: BorderMode<T, CHANNELS>,
159) -> VisionResult<Image<T, CHANNELS>> {
160    repeat_extreme(input, element, iterations, border, Extreme::Maximum)
161}
162
163/// Applies a composite morphology operation.
164pub fn morphology_ex<T: PixelComponent, const CHANNELS: usize>(
165    input: ImageView<'_, T, CHANNELS>,
166    operation: MorphologyOperation,
167    element: &StructuringElement,
168    iterations: usize,
169    border: BorderMode<T, CHANNELS>,
170) -> VisionResult<Image<T, CHANNELS>> {
171    let original = pack(input)?;
172    match operation {
173        MorphologyOperation::Open => {
174            let eroded = erode(input, element, iterations, border)?;
175            dilate(eroded.view(), element, iterations, border)
176        }
177        MorphologyOperation::Close => {
178            let dilated = dilate(input, element, iterations, border)?;
179            erode(dilated.view(), element, iterations, border)
180        }
181        MorphologyOperation::Gradient => {
182            let high = dilate(input, element, iterations, border)?;
183            let low = erode(input, element, iterations, border)?;
184            subtract(high.view(), low.view())
185        }
186        MorphologyOperation::TopHat => {
187            let opened =
188                morphology_ex(input, MorphologyOperation::Open, element, iterations, border)?;
189            subtract(original.view(), opened.view())
190        }
191        MorphologyOperation::BlackHat => {
192            let closed =
193                morphology_ex(input, MorphologyOperation::Close, element, iterations, border)?;
194            subtract(closed.view(), original.view())
195        }
196    }
197}
198
199/// Erodes a grayscale `u8` image with a rectangular element in linear time.
200///
201/// This specialized path is independent of the rectangle area and preserves
202/// the generic implementation's anchor, border, iteration, and metadata
203/// semantics.
204pub fn erode_rect_u8(
205    input: ImageView<'_, u8, 1>,
206    element: &StructuringElement,
207    iterations: usize,
208    border: BorderMode<u8, 1>,
209) -> VisionResult<Image<u8, 1>> {
210    let mut output = vec![0; input.width() * input.height()];
211    let mut workspace = RectMorphologyWorkspace::new();
212    erode_rect_u8_into(input, element, iterations, border, &mut output, &mut workspace)?;
213    Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
214}
215
216/// Dilates a grayscale `u8` image with a rectangular element in linear time.
217pub fn dilate_rect_u8(
218    input: ImageView<'_, u8, 1>,
219    element: &StructuringElement,
220    iterations: usize,
221    border: BorderMode<u8, 1>,
222) -> VisionResult<Image<u8, 1>> {
223    let mut output = vec![0; input.width() * input.height()];
224    let mut workspace = RectMorphologyWorkspace::new();
225    dilate_rect_u8_into(input, element, iterations, border, &mut output, &mut workspace)?;
226    Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
227}
228
229/// Applies composite grayscale `u8` morphology using the rectangular fast path.
230pub fn morphology_rect_u8(
231    input: ImageView<'_, u8, 1>,
232    operation: MorphologyOperation,
233    element: &StructuringElement,
234    iterations: usize,
235    border: BorderMode<u8, 1>,
236) -> VisionResult<Image<u8, 1>> {
237    let mut output = vec![0; input.width() * input.height()];
238    let mut workspace = RectMorphologyWorkspace::new();
239    morphology_rect_u8_into(
240        input,
241        operation,
242        element,
243        iterations,
244        border,
245        &mut output,
246        &mut workspace,
247    )?;
248    Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
249}
250
251/// Reusable scratch storage for packed grayscale rectangular morphology.
252///
253/// The workspace owns every full-image intermediate and one set of line
254/// buffers per Rayon worker. Grow it once for the largest expected image and
255/// element, then reuse it on the same worker thread. It never performs a
256/// hidden device transfer and does not share mutable state between calls.
257#[derive(Debug, Default)]
258pub struct RectMorphologyWorkspace {
259    current: Vec<u8>,
260    horizontal: Vec<u8>,
261    transposed: Vec<u8>,
262    filtered: Vec<u8>,
263    output: Vec<u8>,
264    padded: Vec<u8>,
265    prefix: Vec<u8>,
266    suffix: Vec<u8>,
267    line_buffers: Vec<LineBuffers>,
268    centered_5x5_active: bool,
269}
270
271impl RectMorphologyWorkspace {
272    /// Creates an empty workspace that grows on first use.
273    #[must_use]
274    pub const fn new() -> Self {
275        Self {
276            current: Vec::new(),
277            horizontal: Vec::new(),
278            transposed: Vec::new(),
279            filtered: Vec::new(),
280            output: Vec::new(),
281            padded: Vec::new(),
282            prefix: Vec::new(),
283            suffix: Vec::new(),
284            line_buffers: Vec::new(),
285            centered_5x5_active: false,
286        }
287    }
288
289    /// Returns the largest pixel count reserved by every active full-image plane.
290    #[must_use]
291    pub fn capacity(&self) -> usize {
292        let active =
293            self.current.capacity().min(self.horizontal.capacity()).min(self.output.capacity());
294        if self.centered_5x5_active {
295            active
296        } else {
297            active.min(self.transposed.capacity()).min(self.filtered.capacity())
298        }
299    }
300
301    /// Returns the number of reusable parallel line-buffer sets.
302    #[must_use]
303    pub fn worker_capacity(&self) -> usize {
304        self.line_buffers.len()
305    }
306
307    /// Returns the reusable element count reserved by every active line buffer.
308    #[must_use]
309    pub fn line_capacity(&self) -> usize {
310        if self.line_buffers.is_empty() {
311            return self.padded.capacity().min(self.prefix.capacity()).min(self.suffix.capacity());
312        }
313        self.line_buffers
314            .iter()
315            .map(|buffers| {
316                buffers
317                    .padded
318                    .capacity()
319                    .min(buffers.prefix.capacity())
320                    .min(buffers.suffix.capacity())
321            })
322            .min()
323            .unwrap_or(0)
324    }
325}
326
327/// Erodes into caller-owned packed output using reusable scratch storage.
328pub fn erode_rect_u8_into(
329    input: ImageView<'_, u8, 1>,
330    element: &StructuringElement,
331    iterations: usize,
332    border: BorderMode<u8, 1>,
333    output: &mut [u8],
334    workspace: &mut RectMorphologyWorkspace,
335) -> VisionResult<()> {
336    validate_output_len(output, input.width(), input.height())?;
337    run_rect_sequence(input, element, border, &[(Extreme::Minimum, iterations)], workspace)?;
338    output.copy_from_slice(&workspace.current);
339    Ok(())
340}
341
342/// Dilates into caller-owned packed output using reusable scratch storage.
343pub fn dilate_rect_u8_into(
344    input: ImageView<'_, u8, 1>,
345    element: &StructuringElement,
346    iterations: usize,
347    border: BorderMode<u8, 1>,
348    output: &mut [u8],
349    workspace: &mut RectMorphologyWorkspace,
350) -> VisionResult<()> {
351    validate_output_len(output, input.width(), input.height())?;
352    run_rect_sequence(input, element, border, &[(Extreme::Maximum, iterations)], workspace)?;
353    output.copy_from_slice(&workspace.current);
354    Ok(())
355}
356
357/// Applies composite rectangular morphology into caller-owned packed output.
358///
359/// `output` must contain exactly `input.width() * input.height()` elements.
360/// Safe Rust borrowing requires input and output storage not to overlap.
361#[allow(clippy::too_many_arguments)]
362pub fn morphology_rect_u8_into(
363    input: ImageView<'_, u8, 1>,
364    operation: MorphologyOperation,
365    element: &StructuringElement,
366    iterations: usize,
367    border: BorderMode<u8, 1>,
368    output: &mut [u8],
369    workspace: &mut RectMorphologyWorkspace,
370) -> VisionResult<()> {
371    validate_output_len(output, input.width(), input.height())?;
372    match operation {
373        MorphologyOperation::Open => run_rect_sequence(
374            input,
375            element,
376            border,
377            &[(Extreme::Minimum, iterations), (Extreme::Maximum, iterations)],
378            workspace,
379        )?,
380        MorphologyOperation::Close => run_rect_sequence(
381            input,
382            element,
383            border,
384            &[(Extreme::Maximum, iterations), (Extreme::Minimum, iterations)],
385            workspace,
386        )?,
387        MorphologyOperation::Gradient => {
388            run_rect_sequence(
389                input,
390                element,
391                border,
392                &[(Extreme::Maximum, iterations)],
393                workspace,
394            )?;
395            output.copy_from_slice(&workspace.current);
396            run_rect_sequence(
397                input,
398                element,
399                border,
400                &[(Extreme::Minimum, iterations)],
401                workspace,
402            )?;
403            for (high, &low) in output.iter_mut().zip(&workspace.current) {
404                *high = high.saturating_sub(low);
405            }
406            return Ok(());
407        }
408        MorphologyOperation::TopHat => {
409            pack_u8_into(input, output);
410            run_rect_sequence(
411                input,
412                element,
413                border,
414                &[(Extreme::Minimum, iterations), (Extreme::Maximum, iterations)],
415                workspace,
416            )?;
417            for (original, &opened) in output.iter_mut().zip(&workspace.current) {
418                *original = original.saturating_sub(opened);
419            }
420            return Ok(());
421        }
422        MorphologyOperation::BlackHat => {
423            run_rect_sequence(
424                input,
425                element,
426                border,
427                &[(Extreme::Maximum, iterations), (Extreme::Minimum, iterations)],
428                workspace,
429            )?;
430            copy_subtract_input(input, &workspace.current, output);
431            return Ok(());
432        }
433    }
434    output.copy_from_slice(&workspace.current);
435    Ok(())
436}
437
438#[derive(Clone, Copy)]
439enum Extreme {
440    Minimum,
441    Maximum,
442}
443
444fn validate_rect(element: &StructuringElement) -> VisionResult<()> {
445    if element.mask.iter().all(|&active| active) {
446        Ok(())
447    } else {
448        Err(VisionError::InvalidParameter(
449            "rectangular morphology fast path requires a full rectangular mask".into(),
450        ))
451    }
452}
453
454fn run_rect_sequence(
455    input: ImageView<'_, u8, 1>,
456    element: &StructuringElement,
457    border: BorderMode<u8, 1>,
458    stages: &[(Extreme, usize)],
459    workspace: &mut RectMorphologyWorkspace,
460) -> VisionResult<()> {
461    validate_rect(element)?;
462    let (width, height) = (input.width(), input.height());
463    workspace.current.resize(width * height, 0);
464    pack_u8_into(input, &mut workspace.current);
465    for &(extreme, iterations) in stages {
466        for _ in 0..iterations {
467            let current = std::mem::take(&mut workspace.current);
468            workspace.apply(
469                &current,
470                width,
471                height,
472                element.width,
473                element.height,
474                element.anchor_x,
475                element.anchor_y,
476                border,
477                extreme,
478            );
479            workspace.current = current;
480            std::mem::swap(&mut workspace.current, &mut workspace.output);
481        }
482    }
483    Ok(())
484}
485
486fn validate_output_len(output: &[u8], width: usize, height: usize) -> VisionResult<()> {
487    let len = width
488        .checked_mul(height)
489        .ok_or_else(|| VisionError::InvalidDimensions("morphology output size overflows".into()))?;
490    if output.len() != len {
491        return Err(VisionError::ShapeMismatch(format!(
492            "morphology output needs {len} elements, found {}",
493            output.len()
494        )));
495    }
496    Ok(())
497}
498
499fn pack_u8_into(input: ImageView<'_, u8, 1>, output: &mut [u8]) {
500    for y in 0..input.height() {
501        let start = y * input.width();
502        output[start..start + input.width()]
503            .copy_from_slice(input.row(y).expect("input row in bounds"));
504    }
505}
506
507fn copy_subtract_input(input: ImageView<'_, u8, 1>, high: &[u8], output: &mut [u8]) {
508    for y in 0..input.height() {
509        let start = y * input.width();
510        for ((value, &closed), &original) in output[start..start + input.width()]
511            .iter_mut()
512            .zip(&high[start..start + input.width()])
513            .zip(input.row(y).expect("input row in bounds"))
514        {
515            *value = closed.saturating_sub(original);
516        }
517    }
518}
519
520impl RectMorphologyWorkspace {
521    #[allow(clippy::too_many_arguments)]
522    fn apply(
523        &mut self,
524        input: &[u8],
525        width: usize,
526        height: usize,
527        kernel_width: usize,
528        kernel_height: usize,
529        anchor_x: usize,
530        anchor_y: usize,
531        border: BorderMode<u8, 1>,
532        extreme: Extreme,
533    ) {
534        let len = width * height;
535        self.centered_5x5_active = false;
536        if kernel_width == 5
537            && kernel_height == 5
538            && anchor_x == 2
539            && anchor_y == 2
540            && matches!(border, BorderMode::Replicate)
541        {
542            self.apply_centered_5x5(input, width, height, extreme);
543            return;
544        }
545        if should_parallelize(len, width.max(height), LARGE_PARALLEL_COMPONENTS) {
546            self.apply_parallel(
547                input,
548                width,
549                height,
550                kernel_width,
551                kernel_height,
552                anchor_x,
553                anchor_y,
554                border,
555                extreme,
556            );
557            return;
558        }
559        self.horizontal.resize(len, 0);
560        for y in 0..height {
561            let start = y * width;
562            filter_line(
563                &input[start..start + width],
564                &mut self.horizontal[start..start + width],
565                kernel_width,
566                anchor_x,
567                border,
568                extreme,
569                &mut self.padded,
570                &mut self.prefix,
571                &mut self.suffix,
572            );
573        }
574
575        self.transposed.resize(len, 0);
576        transpose_blocked(&self.horizontal, &mut self.transposed, width, height);
577        self.filtered.resize(len, 0);
578        for x in 0..width {
579            let start = x * height;
580            filter_line(
581                &self.transposed[start..start + height],
582                &mut self.filtered[start..start + height],
583                kernel_height,
584                anchor_y,
585                border,
586                extreme,
587                &mut self.padded,
588                &mut self.prefix,
589                &mut self.suffix,
590            );
591        }
592        self.output.resize(len, 0);
593        transpose_blocked(&self.filtered, &mut self.output, height, width);
594    }
595
596    fn apply_centered_5x5(&mut self, input: &[u8], width: usize, height: usize, extreme: Extreme) {
597        let len = width * height;
598        self.centered_5x5_active = true;
599        self.horizontal.resize(len, 0);
600        if should_parallelize(len, width.max(height), LARGE_PARALLEL_COMPONENTS) {
601            let workers = bounded_workers(
602                len,
603                width.max(height),
604                LARGE_PARALLEL_COMPONENTS,
605                rayon::current_num_threads(),
606            );
607            self.line_buffers.resize_with(workers, LineBuffers::default);
608            let line_len = width.max(height) + 4;
609            for buffers in &mut self.line_buffers {
610                buffers.padded.reserve(line_len.saturating_sub(buffers.padded.len()));
611                buffers.prefix.reserve(line_len.saturating_sub(buffers.prefix.len()));
612                buffers.suffix.reserve(line_len.saturating_sub(buffers.suffix.len()));
613            }
614        }
615        let arch = Arch::new();
616        if should_parallelize(len, height, LARGE_PARALLEL_COMPONENTS) {
617            self.horizontal.par_chunks_mut(width * ROWS_PER_TILE).enumerate().for_each(
618                |(block, outputs)| {
619                    arch.dispatch(|| {
620                        for (row, output) in outputs.chunks_mut(width).enumerate() {
621                            let y = block * ROWS_PER_TILE + row;
622                            filter_centered_5_replicate(
623                                &input[y * width..(y + 1) * width],
624                                output,
625                                extreme,
626                            );
627                        }
628                    });
629                },
630            );
631        } else {
632            arch.dispatch(|| {
633                for (source, output) in input.chunks(width).zip(self.horizontal.chunks_mut(width)) {
634                    filter_centered_5_replicate(source, output, extreme);
635                }
636            });
637        }
638
639        self.output.resize(len, 0);
640        if should_parallelize(len, height, LARGE_PARALLEL_COMPONENTS) {
641            self.output.par_chunks_mut(width * ROWS_PER_TILE).enumerate().for_each(
642                |(block, outputs)| {
643                    arch.dispatch(|| {
644                        for (row, output) in outputs.chunks_mut(width).enumerate() {
645                            filter_vertical_centered_5_replicate(
646                                &self.horizontal,
647                                width,
648                                height,
649                                block * ROWS_PER_TILE + row,
650                                output,
651                                extreme,
652                            );
653                        }
654                    });
655                },
656            );
657        } else {
658            arch.dispatch(|| {
659                for (y, output) in self.output.chunks_mut(width).enumerate() {
660                    filter_vertical_centered_5_replicate(
661                        &self.horizontal,
662                        width,
663                        height,
664                        y,
665                        output,
666                        extreme,
667                    );
668                }
669            });
670        }
671    }
672
673    #[allow(clippy::too_many_arguments)]
674    fn apply_parallel(
675        &mut self,
676        input: &[u8],
677        width: usize,
678        height: usize,
679        kernel_width: usize,
680        kernel_height: usize,
681        anchor_x: usize,
682        anchor_y: usize,
683        border: BorderMode<u8, 1>,
684        extreme: Extreme,
685    ) {
686        let len = width * height;
687        let workers = bounded_workers(
688            len,
689            width.max(height),
690            LARGE_PARALLEL_COMPONENTS,
691            rayon::current_num_threads(),
692        );
693        self.line_buffers.resize_with(workers, LineBuffers::default);
694        self.horizontal.resize(len, 0);
695        let horizontal_rows = items_per_worker(height, workers);
696        self.horizontal
697            .par_chunks_mut(horizontal_rows * width)
698            .zip(self.line_buffers.par_iter_mut())
699            .enumerate()
700            .for_each(|(chunk, (outputs, buffers))| {
701                let first_y = chunk * horizontal_rows;
702                for (local_y, output) in outputs.chunks_mut(width).enumerate() {
703                    let start = (first_y + local_y) * width;
704                    filter_line(
705                        &input[start..start + width],
706                        output,
707                        kernel_width,
708                        anchor_x,
709                        border,
710                        extreme,
711                        &mut buffers.padded,
712                        &mut buffers.prefix,
713                        &mut buffers.suffix,
714                    );
715                }
716            });
717
718        self.transposed.resize(len, 0);
719        self.transposed.par_chunks_mut(height).enumerate().for_each(|(x, output)| {
720            for (y, value) in output.iter_mut().enumerate() {
721                *value = self.horizontal[y * width + x];
722            }
723        });
724        self.filtered.resize(len, 0);
725        let vertical_rows = items_per_worker(width, workers);
726        self.filtered
727            .par_chunks_mut(vertical_rows * height)
728            .zip(self.line_buffers.par_iter_mut())
729            .enumerate()
730            .for_each(|(chunk, (outputs, buffers))| {
731                let first_x = chunk * vertical_rows;
732                for (local_x, output) in outputs.chunks_mut(height).enumerate() {
733                    let start = (first_x + local_x) * height;
734                    filter_line(
735                        &self.transposed[start..start + height],
736                        output,
737                        kernel_height,
738                        anchor_y,
739                        border,
740                        extreme,
741                        &mut buffers.padded,
742                        &mut buffers.prefix,
743                        &mut buffers.suffix,
744                    );
745                }
746            });
747        self.output.resize(len, 0);
748        self.output.par_chunks_mut(width).enumerate().for_each(|(y, output)| {
749            for (x, value) in output.iter_mut().enumerate() {
750                *value = self.filtered[x * height + y];
751            }
752        });
753    }
754}
755
756fn filter_centered_5_replicate(input: &[u8], output: &mut [u8], extreme: Extreme) {
757    if input.len() < 5 {
758        for (x, value) in output.iter_mut().enumerate() {
759            let mut result = input[x.saturating_sub(2)];
760            for offset in 1..5 {
761                let source = (x + offset).saturating_sub(2).min(input.len() - 1);
762                result = extreme_u8(result, input[source], extreme);
763            }
764            *value = result;
765        }
766        return;
767    }
768    output[0] = extreme_u8(extreme_u8(input[0], input[1], extreme), input[2], extreme);
769    output[1] = extreme_u8(
770        extreme_u8(extreme_u8(input[0], input[1], extreme), input[2], extreme),
771        input[3],
772        extreme,
773    );
774    for x in 2..input.len() - 2 {
775        let left = extreme_u8(input[x - 2], input[x - 1], extreme);
776        let middle = extreme_u8(input[x], input[x + 1], extreme);
777        output[x] = extreme_u8(extreme_u8(left, middle, extreme), input[x + 2], extreme);
778    }
779    let last = input.len() - 1;
780    output[last - 1] = extreme_u8(
781        extreme_u8(extreme_u8(input[last - 3], input[last - 2], extreme), input[last - 1], extreme),
782        input[last],
783        extreme,
784    );
785    output[last] =
786        extreme_u8(extreme_u8(input[last - 2], input[last - 1], extreme), input[last], extreme);
787}
788
789fn filter_vertical_centered_5_replicate(
790    input: &[u8],
791    width: usize,
792    height: usize,
793    y: usize,
794    output: &mut [u8],
795    extreme: Extreme,
796) {
797    let y0 = y.saturating_sub(2);
798    let y1 = y.saturating_sub(1);
799    let y3 = (y + 1).min(height - 1);
800    let y4 = (y + 2).min(height - 1);
801    let row0 = &input[y0 * width..(y0 + 1) * width];
802    let row1 = &input[y1 * width..(y1 + 1) * width];
803    let row2 = &input[y * width..(y + 1) * width];
804    let row3 = &input[y3 * width..(y3 + 1) * width];
805    let row4 = &input[y4 * width..(y4 + 1) * width];
806    for x in 0..width {
807        let upper = extreme_u8(row0[x], row1[x], extreme);
808        let lower = extreme_u8(row3[x], row4[x], extreme);
809        output[x] = extreme_u8(extreme_u8(upper, row2[x], extreme), lower, extreme);
810    }
811}
812
813#[derive(Debug, Default)]
814struct LineBuffers {
815    padded: Vec<u8>,
816    prefix: Vec<u8>,
817    suffix: Vec<u8>,
818}
819
820#[allow(clippy::too_many_arguments)]
821fn filter_line(
822    input: &[u8],
823    output: &mut [u8],
824    kernel: usize,
825    anchor: usize,
826    border: BorderMode<u8, 1>,
827    extreme: Extreme,
828    padded: &mut Vec<u8>,
829    prefix: &mut Vec<u8>,
830    suffix: &mut Vec<u8>,
831) {
832    if input.is_empty() {
833        return;
834    }
835    let padded_len = input.len() + kernel - 1;
836    padded.resize(padded_len, 0);
837    prefix.resize(padded_len, 0);
838    suffix.resize(padded_len, 0);
839    let constant = match border {
840        BorderMode::Constant([value]) => value,
841        _ => 0,
842    };
843    for (index, value) in padded.iter_mut().enumerate() {
844        let source = index as isize - anchor as isize;
845        *value = map_index(source, input.len(), border).map_or(constant, |mapped| input[mapped]);
846    }
847
848    for block_start in (0..padded_len).step_by(kernel) {
849        let block_end = (block_start + kernel).min(padded_len);
850        prefix[block_start] = padded[block_start];
851        for index in block_start + 1..block_end {
852            prefix[index] = extreme_u8(prefix[index - 1], padded[index], extreme);
853        }
854        suffix[block_end - 1] = padded[block_end - 1];
855        for index in (block_start..block_end - 1).rev() {
856            suffix[index] = extreme_u8(suffix[index + 1], padded[index], extreme);
857        }
858    }
859    for (index, value) in output.iter_mut().enumerate() {
860        *value = extreme_u8(suffix[index], prefix[index + kernel - 1], extreme);
861    }
862}
863
864#[inline(always)]
865fn extreme_u8(left: u8, right: u8, extreme: Extreme) -> u8 {
866    match extreme {
867        Extreme::Minimum => left.min(right),
868        Extreme::Maximum => left.max(right),
869    }
870}
871
872fn transpose_blocked(input: &[u8], output: &mut [u8], width: usize, height: usize) {
873    const BLOCK: usize = 32;
874    for y0 in (0..height).step_by(BLOCK) {
875        for x0 in (0..width).step_by(BLOCK) {
876            let y1 = (y0 + BLOCK).min(height);
877            let x1 = (x0 + BLOCK).min(width);
878            for y in y0..y1 {
879                for x in x0..x1 {
880                    output[x * height + y] = input[y * width + x];
881                }
882            }
883        }
884    }
885}
886
887fn repeat_extreme<T: PixelComponent, const CHANNELS: usize>(
888    input: ImageView<'_, T, CHANNELS>,
889    element: &StructuringElement,
890    iterations: usize,
891    border: BorderMode<T, CHANNELS>,
892    extreme: Extreme,
893) -> VisionResult<Image<T, CHANNELS>> {
894    let mut output = pack(input)?;
895    for _ in 0..iterations {
896        output = extreme_once(output.view(), element, border, extreme)?;
897    }
898    Ok(output)
899}
900
901fn extreme_once<T: PixelComponent, const CHANNELS: usize>(
902    input: ImageView<'_, T, CHANNELS>,
903    element: &StructuringElement,
904    border: BorderMode<T, CHANNELS>,
905    extreme: Extreme,
906) -> VisionResult<Image<T, CHANNELS>> {
907    let mut output = Vec::with_capacity(input.width() * input.height() * CHANNELS);
908    for y in 0..input.height() {
909        for x in 0..input.width() {
910            let mut values = [0.0; CHANNELS];
911            let mut initialized = false;
912            for ey in 0..element.height {
913                for ex in 0..element.width {
914                    if !element.mask[ey * element.width + ex] {
915                        continue;
916                    }
917                    let pixel = fetch(
918                        input,
919                        x as isize + ex as isize - element.anchor_x as isize,
920                        y as isize + ey as isize - element.anchor_y as isize,
921                        border,
922                    );
923                    if !initialized {
924                        values = pixel.map(PixelComponent::to_f64);
925                        initialized = true;
926                    } else {
927                        for channel in 0..CHANNELS {
928                            let candidate = pixel[channel].to_f64();
929                            let ordering = candidate.total_cmp(&values[channel]);
930                            if matches!(extreme, Extreme::Minimum) && ordering.is_lt()
931                                || matches!(extreme, Extreme::Maximum) && ordering.is_gt()
932                            {
933                                values[channel] = candidate;
934                            }
935                        }
936                    }
937                }
938            }
939            output.extend(values.map(T::from_f64));
940        }
941    }
942    Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
943}
944
945fn subtract<T: PixelComponent, const CHANNELS: usize>(
946    left: ImageView<'_, T, CHANNELS>,
947    right: ImageView<'_, T, CHANNELS>,
948) -> VisionResult<Image<T, CHANNELS>> {
949    if left.width() != right.width() || left.height() != right.height() {
950        return Err(VisionError::ShapeMismatch(
951            "morphology subtraction dimensions must match".into(),
952        ));
953    }
954    let mut output = Vec::with_capacity(left.width() * left.height() * CHANNELS);
955    for y in 0..left.height() {
956        for x in 0..left.width() {
957            let a = left.get(x, y).expect("left coordinate in bounds");
958            let b = right.get(x, y).expect("right coordinate in bounds");
959            output.extend(std::array::from_fn::<_, CHANNELS, _>(|channel| {
960                T::from_f64(a[channel].to_f64() - b[channel].to_f64())
961            }));
962        }
963    }
964    Ok(Image::try_new_with_metadata(left.width(), left.height(), output, left.metadata())?)
965}
966
967fn pack<T: PixelComponent, const CHANNELS: usize>(
968    input: ImageView<'_, T, CHANNELS>,
969) -> VisionResult<Image<T, CHANNELS>> {
970    let mut output = Vec::with_capacity(input.width() * input.height() * CHANNELS);
971    for y in 0..input.height() {
972        for x in 0..input.width() {
973            output.extend_from_slice(input.get(x, y).expect("input coordinate in bounds"));
974        }
975    }
976    Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
977}
978
979fn validate_dimensions(
980    width: usize,
981    height: usize,
982    anchor_x: usize,
983    anchor_y: usize,
984) -> VisionResult<()> {
985    if width == 0 || height == 0 {
986        return Err(VisionError::InvalidParameter(
987            "structuring element dimensions must be non-zero".into(),
988        ));
989    }
990    if anchor_x >= width || anchor_y >= height {
991        return Err(VisionError::InvalidParameter(format!(
992            "structuring element anchor ({anchor_x}, {anchor_y}) is outside {width}x{height}"
993        )));
994    }
995    width
996        .checked_mul(height)
997        .ok_or_else(|| VisionError::InvalidParameter("structuring element overflows".into()))?;
998    Ok(())
999}
1000
1001fn fill_ellipse(mask: &mut [bool], width: usize, height: usize) {
1002    let center_x = width / 2;
1003    let center_y = height / 2;
1004    let radius_y = center_y.max(1) as f64;
1005    for y in 0..height {
1006        let dy = y.abs_diff(center_y) as f64;
1007        if dy > radius_y {
1008            continue;
1009        }
1010        let extent =
1011            (center_x as f64 * (1.0 - dy * dy / (radius_y * radius_y)).sqrt()).round() as usize;
1012        let start = center_x.saturating_sub(extent);
1013        let end = (center_x + extent).min(width - 1);
1014        for x in start..=end {
1015            mask[y * width + x] = true;
1016        }
1017    }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::{
1023        dilate, dilate_rect_u8, erode, erode_rect_u8, morphology_ex, morphology_rect_u8,
1024        morphology_rect_u8_into, MorphologyOperation, MorphologyShape, RectMorphologyWorkspace,
1025        StructuringElement,
1026    };
1027    use crate::BorderMode;
1028    use spatialrust_image::{Image, ImageRegion};
1029
1030    #[test]
1031    fn built_in_masks_have_expected_3x3_layouts() {
1032        let rect = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1033        let cross = StructuringElement::try_new(MorphologyShape::Cross, 3, 3).unwrap();
1034        let ellipse = StructuringElement::try_new(MorphologyShape::Ellipse, 3, 3).unwrap();
1035        let diamond = StructuringElement::try_new(MorphologyShape::Diamond, 3, 3).unwrap();
1036        assert_eq!(rect.mask().iter().filter(|&&v| v).count(), 9);
1037        assert_eq!(cross.mask().iter().filter(|&&v| v).count(), 5);
1038        assert_eq!(ellipse.mask().iter().filter(|&&v| v).count(), 5);
1039        assert_eq!(diamond.mask().iter().filter(|&&v| v).count(), 5);
1040    }
1041
1042    #[test]
1043    fn erode_and_dilate_match_known_extrema_on_roi() {
1044        let parent = Image::<u8, 1>::try_new(5, 3, (0..15).collect()).unwrap();
1045        let roi = parent.view().subview(ImageRegion::new(1, 0, 3, 3)).unwrap();
1046        let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1047        let low = erode(roi, &element, 1, BorderMode::Replicate).unwrap();
1048        let high = dilate(roi, &element, 1, BorderMode::Replicate).unwrap();
1049        assert_eq!(low[(1, 1)][0], 1);
1050        assert_eq!(high[(1, 1)][0], 13);
1051    }
1052
1053    #[test]
1054    fn opening_removes_isolated_impulse() {
1055        let image = Image::<u16, 1>::try_new(3, 3, vec![0, 0, 0, 0, 100, 0, 0, 0, 0]).unwrap();
1056        let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1057        let output = morphology_ex(
1058            image.view(),
1059            MorphologyOperation::Open,
1060            &element,
1061            1,
1062            BorderMode::Constant([0]),
1063        )
1064        .unwrap();
1065        assert!(output.as_slice().iter().all(|&value| value == 0));
1066    }
1067
1068    #[test]
1069    fn gradient_is_dilate_minus_erode_for_float() {
1070        let image = Image::<f32, 1>::try_new(3, 1, vec![1.0, 4.0, 9.0]).unwrap();
1071        let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 1).unwrap();
1072        let output = morphology_ex(
1073            image.view(),
1074            MorphologyOperation::Gradient,
1075            &element,
1076            1,
1077            BorderMode::Replicate,
1078        )
1079        .unwrap();
1080        assert_eq!(output.as_slice(), &[3.0, 8.0, 5.0]);
1081    }
1082
1083    #[test]
1084    fn zero_iterations_return_packed_copy() {
1085        let image = Image::<u8, 3>::from_pixel(2, 2, [1, 2, 3]).unwrap();
1086        let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1087        assert_eq!(erode(image.view(), &element, 0, BorderMode::Replicate).unwrap(), image);
1088    }
1089
1090    #[test]
1091    fn invalid_masks_are_rejected() {
1092        assert!(StructuringElement::try_from_mask(2, 2, 0, 0, vec![false; 4]).is_err());
1093        assert!(StructuringElement::try_from_mask(2, 2, 0, 0, vec![true; 3]).is_err());
1094    }
1095
1096    #[test]
1097    fn rectangular_u8_fast_path_matches_generic_for_anchors_borders_and_iterations() {
1098        let mut state = 0x9e37_79b9_u32;
1099        let pixels = (0..63)
1100            .map(|_| {
1101                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1102                (state >> 24) as u8
1103            })
1104            .collect();
1105        let image = Image::<u8, 1>::try_new(9, 7, pixels).unwrap();
1106        let elements = [
1107            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 1, 1, 0, 0).unwrap(),
1108            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 2, 0, 1).unwrap(),
1109            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 5, 3, 4, 0).unwrap(),
1110            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 11, 9, 5, 4).unwrap(),
1111        ];
1112        let borders = [
1113            BorderMode::Constant([37]),
1114            BorderMode::Replicate,
1115            BorderMode::Reflect,
1116            BorderMode::Reflect101,
1117            BorderMode::Wrap,
1118        ];
1119
1120        for element in &elements {
1121            for &border in &borders {
1122                for iterations in 0..=2 {
1123                    assert_eq!(
1124                        erode_rect_u8(image.view(), element, iterations, border).unwrap(),
1125                        erode(image.view(), element, iterations, border).unwrap()
1126                    );
1127                    assert_eq!(
1128                        dilate_rect_u8(image.view(), element, iterations, border).unwrap(),
1129                        dilate(image.view(), element, iterations, border).unwrap()
1130                    );
1131                }
1132            }
1133        }
1134    }
1135
1136    #[test]
1137    fn rectangular_u8_composites_match_generic() {
1138        let image = Image::<u8, 1>::try_new(
1139            7,
1140            5,
1141            (0..35).map(|index| ((index * 47 + index * index * 3) % 256) as u8).collect(),
1142        )
1143        .unwrap();
1144        let element =
1145            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 3, 1, 2).unwrap();
1146        for operation in [
1147            MorphologyOperation::Open,
1148            MorphologyOperation::Close,
1149            MorphologyOperation::Gradient,
1150            MorphologyOperation::TopHat,
1151            MorphologyOperation::BlackHat,
1152        ] {
1153            for iterations in 0..=2 {
1154                assert_eq!(
1155                    morphology_rect_u8(
1156                        image.view(),
1157                        operation,
1158                        &element,
1159                        iterations,
1160                        BorderMode::Replicate,
1161                    )
1162                    .unwrap(),
1163                    morphology_ex(
1164                        image.view(),
1165                        operation,
1166                        &element,
1167                        iterations,
1168                        BorderMode::Replicate,
1169                    )
1170                    .unwrap()
1171                );
1172            }
1173        }
1174    }
1175
1176    #[test]
1177    fn rectangular_fast_path_rejects_sparse_masks() {
1178        let image = Image::<u8, 1>::from_pixel(3, 3, [1]).unwrap();
1179        let cross = StructuringElement::try_new(MorphologyShape::Cross, 3, 3).unwrap();
1180        assert!(erode_rect_u8(image.view(), &cross, 1, BorderMode::Replicate).is_err());
1181    }
1182
1183    #[test]
1184    fn rectangular_into_matches_allocating_path_for_strided_input() {
1185        let parent = Image::<u8, 1>::try_new(
1186            13,
1187            9,
1188            (0..117).map(|index| ((index * 61 + 17) & 255) as u8).collect(),
1189        )
1190        .unwrap();
1191        let input = parent.view().subview(ImageRegion::new(2, 1, 9, 7)).unwrap();
1192        let element =
1193            StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 6, 1, 4).unwrap();
1194        let mut workspace = RectMorphologyWorkspace::new();
1195        let mut output = vec![0; input.width() * input.height()];
1196
1197        for operation in [
1198            MorphologyOperation::Open,
1199            MorphologyOperation::Close,
1200            MorphologyOperation::Gradient,
1201            MorphologyOperation::TopHat,
1202            MorphologyOperation::BlackHat,
1203        ] {
1204            morphology_rect_u8_into(
1205                input,
1206                operation,
1207                &element,
1208                2,
1209                BorderMode::Reflect101,
1210                &mut output,
1211                &mut workspace,
1212            )
1213            .unwrap();
1214            assert_eq!(
1215                output,
1216                morphology_rect_u8(input, operation, &element, 2, BorderMode::Reflect101,)
1217                    .unwrap()
1218                    .into_vec()
1219            );
1220        }
1221    }
1222
1223    #[test]
1224    fn rectangular_workspace_reuses_full_image_and_worker_capacity() {
1225        let image = Image::<u8, 1>::try_new(
1226            1000,
1227            1000,
1228            (0..1_000_000).map(|index| ((index * 29 + index / 1000) & 255) as u8).collect(),
1229        )
1230        .unwrap();
1231        let element = StructuringElement::try_new(MorphologyShape::Rect, 5, 5).unwrap();
1232        let mut workspace = RectMorphologyWorkspace::new();
1233        let mut output = vec![0; 1_000_000];
1234        morphology_rect_u8_into(
1235            image.view(),
1236            MorphologyOperation::Open,
1237            &element,
1238            1,
1239            BorderMode::Replicate,
1240            &mut output,
1241            &mut workspace,
1242        )
1243        .unwrap();
1244        let capacity = workspace.capacity();
1245        let workers = workspace.worker_capacity();
1246        let line_capacity = workspace.line_capacity();
1247        assert!(capacity >= output.len());
1248        assert!(workers >= 1);
1249        assert!(line_capacity >= 1004);
1250
1251        morphology_rect_u8_into(
1252            image.view(),
1253            MorphologyOperation::Close,
1254            &element,
1255            1,
1256            BorderMode::Replicate,
1257            &mut output,
1258            &mut workspace,
1259        )
1260        .unwrap();
1261        assert_eq!(workspace.capacity(), capacity);
1262        assert_eq!(workspace.worker_capacity(), workers);
1263        assert_eq!(workspace.line_capacity(), line_capacity);
1264    }
1265
1266    #[test]
1267    fn rectangular_into_validates_output_length() {
1268        let image = Image::<u8, 1>::from_pixel(4, 3, [9]).unwrap();
1269        let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1270        let mut workspace = RectMorphologyWorkspace::new();
1271        assert!(morphology_rect_u8_into(
1272            image.view(),
1273            MorphologyOperation::Open,
1274            &element,
1275            1,
1276            BorderMode::Replicate,
1277            &mut [0; 11],
1278            &mut workspace,
1279        )
1280        .is_err());
1281    }
1282}