
简介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(k1)); b2 double(bytes(k2)); s0 b0 * 16 bitand(b2, 15); % 第一通道样本 s1 b1 * 16 bitshift(b2, -4); % 第二通道样本 ecg(1, idx) s0; ecg(2, idx) s1; end endb0 * 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就能把它读进 MATLABfid 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 适合报告和 PPTEPS 矢量图适合 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: 默认 1024opt.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 绘图这件事真正的产出不是某一张漂亮的波形图而是一套“任何记录喂进来都能稳定出图”的流程。希望帮到你。本文还有配套的精品资源点击获取