尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

毫米波雷达生命体征检测:从FMCW原理到呼吸心率算法实例

毫米波雷达生命体征检测:从FMCW原理到呼吸心率算法实例 做毫米波雷达生命体征检测这件事我踩过不少坑。最开始以为只要把雷达模块接上、用上位机读个波形就行结果从ADC原始数据一路走到呼吸、心率数值中间隔了至少四五个坎。今天这篇先讲原理和第一个实例把“为什么能测”和“怎么一步步算出来”讲透后面再补上多目标、4D雷达和视觉融合这些进阶内容。毫米波雷达测心率呼吸核心不是测距离而是测微动。常规雷达看目标的位置、速度生命体征雷达看的是胸腔表面随心跳和呼吸产生的亚毫米级周期性位移。这种位移太小用普通测距是测不出来的必须借助相位。只要把相位序列提取对了呼吸波形和心跳波形就能分离出来剩下的就是滤波和频率估计。这篇文章适合两类人一是刚接触毫米波雷达、手里有24GHz或60GHz模块但不知道从哪下手的嵌入式工程师二是想复现生命体征算法、但被一堆论文公式卡的开发者。我会按我实际走过的流程来讲涉及参数的部分直接给出计算过程和代码片段。1. 项目整体思路怎么用毫米波雷达“看”心跳和呼吸1.1 雷达生命体征检测到底在测什么人平静坐着的时候胸腔会因为呼吸和心跳产生周期性起伏。呼吸带来的胸腔位移大约 1-12 毫米频率通常在 0.1-0.5Hz也就是每分钟 6-30 次心跳引起的皮肤表面振动位移只有 0.1-0.5 毫米频率在 0.8-3Hz对应每分钟 48-180 次。这两种信号幅度差别很大但频率范围不同。雷达并不直接测“位移有多大”而是测“相位变化有多快”。电磁波打到胸腔表面再反射回来胸腔一动回波的相位就跟着变。这个相位变化非常敏感波长越短同样的物理位移引起的相位变化越大。所以毫米波雷达天然适合干这件事因为它比普通微波雷达波长更短。再说得直白一点雷达生命体征检测本质上就是一个“非接触式振动测量”问题。目标不是静止的而是带着周期性微动。常规测距只能把目标定位到一个距离门里生命体征检测需要的是在选定的距离门上继续做“慢时间维”的相位解析。这一步做完你手里就是一条随时间变化的胸腔振动曲线呼吸和心率全在这条曲线里。1.2 为什么选毫米波从波长算出来的优势先看一个公式。雷达检测到的相位变化 (\Delta \phi) 与胸腔位移 (\Delta R) 之间的关系是[ \Delta \phi \frac{4\pi \Delta R}{\lambda} ]其中 (\lambda) 是雷达信号的波长。如果频率是 24GHz波长约为 12.5 毫米那么 0.3 毫米的心跳位移对应约 0.3 弧度的相位变化如果换成 60GHz波长约 5 毫米同样的位移对应约 0.75 弧度的变化。相位变化越大信噪比越高越容易从噪声里把心跳信号捞出来。这就是为什么早期的生命体征雷达很多用 24GHz也能做但灵敏度有限。后来 60GHz 甚至 77GHz 成为主流很大一部分原因就是为了让微小的胸腔振动产生更明显的相位差。当然频率越高对硬件设计和信号处理的要求也越高成本也更贵。我自己做验证的时候常用 24GHz 模块因为便宜、资料多适合先跑通算法然后再迁移到 60GHz。另外FMCW 雷达的测距公式是 (R \frac{c \cdot f_{IF}}{2S})距离分辨率是 ( \Delta R \frac{c}{2B})。以 24GHz 模块为例扫频带宽如果能做到 250MHz距离分辨率大约 0.6 米。这个分辨率不够区分贴得很近的人但生命体征场景下我们只需要找到目标所在的“距离单元”并不需要精细成像所以仍然够用。4D 毫米波雷达在点云密度上强得多但处理思路完全兼容。2. 核心算法细节从原始ADC数据到呼吸心率曲线2.1 FMCW信号模型与距离FFT绝大多数毫米波雷达模块都是 FMCW也就是发射频率随时间线性变化的调频连续波。回波与发射信号混频后得到中频信号中频信号频率和目标距离成正比。这一步的第一步处理就是“距离FFT”也叫Range FFT。假设一个 chirp 内采样点数为 NADC 采样率为 (F_s)chirp 斜率是 (S)单位 Hz/s那么距离 (R) 对应的中频频率是[ f_{IF} \frac{2 S R}{c} ]对 ADC 采样到的 N 点数据做 FFT就得到距离谱。频谱峰值所在位置对应的就是目标距离。在生命体征检测中这个峰值幅度通常比较明显因为我们关心的目标是人雷达截面积远大于墙壁和家具的慢速杂波。实际处理时要注意几点。第一距离FFT之前先做去直流否则零频附近会有很强的分量干扰后续的相位提取。第二建议加窗比如 Hamming 窗或 Hanning 窗减少频谱泄漏。第三距离FFT的结果是复数不要只取幅度相位信息要留着后面全靠它。距离维 FFT 做出来后我们会从距离谱里找一个合适的距离门。怎么找一般是在一个经验范围里比如 0.5-3 米找幅度最大的那个 bin。这个 bin 就是胸腔反射的位置。之后所有慢时间处理都盯着这个 bin 的复数数据看。2.2 慢时间维相位提取核心突破口距离FFT是对单个 chirp 内的数据做的得到的是某一时刻目标在距离维上的分布。每个距离 bin 的复数都有幅度和相位。如果目标静止幅度基本稳定相位也会稳定。一旦目标有微动这个 bin 的相位就会随微动变化。这里的相位变化才是生命体征信号的来源。我们把每一帧距离FFT中同一距离门上的复数相位取出来组成一个时间序列。这个序列就是“慢时间维相位信号”。关键的一步是相位解缠。由于 atan2 返回的相位被限制在 (-\pi) 到 (\pi) 之间当真实相位跨过边界时序列里会出现 2π 的跳变。如果不做解缠后面滤波和FFT会出现大量高频毛刺。相位解缠的常规做法是当前后两个相位差大于 π 时把后面的相位加上或减去 2π 的整数倍让差值落在 (-\pi) 到 (\pi) 之间。代码实现很简但要小心噪声。如果信噪比太低相位噪声会导致误判这也是为什么很多方案先在距离维上做平均或者加窗。提取出相位序列后数据里已经包含呼吸信号、心跳信号、直流偏置、以及低频漂移。呼吸信号幅度很大频率低心跳信号幅度小频率相对高。接下来要做的就是把这两个频段从相位序列里分离出来。2.3 滤波与频谱分析相位序列本质上是一个时域信号采样率就是雷达的帧率。帧率不能太低否则高频的心跳信号会被混叠。心率最高 3Hz按奈奎斯特定理帧率至少 6Hz但实际做的时候我会把帧率控制在 20-50Hz。这样做一方面减少混叠另一方面可以让后续FFT的频率分辨率更好。分离呼吸和心跳最常见的手段是带通滤波器。设计两个滤波器一个呼吸通道带通范围 0.1-0.5Hz一个心电通道带通范围 0.8-3Hz。滤波之后两条干净的波形就出来了。如果你希望看到实时波形可以在界面上同时画呼吸波形和心跳波形如果只需要数值再做一次FFT找峰值。频谱分析时频率分辨率由帧率和FFT点数决定。频率分辨率 帧率 / FFT点数。比如帧率 20Hz做 600 点FFT分辨率约为 0.033Hz换算成每分钟也就 2bpm勉强够用。想要更高精度可以积累更长时间的数据比如 30 秒或者用补零和插值的方法细化峰值位置。相位序列还有一个特点是幅度与调制灵敏度有关。为了消除雷达增益和距离带来的影响通常会对相位序列做归一化或去趋势但频率估计不受幅度影响所以实际项目中我一般只关心频率。3. 实操过程基于24GHz模块的C实现3.1 硬件与参数配置我最早用的是一块很常见的 24GHz FMCW 雷达模块具体型号不多说核心参数类似一发一收扫描带宽 250MHzchirp周期约 200 微秒每帧 128 个chirp或者更多帧率可以配置。模块通过串口输出经过简单预处理后的ADC数据也可以直接读取原始I/Q数据。开始之前先把雷达配置参数列清楚否则后面代码没法写。我用表格整理一份典型配置参数数值说明中心频率24GHz波长约12.5mm扫频带宽 B250MHz距离分辨率约0.6mADC采样率 Fs4MHz实际可根据量程调整chirp内采样点数 N512距离FFT点数一帧chirp数1单通道生命体征只需一个chirp/帧帧率20Hz慢时间采样率检测距离范围0.3m - 5m由IF带宽决定注意生命体征检测并不需要像测角或成像那样多个chirp组成帧每帧有一个chirp就够用。帧率才是关键。很多模块默认配置成每帧128个chirp是为了多普勒测速直接拿来用会发现慢时间采样率低得可怜这是新手最容易踩的坑。3.2 数据采集与预处理采集流程不复杂每个chirp采样N点ADC数据组成一帧然后帧率保持在20Hz。读取数据后先做去直流再丢进距离FFT。这里有一个很重要的点如果ADC输出的是实信号FFT之后频谱是双边带的我们只关心正频率部分。为了让相位提取简单最好使用I/Q采样或者直接用复基带数据。实信号会带来镜像频率干扰相位解缠。去直流的做法也很简单对一组ADC数据求平均值然后减去这个平均值。但还要注意如果雷达模块有高通滤波器它可能已经把直流去掉了这时候不要重复去直流反而可能破坏相位信号。所以拿到新模块第一步是读原始数据看波形再决定预处理流程。预处理完毕后每帧得到一个复数向量也就是距离Profile。假定rangeProfile是一个长度为N的复数数组我们在这个数组里找幅度峰值。但直接在全范围找峰值容易被墙壁强反射带偏所以我会限定一个检测区间比如从第4个bin到第40个bin对应大约0.3-2.5米。选好目标bin后把该bin的复数值存到一个慢时间向量里。接下来对这个向量做相位解析先取实部虚部计算相位然后做解缠。3.3 C FFT关键代码与参数计算距离FFT和后续频谱分析都可以用现成的FFT库比如FFTW或KissFFT。如果不依赖第三方库自己写一个基2的FFT也不难但工程上我建议直接用FFTW性能稳定。下面给出一个核心处理流程的C代码片段展示从ADC数据到相位序列的完整过程。#include iostream #include vector #include complex #include cmath #include algorithm using Complex std::complexdouble; // 加Hamming窗 void applyWindow(std::vectorComplex data) { int N data.size(); for (int i 0; i N; i) { double w 0.54 - 0.46 * std::cos(2.0 * M_PI * i / (N - 1)); data[i] * w; } } // 简单基2迭代FFT仅用于演示实际建议用FFTW void fft(std::vectorComplex a, bool invert false) { int n a.size(); for (int i 1, j 0; i n; i) { int bit n 1; for (; j bit; bit 1) j ^ bit; j ^ bit; if (i j) std::swap(a[i], a[j]); } for (int len 2; len n; len 1) { double ang 2.0 * M_PI / len * (invert ? 1 : -1); Complex wlen(cos(ang), sin(ang)); for (int i 0; i n; i len) { Complex w(1.0, 0.0); for (int j 0; j len / 2; j) { Complex u a[i j]; Complex v a[i j len / 2] * w; a[i j] u v; a[i j len / 2] u - v; w * wlen; } } } if (invert) { for (auto x : a) x / n; } } // 相位解缠输入原始相位序列输出连续相位 std::vectordouble unwrapPhase(const std::vectordouble phase) { std::vectordouble unwrapped(phase.size()); if (phase.empty()) return unwrapped; unwrapped[0] phase[0]; for (size_t i 1; i phase.size(); i) { double d phase[i] - phase[i - 1]; if (d M_PI) d - 2.0 * M_PI; if (d -M_PI) d 2.0 * M_PI; unwrapped[i] unwrapped[i - 1] d; } return unwrapped; } int main() { const int N 512; // chirp采样点数 const int totalFrames 600; // 30秒 20Hz const double frameRate 20.0; // 帧率20Hz std::vectordouble slowPhase(totalFrames); for (int frame 0; frame totalFrames; frame) { // 1. 读取一帧ADC数据 std::vectorComplex adc(N); // readAdcFrame(adc); // 具体接口取决于你的驱动 // 2. 去直流 Complex dcSum(0.0, 0.0); for (int i 0; i N; i) dcSum adc[i]; dcSum / (double)N; for (int i 0; i N; i) adc[i] - dcSum; // 3. 加窗 距离FFT applyWindow(adc); fft(adc, false); // 4. 在有效距离门范围内找幅度最大的bin int startBin 4; int endBin 40; double maxMag 0.0; int targetBin startBin; for (int i startBin; i endBin; i) { double m std::abs(adc[i]); if (m maxMag) { maxMag m; targetBin i; } } // 5. 提取该bin的相位 slowPhase[frame] std::arg(adc[targetBin]); } // 6. 相位解缠 std::vectordouble phaseUnwrapped unwrapPhase(slowPhase); // 7. 后续带通滤波、FFT找峰值计算bpm // ... return 0; }上面代码里最需要注意的是距离FFT之后的bin索引。由于FFT结果是双边带的正频率部分对应距离范围是 0 到 (F_s/2)。如果你用的是实信号镜像会占掉一半检测距离也会减半。所以要么改用I/Q采样要么只取前一半频谱再折算距离。这一点在模块选型时要提前想清楚。滤波部分我用的是在线IIR滤波器两个通道分别提取呼吸和心跳。实际处理时IIR滤波器阶数不宜太高否则相位延迟会让实时波形变得很难看。另外一个容易忽略的点是滤波器启动阶段会有瞬态最好先丢弃开头约1秒的数据。4. 实测中的数据表现与调参记录4.1 距离门选择与静态杂波抑制第一次拿到真实数据时我恐怕连续调了两天才稳定。问题不在算法而在距离门的“选点”上。人体座位后面如果有一堵强反射墙距离FFT的峰值可能落在墙上而不是人身上。墙壁没有微动它的相位是死的但人的反射信号混在同一个range profile里。我后来做了两件事一是限制目标搜索范围把最常见的座椅位置标出来二是做静态杂波对消也就是对慢时间序列减去一个滑动平均。静态杂波对消的原理很简单。墙和家具的反射基本不随时间变化慢时间维上它们的幅度和相位几乎恒定。对slowPhase序列做一个移动平均然后从原始序列中减掉这个平均值就能把静态反射的直流分量去掉。但要注意这种减法也可能削弱呼吸信号因为呼吸信号本身就是低频的。所以不能全用而是只对距离FFT之前的复数数据做静态对消或者在提取相位前用复数域做而不是在相位域直接减。还有一个经验检测静止的人体时人体的反射信号其实在好几个相邻距离门里都有能量。与其只挑一个bin不如把目标bin附近3-5个bin的复数取幅度加权平均。这样相位信噪比更高波形更平滑。我实测下来单bin提取出的呼吸波形有时候会出现毛刺多bin融合后明显好了很多。4.2 呼吸谐波对心跳检测的干扰与解决很多人会以为呼吸和心跳频率差得远用带通滤波分开就行。实际上呼吸信号的谐波是个大麻烦。呼吸波形不是标准正弦它带有大量谐波成分三次谐波可以到 0.9-1.5Hz正好落在心跳频带内。尤其是儿童或者呼吸急促的受试者呼吸谐波会直接把心跳信号淹没。解决这个问题的思路有几个。一是用更高阶带通滤波器抑制频率泄漏但这只能缓解不能根治。二是用谐波消除算法。既然呼吸基波频率可以估计出来就可以做一个梳状滤波器把呼吸基波的整数倍频率成分陷掉。三是从源头上改善信号质量让受试者尽量平稳但这在应用中不现实。我在实际项目中用了一个比较轻量的策略先提取呼吸通道的峰值频率然后心电通道FFT做峰值搜索时把呼吸基频的整数倍频率对应的区间排除掉在剩余谱里找最大值。这样做在大部分场景下都能有效避免误判。如果谐波非常强再上自适应滤波器用呼吸参考信号去做最小均方误差估计把呼吸干扰从心电通道里减掉。调参记录里比较有用的数值是带通滤波器的中心频率不是固定的可以根据上一帧估计的呼吸、心率动态移动。比如呼吸通道滤波器中心频率设为0.2Hz带宽0.2Hz心跳通道中心频率设为1.2Hz带宽1.5Hz。这种自适应方案比固定通带更抗干扰。4.3 帧率和FFT点数怎么配这里再单独说一个参数问题。很多人问帧率是不是越高越好。帧率高了慢时间采样率高抗混叠好但数据量和功耗上来了。对生命体征这种慢信号来说20Hz帧率已经足够。如果你希望FFT精度高一点可以拉长观测时间比如用30秒数据做FFT频率分辨率大约0.033Hz能分辨2bpm的变化。如果实时性要求高用滑窗方式每2秒更新一次虽然分辨率低一些但趋势够用。FFT点数要在频谱分辨率和计算量之间权衡。我看到有同学直接把600帧全部丢进FFT结果频率轴密密麻麻找峰值反而困难。更好的办法是先对相位序列做去趋势和加窗然后做FFT再用抛物线插值细化峰值位置。插值公式很简单取峰值和相邻两点做抛物线拟合能把频率精度提升一个量级。另一个容易被忽略的点是FFT前要不要做带通滤波。我之前图省事直接对原始相位序列做FFT结果0Hz附近的巨大直流分量把整个频谱都压扁了心跳峰值根本看不到。所以顺序一定是相位解缠 - 带通滤波 - 加窗 - FFT。滤波不仅是为了分离频带也是为了把直流和低频漂移干掉。5. 常见问题与排查技巧实录5.1 五个高频问题排查我在调试过程中把最常遇到的问题整理成了一个速查表分享出来现象可能原因解决办法呼吸波形是好的但心跳检测不到心跳幅度太弱或呼吸谐波压制使用60GHz模块多距离门融合在频谱中排除呼吸谐波距离FFT峰值不停跳动目标移动或选门范围太宽限制搜索范围增加静态杂波对消对目标bin做平滑相位解缠后波形出现剧烈毛刺信噪比不足或相位跳变误判增加距离门平均提高发射功率使用复基带数据频谱峰值总是0.1Hz以下目标没对准或过于靠近反射物检查距离门让人体在雷达正前方调整天线角度实时波形滞后严重滤波器阶数过高或数据窗口太长降低IIR滤波阶数缩短FFT窗长用滑窗而非整段处理第一个问题最常见。我一开始用24GHz模块心跳信号几乎被呼吸谐波淹没后来把检测距离压低让胸部正对雷达再对3个距离门做幅度加权融合波形才明显起来。频率越高的雷达越省力60GHz在同样条件下心跳信号比24GHz强不少。第二个问题的解决思路是雷达目标检测的通用做法加一个“跟踪”环节也就是在距离维上记录上一帧的峰值位置当前帧在这个位置附近找峰值。这样即使偶尔有强反射点跳到别的bin也不会让目标门突然切走。另外关于相位解缠的毛刺有一个不起眼但很有效的技巧在提取相位之前对慢时间复数序列做滑动平均。比如对每个距离门上的复数向量做长度为5的均值滤波相位噪声会被明显压制。代价是相当于在做低通滤波可能会稍微模糊高频的心跳信号但利大于弊。5.2 技巧4D雷达与摄像头时空同步的扩展前面讲的是单雷达通道的生命体征检测实际项目里经常会遇到多传感器融合的需求尤其是4D毫米波雷达和摄像头做目标时空同步再用雷达点云锁定目标同时提取呼吸心率。这里的“时空同步”有两个层面。时间同步是指雷达帧和摄像头帧必须对齐。最简单的办法是硬件触发用同一个PPS或者帧同步信号触发两种传感器采集。没有硬件触发时可以记录每个传感器的时间戳用软件插值对齐到统一时间轴。空间同步则是把雷达点云从雷达坐标系投影到相机像素坐标。4D雷达的点云包含距离、方位角、俯仰角和速度标定后可以算出每个目标在图像上的位置。做生命体征融合时视觉主要提供目标的身份和姿态信息比如判断目标是不是坐着的、胸部是否正对雷达。雷达则负责提供精确的微动信号。两者同步后可以让毫米波雷达波束指向视觉检测到的目标区域在多目标场景下非常有用。4D雷达的优势在于点云密集可以把目标身体的多个反射点区分开。对一个人的胸腔能在点云上聚类出胸部区域然后对聚类内的多个点同时做相位分析进一步增强心跳信号。这部分算法比单雷达复杂但思路仍然沿用我前面讲的距离FFT加慢时间相位提取的框架只是把单bin换成点云聚类后的多批数据。5.3 实测前必做的环境检查清单最后给一份我自己写代码前必查的清单省得在错误方向上调半天雷达前面的区域是否空旷有没有风扇、空调出风口、窗帘这类周期性运动物体人体与雷达的距离是否在有效检测范围内24GHz模块建议0.5-2米。人体是否正对雷达侧身会大幅减小胸腔振动投影。帧率是否稳定有些模块在USB传输繁忙时帧率会抖动需要采集端打时间戳。雷达天线极化方向是否和人体目标匹配水平极化有时比垂直极化更好。是否有强Wi-Fi或其它雷达信号干扰24GHz频段周围电磁环境相对干净但也要留意。这些检查每一条都是我用时间换来的。环境里如果有个风扇在转它的叶片反射会在距离FFT里形成一个周期性变化的强点很容易被当成人体目标。遇到这种情况即使算法再好也会测出错误结果。我个人在实际项目中的体会是毫米波雷达生命体征检测80%的功夫在信号调理和参数适配20%在AI或数学模型。把距离FFT、相位解缠、带通滤波和FFT峰值估计这几板斧练熟就能解决绝大多数场景。每次拿到新雷达模块我都会先用一段固定脚本采集数据把距离谱、相位波形、频谱图画出来看一眼再跑后面算法。这个方法帮我省了大量调试时间。后续有机会我准备把呼吸心跳联动的谱图分析、以及60GHz更高频率下的相位噪声抑制再展开写一篇这次的代码和参数都可以作为起点。
返回列表