资讯详情

PPG-BP数据集去噪实战:中值滤波与带通抑制运动伪迹和基线漂移

📅 2026/10/26 13:29:11 | 华诺云谱 👁 阅读
PPG-BP数据集去噪实战:中值滤波与带通抑制运动伪迹和基线漂移
简介面向生物医学信号处理研究人员与Python开发者这份资源提供了读取PPG-BP数据集中脉搏波信号并完成去噪的完整脚本。代码基于scipy.signal实现低通滤波器搭配matplotlib.pyplot完成信号波形可视化能够直观对比原始与滤波后的脉搏波适用于信号预处理、算法验证、教学演示以及血压/心率相关研究的起步环节。压缩包内共14个文件包含主执行脚本、原始脉搏波文本数据、项目配置文件与自动备份文件整体仅6KB轻巧易用。目前已有1790人学习下载。借助该示例可快速掌握文本数据读取、滤波参数设置、时域波形绘制等组合操作理解典型去噪流程同时可基于脚本二次开发替换数据或调整截止频率为后续脉搏波特征提取与分析提供可复用的基础工具。1. 为什么PPG-BP数据集上去噪这一步直接决定血压模型准不准很多人在PPG-BP数据集上做血压估计第一件事就是急着算特征上升沿斜率、脉搏波传导时间、峰值幅度然后丢给回归模型。等验证集上一看误差全乱掉才回头翻原始波形发现里面既有呼吸引起的基线漂移也有手指晃动带进来的运动伪迹。PPG-BP数据集里的脉搏波不是躺平了让你直接提特征的光电容积脉搏波本身是微弱的血管容积变化信号夹带的噪声往往比你要保留的形态信息还要显眼。去噪不只是一个预处理开关它决定你后面提取的每一个血压特征是否可信。滤波过度比不滤波危害更大这是本篇第一个要记住的反直觉结论。这篇笔记适合正在做血压回归、心率变异性分析、以及刚拿到PPG-BP数据集不知道从哪一步下手的研究生和实践者按文中的代码和参数去跑能直接得到一套可用、可验证的去噪流程。2. 先看清PPG-BP数据里到底混了哪几类噪声运动伪迹、基线漂移与高频干扰2.1 三类噪声的来源、频段和它们对血压特征的具体破坏方式PPG信号从指尖或腕部采集光源穿过组织后由光电二极管接收血管容积随心脏搏动周期性变化调制了透射或反射光强。这个机制决定了它天生容易被三样东西干扰。基线漂移来自呼吸、体温波动、电路温漂和肢体轻微移动频率大多在0.1Hz到0.5Hz之间直观表现是整个波形在一个较长时间尺度上整体上下浮动。运动伪迹来自肢体晃动、传感器与皮肤之间的相对位移、组织受压变形这是最棘手的一类——它的频谱和PPG主频段高度重叠而且幅值可以超过信号本身的数倍。高频干扰则主要是50Hz或60Hz工频、肌电噪声、量化噪声落在信号的高频端。血压估计依赖的特征点对这三类噪声的敏感度完全不同。收缩期峰值的时间位置和幅值会被基线漂移直接抬高或压低导致以相对幅值为输入的特征全部失真。运动伪迹会改变一整段的波形形态让重搏波切迹消失、让主峰出现假峰或双峰按固定窗口提取特征时会把噪声当信号算进去。高频干扰对峰值位置影响相对小但它会污染上升沿斜率和切迹定位这两个参数恰好是PPG-BP建模里常用的输入。把握住频段就把握住了选型方向PPG脉搏波的主频通常在1Hz到5Hz一次完整心跳的主波、重搏波和谐波成分会延伸到8Hz到10Hz左右。基线漂移集中在0.5Hz以下工频在50Hz或60Hz北美和部分亚洲地区是60Hz国内市电为50Hz高频噪声再往上。去噪的目标不是把波形磨平而是尽量保留1Hz到8Hz这个窗口里的形态信息其余部分按频谱位置做压制。下面这张表是处理前先要确认的频段分布。信号成分大致频段对血压特征的影响有效脉搏波及谐波0.8Hz ~ 8Hz峰值时间、重搏波切迹、上升沿斜率均依赖此频段基线漂移0.1Hz ~ 0.5Hz直接改变峰值绝对幅值破坏以幅度为输入的特征呼吸波0.2Hz ~ 0.35Hz与基线漂移重叠常被一并归入低频干扰运动伪迹0.5Hz ~ 5Hz与信号重叠改变局部形态产生假峰最难滤除工频干扰50Hz / 60Hz 附近污染切迹高频细节但不影响主峰大致位置2.2 选型逻辑为什么组合滤波比单一滤波器可靠不少人一上来就套一个带通滤波器以为只要把0.5Hz以下和8Hz以上的东西滤掉就算完成。常见做法是把某几个固定截止频率写死换一个采样率的文件就翻车。原因在于带通滤波能处理高频干扰和大部分基线漂移却处理不了两件事一是幅度大且频谱重叠的运动伪迹二是工频干扰这种窄带强噪声。我一般会采用「基线校正 → 带通滤波 → 窄带陷波」的组合策略而不是指望一个漂亮的IIR滤波器解决所有问题。先做基线校正把波形落到零线附近再做带通把频带外的东西压下去最后用工频陷波把50Hz那个尖峰单独抠掉每一步只解决一类问题。选型时还要注意线性滤波器的局限。巴特沃斯滤波器是经典选择因为通带内平坦、实现简单用scipy.signal里的butter加sosfiltfilt就能做到零相位滤波。但对外观近似噪声、频段又和信号完全重叠的运动伪迹线性滤波器只会把它平均化不能真正去掉。遇到这种段落正确做法是先用质量指标把它标记出来再做段级别的剔除或替换而不是强行滤波。另外不要单独依赖Savitzky-Golay滤波器去做全部去噪它能很好地保留峰值细节但对抗大幅基线漂移和运动伪迹时能力不够。把它当作带通后的平滑工具可以当作主力去噪手段不行。2.3 动手写代码前先用可视化和数值指标确认噪声构成拿到PPG-BP数据集的文件先不要急着写函数。随机抽取几段不同受试者的信号用Matplotlib直接打印出来看三个东西整条记录看基线漂移的幅度和周期选择一小段比如5秒放大看主峰形态和重搏波切迹是否清晰再算一下频谱看低频成分和工频尖峰有多强。可视化这一步特别重要只有知道噪声是漂移占主导还是运动段占主导才能决定参数侧重。数值上可以快速算峰值信噪比或者直接用np.diff看看相邻样本的跳变幅度如果跳变动不动就是信号均值的几十倍基本可以判断有尖峰型噪声或运动段混入。确认完噪声类型再进下一章。整体流程按「先摸清噪声 → 再把每个环节写成代码 → 再回到一组信号上验证」来做这样不会跌进只顾调代码不看波形、最后处理完手里只剩一堆光滑但没法用于血压建模的曲线的坑。3. 用中值滤波加巴特沃斯带通做脉搏波去噪Python代码与关键参数设定3.1 核心去噪流程中值滤波去基线、带通滤频带外干扰中值滤波去基线的思路很直接对原始信号跑一个核宽度较大的中值滤波得到一个跟随波形慢变的局部中值序列这个序列就是基线估计。然后用原始信号减去这个基线实现第一步漂移抑制。里面最关键的是核宽度的选取。PPG主周期大概在0.6秒到1.2秒之间对应心率50到100次/分中值滤波的核窗口如果小于一个脉搏周期会把脉搏波本身也当成基线抹掉如果大到几秒又跟不上快速漂移。我一般取0.5秒到1秒对应的样本数作为核宽度。下面是完整的核心去噪代码。import numpy as np from scipy.signal import medfilt, butter, sosfiltfilt, iirnotch, lfilter def ppg_basic_denoise(x, fs125.0, low0.5, high8.0, order5, notch_freq50.0, q30.0): 对单段PPG信号做去噪 1. 中值滤波估计基线并减掉 2. 巴特沃斯带通滤波压制频带外干扰 3. 工频陷波去掉50/60Hz窄带噪声 x np.asarray(x, dtypenp.float64) if x.ndim ! 1: raise ValueError(输入必须是1维数组, 实际拿到的是 {}.format(x.ndim)) # 核宽度按0.5秒计算, 必须为奇数 k int(round(fs * 0.5)) if k % 2 0: k 1 baseline medfilt(x, kernel_sizek) detrended x - baseline # 带通滤波: 截止频率要除以fs/2做归一化 nyq fs / 2.0 sos butter(order, [low / nyq, high / nyq], btypeband, outputsos) filtered sosfiltfilt(sos, detrended, padlen150) # 工频陷波, 只挖掉一个窄带 b_notch, a_notch iirnotch(notch_freq, q, fs) output lfilter(b_notch, a_notch, filtered) return output, baseline这段代码的逻辑先做基线估计再带通顺序不能反过来。如果先带通再减基线带通滤波器本身会滤掉极低频的漂移但会留下阶跃式的瞬态响应最终波形头尾出现明显下沉。把low设为0.5Hz、high设为8Hz覆盖脉搏波主频和重搏波切迹需要的谐波范围。陷波参数q取30在125Hz采样率下带宽大约2Hz既能压掉工频又不会把附近的脉搏波成分误伤。返回值里baseline建议保留下来后面做特征修正或者论文画图对比时用得上。3.2 截止频率、滤波阶数和采样率之间的换算关系参数不能靠抄必须按采样率去换算。最常见的坑是把截止频率直接写成标准化的Wn值换一个采样率的文件就出完全不同的效果。butter函数接受的截止频率永远要除以奈奎斯特频率即fs / 2.0。125Hz采样率下想保留0.5Hz到8Hz的频带两个截止频率分别是0.5 / 62.5 0.008和8 / 62.5 0.128同样一块数据如果重采样到50Hz两个值就变成0.02和0.32差距非常大。阶数决定了滚降快慢。阶数越高阻带衰减越陡但相位失真和计算开销也越大。配合filtfilt做零相位滤波时高阶数还会放大信号首尾的瞬态效应。我一般用4到6阶血压特征提取场景选5阶比较平衡。如果检测到重搏波切迹总是糊掉优先降低high而不是加阶数。sosfiltfilt里的padlen控制首尾补零长度建议至少取150更长更稳处理短片段时不要超过信号长度的三分之一不然会把瞬态误差引入有效数据区。下面的参数速查表按常用情况给出一个保守配置。场景fslowhighorder说明通用血压特征125Hz0.5Hz8.0Hz5保留完整形态只看主峰位置125Hz0.8Hz5.0Hz4切迹会被压弱老年受试者心率偏慢100Hz0.3Hz8.0Hz5基线下限放低高频干扰明显250Hz0.5Hz12Hz6谐波保留更多运动段挑出后二次处理125Hz0.8Hz4.0Hz4配合段级别剔除3.3 把去噪流程写成一个可复用函数批量处理时不该改动的部分写成函数之后批量处理PPG-BP数据集时只需要循环文件。函数内部有一件事绝对不能省返回值和输入必须保持完全相同的长度和顺序。很多滤波代码在处理短片段时额外引入了边界延拓最后输出的长度和原始信号对不上血压样本的标注就没法对齐。这个函数里已经用sosfiltfilt(padlen150)固定了边界处理输出长度等于输入长度。如果要对每一拍单独切出来滤波不要在原数组上做删减用视图切片传进来处理完再按原始索引填回去。这样批量脚本就只需要关心文件读写不必反复处理对齐问题。4. 把去噪脚本跑通环境准备、批量处理PPG-BP数据集的完整流程4.1 先把Python环境和依赖装对版本选择与安装顺序新手在这步最容易耗掉大量时间。Python版本直接选3.8到3.12之间的最新稳定版不要用还在预览中的新大版本。安装时注意勾选把Python加入系统PATH这样命令行里能直接敲python而不需要配置环境变量。依赖只有四个numpy负责数组运算scipy提供全部滤波函数matplotlib用来画对比图pandas可选只在数据文件是表格结构时用到。安装顺序一言以蔽之先把pip本身升级再装科学计算包避免旧版pip解析依赖时出幺蛾子。python -m pip install --upgrade pip python -m pip install numpy scipy matplotlib如果下载速度慢或反复超时用国内镜像源。清华PyPI镜像的命令是python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy scipy matplotlib。装完在Python解释器里执行import scipy; print(scipy.__version__)确认没有报错。环境装到这一步就够了。验证环境时顺手确认采样率来源。PPG-BP数据集里不同设备采集的采样率可能不一致有的125Hz有的50Hz或100Hz。去噪脚本里fs不能写死要么从文件名的元信息读要么从数据表头带进来。后面批处理时我习惯把fs作为参数从外层传入每个文件单独指定。这个细节不处理好后面所有截止频率都是错的。4.2 批量去噪脚本读取、逐段处理、写回与可视化输出PPG-BP数据集常见存储格式有.mat、.csv和.txt.mat文件用scipy.io.loadmat读取csv和txt统一用numpy.loadtxt或pandas.read_csv。批处理脚本只做四件事遍历目录、按格式读取、调用上一章的ppg_basic_denoise、写出新文件并在旁边生成对比图。写一个定时任务就能在几分钟内把所有记录处理完。下面是批量处理主体。from pathlib import Path import numpy as np import scipy.io as sio import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt def process_dataset(src_dir, dst_dir, fs_map): src_dir, dst_dir Path(src_dir), Path(dst_dir) dst_dir.mkdir(parentsTrue, exist_okTrue) for src_path in sorted(src_dir.glob(*.mat)): data sio.loadmat(src_path) # 以常用变量名ppg为例, 按数据集实际键名调整 raw np.asarray(data[ppg], dtypenp.float64).flatten() fs fs_map.get(src_path.stem, 125.0) # 每个文件独立采样率 # 滤波主体 clean, baseline ppg_basic_denoise(raw, fsfs) # 写回, 保留原始字段并追加去噪字段 out_path dst_dir / (src_path.stem _clean.mat) sio.savemat(out_path, {ppg_clean: clean, ppg_raw: raw, baseline: baseline, fs: fs}) # 对比图: 原始、基线、滤波后三段 t np.arange(len(raw)) / fs fig, axes plt.subplots(3, 1, figsize(10, 8), sharexTrue) axes[0].plot(t, raw, linewidth0.8) axes[0].set_title(raw) axes[1].plot(t, baseline, linewidth0.8) axes[1].set_title(baseline) axes[2].plot(t, clean, linewidth0.8) axes[2].set_title(clean) axes[2].set_xlabel(time (s)) fig.tight_layout() fig.savefig(dst_dir / (src_path.stem _compare.png), dpi100) plt.close(fig) print(processed:, src_path.name, fs, fs)这个脚本把原始信号、估计基线和去噪结果各画一行输出文件名和源文件一一对应并打印处理状态。读.mat时注意变量的实际键名有的数据集存成PPG或signal要按表头调整。.csv和.txt格式换成np.loadtxt即可处理逻辑完全一样。写回时保留原始数组是为了后续做特征对比不建议丢弃。4.3 怎样验证去噪结果避免处理后信号没法用的隐蔽问题验证分三个层次肉眼检查波形、数值指标对比、特征点稳定性。肉眼检查重点看三处——滤波后的信号是否围绕零线平稳、主峰和重搏波切迹是否还在、运动伪迹段是否被显著压平但没把整段正常信号削成平线。数值上可以算一个粗略的信噪比把原始信号减滤波后信号当作噪声估计信噪比用10 * log10(sum(clean**2) / sum((raw - clean)**2))计算。这个指标在纯基线漂移和高频干扰的场景下比较可靠但在运动伪迹占主导时会把有效波形成分也当成噪声所以要结合波形判断不能只看数字。特征点稳定性放在第六章展开这章先做到每个文件都能正常产出对比图。5. 脉搏波去噪的6个常见坑现象、原因和解决办法5.1 滤波后波形头尾出现大范围弯曲或下坠现象是滤波结果开头几十个点明显上翘或下弯中间的波形正常尾部又弯回来。原因是filtfilt虽然零相位但边界两侧的信号不连续滤波器瞬态响应在头尾被放大如果先用带通再去基线这个现象会更严重。解决方法是先做中值减基线再做带通并给sosfiltfilt设置合适的padlen。我在3.1节的函数里固定了这两个顺序实际使用时如果还是看到头尾弯曲就把padlen从150提到300或者对每一段信号前后多留50个点的缓冲处理完裁掉缓冲再保存。5.2 重搏波切迹消失主峰变圆变钝现象是滤波后的波形很干净但用于区分收缩期与舒张期的切迹点找不到了。原因是上截止频率设太低比如把high设成3Hz以后切迹对应的高次谐波被一起滤掉。血压分析里重搏波切迹往往落在主峰后约三分之一周期处需要保留到8Hz甚至10Hz。解决方法是把high抬到8Hz以上如果高频干扰依然明显再在滤波后加一个窗口很小的滑动平均5到11个点而不是压低带通的high。5.3 运动伪迹段落怎么都滤不干净带通对它无效现象是整段波形在大幅晃动区域仍然出现和脉搏波同频的波动滤波器参数无论怎么调都压不下去。原因是运动伪迹不是固定频带的平稳噪声它和信号频谱重叠线性滤波器的选择性在这里失效。解决方法是先做质量检测用滑动窗口计算局部幅值和方差当窗口内信号的峰值幅度超过正常段三倍以上时给这段打上运动标记。被标记的段落不要去参与特征提取直接剔除或者用前后正常段的形态做插值替换。把期望放在滤波参数上不如放在段级别处理上这是处理这个数据集的血泪经验。5.4 同一个脚本换一个采样率的文件就崩效果差一大截现象是脚本在125Hz数据上效果很好换成50Hz的数据后滤波结果明显失真。原因是截止频率没有按采样率做归一化butter里传的还是原来那套值。解决方法是所有频率参数一律除以当前文件的fs / 2.0后再传入并且fs不要写死在批量脚本里为每个文件单独维护采样率。更稳妥的做法是在函数内部用nyq fs / 2.0统一换算上一章的ppg_basic_denoise已经包含这一步复制到别的项目里也不会因为采样率改变而出错。5.5 滤波后特征点的数量翻倍峰值检测出现大量假峰现象是用于计算脉率的峰值检测阈值不变但滤波后检出的主峰数量明显多于实际心跳数。原因是带通加上陷波后上升沿和切迹附近残留的高频抖动被find_peaks当成局部峰值也可能把重搏波位置的次峰误判为主峰。解决方法是峰值检测时设置最小间距distancefs*0.4左右并加prominence参数要求峰高至少是局部方差的倍数如果仍然有次峰混入就在带通后再加一个窗口为5到11点的滑动平均压低高频抖动。峰值筛选参数要和滤波参数一起调整不能单独动一边。5.6 批量处理后文件对不上原记录标记的血压值无法与波形片段对齐现象是去噪输出文件比原始文件短了几百个点或者某个片段整体偏移了一段距离。原因是切片处理时把边界补齐的部分直接写进了输出或者filtfilt在分段处理时改变了有效长度。解决方法是函数内部强制保证输出长度与输入完全一致批处理脚本在写文件前断言len(clean) len(raw)不等就打印文件名并跳过。滑动窗口分帧时不要在原数组上做删除用切片视图保留原始索引。这些对齐问题不会报错但一旦发生会污染整个特征表排查起来比滤波器算错还耗时。6. 最后一招用特征一致性测试判定滤波参数是否真的合格参数调完之后不能只靠看得顺眼要有一个可复现的验证手段。最实用的方法是特征一致性测试对同一段记录在原始信号和去噪信号上分别检测主峰计算每两个相邻主峰之间的时间间隔然后比较这两组间距的波动程度。如果去噪有效间隔序列的变异系数应该明显下降因为噪声导致的假峰和漏检在去噪后减少了。这里把scipy.signal.find_peaks封装成一个检查函数只输出一个指标相邻主峰间隔的变异系数。from scipy.signal import find_peaks def peak_stability_cv(x, fs, min_dist0.4): 返回相邻主峰间隔的变异系数, 越小说明周期越稳定 x np.asarray(x, dtypenp.float64) peaks, _ find_peaks(x, distanceint(fs * min_dist), prominence0.25 * np.std(x)) if len(peaks) 5: return None rr np.diff(peaks) / fs # 单位秒, 每个间隔是一次心跳周期 return np.std(rr) / np.mean(rr) # 变异系数把ppg_basic_denoise处理前后的信号都丢进这个函数对比。以125Hz、正常节律数据为例原始信号如果混入工频或高频抖动变异系数可能在0.05以上去噪后应降到0.03以下。如果去噪后反而变大说明滤波把主峰削钝或者参数把真伪边界整混了这时候回头调high或prominence阈值不要急着把代码里的参数当作定论。注意血压或心率本身就有生理变异的受试者变异系数会比正常人偏高此时不要强行追求极低数值要用同一受试者处理前后相对变化来判断。我最早做这个数据集时就是只盯着信噪比直到发现特征提取结果对不上才回头加了这个检查。后来养成的习惯是每次跑批处理前先随机抽三段原始波形、一段运动段、一段干净段打印处理前后对比图和变异系数确认没有异样再全量跑。这样处理过的数据再拿去训血压模型训练集和验证集指标才真正稳得住。希望这篇能帮你在PPG-BP数据集上少踩几个波形处理的坑。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑