Skip to main content

long_screen/engine/
layers.rs

1//! Region learning over a whole recording: the core's per-frame evidence accumulators
2//! (`crate::kernels::layers::Learner`), the last informative native frame kept for appearance evidence, and the
3//! one-shot region construction at the end — the automatic layout (`crate::kernels::regions::finish`) or a
4//! hand-placed one (manual regions, plus the "unassigned" region when they leave pixels uncovered).
5//!
6//! The scan session owns one of these; the web page's `LayerLearner` (src/core/layers.ts) drives one directly
7//! through rust/wasm/src/layers.rs for the layer-learning unit and parity tests.
8
9use crate::kernels::geometry::Rect;
10use crate::kernels::layers::{Field, Learner};
11use crate::kernels::regions::{self, AtlasMask, AtlasRegion, FinishInput, Kind, RegionOut, Rgba};
12
13/// Name of the region that owns whatever native pixels a hand-placed layout leaves uncovered. Region names are a
14/// fixed zh vocabulary, translated only for display (src/i18n/names.ts).
15pub const UNASSIGNED_NAME: &str = "未指定区域 · 屏幕坐标观察";
16
17/// One hand-placed region from the run's settings.
18#[derive(Clone, Debug, PartialEq)]
19pub struct ManualRegion {
20    pub name: String,
21    pub kind: Kind,
22    pub rect: Rect,
23}
24
25/// A native RGBA frame the learner keeps a copy of.
26struct NativeFrame {
27    data: Vec<u8>,
28    width: usize,
29    height: usize,
30}
31
32/// Layer-learning state for one run: accumulators plus the last informative native frame.
33pub struct RegionLearner {
34    learner: Learner,
35    reference: Option<NativeFrame>,
36}
37
38impl RegionLearner {
39    /// A learner over a `width × height` analysis grid.
40    pub fn new(width: usize, height: usize) -> RegionLearner {
41        RegionLearner {
42            learner: Learner::new(width, height),
43            reference: None,
44        }
45    }
46
47    /// The accumulators, for hosts and tests that read them back.
48    pub fn accumulators(&self) -> &Learner {
49        &self.learner
50    }
51
52    /// Accumulates one field (the learner applies its own informative gate). `prev`/`current` are analysis grays;
53    /// `native` is the pair of native RGBA frames with their shared size, when the host has both. When the field
54    /// was accumulated and native frames were given, `current`'s native frame becomes the appearance reference
55    /// `finish` samples. Returns whether the field was accumulated.
56    pub fn add(
57        &mut self,
58        field: &Field<'_>,
59        prev: &[u8],
60        current: &[u8],
61        native: Option<(&[u8], &[u8], usize, usize)>,
62    ) -> bool {
63        let informative = self.learner.add(field, prev, current, native);
64        if let (true, Some((_, frame, width, height))) = (informative, native) {
65            match &mut self.reference {
66                Some(r) if r.width == width && r.height == height => r.data.copy_from_slice(frame),
67                slot => {
68                    *slot = Some(NativeFrame {
69                        data: frame.to_vec(),
70                        width,
71                        height,
72                    })
73                }
74            }
75        }
76        informative
77    }
78
79    /// Builds the run's regions. With `manual` regions, those (in order, each excluding every later one and every
80    /// ignored one) plus an unassigned region when they leave native pixels uncovered; otherwise the automatic
81    /// layout from the accumulators. `factor` is the integer analysis factor.
82    pub fn finish(
83        &self,
84        native_width: f64,
85        native_height: f64,
86        manual: &[ManualRegion],
87        factor: u32,
88    ) -> Vec<RegionOut> {
89        let l = &self.learner;
90        if !manual.is_empty() {
91            return manual_regions(
92                manual,
93                l.width,
94                l.height,
95                native_width,
96                native_height,
97                factor,
98            );
99        }
100        fn optional(values: &[f64]) -> Option<&[f64]> {
101            (!values.is_empty()).then_some(values)
102        }
103        let reference = self.reference.as_ref().map(|r| Rgba {
104            width: r.width,
105            height: r.height,
106            data: &r.data,
107        });
108        let input = FinishInput {
109            width: l.width,
110            height: l.height,
111            cell: l.cell,
112            informative_frames: l.informative_frames as f64,
113            native_frames: l.native_frames as f64,
114            row_change: &l.row_change,
115            col_change: &l.col_change,
116            col_moving: &l.col_moving,
117            col_mean: &l.col_mean,
118            col_gain: &l.col_gain,
119            horizontal_gain: &l.horizontal_gain,
120            split: &l.split,
121            evidence: &l.evidence,
122            activity: &l.activity,
123            observations: &l.observations,
124            native_row_change: optional(&l.native_row_change),
125            native_col_change: optional(&l.native_col_change),
126            reference,
127        };
128        regions::finish(&input, native_width, native_height, factor)
129    }
130}
131
132/// Whether some native pixel of a `native_width × native_height` frame lies in none of `rects`. An empty list
133/// covers nothing.
134pub fn manual_uncovered(rects: &[Rect], native_width: f64, native_height: f64) -> bool {
135    if rects.is_empty() {
136        return true;
137    }
138    let (nw, nh) = (native_width as i64, native_height as i64);
139    (0..nh).any(|y| (0..nw).any(|x| !rects.iter().any(|r| r.contains(x as f64, y as f64))))
140}
141
142fn manual_regions(
143    manual: &[ManualRegion],
144    mask_width: usize,
145    mask_height: usize,
146    native_width: f64,
147    native_height: f64,
148    factor: u32,
149) -> Vec<RegionOut> {
150    let ignored: Vec<Rect> = manual
151        .iter()
152        .filter(|r| r.kind == Kind::Ignore)
153        .map(|r| r.rect)
154        .collect();
155    let region =
156        |id: usize, name: String, kind: Kind, rect: Rect, exclusions: Vec<Rect>| RegionOut {
157            id: format!("layer-{id}"),
158            name,
159            kind,
160            rect,
161            cells: None,
162            mask: None,
163            mask_width,
164            mask_height,
165            manual: true,
166            unassigned: false,
167            exclusions,
168            crop: None,
169            solid: false,
170            factor,
171            band_side: None,
172        };
173    let mut out: Vec<RegionOut> = manual
174        .iter()
175        .enumerate()
176        .map(|(i, r)| {
177            let exclusions = manual[i + 1..]
178                .iter()
179                .map(|later| later.rect)
180                .chain(ignored.iter().copied())
181                .collect();
182            region(i, r.name.clone(), r.kind, r.rect, exclusions)
183        })
184        .collect();
185    let rects: Vec<Rect> = manual.iter().map(|r| r.rect).collect();
186    if manual_uncovered(&rects, native_width, native_height) {
187        let full = Rect {
188            x: 0.0,
189            y: 0.0,
190            width: native_width,
191            height: native_height,
192        };
193        let mut unassigned = region(
194            out.len(),
195            UNASSIGNED_NAME.to_string(),
196            Kind::Fixed,
197            full,
198            rects,
199        );
200        unassigned.unassigned = true;
201        out.push(unassigned);
202    }
203    out
204}
205
206/// The native-resolution label plane for `regions` (0 = no region, otherwise index + 1; the first region that
207/// contains a pixel owns it) and the per-code pixel counts, or `None` past 254 regions. Regions are described the
208/// way the page's region rows carry them: an automatic region always has a crop (its own rect when it has none),
209/// and a solid region's mask is not consulted.
210pub fn label_atlas(
211    regions: &[RegionOut],
212    width: usize,
213    height: usize,
214) -> Option<(Vec<u8>, Vec<u32>)> {
215    let defs: Vec<AtlasRegion> = regions
216        .iter()
217        .map(|r| AtlasRegion {
218            rect: r.rect,
219            exclusions: r.exclusions.clone(),
220            crop: if r.manual {
221                r.crop
222            } else {
223                Some(r.crop.unwrap_or(r.rect))
224            },
225            solid: r.solid,
226            mask: r
227                .mask
228                .as_deref()
229                .filter(|_| !r.solid)
230                .map(|data| AtlasMask {
231                    width: r.mask_width,
232                    height: r.mask_height,
233                    factor: r.factor,
234                    data,
235                }),
236        })
237        .collect();
238    let mut labels = vec![0u8; width * height];
239    let counts = regions::label_atlas(&defs, width, height, &mut labels)?;
240    Some((labels, counts))
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::kernels::layers::FieldMotion;
247
248    fn rect(x: f64, y: f64, width: f64, height: f64) -> Rect {
249        Rect {
250            x,
251            y,
252            width,
253            height,
254        }
255    }
256
257    fn manual(name: &str, kind: Kind, r: Rect) -> ManualRegion {
258        ManualRegion {
259            name: name.to_string(),
260            kind,
261            rect: r,
262        }
263    }
264
265    // Failure cases first: a later region not excluded from an earlier one (overlaps owned twice); an ignored
266    // region not excluded from every sibling; an unassigned region missing when pixels are uncovered, or added when
267    // they are all covered; ids not following list order.
268    #[test]
269    fn manual_regions_exclude_later_and_ignored_regions() {
270        let a = rect(0.0, 0.0, 25.0, 30.0);
271        let b = rect(15.0, 0.0, 25.0, 30.0);
272        let c = rect(0.0, 20.0, 40.0, 10.0);
273        let learner = RegionLearner::new(10, 8);
274        let out = learner.finish(
275            40.0,
276            30.0,
277            &[
278                manual("A", Kind::Moving, a),
279                manual("B", Kind::Ignore, b),
280                manual("C", Kind::Fixed, c),
281            ],
282            4,
283        );
284        assert_eq!(out.len(), 3, "a, b and c cover every pixel");
285        assert_eq!(out[0].id, "layer-0");
286        assert_eq!(out[0].exclusions, vec![b, c, b]);
287        assert_eq!(out[1].exclusions, vec![c, b]);
288        assert_eq!(out[2].exclusions, vec![b]);
289        assert!(out.iter().all(|r| r.manual && !r.unassigned));
290        assert_eq!(
291            (out[2].mask_width, out[2].mask_height, out[2].factor),
292            (10, 8, 4)
293        );
294    }
295
296    #[test]
297    fn a_gap_adds_the_unassigned_region_over_the_whole_frame() {
298        let a = rect(0.0, 0.0, 10.0, 10.0);
299        let out = RegionLearner::new(10, 10).finish(40.0, 30.0, &[manual("A", Kind::Moving, a)], 1);
300        assert_eq!(out.len(), 2);
301        let u = &out[1];
302        assert!(u.unassigned && u.manual);
303        assert_eq!(u.id, "layer-1");
304        assert_eq!(u.name, UNASSIGNED_NAME);
305        assert_eq!(u.kind, Kind::Fixed);
306        assert_eq!(u.rect, rect(0.0, 0.0, 40.0, 30.0));
307        assert_eq!(u.exclusions, vec![a]);
308    }
309
310    #[test]
311    fn coverage_counts_every_native_pixel() {
312        assert!(manual_uncovered(&[], 4.0, 4.0));
313        assert!(!manual_uncovered(&[rect(0.0, 0.0, 4.0, 4.0)], 4.0, 4.0));
314        // Column 3 is outside [0, 3): uncovered.
315        assert!(manual_uncovered(&[rect(0.0, 0.0, 3.0, 4.0)], 4.0, 4.0));
316        assert!(!manual_uncovered(
317            &[rect(0.0, 0.0, 2.0, 4.0), rect(2.0, 0.0, 2.0, 4.0)],
318            4.0,
319            4.0
320        ));
321    }
322
323    fn field<'a>(motions: &'a [FieldMotion], cells: &'a [u8], confidence: &'a [u8]) -> Field<'a> {
324        Field {
325            motions,
326            labels: cells,
327            confidence,
328            dynamic: cells,
329            cols: 1,
330            rows: 1,
331            difference: 5.0,
332            unknown: false,
333        }
334    }
335
336    // The reference must follow the last ACCUMULATED field, not the last field offered, and only when the host
337    // gave native frames.
338    #[test]
339    fn the_reference_follows_the_last_accumulated_native_frame() {
340        let moving = [FieldMotion {
341            x: 0.0,
342            y: 3.0,
343            support: 20,
344            confidence: 0.9,
345        }];
346        let still = [FieldMotion {
347            x: 0.0,
348            y: 0.0,
349            support: 20,
350            confidence: 0.9,
351        }];
352        let (cells, confidence) = ([0u8], [255u8]);
353        let gray = vec![0u8; 24 * 24];
354        let (a, b, c) = (vec![1u8; 8 * 4], vec![2u8; 8 * 4], vec![3u8; 8 * 4]);
355        let mut learner = RegionLearner::new(24, 24);
356        assert!(learner.add(&field(&moving, &cells, &confidence), &gray, &gray, None));
357        assert!(
358            learner.reference.is_none(),
359            "no native frames, no reference"
360        );
361        assert!(learner.add(
362            &field(&moving, &cells, &confidence),
363            &gray,
364            &gray,
365            Some((&a, &b, 4, 2))
366        ));
367        assert_eq!(learner.reference.as_ref().unwrap().data, b);
368        assert!(!learner.add(
369            &field(&still, &cells, &confidence),
370            &gray,
371            &gray,
372            Some((&b, &c, 4, 2))
373        ));
374        assert_eq!(
375            learner.reference.as_ref().unwrap().data,
376            b,
377            "an ignored field keeps the reference"
378        );
379    }
380
381    #[test]
382    fn the_atlas_gives_each_pixel_to_the_first_region_that_holds_it() {
383        let out = RegionLearner::new(4, 4).finish(
384            8.0,
385            4.0,
386            &[
387                manual("A", Kind::Moving, rect(0.0, 0.0, 6.0, 4.0)),
388                manual("B", Kind::Fixed, rect(4.0, 0.0, 4.0, 4.0)),
389            ],
390            2,
391        );
392        let (labels, counts) = label_atlas(&out, 8, 4).unwrap();
393        assert_eq!(
394            &labels[..8],
395            &[1, 1, 1, 1, 2, 2, 2, 2],
396            "A excludes the later B"
397        );
398        assert_eq!(counts, vec![0, 16, 16]);
399    }
400}