blazediff_interpret/region/
mod.rs1mod label_extract;
2mod morphology;
3
4use super::types::BoundingBox;
5use label_extract::extract_labeled_regions;
6use morphology::morph_close;
7
8struct UnionFind {
9 parent: Vec<u32>,
10 rank: Vec<u8>,
11}
12
13impl UnionFind {
14 fn new(size: usize) -> Self {
15 Self {
16 parent: (0..size as u32).collect(),
17 rank: vec![0; size],
18 }
19 }
20
21 fn find(&mut self, mut x: u32) -> u32 {
22 while self.parent[x as usize] != x {
23 self.parent[x as usize] = self.parent[self.parent[x as usize] as usize];
24 x = self.parent[x as usize];
25 }
26 x
27 }
28
29 fn union(&mut self, a: u32, b: u32) {
30 let ra = self.find(a);
31 let rb = self.find(b);
32 if ra == rb {
33 return;
34 }
35 match self.rank[ra as usize].cmp(&self.rank[rb as usize]) {
36 std::cmp::Ordering::Less => self.parent[ra as usize] = rb,
37 std::cmp::Ordering::Greater => self.parent[rb as usize] = ra,
38 std::cmp::Ordering::Equal => {
39 self.parent[rb as usize] = ra;
40 self.rank[ra as usize] += 1;
41 }
42 }
43 }
44}
45
46pub struct ComponentInfo {
47 pub bbox: BoundingBox,
48 pub pixel_count: u32,
49}
50
51pub fn extract_change_mask(output_data: &[u8], width: u32, height: u32) -> Vec<bool> {
53 let total = (width * height) as usize;
54 let mut mask = vec![false; total];
55
56 #[cfg(target_arch = "aarch64")]
57 {
58 unsafe { extract_mask_neon(output_data.as_ptr(), &mut mask, total) };
60 return mask;
61 }
62
63 #[cfg(not(target_arch = "aarch64"))]
64 {
65 for i in 0..total {
66 let pos = i * 4;
67 let r = output_data[pos];
68 let g = output_data[pos + 1];
69 let b = output_data[pos + 2];
70 mask[i] = r != g || r != b;
71 }
72 mask
73 }
74}
75
76#[cfg(target_arch = "aarch64")]
78unsafe fn extract_mask_neon(data: *const u8, mask: &mut [bool], count: usize) {
79 use std::arch::aarch64::*;
80
81 let chunks = count / 16;
82 let mask_ptr = mask.as_mut_ptr() as *mut u8;
83
84 for i in 0..chunks {
85 let base = i * 16 * 4;
86 let rgba = vld4q_u8(data.add(base));
88 let r = rgba.0;
89 let g = rgba.1;
90 let b = rgba.2;
91
92 let ne_rg = vmvnq_u8(vceqq_u8(r, g));
94 let ne_rb = vmvnq_u8(vceqq_u8(r, b));
96 let changed = vorrq_u8(ne_rg, ne_rb);
98
99 let ones = vdupq_n_u8(1);
101 let result = vandq_u8(changed, ones);
102
103 vst1q_u8(mask_ptr.add(i * 16), result);
104 }
105
106 let processed = chunks * 16;
108 for i in processed..count {
109 let pos = i * 4;
110 let r = *data.add(pos);
111 let g = *data.add(pos + 1);
112 let b = *data.add(pos + 2);
113 mask[i] = r != g || r != b;
114 }
115}
116
117#[cfg(test)]
119fn find_connected_components(mask: &[bool], width: u32, height: u32) -> Vec<ComponentInfo> {
120 let labels = label_connected_components(mask, width, height);
121 extract_labeled_regions(&labels, mask, width)
122}
123
124fn label_connected_components(mask: &[bool], width: u32, height: u32) -> Vec<i32> {
127 let w = width as usize;
128 let h = height as usize;
129 let total = w * h;
130 let mut labels = vec![0i32; total];
131
132 if total == 0 {
133 return labels;
134 }
135
136 let mut uf = UnionFind::new(total);
137
138 for y in 0..h {
139 for x in 0..w {
140 let idx = y * w + x;
141 if !mask[idx] {
142 continue;
143 }
144 if x > 0 && mask[idx - 1] {
145 uf.union(idx as u32, (idx - 1) as u32);
146 }
147 if y > 0 && mask[idx - w] {
148 uf.union(idx as u32, (idx - w) as u32);
149 }
150 }
151 }
152
153 let mut root_to_label: std::collections::HashMap<u32, i32> = std::collections::HashMap::new();
155 let mut next_label = 1i32;
156
157 for i in 0..total {
158 if !mask[i] {
159 continue;
160 }
161 let root = uf.find(i as u32);
162 let label = *root_to_label.entry(root).or_insert_with(|| {
163 let l = next_label;
164 next_label += 1;
165 l
166 });
167 labels[i] = label;
168 }
169
170 labels
171}
172
173pub fn detect_regions(mask: &[bool], width: u32, height: u32) -> Vec<ComponentInfo> {
176 let total = (width * height) as usize;
177 if total == 0 || !mask.iter().any(|&m| m) {
178 return Vec::new();
179 }
180
181 let closed = morph_close(mask, width, height);
183
184 let labels = label_connected_components(&closed, width, height);
186
187 extract_labeled_regions(&labels, mask, width)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_extract_change_mask_gray_pixels() {
197 let data = vec![128, 128, 128, 255, 128, 128, 128, 255];
198 let mask = extract_change_mask(&data, 2, 1);
199 assert_eq!(mask, vec![false, false]);
200 }
201
202 #[test]
203 fn test_extract_change_mask_colored_pixels() {
204 let data = vec![255, 0, 0, 255, 128, 128, 128, 255];
205 let mask = extract_change_mask(&data, 2, 1);
206 assert_eq!(mask, vec![true, false]);
207 }
208
209 #[test]
210 fn test_find_connected_components_empty() {
211 let mask = vec![false; 9];
212 let components = find_connected_components(&mask, 3, 3);
213 assert!(components.is_empty());
214 }
215
216 #[test]
217 fn test_find_connected_components_single_pixel() {
218 let mut mask = vec![false; 9];
219 mask[4] = true;
220 let components = find_connected_components(&mask, 3, 3);
221 assert_eq!(components.len(), 1);
222 assert_eq!(components[0].pixel_count, 1);
223 assert_eq!(
224 components[0].bbox,
225 BoundingBox {
226 x: 1,
227 y: 1,
228 width: 1,
229 height: 1,
230 }
231 );
232 }
233
234 #[test]
235 fn test_find_connected_components_two_separate() {
236 let mask = vec![true, false, false, false, true];
237 let components = find_connected_components(&mask, 5, 1);
238 assert_eq!(components.len(), 2);
239 }
240
241 #[test]
242 fn test_find_connected_components_l_shape() {
243 let mask = vec![true, false, false, true, false, false, true, true, true];
244 let components = find_connected_components(&mask, 3, 3);
245 assert_eq!(components.len(), 1);
246 assert_eq!(components[0].pixel_count, 5);
247 assert_eq!(
248 components[0].bbox,
249 BoundingBox {
250 x: 0,
251 y: 0,
252 width: 3,
253 height: 3,
254 }
255 );
256 }
257
258 #[test]
259 fn test_detect_regions_empty() {
260 let mask = vec![false; 100];
261 let regions = detect_regions(&mask, 10, 10);
262 assert!(regions.is_empty());
263 }
264
265 #[test]
266 fn test_detect_regions_single_blob() {
267 let mut mask = vec![false; 10000]; for y in 40..60 {
269 for x in 40..60 {
270 mask[y * 100 + x] = true;
271 }
272 }
273 let regions = detect_regions(&mask, 100, 100);
274 assert_eq!(regions.len(), 1);
275 assert_eq!(regions[0].pixel_count, 400);
276 }
277
278 #[test]
279 fn test_detect_regions_two_separate_blobs() {
280 let mut mask = vec![false; 10000]; for y in 5..15 {
283 for x in 5..15 {
284 mask[y * 100 + x] = true;
285 }
286 }
287 for y in 80..90 {
288 for x in 80..90 {
289 mask[y * 100 + x] = true;
290 }
291 }
292 let regions = detect_regions(&mask, 100, 100);
293 assert_eq!(regions.len(), 2);
294 }
295
296 #[test]
297 fn test_detect_regions_nearby_blobs_bridged() {
298 let mut mask = vec![false; 10000]; for y in 40..50 {
302 for x in 40..48 {
303 mask[y * 100 + x] = true;
304 }
305 }
306 for y in 40..50 {
308 for x in 51..60 {
309 mask[y * 100 + x] = true;
310 }
311 }
312 let regions = detect_regions(&mask, 100, 100);
313 assert_eq!(
315 regions.len(),
316 1,
317 "nearby blobs should be merged by morph close"
318 );
319 }
320
321 #[test]
322 fn test_detect_regions_connected_bridge() {
323 let mut mask = vec![false; 50 * 20]; for y in 6..14 {
326 for x in 2..10 {
327 mask[y * 50 + x] = true;
328 }
329 }
330 for y in 6..14 {
331 for x in 30..38 {
332 mask[y * 50 + x] = true;
333 }
334 }
335 for x in 10..30 {
336 mask[10 * 50 + x] = true;
337 }
338 let regions = detect_regions(&mask, 50, 20);
339 assert_eq!(regions.len(), 1, "connected blobs should be one region");
340 }
341
342 #[test]
343 fn test_extract_mask_simd_matches_scalar() {
344 for size in [1, 15, 16, 17, 31, 32, 33, 64, 100] {
346 let mut data = vec![128u8; size * 4]; for i in (0..size).step_by(3) {
349 data[i * 4] = 255; }
351
352 let mask = extract_change_mask(&data, size as u32, 1);
353
354 for i in 0..size {
355 let r = data[i * 4];
356 let g = data[i * 4 + 1];
357 let b = data[i * 4 + 2];
358 let expected = r != g || r != b;
359 assert_eq!(
360 mask[i], expected,
361 "mismatch at pixel {i} (size={size}): r={r} g={g} b={b}"
362 );
363 }
364 }
365 }
366}