File size: 10,284 Bytes
eae424a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//! Where frames come from.
//!
//! The passthrough camera is not wired up yet, so this exists mainly as the
//! seam it will slot into: the XR loop pulls from a [`FrameSource`] and does
//! not care whether the pixels came from a camera or were made up.
//!
//! Until then [`TestPattern`] provides something with real spatial
//! structure. That matters more than it sounds — a flat or noisy image
//! makes every patch feature statistically identical, so the PCA colouring
//! would look plausible while proving nothing. A scene with distinct
//! regions shows immediately whether features are tracking content.

/// A source of square RGB8 frames at the encoder's input resolution.
pub trait FrameSource {
    /// Side length in pixels; must match `Config::image_size`.
    fn size(&self) -> usize;

    /// The next frame as interleaved RGB8, `[size, size, 3]`, or `None`
    /// when nothing new is available.
    fn next_frame(&mut self) -> Option<&[u8]>;
}

/// A moving synthetic scene: coloured discs drifting over a gradient, with
/// a checkerboard patch for high-frequency contrast.
///
/// Deliberately built from a few large, distinctly-coloured regions, since
/// that is what DINO features separate well and therefore what makes the
/// PCA view legible while bringing the pipeline up.
pub struct TestPattern {
    size: usize,
    buf: Vec<u8>,
    frame: u32,
}

impl TestPattern {
    pub fn new(size: usize) -> Self {
        Self {
            size,
            buf: vec![0; size * size * 3],
            frame: 0,
        }
    }

    fn render(&mut self) {
        let size = self.size;
        let t = self.frame as f32 * 0.02;
        // Three discs on circular paths, each a saturated primary so the
        // top principal components have something unambiguous to separate.
        let discs = [
            (0.30 + 0.18 * t.cos(), 0.30 + 0.18 * t.sin(), 0.16, [230u8, 60, 50]),
            (0.70 + 0.15 * (t * 0.7 + 2.0).cos(), 0.35 + 0.15 * (t * 0.7).sin(), 0.13, [60, 200, 90]),
            (0.50 + 0.20 * (t * 0.5 + 4.0).sin(), 0.72 + 0.10 * (t * 0.9).cos(), 0.15, [70, 110, 235]),
        ];

        for y in 0..size {
            let v = y as f32 / size as f32;
            for x in 0..size {
                let u = x as f32 / size as f32;

                // Background: a slow vertical gradient.
                let mut rgb = [
                    (40.0 + 60.0 * v) as u8,
                    (50.0 + 40.0 * (1.0 - v)) as u8,
                    (70.0 + 50.0 * v) as u8,
                ];

                // A checkerboard corner, for a region whose texture differs
                // from everything else without its colour doing so.
                if u > 0.72 && v > 0.72 {
                    let cell = ((x / 8) + (y / 8)) % 2;
                    let shade = if cell == 0 { 200 } else { 90 };
                    rgb = [shade, shade, shade];
                }

                for &(cx, cy, r, color) in &discs {
                    let dx = u - cx;
                    let dy = v - cy;
                    if dx * dx + dy * dy < r * r {
                        rgb = color;
                    }
                }

                let i = (y * size + x) * 3;
                self.buf[i] = rgb[0];
                self.buf[i + 1] = rgb[1];
                self.buf[i + 2] = rgb[2];
            }
        }
    }
}

impl FrameSource for TestPattern {
    fn size(&self) -> usize {
        self.size
    }

    fn next_frame(&mut self) -> Option<&[u8]> {
        self.render();
        self.frame = self.frame.wrapping_add(1);
        Some(&self.buf)
    }
}

/// Centre-crop an interleaved RGBA image to a square and resample it down
/// to `size`, dropping alpha.
///
/// Box-averaging rather than nearest: a patch embedding sees 16×16 pixels,
/// and point-sampling a 2560-wide screen down to 224 would alias hard
/// enough to change what the features encode. Cheap here — it runs once per
/// captured frame, not per patch.
pub fn square_downscale_rgba(rgba: &[u8], width: usize, height: usize, size: usize) -> Vec<u8> {
    let side = width.min(height);
    let x0 = (width - side) / 2;
    let y0 = (height - side) / 2;

    let mut out = vec![0u8; size * size * 3];
    for oy in 0..size {
        let sy0 = y0 + oy * side / size;
        let sy1 = (y0 + (oy + 1) * side / size).max(sy0 + 1);
        for ox in 0..size {
            let sx0 = x0 + ox * side / size;
            let sx1 = (x0 + (ox + 1) * side / size).max(sx0 + 1);

            let mut acc = [0u32; 3];
            let mut n = 0u32;
            for sy in sy0..sy1.min(height) {
                for sx in sx0..sx1.min(width) {
                    let i = (sy * width + sx) * 4;
                    acc[0] += rgba[i] as u32;
                    acc[1] += rgba[i + 1] as u32;
                    acc[2] += rgba[i + 2] as u32;
                    n += 1;
                }
            }
            let n = n.max(1);
            let o = (oy * size + ox) * 3;
            for c in 0..3 {
                out[o + c] = (acc[c] / n) as u8;
            }
        }
    }
    out
}

/// Live screen capture.
///
/// Exists mostly so the whole pipeline can be developed and demonstrated
/// without a headset: it is the same `FrameSource` contract the Quest
/// passthrough camera will implement, so the capture loop, colour handling,
/// and downscale are all exercised here first.
#[cfg(feature = "capture")]
pub struct ScreenCapture {
    monitor: xcap::Monitor,
    size: usize,
    buf: Vec<u8>,
}

#[cfg(feature = "capture")]
impl ScreenCapture {
    /// Capture the monitor at `index`, or the primary one if out of range.
    pub fn new(size: usize, index: usize) -> Result<Self, Box<dyn std::error::Error>> {
        let monitors = xcap::Monitor::all()?;
        if monitors.is_empty() {
            return Err("no monitors found".into());
        }
        for (i, m) in monitors.iter().enumerate() {
            log::info!(
                "monitor {i}: {}x{}{}",
                m.width().unwrap_or(0),
                m.height().unwrap_or(0),
                if i == index { " (selected)" } else { "" }
            );
        }
        let monitor = monitors
            .into_iter()
            .nth(index)
            .ok_or("monitor index out of range")?;
        Ok(Self {
            monitor,
            size,
            buf: vec![128; size * size * 3],
        })
    }
}

#[cfg(feature = "capture")]
impl FrameSource for ScreenCapture {
    fn size(&self) -> usize {
        self.size
    }

    fn next_frame(&mut self) -> Option<&[u8]> {
        // A dropped frame is not worth failing over — the previous one is
        // still displayable, and capture hiccups when windows change.
        match self.monitor.capture_image() {
            Ok(image) => {
                let (w, h) = (image.width() as usize, image.height() as usize);
                self.buf = square_downscale_rgba(&image.into_raw(), w, h, self.size);
                Some(&self.buf)
            }
            Err(e) => {
                log::warn!("screen capture failed: {e}");
                None
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn downscale_centre_crops_and_averages() {
        // A 40×20 image: left half red, right half blue. The centre crop is
        // the middle 20×20, which straddles the boundary evenly.
        let (w, h) = (40usize, 20usize);
        let mut rgba = vec![0u8; w * h * 4];
        for y in 0..h {
            for x in 0..w {
                let i = (y * w + x) * 4;
                let c = if x < w / 2 { [255, 0, 0] } else { [0, 0, 255] };
                rgba[i..i + 3].copy_from_slice(&c);
                rgba[i + 3] = 255;
            }
        }

        let out = square_downscale_rgba(&rgba, w, h, 4);
        assert_eq!(out.len(), 4 * 4 * 3);
        // Left column should be red, right column blue.
        let px = |x: usize, y: usize| {
            let i = (y * 4 + x) * 3;
            [out[i], out[i + 1], out[i + 2]]
        };
        assert_eq!(px(0, 0), [255, 0, 0], "left edge should be red");
        assert_eq!(px(3, 0), [0, 0, 255], "right edge should be blue");
    }

    #[test]
    fn downscale_averages_rather_than_point_samples() {
        // Alternating single-pixel columns must average to grey, not pick
        // one extreme. Point sampling would give 0 or 255.
        let (w, h) = (64usize, 64usize);
        let mut rgba = vec![255u8; w * h * 4];
        for y in 0..h {
            for x in 0..w {
                let v = if x % 2 == 0 { 0 } else { 255 };
                let i = (y * w + x) * 4;
                rgba[i..i + 3].copy_from_slice(&[v, v, v]);
            }
        }
        let out = square_downscale_rgba(&rgba, w, h, 8);
        for px in out.chunks_exact(3) {
            assert!(
                (100..=155).contains(&px[0]),
                "expected mid-grey from averaging, got {}",
                px[0]
            );
        }
    }

    #[test]
    fn test_pattern_has_spatial_structure() {
        let mut src = TestPattern::new(224);
        let frame = src.next_frame().unwrap().to_vec();
        assert_eq!(frame.len(), 224 * 224 * 3);

        // A uniform image would make the whole exercise meaningless, so
        // check there is real variation to encode.
        let mean = frame.iter().map(|&b| b as f64).sum::<f64>() / frame.len() as f64;
        let var = frame
            .iter()
            .map(|&b| (b as f64 - mean).powi(2))
            .sum::<f64>()
            / frame.len() as f64;
        assert!(var > 400.0, "test pattern is too flat: variance {var:.1}");
    }

    #[test]
    fn test_pattern_animates() {
        let mut src = TestPattern::new(64);
        let a = src.next_frame().unwrap().to_vec();
        for _ in 0..20 {
            src.next_frame();
        }
        let b = src.next_frame().unwrap().to_vec();
        assert_ne!(a, b, "frames are identical; the scene is not moving");
    }
}