1use super::stats::LayerStats;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Kind {
14 Opaque,
15 Veil,
16}
17
18#[derive(Clone, Debug)]
19pub struct Object {
20 pub kind: Kind,
21 pub bx: usize,
23 pub by: usize,
24 pub bw: usize,
25 pub bh: usize,
26 pub mask: Vec<bool>,
28 pub tpl: Vec<[u8; 3]>,
30 pub stable: Vec<u32>,
32 pub coarse: Vec<u32>,
34 pub textured: bool,
36 pub last: (i32, i32),
38 pub flat: Option<[u8; 3]>,
41}
42
43impl Object {
44 pub fn area(&self) -> usize {
45 self.mask.iter().filter(|&&m| m).count()
46 }
47
48 pub fn track(&self) -> Track {
50 if self.bh >= 3 * self.bw {
51 Track::Vertical
52 } else if self.bw >= 3 * self.bh {
53 Track::Horizontal
54 } else {
55 Track::None
56 }
57 }
58
59 pub fn zone(&self, cw: usize, ch: usize) -> (usize, usize, usize, usize) {
62 let (bx0, bx1) = (self.bx.saturating_sub(1), (self.bx + self.bw + 1).min(cw));
63 let (by0, by1) = (self.by.saturating_sub(1), (self.by + self.bh + 1).min(ch));
64 match self.track() {
65 Track::Vertical => (bx0, bx1, 0, ch),
66 Track::Horizontal => (0, cw, by0, by1),
67 Track::None => (bx0, bx1, by0, by1),
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum Track {
75 None,
76 Vertical,
77 Horizontal,
78}
79
80impl LayerModel {
81 pub fn overlay_zone(&self) -> Vec<bool> {
84 let mut z = vec![false; self.cw * self.ch];
85 for o in self.objects.iter().filter(|o| o.kind == Kind::Opaque) {
86 let (x0, x1, y0, y1) = o.zone(self.cw, self.ch);
87 for y in y0..y1 {
88 for x in x0..x1 {
89 z[y * self.cw + x] = true;
90 }
91 }
92 }
93 z
94 }
95}
96
97#[derive(Clone, Debug)]
98pub struct LayerModel {
99 pub x: i64,
100 pub y: i64,
101 pub cw: usize,
102 pub ch: usize,
103 pub objects: Vec<Object>,
104}
105
106const ENOUGH: u16 = 8;
108const POOL: usize = 1; fn pooled(v: &[u32], cw: usize, ch: usize) -> Vec<u64> {
111 let mut rows = vec![0u64; cw * ch];
113 for y in 0..ch {
114 for x in 0..cw {
115 let (a, b) = (x.saturating_sub(POOL), (x + POOL).min(cw - 1));
116 rows[y * cw + x] = (a..=b).map(|i| v[y * cw + i] as u64).sum();
117 }
118 }
119 let mut out = vec![0u64; cw * ch];
120 for y in 0..ch {
121 let (a, b) = (y.saturating_sub(POOL), (y + POOL).min(ch - 1));
122 for x in 0..cw {
123 out[y * cw + x] = (a..=b).map(|j| rows[j * cw + x]).sum();
124 }
125 }
126 out
127}
128
129fn dilate(m: &[bool], cw: usize, ch: usize) -> Vec<bool> {
130 let mut out = vec![false; m.len()];
131 for y in 0..ch {
132 for x in 0..cw {
133 if !m[y * cw + x] {
134 continue;
135 }
136 for yy in y.saturating_sub(1)..=(y + 1).min(ch - 1) {
137 for xx in x.saturating_sub(1)..=(x + 1).min(cw - 1) {
138 out[yy * cw + xx] = true;
139 }
140 }
141 }
142 }
143 out
144}
145
146fn erode(m: &[bool], cw: usize, ch: usize) -> Vec<bool> {
147 let inv: Vec<bool> = m.iter().map(|&b| !b).collect();
148 dilate(&inv, cw, ch).iter().map(|&b| !b).collect()
149}
150
151fn components(m: &[bool], cw: usize, ch: usize) -> (Vec<u32>, u32) {
153 let mut label = vec![0u32; m.len()];
154 let mut next = 0;
155 let mut stack = Vec::new();
156 for start in 0..m.len() {
157 if !m[start] || label[start] != 0 {
158 continue;
159 }
160 next += 1;
161 label[start] = next;
162 stack.push(start);
163 while let Some(i) = stack.pop() {
164 let (x, y) = (i % cw, i / cw);
165 for yy in y.saturating_sub(1)..=(y + 1).min(ch - 1) {
166 for xx in x.saturating_sub(1)..=(x + 1).min(cw - 1) {
167 let j = yy * cw + xx;
168 if m[j] && label[j] == 0 {
169 label[j] = next;
170 stack.push(j);
171 }
172 }
173 }
174 }
175 }
176 (label, next)
177}
178
179pub fn build(stats: &LayerStats, tau: i32) -> LayerModel {
181 let (cw, ch) = (stats.cw, stats.ch);
182 let len = cw * ch;
183 let n = pooled(&stats.field(|a| a.n as u32), cw, ch);
184 let bad = pooled(&stats.field(|a| a.bad as u32), cw, ch);
185 let sr = pooled(&stats.field(|a| a.sr), cw, ch);
186 let so = pooled(&stats.field(|a| a.so), cw, ch);
187 let srr = pooled(&stats.field(|a| a.srr), cw, ch);
188 let sro = pooled(&stats.field(|a| a.sro), cw, ch);
189 let soo = pooled(&stats.field(|a| a.soo), cw, ch);
190 let mut opaque = vec![false; len];
191 let mut veil = vec![false; len];
192 for i in 0..len {
193 if stats.cells[i].n < ENOUGH {
194 continue;
195 }
196 let nn = n[i].max(1) as f64;
197 let frac = bad[i] as f64 / nn;
198 let (mr, mo) = (sr[i] as f64 / nn, so[i] as f64 / nn);
199 let vr = srr[i] as f64 / nn - mr * mr;
200 let cov = sro[i] as f64 / nn - mr * mo;
201 let vo = soo[i] as f64 / nn - mo * mo;
202 let noise = (tau / 4).max(2) as f64;
206 let hi = if vr > noise * noise {
207 let slope = cov / vr;
208 let resid = (vo - cov * cov / vr).max(0.0);
209 let se = ((resid + 4.0) / (nn * vr)).sqrt();
210 slope + 2.0 * se
211 } else {
212 f64::INFINITY
213 };
214 let opaque_gain = 0.15;
217 let veil_gain = 0.85;
218 let repeated_disagreement = stats.cells[i].bad >= 4;
221 if frac > 0.3
222 || (hi < opaque_gain && (vo < vr * opaque_gain * opaque_gain || repeated_disagreement))
223 {
224 opaque[i] = true;
225 } else if hi < veil_gain {
226 let difference_variance = (vr + vo - 2.0 * cov).max(0.0);
231 let mean_error = (difference_variance / stats.cells[i].n as f64).sqrt();
232 let shifted = repeated_disagreement && (mo - mr).abs() > noise + 2.0 * mean_error;
233 if vo < vr * veil_gain * veil_gain || shifted {
234 veil[i] = true;
235 }
236 }
237 }
238 let enough: Vec<bool> = stats.cells.iter().map(|a| a.n >= ENOUGH).collect();
239 let core: Vec<bool> = erode(&dilate(&opaque, cw, ch), cw, ch)
240 .iter()
241 .zip(&enough)
242 .map(|(&a, &b)| a && b)
243 .collect();
244 let grown: Vec<bool> = dilate(&core, cw, ch)
245 .iter()
246 .zip(&enough)
247 .map(|(&a, &b)| a && b)
248 .collect();
249 let veil: Vec<bool> = erode(&dilate(&veil, cw, ch), cw, ch)
250 .iter()
251 .zip(&enough)
252 .zip(&grown)
253 .map(|((&a, &b), &g)| a && b && !g)
254 .collect();
255 let mut objects = Vec::new();
256 for (kind, mask) in [(Kind::Opaque, &grown), (Kind::Veil, &veil)] {
257 let (label, count) = components(mask, cw, ch);
258 let mut boxes = vec![(usize::MAX, usize::MAX, 0usize, 0usize, 0usize); count as usize];
259 for (i, &l) in label.iter().enumerate() {
260 if l == 0 {
261 continue;
262 }
263 let b = &mut boxes[l as usize - 1];
264 let (x, y) = (i % cw, i / cw);
265 b.0 = b.0.min(x);
266 b.1 = b.1.min(y);
267 b.2 = b.2.max(x);
268 b.3 = b.3.max(y);
269 b.4 += 1;
270 }
271 for (k, &(x0, y0, x1, y1, area)) in boxes.iter().enumerate() {
272 if area < 3 {
274 continue;
275 }
276 let (bw, bh) = (x1 - x0 + 1, y1 - y0 + 1);
277 let mut m = vec![false; bw * bh];
278 let mut tpl = vec![[0u8; 3]; bw * bh];
279 let mut stable = Vec::new();
280 for yy in 0..bh {
281 for xx in 0..bw {
282 let i = (y0 + yy) * cw + x0 + xx;
283 if label[i] != k as u32 + 1 {
284 continue;
285 }
286 let j = yy * bw + xx;
287 m[j] = true;
288 let a = stats.cells[i];
289 let nb = a.bad as u32;
290 if nb == 0 {
291 continue;
292 }
293 tpl[j] = [
294 ((a.br + nb / 2) / nb) as u8,
295 ((a.bg + nb / 2) / nb) as u8,
296 ((a.bb + nb / 2) / nb) as u8,
297 ];
298 let ml = a.bl as f64 / nb as f64;
299 let spread = (a.bll as f64 / nb as f64 - ml * ml).max(0.0).sqrt();
300 if kind == Kind::Opaque && core[i] && nb >= 4 && spread <= tau as f64 / 2.0 {
302 stable.push(j as u32);
303 }
304 }
305 }
306 let edges = stable
307 .iter()
308 .filter(|&&j| {
309 let j = j as usize;
310 let (xx, yy) = (j % bw, j / bw);
311 let l = |a: usize| {
312 let t = tpl[a];
313 super::luma(t[0], t[1], t[2]) as i32
314 };
315 let c = l(j);
316 (xx > 0 && (c - l(j - 1)).abs() > 16) || (yy > 0 && (c - l(j - bw)).abs() > 16)
317 })
318 .count();
319 let step = (stable.len() / 2000).max(1);
320 let coarse = stable.iter().step_by(step).copied().collect();
321 let flat = (stable.len() >= 4)
323 .then(|| {
324 let mut ls: Vec<(u32, [u8; 3])> = stable
325 .iter()
326 .map(|&j| {
327 let t = tpl[j as usize];
328 (super::luma(t[0], t[1], t[2]), t)
329 })
330 .collect();
331 ls.sort_by_key(|p| p.0);
332 let med = ls[ls.len() / 2].1;
333 let near = ls
334 .iter()
335 .filter(|(_, t)| (0..3).all(|k| (t[k] as i32 - med[k] as i32).abs() <= tau))
336 .count();
337 (near * 10 >= ls.len() * 6).then_some(med)
338 })
339 .flatten();
340 objects.push(Object {
341 kind,
342 bx: x0,
343 by: y0,
344 bw,
345 bh,
346 mask: m,
347 tpl,
348 textured: edges >= 10,
349 stable,
350 coarse,
351 last: (0, 0),
352 flat,
353 });
354 }
355 }
356 LayerModel {
357 x: stats.x,
358 y: stats.y,
359 cw,
360 ch,
361 objects,
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::kernels::reveal::locate::tiers;
369 use crate::kernels::reveal::stats::CellRef;
370 use crate::kernels::reveal::{Footprint, Frame, TIER_CLEAN};
371
372 struct Page(u8);
373 impl CellRef for Page {
374 fn cell(&self, _: i64, _: i64) -> Option<[u8; 3]> {
375 Some([self.0; 3])
376 }
377 fn cells(&self) -> (i64, i64, i64, i64) {
378 (0, 0, 48, 32)
379 }
380 }
381
382 fn button(page: u8, ink: u8) -> Vec<u8> {
383 let mut rgba = vec![255; 96 * 64 * 4];
384 for y in 0..64 {
385 for x in 0..96 {
386 let v = if (16..80).contains(&x) && (16..48).contains(&y) {
387 if (32..64).contains(&x) && (24..40).contains(&y) {
388 ink
389 } else {
390 240
391 }
392 } else {
393 page
394 };
395 let i = (y * 96 + x) * 4;
396 rgba[i..i + 3].fill(v);
397 }
398 }
399 rgba
400 }
401
402 #[test]
405 fn a_changed_label_on_an_opaque_button_is_not_clean_page_content() {
406 let mut stats = LayerStats::new(0, 0, 96, 64);
407 let mask = vec![1; 96 * 64];
408 let footprint = Footprint {
409 x: 0,
410 y: 0,
411 width: 96,
412 height: 64,
413 mask: &mask,
414 };
415 for background in (0..=240).step_by(16) {
416 let rgba = button(background, 0);
417 stats.accumulate_cells(
418 &Frame {
419 rgba: &rgba,
420 width: 96,
421 height: 64,
422 },
423 &footprint,
424 &Page(background),
425 (0, 0),
426 24,
427 );
428 }
429 let mut model = build(&stats, 24);
430 let rgba = button(128, 80);
431 let tier = tiers(
432 &Frame {
433 rgba: &rgba,
434 width: 96,
435 height: 64,
436 },
437 &mut model,
438 24,
439 );
440 for y in 10..22 {
441 for x in 10..38 {
442 assert!(
443 tier[y * 48 + x] < TIER_CLEAN,
444 "button cell ({x}, {y}) treated as page"
445 );
446 }
447 }
448 assert_eq!(tier[0], TIER_CLEAN);
449 }
450 #[test]
451 fn decorrelation_without_contrast_loss_is_not_attenuation() {
452 struct Pattern;
453 const VALUES: [u8; 6] = [90, 99, 99, 90, 81, 81];
454 impl CellRef for Pattern {
455 fn cell(&self, x: i64, _y: i64) -> Option<[u8; 3]> {
456 Some([VALUES[x.rem_euclid(6) as usize]; 3])
457 }
458 fn cells(&self) -> (i64, i64, i64, i64) {
459 (-1000, -1000, 10000, 10000)
460 }
461 }
462 let (width, height) = (96, 64);
463 let mask = vec![1; width * height];
464 let fp = Footprint {
465 x: 0,
466 y: 0,
467 width,
468 height,
469 mask: &mask,
470 };
471 let model_for = |phase: usize, attenuate: bool, opaque: bool| {
472 let mut stats = LayerStats::new(0, 0, width, height);
473 for step in 0..48 {
474 let mut rgba = vec![255; width * height * 4];
475 for y in 0..height {
476 for x in 0..width {
477 let value = VALUES[(x / 2 + step + phase) % 6];
478 let value = if opaque {
479 90
480 } else if attenuate {
481 ((value as u16 + 90) / 2) as u8
482 } else {
483 value
484 };
485 rgba[(y * width + x) * 4..(y * width + x) * 4 + 3].fill(value);
486 }
487 }
488 let frame = Frame {
489 rgba: &rgba,
490 width,
491 height,
492 };
493 stats.accumulate_cells(&frame, &fp, &Pattern, ((step * 2) as i64, 0), 24);
494 }
495 build(&stats, 24)
496 };
497 for phase in [1, 2] {
500 assert!(
501 model_for(phase, false, false).objects.is_empty(),
502 "a shifted same-contrast pattern cannot establish an overlay"
503 );
504 }
505 assert!(
506 model_for(0, true, false)
507 .objects
508 .iter()
509 .any(|o| o.kind == Kind::Veil),
510 "true attenuation remains detectable"
511 );
512 assert!(
513 model_for(0, false, true)
514 .objects
515 .iter()
516 .any(|o| o.kind == Kind::Opaque),
517 "a truly fixed replacement remains detectable"
518 );
519 }
520 #[test]
521 fn a_moving_replacement_can_keep_or_raise_the_observed_variance() {
522 struct BurnedReference;
525 impl CellRef for BurnedReference {
526 fn cell(&self, x: i64, _y: i64) -> Option<[u8; 3]> {
527 Some([if x.rem_euclid(20) == 0 { 170 } else { 90 }; 3])
528 }
529 fn cells(&self) -> (i64, i64, i64, i64) {
530 (-1000, -1000, 10000, 10000)
531 }
532 }
533 let (width, height) = (96, 64);
534 let mask = vec![1; width * height];
535 let fp = Footprint {
536 x: 0,
537 y: 0,
538 width,
539 height,
540 mask: &mask,
541 };
542 let mut stats = LayerStats::new(0, 0, width, height);
543 for step in 0..60 {
544 let mut rgba = vec![255; width * height * 4];
545 for y in 0..height {
546 for x in 0..width {
547 let phase = (x / 2 + step) % 20;
548 let v = if phase == 5 || phase == 6 { 170 } else { 90 };
549 rgba[(y * width + x) * 4..(y * width + x) * 4 + 3].fill(v);
550 }
551 }
552 stats.accumulate_cells(
553 &Frame {
554 rgba: &rgba,
555 width,
556 height,
557 },
558 &fp,
559 &BurnedReference,
560 ((step * 2) as i64, 0),
561 24,
562 );
563 }
564 let model = build(&stats, 24);
565 assert!(
566 model.objects.iter().any(|o| o.kind == Kind::Opaque),
567 "replacement appearance is not required to lose variance"
568 );
569 }
570 #[test]
571 fn an_intermittent_veil_can_raise_variance_without_being_an_opaque_replacement() {
572 const VALUES: [u8; 6] = [30, 40, 60, 70, 60, 40];
575 struct Pattern;
576 impl CellRef for Pattern {
577 fn cell(&self, x: i64, _y: i64) -> Option<[u8; 3]> {
578 Some([VALUES[x.rem_euclid(6) as usize]; 3])
579 }
580 fn cells(&self) -> (i64, i64, i64, i64) {
581 (-1000, -1000, 10000, 10000)
582 }
583 }
584 let (width, height) = (48, 32);
585 let mask = vec![1; width * height];
586 let footprint = Footprint {
587 x: 0,
588 y: 0,
589 width,
590 height,
591 mask: &mask,
592 };
593 for control in [false, true] {
594 let mut stats = LayerStats::new(0, 0, width, height);
595 for step in 0..120 {
596 let mut rgba = vec![255; width * height * 4];
597 for y in 0..height {
598 for x in 0..width {
599 let background = VALUES[(x / 2 + step + 1) % VALUES.len()];
600 let value = if control && step % 5 == 0 {
601 ((background as u16 + 230) / 2) as u8
602 } else {
603 background
604 };
605 rgba[(y * width + x) * 4..(y * width + x) * 4 + 3].fill(value);
606 }
607 }
608 stats.accumulate_cells(
609 &Frame {
610 rgba: &rgba,
611 width,
612 height,
613 },
614 &footprint,
615 &Pattern,
616 ((step * 2) as i64, 0),
617 24,
618 );
619 }
620 let model = build(&stats, 24);
621 if control {
622 assert!(
623 model.objects.iter().any(|o| o.kind == Kind::Veil),
624 "a repeatedly observed translucent control must remain suspect"
625 );
626 assert!(
627 !model.objects.iter().any(|o| o.kind == Kind::Opaque),
628 "intermittent transmission is not fixed opaque replacement"
629 );
630 } else {
631 assert!(
632 model.objects.is_empty(),
633 "phase noise alone does not establish an overlay"
634 );
635 }
636 }
637 }
638
639 #[test]
640 fn a_flat_page_is_not_an_overlay_of_subnoise_reference_variation() {
641 struct NoisyWhite;
645 impl CellRef for NoisyWhite {
646 fn cell(&self, x: i64, _y: i64) -> Option<[u8; 3]> {
647 const VALUES: [u8; 6] = [250, 253, 255, 253, 250, 247];
648 Some([VALUES[x.rem_euclid(6) as usize]; 3])
649 }
650 fn cells(&self) -> (i64, i64, i64, i64) {
651 (-1000, -1000, 10000, 10000)
652 }
653 }
654 let (width, height) = (48, 32);
655 let mask = vec![1; width * height];
656 let footprint = Footprint {
657 x: 0,
658 y: 0,
659 width,
660 height,
661 mask: &mask,
662 };
663 let mut rgba = vec![252; width * height * 4];
664 for pixel in rgba.chunks_exact_mut(4) {
665 pixel[3] = 255;
666 }
667 let frame = Frame {
668 rgba: &rgba,
669 width,
670 height,
671 };
672 let mut stats = LayerStats::new(0, 0, width, height);
673 for step in 0..120 {
674 stats.accumulate_cells(&frame, &footprint, &NoisyWhite, (step * 2, 0), 24);
675 }
676 assert!(
677 build(&stats, 24).objects.is_empty(),
678 "noise in the reference cannot make clean page pixels opaque"
679 );
680 }
681}