Skip to main content

long_screen/kernels/regions/
mod.rs

1//! Region construction (`src/core/layers.ts::LayerLearner.finish` and `RegionAtlas`'s pixel-labelling
2//! constructor), ported stage by stage from the frozen TS oracle in tests/support/reference/layers.ts. Per-frame
3//! accumulation (rust/long-screen/src/kernels/layers.rs) already runs here; this module turns the accumulators the adapter reads
4//! back into `Region`s, once per run, and then labels every native pixel by first-containing-region-wins.
5//!
6//! f64 throughout (JS numbers are f64); `js_round`/`js_floor`/`js_ceil` (geometry.rs) reproduce `Math.round` /
7//! `Math.floor` / `Math.ceil` exactly, including negative and half-integer inputs. Group/label construction walks
8//! cells in the same order a JS `Map`/`Array` insertion order would (see `cells::group_and_label`), because
9//! later stages in the TS compare region *identity*; this port replaces that with a stable index into `regions`.
10//!
11//! One file per construction stage (mirroring the frozen oracle's stage comments): `bands` (stationary edges +
12//! texture test), `cells` (union-find + grouping), `construct` (per-region mask painting, pane divider, band
13//! expansion, cell granularity), `crops` (native-precision edges and crop/solid), `merge` (sliver/sticky-header
14//! cleanup, freezing stationary regions, and the final filter), `atlas` (`RegionAtlas` pixel labelling).
15//! `finish()`, the top-level orchestrator, and the shared types/helpers stay here.
16
17use crate::kernels::geometry::Rect;
18
19mod atlas;
20mod band_fn;
21mod bands;
22mod cells;
23mod construct;
24mod crops;
25mod merge;
26#[cfg(test)]
27mod tests;
28
29pub use atlas::{label_atlas, AtlasMask, AtlasRegion};
30use band_fn::band;
31use bands::{detect_bands, strongest_cut, Bands};
32use cells::{group_and_label, union_find_cells};
33use construct::{apply_cell_granularity, apply_pane_divider, build_regions, expand_bands};
34use crops::{
35    assign_native_crops, compute_native_edges, recompute_bounding_boxes, refine_vertical_cut_crops,
36};
37use merge::{cleanup_sticky_headers, finalize, freeze_stationary_regions, merge_small_regions};
38
39/// Borrowed RGBA8 image (`RGBA` in types.ts), used only for texture/appearance evidence.
40pub struct Rgba<'a> {
41    pub width: usize,
42    pub height: usize,
43    pub data: &'a [u8],
44}
45
46/// The accumulator arrays `LearnerHandle.read()` returns, plus the sizes and retained native frame `finish()`
47/// needs. Mirrors `tests/support/reference/layers.ts::FinishAccumulators`.
48pub struct FinishInput<'a> {
49    pub width: usize,
50    pub height: usize,
51    pub cell: usize,
52    pub informative_frames: f64,
53    pub native_frames: f64,
54    pub row_change: &'a [f64],
55    pub col_change: &'a [f64],
56    /// `Learner::col_moving`: per column, the informative frames in which it moved.
57    pub col_moving: &'a [f64],
58    pub col_mean: &'a [f64],
59    pub col_gain: &'a [f64],
60    pub horizontal_gain: &'a [f64],
61    pub split: &'a [f64],
62    pub evidence: &'a [f64],
63    pub activity: &'a [f64],
64    pub observations: &'a [f64],
65    /// `Learner::pixel_fixed`/`pixel_moving`, per analysis pixel.
66    pub pixel_fixed: &'a [f64],
67    pub pixel_moving: &'a [f64],
68    pub native_row_change: Option<&'a [f64]>,
69    pub native_col_change: Option<&'a [f64]>,
70    /// `Learner::row_fixed` / `Learner::row_moving`: per analysis row, the sampled pixels at which the stationary
71    /// hypothesis beat the main motion by more than 6 levels, and the reverse. `None` keeps the change-only bands.
72    pub row_fixed: Option<&'a [f64]>,
73    pub row_moving: Option<&'a [f64]>,
74    pub reference: Option<Rgba<'a>>,
75}
76
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78pub enum Kind {
79    Moving,
80    Fixed,
81    Ignore,
82}
83
84/// Constructed region, the Rust analogue of `Region` (types.ts). `mask` is always analysis-resolution
85/// (`mask_width × mask_height`); `RegionAtlas.labels` (`src/core/layers.ts`) is built by
86/// `regions::atlas::label_atlas` over `region::Region<&[u8]>` (region.rs's generic `Region`, instantiated with a
87/// borrowed mask — see `regions/atlas.rs`'s `AtlasRegion`), constructed directly from the wire request in
88/// `abi::regions::ls_regions_label_atlas`. It never goes through this type.
89pub struct RegionOut {
90    pub id: String,
91    pub name: String,
92    pub kind: Kind,
93    pub rect: Rect,
94    pub cells: Option<Vec<i64>>,
95    pub mask: Option<Vec<u8>>,
96    pub mask_width: usize,
97    pub mask_height: usize,
98    pub manual: bool,
99    pub unassigned: bool,
100    pub exclusions: Vec<Rect>,
101    pub crop: Option<Rect>,
102    pub solid: bool,
103    pub factor: u32,
104    /// Construction-only annotation (`Region & { bandSide?: number }` in the TS `finish()`): which stationary
105    /// edge band (1=top, 2=bottom, 3=left, 4=right) `expand_bands` last assigned this region to. A region whose
106    /// cells touch more than one band keeps only the LAST band `expand_bands` visits (1, 2, 3, then 4) —
107    /// reproduced here, not "fixed", because `native_crops` reads it the same way.
108    pub band_side: Option<u8>,
109}
110
111#[inline]
112fn intersect(a: Rect, b: Rect) -> Rect {
113    let x = a.x.max(b.x);
114    let y = a.y.max(b.y);
115    Rect {
116        x,
117        y,
118        width: ((a.x + a.width).min(b.x + b.width) - x).max(0.0),
119        height: ((a.y + a.height).min(b.y + b.height) - y).max(0.0),
120    }
121}
122
123/// A read past the end of a JS `Float64Array` returns `undefined`, which poisons downstream arithmetic to `NaN`
124/// and makes every comparison below false; this reproduces that without ever indexing out of range.
125#[inline]
126fn stat_at(stats: &[f64], index: i64) -> f64 {
127    if index < 0 || index as usize >= stats.len() {
128        f64::NAN
129    } else {
130        stats[index as usize]
131    }
132}
133
134/// Orchestrates every stage above, mirroring `tests/support/reference/layers.ts::referenceFinish` /
135/// `src/core/layers.ts::LayerLearner.finish()` exactly (same stage order, same accumulator reads). The manual-
136/// region branch of `finish()` (`LayerLearner.finish`'s `manual` parameter) is NOT ported here: it is cheap TS
137/// object construction over data the caller already holds (spreading the input `Region`s, no per-pixel work
138/// except the one native-pixel coverage scan, which IS in Rust — `ls_regions_manual_uncovered`,
139/// rust/wasm/src/regions.rs). Porting it would mean Rust re-deriving id/name/exclusions only for TS to reconstruct the
140/// same values back; this function therefore only ever builds the algorithmic-path regions.
141pub fn finish(
142    input: &FinishInput<'_>,
143    native_width: f64,
144    native_height: f64,
145    factor: u32,
146) -> Vec<RegionOut> {
147    let width = input.width as i64;
148    let height = input.height as i64;
149    let cell = input.cell as i64;
150    let cols = (width + cell - 1) / cell.max(1);
151    let rows = (height + cell - 1) / cell.max(1);
152    let reference = input.reference.as_ref();
153    let bands = detect_bands(
154        width,
155        height,
156        input.informative_frames,
157        input.row_change,
158        input.row_fixed.zip(input.row_moving),
159        (input.pixel_fixed, input.pixel_moving),
160        input.col_moving,
161        input.col_mean,
162        reference,
163        native_width,
164        native_height,
165        factor as f64,
166    );
167    let vertical_cut = strongest_cut(input.col_gain, input.informative_frames);
168    let horizontal_cut = strongest_cut(input.horizontal_gain, input.informative_frames);
169    let mut ds = union_find_cells(
170        cols,
171        rows,
172        cell,
173        width,
174        height,
175        bands.top,
176        bands.bottom,
177        bands.left,
178        bands.right,
179        vertical_cut,
180        horizontal_cut,
181        input.evidence,
182        input.split,
183        input.informative_frames,
184    );
185    let n = cols * rows;
186    let (large, labels) = group_and_label(&mut ds, n, cols);
187    let mut regions = build_regions(
188        &large,
189        &labels,
190        cols,
191        cell,
192        width,
193        height,
194        bands.top,
195        bands.bottom,
196        bands.left,
197        bands.right,
198        input.activity,
199        input.observations,
200        input.informative_frames,
201        factor,
202    );
203    let (divider_start, divider_end) = apply_pane_divider(
204        &mut regions,
205        &labels,
206        cols,
207        rows,
208        cell,
209        width,
210        height,
211        bands.top,
212        bands.bottom,
213        vertical_cut,
214        input.col_change,
215        input.informative_frames,
216        factor,
217    );
218    expand_bands(
219        &mut regions,
220        width,
221        height,
222        bands.top,
223        bands.bottom,
224        bands.left,
225        bands.right,
226        native_width,
227        native_height,
228        cols,
229        cell,
230        factor,
231    );
232    apply_cell_granularity(
233        &mut regions,
234        width,
235        height,
236        cell,
237        bands.top,
238        bands.bottom,
239        bands.left,
240        bands.right,
241    );
242    let edges = compute_native_edges(
243        &bands,
244        input.native_row_change,
245        input.native_col_change,
246        input.native_frames,
247        height,
248        width,
249        factor as f64,
250        native_width,
251        native_height,
252    );
253    assign_native_crops(
254        &mut regions,
255        &edges,
256        native_width,
257        native_height,
258        cols,
259        cell,
260        width,
261        height,
262        bands.top,
263        bands.bottom,
264        bands.left,
265        bands.right,
266    );
267    refine_vertical_cut_crops(
268        &mut regions,
269        &edges,
270        vertical_cut,
271        cell,
272        cols,
273        divider_start,
274        divider_end,
275        input.native_col_change,
276        input.native_frames,
277        native_width,
278        factor as f64,
279    );
280    recompute_bounding_boxes(
281        &mut regions,
282        width,
283        height,
284        native_width,
285        native_height,
286        factor as f64,
287    );
288    let valid: Vec<usize> = (0..regions.len())
289        .filter(|&i| regions[i].rect.width > 0.0 && regions[i].rect.height > 0.0)
290        .collect();
291    merge_small_regions(&mut regions, &valid, factor as f64);
292    cleanup_sticky_headers(
293        &mut regions,
294        &valid,
295        edges.content,
296        cell,
297        factor as f64,
298        reference,
299    );
300    freeze_stationary_regions(
301        &mut regions,
302        &valid,
303        input.pixel_fixed,
304        input.pixel_moving,
305        reference,
306        factor as f64,
307    );
308    finalize(regions, &valid, edges.content)
309}