Skip to content

Commit 0c512ed

Browse files
committed
Add MiDaS GPU depth, serial CSI reader, full sensor fusion
- MiDaS depth server: PyTorch on CUDA, real monocular depth estimation - Rust server calls MiDaS via HTTP for neural depth (falls back to luminance) - Serial CSI reader for ESP32 with motion detection + presence estimation - CSI disabled by default (RUVIEW_CSI=1 to enable) — serial reader needs baud config - Edge-enhanced depth for better object boundaries - All sensors wired: camera, ESP32 CSI, mmWave (CSI gated until serial fixed) Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent f39d88e commit 0c512ed

File tree

5 files changed

+378
-87
lines changed

5 files changed

+378
-87
lines changed

rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/depth.rs

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,97 @@ pub fn backproject_depth(
7171

7272
/// Run depth estimation on an image.
7373
///
74-
/// When built with `--features onnx`, uses MiDaS ONNX model.
75-
/// Otherwise, generates synthetic depth from image luminance (for testing).
74+
/// Tries MiDaS GPU server (127.0.0.1:9885) first, falls back to luminance+edges.
7675
pub fn estimate_depth(
7776
image_data: &[u8],
7877
width: u32,
7978
height: u32,
8079
) -> Result<Vec<f32>> {
81-
// Luminance-based pseudo-depth (works without ONNX model)
82-
// Darker pixels = further away (rough approximation)
83-
let mut depth_map = vec![3.0f32; (width * height) as usize];
84-
for y in 0..height {
85-
for x in 0..width {
86-
let idx = (y * width + x) as usize;
87-
let ri = idx * 3;
88-
if ri + 2 < image_data.len() {
89-
let r = image_data[ri] as f32;
90-
let g = image_data[ri + 1] as f32;
91-
let b = image_data[ri + 2] as f32;
92-
let lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
93-
// Map luminance to depth: bright=near (1m), dark=far (5m)
94-
depth_map[idx] = 1.0 + (1.0 - lum) * 4.0;
95-
}
80+
// Try MiDaS GPU server
81+
if let Ok(depth) = estimate_depth_midas_server(image_data, width, height) {
82+
return Ok(depth);
83+
}
84+
85+
// Fallback: luminance + edge-based pseudo-depth
86+
let w = width as usize;
87+
let h = height as usize;
88+
let mut lum = vec![0.0f32; w * h];
89+
for i in 0..w * h {
90+
let ri = i * 3;
91+
if ri + 2 < image_data.len() {
92+
lum[i] = (0.299 * image_data[ri] as f32
93+
+ 0.587 * image_data[ri + 1] as f32
94+
+ 0.114 * image_data[ri + 2] as f32) / 255.0;
95+
}
96+
}
97+
let mut edges = vec![0.0f32; w * h];
98+
for y in 1..h - 1 {
99+
for x in 1..w - 1 {
100+
let gx = -lum[(y-1)*w+x-1] + lum[(y-1)*w+x+1]
101+
- 2.0*lum[y*w+x-1] + 2.0*lum[y*w+x+1]
102+
- lum[(y+1)*w+x-1] + lum[(y+1)*w+x+1];
103+
let gy = -lum[(y-1)*w+x-1] - 2.0*lum[(y-1)*w+x] - lum[(y-1)*w+x+1]
104+
+ lum[(y+1)*w+x-1] + 2.0*lum[(y+1)*w+x] + lum[(y+1)*w+x+1];
105+
edges[y * w + x] = (gx * gx + gy * gy).sqrt().min(1.0);
96106
}
97107
}
108+
let mut depth_map = vec![3.0f32; w * h];
109+
for i in 0..w * h {
110+
let base = 1.0 + (1.0 - lum[i]) * 3.5;
111+
let edge_boost = edges[i] * 1.5;
112+
depth_map[i] = (base - edge_boost).max(0.3);
113+
}
98114
Ok(depth_map)
99115
}
100116

117+
/// Call MiDaS depth server running on GPU (127.0.0.1:9885).
118+
fn estimate_depth_midas_server(rgb: &[u8], width: u32, height: u32) -> Result<Vec<f32>> {
119+
let expected = (width * height * 3) as usize;
120+
if rgb.len() < expected { anyhow::bail!("rgb too small"); }
121+
122+
// Send RGB as JSON array to depth server
123+
let rgb_list: Vec<u8> = rgb[..expected].to_vec();
124+
let body = serde_json::json!({
125+
"width": width,
126+
"height": height,
127+
"rgb": rgb_list,
128+
});
129+
let body_bytes = serde_json::to_vec(&body)?;
130+
131+
let client = std::net::TcpStream::connect_timeout(
132+
&"127.0.0.1:9885".parse()?, std::time::Duration::from_millis(500)
133+
)?;
134+
client.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
135+
client.set_write_timeout(Some(std::time::Duration::from_secs(2)))?;
136+
137+
use std::io::{Read, Write};
138+
let mut stream = client;
139+
let req = format!(
140+
"POST /depth HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n",
141+
body_bytes.len()
142+
);
143+
stream.write_all(req.as_bytes())?;
144+
stream.write_all(&body_bytes)?;
145+
146+
// Read response
147+
let mut resp = Vec::new();
148+
stream.read_to_end(&mut resp)?;
149+
150+
// Skip HTTP headers
151+
let body_start = resp.windows(4).position(|w| w == b"\r\n\r\n")
152+
.map(|p| p + 4).unwrap_or(0);
153+
let depth_bytes = &resp[body_start..];
154+
155+
let n = (width * height) as usize;
156+
if depth_bytes.len() < n * 4 { anyhow::bail!("depth response too small"); }
157+
158+
let depth: Vec<f32> = depth_bytes[..n * 4].chunks_exact(4)
159+
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
160+
.collect();
161+
162+
Ok(depth)
163+
}
164+
101165
/// Capture depth cloud from camera (placeholder — real impl uses nokhwa or v4l2).
102166
pub async fn capture_depth_cloud(frames: usize) -> Result<PointCloud> {
103167
eprintln!("Camera capture not available (no camera on this machine).");

rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod csi;
1515
mod depth;
1616
mod fusion;
1717
mod pointcloud;
18+
mod serial_csi;
1819
mod stream;
1920
mod training;
2021

rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/pointcloud.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub struct GaussianSplat {
8989

9090
pub fn to_gaussian_splats(cloud: &PointCloud) -> Vec<GaussianSplat> {
9191
// Cluster points into voxels and create one Gaussian per cluster
92-
let voxel_size = 0.15; // larger voxels = fewer splats = faster streaming
92+
let voxel_size = 0.08; // smaller voxels = more detail = visible movement
9393
let mut cells: std::collections::HashMap<(i32, i32, i32), Vec<&ColorPoint>> = std::collections::HashMap::new();
9494

9595
for p in &cloud.points {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Serial CSI reader — parse ESP32 CSI data from /dev/ttyACM0 and /dev/ttyUSB0.
2+
//!
3+
//! ESP32 firmware outputs lines like:
4+
//! I (56994) csi_collector: CSI cb #2900: len=256 rssi=-32 ch=5
5+
//!
6+
//! This module reads those lines, extracts RSSI, and tracks signal changes
7+
//! to detect motion and presence.
8+
9+
use std::io::{BufRead, BufReader};
10+
use std::sync::{Arc, Mutex};
11+
12+
#[derive(Clone, Debug)]
13+
pub struct CsiReading {
14+
pub port: String,
15+
pub rssi: i32,
16+
pub len: u32,
17+
pub channel: u8,
18+
pub callback_num: u64,
19+
pub timestamp_ms: i64,
20+
}
21+
22+
#[derive(Clone, Debug)]
23+
pub struct CsiState {
24+
/// Latest readings from each port
25+
pub readings: Vec<CsiReading>,
26+
/// RSSI history for motion detection (last 20 values per port)
27+
pub rssi_history: Vec<Vec<i32>>,
28+
/// Motion score (0.0 = still, 1.0 = strong motion)
29+
pub motion_score: f32,
30+
/// Estimated presence distance (from RSSI)
31+
pub presence_distance_m: f32,
32+
/// Total frames received
33+
pub total_frames: u64,
34+
}
35+
36+
impl Default for CsiState {
37+
fn default() -> Self {
38+
Self {
39+
readings: Vec::new(),
40+
rssi_history: Vec::new(),
41+
motion_score: 0.0,
42+
presence_distance_m: 3.0,
43+
total_frames: 0,
44+
}
45+
}
46+
}
47+
48+
/// Start reading CSI from serial ports in background threads.
49+
/// Returns shared state that updates as frames arrive.
50+
pub fn start_serial_readers(ports: &[&str]) -> Arc<Mutex<CsiState>> {
51+
let state = Arc::new(Mutex::new(CsiState::default()));
52+
53+
for (idx, port) in ports.iter().enumerate() {
54+
let port_path = port.to_string();
55+
let st = state.clone();
56+
57+
std::thread::spawn(move || {
58+
loop {
59+
if let Ok(file) = std::fs::File::open(&port_path) {
60+
let reader = BufReader::new(file);
61+
for line in reader.lines() {
62+
if let Ok(line) = line {
63+
if let Some(reading) = parse_csi_line(&line, &port_path) {
64+
update_state(&st, idx, reading);
65+
}
66+
}
67+
}
68+
}
69+
// Retry if port disconnects
70+
std::thread::sleep(std::time::Duration::from_secs(2));
71+
eprintln!(" CSI: reconnecting {port_path}...");
72+
}
73+
});
74+
75+
eprintln!(" CSI: reading {port}");
76+
}
77+
78+
state
79+
}
80+
81+
fn parse_csi_line(line: &str, port: &str) -> Option<CsiReading> {
82+
// Parse: I (56994) csi_collector: CSI cb #2900: len=256 rssi=-32 ch=5
83+
if !line.contains("csi_collector") || !line.contains("CSI cb") {
84+
return None;
85+
}
86+
87+
let rssi = line.split("rssi=").nth(1)?
88+
.split_whitespace().next()?
89+
.parse::<i32>().ok()?;
90+
91+
let len = line.split("len=").nth(1)?
92+
.split_whitespace().next()?
93+
.parse::<u32>().ok()?;
94+
95+
let channel = line.split("ch=").nth(1)?
96+
.split_whitespace().next()
97+
.unwrap_or("0")
98+
.parse::<u8>().unwrap_or(0);
99+
100+
let cb_num = line.split('#').nth(1)?
101+
.split(':').next()?
102+
.parse::<u64>().ok()?;
103+
104+
Some(CsiReading {
105+
port: port.to_string(),
106+
rssi,
107+
len,
108+
channel,
109+
callback_num: cb_num,
110+
timestamp_ms: chrono::Utc::now().timestamp_millis(),
111+
})
112+
}
113+
114+
fn update_state(state: &Arc<Mutex<CsiState>>, port_idx: usize, reading: CsiReading) {
115+
let mut st = state.lock().unwrap();
116+
117+
// Ensure vectors are big enough
118+
while st.readings.len() <= port_idx {
119+
st.readings.push(reading.clone());
120+
st.rssi_history.push(Vec::new());
121+
}
122+
123+
st.readings[port_idx] = reading.clone();
124+
st.total_frames += 1;
125+
126+
// Track RSSI history
127+
let hist = &mut st.rssi_history[port_idx];
128+
hist.push(reading.rssi);
129+
if hist.len() > 20 { hist.remove(0); }
130+
131+
// Motion detection: RSSI variance over last 20 readings
132+
if hist.len() >= 5 {
133+
let mean: f32 = hist.iter().map(|&r| r as f32).sum::<f32>() / hist.len() as f32;
134+
let variance: f32 = hist.iter().map(|&r| (r as f32 - mean).powi(2)).sum::<f32>() / hist.len() as f32;
135+
// High variance = motion (someone moving changes signal reflections)
136+
st.motion_score = (variance / 50.0).min(1.0); // normalize: variance of 50 = full motion
137+
}
138+
139+
// Estimate presence distance from RSSI (path loss model)
140+
// Free space: RSSI = -10 * n * log10(d) + A
141+
// n ≈ 2.5 for indoor, A ≈ -30 (1m reference)
142+
let avg_rssi: f32 = st.readings.iter().map(|r| r.rssi as f32).sum::<f32>()
143+
/ st.readings.len().max(1) as f32;
144+
let d = 10.0f32.powf((-30.0 - avg_rssi) / (10.0 * 2.5));
145+
st.presence_distance_m = d.clamp(0.3, 10.0);
146+
}
147+
148+
/// Convert CSI state to occupancy influence on the point cloud.
149+
/// Returns (motion_score, presence_distance, total_frames).
150+
pub fn get_csi_influence(state: &Arc<Mutex<CsiState>>) -> (f32, f32, u64) {
151+
let st = state.lock().unwrap();
152+
(st.motion_score, st.presence_distance_m, st.total_frames)
153+
}

0 commit comments

Comments
 (0)