How RuView Extracts Breathing and Heart Rate from CSI Data: A Technical Deep Dive
RuView converts raw Wi-Fi Channel State Information (CSI) into breathing and heart rate estimates using FFT-based band-pass filtering, spectral power analysis, and rule-based heuristics implemented in Rust.
This article explains how the RuView open-source project transforms Wi-Fi CSI streams into quantitative vital signs. The extraction pipeline lives in the Rust-based VitalSignsClassifier and processes amplitude and phase data from ESP32 firmware to output breaths-per-minute (BPM) and heartbeats-per-minute with confidence scores.
Overview of the Vital Signs Pipeline
The VitalSignsClassifier in rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/ml/vital_signs_classifier.rs implements a six-stage pipeline:
- CSI Acquisition – Raw amplitude and phase vectors arrive as a
CsiDataBufferfrom the ESP32 firmware. - Band-Pass Filtering – FFT-based filters isolate the breathing band (0.1–0.5 Hz) and heartbeat band (0.8–2.0 Hz).
- Band-Power Computation – The filtered signals are squared and averaged to produce
breathing_band_powerandheartbeat_band_power. - Spectral Analysis – A short-time FFT extracts the dominant frequency in the breathing band as the primary breathing-rate cue.
- Rule-Based Estimation – Heuristics combine band power, dominant frequency, and signal quality to generate BPM values and confidence scores.
- Uncertainty Handling – Optional Monte-Carlo Dropout (via ONNX) or deterministic uncertainty estimates provide reliability flags for the UI.
Band-Pass Filtering and Power Calculation
The BandpassFilter struct implements lightweight FFT-based filtering to isolate physiological frequencies. The implementation zeroes frequencies outside the pass-band and computes mean-square power.
// rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/ml/vital_signs_classifier.rs
struct BandpassFilter {
low_freq: f64,
high_freq: f64,
sample_rate: f64,
}
impl BandpassFilter {
fn new(low: f64, high: f64, sample_rate: f64) -> Self {
// initialization
}
// FFT-based filter – zeroes frequencies outside the pass-band
fn apply(&self, signal: &[f64]) -> Vec<f64> {
// FFT implementation
}
// Simple mean-square power of the filtered signal
fn band_power(&self, signal: &[f64]) -> f64 {
let filtered = self.apply(signal);
filtered.iter().map(|x| x.powi(2)).sum::<f64>() / filtered.len() as f64
}
}
During feature extraction, the classifier instantiates two filters using the actual sample rate from the CsiDataBuffer:
let breathing_filter = BandpassFilter::new(0.1, 0.5, buffer.sample_rate);
let heartbeat_filter = BandpassFilter::new(0.8, 2.0, buffer.sample_rate);
let breathing_band_power = breathing_filter.band_power(&buffer.amplitudes) as f32;
let heartbeat_band_power = heartbeat_filter.band_power(&buffer.phases) as f32;
Spectral Analysis and Dominant Frequency Detection
The extract_spectral_features method performs a short-time FFT on the amplitude signal to identify the dominant breathing frequency. It applies a Hann window, computes the one-sided power spectrum, and stores the peak frequency.
// inside VitalSignsClassifier::extract_spectral_features
let n = 128.min(signal.len().next_power_of_two());
let mut buffer: Vec<Complex<f64>> = signal.iter()
.take(n)
.map(|&x| Complex::new(x, 0.0))
.collect();
buffer.resize(n, Complex::new(0.0, 0.0));
// Hann window → FFT
for (i, val) in buffer.iter_mut().enumerate() {
let window = 0.5 * (1.0 - (2.0 * std::f64::consts::PI * i as f64 / n as f64).cos());
*val = Complex::new(val.re * window, 0.0);
}
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(n);
fft.process(&mut buffer);
// Power spectrum (first half) → stored in `spectral_features`
let mut features: Vec<f32> = buffer.iter()
.take(n / 2)
.map(|c| (c.norm() / n as f64) as f32)
.collect();
features.resize(64, 0.0);
// Dominant frequency (index of max power) stored in the last slot
let freq_resolution = sample_rate / n as f64;
let (max_idx, _) = features.iter()
.enumerate()
.skip(1) // skip DC
.take(30) // up to ~30% of Nyquist
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap();
features[63] = (max_idx as f64 * freq_resolution) as f32;
The dominant frequency (in Hz) is multiplied by 60 to convert to breaths-per-minute during the estimation phase.
Breathing Rate Estimation Algorithm
The estimate_breathing_rate method combines spectral peak detection with band-power heuristics. If the dominant frequency falls within the valid breathing range (0.1–0.5 Hz), it converts directly to BPM. Otherwise, it falls back to a power-ratio heuristic.
// VitalSignsClassifier::estimate_breathing_rate
let dominant_freq = features.spectral_features[63];
if (0.1..=0.5).contains(&dominant_freq) {
dominant_freq * 60.0 // direct conversion to BPM
} else {
// fallback: ratio of breathing-band power to movement-band power
let power_ratio = features.breathing_band_power /
(features.movement_band_power + 0.001);
(12.0 + power_ratio * 8.0).clamp(6.0, 30.0)
}
The classify_breathing_rules function validates the estimate before returning it. It checks that breathing_band_power exceeds 0.01 and that overall signal quality is greater than 0.2. Confidence scores derive from both the band power magnitude and the signal quality metric.
Heart Rate Estimation Algorithm
Heart rate extraction relies on phase-domain features and the ratio of heartbeat-band power to breathing-band power. The estimate_heart_rate method calculates a base rate from phase feature energy, then adjusts it based on the power ratio between the heartbeat and breathing bands.
// VitalSignsClassifier::estimate_heart_rate
let phase_power = features.phase_features.iter()
.take(10)
.map(|&x| x.abs())
.sum::<f32>() / 10.0;
let power_ratio = features.heartbeat_band_power /
(features.breathing_band_power + 0.001);
let base_rate = 70.0 + phase_power * 20.0;
let adjusted = if power_ratio > 0.5 {
base_rate * 1.1
} else {
base_rate * 0.9
};
adjusted.clamp(40.0, 180.0)
The classify_heartbeat_rules validator requires heartbeat_band_power ≥ 0.005 and signal quality ≥ 0.3. It categorizes confidence into tiers (Strong, Moderate, Weak, VeryWeak) based on the heartbeat band power magnitude, returning a HeartbeatClassification struct containing the final BPM and confidence score.
Integration Example
The following Rust example demonstrates the complete workflow from raw CSI buffer to vital signs output:
use wifi_densepose_mat::ml::vital_signs_classifier::{
VitalSignsClassifier, VitalSignsClassifierConfig,
};
use wifi_densepose_signal::domain::CsiDataBuffer;
// 1️⃣ Load a CSI buffer (e.g., from a file or live stream)
let buffer: CsiDataBuffer = load_my_csi_buffer()?; // user-provided helper
// 2️⃣ Create a rule-based classifier (no ONNX model needed)
let config = VitalSignsClassifierConfig::default();
let classifier = VitalSignsClassifier::rule_based(config);
// 3️⃣ Extract spectral & band-power features
let features = classifier.extract_features(&buffer)?;
// 4️⃣ Run the rule-based classifier
let output = classifier.classify(&features).await?;
// 5️⃣ Read the breathing & heart-rate estimates
if let Some(breathing) = output.breathing {
println!("Breathing rate: {:.1} BPM (conf {:.2})",
breathing.rate_bpm, breathing.confidence);
}
if let Some(heartbeat) = output.heartbeat {
println!("Heart rate: {:.0} BPM (conf {:.2})",
heartbeat.rate_bpm, heartbeat.confidence);
}
Key implementation details:
- Band-power values are dimensionless (normalized by sample count) but serve as proxies for signal energy in physiological bands.
- Signal quality metrics (via
estimate_signal_quality) cap confidence levels to prevent false detections during noisy CSI periods. - The architecture supports optional ONNX model inference via
VitalSignsClassifier::from_onnxfor Monte-Carlo dropout-based uncertainty quantification when higher accuracy is required.
Key Source Files
Summary
RuView extracts breathing and heart rate from CSI data through a deterministic, rule-based signal processing pipeline:
- FFT-based band-pass filtering isolates the breathing (0.1–0.5 Hz) and heartbeat (0.8–2.0 Hz) frequency bands from raw CSI amplitude and phase streams.
- Band-power calculation measures signal energy within each physiological band to detect the presence of vital signs.
- Spectral peak detection identifies the dominant breathing frequency via short-time FFT with Hann windowing.
- Rule-based estimation converts dominant frequencies and power ratios into BPM values, clamped to physiologically valid ranges (6–30 BPM for breathing, 40–180 BPM for heart rate).
- Confidence gating uses signal quality metrics and power thresholds to suppress false detections during noisy CSI periods.
The entire workflow is encapsulated in the VitalSignsClassifier struct within the Rust backend, enabling real-time processing on resource-constrained devices while maintaining the flexibility to swap in ONNX-based neural networks for enhanced accuracy.
Frequently Asked Questions
What frequency bands does RuView use to isolate breathing and heart rate from CSI data?
RuView applies FFT-based band-pass filters to isolate the breathing band (0.1–0.5 Hz) and the heartbeat band (0.8–2.0 Hz). These ranges correspond to 6–30 breaths per minute and 48–120 beats per minute, respectively. The BandpassFilter struct in vital_signs_classifier.rs implements this by zeroing frequency components outside these ranges in the FFT domain.
How does RuView calculate confidence scores for vital sign detection?
Confidence scores derive from signal quality metrics and band-power thresholds. For breathing detection, the system requires breathing_band_power > 0.01 and signal quality > 0.2. For heart rate, it requires heartbeat_band_power ≥ 0.005 and signal quality > 0.3. The classify_breathing_rules and classify_heartbeat_rules functions categorize detections into tiers (Strong, Moderate, Weak, VeryWeak) based on these power levels, producing normalized confidence values between 0.0 and 1.0.
Can RuView use machine learning models instead of rule-based detection?
Yes. While the default implementation uses deterministic rule-based classification, the VitalSignsClassifier supports ONNX model inference via the from_onnx constructor. When an ONNX model is loaded, the system can perform Monte-Carlo Dropout to generate uncertainty estimates alongside BPM predictions. This hybrid approach allows users to balance real-time performance (rule-based) against higher accuracy requirements (neural network) without changing the underlying CSI acquisition pipeline.
What hardware sampling rate does RuView require for accurate vital sign extraction?
The VitalSignsClassifier adapts to the actual sample rate provided by the CsiDataBuffer, typically derived from the ESP32 CSI collection firmware. The FFT-based spectral analysis uses the sample rate to calculate frequency resolution (sample_rate / n) and correctly map FFT bin indices to physical frequencies (Hz). For the default 0.1–0.5 Hz breathing band and 0.8–2.0 Hz heartbeat band, the system requires sufficient temporal resolution to resolve these low frequencies, typically achieved with sampling rates of 100–1000 Hz and FFT windows of 128 samples or more.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →