ndarray_ndimage/
measurements.rs

1use ndarray::{s, Array3, ArrayBase, Axis, Data, Ix3, Zip};
2use num_traits::{Bounded, FromPrimitive, NumAssignOps, ToPrimitive, Unsigned};
3
4use crate::Mask;
5
6pub trait LabelType:
7    Copy + FromPrimitive + ToPrimitive + Ord + Unsigned + NumAssignOps + Bounded
8{
9    fn background() -> Self;
10    fn foreground() -> Self;
11}
12
13impl<T> LabelType for T
14where
15    T: Copy + FromPrimitive + ToPrimitive + Ord + Unsigned + NumAssignOps + Bounded,
16{
17    fn background() -> Self {
18        T::zero()
19    }
20    fn foreground() -> Self {
21        T::one()
22    }
23}
24
25/// Calculates the histogram of a label image.
26///
27/// * `labels` - 3D labels image, returned by the `label` function.
28/// * `nb_features` - Number of unique labels, returned by the `label` function.
29pub fn label_histogram<S>(labels: &ArrayBase<S, Ix3>, nb_features: usize) -> Vec<usize>
30where
31    S: Data,
32    S::Elem: LabelType,
33{
34    let mut count = vec![0; nb_features + 1];
35    Zip::from(labels).for_each(|&l| {
36        count[l.to_usize().unwrap()] += 1;
37    });
38    count
39}
40
41/// Returns the most frequent label and its index.
42///
43/// Ignores the background label. A blank label image will return None.
44///
45/// * `labels` - 3D labels image, returned by the `label` function.
46/// * `nb_features` - Number of unique labels, returned by the `label` function.
47pub fn most_frequent_label<S>(
48    labels: &ArrayBase<S, Ix3>,
49    nb_features: usize,
50) -> Option<(S::Elem, usize)>
51where
52    S: Data,
53    S::Elem: LabelType,
54{
55    let hist = label_histogram(labels, nb_features);
56    let (max, max_index) =
57        hist[1..].iter().enumerate().fold((0, 0), |acc, (i, &nb)| acc.max((nb, i)));
58    (max > 0).then(|| (S::Elem::from_usize(max_index + 1).unwrap(), max))
59}
60
61/// Returns a new mask, containing the biggest zone of `mask`.
62///
63/// * `mask` - Binary image to be labeled and studied.
64/// * `structure` - Structuring element used for the labeling. Must be 3x3x3 (e.g. the result
65///   of [`Kernel3d::generate`](crate::Kernel3d::generate)) and centrosymmetric. The center must be `true`.
66///
67/// The labeling is done using `u16`, this may be too small when `mask` has more than [`u16::MAX`] elements.
68pub fn largest_connected_components<S>(
69    mask: &ArrayBase<S, Ix3>,
70    structure: &ArrayBase<S, Ix3>,
71) -> Option<Mask>
72where
73    S: Data<Elem = bool>,
74{
75    let (labels, nb_features) = label::<_, u16>(mask, structure);
76    let (right_label, _) = most_frequent_label(&labels, nb_features)?;
77    Some(labels.mapv(|l| l == right_label))
78}
79
80/// Labels features of 3D binary images.
81///
82/// Returns the labels and the number of features.
83///
84/// * `mask` - Binary image to be labeled. `false` values are considered the background.
85/// * `structure` - Structuring element used for the labeling. Must be 3x3x3 (e.g. the result
86///   of [`Kernel3d::generate`](crate::Kernel3d::generate)) and centrosymmetric. The center must be `true`.
87///
88/// The return type of `label` can be specified using turbofish syntax:
89///
90/// ```
91/// // Will use `u16` as the label type
92/// ndarray_ndimage::label::<_, u16>(
93///     &ndarray::Array3::from_elem((100, 100, 100), true),
94///     &ndarray_ndimage::Kernel3d::Star.generate()
95/// );
96/// ```
97///
98/// As a rough rule of thumb, the maximum value of the label type should be larger than `data.len()`.
99/// This is the worst case, the exact bound will depend on the kernel used. If the label type overflows
100/// while assigning labels, a panic will occur.
101pub fn label<S, O>(data: &ArrayBase<S, Ix3>, structure: &ArrayBase<S, Ix3>) -> (Array3<O>, usize)
102where
103    S: Data<Elem = bool>,
104    O: LabelType,
105{
106    assert!(structure.shape() == &[3, 3, 3], "`structure` must be size 3 in all dimensions");
107    assert!(structure == structure.slice(s![..;-1, ..;-1, ..;-1]), "`structure is not symmetric");
108
109    let len = data.dim().2;
110    let mut line_buffer = vec![O::background(); len + 2];
111    let mut neighbors = vec![O::background(); len + 2];
112
113    let mut next_region = O::foreground() + O::one();
114    let mut equivalences: Vec<_> =
115        (0..next_region.to_usize().unwrap()).map(|x| O::from_usize(x).unwrap()).collect();
116
117    // We only handle 3D data for now, but this algo can handle N-dimensional data.
118    // https://github.com/scipy/scipy/blob/v0.16.1/scipy/ndimage/src/_ni_label.pyx
119    // N-D: Use a loop in `is_valid` and change the `labels` indexing (might be hard in Rust)
120
121    let nb_neighbors = structure.len() / (3 * 2);
122    let kernel_data: Vec<([bool; 3], [isize; 2])> = structure
123        .lanes(Axis(2))
124        .into_iter()
125        .zip(0isize..)
126        // Only consider lanes before the center
127        .take(nb_neighbors)
128        // Filter out kernel lanes with no `true` elements (since that are no-ops)
129        .filter(|(lane, _)| lane.iter().any(|x| *x))
130        .map(|(lane, i)| {
131            let kernel = [lane[0], lane[1], lane[2]];
132            // Convert i into coordinates
133            let y = i / 3;
134            let x = i - y * 3;
135            (kernel, [y, x])
136        })
137        .collect();
138
139    let use_previous = structure[(1, 1, 0)];
140    let width = data.dim().0 as isize;
141    let height = data.dim().1 as isize;
142    let mut labels = Array3::from_elem(data.dim(), O::background());
143    Zip::indexed(data.lanes(Axis(2))).for_each(|idx, data| {
144        for (&v, b) in data.iter().zip(&mut line_buffer[1..]) {
145            *b = if !v { O::background() } else { O::foreground() }
146        }
147
148        let mut needs_self_labeling = true;
149        for (i, (kernel, coordinates)) in kernel_data.iter().enumerate() {
150            // Check that the neighbor line is in bounds
151            if let Some((x, y)) = is_valid(&[idx.0, idx.1], coordinates, &[width, height]) {
152                // Copy the interesting neighbor labels to `neighbors`
153                for (&v, b) in labels.slice(s![x, y, ..]).iter().zip(&mut neighbors[1..]) {
154                    *b = v;
155                }
156
157                let label_unlabeled = i == kernel_data.len() - 1;
158                next_region = label_line_with_neighbor(
159                    &mut line_buffer,
160                    &neighbors,
161                    &mut equivalences,
162                    *kernel,
163                    use_previous,
164                    label_unlabeled,
165                    next_region,
166                );
167                if label_unlabeled {
168                    needs_self_labeling = false;
169                }
170            }
171        }
172
173        if needs_self_labeling {
174            // We didn't call label_line_with_neighbor above with label_unlabeled=True, so call it
175            // now in such a way as to cause unlabeled regions to get a label.
176            next_region = label_line_with_neighbor(
177                &mut line_buffer,
178                &neighbors,
179                &mut equivalences,
180                [false, false, false],
181                use_previous,
182                true,
183                next_region,
184            );
185        }
186
187        // Copy the results (`line_buffer`) to the output labels image
188        Zip::from(&line_buffer[1..=len])
189            .map_assign_into(labels.slice_mut(s![idx.0, idx.1, ..]), |&b| b);
190    });
191
192    // Compact and apply the equivalences
193    let nb_features = compact_equivalences(&mut equivalences, next_region);
194    labels.mapv_inplace(|l| equivalences[l.to_usize().unwrap()]);
195
196    (labels, nb_features)
197}
198
199fn is_valid(idx: &[usize; 2], coords: &[isize; 2], dims: &[isize; 2]) -> Option<(usize, usize)> {
200    let valid = |i, c, d| -> Option<usize> {
201        let a = i as isize + (c - 1);
202        if a >= 0 && a < d {
203            Some(a as usize)
204        } else {
205            None
206        }
207    };
208    // Returns `Some((x, y))` only if both calls succeeded
209    valid(idx[0], coords[0], dims[0])
210        .and_then(|x| valid(idx[1], coords[1], dims[1]).and_then(|y| Some((x, y))))
211}
212
213fn label_line_with_neighbor<O>(
214    line: &mut [O],
215    neighbors: &[O],
216    equivalences: &mut Vec<O>,
217    kernel: [bool; 3],
218    use_previous: bool,
219    label_unlabeled: bool,
220    mut next_region: O,
221) -> O
222where
223    O: LabelType,
224{
225    let mut previous = line[0];
226    for (n, l) in neighbors.windows(3).zip(&mut line[1..]) {
227        if *l != O::background() {
228            for (&n, &k) in n.iter().zip(&kernel) {
229                if k {
230                    *l = take_label_or_merge(*l, n, equivalences);
231                }
232            }
233            if label_unlabeled {
234                if use_previous {
235                    *l = take_label_or_merge(*l, previous, equivalences);
236                }
237                // Still needs a label?
238                if *l == O::foreground() {
239                    *l = next_region;
240                    equivalences.push(next_region);
241                    assert!(next_region < O::max_value(), "Overflow when assigning label");
242                    next_region += O::one();
243                }
244            }
245        }
246        previous = *l;
247    }
248    next_region
249}
250
251/// Take the label of a neighbor, or mark them for merging
252fn take_label_or_merge<O>(current: O, neighbor: O, equivalences: &mut [O]) -> O
253where
254    O: LabelType,
255{
256    if neighbor == O::background() {
257        current
258    } else if current == O::foreground() {
259        neighbor // neighbor is not background
260    } else if current != neighbor {
261        mark_for_merge(neighbor, current, equivalences)
262    } else {
263        current
264    }
265}
266
267/// Mark two labels to be merged
268fn mark_for_merge<O>(mut a: O, mut b: O, equivalences: &mut [O]) -> O
269where
270    O: LabelType,
271{
272    // Find smallest root for each of a and b
273    let original_a = a;
274    while a != equivalences[a.to_usize().unwrap()] {
275        a = equivalences[a.to_usize().unwrap()];
276    }
277    let original_b = b;
278    while b != equivalences[b.to_usize().unwrap()] {
279        b = equivalences[b.to_usize().unwrap()];
280    }
281    let lowest_label = a.min(b);
282
283    // Merge roots
284    equivalences[a.to_usize().unwrap()] = lowest_label;
285    equivalences[b.to_usize().unwrap()] = lowest_label;
286
287    // Merge every step to minlabel
288    a = original_a;
289    while a != lowest_label {
290        let a_copy = a;
291        a = equivalences[a.to_usize().unwrap()];
292        equivalences[a_copy.to_usize().unwrap()] = lowest_label;
293    }
294    b = original_b;
295    while b != lowest_label {
296        let b_copy = b;
297        b = equivalences[b.to_usize().unwrap()];
298        equivalences[b_copy.to_usize().unwrap()] = lowest_label;
299    }
300
301    lowest_label
302}
303
304/// Compact the equivalences vector
305fn compact_equivalences<O>(equivalences: &mut [O], next_region: O) -> usize
306where
307    O: LabelType,
308{
309    let no_labelling = next_region == O::from_usize(2).unwrap();
310    let mut dest_label = if no_labelling { 0 } else { 1 };
311    for i in 2..next_region.to_usize().unwrap() {
312        if equivalences[i] == O::from_usize(i).unwrap() {
313            equivalences[i] = O::from_usize(dest_label).unwrap();
314            dest_label = dest_label + 1;
315        } else {
316            // We've compacted every label below this, and equivalences has an invariant that it
317            // always points downward. Therefore, we can fetch the final label by two steps of
318            // indirection.
319            equivalences[i] = equivalences[equivalences[i].to_usize().unwrap()];
320        }
321    }
322    if no_labelling {
323        0
324    } else {
325        equivalences.iter().max().unwrap().to_usize().unwrap()
326    }
327}