简介:MIT-BIH心电图数据库是心律失常领域公认的经典数据集,由麻省理工学院与波士顿布莱根妇女医院联合发布,面向心脏疾病诊断、心电生理分析及信号处理研究者。压缩包内为1分钟心电记录及配套MATLAB绘图程序,数据附有采样率等关键参数,便于理解信号特性。包内共4个文件,包括mat数据文件、info信息文件、m绘图脚本和asv自动保存文件,整体仅20KB,轻量易用。绘图脚本可直观呈现P波、QRS复合波与T波等典型波形,支持分析心率、心律及QT间期等指标,结合MATLAB滤波、特征提取与自动检测功能,能完成从数据读取到可视化分析的全流程。配套详解说明了程序运行逻辑与数据读取方式,适合快速上手心电图数据分析和MATLAB绘图的初学者或研究人员。已有7597人学习下载,实用性和参考价值获得较多认可。
1. MIT-BIH ECG 心电数据,第一道坎在格式不在绘图
第一次拿到 MIT-BIH ECG 心电数据的人,十有八九会卡在 MATLAB 绘图这一步。不是不会调plot,而是把.dat文件读进来之后发现:数值既不是毫伏、波形上下翻转、基线漂移大到把 P 波整个淹没,怎么看都不像教科书上那张心电图的形状。这套数据本身是公开心电数据库中流传最广的心律失常样本,每条记录大约 30 分钟,采样率 360 Hz,多数记录包含双导联信号和逐拍 R 峰注记。真正困扰人的是文件格式和单位换算:.hea管元数据,.dat管波形,.atr管标注,三者分开存放,读取逻辑需要自己拼。这份资源恰好把“从 212 格式解包、基线去除、QRS 检测到论文级绘图参数”整条链路都补全了,适合准备做心电算法验证、课程设计或者论文复现的从业者,也适合想用一份可靠数据练手 MATLAB 的新手。
2. 读数据这关:.hea/.dat/.atr 三件套的读取与单位换算
2.1 文件结构:四类文件各管什么
MIT-BIH 数据不是单个 CSV 或 MAT 文件,而是一整个记录目录。以记录 100 为例,目录里至少会有四个文件,它们的职责完全不同:
| 文件 | 内容 | 在 MATLAB 里的用途 |
|---|---|---|
| .hea | 文本头文件 | 解析采样率、通道数、ADC 增益、基线值 |
| .dat | 二进制波形文件 | 原始 212 格式信号,需要解包成数值 |
| .atr | 二进制注释文件 | R 峰位置、节律类型、起止标注 |
| .info | 文本摘要 | 记录编号、导联名称、时长、病人信息 |
.hea是第一行就要读的入口文件。100.hea 的第一行记录名、通道数、采样率、采样点数,第二行开始才逐通道给增益和基线。很多绘图翻车的根源是直接跳过.hea,把.dat里的 ADC 计数当成 mV 画,那画出来的波形幅值会膨胀几百倍。就我拆这套数据的经验,先把.hea解析干净,后面所有步骤才站得住。
2.2 两条读取路线:WFDB 工具箱还是自写解析
读取方式主流有两种:一条是装 WFDB 工具箱,用rdsamp和rdann直接读,代码极短;另一条是不依赖任何工具箱,自己用 MATLAB 解析.hea和 212 格式的.dat。两条路我都在项目里用过,结论是:版本验证、算法调试、论文复现推荐自写解析;只是想快速看一眼波形,WFDB 一行就能满足。
自写解析的好处是可控。你能明确看到每个样本在内存里的排列顺序,知道为什么一条双导联记录的文件大小大约是单导联的两倍。对于要跑批处理、做数据增强或者把数据导入深度学习的从业者,自写解析比强迫所有人装同一版工具箱更省事。WFDB 的优势是快,尤其读.atr注释,rdann一行就能拿全部 R 峰位置,自己解析.atr却要处理二进制位段,工程量不小。
2.3 自写解析实战:212 格式解包与 mV 换算
MIT-BIH 里大部分.dat文件是 212 格式:每 12 位存一个采样值,三个字节打包两个样本。双导联记录里,同一时间戳的两个导联样本会紧挨着放在一个“帧”里,所以第一导联和第二导联要拆开存放。解析代码如下:
fid = fopen('100.hea', 'r'); if fid == -1 error('当前路径找不到 100.hea,请先确认记录文件位置'); end hdr = textscan(fid, '%s', 'Delimiter', '\n'); fclose(fid); line1 = regexp(hdr{1}{1}, '\S+', 'match'); % 第一行:记录名 通道数 采样率 点数 line2 = regexp(hdr{1}{2}, '\S+', 'match'); % 第二行:通道1文件名 格式 增益 基线 Fs = str2double(line1{3}); % 采样率,MIT-BIH 多为 360 Hz adc_gain = str2double(line2{3}); % ADC增益,常见 200 ADC/mV adc_zero = str2double(line2{4}); % 零电平基线,常见 1024 fid = fopen('100.dat', 'r'); raw = fread(fid, inf, 'uint8')'; fclose(fid); nPack = floor(numel(raw) / 3); % 三字节一组 b = reshape(raw(1:nPack*3), 3, []); s1 = double(b(1, :)) + bitshift(bitand(b(2, :), 15), 8); % 导联1 s2 = double(b(3, :)) + bitshift(bitand(b(2, :), 240), 4); % 导联2 ecg_mV = [s1(:), s2(:)]; ecg_mV = (ecg_mV - adc_zero) / adc_gain; % ADC计数 -> mV t = (0:size(ecg_mV, 1) - 1) / Fs; % 时间轴,单位秒代码的逻辑是把 12 位采样值拆成“低 8 位 + 高 4 位”:b(1,:)是导联 1 的低 8 位,b(2,:)的低 4 位是导联 1 的高 4 位;b(3,:)是导联 2 的低 8 位,b(2,:)的高 4 位是导联 2 的高 4 位。adc_gain在 100.hea 里常见值是 200,即 200 个 ADC 计数对应 1 mV;adc_zero是基线偏移,通常 1024。先减基线、再除增益,波形才是真实的 mV 幅值。如果这套数据里的记录是单导联,.dat的打包逻辑会变成连续单通道样本,需要在读取前先判断.hea里的通道数,否则解析出来的两列会把一条完整波形劈成两段。
够装工具箱的人可以这样对照验证:
[ecg, Fs] = rdsamp('100', 1); % 直接读通道1 ann = rdann('100', 'atr'); % 读R峰注记,单位是样本索引第一次跑通这段脚本时,建议打印一行中间结果:disp(max(ecg_mV(:,1)))和disp(min(ecg_mV(:,1)))。MIT-BIH 记录的正常电压动态范围在正负几毫伏内,如果你看到几百甚至几千的数值,基本可以肯定是增益除错了,不是原始数据坏了。
3. 预处理这层:基线漂移、工频噪声的判断与滤波参数选择
3.1 先看频谱再动手,别凭感觉滤
很多人在预处理阶段的第一反应是直接套一个带通滤波器,比如 0.5–100 Hz,结果基线没去干净,工频干扰还残留了一大截。我建议先画出原始信号的频谱,用结果决定要滤哪几段。
sig = ecg_mV(:, 1); % 选第一导联 N = length(sig); f = Fs * (0:N/2 - 1) / N; X = fft(sig - mean(sig)); plot(f, abs(X(1:N/2)), 'LineWidth', 0.8); xlim([0 100]); grid on; xlabel('Frequency (Hz)'); ylabel('Magnitude');跑出来会看到三个典型特征:靠近 0 Hz 的那片巨大低频频段是基线漂移,呼吸运动和电极移动造成的,能量集中在 1 Hz 以下;50 Hz 处的尖峰是工频干扰;高频部分平缓的抬升是肌电噪声,通常从几十赫兹一直延伸到几百赫兹。频谱看完,滤波目标就清晰了:搬走 1 Hz 以下的“地板”,压掉 50 Hz 的“钉子”,再削平高频尾巴。盲目用带通不是不行,但你不知道自己在滤哪个分量,后面调参数只能靠猜。
3.2 基线漂移:中值滤波窗口怎么选
基线漂移在时域上的表现是波形整体上下摆动,严重时能让 R 波在屏幕上像坐过山车。最常见做法是用中值滤波估计基线,再从原信号里减掉。窗口选多大是个经验活:太小会把 QRS 群当成基线的一部分,滤完 R 波变形;太大会让基线估计跟不上呼吸引起的慢起伏。我在这套数据上常用的窗口是 0.2 秒,对应 360 Hz 采样率下约 72 个采样点。
win = round(0.2 * Fs); baseline = medfilt1(sig, win); % 第一次估计 baseline = medfilt1(baseline, win); % 第二次估计,压平剩余毛刺 sig_detrend = sig - baseline; % 如果还有剩余趋势,补一阶高通 [b_hp, a_hp] = butter(2, 0.5 / (Fs / 2), 'high'); sig_detrend = filtfilt(b_hp, a_hp, sig_detrend);这段代码的核心是用两次中值滤波把基线估得像一条平滑曲线。medfilt1每次输出窗口内的中位数,对 QRS 这种短促尖峰不敏感,所以估计出的基线基本不会跟着 R 波走。补高通是为了处理慢趋势残留,0.5 Hz 的截止频率拿捏在不会伤害 P 波和 T 波,这两个波段的主要能量大约在 1–5 Hz。用filtfilt而不是filter,是保证零相位延迟,后续做 R 峰定位时波形位置不会偏移。
参数调整时记住一个规律:如果滤完后波形大幅变平、R 波顶部发钝,说明窗口太小或者高通截止高了;如果基线还有明显起伏,先加窗口中值滤波次数,再把高通截止提到 1 Hz 试试。0.5 Hz 是默认起点,不是固定答案。
3.3 工频干扰与肌电噪声:陷波和低通的组合
工频干扰在频谱上是 50 Hz 的窄尖峰,用常规的低通滤波器压不干净,而且低通截止设太低会削掉 QRS 的高频成分。最好用陷波器精准狙击。MATLAB 里designfilt可以直接生成窄带陷波:
d_notch = designfilt('bandstopiir', 'FilterOrder', 4, ... 'HalfPowerFrequency1', 49, 'HalfPowerFrequency2', 51, ... 'SampleRate', Fs, 'DesignMethod', 'butter'); sig_clean = filtfilt(d_notch, sig_detrend); % 肌电噪声用 80 Hz 低通收尾 [b_lp, a_lp] = butter(4, 80 / (Fs / 2)); sig_clean = filtfilt(b_lp, a_lp, sig_clean);陷波器的两个半功率频率取 49 Hz 和 51 Hz,带宽只有 2 Hz,目的是只切 50 Hz 那根刺,不碰 QRS 的能量集中区。QRS 的频率范围主要在 5–25 Hz,虽然也有一小部分频谱延伸到 50 Hz 附近,但窄带陷波对它的影响很小。如果采集环境工频干扰特别重,可以把半功率频率扩到 48–52 Hz,代价是 50 Hz 附近的心电微弱分量也会被切掉一部分。肌电低通常见截止取 80 Hz,足够保留 QRS 形态,又能大幅压掉肌肉抖动的高频毛刺。做完预处理再画一次频谱,50 Hz 那根尖峰应该基本消失,0–20 Hz 频段占据主导,这样信号才适合进入 R 峰检测环节。
4. 波形解析这步:QRS 检测与 R 峰定位的 MATLAB 实现
4.1 为什么先带通再做差分与积分
QRS 检测不能直接用原始信号找极大值,因为 T 波在某些导联上可能比 R 波还高,直接找峰值会误判。经典 Pan-Tompkins 算法的思路是先把 5–15 Hz 这段 R 波能量集中的频段提取出来,再对波形做差分、平方、滑动积分,最后用阈值判定。带通在这里的作用有两个:一个是滤掉 P 波和 T 波的低频主体,另一个是削弱基线残留。差分相当于把信号变成斜率信息,R 波本身的陡峭上升沿会产生明显的正尖峰,T 波斜率变化慢,在差分域的能量会被压得极低。平方进一步放大这种差异,滑动积分则把接续的峰聚拢成一个平滑凸起,让阈值判断更稳定。
4.2 Pan-Tompkins 精简实现与阈值调参
我在这份资源里提供的精简版实现分四步,代码量不大,但参数点比较密:
% 第一步,带通 5-15 Hz,增强R波斜率 [b_bp, a_bp] = butter(2, [5, 15] / (Fs / 2), 'bandpass'); y = filtfilt(b_bp, a_bp, sig_clean); % 第二步,差分加平方 dy = diff([y(1); y]); sq = dy .^ 2; % 第三步,滑动积分窗口 150 ms win_int = round(0.15 * Fs); integ = conv(sq, ones(win_int, 1) / win_int, 'same'); % 第四步,阈值和不应期 th0 = 0.4 * max(integ(5 * Fs : 15 * Fs)); % 用前10秒估计阈值 ref = 0.2 * Fs; % 不应期 200 ms rIdx = []; i = 1; while i <= length(integ) if integ(i) > th0 j = min(i + win_int, length(y)); [~, loc] = max(y(i : j)); % 在积分窗内找原始R峰 rIdx = [rIdx; i + loc - 1]; i = j + 1; % 跳过不应期 else i = i + 1; end end代码的最后一个环节最容易出错:integ超过阈值时,说明这个时间段内大概率有 QRS,但精确位置要回到y上找局部最大值,因为积分波形本来就是平滑过的,直接把积分峰值当 R 峰会有几个样本的偏移。th0用前 10 秒数据乘 0.4,避免整条信号全部参与计算后被偶发大伪迹抬高阈值。不应期 200 ms 对应最大 300 bpm 的心率,足以应对绝大多数心律失常数记录。如果这段代码漏检特别多,先把带通上缘降到 12 Hz,T 波的影响会再压一截;如果误检多,把th0提高到 0.5 倍,多数 T 波凸起会被挡在阈值下面。
4.3 把 R 峰和心率变化叠到原图上
检测完 R 峰,绘图这一步才是资源的核心价值。单导联的排版可以这样出:
figure('Color', 'w'); subplot(2, 1, 1); plot(t, sig_clean, 'LineWidth', 0.8, 'Color', [0.2 0.2 0.2]); hold on; plot(t(rIdx), sig_clean(rIdx), 'rv', 'MarkerFaceColor', 'r'); xlim([5 10]); xlabel('Time (s)'); ylabel('mV'); title('Lead MLII with R peaks'); grid on; rr = diff(t(rIdx)); % 相邻R峰时间差 hr = 60 ./ rr; % 瞬时心率 bpm subplot(2, 1, 2); plot(t(rIdx(2:end)), hr, '.-', 'LineWidth', 1); xlim([5 10]); xlabel('Time (s)'); ylabel('HR (bpm)'); grid on;上面代码里t(rIdx)是每个 R 峰对应的时间戳,diff(t(rIdx))得到 RR 间期,60 ./ rr就是瞬时心率。心率图能快速看出这段记录里有没有早搏或漏检:正常窦性心律的散点会稳定在 70–80 bpm 附近,出现一个突然下掉的点,基本就是早搏位置,同时也能反向检查前面 R 峰检测对不对。子图 1 和子图 2 共用同一个时间轴,缩放时用xlim同步,整张图的信息密度比单画一条波形高很多。
5. 常见问题排查:读取、标定、滤波、检测的四个翻车现场
5.1 读文件失败:路径、扩展名和工具箱版本
现象:fopen返回-1,或者rdsamp直接报错,提示找不到文件。
原因:当前工作目录不在数据记录所在文件夹;或者是文件名拼写带了多余后缀;还有一种情况是 WFDB 工具箱版本过老,对 212 格式的支持有兼容性问题。
解决:先pwd确认路径,再用exist('100.hea', 'file')检查文件;.hea、.dat、.atr三个文件必须放在同一目录,文件名主部分一致。我自己排查时习惯把路径写成fullfile('path/to/data', '100'),避免手拼分隔符在换系统后翻车。如果确认文件和路径都没问题,重装最新版 WFDB,或者按第 2 章的自写解析脚本读一遍。
5.2 波形翻转或幅值异常:单位换算和位序的坑
现象:信号画出来上下颠倒,或者幅值到了几千,明显不是毫伏级别。
原因:212 格式的位解析顺序写反,导致高 4 位和低 8 位拼错;adc_gain没有用来除,直接把 ADC 计数当电压画;adc_zero没减,波形整体上偏到一个大直流电平上。
解决:先打印原始 ADC 计数的前 5 个值,正常应该在 1024 附近,这是基线偏移值。如果前几个值接近 3000 或 -3000,位序组合错了。s1 = b(1,:) + bitshift(bitand(b(2,:), 15), 8)和s2 = b(3,:) + bitshift(bitand(b(2,:), 240), 4)这两个表达式不要调换位置。换算时严格走(ADC - adc_zero) / adc_gain,其中adc_gain多数记录是 200,但每条记录的.hea都写着自己的值,用硬编码 200 早晚会踩到特殊记录。
5.3 滤波后 R 峰位置整体偏移:相位失真
现象:预处理前后的波形形态一致,但把两幅图叠在一起看,滤波后的 QRS 位置比原图晚了几个采样点。
原因:filter是有因果性的滤波器,信号经过会产生相位延迟;陷波器阶数越高,延迟越明显。
解决:所有滤波统一用filtfilt。它的实现是正反向各滤一次,相位在数学上完全抵消,输出的 R 峰位置和原始信号对齐。如果你做的是实时处理,filtfilt用不了,那就要记下滤波器群延迟做补偿;但离线分析场景没理由不用filtfilt。这条坑最容易出现在把网上抄来的代码原样跑一遍的人身上,波形看着像模像样,R 峰却整体偏移了上百毫秒,连上.atr注记对比时才发现对不齐。
5.4 QRS 误检和漏检:阈值与不应期的调参顺序
现象:检测出的 R 峰数量明显偏多,或者某些肉眼可见的 R 波没被标出来;心率图上出现莫名其妙的毛刺。
原因:大多数情况是带通上缘太高,把 T 波的一部分能量放进来了;少数情况是th0阈值过低,或不应期太短。
解决:调参顺序必须从带通开始,不要一上来就改阈值。先把带通上缘从 15 Hz 降到 12 Hz,重跑检测,误检通常会先降一截;再看integ波形,如果 T 波对应位置还冒头,再把th0从0.4 * max提到0.5 * max。最后才检查不应期,200 ms 能应对大多数记录,不要为了追求低漏检把不应期压到 150 ms 以下,那样晚搏被吞的概率极大。改完一组参数后,用.atr里的 R 峰位置做对照,计算差值和数量,这一步在第 6 章展开。
6. 绘图进阶:把波形调到能直接放进论文的程度
6.1 论文级波形的默认样式
心电波形图在论文里出现时,通常要求清晰、紧凑、无干扰视觉元素。我在画图时会固定一套样式:背景纯白、波形用深灰或黑色细线、R 峰用红色倒三角或空心圆标记,坐标轴字体用 Times New Roman 或宋体加粗,字号统一到 8–9 pt。grid on要开,但把网格线颜色调浅,比如[0.8 0.8 0.8],否则会压过波形本身。时间窗用 5 秒或 10 秒比较合适,太短看不出节律趋势,太长又显得波形挤成一团。
figure尺寸也需要固定,单栏图建议figure('Units','centimeters','Position',[2 2 14 8]),双栏图把宽度压到 8 厘米。导出时用print而不是截屏:
print(gcf, 'ecg_plot', '-dpng', '-r600');600 dpi 的 PNG 既满足印刷要求,文件体积也不大。R 峰标注不要直接标数字,用图例说明一次就够了,否则图上全是数字会非常乱。
6.2 验证整理:与 .atr 注记锚定一轮
画完图不能直接收工。我的习惯是强制做一轮“注记对照”:把检测到的 R 峰位置和.atr里的标准 R 峰位置对齐,检查是否漏检和误检。rdann读出来的注记是样本索引,直接和rIdx对比,允许的偏差范围通常设在正负 5 个采样点内。如果检测结果一致性超过 95%,图才有说服力。这套验证流程我已经固定在每次做完预处理之后:先跑检测,再对照注记,最后画图。也就是这个习惯,让我少交了至少两版返工图。希望帮到你。
本文还有配套的精品资源,点击获取