news 2026/9/14 14:56:09

MATLAB实现MIT-BIH心电信号预处理:从WFDB读取到QRS检测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MATLAB实现MIT-BIH心电信号预处理:从WFDB读取到QRS检测

简介:面向MIT-BIH心律失常数据库的MATLAB心电信号预处理程序,适合生物医学工程、数据科学及心脏病学领域的研究者与工程师,用于去除ECG中的基线漂移、肌电干扰和电源噪声,提升后续分析可靠性。压缩包共含2个文件,以m脚本和txt说明为主,MATLAB脚本实现滤波、去噪、R波检测等常见预处理流程,txt文件提供配套说明或数据参考,整体仅20KB,轻量易用,便于对照学习。已有323人学习/下载,适合刚接触心电信号处理的学生快速理解核心步骤。通过运行该程序,可掌握低通/高通滤波、小波去噪、QRS波群识别等关键技术,并在此基础上开展心率变异性计算或异常心搏分类等进阶研究,是生物医学信号处理入门与实践的实用工具。

1. 先想清楚:解压出来的不是“心电信号”,而是一个待组装的格式包

从网上下载“MIT数据库的心电信号预处理matlab程序.rar”之后,解压看到 .hea、.dat、.atr 这一堆后缀,很多人第一反应是文件不全或者下载错误。其实这就是 MIT-BIH 心律失常数据库的标准三件套:头文件、信号文件、注释文件。真正的问题在于,预处理的第一步从来不是滤波,而是把这些格式各异的二进制文件读进 MATLAB,校准成以毫伏为单位的时序信号。文件读不对,后面所有滤波、R峰检测、特征提取的结果都悬空。这篇文章按“读格式 → 去噪 → QRS检测 → 批量封装”的顺序,给出一套在 MATLAB 里可直接复现的 MIT-BIH 心电信号预处理流程,针对拿这个数据库做分类、做HRV分析、做心律失常检测的人。

2. 把MIT-BIH的WFDB格式拆开:读.hea、解析ADC码值、对齐时间轴

2.1 预处理从哪一步开始:文件结构决定读取顺序

MIT-BIH 数据库里每条记录都包含三个核心文件:

文件后缀作用读取方式
.hea头文件,记录采样率、导联数、增益、基线、ADConverter位数纯文本,直接 readtable 或 textscan
.dat信号文件,按 WFDB 格式压缩存储的心电采样值需要按位解析,不能直接 load
.atr注释文件,记录了每个心跳的类型和位置通过 rdann 函数读取,是评价 R 峰检测的基准

如果把 .dat 当成普通 int16 读取,大概率得到一堆乱码。原因是 WFDB 的老式格式里,每个采样点按 12 位有符号数存储,高低位在文件里做了重排,两个导联的数据还会交替存放。头文件里存的增益 gain 和基线 baseline,则负责把 ADC 码值还原成物理世界里的毫伏数。头文件没读对,后续所有阈值都建立在错误的量纲上。

2.2 在MATLAB里读MIT-BIH数据的最小命令

最省事的方式是装一个 WFDB Toolbox for Matlab,安装后把目录 addpath 进去,用 rdsamp 一行读出信号。最小可运行代码:

addpath('E:\wfdb-toolbox\mcode'); % 指向你解压的WFDB工具箱路径 [sig, Fs] = rdsamp('100', 1); % 读取记录100的第1导联(通常为MLII) ann = rdann('100', 'atr'); % 读取atr注释文件,返回每个心拍的采样点位置

如果机器上没有工具箱,也可以手动解析头文件和信号文件:

fid = fopen('100.hea', 'r'); header = textscan(fid, '%s', 'Delimiter', '\n'); fclose(fid); % 第二行开始是每个导联的格式说明:文件名,采样点数,ADC增益,基线,ADC位数

逻辑说明:rdsamp的返回值第一维是采样点序号,第二维是导联数;Fs对 MIT-BIH 数据来说通常恒为 360Hz。建议先看.hea里的导联顺序再决定取哪一列,因为有些记录的首导联不是我们想要的。注意路径里不要省.dat后缀,rdsamp会根据记录名自动拼接,但如果你把.dat文件改名过,需要先改回头文件里的原文件名。

2.3 从ADC码值到毫伏:增益与基线的单位换算

取一段 10 秒的信号,先用wfdbdesc('100')查看该记录实际使用的增益与基线,然后按下面公式转换:

% 假设从.hea读出的增益为gain,基线为baseline ecg_mV = (double(sig) - baseline) / gain;

参数说明:baseline是 ADC 的零点偏移,常见记录里基线在 1024 附近;gain表示每毫伏对应的 ADC 计数,常见值如 200。图省事直接对rdsamp的原始返回值做阈值判断,会发现阈值在 0.8 到 1.2 之间飘忽不定,原因就是没做这个除法。预处理脚本里建议把这段写在最前面,输出一个ecg_raw_mV变量,后续所有滤波和检测函数都以此为输入。

2.4 读取过程中的常见坑

  • 文件路径分隔符在 Windows 下要写成双反斜杠,否则 MATLAB 报路径不存在。
  • rdsamp默认读全部导联,内存占用大时只读指定导联可以省一半开销。
  • 注释文件的采样点位置基于整个记录的时间轴,如果你截取了中间一段做预处理,记得将 R 峰位置整体减去截取起点的采样点序号。
  • 如果从 MATLAB Online 或远程服务器访问数据,注意.dat文件不能以文本模式打开,必须用二进制方式读取。
  • 手头只有.mat格式转换版数据时,弄清楚原数据的增益是否已经被还原成毫伏,否则后续代码里再除一次 gain 会直接把信号缩小两个数量级。

3. 用MATLAB做心电信号去噪:基线漂移、工频、肌电干扰的滤波顺序与参数

3.1 三种干扰的频率范围与顺序设计

MIT-BIH 里的心电数据虽然是临床采集,仍混入三类常见干扰:

干扰类型频率范围典型来源
基线漂移0.05 ~ 1Hz呼吸、电极移动
工频干扰50Hz 或 60Hz 及其谐波电力系统
肌电干扰20Hz 以上肌肉收缩

滤波顺序建议固定在“先基线,再工频,最后肌电”。先去掉低频大漂移,能防止后续陷波器在工作时产生不必要的边缘振荡;而先陷波再高通,则会在工频陷波过程中,把基线漂移的低频成分也不小心切掉一部分。

3.2 用双中值滤波去掉基线漂移

[数据预处理] 常见的做法是直接用高通滤波器,但我会用双中值滤波,因为它不会引入相位失真,尤其在 R 峰附近不会制造过冲。所谓双中值,是先粗后细:第一次用一个较宽的窗口估出呼吸级漂移,第二次用较窄窗口估出剩余细漂移。

function ecg_nobase = remove_baseline(ecg, Fs) win1 = round(0.6 * Fs); % 600ms窗口,覆盖呼吸周期 win2 = round(0.2 * Fs); % 200ms窗口,覆盖残余漂移 base1 = medfilt1(ecg, win1, 'truncate'); rough = ecg - base1; base2 = medfilt1(rough, win2, 'truncate'); ecg_nobase = rough - base2; end

逻辑说明:medfilt1truncate参数让数据首尾不补零,而是用边界值复制,避免开头和结尾出现人为的向下塌陷。窗口宽度和采样率直接联动:0.6 秒保证不会吞掉 P 波和 QRS 的缓变成分,0.2 秒进一步削减高频组织运动。如果改用高通滤波器,建议阶数不要超过 4,截止频率设在 0.5Hz,否则 ST 段会被拉歪。

3.3 用IIR陷波器切掉工频干扰

MIT-BIH 数据采集时的环境是 60Hz 市电,国内处理自己采集的数据时直接用 50Hz。设计一个二阶级联 IIR 陷波器:

fo = 60; % 陷波频率,视数据而定 q = 35; % 品质因数,控制陷波宽度 [b, a] = iirnotch(fo/(Fs/2), fo/(Fs/2)/q); ecg_notch = filtfilt(b, a, ecg_nobase);

参数说明:iirnotch第一个参数是归一化陷波频率,第二个参数是带宽。Q 值越大陷波越窄,对 QRS 中高频分量的损伤也越小,但 Q 太大会导致对市电频率偏差敏感;实测中 Q=30~45 都可行。必须用filtfilt做零相位滤波,否则 R 峰位置会发生群延迟偏移,后面检测出来的心拍位置会整体偏斜几个采样点。

3.4 低通滤波去肌电但保留QRS的高频能量

肌电干扰通常在 20Hz 以上,而 QRS 波群的频率能量主要集中在 5~20Hz,折中方案是把截止频率放在 45Hz 附近,给高频陡峭的 R 峰留出余量。

[b_lp, a_lp] = butter(4, 45/(Fs/2), 'low'); ecg_clean = filtfilt(b_lp, a_lp, ecg_notch);

注意,不要一口气把截止频率降到 25Hz,那样 R 峰幅度会被明显压低,T 波反而变得相对突出,之后做 QRS 检测时会把 T 波误判成心拍。保存预处理结果时建议同时保留ecg_nobaseecg_clean两份数据,前者保留更多细节用于波形测量,后者用于心拍检测。

4. QRS检测与R峰修正:从能量包络到原始信号重定位

4.1 为什么幅值阈值直接用会漏检双峰与T波

心电信号的幅值在不同导联、不同病人之间差异很大,同一段记录里还可能同时存在高耸 T 波和低矮 QRS 波。直接用固定阈值找极大值,轻则把 T 波当成一次心跳,重则漏掉室早或室性心拍。预处理到这里,需要的是一个带自适应阈值的检测器,而不是几行findpeaks草草收场。MIT-BIH 提供了一整套注释文件,正好用来验证检测效果,这也是比“肉眼看着对”更可靠的方法。

4.2 幅度-斜率联合检测与自适应阈值

我会在预处理信号上,先构造一个带通滤波后的差分能量包络,再对包络做移动窗口积分,最后用动态阈值确定候选 R 峰区间:

% 对3.4节得到的ecg_clean做QRS检测前的带通,突出QRS [b_bp, a_bp] = butter(2, [8 20]/(Fs/2), 'bandpass'); qrsband = filtfilt(b_bp, a_bp, ecg_clean); % 斜率特征:一阶差分模拟QRS陡峭边沿 slope = diff([0; qrsband]); % 能量包络:平方后加窗积分,窗口160ms energy = slope .^ 2; win = round(0.16 * Fs); envelope = movmean(energy, win); % 自适应阈值:初始取包络最大值的40%,之后用历史峰值滚动更新 thr = 0.4 * max(envelope(1:Fs*2)); % 前2秒用于初始估计 refractory = round(0.2 * Fs); % 不应期200ms candidates = find(envelope > thr); peaks = []; last = -refractory; for i = 1:length(candidates) if candidates(i) - last >= refractory peaks = [peaks; candidates(i)]; last = candidates(i); thr = 0.6 * envelope(candidates(i)) + 0.4 * thr; % 滑动更新 end end

参数说明:带通 8~20Hz 是为了突出 QRS 的主瓣能量,同时压制 P 波、T 波和残余肌电。movmean的滑动窗口长度决定了包络的平滑程度,160ms 略宽于一个 QRS 时限,短了会出现多峰,长了会把两个紧邻心拍合并成一个候选区。thr的更新是“参考上一次实际峰值做指数滑动”,这样做比单纯固定百分比更适应心率突然变化。

4.3 R峰重定位与不应期参数

上面的检测器输出的是候选峰在包络上的位置,不能直接当 R 峰用。原因有两点:第一,带通滤波和movmean会引入几十毫秒的延迟;第二,包络极大值点和原始信号的 R 峰尖不一定对齐。修正方法是以候选点为中心,在原始ecg_clean信号上开一个 120ms 的窗口,取绝对值最大的采样点作为最终 R 峰位置:

final_r = zeros(size(peaks)); for i = 1:length(peaks) idx = max(1, peaks(i)-round(0.06*Fs)) : min(length(ecg_clean), peaks(i)+round(0.06*Fs)); [~, loc] = max(abs(ecg_clean(idx))); final_r(i) = idx(loc); end

这里还有一个细节:不应期不要设成固定值。正常窦性心搏时 200ms 足够,但如果后面要做室早分析,200ms 反而会漏掉真正的提前心搏。我的处理是预留一个min_rr参数,设为 0.4 倍当前 RR 间期,让检测器在心率突然加快时也能自适应收缩不应期。

5. 批量预处理与验证:把整套流程固化成一个MATLAB函数

5.1 一个可复用的ecg_preprocess函数设计

把前两章步骤封装成参数可配的函数,比把所有代码堆在一个脚本里更值得推荐。函数的好处是,换一条记录测试时,只需要改文件名和参数结构体。函数骨架如下:

function out = ecg_preprocess(record, params) % record: 类似'100'的字符串 % params: 含有Fs, gain, baseline等字段的结构体 [sig, Fs] = rdsamp(record, params.channel); ecg_mV = (double(sig) - params.baseline) / params.gain; ecg_nobase = remove_baseline(ecg_mV, Fs); ecg_notch = filtfilt(params.bn, params.an, ecg_nobase); ecg_clean = filtfilt(params.bl, params.al, ecg_notch); r_peaks = detect_qrs(ecg_clean, Fs); out.ecg_clean = ecg_clean; out.r_peaks = r_peaks; end

注意,检测结果要和原始注释对齐,数据结构里还要带上Fs和记录名,方便后续合并结果时做溯源。

5.2 用注释文件对齐检测结果:漏检/误检一看便知

MIT-BIH 的.atr注释文件是最客观的校验工具。常见的做法是把rdann返回的注释位置与检测出的 R 峰做最近邻匹配:

ann = rdann(record, 'atr'); matched = false(size(ann)); for i = 1:length(r_peaks) [~, j] = min(abs(ann - r_peaks(i))); if abs(ann(j) - r_peaks(i)) < round(0.05 * Fs) matched(j) = true; end end sensitivity = sum(matched) / length(ann);

时间窗取 50ms,是生理上可接受的心拍标注误差上限。如果 sensitivity 低于 95%,就说明预处理阶段把 R 峰压得太狠,或者 QRS 检测的带通范围太窄。用这样的量化指标去调参数,比盯着findpeaks输出的图反复试更高效。

5.3 最后的小技巧:同步导出去噪波形与RR间期特征矩阵

批量跑完整库之后,把每条记录的 R 峰序列做一阶差分得到 RR 间期序列,再去掉异常间期(小于 0.4 倍中位数或大于 2.5 倍中位数的值),然后和去噪后的波形片段一起落盘。按“记录名、心拍序号、R峰时间点、心拍前后各 100ms 的波形、RR间期”结构存入表格。这样后续做分类、做 HRV 特征提取时,已经可以直接在表格上跑,不用再重新读一遍 WFDB 格式。我自己在调参时还有一个习惯:把sensitivity、记录的 RR 间期均值和去噪后的基线标准差一并打印到控制台。基线标准差低于 0.05mV,且 sensitivity 高于 97%,这组预处理参数才算真正符合预期。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:55:44

Windows内存占用居高不下?从任务管理器到RAMMap的排查实战

前几天一个朋友找我说&#xff0c;电脑没开游戏、也没跑渲染&#xff0c;刚开机一会儿内存占用直接飙到90%&#xff0c;鼠标飘得跟喝醉了一样。我远程一看&#xff0c;任务管理器里一堆奇奇怪怪的进程排在前排&#xff0c;罪魁祸首根本不是他以为的“某个软件”&#xff0c;而是…

作者头像 李华
网站建设 2026/9/14 14:53:52

MATLAB时频分析实战:STFT、CWT与EMD-HHT全解析

简介&#xff1a;这份MATLAB时频分析程序包面向信号处理初学者与工程师&#xff0c;覆盖短时傅里叶变换、小波变换、Wigner-Ville分布及EMD/EEMD等常见方法&#xff0c;配套大量带exa编号的示例脚本&#xff0c;可系统学习时频分析原理与实现。压缩包共40个文件&#xff0c;以3…

作者头像 李华
网站建设 2026/9/14 14:51:50

TurboQuant技术:大模型KV Cache高效压缩方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:48:41

火鸟门户V4.8部署指南:PHP地方门户框架环境配置与五端同步实践

简介&#xff1a;这是一套面向地方政务与社区服务场景的PHP开源门户系统&#xff0c;适用于有本地化运营需求的开发者、中小型企业及地方政府信息化建设团队&#xff0c;可快速搭建集资讯发布、用户互动、养老服务与社群管理于一体的五端同步&#xff08;PC、H5、小程序、APP、…

作者头像 李华
网站建设 2026/9/14 14:47:41

锂电池RUL预测:PCA-BiLSTM混合模型实现与优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华