news 2026/10/17 16:59:51

MIT-BIH心电数据解析与MATLAB绘图:从二进制格式到波形可视化全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIT-BIH心电数据解析与MATLAB绘图:从二进制格式到波形可视化全流程

简介:MIT-BIH ECG心电数据集与MATLAB绘图详解是一份面向心电图研究初学者与进阶用户的入门实操资源,聚焦临床心律失常数据读取与可视化分析。资源包内共4个文件,涵盖MAT格式心电数据、MATLAB绘图脚本、辅助脚本及数据说明文件,能帮助读者在MATLAB环境中直接加载并绘制心电信号波形,识别P波、QRS复合波与T波等关键特征,为进一步计算心率、QT间期及开展心率变异性分析打下基础。压缩包整体仅20KB,轻量易用,适合信号处理课程设计、科研入门或工程复现。目前已有7597人学习使用,可见其在ECG分析场景中的实用价值。通过这份资料,读者可以掌握从数据读取、波形显示到基础特征提取的完整流程,同时理解采样率、奈奎斯特定理等核心概念,从而更高效地开展心脏电生理研究、滤波降噪和自动QRS检测等后续任务。

1. MIT-BIH ECG 心电数据 + MATLAB 绘图:别在格式解析上浪费一周

你从公开镜像下载了 MIT-BIH 心电记录,满心期待打开文件夹,发现全是 .dat、.hea、.atr 这类没有见过的文件,没有现成的 CSV,第一张波形愣是画不出来。这不是个例。MIT-BIH 的二进制存储格式和普通文本完全不一样,采样率换算、基线偏移、增益处理这些细节只要错一个,画面就是一片噪点或者一条悬在半空的直线。这份资源把数据样本和 MATLAB 绘图脚本打包在一起,覆盖了 212 格式解析、导联参数读取、注释叠加、滤波前后对比这整条流程,你拿到手改几个路径就能跑,不用从零啃格式文档。适合做心电算法验证、课程设计需要出波形图、以及想复现论文实验图的同学和工程师。下面按我实际拆这个数据包的过程,把每一道坎都讲清楚。

2. 先搞懂三个文件:HEA 是配方,DAT 是波形,ATR 是注释

2.1 一条记录长什么样:采样率 360 Hz、双导联、30 分钟

每条 MIT-BIH 记录,本质上是某个受试者 30 分钟的双导联心电,采样率 360 Hz。360 这个数字不是随手定的,它有两个实用价值:一是能覆盖心电信号 0.05~100 Hz 的有效频段;二是刚好是 60 Hz 工频的整数倍,做工频陷波时相位对齐非常方便。我一般拿到一条记录,先确认三个文件是否齐全。

文件后缀内容在 MATLAB 绘图里的角色
.hea文本格式的头部描述:采样率、导联数、增益、基线提供 fs、gain、baseline 参数
.dat二进制波形数据,212 编码画波形的主体数据
.atr二进制注释数据,变长结构叠加心拍标记、节律标记

.hea 是说明书,.dat 是真正干活的波形文件,.atr 是心电专家预先标注好的心拍位置和类型,比如正常心拍 N、室性早搏 V、房性早搏 A。画一张合格的心电波形图,波形来自 .dat,标注来自 .atr,坐标刻度来自 .hea。三者对应关系理清了,后面所有操作都不会乱。

2.2 HEA 头文件里藏着绘图需要的一切参数

打开任意一个 .hea 文件,内容类似这样,我删掉了部分不常用字段:

100 2 360 650000 100.dat 212 200 11 1024 9952 100.dat 212 200 11 1024 9952

第一行四个字段依次是:记录名、导联数、采样率、采样点数。100 号记录双导联、360 Hz、65 万个采样点,对应 1800 秒乘 360 Hz,约 30 分钟时长。第二行开始是逐导联描述,这里我重点讲三个绘图必用的参数:格式 212、增益 200、基线 1024。

增益 200 的含义是 200 个 ADC 单位对应 1 mV。也就是说 .dat 里读出来的原始数值要除以 200,才是物理单位毫伏。基线 1024 表示零幅值对应的 ADC 读数,采集时心电信号叠加了一个直流偏置,把整个波形抬高到了 1024 附近。绘图前不把这个偏置减掉,波形会悬在距离零线 1024 的位置,看起来像加了 5 mV 的直流电平,QRS 形态完全走样。

我习惯在脚本开头写一个读 HEA 的小函数,把 fs、gain、baseline 读成变量,而不是写死在代码里。因为 100 号记录的增益和 101 号记录可能不同,写死参数换一条记录就得改源码,这个亏我吃过。

function [fs, gain, baseline] = read_hea_info(heaFile) fid = fopen(heaFile, 'r'); if fid == -1 error('打不开 HEA 文件: %s', heaFile); end % 第一行:记录名 导联数 采样率 样本数 line1 = fgetl(fid); parts = strsplit(strtrim(line1)); fs = str2double(parts{3}); % 第二行:第一个导联的格式、增益、位数、基线 line2 = fgetl(fid); parts2 = strsplit(strtrim(line2)); gain = str2double(parts2{3}); baseline = str2double(parts2{5}); fclose(fid); end

逻辑说明:第一行第三个字段是采样率,第二行第三个字段是增益,第五个字段是基线。代码按标准两导联格式取数,两个导联增益一致时完全够用。参数说明:如果遇到某个记录的第二个导联增益和第一个不同,需要把第二行也存进数组,按导联号索引。这个函数返回的三个变量,后面解析和绘图都要用。

3. WFDB 工具箱路线:十分钟跑通第一张波形图

3.1 安装与路径配置:装完先验证 rdsamp 能读通

WFDB 工具箱是读取 MIT-BIH 最省事的路线,它把 212 格式拆包、基线偏移、增益换算、注释读取全部封装好了,不需要自己处理底层字节。安装方式有两种。第一种是把工具箱文件夹放到任意位置,然后用 addpath 引入:

addpath('D:\toolbox\wfdb\matlab'); savepath;

savepath 是为了下次启动 MATLAB 不用重新 addpath。如果当前目录没有写权限保存失败,就在工程入口脚本里每次启动时 addpath。我一般单独建一个 init.m 管理所有工具路径,换电脑迁移工程时不丢路径。

第二种方式是用 MATLAB 附加功能界面图形化安装,装完即用。装好之后先验证再画图:

[~, sig] = rdsamp('100', [], 1); % 读 100 号记录第一导联全部数据 size(sig)

如果 size 返回的是几十万行的一列,说明读取正常。不同版本工具箱的 rdsamp 参数顺序可能有差异,保险起见先 help rdsamp 确认一下签名。

3.2 用官方函数画第一张图:时间轴必须除以采样率

读数据之后画图的代码非常短,但有两个细节常被忽略:

[~, sig] = rdsamp('100', [], 1); fs = 360; t = (0:length(sig)-1) / fs; figure; plot(t, sig, 'k', 'LineWidth', 0.6); xlim([0 10]); % 只看前 10 秒,画整条会压成黑带 xlabel('时间 (s)'); ylabel('幅值 (mV)'); grid on;

逻辑说明:rdsamp 返回的 sig 已经完成基线和增益换算,单位是 mV,所以 y 轴直接标 mV。时间轴必须除以 360,否则横轴是样本索引,和真实时间对不上,后续叠加注释时也会错位。参数说明:xlim 限制到 0~10 秒是关键操作。心电波形里 QRS 群、ST 段的细节都在几十到几百毫秒尺度,10 秒窗口能看 20 个左右心拍,既能观察节律,又能看清单拍形态。画整条 30 分钟波形只会得到一团黑。

3.3 画图前先看 min / max:数据范围能筛掉一半问题

我拿到任何一条记录,画图之前都会先打印数据范围:

fprintf('min = %.3f mV, max = %.3f mV\n', min(sig), max(sig));

正常心电信号经增益换算后,幅值应该在 ±1 mV 量级,剧烈情况下也不超过 ±5 mV。如果 min 和 max 是几十甚至几千,说明数据还没正确转成物理值。常见原因有三个:记录本身的格式不是 212,比如 108 号记录是 311 格式;rdsamp 调用时没指定导联,返回了双导联拼接的矩阵;或者工具箱版本较老,返回值顺序不一样。上次给 A同学调代码,他读 108 号记录一直得到一个 60 万行的大矩阵,后来才发现是忘写导联参数,工具箱默认把双导联 130 万样本全返回了。

官方工具箱能覆盖读数据、画波形、加注释这三件事,适合快速出图验证。但它不能解决所有问题,比如自定义批处理、特殊格式记录、滤波流程嵌入,这些还是需要理解底层格式。第 4 章的手写解析器就是干这个的。

4. 手写解析器:从 DAT 字节流到 MATLAB 波形

4.1 212 格式拆包:三字节装载两个导联样本

MIT-BIH 最常见的 .dat 文件使用 212 格式存储,意思是每 3 个字节存放两个 12 位样本,两个导联交错打包。具体排列是:第一个字节作为低 8 位,第二个字节的低 4 位作为高 4 位,组成导联 1 的样本;第二个字节的高 4 位作为低 4 位,第三个字节的 8 位左移 4 位作为高 8 位,组成导联 2 的样本。整条波形就是这样一组三字节一路排下来的。

function [ecg1, ecg2] = read_dat_212(filename, numSamples) fid = fopen(filename, 'rb'); if fid == -1 error('无法打开 DAT 文件: %s', filename); end % 每组三字节对应同一时刻的两个导联样本 groupCount = numSamples; raw = fread(fid, [3, groupCount], 'uint8')'; fclose(fid); b1 = uint16(raw(:, 1)); b2 = uint16(raw(:, 2)); b3 = uint16(raw(:, 3)); % 导联1:b1 全部 8 位作低位,b2 低 4 位作高位 ecg1 = b1 + bitshift(bitand(b2, 15), 8); % 导联2:b2 高 4 位作低位,b3 全部 8 位左移 4 位作高位 ecg2 = bitshift(b3, 4) + bitshift(b2, -4); ecg1 = double(ecg1) - 1024; ecg2 = double(ecg2) - 1024; end

逻辑说明:fread 按 [3, groupCount] 把整个文件切成三行多列,每列是一组三字节,之后按列拆成 b1、b2、b3 三个向量。ecg1 和 ecg2 各得到 numSamples 个 12 位整数,这是两个导联完整的采样序列。最后减 1024 是去掉 HEA 文件里写明的基线偏移,让零幅值回到零点。

参数说明:这里有三个容易被忽略的细节。一是 bitshift 作用于 uint8 时左移会截断到 8 位,必须先把字节转成 uint16,否则 b3 左移 4 位后高 4 位全部丢失,这是 MATLAB 的典型坑。二是 numSamples 是单导联的样本数,也就是 HEA 第一行的 650000,不是双导联总数。三是减 1024 后得到的仍是 ADC 计数值,绘图时还要再除以增益才能标 mV。

4.2 校验解析结果:和官方工具箱对拍一次

手写解析最容易出隐性问题,最稳的验证方法是用官方工具箱做对照。读同一条记录、同一个导联,把前几秒的波形叠在一起:

[~, sigOfficial] = rdsamp('100', [], 1); [ecg1, ~] = read_dat_212('100.dat', 650000); figure; plot(ecg1(1:3600)/200, 'b'); hold on; plot(sigOfficial(1:3600), 'r', 'LineWidth', 0.4); legend('手写解析', '官方工具箱');

如果两条线完全重合,说明拆包逻辑正确。如果不重合,先看差值形态:差一个固定常数,说明基线没减干净;差一个比例,说明增益没除;波形完全错位,说明字节拼接方向反了。这个方法比对着格式文档猜快得多,我把这个对拍习惯保留到了现在。

第 3 章用官方工具箱,第 4 章又手写一套解析,看上去重复,但各有用途。官方工具箱适合快速出图,手写解析器适合嵌入批量流程。比如要跑 50 条记录、逐条提取 ST 段特征,用工具箱逐条调用会产生大量重复 IO,手写解析器读一次 HEA 参数就能循环处理所有文件,速度差一个量级。

4.3 顺手处理基线漂移:一个移动平均就够

心电信号自带呼吸引起的基线漂移,频率很低,大约在 0.1~0.5 Hz。绘图前不处理,波形会像波浪一样上下起伏,ST 段判断很容易被干扰。我用移动平均估计漂移,再从原信号里减去:

windowSize = round(fs * 0.5); % 0.5 秒窗口,360 Hz 时等于 180 baselineDrift = movmean(ecg1, windowSize); ecg1Clean = ecg1 - baselineDrift;

逻辑说明:movmean 在 0.5 秒窗口内求局部平均,得到的是一条贴着原始波形缓慢变化的曲线,也就是基线漂移的估计。原信号减去它,基线基本回零。参数说明:windowSize 用 fs 乘 0.5 秒计算,换采样率时公式依然成立。注意移动平均只能处理低频呼吸漂移,不要拿它替代带通滤波,否则 QRS 的高频成分也会被削平,具体坑在第 5 章讲。

5. 避坑指南:五个让新手翻车的格式细节

5.1 波形整体反向或数值乱跳:bit 拼接方向反了

现象:手写解析画出来的波形像镜像,QRS 主波朝下,或者数值在正负几千之间乱跳。

原因:212 格式里 12 位样本的拼接顺序写反了。常见错误是把 ecg1 写成了 b1 右移加 b2 左移,把高低位颠倒;另一个隐患是没把 uint8 转成 uint16,bitshift 左移直接截断丢位,数值范围立刻错乱。

解决:先打印前 20 个样本的数值,正常应该在 1024 附近上下波动。如果数值范围不在 0~2047 之间,先检查字节类型转换,再检查拼接方向。对照官方工具箱对拍是最终的确认手段。

5.2 时间轴错位:样本索引被当成秒

现象:画出来的横轴刻度是 0 到 650000,波形密集到看不清,单看一个心拍要手动算很久。

原因:直接用样本索引做了 x 轴,没有除以采样率。360 Hz 下 1 秒是 360 个点,索引和秒数差了 360 倍。

解决:t = (0:length(sig)-1) / fs。横轴换成秒之后,10 秒窗口能自然显示 10 个心拍,单个 PQRST 的宽度一目了然。这条规则对所有生理信号绘图都适用,不只是 MIT-BIH。

5.3 ATR 注释解析翻车:别自己硬读可变长结构

现象:rdann 读注释很顺利,但自己 fopen 读 .atr 时,注释位置和波形完全对不上,越往后偏差越大。

原因:.atr 用的是变长注释结构,带辅助信息的注释比普通注释多几个字节,逐字节硬解析时偏移量会积累错误。注释时间戳的单位是采样点序号而不是秒,也容易混。

解决:用官方 rdann 读注释,它内部处理了变长结构。拿到的是采样点索引,绘图时注意 MATLAB 数组下标从 1 开始,而注释索引是 C 风格从 0 开始,画图时索引加 1 才是对应数据点。

5.4 全图变成一团黑:65 万点挤在一个 figure 里

现象:plot 整条 30 分钟波形,图形窗口里只有一条浓黑曲线,放大后还有明显锯齿。

原因:一个像素宽的线条根本画不下 65 万个数据点,大量数据点叠在同一列像素上,波形糊成一条黑带。

解决:画细节图时先用 xlim 限制到 5~10 秒;画全局概览图时先降采样,用 decimate(sig, 10) 把采样率降到 36 Hz 再画,能看到整体节律趋势。我习惯细节图和概览图分开输出,一张看形态,一张看全貌。

5.5 滤波后 QRS 没了:窗口选得太长

现象:做移动平均滤波后,R 波明显变矮甚至消失,波形变得平滑但心拍特征没了。

原因:移动平均窗口选了 1 秒甚至更长,把 QRS 的高频成分当成噪声一起抹掉了。QRS 持续约 80~120 毫秒,窗口太长必然削平。

解决:移动平均只用于基线漂移估计,窗口 0.3~0.5 秒;真正的心电滤波用 butterworth 带通,比如 butter(2, [0.5 45]/(fs/2)),保留 0.5~45 Hz 这个有效频段。滤波后务必做一次幅值范围检查,确认 R 波幅度没有明显下降。

6. 进阶绘图:从裸波形到可发表的注释图

6.1 叠加 ATR 注释:让心拍标记和波形对齐

基础波形画出来之后,最实用的进阶操作是把 ATR 注释叠到图上,这样能直接看到每个心拍的专家标注位置。我用的是官方 rdann 加手写波形混搭的方式:

[annIdx, annType] = rdann('100', 'atr'); annIdx = annIdx + 1; % C 风格索引转 MATLAB 下标 tAnn = annIdx / fs; figure; plot(t, ecg1/gain, 'k'); hold on; % 先用圆点标出所有注释位置,形态和类型再做筛选 plot(tAnn, ecg1(annIdx)/gain, 'bo', 'MarkerSize', 4); xlim([0 10]);

逻辑说明:rdann 返回的 annIdx 是采样点索引,转换为 MATLAB 下标要加 1。ecg1(annIdx) 直接从波形数据里取对应幅值,这样注释点和波形天然对齐。参数说明:先全量标注观察分布,再按 annType 筛选具体心拍类型,不同类型的映射关系以工具箱文档为准。只看一个心拍时,把 xlim 改成 [5 6], 1 秒窗口内能看到完整的 P 波、QRS 群和 T 波。

这条路走通之后,批处理也顺理成章:循环读取记录、读取注释、切片提取单拍特征,全部可以脚本化。我自己刚开始接触这个数据集时,在某条记录上卡了两天,最后发现就是 bit 位拼错了一个方向。从那以后,每次拿到新记录都强制走一遍固定流程:先读 HEA 校验采样率和增益,再画前 10 秒看波形形态,再叠注释对 QRS,最后才敢说这条数据解析正确。这套配套脚本已经是按这个顺序整理好的,下载后把记录路径改成你本地的文件夹位置,第一张图就能直接出。希望帮到你。

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

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

wxid转二维码:用Python生成微信联系人二维码的完整方案

简介:工具包采用纯前端方案,可将微信用户个人标识wxid转换为带weixin://contacts/前缀的好友链接二维码,主要面向需要找回误删好友、或已知对方wxid但未保存聊天的普通用户,也适合客服人员和社群运营者临时应急使用。它借助qrcode…

作者头像 李华
网站建设 2026/10/17 16:57:16

CAA R19+VS2005+CATIA V5R19旧环境配置实战指南

简介:本资源是一份面向CATIA二次开发初学者的完整环境配置指南,聚焦CAA与RADE两大核心开发平台在CATIA V5R19下的集成部署,解决高校学生、制造业数字化研发人员在入门阶段普遍面临的软件安装混乱、依赖缺失、破解失效及路径配置错误等实际难题…

作者头像 李华
网站建设 2026/10/17 16:56:11

工程BOM、制造BOM、采购BOM的区别与协同:制造企业BOM体系详解

同样的产品,研发拿着图纸清单(工程BOM)做设计评审,工艺部门按加工装配顺序整理出一份制造BOM下发车间,采购那边又单独维护一份外购件清单下单管供应商。很多人第一次接触制造业的数据管理时会问同一个问题:…

作者头像 李华
网站建设 2026/10/17 16:56:10

Multi-Agent系统实战:从架构选型到工程落地的完整指南

1. 从大模型到多智能体:为什么单个LLM不够用这两年聊LLM应用,大家已经从“怎么调Prompt”跳到了“怎么搭系统”。我自己的体会是,单靠一个大模型去完成复杂任务,就像让一个全能型员工包揽所有脏活累活——不是不行,但很…

作者头像 李华
网站建设 2026/10/17 16:56:09

vibe coding实操指南:自然语言驱动开发的方法与工具选型

最近半年,圈子里高频冒出来的词就是 vibe coding,翻译成大白话就是“自然语言驱动开发方法”——你负责描述需求和拍板方向,AI 负责把自然语言变成能跑的代码。我刚接触时也怀疑过这是不是又一波概念炒作,但实际用了几个月之后&am…

作者头像 李华
网站建设 2026/10/17 16:55:40

Bat转Exe封装工具实战:原理、避坑与自动化打包指南

简介:Bat-To-Exe-Converter 是一款面向 Windows 管理员与脚本编写者的批处理打包工具,可将 bat 脚本封装为独立 exe 文件,解决脚本分发时用户不熟悉命令行、运行门槛高的问题。资源包共 5 个文件,以 exe 主程序、settings 配置、t…

作者头像 李华