//! Multi-modal fusion: camera depth + WiFi RF tomography → unified point cloud. use crate::pointcloud::{ColorPoint, PointCloud}; use std::collections::HashMap; /// Occupancy volume from WiFi RF tomography (mirrors RuView's OccupancyVolume). #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct OccupancyVolume { pub densities: Vec, // [nz][ny][nx] voxel densities pub nx: usize, pub ny: usize, pub nz: usize, pub bounds: [f64; 6], // [x_min, y_min, z_min, x_max, y_max, z_max] pub occupied_count: usize, } /// Convert WiFi occupancy volume to a sparse point cloud. /// /// Each occupied voxel (density > threshold) becomes a point at the voxel center. pub fn occupancy_to_pointcloud(vol: &OccupancyVolume) -> PointCloud { let mut cloud = PointCloud::new("wifi_occupancy"); let threshold = 0.3; let dx = (vol.bounds[3] - vol.bounds[0]) / vol.nx as f64; let dy = (vol.bounds[4] - vol.bounds[1]) / vol.ny as f64; let dz = (vol.bounds[5] - vol.bounds[2]) / vol.nz as f64; for iz in 0..vol.nz { for iy in 0..vol.ny { for ix in 0..vol.nx { let idx = iz * vol.ny * vol.nx + iy * vol.nx + ix; let density = vol.densities[idx]; if density > threshold { let x = vol.bounds[0] + (ix as f64 + 0.5) * dx; let y = vol.bounds[1] + (iy as f64 + 0.5) * dy; let z = vol.bounds[2] + (iz as f64 + 0.5) * dz; // Color by density (green=low, red=high) let t = ((density - threshold) / (1.0 - threshold)).min(1.0); let r = (t * 255.0) as u8; let g = ((1.0 - t) * 200.0) as u8; cloud.points.push(ColorPoint { x: x as f32, y: y as f32, z: z as f32, r, g, b: 50, intensity: density as f32, }); } } } } cloud } /// Fuse multiple point clouds with voxel-grid downsampling. /// /// Points from all clouds are binned into voxels of the given size. /// Each voxel produces one averaged point (position, color, max intensity). /// Per-voxel accumulator: (sum_x, sum_y, sum_z, sum_r, sum_g, sum_b, max_intensity, count). type VoxelAccum = (f32, f32, f32, f32, f32, f32, f32, u32); pub fn fuse_clouds(clouds: &[&PointCloud], voxel_size: f32) -> PointCloud { let mut cells: HashMap<(i32, i32, i32), VoxelAccum> = HashMap::new(); for cloud in clouds { for p in &cloud.points { let key = ( (p.x / voxel_size).floor() as i32, (p.y / voxel_size).floor() as i32, (p.z / voxel_size).floor() as i32, ); let entry = cells .entry(key) .or_insert((0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)); entry.0 += p.x; entry.1 += p.y; entry.2 += p.z; entry.3 += p.r as f32; entry.4 += p.g as f32; entry.5 += p.b as f32; entry.6 = entry.6.max(p.intensity); entry.7 += 1; } } let mut fused = PointCloud::new("fused"); for (sx, sy, sz, sr, sg, sb, mi, n) in cells.values() { let n = *n as f32; fused.points.push(ColorPoint { x: sx / n, y: sy / n, z: sz / n, r: (sr / n) as u8, g: (sg / n) as u8, b: (sb / n) as u8, intensity: *mi, }); } fused } /// Generate a demo occupancy volume (room with person). pub fn demo_occupancy() -> OccupancyVolume { let nx = 10; let ny = 10; let nz = 5; let mut densities = vec![0.0f64; nx * ny * nz]; // Walls (high density at edges) for iz in 0..nz { for iy in 0..ny { for ix in 0..nx { let idx = iz * ny * nx + iy * nx + ix; // Edges = walls if ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 { densities[idx] = 0.8; } // Floor if iz == 0 { densities[idx] = 0.6; } // Person at center (iz=1-3, ix=4-6, iy=4-6) if (4..=6).contains(&ix) && (4..=6).contains(&iy) && (1..=3).contains(&iz) { densities[idx] = 0.9; } } } } let occupied_count = densities.iter().filter(|&&d| d > 0.3).count(); OccupancyVolume { densities, nx, ny, nz, bounds: [0.0, 0.0, 0.0, 5.0, 5.0, 3.0], occupied_count, } } #[cfg(test)] mod tests { use super::*; fn cloud_with(name: &str, pts: &[(f32, f32, f32)]) -> PointCloud { let mut c = PointCloud::new(name); for &(x, y, z) in pts { c.points.push(ColorPoint { x, y, z, r: 10, g: 20, b: 30, intensity: 0.5, }); } c } #[test] fn fuse_clouds_merges_non_overlapping() { let a = cloud_with("a", &[(0.0, 0.0, 0.0)]); let b = cloud_with("b", &[(5.0, 5.0, 5.0)]); let fused = fuse_clouds(&[&a, &b], 0.1); assert_eq!( fused.points.len(), 2, "two far-apart points should yield two voxels" ); } #[test] fn fuse_clouds_voxel_dedup() { // Points all within one voxel must collapse to a single averaged point. let a = cloud_with( "a", &[(0.01, 0.02, 0.03), (0.04, 0.01, 0.02), (0.03, 0.03, 0.01)], ); let fused = fuse_clouds(&[&a], 0.5); assert_eq!(fused.points.len(), 1, "three close points → one voxel"); } }