ndarray_ndimage/morphology/
mod.rs

1mod offsets;
2
3use ndarray::{Array3, ArrayBase, ArrayView3, ArrayViewMut3, Data, Ix3};
4
5use crate::Mask;
6use offsets::Offsets;
7
8/// Binary erosion of a 3D binary image.
9///
10/// * `mask` - Binary image to be eroded.
11/// * `kernel` - Structuring element used for the erosion. Must be of odd length. The center must
12///   be `true`.
13/// * `iterations` - The erosion is repeated iterations times.
14pub fn binary_erosion<SM, SK>(
15    mask: &ArrayBase<SM, Ix3>,
16    kernel: &ArrayBase<SK, Ix3>,
17    iterations: usize,
18) -> Mask
19where
20    SM: Data<Elem = bool>,
21    SK: Data<Elem = bool>,
22{
23    mask.as_slice_memory_order()
24        .expect("Morphological operations can only be called on arrays with contiguous memory.");
25
26    // We can't really reserve a good number of elements here. It could be anything.
27    let mut last_indices = (iterations > 1).then_some(vec![]);
28    let mut eroded = mask.to_owned();
29    let mut offsets = Offsets::new(mask, kernel.view(), false);
30    erode(mask.view(), &mut eroded.view_mut(), &mut offsets, &mut last_indices);
31
32    if let Some(mut last_indices) = last_indices {
33        for it in 1..iterations {
34            if last_indices.is_empty() {
35                break;
36            }
37
38            let save_next_indices = it < iterations - 1;
39            next_it(&mut eroded, &mut offsets, &mut last_indices, save_next_indices, true, false);
40        }
41    }
42    eroded
43}
44
45/// Binary dilation of a 3D binary image.
46///
47/// * `mask` - Binary image to be dilated.
48/// * `kernel` - Structuring element used for the erosion. Must be of odd length. The center must
49///   be `true`.
50/// * `iterations` - The dilation is repeated iterations times.
51pub fn binary_dilation<SM, SK>(
52    mask: &ArrayBase<SM, Ix3>,
53    kernel: &ArrayBase<SK, Ix3>,
54    iterations: usize,
55) -> Mask
56where
57    SM: Data<Elem = bool>,
58    SK: Data<Elem = bool>,
59{
60    mask.as_slice_memory_order()
61        .expect("Morphological operations can only be called on arrays with contiguous memory.");
62
63    // We can't really reserve a good number of elements here. It could be anything.
64    let mut last_indices = (iterations > 1).then_some(vec![]);
65    let mut dilated = mask.to_owned();
66    let mut offsets = Offsets::new(mask, kernel.view(), true);
67    dilate(mask.view(), &mut dilated, &mut offsets, &mut last_indices);
68
69    if let Some(mut last_indices) = last_indices {
70        for it in 1..iterations {
71            if last_indices.is_empty() {
72                break;
73            }
74
75            let save_next_indices = it < iterations - 1;
76            next_it(&mut dilated, &mut offsets, &mut last_indices, save_next_indices, false, true);
77        }
78    }
79    dilated
80}
81
82/// Binary opening of a 3D binary image.
83///
84/// The opening of an input image by a structuring element is the dilation of the erosion of the
85/// image by the structuring element.
86///
87/// Unlike other libraries, the **border values** of the:
88/// - dilation is always `false`, to avoid dilating the borders
89/// - erosion is always `true`, to avoid *border effects*
90///
91/// * `mask` - Binary image to be opened.
92/// * `kernel` - Structuring element used for the opening.
93/// * `iterations` - The erosion step of the opening, then the dilation step are each repeated
94///   iterations times.
95pub fn binary_opening<SM, SK>(
96    mask: &ArrayBase<SM, Ix3>,
97    kernel: &ArrayBase<SK, Ix3>,
98    iterations: usize,
99) -> Mask
100where
101    SM: Data<Elem = bool>,
102    SK: Data<Elem = bool>,
103{
104    let eroded = binary_erosion(mask, kernel, iterations);
105    binary_dilation(&eroded, kernel, iterations)
106}
107
108/// Binary closing of a 3D binary image.
109///
110/// The closing of an input image by a structuring element is the erosion of the dilation of the
111/// image by the structuring element.
112///
113/// Unlike other libraries, the **border values** of the:
114/// - dilation is always `false`, to avoid dilating the borders
115/// - erosion is always `true`, to avoid *border effects*
116///
117/// * `mask` - Binary image to be closed.
118/// * `kernel` - Structuring element used for the closing.
119/// * `iterations` - The dilation step of the closing, then the erosion step are each repeated
120///   iterations times.
121pub fn binary_closing<SM, SK>(
122    mask: &ArrayBase<SM, Ix3>,
123    kernel: &ArrayBase<SK, Ix3>,
124    iterations: usize,
125) -> Mask
126where
127    SM: Data<Elem = bool>,
128    SK: Data<Elem = bool>,
129{
130    let dilated = binary_dilation(mask, kernel, iterations);
131    binary_erosion(&dilated, kernel, iterations)
132}
133
134/// Actual erosion work.
135///
136/// `out` MUST be a clone of `mask`, otherwise this won't work. We're not setting any values
137/// uselessly, to be as fast as possible.
138fn erode(
139    mask: ArrayView3<bool>,
140    out: &mut ArrayViewMut3<bool>,
141    offsets: &mut Offsets,
142    last_indices: &mut Option<Vec<isize>>,
143) {
144    let mask = mask.as_slice_memory_order().unwrap();
145    let out = out.as_slice_memory_order_mut().unwrap();
146    let ooi_offset = mask.len() as isize;
147
148    let mut i = 0;
149    for (&m, o) in mask.iter().zip(out) {
150        if m {
151            for &offset in offsets.range() {
152                // Is offset the special value "Out Of Image"?
153                if offset == ooi_offset {
154                    // The offsets are sorted so we can quit as soon as we see the `ooi_offset`
155                    break;
156                } else {
157                    // unsafe { !*mask.get_unchecked((i + offset) as usize) }
158                    if !mask[(i + offset) as usize] {
159                        *o = false;
160                        // If we have more than one iteration, note all modified indices
161                        if let Some(last_indices) = last_indices {
162                            // Remember that `i` IS the neighbor, not the "center", so we're adding
163                            // `i` here, not `i + offset`.
164                            last_indices.push(i);
165                        }
166                        break;
167                    }
168                }
169            }
170        }
171        offsets.next();
172        i += 1;
173    }
174}
175
176/// Actual dilation work.
177///
178/// `out` MUST be a clone of `mask`, otherwise this won't work. We're not setting any values
179/// uselessly, to be as fast as possible.
180fn dilate(
181    mask: ArrayView3<bool>,
182    out: &mut Array3<bool>,
183    offsets: &mut Offsets,
184    last_indices: &mut Option<Vec<isize>>,
185) {
186    // Even if `erode` and `dilate` could share the same code (as SciPy does), it produces much
187    // slower code in practice. See previous function for some documentation.
188    let mask = mask.as_slice_memory_order().unwrap();
189    let out = out.as_slice_memory_order_mut().unwrap();
190    let ooi_offset = mask.len() as isize;
191
192    let mut i = 0;
193    for (&m, o) in mask.iter().zip(out) {
194        if !m {
195            for &offset in offsets.range() {
196                if offset == ooi_offset {
197                    break;
198                } else {
199                    // unsafe { *mask.get_unchecked((i + offset) as usize) }
200                    if mask[(i + offset) as usize] {
201                        *o = true;
202                        if let Some(last_indices) = last_indices {
203                            last_indices.push(i);
204                        }
205                        break;
206                    }
207                }
208            }
209        }
210        offsets.next();
211        i += 1;
212    }
213}
214
215/// Common function do compute another iteration of dilate or erode.
216///
217/// Use only when `dilate` or `erode` have been called and the `last_indices` collected.
218///
219/// - Use `false` and `true` for dilate.
220/// - Use `true` and `false` for erode.
221fn next_it(
222    out: &mut Array3<bool>,
223    offsets: &mut Offsets,
224    last_indices: &mut Vec<isize>,
225    save_next_indices: bool,
226    b1: bool,
227    b2: bool,
228) {
229    let out = out.as_slice_memory_order_mut().unwrap();
230    let ooi_offset = out.len() as isize;
231
232    // Again, it's complex to guess the right number of elements. I think the same number as last
233    // time + ?% makes sense, but it could also be empty.
234    let mut new_indices = vec![];
235    for &i in &*last_indices {
236        offsets.move_to(i);
237        for &offset in offsets.range() {
238            if offset == ooi_offset {
239                break;
240            } else {
241                let out = &mut out[(i + offset) as usize];
242                if save_next_indices && *out == b1 {
243                    // This time, `i` is the center and `i + offset` is the neighbor
244                    new_indices.push(i + offset);
245                }
246                *out = b2;
247            }
248        }
249    }
250    *last_indices = new_indices;
251}