1. 从光学衍射到无线感知:一个跨域建模的动机
第一次看到"菲涅尔衍射"和"WiFi呼吸检测"这两个词摆在一起时,我的反应是:这俩东西真的能扯上关系?一个是十九世纪的光学经典理论,一个是近十年才火起来的无线感知应用。但仔细琢磨之后我发现,把它们放在同一个框架下理解,不仅合理,而且能帮我们打通很多之前模模糊糊的认知盲区。
这篇笔记记录的是我在做一个非接触式呼吸监测方案时的思考过程。目标很明确:用普通的WiFi信号,在不接触人体的前提下,检测出人的呼吸频率和呼吸深度变化。听起来像是某种黑科技,但拆开来看,它的底层逻辑和光学里的菲涅尔衍射模型有着惊人的结构相似性。
为什么值得把这两件事放在一起讲?因为大多数做WiFi感知的教程一上来就丢给你一堆CSI(信道状态信息)数据处理的代码,告诉你"这样滤波、那样做FFT就能出呼吸波形",但从来不解释为什么电磁波在人体附近的传播可以用这套数学来描述。结果就是,一旦场景变了、参数调了,你就完全不知道该怎么改。我自己就踩过这个坑——换了一个房间布局,之前调好的算法直接失效,排查了整整两天才发现是对菲涅尔区的理解不到位。
所以这篇内容适合两类人看:一类是对无线感知感兴趣、想动手做呼吸检测的开发者;另一类是想理解"波动理论如何统一描述不同尺度的物理现象"的跨学科爱好者。不管你是哪种,我都会从最基础的物理图像讲起,一步步推到可落地的算法实现,中间穿插我自己踩过的坑和调参经验。
需要提前说明的是,文中涉及的硬件平台和代码框架都是通用方案,不绑定任何特定品牌或商业产品。你用手头的软件无线电设备或者支持CSI采集的网卡都能复现。
2. 菲涅尔区到底在描述什么:从光学到电磁波的类比
2.1 惠更斯原理视角下的波前重构
要理解菲涅尔衍射,得先从惠更斯原理说起。这个原理的核心思想很朴素:波前上的每一个点都可以看作是一个新的次级波源,这些次级波源发出的球面波叠加起来,就形成了下一时刻的波前。菲涅尔在这个基础上引入了干涉的概念,把衍射问题转化成了积分问题。
在光学里,菲涅尔衍射描述的是光源和观察屏距离有限远时,光波通过孔径或遇到障碍物后的传播行为。关键参数是菲涅尔数,它决定了衍射图样的特征。当菲涅尔数很大时,衍射效应不明显,近似为几何光学;当菲涅尔数接近1或更小时,衍射条纹变得显著。
把这个框架搬到WiFi频段(2.4GHz或5GHz),情况是这样的:发射端和接收端之间的距离通常在几米到十几米,波长在厘米量级。人体尺寸(比如胸腔厚度约20-30厘米)和波长处于同一量级,这意味着人体对电磁波的散射和衍射效应不可忽略。你不能简单地把人当成一个反射点来处理,而必须考虑波在人体周围绕射、干涉形成的复杂场分布。
这就是为什么菲涅尔区模型在这里特别有用——它给出了一个系统化的方法来划分空间区域,判断哪些区域的物体对接收信号影响最大。
2.2 第一菲涅尔区半径的计算与物理含义
第一菲涅尔区的半径公式是:
r1 = sqrt(λ * d1 * d2 / (d1 + d2))其中λ是波长,d1和d2分别是障碍物到发射端和接收端的距离。当d1等于d2时(障碍物在中间),公式简化为r1 = sqrt(λ * d / 4),d是收发总距离。
举个实际数字:2.4GHz WiFi的波长约12.5厘米。如果收发距离是8米,障碍物在正中间,那么第一菲涅尔区半径约为sqrt(0.125 * 8 / 4) = sqrt(0.25) = 0.5米。也就是说,在收发连线的中点附近,一个直径约1米的圆柱形空间内的任何物体,都会对信号产生显著影响。
这个数字很关键。人的胸腔在呼吸时的起伏幅度大约是1-3厘米,相对于0.5米的第一菲涅尔区半径来说很小。但注意,呼吸引起的不是物体位置的大幅移动,而是散射体截面积的微小变化。这个微小变化落在菲涅尔区的边界附近时,会引起接收信号的相位和幅度发生可检测的变化。
我实测下来的经验是:当人站在第一菲涅尔区边界附近时,呼吸信号的信噪比最高。如果人完全在菲涅尔区内部或者完全在外面,信号变化反而没那么明显。这个现象用菲涅尔衍射的矢量叠加模型可以很好地解释——边界处的相位变化率最大。
2.3 从菲涅尔积分到信道冲激响应
菲涅尔衍射的数学表达是一个复积分:
U(P) = (1/jλ) * ∫∫ U(Q) * exp(jkr)/r * dS其中U(Q)是孔径面上的场分布,r是孔径点到观察点的距离。这个积分在光学里通常用菲涅尔正弦和余弦积分来数值计算。
在WiFi信道建模中,我们做的事情本质上是类似的:把人体表面离散化成若干散射单元,每个单元对入射波产生二次辐射,接收端收到的是所有二次辐射的叠加。区别在于,光学里我们关心的是光强(幅度的平方),而WiFi CSI给我们的是复数的信道频率响应,包含了幅度和相位信息。
这个对应关系让我豁然开朗:菲涅尔衍射模型提供的是一个确定性框架,用来预测给定几何布局下的场分布;而WiFi CSI测量的是这个场分布在接收天线处的实际值。两者结合,就可以反推出人体呼吸引起的散射体变化。
3. WiFi CSI数据的采集与预处理:从原始相位到可用信号
3.1 硬件选型与CSI采集的基本要求
做WiFi呼吸检测,第一步是拿到CSI数据。普通网卡只能给你RSSI(接收信号强度指示),这个粒度太粗了,呼吸引起的信号变化完全被淹没在噪声里。你需要能输出子载波级别信道响应的设备。
常见的方案有几类:一是基于软件无线电平台自己搭建收发链路,灵活性最高但门槛也最高;二是使用支持CSI提取的商用网卡配合开源工具,这是大多数研究者和爱好者的选择;三是使用专用的信道测量设备,精度高但成本也高。
我自己的选择是第二类方案。具体来说,需要满足几个条件:网卡驱动支持CSI上报、收发端时钟同步、采样率足够高(至少能覆盖呼吸频率范围,即0.1-0.5Hz,但为了捕捉谐波和运动伪影,建议采样率在50Hz以上)。
这里有个容易忽略的细节:收发端的载波频率偏移(CFO)和采样频率偏移(SFO)会严重污染相位信息。呼吸检测主要依赖相位变化,如果不对CFO和SFO做补偿,相位会随时间线性漂移,把真正的呼吸信号淹没掉。我的做法是在数据采集阶段就让收发端共享同一个时钟源,如果做不到,就在后处理阶段用线性拟合去除漂移。
3.2 相位解缠与线性校正的实操细节
CSI的原始相位是包裹在[-π, π]之间的,直接使用会出现跳变。相位解缠是必须的,但解缠算法本身也会引入误差,尤其是在信噪比低的子载波上。
我的处理流程是这样的:
- 对每个子载波的相位序列做解缠,使用基于相邻点相位差的方法,阈值设为π。
- 解缠后,对所有子载波的相位做线性拟合,因为CFO和SFO引起的相位误差在子载波索引上是线性的。
- 从每个子载波的解缠相位中减去拟合出的线性分量,得到校正后的相位。
- 对校正后的相位做滑动平均,窗口长度根据呼吸频率范围确定,通常取0.5-1秒。
这里有个坑:不同子载波的信噪比差异很大。有些子载波恰好落在频率选择性衰落的深谷里,相位噪声极大。如果对所有子载波一视同仁地做平均,反而会降低信噪比。我的做法是先计算每个子载波相位序列的方差,剔除方差最大的20%子载波,再用剩下的做加权平均,权重与方差成反比。
实测下来,这个简单的筛选策略能让呼吸信号的信噪比提升6-10dB,效果非常明显。
3.3 子载波选择与菲涅尔区位置的关联
子载波选择不是随机的。不同频率的子载波对应不同的波长,因而对应不同的菲涅尔区半径。当人体位置固定时,某些子载波的菲涅尔区边界恰好落在人体胸腔附近,这些子载波对呼吸引起的散射变化最敏感。
我做过一个实验:让测试者站在收发连线的不同位置上,记录每个子载波的呼吸信号强度。结果发现,最佳子载波索引确实随位置变化而漂移。这提示我们,如果要做鲁棒的呼吸检测,不应该固定使用某几个子载波,而应该动态选择。
具体做法是:先用一个短时窗口(比如5秒)估计每个子载波的呼吸频带能量,选择能量最高的3-5个子载波作为当前的主子载波,然后每隔一段时间重新评估。这个策略在人员缓慢移动的场景下特别有效。
4. 呼吸信号的提取:滤波、变换与参数估计
4.1 带通滤波器的设计:为什么不能简单用FFT
很多人拿到CSI相位序列后的第一反应是做FFT,然后找0.2-0.3Hz附近的峰值。这个方法在理想条件下能work,但实际场景中有几个问题。
首先,呼吸信号不是严格周期的。人的呼吸频率会在0.15-0.4Hz之间自然波动,而且呼吸深度也不恒定。FFT假设信号在分析窗口内是平稳的,这个假设对呼吸信号来说太强了。窗口太长,频率分辨率高但时间分辨率低,呼吸频率的变化被平均掉;窗口太短,频率分辨率不够,0.2Hz和0.3Hz的呼吸区分不开。
我的做法是用带通滤波器先把呼吸频带之外的成分滤掉,然后用滑动窗口做短时傅里叶变换或者小波变换。带通滤波器的通带设为0.1-0.5Hz,阻带衰减至少40dB。滤波器类型选巴特沃斯,因为它的通带最平坦,不会引入额外的相位失真。
注意:滤波器的阶数不是越高越好。高阶滤波器虽然过渡带更陡,但群延迟更大,而且数值稳定性更差。我一般用4阶,配合零相位滤波(filtfilt)来避免相位偏移。
4.2 基于菲涅尔模型的信号幅度与相位联合估计
单独用幅度或者单独用相位都有局限。幅度对呼吸引起的散射截面变化敏感,但容易受到发射功率波动和天线方向性的影响。相位对距离变化敏感,但容易受到CFO和SFO的污染。
我的方案是联合使用幅度和相位。具体来说,把校正后的CSI表示为复数形式,然后计算相邻时间采样点之间的复数差值。这个差值的模长反映了散射体变化的强度,差值的相位反映了变化的方向。呼吸引起的胸腔起伏是一个周期性的往复运动,所以复数差值的相位会在正负之间周期性地变化。
这个方法的妙处在于,它自动抵消了静态多径分量的影响。静态多径(比如墙壁、家具的反射)在相邻采样点之间是不变的,做差之后就消掉了。剩下的动态分量主要来自呼吸和身体的其他微小运动。
实测中我发现,联合估计比单独用相位或幅度,呼吸频率的估计误差能降低30%以上。尤其是在测试者穿着较厚衣服的情况下,单独用幅度几乎检测不到呼吸,但联合估计仍然能给出可靠的结果。
4.3 呼吸频率与深度的分离估计
呼吸频率和呼吸深度是两个独立的参数,但在信号上它们是耦合的。频率对应的是信号变化的快慢,深度对应的是信号变化的幅度。如果只做频谱分析,你只能得到频率,深度信息被归一化了。
要同时估计两者,需要建立一个参数化模型。我用的模型是:假设呼吸引起的胸腔位移近似为正弦运动,那么CSI的复数差值可以表示为:
ΔH(t) = A * sin(2πf t + φ) * exp(jθ(t))其中A与呼吸深度相关,f是呼吸频率,θ(t)是残余的相位噪声。通过对ΔH(t)的实部和虚部分别做拟合,可以同时解出A和f。
这个模型的局限性在于它假设呼吸是单频的。实际呼吸信号包含谐波,尤其是当呼吸不规则时。我的处理方式是先用这个模型做粗估计,然后用自适应滤波跟踪频率和幅度的慢变化。这样既能保证实时性,又能适应呼吸模式的自然变化。
5. 实测中的意外与调参经验
5.1 多径环境下的菲涅尔区畸变
在空旷环境中,菲涅尔区的形状是规则的椭球体。但在室内环境中,墙壁、地板、天花板的反射会改变场的分布,菲涅尔区会发生畸变。这意味着你在空旷环境中标定好的最佳子载波和最佳人体位置,到了室内可能完全不适用。
我第一次在会议室做测试时就遇到了这个问题。按照空旷环境的计算,测试者应该站在收发连线中点附近。但实际测试发现,站在那个位置时呼吸信号很弱,反而站在偏离连线约1米的位置时信号最强。后来用射线追踪仿真才发现,会议室的金属白板反射形成了一个虚拟源,改变了菲涅尔区的实际分布。
这个经历告诉我:菲涅尔区模型给出的是自由空间下的参考,实际部署时必须做现场校准。校准的方法很简单:让测试者在几个候选位置上各站30秒,记录呼吸信号强度,选最强的位置。
5.2 人体姿态与呼吸信号强度的关系
呼吸引起的胸腔运动在不同姿态下的信号特征差异很大。站立时胸腔运动最自由,信号最强。坐姿时如果靠在椅背上,背部反射会抵消一部分前胸的运动,信号会减弱。躺姿时呼吸运动主要是腹部的起伏,胸腔运动相对较小。
我做过一组对比实验,让同一测试者在站立、坐姿(无靠背)、坐姿(有靠背)、躺姿四种状态下各呼吸2分钟,记录信号强度。结果如下:
| 姿态 | 相对信号强度 | 主要挑战 |
|---|---|---|
| 站立 | 1.0(基准) | 身体晃动引入低频干扰 |
| 坐姿无靠背 | 0.85 | 腿部微小运动引入噪声 |
| 坐姿有靠背 | 0.55 | 背部反射抵消胸腔运动 |
| 躺姿 | 0.70 | 呼吸模式改变,频率偏低 |
这个表格对我的方案设计影响很大。如果应用场景主要是坐姿(比如办公桌前的呼吸监测),就需要特别处理背部反射的问题。我的解决方案是在背部对应位置放置一个吸波材料,减少反射。如果没有条件,就改用腹部运动作为主要检测目标,因为腹部在坐姿下运动相对自由。
5.3 采样率与呼吸信号带宽的匹配
呼吸信号的基频在0.1-0.5Hz,但包含谐波成分,尤其是呼吸不规则时,谐波可以延伸到2-3Hz。根据奈奎斯特采样定理,采样率至少要是最高频率成分的两倍。但实际中我建议采样率不低于20Hz,最好在50Hz以上。
为什么?因为CSI的相位噪声主要集中在低频段,如果采样率太低,相位噪声会混叠到呼吸频带内。高采样率虽然增加了数据量,但给了后处理阶段更多的灵活性来设计数字滤波器。
我试过用10Hz采样率,结果发现呼吸信号的谐波成分被截断了,导致呼吸波形失真,频率估计出现偏差。后来统一用50Hz采样,问题就消失了。数据量确实大了5倍,但对于离线分析来说完全可以接受。如果是实时系统,可以在采集端做降采样,但降采样之前一定要先做抗混叠滤波。
6. 从模型到代码:核心算法的实现要点
6.1 菲涅尔区半径计算的代码实现
import numpy as np def fresnel_radius(freq_ghz, d1, d2): """ 计算第一菲涅尔区半径 freq_ghz: 频率,单位GHz d1: 发射端到障碍物距离,单位米 d2: 障碍物到接收端距离,单位米 返回:第一菲涅尔区半径,单位米 """ c = 3e8 # 光速 wavelength = c / (freq_ghz * 1e9) r1 = np.sqrt(wavelength * d1 * d2 / (d1 + d2)) return r1 # 示例:2.4GHz,收发距离8米,障碍物在中间 r = fresnel_radius(2.4, 4, 4) print(f"第一菲涅尔区半径: {r:.3f} 米") # 输出: 第一菲涅尔区半径: 0.500 米这个函数虽然简单,但在实际部署时非常有用。你可以用它来快速估算最佳的人体位置范围。比如,如果第一菲涅尔区半径是0.5米,那么人体应该站在距离收发连线中点0.3-0.7米的范围内,这样呼吸引起的散射变化最显著。
6.2 相位校正与呼吸信号提取的完整流程
import numpy as np from scipy.signal import butter, filtfilt, find_peaks def unwrap_phase(phase_matrix): """对CSI相位矩阵做解缠,phase_matrix形状为(子载波数, 时间采样数)""" return np.unwrap(phase_matrix, axis=1) def remove_linear_trend(phase_matrix): """去除子载波维度上的线性相位偏移""" n_subcarriers = phase_matrix.shape[0] subcarrier_idx = np.arange(n_subcarriers) corrected = np.zeros_like(phase_matrix) for t in range(phase_matrix.shape[1]): p = phase_matrix[:, t] coeffs = np.polyfit(subcarrier_idx, p, 1) corrected[:, t] = p - np.polyval(coeffs, subcarrier_idx) return corrected def select_best_subcarriers(phase_matrix, fs, band=(0.1, 0.5)): """选择呼吸频带能量最高的子载波""" from scipy.signal import welch n_subcarriers = phase_matrix.shape[0] energies = [] for i in range(n_subcarriers): f, psd = welch(phase_matrix[i, :], fs=fs, nperseg=min(256, phase_matrix.shape[1])) band_mask = (f >= band[0]) & (f <= band[1]) energies.append(np.trapz(psd[band_mask], f[band_mask])) energies = np.array(energies) # 选择能量最高的5个子载波 best_idx = np.argsort(energies)[-5:] return best_idx def extract_breathing(phase_matrix, fs): """完整的呼吸信号提取流程""" # 1. 解缠 unwrapped = unwrap_phase(phase_matrix) # 2. 线性校正 corrected = remove_linear_trend(unwrapped) # 3. 子载波选择 best_idx = select_best_subcarriers(corrected, fs) selected = corrected[best_idx, :] # 4. 加权平均 weights = 1.0 / (np.var(selected, axis=1) + 1e-10) weights = weights / np.sum(weights) combined = np.average(selected, axis=0, weights=weights) # 5. 带通滤波 b, a = butter(4, [0.1/(fs/2), 0.5/(fs/2)], btype='band') breathing = filtfilt(b, a, combined) return breathing这段代码是我实际使用的简化版本。几个关键点:np.unwrap的axis参数要设对,否则会沿着错误的方向解缠;线性校正必须逐时间点做,因为CFO是随时间变化的;子载波选择用PSD积分而不是简单的方差,因为方差会被低频漂移主导。
6.3 呼吸频率估计的峰值检测与频谱细化
拿到呼吸波形后,估计频率有两种主流方法:峰值检测和频谱分析。峰值检测直观但容易受噪声影响,频谱分析鲁棒但频率分辨率受窗口长度限制。
我的做法是两者结合:先用频谱分析找到粗略的频率范围,然后在这个范围内用峰值检测做精细估计。具体来说,对呼吸波形做FFT,找到0.1-0.5Hz范围内的峰值频率f_coarse。然后以f_coarse为中心,取一个包含5个呼吸周期的窗口,用抛物线插值对峰值位置做亚采样精度的估计。
这个方法在呼吸频率缓慢变化时特别有效。我实测过,对于0.2-0.4Hz的呼吸,频率估计误差可以控制在0.01Hz以内,相当于每分钟0.6次的精度,完全满足日常监测需求。
提示:如果要做实时显示,不要每来一个采样点就重新估计频率。用一个长度为30秒的滑动窗口,每隔1秒更新一次估计结果,这样既有足够的时间分辨率,又不会因为窗口太短导致频率估计跳动。
7. 这套方法能走多远:边界条件与扩展方向
7.1 多人场景下的信号分离难题
单人的呼吸检测已经能做得比较可靠了,但多人场景是另一个量级的挑战。当多个人同时处于菲涅尔区内时,接收信号是所有人呼吸引起的散射变化的叠加。如果他们的呼吸频率不同,理论上可以用盲源分离来拆解;如果频率相近,分离就非常困难。
我尝试过用独立成分分析(ICA)来做多人分离,效果取决于呼吸频率的差异程度。当两个人的呼吸频率相差超过0.05Hz时,ICA能较好地分离;当频率差小于0.02Hz时,分离结果就不可靠了。这个局限性来自于ICA的基本假设——源信号统计独立,而相近频率的呼吸信号在统计上不够独立。
一个可能的改进方向是利用空间分集:在接收端使用多根天线,不同天线接收到的多径组合不同,相当于对同一个混合信号做了多次不同权重的观测。这样即使呼吸频率相近,也可以通过空间滤波来分离。我目前正在尝试这个方向,初步结果比单天线ICA有改善,但还没有做到完全可靠。
7.2 运动伪影的识别与抑制
呼吸检测最大的干扰源是身体的其他运动——说话、咳嗽、转头、手臂移动。这些运动引起的信号变化幅度通常比呼吸大一个数量级,如果不加处理,会完全淹没呼吸信号。
我的策略是分两步:先检测运动,再决定是否输出呼吸估计。运动检测用短时能量比:计算0.5秒窗口内的信号能量与之前5秒平均能量的比值,如果比值超过阈值(我设的是3),就判定为运动干扰,暂停呼吸估计,直到信号恢复平稳。
这个简单的策略在实际使用中效果不错,但有一个问题:深呼吸本身也会引起较大的信号变化,可能被误判为运动。我的解决方案是同时检查信号的频率特征——呼吸信号的频率在0.1-0.5Hz,而大多数身体运动的频率要么更低(如缓慢转身),要么更高(如说话时的胸腔振动)。用这个频率特征做二次判断,可以显著降低误判率。
7.3 从呼吸检测到更广泛的生理参数监测
呼吸只是开始。同样的菲涅尔衍射框架和CSI处理流程,经过适当调整,可以扩展到心率检测、体动识别甚至睡眠分期。心率的频率范围在0.8-2Hz,比呼吸高,但引起的胸腔位移更小(约0.1-0.5毫米),对信噪比的要求更高。
我试过用心跳信号做检测,在测试者静坐不动的条件下,确实能在频谱上看到心跳的峰值。但一旦有轻微的身体晃动,心跳信号就被淹没了。所以心率检测对运动伪影抑制的要求比呼吸检测高得多。
一个有意思的扩展方向是呼吸和心率的联合分析。呼吸性窦性心律不齐(呼吸引起的心率自然波动)是一个已知的生理现象,如果能同时检测到呼吸和心率,就可以分析两者的耦合关系,这在情绪监测和自主神经功能评估中有潜在价值。我目前正在收集数据验证这个想法,初步结果看起来有希望,但还需要更多的样本才能下结论。
8. 一些让我少走弯路的实操建议
做这个项目前后花了大概三个月,中间有几次差点放弃。回头看,如果一开始就知道下面这几件事,至少能省下一半的时间。
第一,不要一上来就追求高精度。我最初的目标是把呼吸频率误差控制在0.005Hz以内,结果在数据预处理上花了大量时间,却忽略了更基础的问题——人体位置和姿态对信号质量的影响。后来把精度目标放宽到0.02Hz,先把整个流程跑通,再逐步优化各个环节,效率反而高得多。
第二,菲涅尔区模型是理解问题的框架,不是精确的预测工具。室内环境的反射和散射太复杂了,任何解析模型都只能给出定性指导。真正靠谱的做法是现场校准——花10分钟让测试者在几个位置各站一会儿,记录信号质量,比任何仿真都管用。
第三,相位比幅度重要,但幅度不能丢。我一开始只关注相位,因为理论上相位对距离变化更敏感。但实际中相位受CFO和SFO的影响太大,单独使用经常出问题。后来改成幅度和相位联合使用,虽然算法复杂了一点,但鲁棒性提升非常明显。
第四,采样率宁高勿低。我最初用10Hz采样,后来发现呼吸谐波被截断了。改成50Hz之后,不仅谐波完整了,滤波器的设计也自由多了。数据量大了5倍,但对于离线分析来说完全不是问题。如果是实时系统,可以在采集端做降采样,但降采样之前一定要先做抗混叠滤波。
第五,多准备几套子载波选择策略。固定子载波在实验室里能work,到了实际环境中经常失效。我现在的做法是准备三套策略——基于能量的、基于方差的、基于菲涅尔区计算的——根据场景自动切换或者手动选择。多花一点时间做这个,比事后反复调参要划算得多。