ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MIT-BIH心电数据MATLAB绘图全攻略:格式解析、波形绘制与标注可视化

MIT-BIH心电数据MATLAB绘图全攻略:格式解析、波形绘制与标注可视化

简介:MIT-BIH心电数据库是心律失常研究领域广泛使用的经典公开数据集,由麻省理工学院与波士顿布莱根妇女医院联合发布。资源选取其中一段1分钟ECG记录,并配套MATLAB绘图详解脚本,面向心电信号处理初学者、生物医学工程学生及相关科研人员,解决“如何读取数据、如何可视化波形、如何理解关键参数”等入门问题。资源共4个文件,包括MATLAB脚本(.m)、脚本自动备份(.asv)、可直接加载的数据文件(.mat)以及记录采样率等信息的info文件,压缩包整体仅20KB,轻量便携,便于快速下载和课堂演示。借助脚本可在MATLAB中直接加载数据、绘制心电波形,直观观察P波、QRS复合波与T波等特征;同时结合info文件理解采样率对信号采集与还原的影响,为后续去噪、滤波、特征提取等处理打下基础。已有7592人学习下载,适合作为理解MIT-BIH数据格式、入门ECG信号处理与绘图的实操参考。

1. MIT-BIH ECG 心电数据+matlab绘图:这题到底在解决什么

拿到 MIT-BIH 心电数据的第一天,大多数人会做同一件事:把100.dat拖进 MATLAB,用plot画一下,然后发现波形完全不是想象中那回事。不是一团黑块,就是坐标轴上 0 到 648000 的采样点序列,根本没有 P 波、QRS 波、T 波的样子。这个标题真正要解决的,不是“怎么画一条线”,而是三件事:把 MIT-BIH 的二进制数据正确读成心电信号、把采样点换算成物理时间、把数据里自带的心拍标注叠加到波形上,最后导出成论文或报告里能直接用的图。MIT-BIH 是心电算法验证最常被拿来做基准的公开数据集,48 条双导联 30 分钟记录、360 Hz 采样率,读懂它再配合 MATLAB 绘图,R 峰检测、心率变异性分析、异常心拍可视化这些后续工作才落得了地。这篇笔记就按“格式→读数据→画波形→画标注→避坑”的顺序往下推。

2. MIT-BIH 数据格式拆解:.dat/.hea/.atr 三个文件如何协同

从 PhysioNet 把某个记录目录拉到本地后,一个记录通常给三个文件:100.dat、100.hea、100.atr。刚接触的人习惯先双击.dat或者直接load,然后发现画出来完全不是心电图。原因在于.dat是二进制,用的是 MIT-BIH 自己的 Format 212 压缩存储,两个 12 位样本塞进 3 个字节;.hea是纯文本说明书,采样率、增益、基线都在里面;.atr是标注文件,每个心跳的位置和类型都由它记录。读懂这三个文件的配合关系,后面绘图才不是玄学。

2.1 挑哪条记录练手:48 条里从 100 号开始

MIT-BIH 心律失常库一共 48 条记录,每条包含双导联、约 30 分钟的连续心电信号,采样率 360 Hz。编号上有个规律:100段的记录偏常规或接近正常,200段的记录包含更多复杂心律失常,比如室早、短阵室速、传导阻滞。第一次跑流程我建议从100开始,这条记录基线平稳,P-QRS-T 形态清晰,标注也齐全,适合用来验证读取脚本和绘图流程。跑通之后换200这类带明显室性早搏的记录,能立刻看到标注可视化带来的信息量差异。

数据组织方式很简单,一条记录名为100,对应100.dat、100.hea、100.atr三个文件。.dat里存的是原始 ADC 码,.hea会告诉你每个通道的增益和基线,.atr里是医生或程序标注好的心拍类型。绘图时不要只盯着.dat,.hea里的参数不读对,后面画出来的图纵坐标就会差出几个数量级。

2.2 读入心电波形:自写 Format 212 解码函数

MIT-BIH 的.dat文件不是直接能读的整数数组。Format 212 的含义是:每个样本占 12 位,存储时两个样本一组塞进 3 个字节。第一个字节存第一个样本的高 8 位,第二个字节存第二个样本的高 8 位,第三个字节的低 4 位是第一个样本的低 4 位,高 4 位是第二个样本的低 4 位。理解这个位排列之后,读取脚本就很固定:

function ecg = read_mitbih_212(datFile, nsamp, nchan) % 读取 MIT-BIH Format 212 数据 % nsamp: 每通道采样点数, nchan: 通道数(MIT-BIH 通常为 2) fid = fopen(datFile, 'rb'); bytes = fread(fid, Inf, 'uint8'); fclose(fid); % 取 3 的整数倍长度,避免文件尾部不完整 tail = numel(bytes) - rem(numel(bytes), 3); bytes = bytes(1:tail); ecg = zeros(nchan, floor(tail / 3)); idx = 0; for k = 1:3:tail-2 idx = idx + 1; b0 = double(bytes(k)); b1 = double(bytes(k+1)); b2 = double(bytes(k+2)); s0 = b0 * 16 + bitand(b2, 15); % 第一通道样本 s1 = b1 * 16 + bitshift(b2, -4); % 第二通道样本 ecg(1, idx) = s0; ecg(2, idx) = s1; end end

b0 * 16等价于b0 * 2^4,把高 8 位抬到 12 位的高位位置;bitand(b2, 15)取第三字节的低 4 位,组合成第一个样本的完整 12 位。第二个样本同理,用bitshift(b2, -4)取高 4 位。循环逐组解码 3 字节,输出矩阵第一行是通道一,第二行是通道二。这套逻辑在 MATLAB 里跑一遍,10 分钟的数据大概几十秒能读完,追求速度的话可以改写成向量化索引,但初学阶段按字节循环最不容易出错。

注意:MIT-BIH 心电值域通常按 0 到 2047 处理,所以读取时不需要做额外的符号扩展。后面转物理单位时统一减基线、除增益即可。

2.3 .hea 头文件关键参数:采样率、增益、基线决定了你能不能画对

.hea文件是纯文本,第一行记录级信息,后面每行对应一个导联通道。用textscan就能把它读进 MATLAB:

fid = fopen('100.hea', 'r'); info = textscan(fid, '%s %d %d %d', 1); % 记录名, 通道数, 采样率, 样本数 chan = textscan(fid, '%s %d %d %d', 1); % 文件名, 格式, 增益, 基线 fclose(fid); fs = info{3}; gain = chan{3}; % 单位: ADC 码 / mV baseline = chan{4}; % 基线 ADC 码, 常见为 1024

.hea里这几个数直接决定绘图公式:采样率 360 Hz,时间轴就是t = (0:N-1) / 360,单位是秒;增益 200,基线 1024,幅值换算就是mV = (raw - baseline) / gain。很多人画出来的图纵坐标是几百几千,就是因为少做了这一步换算。值得多说一句的是,基线不要写死 1024,从.hea里读是最稳的,因为不同版本的数据文件基线字段可能不同,写死常数早晚翻车。下面把常用字段列一张表,出图前对着检查一遍。

字段典型值对绘图的影响
采样率 fs360 Hz时间轴必须除以 fs,否则横轴是采样点序号
通道数2排版时用 subplot 或分两个 ylabel
增益 gain200 ADC/mV纵坐标还原成 mV 的关键除数
基线 baseline1024减去后才得到以 0 为参考的生理信号
数据格式212必须按字节位运算解码,不能直接 load

.hea里还标注了各通道的导联名,比如第一通道 MLII、第二通道 V1 或 V5。绘图时把导联名写进 ylabel 或标题里,图才有论文价值。我的习惯是每次打开新记录,先打印一遍.hea内容确认参数,再进绘图流程,这个习惯能省掉后面大半的排查时间。

3. matlab 绘制心电波形:从时间轴换算到多导联排版

读数据只是第一步。绘图阶段最常见的失败不是 MATLAB 画不了线,而是画出来的图不符合物理含义:横轴是采样点、纵轴是 ADC 码、窗口大到看不见单个心拍。这一章按“单导联最小绘图→双导联排版→频谱自检”三步展开,每一步都可以直接抄。

3.1 一条能进论文的波形:plot 的五个关键设置

把上一章的读取函数跑通后,画第一张图的代码可以这样写:

fs = 360; raw = read_mitbih_212('100.dat', 648000, 2); % 30 分钟双通道 ch1 = double(raw(1, 1:fs*10)); % 只取前 10 秒 t = (0:numel(ch1)-1) / fs; % 换算成秒 ch1_mV = (ch1 - 1024) / 200; % ADC 码转 mV figure; plot(t, ch1_mV, 'b', 'LineWidth', 0.8); xlabel('Time (s)'); ylabel('Amplitude (mV)'); title('Record 100, MLII, first 10 s'); grid on; xlim([0 10]);

这里面每个设置都有目的。取1:fs*10是只截取前 10 秒,避免 30 分钟 64 万个点全画出来变成一根黑带;double()转换防止后续减法运算溢出到整数类型;减 1024 是去基线,除 200 是还原成 mV。xlim([0 10])和截取窗口配合,保证图上能清楚看到 10 个左右的心拍。如果画出来 P 波和 T 波不明显,先检查是不是纵坐标没有换算,再检查是不是窗口太大把单个心拍压扁了。

3.2 双导联一起画:subplot 与 linkaxes 保持时间对齐

MIT-BIH 每条记录有两个导联,排版上我一般用上下两个 subplot,并把时间轴联起来,这样缩放一个另一个跟着动,对比两个导联的形态差异非常方便:

ch2 = double(raw(2, 1:fs*10)); ch2_mV = (ch2 - 1024) / 200; figure; subplot(2,1,1); plot(t, ch1_mV, 'b', 'LineWidth', 0.8); ylabel('MLII (mV)'); grid on; title('MIT-BIH Record 100'); subplot(2,1,2); plot(t, ch2_mV, 'r', 'LineWidth', 0.8); ylabel('V1 (mV)'); xlabel('Time (s)'); grid on; linkaxes(findall(gcf,'Type','axes'), 'x'); % 两个子图时间轴联动 xlim([0 10]);

linkaxes是这里最值得记的一个函数:不联动的话,用xlim放大一个子图,另一个还在原位,对比导联就失去了意义。颜色上我习惯第一通道蓝色、第二通道红色,便于区分。导联名直接从.hea读出来写进ylabel,比写Channel 1专业得多。如果两个通道幅值差异明显,可以给第二个子图单独设置ylim,别强行共轴,否则幅值小的导联看起来就是一条直线。

3.3 用频谱图验证数据质量:单边谱与工频干扰

绘图前先花 30 秒看一眼频谱,能避免把受干扰的数据画进论文。心电信号的能量集中在低频段,QRS 波群会贡献十几 Hz 附近的宽带分量。用 FFT 看单边幅度谱是最快的验证手段:

N = numel(ch1_mV); X = fft(ch1_mV - mean(ch1_mV)); % 去均值,消除 0 Hz 直流分量 f = (0:N-1) * fs / N; figure; plot(f(1:floor(N/2)), abs(X(1:floor(N/2)))); xlabel('Frequency (Hz)'); ylabel('Magnitude'); xlim([0 80]); grid on;

fft的结果是双边谱,取前半段就是 0 到奈奎斯特频率 180 Hz。减去均值非常重要,心电信号如果有直流偏置,0 Hz 会出现一个巨大峰值,把其他频率分量全部压扁。画完后重点看两件事:一是有没有在 50 Hz 或 60 Hz 附近出现针状峰,那是电源工频干扰,出现在图里需要滤波处理;二是能量是否集中在几十 Hz 以下,如果高频部分一片平坦,说明读入的可能不是心电信号,而是文件解析错了。

4. 标注数据可视化:把 beat 类型和 R 峰叠到波形上

波形图只是基础,MIT-BIH 最值钱的是.atr标注:文件里记录了每个心跳的位置和类型,比如 N 代表正常心拍,V 代表室性早搏,L 代表左束支阻滞。把标注叠到波形上,图立刻从“一条曲线”变成“有诊断信息的心电图”。

4.1 读标注并画类型线:rdann 的返回结构与颜色映射

读取标注最常用的工具是 WFDB Toolbox 里的rdann。不同版本返回格式略有差异,有的返回两个等长向量,有的返回结构体,但核心内容一致:一个时间数组(单位是秒),一个类型码数组。拿到这两个数组后,用垂直线叠加到波形上:

% 假设 rdann 返回时间向量和类型向量,按你的版本调整字段名 [timeVec, typeVec] = rdann('100', 'atr'); figure; plot(t, ch1_mV, 'b', 'LineWidth', 0.8); hold on; % 只画 2 到 12 秒内的标注,避免垂直线太密 inWin = timeVec >= 2 & timeVec <= 12; tt = timeVec(inWin); ty = typeVec(inWin); colors = containers.Map({'N','V','L','R','A'}, ... {'k','r','g','m','c'}); for i = 1:numel(tt) if isKey(colors, ty(i)) line([tt(i) tt(i)], ylim, 'Color', colors(ty(i)), ... 'LineWidth', 1.0); end end xlim([2 12]); xlabel('Time (s)'); ylabel('MLII (mV)');

ylim在plot之后调用,取的是当前坐标轴范围,所以垂直线会自动覆盖整个波形高度。containers.Map让类型到颜色的映射集中在表里,换颜色只改一处。画完后图面上会出现很多竖线,正常心拍是黑色,室早是红色,一眼就能看出异常位置分布。如果标注线和 QRS 波峰没有对齐,不要急着调代码,先确认timeVec的单位是不是秒,以及当前绘图窗口的时间范围是否和标注时间范围一致。

4.2 用 R 峰间隔画瞬时心率:diff 与 60./ 的配合

标注点本质上就是每个心拍的位置,相邻标注之间的时间差就是 RR 间期,也就是心拍间隔。瞬时心率的计算在 MATLAB 里只需要两行:

rr = diff(timeVec); % RR 间期,单位秒 hr = 60 ./ rr; % 瞬时心率,单位 bpm figure; plot(timeVec(2:end), hr, '.-', 'MarkerSize', 8); xlabel('Time (s)'); ylabel('HR (bpm)'); grid on; ylim([40 140]);

diff计算相邻标注的差值,60 ./ rr把秒换算成每分钟的心拍数。画出来的曲线会比普通心电图直观得多:心率平稳时是一条近似的水平线,室早出现时会看到先低后高的波动。如果曲线上出现突然冲到 200 以上的尖峰,大概率不是心率真的那么快,而是标注里混入了误检点,回到波形图上人工核对那一段最省事。

4.3 截取一个 PVC 心拍:窗口选择与形态对比

做算法验证时,经常需要把某个异常心拍单独截出来看。以室性早搏(类型 V)为例,我一般取它前 0.3 秒、后 0.6 秒的窗口,因为 PVC 的 QRS 波宽大畸形,需要多一点尾部观察 T 波是否倒置:

vIdx = find(typeVec == 'V'); if ~isempty(vIdx) mid = timeVec(vIdx(1)); % 第一个室早的时刻 winStart = max(round((mid - 0.3) * fs), 1); winEnd = min(round((mid + 0.6) * fs), numel(ch1_mV)); ttLocal = (winStart-1:winEnd-1) / fs; figure; plot(ttLocal, ch1_mV(winStart:winEnd), 'b', 'LineWidth', 1.0); hold on; line([mid mid], ylim, 'Color', 'r', 'LineStyle', '--'); title('First PVC beat in Record 100'); xlabel('Time (s)'); ylabel('mV'); end

窗口参数 0.3 秒和 0.6 秒不是拍脑袋定的:心电信号里正常 QRS 波宽约 0.08 到 0.12 秒,PVC 的 QRS 往往超过 0.12 秒,前 0.3 秒足够包含前面的 P 波,后 0.6 秒足够看到 ST 段和 T 波。如果截出来发现波形被切断,检查mid是否靠近记录开头或结尾,必要时把窗口缩短而不是硬截。

5. MIT-BIH 绘图避坑:时间轴、增益换算与导出时的五个高频事故

画图这个环节的坑,几乎每个人第一次碰 MIT-BIH 都会踩一遍。下面五条按“现象→原因→解决”写,全是实际处理中翻车概率最高的地方。

5.1 现象:横轴是 0 到 648000 的大数,波形被压成一条黑带

原因:时间轴直接用了采样点序号,没有除以采样率。呼吸、基线漂移被压缩到看不见,整段 30 分钟数据挤在一个图里。

解决:索引要单独生成时间向量,t = (0:N-1) / fs。注意这里fs必须从.hea读,不要拍脑袋写 360,虽然大部分记录确实是 360 Hz,但养成读头文件的习惯,换数据集才不会二次翻车。另外画图前先截取 10 秒窗口,比画完再xlim更省内存。

5.2 现象:波形看起来全是密集锯齿,放大后也看不清单个心拍

原因:一次性plot了全部 64 万个采样点,360 Hz 采样率下 30 分钟信号点距远小于像素尺寸,曲线糊成一片。

解决:先截取再绘图,seg = raw(1, 1:fs*10),只画 3600 个点。如果确实需要看长时程趋势,用plot配合xlim缩窗,但不要对全量数据直接出图。需要逐段翻阅时,用循环切窗口画,后续还会讲到。

5.3 现象:波形形态没问题,但纵坐标数值是几百甚至上千

原因:直接拿 ADC 码绘图,没有做(raw - baseline) / gain的物理量换算。MIT-BIH 的原始数据是 12 位 ADC 码,基线约 1024,增益常见 200 ADC/mV,不换算的话你画的是“机器码”而不是“毫伏”。

解决:一行代码搞定,mV = (double(raw) - baseline) / gain。基线优先从.hea解析,实在拿不到再按 1024 兜底,并在注释里标明。

5.4 现象:导出 PNG 模糊、白边巨大,投稿或写报告被退回

原因:直接saveas导出,保存的是屏幕分辨率,默认渲染质量低;纸张模式没设置,画布比例和屏幕不一致。

解决:固定用print命令,分辨率、格式、纸张三个参数一起控制:

set(gcf, 'PaperPositionMode', 'auto'); print('-dpng', '-r300', '100_first10s.png'); % 位图,300 dpi print('-depsc2', '100_first10s.eps'); % 矢量图,排版用

PaperPositionMode设为auto后,导出的画布大小跟着屏幕上的 figure 走,不会出现白边。PNG 适合报告和 PPT,EPS 矢量图适合 LaTeX 投稿,两个都导出是我出图的标准动作。-r300控制分辨率,低于这个数放大就会看到锯齿。

5.5 现象:标注竖线画在波形外面,或者所有标注整体偏移

原因:最常见的是标注时间轴和绘图窗口不一致,rdann返回的时间单位是秒,而绘图窗口横轴仍是采样点序号;其次是hold on之前ylim的顺序问题,导致垂直线用错了坐标范围。

解决:先统一时间单位,绘图用秒,标注也用秒,然后再画线。绘制竖线用line([t t], ylim)而不是手动指定 y 坐标,这样波形窗口怎么缩放,竖线都会自动盖满整个坐标轴高度。

6. 进阶:把绘图流程封装成函数,搞定批量出图和自检

撞过几轮坑之后,我把零散的绘图脚本收敛成了一个参数化函数。核心思路是:输入记录名和时间窗口,输出一张排版好、换算好、标注好的图。这样做最大的好处是,48 条记录可以循环批量出图,不需要每条手动改一遍代码。

function plot_ecg_record(recordName, tRange, opt) % recordName: 如 '100' % tRange: [startSec endSec] % opt.savePath: 非空则导出图片;opt.baseline: 默认 1024;opt.gain: 默认 200 if nargin < 3, opt = struct(); end if ~isfield(opt, 'baseline'), opt.baseline = 1024; end if ~isfield(opt, 'gain'), opt.gain = 200; end fs = 360; raw = read_mitbih_212([recordName '.dat'], 648000, 2); idx = round(tRange(1)*fs)+1 : round(tRange(2)*fs); t = (0:numel(idx)-1) / fs; ch1_mV = (double(raw(1, idx)) - opt.baseline) / opt.gain; figure; plot(t, ch1_mV, 'b', 'LineWidth', 0.8); hold on; [timeVec, typeVec] = rdann(recordName, 'atr'); inWin = timeVec >= tRange(1) & timeVec <= tRange(2); for k = find(inWin) line([timeVec(k) timeVec(k)], ylim, 'Color', 'k', 'LineWidth', 0.6); end xlabel('Time (s)'); ylabel('mV'); xlim(tRange); if isfield(opt, 'savePath') set(gcf, 'PaperPositionMode', 'auto'); print('-dpng', '-r300', [opt.savePath '.png']); end end

出图后我还会做三个自检:第一,看横轴单位是否为秒,纵轴是否为 mV 量级;第二,随机放大一个 QRS 波,确认 P-QRS-T 形态完整;第三,对比标注竖线和波形波峰是否对齐。这三个检查加起来不到一分钟,但能拦下大部分低级错误。

以前我有段时间图快,所有图一律saveas导出,结果投稿时被编辑退回说分辨率不足,返工重画浪费了整整一个晚上。那之后我给自己定了个规矩:宣传图和论文图分两套流程,论文图固定走 300 dpi 的 PNG 加一份 EPS,绝不临时改参数。做 MIT-BIH 绘图这件事,真正的产出不是某一张漂亮的波形图,而是一套“任何记录喂进来都能稳定出图”的流程。希望帮到你。

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

返回列表