
简介面向生物医学信号处理与可穿戴设备数据分析场景这套资料整合了PPG与ECG同步采集数据及可一键运行的Python预处理与特征提取代码。基于NeuroKit2库完成两类信号的去噪进而提取潮波幅值比h2/h1、重搏波幅值比h4/h1、收缩面积比S1/S、舒张面积比S2/S以及主波高度、波形周期面积、收缩期面积、舒张期面积、脉搏传导时间PWTT等核心指标适合需要快速搭建脉搏波-心电特征工程流程的研究者或开发者。压缩包共43个文件体积仅4.31MB其中29个txt存放数据样本2个py为主程序脚本另有ini配置和bak备份文件便于查看数据格式与恢复修改。目前已有744人学习下载资源结构清晰覆盖面从数据读取到特征输出省去自行整理与调试环节特别适合作为生理信号分析课程设计或算法验证的参考基线。1. 预处理与特征提取在 PPG-ECG 里的真实分量先让信号可信再谈特征一个看似只值一晚上的任务实际花费往往超过整个项目的三分之二。如果你手头有一批同步采集的 PPG 与 ECG 数据准备提取特征训练模型不管是做情绪识别、疲劳监测还是逐拍血压估计大概率会在第一步就翻车波形里有噪声、峰检不准、两个信号时间轴对不上最后进入模型的特征全是垃圾。这个项目标题讲的核心就一句话——用什么样的顺序把 PPG 和 ECG 从原始波形处理成可信、可复现、有明确物理意义的特征向量。它不是单纯的信号处理理论而是一条能直接落地进训练流程的流水线。本文会从两种信号的物理差异拆起给出一套参数具体、能直接跑的 Python 实现并把常见错误按现象、原因、解决方式列清楚。适合正在做可穿戴设备算法、医学信号分析或生理多模态融合的工程师也适合刚接触这两类信号、想把预处理基本功一次补齐的人。2. 先搞清楚信号本质PPG 和 ECG 为什么不能共用一套预处理2.1 两者在物理源头上就完全不同ECG 记录的是心肌细胞除极与复极过程中在体表形成的电位差信号源是电生理活动频率成分集中在 0.5 到 30Hz最大特征是 QRS 波群尤其是 R 峰尖锐且能量集中。PPG 则是用 LED 照射皮肤光电二极管接收经组织调制后的反射光测量的是微血管血容量随心跳的周期性变化信号源是光学容积变化脉动分量的主频在 0.5 到 4Hz 之间跟心率同频但它还叠加了一个很大的直流分量——皮肤、组织、静脉血、指甲、传感器贴合压力全都在贡献直流电平。这个物理差异决定了预处理的侧重点完全不同。ECG 的问题在于工频干扰、基线漂移和肌电噪声处理时需要在去除干扰的同时保留下 QRS 的陡峭形态PPG 的问题在于运动伪差、环境光干扰和接触压力变化处理时要避免把真正的脉动波形抹平同时还要有办法识别哪一段信号根本不可信。一个常见的误操作是照搬 ECG 的 50Hz 陷波到 PPG 上结果陷波器把 PPG 波形滤出了振铃假峰反过来把 PPG 的低通带宽用到 ECG 上QRS 直接变成钝角R 峰检测全面失灵。所以第一步不是写滤波代码而是先搞清楚你手上这两个信号各自的频率范围、噪声模型和可用带宽。2.2 干扰来源与频率重叠决定了方法选型从工程角度PPG 的噪声可以粗略分成三类。第一类是环境光和传感器滑动导致的随机起伏频谱很宽无法靠固定滤波完全消除。第二类是运动伪差运动会压迫组织改变血流产生与心率频带高度重叠的干扰幅度可以比真实脉动波大好几倍。第三类是基线漂移呼吸、冷热、情绪引起的血管收缩都会造成缓慢的电平变化频率在 0.2Hz 以下。ECG 的干扰来源更直接工频干扰的基波频率在 50 或 60Hz和 QRS 的高频段完全不重叠所以陷波是有效手段肌电干扰频带很宽峰值在 20 到 200Hz会污染 QRS 边沿电极与皮肤之间的极化电位变化带来基线漂移频率同样很低。这里有一个关键结论PPG 的噪声和信号在频带上严重重叠滤波能解决的部分有限更多精力应该花在信号质量判别和坏段剔除上而 ECG 的工频、肌电与 QRS 在频带上可分离性更强滤波优先解决的是能不能保留 R 峰锐利度。我一般会在动手前先画出每一段信号的频谱看一眼信号能量和噪声能量分别在哪个频段再决定滤波参数。这个动作看起来慢实际上能省掉后面大量调参的血泪时间。2.3 预处理流水线的总体框架预处理环节越早统一越好。我常用的流程是原始信号进入后先做采样率统一与时间轴对齐再做去直流和去噪然后做信号质量标记之后是坏段剔除与分段最后才进入特征提取。采样率统一放在最开始是因为一旦两个信号频率不同步后面任何峰检测对齐工作都是错的。去直流放在滤波之前是因为直流电平会影响滤波器的数值稳定性尤其是在做高阶高通的时候。质量标记放在滤波之后因为噪声被压下去以后幅度和差分指标更容易区分好坏信号。时间对齐这一步常被忽略。PPG 和 ECG 往往来自两块独立芯片采集起始时刻不同甚至有固定的时间偏移。如果目标是做 PTT脉搏传播时间这类跨信号特征时间轴对不齐会让特征值整段偏离物理意义。最简单的做法是在采集时留一个同步脉冲标记两个信号各自的起始点没有同步脉冲的话可以根据两路信号共同的心率峰值做互相关对齐这在第 6 章避坑部分会再展开。总之预处理不是简单的滤波而是从信号进入训练流程之前的全链路重建。3. PPG 预处理实战滤波参数与运动伪差处理3.1 带通滤波参数怎么定PPG 的有效脉动分量对应心率范围大约是 30 到 240 次/分钟折算成频率是 0.5 到 4Hz。但波形形态分析比如求二阶导数、检测重搏波切迹时需要保留一些更高频的细节所以我一般把通带取到 0.5 到 8Hz。低端截止频率选 0.5Hz 还是 1Hz 要看你是否需要保留呼吸调制分量。做心率变异性分析的时候呼吸性窦性心律不齐会体现在脉动间隔的慢速变化上低端太干净反而把这些变化滤掉了。因此只做心率计算就选 0.5 到 8Hz做 HRV 相关特征就保留低频高端也可以放宽到 10Hz但超过 10Hz 以后 PPG 信噪比会明显下降噪声占比上升。滤波器类型我固定用 Butterworth 带通加上零相位滤波。零相位意味着用 filtfilt 或 sosfiltfilt 做前后向滤波这样能保证滤波后波形没有相位偏移峰的左右位置和原始波形完全一致。阶数一般取 4太高会在带通边界产生振铃太低则过渡带太宽抑制不住运动伪差。采样率低于 50Hz 时8Hz 的高端截止频率已经接近奈奎斯特频率滤波效果会变差所以做形态学特征前先确认采样率至少 100Hz。scipy.signal.butter里的输出方式建议用 SOS 而不是 BA高阶滤波时 SOS 数值稳定性好得多不会出现频谱毛刺。3.2 运动伪差是 PPG 的最大问题滤波能解决随机噪声但解决不了运动伪差。运动时传感器贴合压力、组织血流都在随机变化伪差幅度大、频带宽和真实脉动波完全重叠滤波后依然残留明显的异常波动。针对这种情况常见做法是加一个信号质量指标按段打标签劣质段直接丢弃不参与后续特征提取。最简单有效的质量指标有两个配合使用就好。第一个是幅度范围检查人体稳定的 PPG 波形经过标准化以后脉动幅度集中在 0.5 到 2 个标准差范围内滑动窗口内如果幅度超过 3 个标准差就有理由怀疑有运动伪差。第二个是主频检查对每一段信号做短时傅里叶变换找频谱峰值对应的频率正常心率段的主峰频率基本落在一个窄区间里如果谱峰分散或是出现了接近直流的强分量大概率是伪差。实际中更稳妥的做法是用滑动窗口结合半波上升沿的规律性来判断因为真实 PPG 每个脉动周期的上升沿时间比较一致伪差不会遵循这一规律。3.3 一段可复用的 PPG 预处理代码import numpy as np from scipy.signal import butter, sosfiltfilt def process_ppg(raw, fs, low_cut0.5, high_cut8.0, order4, win_s5): PPG 预处理去直流 带通 坏段标记 raw : 原始 PPG 一维数组 fs : 采样率单位 Hz win_s: 质量标记的滑动窗口长度单位秒 # 1. 去直流 raw raw - np.mean(raw) # 2. 设计带通滤波器并做零相位滤波 sos butter(order, [low_cut, high_cut], btypebandpass, fsfs, outputsos) filtered sosfiltfilt(sos, raw) # 3. 滑动窗口质量标记 win_len int(win_s * fs) quality np.ones(len(filtered), dtypebool) for start in range(0, len(filtered) - win_len, win_len): seg filtered[start: start win_len] amp np.ptp(seg) # 窗口内峰谷差 rms np.sqrt(np.mean(np.diff(seg) ** 2)) # 相邻差分能量 if amp 3 * np.std(filtered) or rms 2 * np.mean(np.abs(np.diff(filtered))): quality[start: start win_len] False return filtered, quality这段代码的核心逻辑分三步先把直流分量去掉因为直流电平对滤波器和后续峰检测没有任何帮助再用 4 阶 Butterworth 带通做零相位滤波最后按窗口计算幅度和差分能量超过正常范围的段标记为 False。用这个函数时输出里的quality数组要一路带进特征提取阶段不能只在画图时看一眼否则坏段照样会把特征均值拉偏。参数的取值依据是low_cut0.5保住呼吸对脉动间隔的调制high_cut8保住波形细节order4在没有明显振铃的前提下过渡带够窄。如果你的设备采样率只有 25Hzhigh_cut必须降到 8 以下否则高频全是噪声这种情况我建议直接放弃形态学特征只提取峰值间隔。win_s取 5 秒是经验值窗口太短质量指标波动大太长坏段边界切不干净。检测到坏段后一般做法是把它前后各扩张半秒再丢弃避免把过渡段误当成好信号。3.4 分段与归一化的细节特征提取通常不是针对整条记录做的而是切成长度固定的窗口。拿 5 分钟 HRV 分析来说常见的窗口长度是 30 秒、1 分钟和 5 分钟相邻窗口之间叠加 50% 滑动。窗口宽度直接决定频域特征的分辨率1 分钟窗口的频谱分辨率是 1/60 Hz正好能把 HF 频段0.15 到 0.4Hz拆成多根谱线而 30 秒窗口只有不到 8 根谱线落在 HF 频段估计出的功率值方差很大。所以先决定目标特征再决定窗口长度不能反过来。归一化则要看后续模型。做分类模型时我习惯把每段窗口的波形先减去均值再除以标准差这样模型输入不随设备增益变化做回归任务比如血压估计时幅度信息本身可能有意义就不做标准化只做极值幅度裁剪。归一化放在坏段剔除之后否则坏段的异常幅度会把正常段也压成一条近乎平坦的线。4. ECG 预处理实战基线漂移、工频干扰与波形保真4.1 三个干扰源逐个拆ECG 的干扰虽然看起来和 PPG 相似处理方式却有很大不同。基线漂移源于电极接触界面的极化电位变化、呼吸和身体位置移动频率很低一般低于 0.5 到 1Hz。工频干扰来自交流电供电的电磁耦合在 50Hz国内或 60Hz部分国家及其整数倍谐波上幅值可能达到 R 波的 10% 到 50%。肌电干扰来自身躯肌肉收缩频谱覆盖 20Hz 到 200Hz 以上它对 QRS 检测的影响最隐蔽因为它会改变 R 峰边沿的锐利度。处理顺序我固定为先做基线漂移再做工频陷波最后看情况决定是否处理肌电。基线漂移不先解决后面的陷波和低通都会受到低频大振幅的影响导致滤波后波形在边缘出现畸形。工频陷波使用窄带陷波器设计时要注意品质因数 Q 的取值Q 太大会在陷波点附近产生明显振铃Q 太小会把信号能量带走一部分。EEG 和 ECG 处理中常见的一个翻车点是直接用notch的默认参数结果 QRS 波群后面拖出一条长长的假振荡被误检成另一个心跳。4.2 滤波与去噪的具体实现下面这段代码把 ECG 预处理的三个步骤写在一起输出多个版本分别用于不同的下游任务。其中无基线漂移版本用于波形显示和形态学测量去工频版本用于峰值检测低通平滑版本用于特征提取。保留多份结果不是多余因为 QRS 检测对高频锐利度敏感而 ST 段分析对低频保真度敏感一个统一输出没有办法同时满足两种需求。import numpy as np from scipy.signal import butter, iirnotch, sosfiltfilt def process_ecg(raw, fs, notch_freq50.0, q25.0, lowpass_cut30.0): ECG 预处理去基线漂移 工频陷波 可选低通平滑 返回 dict包含三种形态的信号供不同下游任务使用。 # 1. 去除基线漂移0.5Hz 高通 sos_high butter(2, 0.5, btypehighpass, fsfs, outputsos) no_baseline sosfiltfilt(sos_high, raw) # 2. 工频陷波窄带滤除工频及其谐波干扰 b, a iirnotch(w0notch_freq, Qq, fsfs) # iirnotch 返回的是 ba 形式转成 sos 后做零相位滤波 sos_notch butter(2, [notch_freq - 1, notch_freq 1], btypebandstop, fsfs, outputsos) no_powerline sosfiltfilt(sos_notch, no_baseline) # 3. 低通平滑抑制肌电噪声 sos_low butter(4, lowpass_cut, btypelowpass, fsfs, outputsos) smooth sosfiltfilt(sos_low, no_powerline) return { no_baseline: no_baseline, no_powerline: no_powerline, smooth: smooth, }这里有一个值得注意的参数我用butter设计了一个带宽为 2Hz 的带阻滤波器代替直接调用iirnotch的 BA 输出。原因是单靠iirnotch的a, b直接在长信号上做零相位滤波时数值误差会在信号起点和终点被放大而带阻形式的 SOS 配合sosfiltfilt能稳定得多。Q 值取 25 表示在 50Hz 附近大概两三赫兹范围内做抑制既能有效削弱工频基波又不会像 Q 大于 40 那样产生振铃。如果你的数据里存在 100Hz 或 150Hz 谐波干扰可以再加一个同样参数的带阻滤波器但不要无节制叠加每个滤波器都会引入延迟和数值误差。4.3 ECG 质量先看看 R 峰还能不能被检测ECG 预处理的效果检验不看波形平滑不平滑而看它是否经得起标准 QRS 检测算法的折腾。一个很有效的验证方式滤波完成后用scipy.signal.find_peaks在预设高度、距离的基础上检测 R 峰然后检查 R 峰间隔的变异系数。正常窦性心律的 R-R 间隔应当在 0.6 到 1.2 秒范围内变异系数极端情况下也不应超过 20%。如果变异系数看起来离谱不是心脏有问题就是预处理阶段产生了多余假峰或漏检真峰。我习惯把 R 峰位置输出到数组里和原始信号、滤波信号一起保存下来后续特征提取直接基于这个位置数组节省重复检测的计算量。ECG 信号质量评估还可以用NeuroKit2这类现成库但要先确认它的内部滤波器参数和你自己设计的流程是否冲突。至少我在实际项目中自己控制滤波器参数的可解释性要比黑匣子库强得多。5. 从干净波形到特征时域、频域与 PPG-ECG 联合特征5.1 时域特征与峰检测参数PPG 与 ECG 预处理完成后的第一类特征来自峰的相对位置。ECG 检测 R 峰PPG 检测脉动波峰二者原理一致但各有各的坑。scipy.signal.find_peaks里的prominence参数对 PPG 尤其重要因为 PPG 波形有时重搏波会比主波还高按普通最大值检测会把重搏波误当主波。prominence0.3表示峰相对两侧相邻波谷高出至少 0.3 倍标准差这样能压制掉大部分切迹点。distance参数按采样率和最大心率的比值来设置例如 50Hz 采样率、最高心率 200bpm 时最小峰间距设为 15 个采样点。得出峰位置后时域特征就是逐拍特征再统计。常见指标包括平均心率R-R 间隔标准差 SDNN相邻间隔差值的均方根 RMSSD以及 VLF、LF、HF 频域功率。从 PPG 峰间隔同样可以算出 HR、SDNN、RMSSD 等但与 ECG 相比PPG 的峰间隔更容易受波形形态波动干扰所以提取 HRV 特征时我优先用 ECG。两者同时存在时还可以用 PPG 峰间隔和 ECG 峰间隔做一致性校验偏差超过阈值就标记为坏段。5.2 频域特征的计算与频段选择频域特征用 Welch 周期图法计算功率谱密度频段划分沿用经典心率变异性标准VLF 在 0.0033 到 0.04HzLF 在 0.04 到 0.15HzHF 在 0.15 到 0.4Hz。计算前把峰间隔序列通过三次样条插值重采样到 4Hz 或 8Hz 的均匀时序否则直接对不均匀间隔做 FFT 会引入明显的频谱泄漏。窗口选 1 分钟以上长段计算短窗口的低频估计方差太大。一个常见错误是把用原始波形 FFT 得到的 0.5 到 4Hz 占比也称作“频域特征”这其实反映的是波形的形态谱而不是心率调节谱两者物理含义完全不同特征命名时必须区分。5.3 形态学特征与 PPG-ECG 联合特征提取形态特征提取依赖预处理保留的波形细节。ECG 上可以量取 QRS 宽度、QT 间期、T 波幅度PPG 上可以定位主波峰、重搏波切迹计算出反射指数和老化指数这些指标与动脉硬化程度相关。PPG 的二阶导数在临床文献里也常被用到把二次求导后的 a、b、c、d、e 各点位置和比值作为输入特征。真正体现 PPG 与 ECG 联合价值的是 PTT 特征。PTT 本质上是脉搏波从心脏传播到外周测量点所花的时间近似用 ECG R 峰到 PPG 上升沿起始点的延迟来估计。它与血压强相关是血压趋势估计的可穿戴方案里最常见的关键特征。提取 PTT 的方式如下先检测 ECG R 峰位置再在每个 R 峰后一个固定的搜索窗内找 PPG 波形最大斜率点或 PPG 波脚点两段时间差即为 PTT。搜索窗可以是 100 到 300 毫秒取决于个体差异和测量部位。做这个特征之前必须保证两路信号采样率已统一且时间轴对齐到同一参考点否则 PTT 会出现系统性偏置。import numpy as np from scipy.signal import find_peaks def extract_ptt(ecg_filt, ppg_filt, fs, r_peaksNone): 基于 ECG R 峰与 PPG 上升沿的 PTT 提取 r_peaks : ECG R 峰位置数组若为 None 则自动检测 if r_peaks is None: r_peaks, _ find_peaks(ecg_filt, distanceint(0.4 * fs), prominence0.5) # PPG 每个周期找上升沿最大斜率点 ppg_diff np.gradient(ppg_filt) ppt_list [] for r in r_peaks: search_win slice(r, r int(0.3 * fs)) if search_win.stop len(ppg_filt): continue # 限制在 R 峰后 300ms 内找最大斜率位置 slope_idx np.argmax(ppg_diff[search_win]) r ppt_list.append((slope_idx - r) / fs * 1000.0) # 转换为毫秒 return np.array(ppt_list)这段代码里最关键的参数是搜索窗上限 300 毫秒。PTT 在不同测量部位的正常值通常在 50 到 300 毫秒之间窗口设得太短会漏掉上升沿太长会把下一拍的趋势也包进来。np.gradient计算差分后取最大斜率点比直接用幅度阈值找波脚对滤波残留更鲁棒。如果做完发现 PTT 值明显超过生理范围先检查是不是两个信号的起始时间戳没有对齐而不是急着调搜索窗。6. 三个高频避坑记录与预处理结果的自检技巧6.1 滤波后波形出现振铃假峰现象滤波后的 PPG 波形在每一个主峰后面拖出一条反向的假振荡幅度能到主峰的 30% 左右导致峰检测把假振荡也当成一次心跳。原因有两种最常被撞上一是 Butterworth 滤波器阶数过高二是用sosfilt单向滤波造成相位失真振铃被叠加到波形上。解决方式是固定用sosfiltfilt做零相位滤波并把order降回 4。滤波后第一时间把原始波形和滤波波形叠在一起画出来扫一眼有没有幅度超过主峰 20% 的额外波动。不要只盯着 R 峰检测率看。6.2 双通道信号时间轴错位导致 PTT 全乱现象PTT 特征算出来有的负数、有的超过 500 毫秒方差大到离谱。原因基本是两路 ADC 各自独立采集没有统一的起始时间戳或者采样率标称一致但晶振有额外误差。解决方式是在采集阶段埋一个同步脉冲开始记录前同时给两路一个电压台阶利用台阶的上升沿对齐时间轴。如果数据已经采集完了就用两路信号各自检测心跳峰计算两组峰位置序列的互相关系数找到最佳偏移量后做整体平移。这类问题在预处理阶段不解决特征阶段无论怎么调参数都是白费功夫。6.3 坏段没有剔除干净特征均值被拉偏现象整段信号的 HR、RMSSD 等指标忽高忽低且不同窗口之间的差异大得反常。原因往往是只对信号做了滤波却没有把运动伪差或电极脱落部分标记出来坏段的异常幅度直接参与了统计量计算。解决方式就是回到第 3 章的质量标记流程在特征提取前按窗口过滤掉所有质量标记为坏段的区间。这里还有个细节坏段周围的过渡期也会受影响我习惯把坏段前后各扩展 0.5 秒再剔除。6.4 预处理结果自检的一个硬习惯我现在每次跑完预处理都会挑出三到五条有代表性的信号段把原始信号、滤波后信号、峰位置标记画在一张图里输出成一个 PDF 文件。这个习惯帮我抓到了至少十次以上的峰误检。原因是自动评估指标比如检测率、信噪比无法替代人眼对波形形态的整体判断。画图加注释不需要太复杂一条数据一张图注上文件名、窗口序号、采样率、滤波器参数出了问题能直接回溯到是哪一段预处理造成的。结构化信号处理项目里可复现性永远比一次性的好看结果重要。希望这个自检习惯能帮你也少走几次弯路。本文还有配套的精品资源点击获取