Skip to main content

ndarray_ndimage/
measurements.rs

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