examples/through-wall/README.md
wiflow_browser.html)A single self-contained HTML page that does the entire camera-supervised WiFi-pose loop in your browser, in your laptop camera's coordinate frame, as a 4-stage gated flow with a progress stepper (each stage unlocks the next):
x_norm = (x − base_mean) / (base_std + ε)), so a body's perturbation stands
out from the static channel. Persisted to IndexedDB. Can't capture without it.CSI → pose in-browser. Honest
held-out [email protected] / [email protected] / MPJPE, plus a mean-pose baseline the model
must beat (the project's whole ethos — no baseline-beating signal, it says so).
Can't train with <200 samples.The Python pipeline (wiflow_capture.py → wiflow_train.py → wiflow_infer.py)
proved the signal is real (held-out [email protected] ≈ 59.5% vs a 50% mean-pose baseline
= +9.4 pp). But it trained in a different camera's frame, so the inferred
skeleton never lined up with the laptop camera. Doing capture + train + infer all
in the browser with the same camera makes the training frame and the
inference frame identical → the skeleton aligns.
Training and inference run on TensorFlow.js. The page selects the backend at startup, preferring the fastest available:
localhost qualifies) — GPU compute.tfjs-backend-wasm, SIMD enabled, .wasm from the CDN).The active backend is shown as a badge in the header (compute: WebGPU /
WASM-SIMD / WebGL) so it's honest about what's actually running. The model
code is backend-agnostic — tf.js abstracts the device.
source: "esp32") / SIMULATED — not real (any other source)
/ NO-CSI-SERVER. The page never invents frames.cd v2
cargo build -p wifi-densepose-sensing-server
./target/debug/sensing-server.exe --ws-port 8765 --udp-port 5005
A real ESP32-S3 must be provisioned and streaming for source to read esp32
(see CLAUDE.local.md for the firmware build/provision steps). The page expects
the verified live endpoint ws://localhost:8765/ws/sensing with
source:"esp32", nodes [9, 13], features.*, node_features[].features.*,
and signal_field.values (400 floats).
Any static localhost server works. For example:
python -m http.server 8099
# then open: http://localhost:8099/examples/through-wall/wiflow_browser.html
(8099 is just the static file server — 8765 is a separate process, the CSI WebSocket.) Allow camera access when the browser prompts.
Point at a CSI server on another host with ?ws=:
http://localhost:8099/examples/through-wall/wiflow_browser.html?ws=ws://192.168.1.20:8765/ws/sensing
esp32 CSI frame coexist. Aim for a few
thousand samples. Samples persist in IndexedDB across refreshes.[ mean_rssi, variance, motion_band_power, breathing_band_power ] # 4 (features.*)
+ for node 9 then node 13: [ mean_rssi, variance, motion_band_power ] # 6 (node_features[].features.*)
+ signal_field.values, padded / truncated to 400 # 400
= 410-d
Verified against a real live frame: the in-browser csiVector() produces the
identical 410 vector as wiflow_capture.py's csi_vector() (node 9 first, then
node 13; field zero-padded).
| Library | CDN |
|---|---|
| TensorFlow.js core | @tensorflow/[email protected]/dist/tf.min.js |
| TF.js WebGPU backend | @tensorflow/[email protected]/dist/tf-backend-webgpu.min.js |
| TF.js WASM backend | @tensorflow/[email protected]/dist/tf-backend-wasm.min.js |
| MediaPipe Pose 0.5 (legacy solutions) | @mediapipe/[email protected]/pose.js |
Same person, same room, same session. Not validated cross-day, cross-room, or through-wall. The inferred pose is coarse ([email protected] is typically weak). If the model does not beat the mean-pose baseline, the page says so — that is a feature.