Skip to main content

ndarray_ndimage/morphology/
mod.rs

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