简介:面向需要处理同步脉搏与心电信号的生物医学工程学习者,这份压缩包提供了可直接运行的PPG与ECG预处理及特征提取方案。代码基于NeuroKit2库完成去噪,并计算潮波幅值比h2/h1、重搏波幅值比h4/h1、收缩/舒张面积比S1/S、S2/S及主波高度、波形周期面积、脉搏传导时间PWTT等指标;其中PWTT通过ECG与PPG之间渡越时间计算,适合开展无创血压、动脉硬化等方向的前期研究。包内共43个文件,主体为29个txt信号数据集,另含2个Python脚本、8个ini配置与4个bak备份文件,压缩后仅4.31MB,轻量易部署。目前该资源已有744人学习下载,适合希望在真实数据上复现预处理流程、快速提取多类脉搏特征的研究者参考。
1. PPg-ECG预处理及特征提取:可穿戴健康监测里最难啃的一段数据管道
在可穿戴设备、临床监护和运动健康研究里,PPG和ECG经常成对出现:ECG给出精准的心脏电活动,PPG给出外周血流灌注信息,两者结合能算出脉搏波传导时间、心输出量趋势、血管弹性这类单信号拿不到的指标。但这个组合的落地成本远比想象中高——PPG对运动伪影和皮肤接触压力极其敏感,ECG又容易被肌电和工频干扰污染,两路信号在采集端的噪声机制完全不同,拿到原始波形直接提特征,特征里一半是“设备的脾气”而不是人体的生理状态。这篇文章适合那些手里已经有一批同步采集的PPG-ECG数据、下一步要做心率变异性分析或训练分类模型的人,把从原始波形到干净特征之间的每一道工序讲透,包括滤波器参数怎么定、时间对齐怎么做、哪些特征真的稳定可复现。
2. 两个信号的物理本质不同,决定了预处理思路不能共用
2.1 PPG是光学信号,ECG是电学信号,噪声源完全不一样
PPG的原理是光电容积脉搏波描记:LED发出的光穿透或反射出组织后,由光电二极管接收,血容量的脉动变化会调制光的吸收强度。这个原理决定了它对探头压紧程度、皮肤颜色深浅、组织灌注状态、甚至手部微动作都高度敏感。常见可穿戴设备里PPG采样率一般在25到100 Hz之间,信号主要能量集中在0.5到5 Hz,但运动伪影的能量往往也落在这个频段,这给后面的去噪埋了一个大坑。
ECG则是记录心肌细胞去极化和复极化的电偶极子在体表产生的电位差,信号幅度只有0.5到4 mV,频谱范围从0.05 Hz到100 Hz以上。ECG的问题在于工频干扰(50/60 Hz)、肌电噪声(20到500 Hz,频谱和QRS波有重叠)以及电极松动导致的基线漂移。临床诊断级ECG通常需要500 Hz采样率,以保证QRS波的时限测量和ST段分析有足够的时间分辨率。
这两个信号在预处理阶段必须分开对待:PPG的核心矛盾是“运动伪影与真实脉搏信号频段重叠”,ECG的核心矛盾是“工频干扰和肌电污染叠加在低频心电成分上”。我见过有人图省事,把ECG那套带通滤波参数直接套到PPG上,结果PPG的舒张期波峰被削平,算出来的脉搏波传导时间整体偏移了几十毫秒。
2.2 联合采集时的时间对齐:采样率差异和硬件延迟比想象中麻烦
同步采集设备通常会让PPG和ECG使用同一个时钟源,但模拟前端的抗混叠滤波器和ADC转换会引入群延迟,这个延迟对不同频率成分还不一样。如果设备手册没有给出明确的通道延迟补偿参数,拿到数据后第一步就要做对齐校验。
一个实用的对齐方法是用心跳事件做基准:ECG的R波峰值和PPG的波峰之间有生理上固有的时间差(脉搏波传导时间,一般在50到200 ms之间),但二者应当是逐拍对应的。先分别做R波检测和PPG峰值检测,得到两列心跳时间戳,计算相邻心跳间隔的相关性。如果相关系数低于0.9,说明存在拍级错位;如果两条心率曲线延迟了固定样本数,则需要做固定偏移补偿。
2.3 信号质量评估:先把坏段挑出来再谈预处理
很多预处理流程一上来就滤波,这是顺序上的错误。滤波只能抑制频带外噪声,对于接触不良、剧烈运动、传感器脱落造成的平坦段或饱和段,任何滤波器都无能为力。正确的做法是在滤波前先做信号质量评估(SQI),把不可用的数据段标记出来,后续只在质量合格的片段上做特征提取。
我一般用三类SQI:一是时域幅度范围检查,PPG信号幅度在有效时段内应当有周期性波动,若超过5秒没有明显的脉动波峰,直接标记为坏段;二是模板相关性检查,将最近10秒信号和自适应更新的标准脉搏波模板做相关系数计算,低于0.6的片段视为受运动干扰;三是相邻心跳间隔合理性检查,基于心率生理范围设置上下限,比如30到200 bpm,超出范围的时间戳需要复核。
提示:信号质量评估的值不要直接丢弃,它本身是一类重要特征。很多睡眠分期模型里,SQI的分布特征对区分清醒/浅睡/深睡有直接的判别力。
3. 预处理流水线:从原始波形到干净曲线的完整步骤与参数
3.1 第一步:滤波和去噪的参数整定,分通道差异化处理
ECG通道的重点是保留QRS波和ST段信息,同时压掉肌电和工频。常见做法是零相位带通滤波,带宽选0.5到45 Hz,对50/60 Hz再加一个窄带陷波器。这里有一个容易被忽视的细节:如果用IIR滤波器,必须用filtfilt做零相位滤波,否则群延迟会造成QRS波位置偏移几毫秒,心率变异性分析对时间精度要求极高。0.5 Hz低切是底线,再高会压低T波和ST段。
PPG通道的滤波逻辑完全不同:它的有用频段是0.5到5 Hz之间。低切0.1 Hz加上高切8到10 Hz的带通是常见选择,但运动伪影往往恰好落在0.5到2 Hz区间,这导致单纯滤波并不能彻底解决问题。先看一个基础的PPG滤波实现:
import numpy as np from scipy.signal import butter, filtfilt, iirnotch def bandpass_filter(data, fs, lowcut, highcut, order=4): """零相位带通滤波器,避免IIR滤波器造成的时间偏移""" if lowcut <= 0: raise ValueError("低切频率必须大于0,直流分量不能通过高通") nyquist = 0.5 * fs low = lowcut / nyquist high = highcut / nyquist b, a = butter(order, [low, high], btype='band') # 用 padlen 和 filtfilt 消除相位延迟 padlen = min(3 * max(len(a), len(b)), data.shape[-1] - 1) filtered = filtfilt(b, a, data, padlen=padlen) return filtered fs = 125 # 常见可穿戴设备PPG采样率 ppg_raw = np.random.randn(fs * 10) # 示例数据 # 实际处理时用真实采集数据替代 ppg_filtered = bandpass_filter(ppg_raw, fs, lowcut=0.5, highcut=8.0, order=4)这里有一个血泪经验:order不要超过4。滤波器的阶数越高,过渡带越窄,但通带纹波和相位非线性越严重,PPG波形的波峰位置会被非线性拉伸,导致脉搏波传导时间计算出系统性偏差。滤波器的输出要和原始信号做一次差值对比,正常情况下差值应在一定范围内,如果出现明显的高频振荡,说明截止频率设定过严或滤器阶数过高。
3.2 第二步:PPG运动伪影去除,用自适应滤波补偿频段重叠问题
PPG运动伪影去除是这个领域的研究重点,工程上可落地、不依赖外部设备的情况下,最可靠的方案是自适应滤波。自适应滤波的原理是:利用一个与运动噪声相关的参考信号(可穿戴设备里的加速度计信号,或者通过独立成分分析从多路PPG中分离出的噪声分量),不断调整滤波系数,让参考信号经过加权后逼近噪声,再从原始信号中扣除。
最常用的自适应算法是LMS(最小均方)和RLS(递归最小二乘)。LMS计算量小,适合嵌入式部署;RLS收敛更快,但计算复杂度高,适合离线处理。实现时需要注意学习步长的选择,过大会导致滤波器发散,过小则收敛缓慢。一个简化版的LMS自适应滤波实现如下:
import numpy as np def lms_adaptive_noise_canceller(signal, reference, mu=0.01, taps=16): """ 信号清理:signal - reference * w signal: 含噪的PPG信号 reference: 加速度计信号或噪声参考 mu: 步长,取值过大容易发散,实践中从0.01开始调 """ n_samples = len(signal) w = np.zeros(taps) output = np.zeros(n_samples) for i in range(taps, n_samples): ref_block = reference[i-taps:i] # 误差计算:从含噪信号中减掉参考信号对噪声的估计 estimated_noise = np.dot(w, ref_block) error = signal[i] - estimated_noise output[i] = error # LMS权重更新 w = w + 2 * mu * error * ref_block # 权重约束:防止滤波器系数漂移过大 w = np.clip(w, -1.0, 1.0) return output这段代码的关键是mu的选取。mu太大,滤波器的权重更新就会震荡,输出的波形里会出现周期性的噪声脉冲;mu太小,滤波器需要很长时间才能跟上运动状态的变化。我的经验是先用一段人为标注了运动干扰的数据做测试,从0.01开始调,观察输出波形的平滑度和心率检测的准确率,找到合适的值后再固定下来。
另外说一个容易忽略的点:参考信号的质量直接决定自适应滤波的上限。如果参考信号本身和噪声相关性很弱(比如加速度计没贴紧外壳,测不到真实的运动频谱),那么自适应滤波不仅去不掉噪声,反而会把有用的脉搏波成分当作噪声减掉。离线分析的时候,我会先用互相关计算参考信号和目标噪声的相关系数,低于0.4就换一种去噪方案。
3.3 第三步:重采样与插值,统一时间轴是特征提取的前提
PPG和ECG即使在同一设备中采集,采样率也可能不同——很多可穿戴设备PPG是25 Hz,ECG是250 Hz。特征提取前必须统一时间轴。常见做法是以高采样率的ECG时间轴为基准,把PPG重采样到和ECG相同的采样率。重采样的过程中要考虑抗混叠,直接用线性插值会引入高频噪声,先用低通滤波器把PPG限制在奈奎斯特频率以下,再插值。
对于数据缺失的片段(传感器瞬间脱落),不要用线性插值去填补,因为线性插值会在波形上制造尖角,后续的频谱分析会被这些尖角污染。我一般用三次样条插值,并且在插值后的片段开头和结尾各留出一段过渡区,标记为低置信度区域,特征提取时跳过这些区域。
下面是重采样和插值的参考实现:
from scipy.interpolate import CubicSpline def resample_ppg_to_ecg(ppg_signal, ppg_fs, ecg_fs, missing_mask=None): """ 将PPG信号重采样到ECG采样率 missing_mask: 标记坏段的布尔数组,True表示该样本缺失 """ old_time = np.arange(len(ppg_signal)) / ppg_fs new_time = np.arange(int(len(ppg_signal) * ecg_fs / ppg_fs)) / ecg_fs if missing_mask is None: missing_mask = np.zeros(len(ppg_signal), dtype=bool) # 缺失段标记为NaN,插值后这些位置数值不可信 ppg_work = ppg_signal.copy().astype(float) ppg_work[missing_mask] = np.nan cs = CubicSpline(old_time[~missing_mask], ppg_work[~missing_mask]) ppg_resampled = cs(new_time) # 重采样后的缺失区域重新标记 new_missing_mask = np.interp(new_time, old_time, missing_mask.astype(float)) > 0.5 return ppg_resampled, new_missing_mask有一个重要原则:重采样的核心不是提高采样率本身,而是保证PPG和ECG峰值检测的时间戳精度对齐。如果你打算做脉搏波传导时间计算,两个信号的时间轴误差必须控制在1 ms以内,这意味着重采样时不能简单四舍五入取整,要保留浮点时间戳。225 Hz的PPG重采样后时间戳精度大约在4.4 ms,这个精度对于PTT计算是不够的,我一般会用插值到至少500 Hz再做峰值检测。
3.4 第四步:逐跳分割和峰值检测,把连续波形切成单个周期
完成信号清理和时间对齐后,下一步是逐跳分割。ECG端用Pan-Tompkins算法或更简单的自适应阈值法检测R波,PPG端则检测每个周期的波峰和舒张谷。峰值检测的精度决定了后续所有特征的质量——心率变异性指标里相邻心跳间隔的误差只要超过10 ms,SDNN的高频分量就会被显著低估。
PPG的峰值检测比ECG麻烦得多。因为PPG波形受呼吸调制、外周血管张力和运动残留等因素影响,波峰形态会受到干扰,一个最常见的问题是反射波干扰——当血管弹性好时,反射波可能接近甚至超过主波峰,造成误检。解决方法是加一个最小的峰间隔约束,并做模板匹配:取最近20个心跳的平均形态作为模板,每次检测结果和模板的相关系数低于0.7就认为该峰可疑,标记为待复核。
4. 特征提取:从干净波形里拿出能放进模型的那组数
4.1 时域特征:均值、方差、和那些容易被忽略的波形形态参数
时域特征是最容易想到的一类,但不同特征的稳定性差别很大。均值心率、心率标准差这类统计量适合做长时段分析;而逐拍的RR间期和PPG峰值间期则适合做短时变异性分析。这里有一个坑:直接用原始波形的均值方差作为特征,会隐含信号质量的干扰。如果某段数据的噪声水平较高,处理后残余的伪影方差会压过生理方差。
推荐一组对质量不敏感时域特征:RR间期标准差(SDNN)、相邻RR间期差值的均方根(RMSSD)、RR间期差值大于50 ms的百分比(PNN50)、以及PPG波形的主波峰幅度变异系数。前三个是HRV时域分析的经典指标,反映的是自主神经张力,和绝对波形幅度无关。第四个反映外周血流灌注的稳定性,但受传感器佩戴压力影响较大,只适合在固定佩戴条件下比较同一个体的纵向变化。
4.2 频域特征:功率谱密度、频带能量和LF/HF比值
频域特征需要先估计功率谱密度。常用的方法是Welch法,它通过对分段加窗的信号做FFT再把各段结果平均,能有效降低频谱估计的方差。分段长度和窗函数的选择会直接影响结果:分段太长,低频分辨率高但时间分辨率差;分段太短,低频段的估计方差大。我一般用4秒窗长、50%重叠,频率分辨率大约0.25 Hz,这对LF(0.04-0.15 Hz)和HF(0.15-0.4 Hz)的区分是足够的。
频带能量的计算要归一化,否则不同受试者的总功率差异会掩盖频段比例的变化。计算LF功率占LF+HF总功率的比例比单独用LF的绝对值更稳定,原因在于心率变异性的总功率受个体基础心率影响很大,而频段比例的生理意义更明确:LF/HF比值被大量研究用来反映交感/迷走神经张力平衡。
4.3 非线性特征和HRV指标:样本熵、近似熵的适用边界
非线性特征这几年很受欢迎,尤其在睡眠分期、压力识别和疲劳状态检测场景中。样本熵(Sample Entropy)是最常用的一个,它度量时间序列的规则性,数值越高表示信号越不规则。计算样本熵有两个关键参数:嵌入维数m和相似容限r。m一般取2,r取信号标准差的0.1到0.25倍。这两个参数对结果影响非常大,不同研究中取值不统一导致结果可比性差,因此在使用时必须在论文或报告里注明参数。
样本熵对信号质量极其敏感,运动伪影产生的尖峰会让熵值急剧升高,从而掩盖真实的生理变化。所以非线性特征不应该单独使用,而要和SQI特征配合,只对高质量片段计算,并且计算前建议做一次精细的去脉冲处理——用中值滤波剔除单个异常采样点,这些点对熵值的贡献远大于对统计均值的影响。
特征提取的一个关键思路是:不要只提取单尺度的特征,不同生理状态的信息分布在不同的时间尺度上。心率变异性的超低频分量(0.003-0.04 Hz)反映体温调节和肾素-血管紧张素系统的活动,这部分信息包含在超过5分钟的长时段记录里。如果数据长度不够,明确放弃超低频特征,而不是强行用加长窗长的Welch估计去拼凑,后者会产生大量不可靠的估计值。
5. 避坑指南:PPG-ECG预处理和特征提取的五个常见翻车点
5.1 滤波造成波形畸变,R波和PPG波峰位置系统性偏移
现象:滤波后的信号看上去很平滑,但做峰值检测时R波位置和原始信号里的明显尖峰位置有几十毫秒的偏移,心率变异性指标计算出来后与实测不符。
原因:使用了非零相位的IIR滤波器,或者滤波器阶数过高。IIR滤波器是递归结构,相位响应是非线性的,不同频率成分在时间轴上被延迟的量不同,这导致波形形状改变的同时峰位移动。
解决:滤波器一律使用scipy.signal.filtfilt做零相位滤波;降低滤波器阶数到4阶以内;对比滤波前后的峰值位置差,如果超过一个采样周期,说明相位畸变未消除。
5.2 时间对齐只做了固定样本平移,忽视了频率相关的群延迟
现象:ECG和PPG的心跳事件能对上,但脉搏波传导时间的计算结果在心率变化时出现阶梯状跳变。
原因:模拟前端的抗混叠滤波器对不同频率成分的延迟不同,固定样本数平移只能补偿某个频率点的延迟。心率为60 bpm时,PPG峰值的能量集中在1 Hz附近;心率为120 bpm时移到2 Hz附近,群延迟随之变化。
解决:用已知频率的正弦波测试信号对采集链做延迟标定,建立延迟-频率曲线;或者逐拍计算PPG峰值与对应R波的相对时间差,并检查该差值是否随心率变化,若变化则采用基于插值的时变对齐。
5.3 直接在坏段上提取特征,把伪影当成了生理信号
现象:某些受试者的特征显著偏离生理范围,比如LF/HF比值高达几十,核查发现是有运动伪影片段混入后频谱被污染。
原因:预处理阶段没有提前标记坏段,滤波后部分伪影仍然保留,而这些片段恰恰产生极端的特征值。一个时长为总数据5%的坏段可以让SDNN抬高20%以上。
解决:在滤波前运行SQI评估,生成一个坏段掩码,特征提取循环遍历有效段并跳过掩码区域;同时记录坏段占比作为独立特征,坏段比例超过50%的样本在建模时直接剔除。
5.4 HRV特征对窗口长度极其敏感,但很多人用一个窗长打天下
现象:用30秒窗口算SDNN得到15 ms,换5分钟窗口算同一段数据SDNN变成45 ms,模型在不同窗口配置下结论相反。
原因:SDNN本身受记录时长影响——超低频分量需要长时间记录才能完整展现在方差里。30秒的短窗只有3到5个心拍,SDNN估计方差极大。
解决:根据目标生理成分选择窗长。分析高频变异性(呼吸性窦性心律不齐)可以用1到2分钟窗口;分析低频和超低频成分至少用5分钟,理想情况是24小时。在发表的结论里明确标注窗长和采样率。
5.5 特征之间的共线性没被处理,一路带病进入模型
现象:随机森林模型的特征重要性输出里,高度相关的两个特征重要性都虚高,用A特征替换B特征后A的重要性暴涨,模型可解释性完全丧失。
原因:PPG和ECG共同反映心血管系统,时域特征和频域特征之间存在天然相关,比如SDNN与总功率谱密度高度正相关,RMSSD与HF频带功率几乎表述同一个生理过程。
解决:特征提取后对特征矩阵做相关性分析,两两相关系数超过0.95的特征只保留一个;或者改用PCA / PLS这类压缩方法,把共线特征投影到正交空间后再作为模型输入。
6. 上线前先验证:用模拟数据和交叉验证保住特征可靠性
特征提取方案跑通了以后,别急着一股脑训练模型。先做三件事验证特征的可靠性。第一件事是模拟信号验证:用已知频率和幅值生成一段合成PPG(叠加呼吸调制)和ECG(标准QRS形态),对比提取出的心率、呼吸频率与真值的误差。如果误差超过0.5%,说明峰值检测和频谱估计环节仍有系统性问题需要排查。
第二件事是数据切分验证:将生理数据按时间段切分成前一半和后一半,分别在两个子集上提取特征,计算同一受试者特征对数值的组内相关系数。以SDNN为例,长时HRV特征的组内相关系数应该在0.8以上。如果同一受试者前后两个时段的特征差异过大,原因通常不是生理波动,而是采集环境发生了变化(比如佩戴松紧度差异大)。这种情况下要对特征做佩戴不敏感化处理,比如把时域特征和信号幅度特征合并成一个比值。
第三件事是坏段注入测试:从干净数据中截取一段MAP(平均动脉压)已知的信号,人为叠加不同强度、不同频率的运动伪影,观察特征在伪影强度梯度下的变化轨迹。一个有用的做法是把特征输出作为伪影强度的函数曲线,如果曲线斜率在早期就剧烈变化,说明特征对伪影的鲁棒性不足;如果斜率平缓、只在高强度时才偏移,说明预处理流水线的冗余度是够的。
我个人的习惯是在每次特征提取跑完以后,额外输出一份质量报告,包含每段信号的SQI分布、峰值检测错误率、坏段占比、滤波前后的频谱能量比。这些附加信息在模型上线出现性能漂移的时候,能省掉大量排查时间。以前做过一个疲劳监测项目,模型在部署后第两周开始出现心率误判,翻遍算法逻辑都没找到问题,最后回头看质量报告发现是使用者换了佩戴位置导致PPG信号质量全面下降——质量报告里的SQI分布图一眼就定位了根因。从那以后,任何特征提取流程我都默认附带质量报告,这不是可选项,是必需品。
特征提取的方向本质上做的是“信息压缩”:把冗长的波形压缩成少量稳定、可解释、可跨个体比较的数值。判断这条流水线是否合格,不看处理了多少数据,而看压缩后的信息有多少能在真实场景中复现。希望这套流程能帮你少走几段弯路。
本文还有配套的精品资源,点击获取