Skip to main content

eventcv_core/
cluster.rs

1//! Connected-component labelling on event frames — a simple clustering / segmentation step over a
2//! dense [`EventFrame`] (e.g. a `count` or `binary` image). A pixel is **foreground** when any of
3//! its channels is non-zero; a flood fill assigns each maximal 4- or 8-connected foreground blob a
4//! distinct label `1..=k`, with background `0`. Returns a single-channel `u64` "labels" frame.
5
6use std::{error::Error, fmt};
7
8use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
9
10impl EventFrame {
11    /// Labels 4- or 8-connected foreground components. `connectivity` must be `4` or `8`. The
12    /// result is a single-channel `u64` frame the same size as `self`: `0` is background, and each
13    /// component gets a distinct label in scan order starting at `1`.
14    pub fn connected_components(&self, connectivity: u8) -> Result<EventFrame, ClusterError> {
15        if connectivity != 4 && connectivity != 8 {
16            return Err(ClusterError::InvalidConnectivity(connectivity));
17        }
18        let (channels, height, width) = self.shape();
19        let plane = width
20            .checked_mul(height)
21            .ok_or(ClusterError::SizeOverflow)?;
22
23        let foreground = foreground_mask(self.data(), plane, channels);
24        let mut labels = vec![0_u64; plane];
25        let mut next = 0_u64;
26        let mut stack = Vec::new();
27        let neighbours = neighbour_offsets(connectivity);
28
29        for start in 0..plane {
30            if !foreground[start] || labels[start] != 0 {
31                continue;
32            }
33            next += 1;
34            labels[start] = next;
35            stack.push(start);
36            while let Some(index) = stack.pop() {
37                let (x, y) = (index % width, index / width);
38                for &(dx, dy) in neighbours {
39                    let (nx, ny) = (x as isize + dx, y as isize + dy);
40                    if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
41                        continue;
42                    }
43                    let neighbour = ny as usize * width + nx as usize;
44                    if foreground[neighbour] && labels[neighbour] == 0 {
45                        labels[neighbour] = next;
46                        stack.push(neighbour);
47                    }
48                }
49            }
50        }
51
52        Ok(EventFrame::from_parts(
53            EventFrameData::U64(labels),
54            width,
55            height,
56            RepresentationKind::Labels,
57            vec!["labels".to_owned()],
58        ))
59    }
60}
61
62/// A pixel is foreground when any channel is non-zero.
63fn foreground_mask(data: &EventFrameData, plane: usize, channels: usize) -> Vec<bool> {
64    let mut mask = vec![false; plane];
65    let mut mark = |nonzero: &dyn Fn(usize) -> bool| {
66        for c in 0..channels {
67            let base = c * plane;
68            for (i, slot) in mask.iter_mut().enumerate() {
69                if nonzero(base + i) {
70                    *slot = true;
71                }
72            }
73        }
74    };
75    match data {
76        EventFrameData::U8(v) => mark(&|k| v[k] != 0),
77        EventFrameData::U16(v) => mark(&|k| v[k] != 0),
78        EventFrameData::U64(v) => mark(&|k| v[k] != 0),
79        EventFrameData::F32(v) => mark(&|k| v[k] != 0.0),
80    }
81    mask
82}
83
84fn neighbour_offsets(connectivity: u8) -> &'static [(isize, isize)] {
85    const FOUR: [(isize, isize); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
86    const EIGHT: [(isize, isize); 8] = [
87        (-1, -1),
88        (0, -1),
89        (1, -1),
90        (-1, 0),
91        (1, 0),
92        (-1, 1),
93        (0, 1),
94        (1, 1),
95    ];
96    if connectivity == 4 {
97        &FOUR
98    } else {
99        &EIGHT
100    }
101}
102
103#[derive(Debug, PartialEq, Eq)]
104pub enum ClusterError {
105    SizeOverflow,
106    InvalidConnectivity(u8),
107}
108
109impl fmt::Display for ClusterError {
110    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::SizeOverflow => formatter.write_str("label field dimensions are too large"),
113            Self::InvalidConnectivity(value) => {
114                write!(formatter, "connectivity must be 4 or 8, got {value}")
115            }
116        }
117    }
118}
119
120impl Error for ClusterError {}
121
122#[cfg(test)]
123mod tests {
124    use super::ClusterError;
125    use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
126
127    fn frame(width: usize, height: usize, pixels: &[u8]) -> EventFrame {
128        EventFrame::from_parts(
129            EventFrameData::U8(pixels.to_vec()),
130            width,
131            height,
132            RepresentationKind::Count,
133            vec!["count".to_owned()],
134        )
135    }
136
137    fn labels(frame: &EventFrame) -> Vec<u64> {
138        let EventFrameData::U64(values) = frame.data() else {
139            panic!("labels are u64");
140        };
141        values.clone()
142    }
143
144    #[test]
145    fn rejects_bad_connectivity() {
146        let f = frame(2, 2, &[0, 0, 0, 0]);
147        assert_eq!(
148            f.connected_components(6).unwrap_err(),
149            ClusterError::InvalidConnectivity(6)
150        );
151    }
152
153    #[test]
154    fn empty_frame_is_all_background() {
155        let out = frame(3, 3, &[0; 9]).connected_components(8).unwrap();
156        assert_eq!(out.shape(), (1, 3, 3));
157        assert!(labels(&out).iter().all(|&l| l == 0));
158    }
159
160    #[test]
161    fn two_diagonal_blobs_split_under_4_but_merge_under_8() {
162        // Two pixels touching only at a corner: (0,0) and (1,1).
163        let pixels = [1, 0, 0, 1];
164        let four = frame(2, 2, &pixels).connected_components(4).unwrap();
165        assert_eq!(labels(&four), vec![1, 0, 0, 2]);
166        let eight = frame(2, 2, &pixels).connected_components(8).unwrap();
167        assert_eq!(labels(&eight), vec![1, 0, 0, 1]);
168    }
169
170    #[test]
171    fn foreground_is_any_nonzero_channel() {
172        // Two channels; pixel 1 is foreground only via channel 1.
173        let data = EventFrameData::U8(vec![0, 0, 0, 1]);
174        let f = EventFrame::from_parts(
175            data,
176            2,
177            1,
178            RepresentationKind::Polarity,
179            vec!["a".to_owned(), "b".to_owned()],
180        );
181        assert_eq!(labels(&f.connected_components(4).unwrap()), vec![0, 1]);
182    }
183}