3、PPG信号噪声分析:运动伪影、环境光干扰、基线漂移、工频干扰
做血氧仪最头疼的是什么?不是算法写不出来,而是信号里藏着的各种噪声。我刚开始接触PPG时,天真地以为传感器采回来就能直接用。结果呢?波形乱得像心电图里的房颤,根本没法看。
今天咱们就把PPG信号里的四大噪声挨个扒一遍。你想想看,一个贴在手指上的传感器,要对抗你手的晃动、环境光的干扰、呼吸引起的基线漂移,还有无处不在的50Hz工频。这活儿真不轻松。
3.1 运动伪影——最让人头疼的噪声
运动伪影,简称MA(Motion Artifact)。说白了,就是手指动的时候,传感器和皮肤之间的相对位置变了。光路被破坏,信号就乱了。
我在项目中遇到过最夸张的情况:测试者戴着血氧仪慢跑,PPG波形直接变成了一团乱麻。心率算法算出来的数值,一会儿120,一会儿60,完全没法用。
运动伪影的典型特征:
- 幅度变化剧烈,远大于正常脉搏波
- 频率范围宽,从0.1Hz到10Hz都有
- 与运动模式相关(走路、跑步、手指抖动各有不同)
为什么会这样?因为PPG用的是光电容积描记法。手指动的时候,毛细血管里的血容量变化被破坏了,传感器检测到的光强变化就不再是脉搏波,而是机械运动引起的伪迹。
我个人的处理经验:
- 硬件上:用加速度计同步采集运动信号,做自适应滤波
- 算法上:用滑动窗口的方差检测运动段,直接丢弃或插值
- 实在不行:加一个运动检测标志位,告诉上层「这段数据不可信」
避坑指南:我曾经试过用固定阈值去检测运动伪影,结果发现不同人的运动幅度差异很大。后来改用相对阈值——以当前信号均方根的3倍作为门限,效果好了不少。
3.2 环境光干扰——白天黑夜不一样
环境光干扰,这个其实很好理解。你想想,PPG传感器是光学的,环境光一变化,它采集到的信号就跟着变。
尤其是红光和红外光两个通道,对环境光的敏感度还不一样。我见过一个案例:测试者在窗边晒太阳,血氧饱和度读数直接从98%掉到了92%。其实不是血氧变了,是环境光把信号淹没了。
| 环境光类型 | 影响程度 | 典型频率 |
|---|---|---|
| 日光灯(50Hz/60Hz) | 高 | 100Hz/120Hz(倍频) |
| 白炽灯 | 中 | 100Hz |
| 自然光(太阳) | 低 | 直流分量为主 |
| LED照明 | 中高 | 高频纹波 |
怎么处理?
- 硬件上:用遮光罩、加屏蔽、采用差分检测
- 采样策略:让LED以高频闪烁,采样时只取LED亮时的信号,减去LED灭时的背景光
- 软件上:高通滤波器干掉直流分量,但要注意别把脉搏波的低频成分也滤掉了
注意:环境光干扰往往是周期性的,但频率不固定。比如你在室内走动,头顶的灯光角度在变,干扰信号就跟着变。这时候固定频率的陷波器就不够用了,得用自适应滤波器。
3.3 基线漂移——呼吸带来的麻烦
基线漂移,说白了就是信号的零线在慢慢上下移动。你观察一下PPG波形,会发现整个波形像在「呼吸」一样缓慢起伏。
嗯,这里要注意:这个「呼吸」真的就是呼吸引起的。人呼吸的时候,胸腔压力变化会影响静脉回流,进而改变手指的血容量。这个变化很慢,大概0.2~0.5Hz,正好和脉搏波(0.8~2Hz)挨得很近。
基线漂移的特点:
- 频率低,通常小于0.5Hz
- 幅度变化缓慢,不会突变
- 两个通道(红光和红外)的漂移方向一致
我建议的处理方式是这样的:
// 基线漂移去除 - 高通滤波器示例
// 截止频率 0.5Hz,采样率 100Hz
#define ALPHA 0.969 // 一阶高通滤波器系数
float baseline_removal(float input, float *prev_input, float *prev_output) {
float output = ALPHA * (*prev_output) + ALPHA * (input - (*prev_input));
*prev_input = input;
*prev_output = output;
return output;
}
这个滤波器很简单,但够用。不过要注意:截止频率不能设得太高,否则会把脉搏波的低频成分也干掉。我一般取0.3~0.5Hz之间,具体看你的采样率和信号质量。
小技巧:如果你发现滤波后的波形有畸变,试试用中值滤波代替高通滤波。中值滤波对基线漂移的抑制效果不错,而且不会引入相位失真。我曾在一次可穿戴设备项目中用过,效果出奇的好。
3.4 工频干扰——无处不在的50Hz
工频干扰,50Hz(国内)或60Hz(国外)。这东西就像空气里的灰尘,你躲都躲不掉。电源线、插座、甚至你手机充电器,都会辐射出工频电磁场。
PPG传感器是模拟电路,对这些电磁场特别敏感。尤其是当传感器和皮肤接触不良时,工频干扰会直接耦合进信号里。
工频干扰的表现:
- 波形上能看到细密的锯齿或正弦波叠加
- 频率固定(50Hz或60Hz)
- 幅度可能随时间变化(靠近电源时变大)
处理工频干扰,我常用的方法有几种:
- 陷波滤波器:最直接的方法。设计一个50Hz的带阻滤波器,把那个频率点干掉。
- 自适应滤波:如果你能采集到工频的参考信号(比如直接从电源取一个50Hz信号),用自适应滤波器效果更好。
- 硬件屏蔽:在PCB布局上,把模拟信号线走短、走粗,用地线包围起来。
// 50Hz 陷波滤波器 - IIR 双二阶结构
// 采样率 100Hz,陷波频率 50Hz,Q=30
float notch_50hz(float input, float *state) {
float b0 = 0.9902;
float b1 = -1.8004;
float b2 = 0.9902;
float a1 = -1.8004;
float a2 = 0.9805;
float output = b0 * input + b1 * state[0] + b2 * state[1]
- a1 * state[2] - a2 * state[3];
state[1] = state[0];
state[0] = input;
state[3] = state[2];
state[2] = output;
return output;
}
警告:陷波滤波器虽然好用,但会引入相位延迟。如果你后续要做实时心率检测,这个延迟可能会影响算法的实时性。我建议在非实时处理(比如离线分析)时用陷波器,实时系统里优先考虑硬件屏蔽和自适应滤波。
3.5 噪声的综合处理策略
好了,四种噪声都讲完了。但实际项目中,这些噪声是同时出现的。你不能只处理一种,不管其他的。
我个人的处理流程:
- 先做运动伪影检测,标记出不可信的数据段
- 对可信数据段,用高通滤波器去除基线漂移
- 再用陷波器干掉工频干扰
- 最后用低通滤波器(截止频率5~10Hz)平滑信号
- 如果还有残留的环境光干扰,用自适应滤波器做二次处理
这个流程不是固定的。你想想看,如果测试者躺着不动,运动伪影就很少,那第一步可以跳过。如果测试环境是屏蔽室,工频干扰几乎没有,那第三步也可以省掉。
做信号处理,最忌讳的就是「一刀切」。我见过有人把所有滤波器都堆上去,结果信号是干净了,但脉搏波的形态也变了,血氧饱和度算出来反而不准。
核心原则:能少处理就少处理,能简单就别复杂。滤波器的阶数越低越好,延迟越小越好。你的目标是保留脉搏波的形态,不是把信号变成一条直线。
最后说一句:这些噪声分析,说白了就是「知己知彼」。你只有知道噪声长什么样、从哪里来,才能设计出有效的处理方案。下次咱们聊具体的滤波算法实现,到时候我会拿实际数据出来演示。