Skip to main content

spatialrust_vision/
border.rs

1//! Shared border extrapolation for CPU image kernels.
2
3#[cfg(any(
4    feature = "warp",
5    feature = "imgproc-filter",
6    feature = "imgproc-morphology",
7    feature = "imgproc-analysis"
8))]
9use spatialrust_image::ImageView;
10
11#[cfg(any(
12    feature = "warp",
13    feature = "imgproc-filter",
14    feature = "imgproc-morphology",
15    feature = "imgproc-analysis"
16))]
17use crate::PixelComponent;
18
19/// Out-of-bounds sampling behavior for CPU image operations.
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub enum BorderMode<T, const CHANNELS: usize> {
22    /// Returns a fixed pixel outside the source image.
23    Constant([T; CHANNELS]),
24    /// Repeats the closest edge pixel.
25    Replicate,
26    /// Reflects including the edge pixel (`fedcba|abcdefgh|hgfedc`).
27    Reflect,
28    /// Reflects without repeating the edge (`gfedcb|abcdefgh|gfedcb`).
29    Reflect101,
30    /// Periodically wraps source coordinates.
31    Wrap,
32}
33
34#[cfg(any(
35    feature = "warp",
36    feature = "imgproc-filter",
37    feature = "imgproc-morphology",
38    feature = "imgproc-analysis"
39))]
40pub(crate) fn constant_pixel<T: PixelComponent, const CHANNELS: usize>(
41    border: BorderMode<T, CHANNELS>,
42) -> [T; CHANNELS] {
43    match border {
44        BorderMode::Constant(pixel) => pixel,
45        BorderMode::Replicate | BorderMode::Reflect | BorderMode::Reflect101 | BorderMode::Wrap => {
46            std::array::from_fn(|_| T::from_f64(0.0))
47        }
48    }
49}
50
51#[cfg(any(
52    feature = "warp",
53    feature = "imgproc-filter",
54    feature = "imgproc-morphology",
55    feature = "imgproc-analysis"
56))]
57pub(crate) fn fetch<T: PixelComponent, const CHANNELS: usize>(
58    input: ImageView<'_, T, CHANNELS>,
59    x: isize,
60    y: isize,
61    border: BorderMode<T, CHANNELS>,
62) -> [T; CHANNELS] {
63    if let (Some(ix), Some(iy)) =
64        (map_index(x, input.width(), border), map_index(y, input.height(), border))
65    {
66        return *input.get(ix, iy).expect("mapped coordinate is in bounds");
67    }
68    constant_pixel(border)
69}
70
71#[cfg(any(
72    feature = "warp",
73    feature = "imgproc-filter",
74    feature = "imgproc-morphology",
75    feature = "imgproc-analysis"
76))]
77pub(crate) fn map_index<T, const CHANNELS: usize>(
78    index: isize,
79    length: usize,
80    border: BorderMode<T, CHANNELS>,
81) -> Option<usize> {
82    if length == 0 {
83        return None;
84    }
85    if index >= 0 && index < length as isize {
86        return Some(index as usize);
87    }
88    match border {
89        BorderMode::Constant(_) => None,
90        BorderMode::Replicate => Some(index.clamp(0, length as isize - 1) as usize),
91        BorderMode::Reflect => Some(reflect_index(index, length, false)),
92        BorderMode::Reflect101 => Some(reflect_index(index, length, true)),
93        BorderMode::Wrap => Some(index.rem_euclid(length as isize) as usize),
94    }
95}
96
97#[cfg(any(
98    feature = "warp",
99    feature = "imgproc-filter",
100    feature = "imgproc-morphology",
101    feature = "imgproc-analysis"
102))]
103fn reflect_index(mut index: isize, length: usize, reflect101: bool) -> usize {
104    if length <= 1 {
105        return 0;
106    }
107    let length = length as isize;
108    while index < 0 || index >= length {
109        index = if index < 0 {
110            if reflect101 {
111                -index
112            } else {
113                -index - 1
114            }
115        } else if reflect101 {
116            2 * length - index - 2
117        } else {
118            2 * length - index - 1
119        };
120    }
121    index as usize
122}
123
124#[cfg(all(
125    test,
126    any(
127        feature = "warp",
128        feature = "imgproc-filter",
129        feature = "imgproc-morphology",
130        feature = "imgproc-analysis"
131    )
132))]
133mod tests {
134    use super::{map_index, BorderMode};
135
136    #[test]
137    fn maps_single_pixel_without_looping() {
138        for border in [
139            BorderMode::<u8, 1>::Replicate,
140            BorderMode::Reflect,
141            BorderMode::Reflect101,
142            BorderMode::Wrap,
143        ] {
144            assert_eq!(map_index(-100, 1, border), Some(0));
145            assert_eq!(map_index(100, 1, border), Some(0));
146        }
147    }
148
149    #[test]
150    fn empty_images_always_map_to_constant_space() {
151        assert_eq!(map_index(0, 0, BorderMode::<u8, 1>::Wrap), None);
152    }
153}