1use crate::kernels::geometry::{js_ceil, js_floor, js_round, Rect};
10use crate::kernels::pool::SyncPtr;
11
12pub struct Vote<'a> {
14 pub x0: i32,
15 pub y0: i32,
16 pub w: i32,
17 pub h: i32,
18 pub bits: &'a [u8],
20 pub clean: &'a [u8],
22}
23
24impl Vote<'_> {
25 #[inline]
27 pub fn any_negative(&self, ax0: i32, ax1: i32, ay: i32) -> bool {
28 (ax0..=ax1).any(|ax| self.verdict(ax, ay) < 0)
29 }
30
31 #[inline]
33 pub fn verdict(&self, ax: i32, ay: i32) -> i32 {
34 let lx = ax - self.x0;
35 let ly = ay - self.y0;
36 if lx < 0 || ly < 0 || lx >= self.w || ly >= self.h {
37 return 0;
38 }
39 let i = (ly * self.w + lx) as usize;
40 let bit = 1u8 << (i & 7);
41 if self.bits.get(i >> 3).is_some_and(|b| b & bit != 0) {
42 -1
43 } else if self.clean.get(i >> 3).is_some_and(|b| b & bit != 0) {
44 1
45 } else {
46 0
47 }
48 }
49}
50
51pub struct Neighbour<'a> {
52 pub rgba: &'a [u8],
53 pub pose_x: f64,
54 pub pose_y: f64,
55 pub occlusions: &'a [Rect],
56 pub vote: Option<Vote<'a>>,
57}
58
59pub struct MaskInput<'a> {
60 pub rgba: &'a [u8],
61 pub labels: &'a [u8],
62 pub width: usize,
63 pub height: usize,
64 pub region: Rect,
65 pub code: u8,
66 pub pose_x: f64,
67 pub pose_y: f64,
68 pub prev: Option<Neighbour<'a>>,
69 pub next: Option<Neighbour<'a>>,
70 pub vote: Option<Vote<'a>>,
71 pub factor: i32,
72 pub noise: f64,
73}
74
75struct Resolved<'n, 'a> {
76 rgba: &'a [u8],
77 dx: i64,
78 dy: i64,
79 occlusions: &'a [Rect],
80 vote: Option<&'n Vote<'a>>,
81}
82
83fn resolve<'n, 'a>(n: &'n Neighbour<'a>, cx: i32, cy: i32) -> Resolved<'n, 'a> {
84 Resolved {
85 rgba: n.rgba,
86 dx: (cx - js_round(n.pose_x)) as i64,
87 dy: (cy - js_round(n.pose_y)) as i64,
88 occlusions: n.occlusions,
89 vote: n.vote.as_ref(),
90 }
91}
92
93#[inline]
95fn span(x: f64, width: f64) -> (i64, i64) {
96 (x.ceil() as i64, (x + width).ceil() as i64)
97}
98
99fn sum_limit(noise: f64) -> u32 {
102 (0..=765u32)
103 .take_while(|&s| s as f64 / 3.0 <= noise)
104 .last()
105 .unwrap_or(0)
106}
107
108struct Ctx<'n, 'a> {
110 input: &'n MaskInput<'a>,
111 neighbours: [Option<Resolved<'n, 'a>>; 2],
112 w: i64,
113 h: i64,
114 rx0: i64,
115 rx1: i64,
116 factor: i64,
117 limit: u32,
118}
119
120struct NeighbourRow<'r, 'n, 'a> {
122 n: &'r Resolved<'n, 'a>,
123 labels: &'a [u8],
124 rgba: &'a [u8],
125 lo: i64,
127 hi: i64,
128 ay: i32,
130 fy: f64,
132 occluded: bool,
134}
135
136impl NeighbourRow<'_, '_, '_> {
137 #[inline]
138 fn occludes(&self, sx: i64) -> bool {
139 self.occluded
140 && self.n.occlusions.iter().any(|o| {
141 if !(self.fy >= o.y && self.fy < o.y + o.height) {
142 return false;
143 }
144 let (a, b) = span(o.x, o.width);
145 sx >= a - self.n.dx && sx < b - self.n.dx
146 })
147 }
148}
149
150pub fn consistency_mask(input: &MaskInput<'_>, out: &mut [u8]) {
154 let (w, h) = (input.width as i64, input.height as i64);
155 let (cx, cy) = (js_round(input.pose_x), js_round(input.pose_y));
156 let ctx = Ctx {
157 input,
158 neighbours: [
159 input.prev.as_ref().map(|n| resolve(n, cx, cy)),
160 input.next.as_ref().map(|n| resolve(n, cx, cy)),
161 ],
162 w,
163 h,
164 rx0: js_floor(input.region.x).clamp(0, w as i32) as i64,
165 rx1: js_ceil(input.region.x + input.region.width).clamp(0, w as i32) as i64,
166 factor: input.factor.max(1) as i64,
167 limit: sum_limit(input.noise),
168 };
169 let ry0 = js_floor(input.region.y).clamp(0, h as i32) as usize;
170 let ry1 = js_ceil(input.region.y + input.region.height).clamp(0, h as i32) as usize;
171 let width = input.width;
172 let out = &mut out[..input.width * input.height];
173 out[..ry0 * width].fill(1);
175 out[ry1.max(ry0) * width..].fill(1);
176 let rows = ry1.saturating_sub(ry0);
177 let chunks = crate::kernels::pool::chunks_for(rows * width, 64 * 1024);
178 let base = SyncPtr(out.as_mut_ptr());
179 crate::kernels::pool::par_for(chunks, |c| {
180 let (a, b) = (
181 ry0 + crate::kernels::pool::split(rows, chunks, c),
182 ry0 + crate::kernels::pool::split(rows, chunks, c + 1),
183 );
184 for sy in a..b {
185 let row = unsafe { std::slice::from_raw_parts_mut(base.get().add(sy * width), width) };
187 row.fill(1);
188 mask_row(&ctx, sy as i64, row);
189 }
190 });
191}
192
193fn mask_row(ctx: &Ctx<'_, '_>, sy: i64, out_row: &mut [u8]) {
194 let input = ctx.input;
195 let (w, h, factor) = (ctx.w, ctx.h, ctx.factor);
196 let width = input.width;
197 let labels = input.labels;
198 let ay = sy.div_euclid(factor) as i32;
199 let mut rows: [Option<NeighbourRow<'_, '_, '_>>; 2] = [None, None];
200 let mut count = 0usize;
201 for n in ctx.neighbours.iter().flatten() {
202 let iy = sy + n.dy;
203 if iy < 0 || iy >= h {
204 continue;
205 }
206 let base = (iy * w) as usize;
207 let fy = iy as f64;
208 rows[count] = Some(NeighbourRow {
209 n,
210 labels: &labels[base..base + width],
211 rgba: &n.rgba[base * 4..(base + width) * 4],
212 lo: (-n.dx).max(ctx.rx0),
213 hi: (w - n.dx).min(ctx.rx1),
214 ay: iy.div_euclid(factor) as i32,
215 fy,
216 occluded: n
217 .occlusions
218 .iter()
219 .any(|o| fy >= o.y && fy < o.y + o.height),
220 });
221 count += 1;
222 }
223 let rows = &rows;
224 let row_vote = input
225 .vote
226 .as_ref()
227 .filter(|v| ay >= v.y0 && ay < v.y0 + v.h);
228 let base = (sy * w) as usize;
229 let lab_row = &labels[base..base + width];
230 let rgb_row = &input.rgba[base * 4..(base + width) * 4];
231 let mut sx = ctx.rx0;
232 let (fast_lo, fast_hi) = rows
236 .iter()
237 .flatten()
238 .fold((ctx.rx0, ctx.rx1), |(lo, hi), r| {
239 if r.occluded {
240 (lo, lo)
241 } else {
242 (lo.max(r.lo), hi.min(r.hi))
243 }
244 });
245 while sx < ctx.rx1 {
246 if sx >= fast_lo && sx + 4 <= fast_hi {
247 let votes_clear = row_vote.is_none_or(|v| {
248 !v.any_negative(
249 sx.div_euclid(factor) as i32,
250 (sx + 3).div_euclid(factor) as i32,
251 ay,
252 )
253 });
254 if votes_clear && block_agrees(ctx, rows, lab_row, rgb_row, sx as usize) {
255 sx += 4;
256 continue;
257 }
258 for x in sx..sx + 4 {
259 mask_pixel(ctx, rows, row_vote, ay, lab_row, rgb_row, out_row, x);
260 }
261 sx += 4;
262 continue;
263 }
264 mask_pixel(ctx, rows, row_vote, ay, lab_row, rgb_row, out_row, sx);
265 sx += 1;
266 }
267}
268
269#[inline]
273fn block_agrees(
274 ctx: &Ctx<'_, '_>,
275 rows: &[Option<NeighbourRow<'_, '_, '_>>; 2],
276 lab_row: &[u8],
277 rgb_row: &[u8],
278 x: usize,
279) -> bool {
280 let code = ctx.input.code;
281 #[cfg(target_feature = "simd128")]
282 {
283 use core::arch::wasm32::*;
284 let _ = code;
285 unsafe {
288 let codes = u32x4_splat(ctx.input.code as u32);
289 let labels4 = |row: &[u8], at: usize| {
290 let v = v128_load32_zero(row.as_ptr().add(at) as *const u32);
291 u32x4_extend_low_u16x8(u16x8_extend_low_u8x16(v))
292 };
293 let inside = u32x4_eq(labels4(lab_row, x), codes);
294 if !v128_any_true(inside) {
295 return true;
296 }
297 let a = v128_load(rgb_row.as_ptr().add(x * 4) as *const v128);
298 let limit = u32x4_splat(ctx.limit);
299 let rgb = u32x4_splat(0x00ff_ffff);
300 let mut bad = u32x4_splat(0);
301 for r in rows.iter().flatten() {
302 let ix = (x as i64 + r.n.dx) as usize;
303 let b = v128_load(r.rgba.as_ptr().add(ix * 4) as *const v128);
304 let d = v128_and(v128_or(u8x16_sub_sat(a, b), u8x16_sub_sat(b, a)), rgb);
305 let sum = u32x4_extadd_pairwise_u16x8(u16x8_extadd_pairwise_u8x16(d));
306 let differs = u32x4_gt(sum, limit);
307 let counted = u32x4_eq(labels4(r.labels, ix), codes);
308 bad = v128_or(bad, v128_and(differs, counted));
309 }
310 !v128_any_true(v128_and(bad, inside))
311 }
312 }
313 #[cfg(not(target_feature = "simd128"))]
314 {
315 (x..x + 4).all(|x| {
316 lab_row[x] != code
317 || rows.iter().flatten().all(|r| {
318 let ix = (x as i64 + r.n.dx) as usize;
319 if r.labels[ix] != code {
320 return true;
321 }
322 let (i, j) = (x * 4, ix * 4);
323 let sum = (rgb_row[i] as i32 - r.rgba[j] as i32).unsigned_abs()
324 + (rgb_row[i + 1] as i32 - r.rgba[j + 1] as i32).unsigned_abs()
325 + (rgb_row[i + 2] as i32 - r.rgba[j + 2] as i32).unsigned_abs();
326 sum <= ctx.limit
327 })
328 })
329 }
330}
331
332#[allow(clippy::too_many_arguments)]
334#[inline]
335fn mask_pixel(
336 ctx: &Ctx<'_, '_>,
337 rows: &[Option<NeighbourRow<'_, '_, '_>>; 2],
338 row_vote: Option<&Vote<'_>>,
339 ay: i32,
340 lab_row: &[u8],
341 rgb_row: &[u8],
342 out_row: &mut [u8],
343 sx: i64,
344) {
345 let code = ctx.input.code;
346 let factor = ctx.factor;
347 let x = sx as usize;
348 if lab_row[x] != code {
349 return;
350 }
351 if row_vote.is_some_and(|v| v.verdict(sx.div_euclid(factor) as i32, ay) < 0) {
352 out_row[x] = 0;
353 return;
354 }
355 let k = x * 4;
356 let (r, g, b) = (
357 rgb_row[k] as i32,
358 rgb_row[k + 1] as i32,
359 rgb_row[k + 2] as i32,
360 );
361 let (mut checked, mut condemned, mut excused) = (0u32, false, 0u32);
362 for row in rows.iter().flatten() {
363 if sx < row.lo || sx >= row.hi {
366 continue;
367 }
368 let ix = (sx + row.n.dx) as usize;
369 if row.labels[ix] != code || row.occludes(sx) {
370 continue;
371 }
372 checked += 1;
373 let j = ix * 4;
374 let (nr, ng, nb) = (
375 row.rgba[j] as i32,
376 row.rgba[j + 1] as i32,
377 row.rgba[j + 2] as i32,
378 );
379 if r == nr && g == ng && b == nb {
380 continue;
381 }
382 let diff = ((r - nr).abs() + (g - ng).abs() + (b - nb).abs()) as f64 / 3.0;
383 if diff <= ctx.input.noise {
384 continue;
385 }
386 if row
387 .n
388 .vote
389 .is_some_and(|v| v.verdict((ix as i64).div_euclid(factor) as i32, row.ay) < 0)
390 {
391 excused += 1;
392 } else {
393 condemned = true;
394 break;
395 }
396 }
397 if condemned || (excused > 0 && checked >= 2) {
398 out_row[x] = 0;
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 fn frame(w: usize, h: usize, value: u8) -> Vec<u8> {
407 (0..w * h)
408 .flat_map(|_| [value, value, value, 255])
409 .collect()
410 }
411
412 #[test]
413 fn lone_disagreeing_neighbour_condemns_unless_excused_by_its_verdict() {
414 let (w, h) = (4usize, 4usize);
415 let current = frame(w, h, 10);
416 let other = frame(w, h, 200);
417 let labels = vec![1u8; w * h];
418 let region = Rect {
419 x: 0.0,
420 y: 0.0,
421 width: 4.0,
422 height: 4.0,
423 };
424 let mut out = vec![0u8; w * h];
425 let base = |vote| MaskInput {
426 rgba: ¤t,
427 labels: &labels,
428 width: w,
429 height: h,
430 region,
431 code: 1,
432 pose_x: 0.0,
433 pose_y: 0.0,
434 prev: Some(Neighbour {
435 rgba: &other,
436 pose_x: 0.0,
437 pose_y: 0.0,
438 occlusions: &[],
439 vote,
440 }),
441 next: None,
442 vote: None,
443 factor: 1,
444 noise: 10.0,
445 };
446 consistency_mask(&base(None), &mut out);
447 assert!(out.iter().all(|&v| v == 0));
448 let bits = vec![0xffu8; 2];
449 let clean = vec![0u8; 2];
450 let excused = base(Some(Vote {
451 x0: 0,
452 y0: 0,
453 w: 4,
454 h: 4,
455 bits: &bits,
456 clean: &clean,
457 }));
458 consistency_mask(&excused, &mut out);
459 assert!(out.iter().all(|&v| v == 1));
460 }
461}