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");
}
}
|