ECG/PPG/BP三路同步信号特征提取与批量处理方法
2026/9/7 6:17:41 网站建设 项目流程

简介:面向生物医学工程研究人员与信号处理开发者,这份MATLAB项目为同步采集的PPG、ECG、BP三路生理信号提供完整的特征提取流程。代码覆盖信号预处理、峰值与间期检测、时频域分析及基于参考信号的对齐处理,例如R波检测、脉搏波峰谷定位、小波分解、频域样本熵计算等,并内置一段.mat实测数据可直接运行主程序,适合用于健康监测或疾病特征研究。压缩包共30个文件,其中包含28个.m源码、1个.mat示例数据和1个README说明文档,整体大小33.31MB,目录划分清晰,便于按功能模块查阅。目前已有1401人学习,对于希望快速上手多模态生理信号特征工程的研究者与开发者,是一份可复现、可扩展的实用代码库,有助于缩短算法验证与实验开发周期。 做生理信号处理的人应该都有体会:采集设备一开,三路波形哗哗地录下来,看着挺漂亮,但真正把数据变成可以进统计模型的特征,才是费时费力的地方。我这次整理的bp_features_extraction程序,处理的正是同步采集的ECG、PPG和BP三路信号——心脏电活动、外周血流容积、动脉内压力,三个维度各讲各的故事,又要放在同一个时间轴上比对。这个程序的价值,就是把这套从原始波形到特征表格的管线跑通,每搏切分、时域/频域特征计算一步到位,最终输出标准化的CSV特征表。适合做心血管功能评估、连续血压监测研究、可穿戴生理信号分析的人直接抄作业。

1. 项目背景与整体设计思路

1.1 三个信号为什么放在一起处理

ECG记录的是心肌细胞的电活动,体表电极就能拾取到,QRS波群的出现代表了心脏一次机械收缩的启动指令;PPG利用光电传感器检测组织内血液容积的搏动变化,指尖夹个探头就能采到;BP则是动脉内压力的直接体现,临床常用的连续血压波形能够逐搏给出收缩压、舒张压和平均压。三者从不同层面描述同一个生理过程:电兴奋、容积脉动、压力变化。

同步处理的价值就体现在“联合”上。最典型的例子就是PTT——心电图R峰到外周搏动波某个特征点(PPG波峰或BP波形上升支)的时间差。这个时间差与动脉血压之间有较强相关性,也是很多无创连续血压估计算法的基础。再比如HRV与血压变异性(BPV)的频域耦合分析、PPG与ECG之间的相位关系,都需要三路信号严格同步。如果各提各的,最后拿到的时间轴都对不上,再好的特征也没法用。

1.2 程序的功能定位与处理流程

最初拿到这批数据时,我最大的诉求是批量、可复现。实验涉及几十个受试者、每段记录几十分钟,手动在软件里一个个数搏动、算特征根本不现实。所以程序从一开始就按流水线设计:读取原始数据,统一时间轴与采样率;对三路信号分别做针对性预处理;再以ECG的R峰作为全周期基准,把PPG、BP波形切成一个个心搏窗口;在每个窗口里依次计算三路信号的时域特征;所有搏动特征汇总后再做频域计算和描述统计;最后把特征表写成CSV和MAT两种格式,方便后续导入Excel、SPSS或Python的统计脚本。

这套流程看起来直白,但每一步都有细节。比如预处理时滤波器参数怎么选、R峰检测阈值怎么定、坏段怎么筛,这些决策会直接决定最终特征的质量。后面几节我会把每个环节的具体做法和踩过的坑都展开说。

1.3 为什么选Matlab而不是Python

我是有意选Matlab搭的这个程序。原因很实际:Signal Processing Toolbox里有几个函数是这个任务的地基,比如filtfilt做零相位滤波、findpeaks做峰值检测、iirnotch做工频陷波、pwelch做功率谱估计,一套流程下来代码量能比Python少不少。而且我做实验用的采集系统(BIOPAC、PowerLab这类)导出格式以及很多门内流传的旧脚本都是Matlab生态,和同门对数据、对代码时沟通成本低。

当然Python现在也很强,但如果你只是处理一批离线数据,Matlab的交互式调试体验和绘图响应速度确实更顺手。这个选择并不绝对,按自己课题组的生态来就好。

2. 数据接入与预处理三板斧

2.1 数据格式确认与同步对齐

拿到数据的第一步永远不是滤波,而是把时间轴搞明白。三路信号虽然来自同一个采集系统,但不同设备、不同导出软件的坑非常多:有的波形文件里时间轴是单独一列,有的按起始时间加采样率隐式给出,有的三个通道采样率还不一样。先查清楚这些,写成硬性校验。

常规做法是读进后用统一采样率重建时间轴,线性插值到共同网格。Matlab里interp1或resample都行,但要注意resample会自动做抗混叠滤波,处理阶跃突变时会引入边缘效应;没有特殊需求时我一般用resample,并在头尾各截掉几十个点来消除过渡带影响。

data = readtable('record.csv'); t = data.time; ecg = data.ecg; ppg = data.ppg; bp = data.bp; fs = 1000; t_common = (0 : length(t)-1) / fs; ppg_r = interp1(t, ppg, t_common, 'linear'); bp_r = interp1(t, bp, t_common, 'linear');

同步对齐之后,最好用ECG的R峰和PPG的搏动波峰做一次交叉验证——两条通道的搏动计数应该一致(或者差一个固定的生理延迟),如果数目对不上,说明时间轴或通道映射出了问题,这时候值得回去核对原始数据。

2.2 ECG信号的工频陷波与基线校正

ECG最典型的干扰是工频噪声,国内就是50Hz。采集环境只要接地没做好,波形上就是厚厚一层高频毛刺。这里我用的是iirnotch设计陷波器,陷波频率wo和带宽BW都按采样率做归一化:

fs = 1000; wo = 50 / (fs/2); bw = wo / 35; [b_notch, a_notch] = iirnotch(wo, bw); ecg_f = filtfilt(b_notch, a_notch, ecg); [b_hp, a_hp] = butter(2, 0.5 / (fs/2), 'high'); ecg_f = filtfilt(b_hp, a_hp, ecg_f);

这里两个参数值得说一下。wo是50Hz映射到奈奎斯特频率后的归一化位置,bw取wo/35,对应的品质因子Q=35,实测下来既能把50Hz分量压住,也不会因为带宽过窄而对真实频率偏移敏感。高通滤波的截止频率选0.5Hz,是为了去掉呼吸引起的基线漂移和电极慢漂移,同时保证QRS波群(能量集中在几Hz到几十Hz)完全不受伤。注意连续用了两次filtfilt,这是我一直坚持的习惯:零相位滤波,波形不会整体延迟,后面做各通道时间对比时少了很头疼的矫正步骤。

2.3 PPG和BP信号的平滑去噪

PPG受环境光和运动伪迹的影响大,与ECG相比往往更脏。它的主要能量集中在心率基频以及前几次谐波上,因此一个10Hz左右的低通滤波器就能把大部分高频噪声压掉。BP连续波形频率范围和PPG接近,也是低通处理,同时保留动脉压力波本身的形态。

这里一个小技巧:低通之前先做异常尖峰剔除。有些数据里会突然出现一个孤立的毛刺信号,比如电极松动、光线遮挡、体温变化,如果直接滤波会把尖峰扩散成一段隆起,影响后面的峰值检测。我习惯预先做一个中值滤波(medfilt1,几十个点的窗口)或阶梯式离群值剔除,只对明显超出局部均值好几个标准差的点做替换,而不是对整个信号做全局平滑,这样能最大限度保住真实搏动边缘的陡峭程度。

3. 按搏特征提取的核心实现

3.1 用ECG的R峰作为整条流水线的锚点

特征提取的核心,先把“一个搏动”的定义定死。我选择ECG的R峰作为时刻锚点,原因很简单:R峰是三个信号里信噪比最好、形态最单调的波峰,检测算法成熟、误检率低。把R峰位置定位后,PPG和BP都以它为基准取窗口——默认取R峰之前0.2秒到R峰之后0.6秒(可按心率调整),这样一个窗口内的ECG片段、PPG搏动波形、BP压力搏动天然对齐。

R峰检测我用findpeaks而不是从零手写Pan-Tompkins:

[pks, locs] = findpeaks(ecg_f, 'MinPeakHeight', threshold, ... 'MinPeakDistance', round(0.3 * fs), ... 'MinPeakProminence', 0.3 * std(ecg_f));

MinPeakHeight和MinPeakProminence需要用一段手工标注的数据来试,把它们定在“能检出所有正常R峰、同时不把T波当作R峰”的位置。MinPeakDistance设成0.3s对应的样本数,这是生理极限:成人最小心动周期也就是紧张、剧烈运动时可能到0.3s上下,小于这个间隔的峰一定不是相邻心搏。

3.2 三路信号的时域特征计算

拿到每个搏动窗口后,特征计算就是纯粹的数学了。核心循环大致长这样:

for i = 1:length(locs)-1 idx = locs(i) : locs(i+1); ecg_beat = ecg_f(idx); ppg_beat = ppg_f(idx); bp_beat = bp_f(idx); rr_ms(i) = (locs(i+1) - locs(i)) / fs * 1000; sbp(i) = max(bp_beat); dbp(i) = min(bp_beat); map(i) = mean(bp_beat); pp(i) = sbp(i) - dbp(i); ppg_amp(i) = max(ppg_beat) - min(ppg_beat); ppg_pi(i) = ppg_amp(i) / mean(ppg_beat) * 100; [~, pkg_peak] = max(ppg_beat); [~, bp_peak] = max(bp_beat); rise_time_ppg(i) = pkg_peak / fs * 1000; end

这里有几个细节:MAP用的是搏动区间内的时间平均而不是简单的(SBP+2DBP)/3,后者是临床估算公式,在有完整波形时直接积分平均更准确;PPG的PI是搏动分量幅度除以平均光强再乘100%,反映灌注水平,这个指标受传感器贴合力度影响大,横向比较时要小心。

信号特征名称计算方式单位
ECGRR间期相邻R峰时间差ms
ECGSDNN全部RR间期标准差ms
ECGRMSSD相邻RR差值均方根ms
PPG脉率60/平均峰值间隔bpm
PPGPI搏动幅度/平均光强×100%%
PPG上升时间谷到峰时间ms
BPSBP/DBP窗口最大值/最小值mmHg
BPMAP窗口时间平均mmHg
BPPPSBP-DBPmmHg

3.3 HRV与血压变异性的频域特征

时域特征是一拍一拍的,而频域特征往往要把一段长时间序列切成若干子段来做。HRV频域分析最常用的做法:先把逐搏RR间期序列用三次样条插值重采样到4Hz均匀序列,再做Welch功率谱估计,然后在标准频带内积分功率:

ts = cumsum(rr_ms) / 1000; tq = ts(1) : 1/4 : ts(end); rr_interp = interp1(ts, rr_ms, tq, 'spline'); [pxx, f] = pwelch(rr_interp, hann(256), 128, 256, 4); lf = trapz(f(f>=0.04 & f<=0.15), pxx(f>=0.04 & f<=0.15)); hf = trapz(f(f>=0.15 & f<=0.4), pxx(f>=0.15 & f<=0.4));

频带划分是行业标准:高频段(0.15-0.4Hz)一般认为反映副交感神经调节,低频段(0.04-0.15Hz)反映交感与副交感的共同作用,LF/HF常被拿来当自主神经平衡指标。需要注意的是,短时记录的频域HRV可靠性并不好,一般要求至少5分钟;我在程序里把长记录切成5分钟子段再分别算LF/HF,输出的时候保留子段编号。同样的流程在BP上就是对逐搏SBP序列做血压变异性频域分析。

3.4 特征整理与批量导出

所有逐搏特征算完后,把整个网络的组织工作交给table和timetable。timetable可以直接带时间戳,后面画趋势图、做滑动平均都很方便。用writetable直接落盘CSV:

T = table(rr_ms', sbp', dbp', map', pp', ppg_pi', ... 'VariableNames', {'RR_ms','SBP','DBP','MAP','PP','PPG_PI'}); writetable(T, 'features_per_beat.csv');

我一般会同时输出两种粒度:逐搏明细表,每个心搏一行,包含ECG/PPG/BP的所有特征;汇总统计表,每个受试者或每个时间段一行,包含均值、标准差、中位数、分位数。后者才是最终送进统计软件做组间比较的数据。

4. 常见问题与排查技巧实录

4.1 时间轴错位与采样率不一致

踩过的第一个坑是:同一批记录里,部分文件ECG是1000Hz,PPG和BP导出时被降到了250Hz。如果不做处理直接按相同索引取窗口,后面所有特征对不齐。排查办法:对两个通道做互相关,横轴滞后对应的时间偏移,正常情况下应该是一个固定值(比如几十毫秒的心电-脉搏延迟),如果斜率不对或呈现分段跳变,基本就是采样率不一致。解决方式是统一用高采样率那个通道的时间基准,对低采样率的做interp1线性插值补齐。

4.2 运动伪迹与坏段剔除

数据里总有不听话的受试者。轻微身体动作会让PPG和BP波形出现大段基线台阶甚至脱落。我的程序里加了坏段标记逻辑:对每个5秒窗口,计算PPG搏动幅度序列的变异系数,超过阈值就标记为疑似运动段;再检查BP的收缩压值是否落在合理生理范围,比如30-260mmHg之外就是明确异常。被标记的坏段不进特征统计,但在汇总表里会记录坏段占比,这一步对后续数据质量说明很有用。

4.3 findpeaks参数调优的坑

findpeaks虽然好用,但参数设置不好也会丑态百出。印象最深的是MinPeakProminence没设好时,T波被当成了R峰——心率直接翻倍;而MinPeakDistance设太大又会在心动过速时漏检。我的经验是:先用一段5分钟、信噪比高的数据手工标注R峰位置,把两组候选参数跑出来的输出与手工标注对比,让检出率、误检率都在可接受范围,再把参数固定下来跑批量。记住一点:调参是适配你的数据的,不是适配某个理论公式的。

4.4 长时数据的性能与存储

一小时的三路1kHz原始数据,存成mat文件大约百兆级别,按搏循环算特征如果每拍都反复用索引取数据,几十分钟的记录也可能跑出十分钟的耗时。两个优化点:一是循环前把需要重复使用的信号片段全部转成列向量并按区间索引切片,避免频繁的矩阵访问;二是所有输出变量在循环前先zeros预分配。实测这样几十分钟的记录能在几十秒内算完。另外,CSV小了但信息量也小了,建议同时保留一个中间MAT数据,包含清洗后的信号和所有峰值位置,后续想换个特征重算,就不用从头读原始文件。

最后说点个人体会。这个程序最初版本是三路信号各管各预处理、各算各的特征,最后再去合并对齐,结果每次都被时间轴问题折磨。后来狠下心重构,把预处理完的三路信号放进同一个矩阵,再用ECG的R峰作为唯一锚点切窗口,之后ECG、PPG、BP的特征天然对齐,流程一下子顺了。所以如果参考我这个思路,第一条建议就是:先解决时间基准,再谈特征质量。后续想继续扩展的话,可以在这个框架上加入PTT的计算——R峰到PPG波峰的时间差,以及利用血压和脉搏波形态估算动脉硬度、心输出量这些更宏观的指标,管线不用大动,往里加特征就行。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询