Skip to main content

long_screen/kernels/
region.rs

1//! Region membership: rect, exclusions, optional native crop, and an analysis-resolution mask looked up at
2//! `floor(x / factor)` (or by ratio for masks built without a factor).
3//!
4//! Generic over the mask storage (`M: AsRef<[u8]>`, defaulting to `Vec<u8>`) so this ONE `contains()` serves both
5//! `voting.rs`/`track.rs` (which retain a region across many calls and must own its mask) and
6//! `regions::atlas` (which only needs its mask to live for one `label_atlas` call and borrows it instead) —
7//! one implementation, not two mirrored copies of this method.
8
9use crate::kernels::features::Feature;
10use crate::kernels::geometry::{js_floor, Rect};
11
12pub struct Mask<M: AsRef<[u8]> = Vec<u8>> {
13    pub width: usize,
14    pub height: usize,
15    /// Integer analysis factor the mask was built at; 0 selects the legacy ratio lookup.
16    pub factor: u32,
17    pub data: M,
18}
19
20pub struct Region<M: AsRef<[u8]> = Vec<u8>> {
21    pub rect: Rect,
22    pub exclusions: Vec<Rect>,
23    pub crop: Option<Rect>,
24    pub solid: bool,
25    pub mask: Option<Mask<M>>,
26}
27
28#[inline]
29fn clamp_index(value: i32, len: usize) -> usize {
30    value.clamp(0, len as i32 - 1) as usize
31}
32
33impl<M: AsRef<[u8]>> Region<M> {
34    pub fn contains(&self, x: f64, y: f64, native_width: f64, native_height: f64) -> bool {
35        if !self.rect.contains(x, y) || self.exclusions.iter().any(|r| r.contains(x, y)) {
36            return false;
37        }
38        if self.crop.is_some_and(|c| !c.contains(x, y)) {
39            return false;
40        }
41        if self.solid {
42            return true;
43        }
44        let Some(mask) = &self.mask else {
45            return true;
46        };
47        let (xx, yy) = if mask.factor > 0 {
48            let f = mask.factor as f64;
49            (
50                clamp_index(js_floor(x / f), mask.width),
51                clamp_index(js_floor(y / f), mask.height),
52            )
53        } else {
54            (
55                clamp_index(js_floor(x * mask.width as f64 / native_width), mask.width),
56                clamp_index(
57                    js_floor(y * mask.height as f64 / native_height),
58                    mask.height,
59                ),
60            )
61        };
62        mask.data.as_ref()[yy * mask.width + xx] != 0
63    }
64}
65
66/// `src/pipeline/solve/track.ts::ownFeaturesOf`'s region-membership feature filter, moved out of TS.
67/// Analysis-coordinate features are scaled to native pixels by `factor` (`p.x * f, p.y * f`) before the same
68/// `contains()` test above.
69pub fn filter_features<M: AsRef<[u8]>>(
70    region: &Region<M>,
71    features: &[Feature],
72    factor: f64,
73    native_width: f64,
74    native_height: f64,
75) -> Vec<Feature> {
76    features
77        .iter()
78        .filter(|p| {
79            region.contains(
80                p.x as f64 * factor,
81                p.y as f64 * factor,
82                native_width,
83                native_height,
84            )
85        })
86        .cloned()
87        .collect()
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn mask_lookup_uses_floor_by_factor_and_clamps_the_partial_cell() {
96        let region = Region {
97            rect: Rect {
98                x: 0.0,
99                y: 0.0,
100                width: 10.0,
101                height: 10.0,
102            },
103            exclusions: vec![],
104            crop: None,
105            solid: false,
106            mask: Some(Mask {
107                width: 3,
108                height: 3,
109                factor: 4,
110                data: vec![1, 0, 0, 0, 1, 0, 0, 0, 1],
111            }),
112        };
113        assert!(region.contains(0.0, 0.0, 10.0, 10.0));
114        assert!(!region.contains(4.0, 0.0, 10.0, 10.0));
115        assert!(region.contains(9.0, 9.0, 10.0, 10.0));
116        assert!(!region.contains(10.0, 9.0, 10.0, 10.0));
117    }
118
119    #[test]
120    fn filter_features_scales_by_factor_before_testing_containment() {
121        let region: Region = Region {
122            rect: Rect {
123                x: 0.0,
124                y: 0.0,
125                width: 4.0,
126                height: 10.0,
127            },
128            exclusions: vec![],
129            crop: None,
130            solid: true,
131            mask: None,
132        };
133        let inside = Feature {
134            x: 1,
135            y: 1,
136            score: 0.0,
137            descriptor: [0; crate::kernels::features::DESCRIPTOR_WORDS],
138        };
139        let outside = Feature {
140            x: 3,
141            y: 1,
142            score: 0.0,
143            descriptor: [0; crate::kernels::features::DESCRIPTOR_WORDS],
144        };
145        let kept = filter_features(&region, &[inside.clone(), outside], 2.0, 10.0, 10.0);
146        assert_eq!(kept, vec![inside]);
147    }
148}