WiFi 感知人体呼吸与心率:CSI 原理、可行做法与真实局限
作者 Wu Jun · 更新于 2026-11-02 · 约 8 分钟
呼吸与心率检测的基本原理是:胸腔起伏和心跳会改变无线多径,这些微小变化可能出现在 CSI 的幅度或相位里 [1]。但"频谱里有一个峰"不等于测到了生理信号。风扇、 空调、走路、设备自动换链和相位漂移都可能制造一个看起来合理的数字。
本文给出一条可复现的工程路线:如何正确计算独立测量率、选出相对稳定的信号、按时间戳重采样、 做 30 秒滑窗、设置可用性门控,以及用外部参考设备做标定。本文不是医疗建议,也不声称当前 开源数据集已经达到某个临床精度。
0. 先看结论
| 项目 | 相对可行性 | 关键原因 |
|---|---|---|
| 呼吸率 | 中等 | 变化较慢、幅度通常较大,但仍会被动作和 HVAC 周期干扰 |
| 心率 | 低到中等 | 胸腔位移更小,容易混入呼吸谐波、相位噪声和多径变化 |
| 多人同时测量 | 低 | 多人的反射叠加,单峰频率通常无法可靠分配给某个人 |
| 穿墙或跨房间 | 不可默认假设 | 墙体和距离会显著改变信噪比,必须重新实测 |
| 医疗诊断 | 不适用 | 本项目和消费级 CSI 都不能替代合规医疗设备 |
可行做法不是"每个窗口强行输出一个 bpm",而是同时输出测量值、质量指标和
Unavailable 状态。精度只能相对于同步的呼吸带、胸阻抗、脉搏血氧仪或人工计数来评估。
1. 采样率:记录率不是独立测量率
本项目 10 分钟抓包有 57,293 条链记录,平均约 95.5 条记录/秒。但一次 CSI 测量通常
包含 4 条或 8 条链记录,它们共享同一个测量时刻。按 chain_info bit15 切分后得到 7,609
个完整测量组,实际独立测量率约为:
7,609 / 599.924 s ≈ 12.68 measurements/s
分析脚本进一步剔除 25 个零值过多的异常组,使用 7,583 个测量,得到约 12.64 Hz。这个 速率高于 2 Hz 心率上限所需的 4 Hz Nyquist 下限,但余量并不夸张。频率分辨率、丢包、 时间戳抖动和窗口长度仍然决定结果是否可信。
因此第一件事不是画频谱,而是:
- 按
chain_infobit15 得到完整测量组; - 每个测量组只选择一条有效链,或构造双天线比值;
- 用实际时间戳排序,不要拿"记录序号 ÷ 常数帧率"伪造时间轴;
- 检查缺失链、64 子载波记录、全零 I/Q 和长于 1 秒的空档。
2. 选什么信号
幅度容易使用,但会被自动增益、自动速率和大幅动作共同影响。相位包含更细的位移信息, 但商用网卡的原始相位通常混有载波频偏、采样频偏和硬件漂移。单独对一条链"去卷绕"并不能 自动消除这些误差。
PhaseBeat 使用 CSI 相位信息做呼吸和心率监测 [2];FarSense 则强调用两根 天线的 CSI ratio 抑制共模噪声并扩大呼吸检测距离 [3]。工程上可以按以下 优先级试验:
- 同一测量中的双天线相位差或复数比值;
- 相邻子载波相位差;
- 多子载波幅度变化的第一主成分;
- 单链幅度,只作为最简基线。
无论选哪一种,都要先用静止空房、有人静止、有人走动三类数据检查:信号是否真的对呼吸周期 敏感,还是只对运动敏感。
3. 一个不夸张的估计流程
下面的函数假设输入已经是每次测量选一条链后得到的实值信号。它完成线性去趋势、Hann 窗、频带功率谱、峰值频率和两个质量指标。先按时间戳插值到统一采样率:
import numpy as np
from numpy.fft import rfft, rfftfreq
def resample_by_time(x, t, fs=20.0):
x = np.asarray(x, dtype=float)
t = np.asarray(t, dtype=float)
order = np.argsort(t)
t, x = t[order], x[order]
tu = np.arange(t[0], t[-1], 1.0 / fs)
xu = np.interp(tu, t, x)
return xu, tu
def estimate_bpm(x, fs, band):
x = np.asarray(x, dtype=float)
n = len(x)
if n < int(fs * 20):
return None
# Remove a linear trend before windowing.
idx = np.arange(n, dtype=float)
slope, intercept = np.polyfit(idx, x, 1)
y = x - (slope * idx + intercept)
y = (y - y.mean()) * np.hanning(n)
power = np.abs(rfft(y)) ** 2
freqs = rfftfreq(n, 1.0 / fs)
mask = (freqs >= band[0]) & (freqs <= band[1])
if not mask.any():
return None
band_power = power[mask]
band_freqs = freqs[mask]
k = int(np.argmax(band_power))
peak_freq = band_freqs[k]
floor = float(np.median(band_power)) + 1e-12
prominence_db = 10.0 * np.log10((band_power[k] + 1e-12) / floor)
band_ratio = float(band_power.sum() / (power.sum() + 1e-12))
return {
"bpm": float(peak_freq * 60.0),
"prominence_db": float(prominence_db),
"band_power_ratio": band_ratio,
}
示例频带:
# 先统一到 20 Hz 时间网格;插值不会增加真实信息。
xu, tu = resample_by_time(signal, timestamps, fs=20.0)
breath = estimate_bpm(xu, fs=20.0, band=(0.10, 0.50)) # 6–30 bpm
heart = estimate_bpm(xu, fs=20.0, band=(0.80, 2.00)) # 48–120 bpm
30 秒窗口的理论频率间隔约为 1 / 30 = 0.033 Hz,也就是约 2 bpm。它不代表误差一定是
2 bpm;峰的位置还受信噪比、窗函数和信号非平稳性影响。
4. 30 秒滑窗与可用性门控
推荐从 30 秒窗口、5 秒步长开始。呼吸至少覆盖 3 个周期,心率覆盖更多周期;代价是首次数值 至少延迟约 30 秒。对每个窗口保存:
window = {
"start_s": start,
"end_s": end,
"valid_sample_ratio": valid_count / expected_count,
"max_gap_s": max_gap,
"motion_score": motion_score,
"band_power_ratio": result["band_power_ratio"],
"prominence_db": result["prominence_db"],
"bpm": result["bpm"],
}
门控示例:
def vitals_status(w, limits):
if w["valid_sample_ratio"] < limits["min_valid_ratio"]:
return "Unavailable: data coverage"
if w["max_gap_s"] > limits["max_gap_s"]:
return "Unavailable: gap"
if w["motion_score"] > limits["max_motion"]:
return "Unavailable: motion"
if w["prominence_db"] < limits["min_prominence_db"]:
return "Unavailable: weak peak"
if w["band_power_ratio"] < limits["min_band_ratio"]:
return "Unavailable: broadband noise"
return "OK"
这些阈值不能照抄成全世界通用的常数。正确做法是收集有参考标签的"空房、静止、说话、走动、 翻身"数据,在自己设备的固件、信道、链路和安装位置下选取阈值,并同时报告误报率和可用率。
5. 一套可复现的标定协议
本节是实验设计建议,不是已经完成的精度结论。受试者应知情同意,并明确这只是研究用途。
5.1 设备与场景
- 固定路由器信道、带宽、发射功率和天线方向;
- 记录 AP 与受试者的距离、朝向、墙体材质和家具位置;
- 使用独立 AP 接口产生稳定流量;
- 参考设备放在容易同步的位置,例如呼吸带、脉搏血氧仪或独立呼吸计数应用;
- 先采 2 分钟空房基线,用来识别风扇、空调和供电周期。
5.2 采集顺序
- 空房 2 分钟;
- 受试者静坐 2 分钟,不讲话、不改变姿势;
- 按节拍器进行 10、15、20 次/分的呼吸段,每段至少 1 分钟;
- 正常呼吸 2 分钟,并用参考设备同步记录;
- 原地小幅动作 30 秒,再恢复静止 60 秒;
- 改变距离或朝向,重复至少 3 次。
心率部分不要用自报"我感觉是 70 bpm"作真值。使用带时间同步的脉搏血氧仪或其他经 合规验证的设备,只用于研究对照,不用于诊断。
5.3 每次实验至少保存
| 字段 | 目的 |
|---|---|
| UTC 起始时间与单调时钟 | 对齐 CSI 与参考设备 |
| 设备、固件、信道、带宽 | 判断结果是否可复现 |
| 距离、朝向、姿势 | 解释多径变化 |
| 参考 bpm 与更新时间 | 计算误差,而不是只看波形漂亮 |
| RSSI、SNR、链选择 | 发现弱信号或频繁换链 |
| 丢包、最大空档、有效样本率 | 解释 Unavailable |
| 空房、静止、动作标签 | 计算漏报与误报 |
6. 指标怎么报
只报一个有参考值时最准确的窗口,会系统性高估算法。至少同时报告:
- 覆盖率:所有窗口中通过质量门控的比例;
- 有条件 MAE/RMSE:只在
OK窗口且参考值有效时计算; - 空房误报率:没有任何人时输出呼吸/心率为正常的比例;
- 动作恢复时间:动作停止后多久重新给出稳定结果;
- 延迟:窗口长度与更新步长造成的时间延迟;
- 分场景结果:不同距离、朝向、人和设备分别列出。
如果比较两种算法,应使用同一批窗口、同一参考时间轴和同一门控规则。若一个算法只输出 20% 的窗口却获得更低 MAE,必须把覆盖率一并展示。
7. 最常见的失败模式
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 空房一直显示 15 bpm | 风扇、HVAC、路由器周期性负载 | 采空房基线,做周期源排查,不要删除失败样本 |
| 人一说话结果跳到高频 | 语音和下颌运动远强于胸廓微动 | 质量门控,状态标记为动作期间不可用 |
| 心率总等于呼吸的 3–4 倍 | 呼吸谐波或非线性混频 | 检查峰的可分性,不把最高谐波硬标为心率 |
| 换一个窗口就换链 | 某些链为空或自动选链 | 固定有效链或使用双天线 ratio |
| 时间轴看起来均匀但频谱有假峰 | 用记录序号代替时间戳 | 按时间戳重采样并检查长空档 |
| 换房间后全部失准 | 多径、距离、遮挡和增益改变 | 重新标定,不沿用旧阈值 |
| 深度学习精度很高但无法复现 | 同一受试者数据泄漏或窗口重叠 | 按受试者和日期划分训练/测试集 |
8. 这个数据集能证明什么
本项目公开的 10 分钟数据可用于练习:
- 解析 24 字节 CSI 头与 I/Q;
- 按 bit15 组合测量;
- 生成幅度热图和运动指标;
- 检验时间戳、丢包与链选择代码。
它不包含同步呼吸带或心率参考值,所以不能拿它宣称"呼吸误差 2 bpm"或"心率误差 5 bpm"。 这类结论必须来自新的、经过知情同意并带参考设备的采集。
9. 最小实施清单
- 按每次测量选链,不把 95.5 条记录/秒当 95.5 Hz 生理信号;
- 用时间戳重采样,并记录最大空档;
- 先做空房和静止基线,再尝试呼吸;
- 每位受试者、每个姿势和距离重复至少 3 次;
- 同时保存原始 CSI、参考值、标签和质量指标;
- 先证明呼吸,再尝试心率;
- 未通过质量门控时输出
Unavailable; - 不使用"诊断、监测疾病、保证准确"等医疗承诺。
10. 参考实现与下一步
- 相位处理与谱估计:本文示例 + Python 处理链路;
- 用真实数据练手:10 秒样例 + 10 分钟数据集;
- 数据集解析:从 8 个分片到热图;
- 已发表方法:PhaseBeat [2]、FarSense [3],以及 Wi-Fi CSI 感知综述 [1]。
参考与数据来源
本文的每条外部结论都标注来源与证据等级(分级标准);标注 E 级的是本项目可复现的实测数据。
- A 级 · 标准/综述
WiFi Sensing with Channel State Information: A Survey
W. Ma, W. Zhou, X. Wang · ACM Computing Surveys · 2019
doi:10.1145/3310194 · 被引 857 · 核实: CrossRef API, 2026-10-11 · 领域内最常被引用的 CSI 感知综述(被引 857 次,核实于 CrossRef)。 - B 级 · 同行评审
PhaseBeat: Exploiting CSI Phase Data for Vital Sign Monitoring with Commodity WiFi Devices
X. Wang, C. Yang, S. Mao · IEEE ICDCS 2017 · 2017
doi:10.1109/icdcs.2017.206 · 被引 325 · 核实: CrossRef API, 2026-10-11 · 用 CSI 相位做呼吸/心率监测的代表性工作(被引 325 次)。 - B 级 · 同行评审
FarSense: Pushing the Range Limit of WiFi-based Respiration Sensing with CSI Ratio of Two Antennas
Y. Zeng, D. Wu, J. Xiong, et al. · Proceedings of the ACM on Interactive, Mobile, Wearable and Ubiquitous Technologies (IMWUT) · 2019
doi:10.1145/3351279 · 被引 461 · 核实: CrossRef API, 2026-10-11 · 呼吸感知的距离上限与 CSI ratio 方法(被引 461 次)。