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            pixel_fixed: &l.pixel_fixed,
125            pixel_moving: &l.pixel_moving,
126            native_row_change: optional(&l.native_row_change),
127            native_col_change: optional(&l.native_col_change),
128            row_fixed: Some(&l.row_fixed),
129            row_moving: Some(&l.row_moving),
130            reference,
131        };
132        regions::finish(&input, native_width, native_height, factor)
133    }
134}
135
136/// Whether some native pixel of a `native_width × native_height` frame lies in none of `rects`. An empty list
137/// covers nothing.
138pub fn manual_uncovered(rects: &[Rect], native_width: f64, native_height: f64) -> bool {
139    if rects.is_empty() {
140        return true;
141    }
142    let (nw, nh) = (native_width as i64, native_height as i64);
143    (0..nh).any(|y| (0..nw).any(|x| !rects.iter().any(|r| r.contains(x as f64, y as f64))))
144}
145
146fn manual_regions(
147    manual: &[ManualRegion],
148    mask_width: usize,
149    mask_height: usize,
150    native_width: f64,
151    native_height: f64,
152    factor: u32,
153) -> Vec<RegionOut> {
154    let ignored: Vec<Rect> = manual
155        .iter()
156        .filter(|r| r.kind == Kind::Ignore)
157        .map(|r| r.rect)
158        .collect();
159    let region =
160        |id: usize, name: String, kind: Kind, rect: Rect, exclusions: Vec<Rect>| RegionOut {
161            id: format!("layer-{id}"),
162            name,
163            kind,
164            rect,
165            cells: None,
166            mask: None,
167            mask_width,
168            mask_height,
169            manual: true,
170            unassigned: false,
171            exclusions,
172            crop: None,
173            solid: false,
174            factor,
175            band_side: None,
176        };
177    let mut out: Vec<RegionOut> = manual
178        .iter()
179        .enumerate()
180        .map(|(i, r)| {
181            let exclusions = manual[i + 1..]
182                .iter()
183                .map(|later| later.rect)
184                .chain(ignored.iter().copied())
185                .collect();
186            region(i, r.name.clone(), r.kind, r.rect, exclusions)
187        })
188        .collect();
189    let rects: Vec<Rect> = manual.iter().map(|r| r.rect).collect();
190    if manual_uncovered(&rects, native_width, native_height) {
191        let full = Rect {
192            x: 0.0,
193            y: 0.0,
194            width: native_width,
195            height: native_height,
196        };
197        let mut unassigned = region(
198            out.len(),
199            UNASSIGNED_NAME.to_string(),
200            Kind::Fixed,
201            full,
202            rects,
203        );
204        unassigned.unassigned = true;
205        out.push(unassigned);
206    }
207    out
208}
209
210/// The native-resolution label plane for `regions` (0 = no region, otherwise index + 1; the first region that
211/// contains a pixel owns it) and the per-code pixel counts, or `None` past 254 regions. Regions are described the
212/// way the page's region rows carry them: an automatic region always has a crop (its own rect when it has none),
213/// and a solid region's mask is not consulted.
214pub fn label_atlas(
215    regions: &[RegionOut],
216    width: usize,
217    height: usize,
218) -> Option<(Vec<u8>, Vec<u32>)> {
219    let defs: Vec<AtlasRegion> = regions
220        .iter()
221        .map(|r| AtlasRegion {
222            rect: r.rect,
223            exclusions: r.exclusions.clone(),
224            crop: if r.manual {
225                r.crop
226            } else {
227                Some(r.crop.unwrap_or(r.rect))
228            },
229            solid: r.solid,
230            mask: r
231                .mask
232                .as_deref()
233                .filter(|_| !r.solid)
234                .map(|data| AtlasMask {
235                    width: r.mask_width,
236                    height: r.mask_height,
237                    factor: r.factor,
238                    data,
239                }),
240        })
241        .collect();
242    let mut labels = vec![0u8; width * height];
243    let counts = regions::label_atlas(&defs, width, height, &mut labels)?;
244    Some((labels, counts))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::kernels::layers::FieldMotion;
251
252    fn rect(x: f64, y: f64, width: f64, height: f64) -> Rect {
253        Rect {
254            x,
255            y,
256            width,
257            height,
258        }
259    }
260
261    fn manual(name: &str, kind: Kind, r: Rect) -> ManualRegion {
262        ManualRegion {
263            name: name.to_string(),
264            kind,
265            rect: r,
266        }
267    }
268
269    // Failure cases first: a later region not excluded from an earlier one (overlaps owned twice); an ignored
270    // region not excluded from every sibling; an unassigned region missing when pixels are uncovered, or added when
271    // they are all covered; ids not following list order.
272    #[test]
273    fn manual_regions_exclude_later_and_ignored_regions() {
274        let a = rect(0.0, 0.0, 25.0, 30.0);
275        let b = rect(15.0, 0.0, 25.0, 30.0);
276        let c = rect(0.0, 20.0, 40.0, 10.0);
277        let learner = RegionLearner::new(10, 8);
278        let out = learner.finish(
279            40.0,
280            30.0,
281            &[
282                manual("A", Kind::Moving, a),
283                manual("B", Kind::Ignore, b),
284                manual("C", Kind::Fixed, c),
285            ],
286            4,
287        );
288        assert_eq!(out.len(), 3, "a, b and c cover every pixel");
289        assert_eq!(out[0].id, "layer-0");
290        assert_eq!(out[0].exclusions, vec![b, c, b]);
291        assert_eq!(out[1].exclusions, vec![c, b]);
292        assert_eq!(out[2].exclusions, vec![b]);
293        assert!(out.iter().all(|r| r.manual && !r.unassigned));
294        assert_eq!(
295            (out[2].mask_width, out[2].mask_height, out[2].factor),
296            (10, 8, 4)
297        );
298    }
299
300    #[test]
301    fn a_gap_adds_the_unassigned_region_over_the_whole_frame() {
302        let a = rect(0.0, 0.0, 10.0, 10.0);
303        let out = RegionLearner::new(10, 10).finish(40.0, 30.0, &[manual("A", Kind::Moving, a)], 1);
304        assert_eq!(out.len(), 2);
305        let u = &out[1];
306        assert!(u.unassigned && u.manual);
307        assert_eq!(u.id, "layer-1");
308        assert_eq!(u.name, UNASSIGNED_NAME);
309        assert_eq!(u.kind, Kind::Fixed);
310        assert_eq!(u.rect, rect(0.0, 0.0, 40.0, 30.0));
311        assert_eq!(u.exclusions, vec![a]);
312    }
313
314    #[test]
315    fn coverage_counts_every_native_pixel() {
316        assert!(manual_uncovered(&[], 4.0, 4.0));
317        assert!(!manual_uncovered(&[rect(0.0, 0.0, 4.0, 4.0)], 4.0, 4.0));
318        // Column 3 is outside [0, 3): uncovered.
319        assert!(manual_uncovered(&[rect(0.0, 0.0, 3.0, 4.0)], 4.0, 4.0));
320        assert!(!manual_uncovered(
321            &[rect(0.0, 0.0, 2.0, 4.0), rect(2.0, 0.0, 2.0, 4.0)],
322            4.0,
323            4.0
324        ));
325    }
326
327    fn field<'a>(motions: &'a [FieldMotion], cells: &'a [u8], confidence: &'a [u8]) -> Field<'a> {
328        Field {
329            motions,
330            labels: cells,
331            confidence,
332            dynamic: cells,
333            cols: 1,
334            rows: 1,
335            difference: 5.0,
336            unknown: false,
337        }
338    }
339
340    // The reference must follow the last ACCUMULATED field, not the last field offered, and only when the host
341    // gave native frames.
342    #[test]
343    fn the_reference_follows_the_last_accumulated_native_frame() {
344        let moving = [FieldMotion {
345            x: 0.0,
346            y: 3.0,
347            support: 20,
348            confidence: 0.9,
349        }];
350        let still = [FieldMotion {
351            x: 0.0,
352            y: 0.0,
353            support: 20,
354            confidence: 0.9,
355        }];
356        let (cells, confidence) = ([0u8], [255u8]);
357        let gray = vec![0u8; 24 * 24];
358        let (a, b, c) = (vec![1u8; 8 * 4], vec![2u8; 8 * 4], vec![3u8; 8 * 4]);
359        let mut learner = RegionLearner::new(24, 24);
360        assert!(learner.add(&field(&moving, &cells, &confidence), &gray, &gray, None));
361        assert!(
362            learner.reference.is_none(),
363            "no native frames, no reference"
364        );
365        assert!(learner.add(
366            &field(&moving, &cells, &confidence),
367            &gray,
368            &gray,
369            Some((&a, &b, 4, 2))
370        ));
371        assert_eq!(learner.reference.as_ref().unwrap().data, b);
372        assert!(!learner.add(
373            &field(&still, &cells, &confidence),
374            &gray,
375            &gray,
376            Some((&b, &c, 4, 2))
377        ));
378        assert_eq!(
379            learner.reference.as_ref().unwrap().data,
380            b,
381            "an ignored field keeps the reference"
382        );
383    }
384
385    #[test]
386    fn the_atlas_gives_each_pixel_to_the_first_region_that_holds_it() {
387        let out = RegionLearner::new(4, 4).finish(
388            8.0,
389            4.0,
390            &[
391                manual("A", Kind::Moving, rect(0.0, 0.0, 6.0, 4.0)),
392                manual("B", Kind::Fixed, rect(4.0, 0.0, 4.0, 4.0)),
393            ],
394            2,
395        );
396        let (labels, counts) = label_atlas(&out, 8, 4).unwrap();
397        assert_eq!(
398            &labels[..8],
399            &[1, 1, 1, 1, 2, 2, 2, 2],
400            "A excludes the later B"
401        );
402        assert_eq!(counts, vec![0, 16, 16]);
403    }
404}