MIT-BIH心电数据MATLAB读取绘图与R峰检测实战 简介这份资源面向心电信号处理初学者与科研入门者围绕MIT-BIH ECG数据库展开解决从数据读取到波形可视化的实操问题。压缩包共4个文件约20KB包含mat数据文件、m脚本、info说明与asv备份其中mat文件用于在MATLAB中加载心电矩阵数据m脚本负责绘图与处理info记录采样率等元信息便于快速复现分析流程。内容涉及采样率与奈奎斯特定理、P波/QRS复合波/T波识别、RR间期与心率变异性分析等知识点并演示滤波去噪、QRS自动检测等常见处理思路。已有7600人学习下载适合希望借助MATLAB上手ECG数据探索、理解心电波形特征与基础信号处理流程的读者参考。1. 从一条平直的心电波形说起这套 MIT-BIH 数据加 MATLAB 绘图到底能干什么很多人第一次拿到 MIT-BIH 心电数据时都会经历一个相同的瞬间用load把文件读进来plot一画屏幕上出现一条几乎平直的线心里咯噔一下——数据坏了其实不是数据坏了是你还没搞懂它的量纲和通道结构。MIT-BIH 心律失常数据库是心电算法领域被引用最多的公开数据集之一每条记录包含两路导联信号、采样率 360 Hz、11 位分辨率还附带逐拍的心搏注释。它最常被用来做 QRS 检测、心律失常分类、心率变异性分析这些任务的验证集。而 MATLAB 恰好是处理这类信号最顺手的工具读数据、滤波、定位 R 峰、画标注图一套脚本就能跑通。这套资源适合两类人一类是刚接触生理信号处理、想找一个能跑通全流程的入门项目另一类是做算法验证、需要一个稳定基准数据来对比自己方法的从业者。下面我按实际拆包复现的顺序把读数据、画波形、标 R 峰、避坑这几件事讲透。2. 读懂 MIT-BIH 的文件格式从 .dat 到 .atr 到底存了什么2.1 三个文件各管什么MIT-BIH 每条记录通常由三个文件组成扩展名不同职责也不同。.dat存的是二进制采样值.hea是头文件描述这条记录的导联数、采样率、采样点数、增益、ADC 分辨率等信息.atr存的是专家标注的心搏位置和类型。很多人只盯着.dat结果画出来的波形幅度不对、时间轴也不对问题就出在没读.hea。头文件里有两个参数特别关键gain和baseline。原始采样值是整数要还原成毫伏得用(sample - baseline) / gain。不同记录的 gain 可能不一样常见是 200 ADC units/mV但你不能硬编码必须从头文件里读。2.2 用 MATLAB 读头文件和二进制数据MATLAB 没有内置直接读 MIT-BIH 格式的函数但 WFDB 工具箱可以或者你自己写解析脚本。我一般先用一个轻量函数把头文件解析出来再按头文件里的参数去读.dat。下面这段代码是我常用的读取方式不依赖额外工具箱纯 MATLAB 实现。function [signal, fs, gain, baseline, sigName] read_mitbih(recordPath) % read_mitbih 读取 MIT-BIH 记录的 .hea 和 .dat 文件 % recordPath: 不带扩展名的记录路径例如 100 % 输出: signal 为 N×M 矩阵fs 采样率gain/baseline 用于还原毫伏 heaFile [recordPath .hea]; datFile [recordPath .dat]; fid fopen(heaFile, r); if fid -1 error(找不到头文件: %s, heaFile); end % 第一行格式: recordName sigCount fs sigLen ... firstLine fgetl(fid); tokens strsplit(strtrim(firstLine)); sigCount str2double(tokens{2}); fs str2double(tokens{3}); gain zeros(sigCount, 1); baseline zeros(sigCount, 1); sigName cell(sigCount, 1); % 后续每行描述一个导联 for k 1:sigCount line fgetl(fid); parts strsplit(strtrim(line)); sigName{k} parts{9}; % 导联名称如 MLII、V1 gainVal parts{3}; % 形如 200 gain(k) str2double(gainVal); baseline(k) str2double(parts{5}); % 基线值 end fclose(fid); % 读取二进制 .dat格式为 16 位有符号整数交替存储各导联 fid fopen(datFile, rb); raw fread(fid, [sigCount, inf], int16); fclose(fid); raw raw; % 转成 采样点 × 导联 % 还原为毫伏 signal (double(raw) - baseline) ./ gain; end这段代码的逻辑很直接先解析头文件拿到导联数、采样率、增益和基线再按int16格式读二进制。注意fread的第二个参数[sigCount, inf]它保证按导联交错的方式读入读完后转置成「采样点 × 导联」的矩阵。参数说明gain是每个导联的增益baseline是 ADC 零点对应的整数值这两个值必须逐导联使用不能混。如果你读的是100号记录第一导联通常是 MLII第二导联是 V1画图时建议分开画不要叠在一起。2.3 采样率与时间轴对齐MIT-BIH 的采样率是 360 Hz这个值从头文件读出来是 360但有些衍生版本可能被重采样过。画时间轴时横坐标用(0:length(sig)-1)/fs得到秒数。很多人直接用样本序号当横坐标结果算心率时单位全乱。记住一个换算360 个采样点对应 1 秒一个典型 QRS 波群宽度大约 80 到 100 毫秒也就是 29 到 36 个采样点。这个数量级心里有数后面定位 R 峰时就不会把噪声当成心搏。3. MATLAB 绘图实战从原始波形到带标注的 R 峰图3.1 先画原始信号确认量纲和通道拿到signal矩阵后第一件事不是急着滤波而是先画原始波形确认幅度在合理范围。正常心电信号在毫伏级别QRS 峰大约 1 到 2 mVP 波和 T 波更小。如果你画出来幅度是几百甚至几千说明 gain 没用对如果是一条直线可能是 baseline 减错了或者读错了数据类型。% 读取记录并绘制原始波形 record 100; % 记录编号按实际数据替换 [sig, fs, gain, baseline, names] read_mitbih(record); t (0:size(sig,1)-1) / fs; % 时间轴单位秒 figure(Name, MIT-BIH 原始波形); for k 1:size(sig,2) subplot(size(sig,2),1,k); plot(t, sig(:,k), b); xlabel(时间 (s)); ylabel(幅值 (mV)); title(sprintf(导联 %s, names{k})); grid on; xlim([0, min(10, t(end))]); % 先看前 10 秒 end这里只画前 10 秒是因为整条记录可能长达 30 分钟全画出来屏幕上一团黑看不出细节。subplot把两个导联分开避免相互遮挡。如果你发现某个导联幅度异常大先检查该导联的gain和baseline是否读对再检查.dat文件是否完整。3.2 带通滤波与 R 峰检测原始信号里通常有基线漂移和工频干扰直接做峰值检测会误判。常见做法是先做 0.5 到 40 Hz 的带通滤波再做大致的 R 峰定位。MATLAB 里可以用bandpass或者自己设计 Butterworth 滤波器。下面这段代码演示滤波加一个简单的阈值法 R 峰检测目的是让你看到标注效果不是生产级算法。% 带通滤波 0.5-40 Hz order 4; [b, a] butter(order, [0.5 40]/(fs/2), bandpass); sigFilt filtfilt(b, a, sig(:,1)); % 对第一导联滤波 % 简单的 R 峰检测差分 平方 移动平均 阈值 diffSig [0; diff(sigFilt)]; squared diffSig .^ 2; win round(0.15 * fs); % 150 ms 窗口 ma movmean(squared, win); threshold mean(ma) 2 * std(ma); [pks, locs] findpeaks(ma, MinPeakHeight, threshold, ... MinPeakDistance, round(0.25 * fs)); % 把检测点映射回滤波信号 rPeaks locs; figure(Name, R 峰检测结果); plot(t, sigFilt, b); hold on; plot(t(rPeaks), sigFilt(rPeaks), ro, MarkerSize, 6); xlabel(时间 (s)); ylabel(幅值 (mV)); title(滤波后信号与 R 峰标注); legend(滤波信号, 检测到的 R 峰); grid on; xlim([0, min(10, t(end))]);逻辑说明filtfilt做零相位滤波避免波形时移差分和平方是为了突出 QRS 的高频高幅特征移动平均平滑后用均值加两倍标准差做阈值MinPeakDistance限制两个 R 峰至少间隔 250 毫秒防止把 T 波误判成 R 波。参数方面win取 150 毫秒是经验值对应 QRS 宽度阈值系数 2 可以根据信号质量调整噪声大就调到 2.5 或 3。这套方法在 MIT-BIH 的多数记录上能检出九成以上的心搏但遇到严重噪声段仍会漏检或误检所以只适合做可视化验证不适合直接当最终算法。3.3 叠加专家标注做对比MIT-BIH 的.atr文件里存了专家标注读出来和你的检测结果画在一起才能看出差距。.atr是二进制格式每个标注占两个 16 位字解析稍麻烦。如果你有 WFDB 工具箱用rdann最省事没有的话可以先用一个简化解析器读标注位置。% 简化读取 .atr 中的标注位置仅取位置不解析类型 function annLocs read_atr_locs(recordPath) fid fopen([recordPath .atr], rb); raw fread(fid, inf, uint16); fclose(fid); % 每两个 uint16 组成一个标注第一个字低 10 位是时间间隔 annLocs []; idx 1; current 0; while idx 1 length(raw) word1 raw(idx); word2 raw(idx 1); timeDiff bitand(word1, 1023); % 低 10 位 current current timeDiff; annLocs(end1) current; %#okAGROW idx idx 2; end end这段解析只取了时间位置跳过了标注类型够用来画对比图。把annLocs和你的rPeaks画在同一张图上用不同颜色区分一眼就能看出漏检和误检。注意.atr里的时间单位是采样点不是秒画图时要除以fs。4. 避坑与排查读数据画图时最容易翻车的五个地方4.1 波形是一条直线现象plot出来几乎没变化幅度接近零。原因最常见的是把int16读成了uint16或者baseline减错导致数值被压平。解决检查fread的精度参数是否为int16再核对头文件里的baseline是否逐导联使用。如果还不行打印原始raw的前 20 个值看是不是全零或全同一个数。4.2 幅度单位不对心率算出来离谱现象波形形状对但幅度是几百 mV或者算心率时得到几百 bpm。原因没有除以gain或者时间轴用了样本序号而不是秒。解决确认signal (raw - baseline) / gain这一步做了且gain来自头文件而不是猜的。时间轴统一用(0:N-1)/fs。4.3 R 峰检测把 T 波也标上了现象一张图里标注点数量几乎是实际心搏的两倍。原因阈值太低或者MinPeakDistance设得太小。解决把MinPeakDistance设为 0.25 秒对应的采样点数阈值系数从 2 提到 2.5 到 3。如果还不行先检查滤波是否到位基线漂移没滤干净时移动平均曲线会整体抬高阈值失效。4.4 两个导联画在一起互相遮挡现象一张图里两条线重叠看不出各自形态。原因用了plot(t, sig)直接画矩阵MATLAB 会把每列当一条线画在同一坐标轴。解决用subplot分开画或者用plot(t, sig(:,1), t, sig(:,2))并加legend。更推荐分开画因为两个导联幅度可能差很多共用纵轴会压扁其中一个。4.5 读.atr时标注位置整体偏移现象专家标注点和你画的波形对不上整体差一个固定值。原因.atr解析时时间间隔累加起点搞错或者把两个字的位置搞反了。解决确认每个标注由两个uint16组成时间间隔在第一个字的低 10 位累加时从 0 开始。如果用的是 WFDB 工具箱直接用rdann并指定ann类型不要自己手写解析。5. 进阶技巧把单条记录脚本改成批量处理与结果导出单条记录跑通之后下一步通常是把几十条记录批量跑一遍把 R 峰位置和心率统计导出成表格。我一般会写一个循环遍历记录编号列表每条记录调用读取和检测函数把结果存到一个结构体数组里最后用writetable导出 CSV。这里有个细节不同记录的导联名称可能不同有的第一导联是 MLII有的是其他检测前最好判断一下用哪个导联。常见做法是优先选 MLII如果没有就选第一个导联。records {100, 101, 102, 103}; % 按实际数据替换 results struct(record, {}, numBeats, {}, meanHR, {}); for i 1:length(records) rec records{i}; [sig, fs, ~, ~, names] read_mitbih(rec); % 选择导联优先 MLII leadIdx 1; for k 1:length(names) if strcmpi(names{k}, MLII) leadIdx k; break; end end sigFilt filtfilt(b, a, sig(:, leadIdx)); % ... 此处复用前面的 R 峰检测逻辑 ... rPeaks locs; % 假设已得到 numBeats length(rPeaks); if numBeats 1 rr diff(rPeaks) / fs; % RR 间期秒 meanHR 60 / mean(rr); % 平均心率 else meanHR NaN; end results(i).record rec; results(i).numBeats numBeats; results(i).meanHR meanHR; end T struct2table(results); writetable(T, mitbih_summary.csv); disp(T);这段代码的关键点有三个一是导联选择逻辑避免硬编码列号二是 RR 间期用采样点差值除以fs得到秒再换算心率三是用struct2table加writetable导出方便后续在 Excel 或 Python 里做统计。参数上records列表按你实际下载的数据替换不要照抄。跑批量时建议加一个try-catch某条记录读失败不至于中断整个循环。另外批量跑之前先确认所有记录的.hea、.dat、.atr三个文件都在同一目录下缺一个就会报错。我自己的习惯是每次换数据目录先跑一条记录确认路径和文件名大小写没问题再放开批量。从那以后我每次拿到新的心电数据包都强制先跑一条最短的记录做冒烟测试确认读取、滤波、画图三步都正常再写循环。希望帮到你。本文还有配套的精品资源点击获取