ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于菲涅尔衍射模型的WiFi呼吸检测:从CSI采集到算法实现

基于菲涅尔衍射模型的WiFi呼吸检测:从CSI采集到算法实现

1. 从光学公式到无线信号:这个项目到底在折腾什么

第一次看到“菲涅尔衍射模型”和“WiFi呼吸检测”这两个词摆在一起,我脑子里蹦出来的画面是:一帮搞通信的人,把光学课本里的公式硬生生搬到了路由器信号上。后来自己动手跑了一遍数据才发现,这事儿不仅成立,而且背后的物理直觉相当漂亮。

这个项目的核心目标很明确:用家里常见的WiFi信号,在不接触人体的情况下,检测出人的呼吸频率。听起来像变魔术,但原理说穿了并不复杂——人体呼吸时胸腔起伏,会改变WiFi信号在空间中传播的路径,而菲涅尔衍射模型恰好能描述这种“信号绕过人体边缘时产生的干涉和衰减模式”。换句话说,呼吸不是一个孤立的动作,它会在电磁波的空间分布上留下可被计算的“指纹”。

适合谁来参考这篇笔记?如果你是有一定信号处理基础、玩过软件无线电或者做过CSI(信道状态信息)数据采集的开发者,这篇内容能帮你把菲涅尔区的概念从公式变成可运行的代码逻辑。如果你只是好奇“WiFi怎么知道我呼吸快慢”,那也没问题,我会尽量用生活化的类比把关键环节讲清楚。整个项目不需要昂贵的专业设备,一台支持CSI提取的路由器、两块开发板、一台普通电脑,再加上耐心,就能复现出可用的呼吸检测原型。

我实际跑下来的感受是:菲涅尔模型不是装饰品,它直接决定了你把设备摆在哪里、怎么摆、为什么某些位置信号强得离谱而某些位置几乎没反应。很多人做WiFi感知失败,不是算法不行,是压根没考虑电磁波的空间结构。这篇笔记就围绕这个核心展开,把模型、实操、踩坑点一次讲透。

2. 菲涅尔衍射模型:为什么呼吸能在WiFi信号里“显形”

2.1 菲涅尔区的物理直觉:电磁波不是一条直线

中学物理告诉我们光沿直线传播,但到了微波频段,事情变得微妙起来。当WiFi信号从发射端到接收端,如果中间有个物体(比如人体),信号并不是简单地“被挡住”或“没被挡住”。电磁波会绕过物体边缘,产生衍射,而衍射后的波又会和直达波发生干涉。菲涅尔区就是用来描述这种干涉模式的几何工具。

想象你往池塘里扔两块石头,水波相遇时有的地方波峰叠波峰(增强),有的地方波峰遇波谷(抵消)。菲涅尔区把空间切成一系列椭球面,第一菲涅尔区内的反射路径与直达路径的波程差小于半个波长,干涉是建设性的;第二菲涅尔区则相反,是破坏性的。人体呼吸时,胸腔前后移动几厘米,恰好能让某条反射路径在“建设性”和“破坏性”之间来回切换,接收端看到的信号幅度就会呈现周期性起伏——这个周期就是呼吸周期。

注意:菲涅尔区的半径和频率、距离都有关。2.4GHz下,收发相距1米时,第一菲涅尔区最粗的地方半径大约8.7厘米。这个尺寸和人体胸腔厚度在同一量级,所以呼吸引起的信号变化才足够明显。

2.2 从连续波到CSI:我们到底在测量什么

普通WiFi网卡只告诉你“信号强度”,那是一个经过平均和压缩的标量,信息量太少。要做呼吸检测,必须拿到CSI,也就是信道状态信息。CSI本质上是OFDM系统中每个子载波的幅度和相位,它描述了无线信道在频域的传递函数。你可以把它理解成:普通RSSI是黑白照片,CSI是彩色高清图,呼吸引起的微小扰动在CSI的多个子载波上会呈现不同的响应模式。

为什么用CSI而不是RSSI?因为呼吸引起的胸腔位移只有几毫米到几厘米,对应到信号幅度变化可能只有零点几dB,RSSI的量化精度根本捕捉不到。而CSI的相位信息对路径长度变化极其敏感——路径长度变化半个波长(2.4GHz下约6.25厘米),相位就会翻转180度。呼吸时胸腔移动几厘米,相位变化足以被清晰观测。

2.3 菲涅尔模型如何指导设备摆放

这是整个项目最实用的部分。很多人把收发设备随便一放就开始采数据,结果信号要么太强饱和,要么太弱淹没在噪声里。菲涅尔模型告诉你:当人体位于第一菲涅尔区边界附近时,呼吸引起的信号变化最大。

具体来说,如果你把收发天线正对着放,人体站在中间,那人体主要遮挡的是直达路径,信号变化反而可能因为遮挡过强而失真。更好的做法是让人体稍微偏离直达路径,处于第一菲涅尔区的边缘。此时呼吸引起的胸腔运动会让反射路径的波程差在半个波长附近波动,干涉效果最剧烈。

我实测下来,收发相距1.5米、人体距离收发连线垂直偏移约20到30厘米时,CSI幅度谱上呼吸对应的多普勒峰最干净。这个距离不是拍脑袋来的,而是根据第一菲涅尔区半径公式算出来的:r1 = sqrt(λ * d1 * d2 / (d1 + d2)),其中λ是波长,d1和d2分别是人体到收发端的距离。代入2.4GHz和1.5米总距离,算出来r1约10厘米,实际取2到3倍r1作为偏移量效果比较稳。

3. 硬件选型与数据采集:把CSI从网卡里掏出来

3.1 哪些设备能拿到CSI

不是所有WiFi网卡都开放CSI接口。市面上能用的方案就那么几类:早期Intel 5300网卡配合修改版驱动可以输出CSI,但带宽只有20MHz且只支持2.4GHz; Atheros AR9580等芯片配合开源工具也能采,但驱动适配比较折腾;还有一类是基于软件无线电的通用方案,灵活但成本高。

我选择的是双开发板方案:一块作为发射端持续发送已知序列,另一块作为接收端采集CSI。这样做的好处是发射信号完全可控,不受路由器自适应速率和波束成形的影响。开发板通过串口把CSI数据实时传到电脑,采样率可以做到每秒1000包以上,远高于呼吸频率(0.2到0.5Hz),满足奈奎斯特采样定理绰绰有余。

提示:如果你手头只有普通路由器,也可以尝试用开源固件提取CSI,但数据稳定性和时间戳精度会差一些。呼吸检测对时间同步要求不算极端,但相位解缠时如果时间戳抖动太大,呼吸波形会失真。

3.2 采集环境搭建的五个关键细节

第一,天线极化方向要一致。发射和接收天线的极化方向如果正交,信号会衰减20dB以上,呼吸引起的微小变化直接淹没。我一般让两根天线都垂直极化,并且保持平行。

第二,采样率不要盲目求高。呼吸信号带宽不到1Hz,理论上2Hz采样就够,但为了后续做相位解缠和滤波,实际取100Hz以上比较稳妥。我通常设200Hz,数据量适中,处理起来不卡。

第三,收发端要固定牢。呼吸引起的路径变化是毫米级,如果设备本身在晃动,那测到的就是设备抖动而不是呼吸。我用三脚架加橡皮筋把开发板绑死,桌面震动都能明显减少。

第四,避开金属和强反射面。金属会强烈反射电磁波,形成多径干扰。如果房间里有大面积金属柜或者钢筋墙面,CSI会变得非常“脏”。我一般选在相对空旷的室内,或者用吸波材料临时遮挡强反射源。

第五,记录参考呼吸信号。为了验证算法效果,我同时用一个胸带式呼吸传感器采集真实呼吸波形,作为地面真值。没有这个参考,你根本不知道算法输出的“呼吸”是不是噪声。

3.3 数据格式与预处理流程

开发板输出的CSI数据通常是复数形式,每个包包含多个子载波的实部和虚部。我拿到的原始数据格式是:时间戳、包序号、子载波索引、实部、虚部。预处理分四步:

  1. 相位解缠:原始相位被限制在[-π, π],呼吸引起的相位变化可能超过这个范围,需要解缠恢复连续相位。
  2. 子载波选择:不是所有子载波都对呼吸敏感。我通常计算每个子载波的方差,选方差最大的前10到20个,这些子载波要么处于菲涅尔区敏感位置,要么受多径影响小。
  3. 带通滤波:呼吸频率在0.1到0.5Hz,用巴特沃斯带通滤波器把其他频段(比如人体走动、手臂摆动)滤掉。
  4. 降采样:滤波后信号带宽很窄,降采样到10Hz减少计算量,同时不影响呼吸波形。
# 相位解缠示例(Python) import numpy as np def unwrap_phase(phase): return np.unwrap(phase) # 带通滤波示例 from scipy.signal import butter, filtfilt def bandpass_filter(data, lowcut=0.1, highcut=0.5, fs=200, order=4): nyq = 0.5 * fs low = lowcut / nyq high = highcut / nyq b, a = butter(order, [low, high], btype='band') return filtfilt(b, a, data)

4. 从CSI到呼吸波形:算法链路拆解

4.1 为什么不能直接对幅度做FFT

最朴素的想法是:把CSI幅度随时间的变化做FFT,看哪个频率有峰。但实际数据里,呼吸引起的幅度变化往往被其他运动干扰淹没。人体哪怕轻微转头、手指敲键盘,引起的多普勒频移都比呼吸大。直接FFT的结果是频谱上一堆杂峰,根本分不清哪个是呼吸。

更稳的做法是利用相位信息。相位对路径长度变化极其敏感,呼吸引起的胸腔位移虽然只有几毫米,但对应到2.4GHz的相位变化可能有几十度。而且相位变化是连续的,不像幅度那样容易受多径叠加影响。我实际对比过,同一段数据,幅度谱上呼吸峰比噪声高3dB,相位谱上能高12dB以上。

4.2 相位差分与多子载波融合

单个子载波的相位包含两部分:真实路径相位和硬件引入的相位偏移。硬件偏移可以通过共轭相乘消除:把相邻两个子载波的CSI共轭相乘,硬件偏移相同,相减后抵消,剩下的就是路径相位差。这个技巧在CSI处理里非常常用。

多子载波融合的思路是:不同子载波对应不同频率,它们在菲涅尔区里的敏感位置略有差异。有的子载波对胸腔前后移动敏感,有的对左右移动敏感。把多个子载波的相位差做加权平均,权重用各自信噪比,能显著提升呼吸波形的质量。

# 共轭相乘消除硬件相位偏移 def conjugate_multiply(csi_matrix): # csi_matrix shape: (subcarriers, time) return csi_matrix[1:, :] * np.conj(csi_matrix[:-1, :]) # 加权融合 def weighted_fusion(phase_diff, snr): weights = snr / np.sum(snr) return np.sum(phase_diff * weights[:, np.newaxis], axis=0)

4.3 呼吸频率提取:峰值检测与过零率

拿到干净的呼吸波形后,提取频率有两种常用方法。峰值检测是找波形的局部极大值,计算相邻峰之间的时间间隔,取倒数就是瞬时呼吸频率。这个方法直观,但对波形质量要求高,如果波形有毛刺会误检。过零率是统计波形穿过均值线的次数,对噪声更鲁棒,但分辨率不如峰值检测。

我一般先用峰值检测,如果发现峰间隔方差太大,再切到过零率做交叉验证。实际呼吸频率在0.2到0.4Hz之间,对应每分钟12到24次,超出这个范围的峰一律视为噪声剔除。

注意:呼吸波形不是标准正弦波,吸气快呼气慢,所以峰值间隔会有自然波动。不要用固定阈值判断异常,要用滑动窗口内的统计特性。

4.4 菲涅尔模型在算法中的隐式作用

你可能注意到,上面算法链路里没有显式地出现菲涅尔公式。但菲涅尔模型其实贯穿始终:子载波选择时,我优先选那些理论计算表明处于第一菲涅尔区敏感位置的子载波;设备摆放时,我根据菲涅尔半径确定人体偏移量;异常检测时,如果某个子载波的呼吸信号突然消失,我会检查是不是人体移动到了菲涅尔区零点位置。

换句话说,菲涅尔模型不是算法的一个模块,而是整个系统设计的“空间约束条件”。没有它,你就是在盲调参数;有了它,每一步都有物理依据。

5. 实操中踩过的坑与排查技巧

5.1 呼吸波形忽大忽小,像被调制了

第一次跑通算法时,呼吸波形幅度周期性起伏,周期大约30秒。我以为是呼吸深浅变化,后来发现是开发板晶振温漂导致的相位缓慢旋转。晶振频率随温度变化,收发端时钟不同步,相位就会线性漂移。解决方法很简单:在预处理阶段加一个相位去趋势,用滑动窗口拟合线性项并减去。

# 相位去趋势 def detrend_phase(phase, window=500): from scipy.signal import detrend return detrend(phase, type='linear')

5.2 多人场景下呼吸信号混叠

房间里两个人时,CSI里会同时包含两个呼吸频率。如果两人呼吸频率接近,FFT谱上就是一个宽峰,分不开。我试过用独立成分分析做盲源分离,效果一般,因为两个呼吸信号在CSI上的混合方式不是线性的。后来改用多天线空间滤波:接收端用两根天线,利用到达角差异把不同位置的呼吸信号分开。这个方案对硬件要求高一些,但分离效果明显更好。

5.3 常见问题速查表

现象可能原因排查方法解决思路
CSI幅度全为零发射端未工作或天线未接检查开发板指示灯和串口输出重新烧录发射端固件,确认天线连接
相位跳变剧烈采样时间戳不同步对比收发端时间戳差值用同一时钟源触发收发,或后期做时间对齐
呼吸峰不明显人体不在菲涅尔敏感区移动人体位置,观察CSI方差变化按菲涅尔半径公式重新摆放设备
波形有规律毛刺附近有周期性运动源(风扇、钟摆)关闭可疑设备再采数据带通滤波截止频率收紧到0.1-0.5Hz
呼吸频率偏快检测到的是心跳而非呼吸对比胸带参考信号降低带通上限到0.4Hz,或增加平滑窗口

5.4 一个容易被忽略的细节:天线高度

人体呼吸时胸腔起伏主要在水平方向,但如果你把天线架得太高或太低,呼吸引起的路径变化在垂直方向的分量会变小。我实测发现,天线高度与人体胸腔中心齐平时,呼吸信号最强。这个高度通常在1.2到1.4米之间,具体取决于人的身高和坐姿站姿。很多人把开发板放桌上,天线高度只有0.8米,信号强度直接打七折。

6. 这个项目还能怎么玩:从呼吸检测延伸出去

呼吸检测只是菲涅尔模型在WiFi感知里的一个入门应用。同样的硬件和算法框架,稍微改改就能做手势识别——手掌在菲涅尔区里挥动,CSI相位变化模式对应不同手势。再进一步,跌倒检测也是热门方向:人体从站立到跌倒,胸腔位置在菲涅尔区里的移动轨迹和呼吸完全不同,用隐马尔可夫模型或者简单的阈值状态机就能区分。

我个人觉得最有意思的是睡眠分期。呼吸频率和深度在睡眠不同阶段有显著差异,配合菲涅尔模型对体动引起的信号突变做检测,理论上可以用WiFi判断浅睡、深睡和REM期。当然这需要大量数据训练分类器,但物理层的信息量是够的。

如果你打算继续深入,我建议先把菲涅尔区可视化做出来:用Python画一个收发天线之间的椭球截面,把人体位置标上去,直观看到当前处于第几菲涅尔区。这个工具能帮你快速理解为什么某些位置信号好、某些位置信号差,比看公式快得多。

最后分享一个我常用的调试技巧:先用已知频率的机械振动源代替人体。比如用一个小电机带动纸片以0.3Hz摆动,放在菲涅尔敏感区,看CSI能不能检测出0.3Hz的峰。如果能,说明硬件和算法链路没问题,再去测真人。这样能把“设备问题”和“人体问题”分开排查,省很多时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进