news 2026/10/1 7:13:26

MIT-BIH心电数据库读取指南:.hea/.dat/.atr格式全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIT-BIH心电数据库读取指南:.hea/.dat/.atr格式全解析

做心电信号分析的人,十有八九绕不开MIT-BIH这个数据库。它是心电领域最经典的公开数据集,也是心律失常检测算法绕不开的评测基准。很多人第一次接触它时,第一反应都是:数据从哪下载?怎么读取?那些.hea、.dat、.atr文件到底是干什么的?这篇文章就把这些文件从头到尾拆一遍,顺便给出完整的读取过程和避坑经验。不管你是要做信号处理、训练深度学习模型,还是做心率变异性分析,读完都能直接上手。

MIT-BIH心律失常数据库由美国麻省理工学院与Beth Israel医院合作建立,目前挂在PhysioNet平台上公开下载。整套数据包含48条时长约30分钟的双通道动态心电图记录,采样率360Hz,总标注心拍超过11万个。它之所以被广泛使用,是因为每一条记录都带有人工专家逐心拍的标注——你既可以从里面提取训练样本,也可以把它当作测试算法的“标准答案”。这篇文章的目标读者很明确:刚接触MIT-BIH、搞不清文件格式、读取时反复报错的人。

1. 项目概述:这套数据到底解决什么问题

1.1 什么是MIT-BIH,为什么它是心电分析的“默认答案”

MIT-BIH并不是一个简单的波形文件集合,而是一套经过精心设计、带标注的临床数据库。它收录了48条记录,每一条都来自一位真实的受试者,覆盖了正常窦性心律、室性早搏、房性早搏、束支传导阻滞、起搏心律等多种心律失常类型。受试者的年龄分布很广,男性和女性都有,这使得数据具备不错的多样性,做出来的算法不会只对某类人群有效。

为什么说它是“默认答案”?因为心电算法要评估好坏,必须有一个大家都能跑、指标可横向对比的数据集。MIT-BIH刚好满足这个条件:数据公开、标注精准、格式固定、文档齐全。到今天为止,大量论文里的心律失常分类、QRS波检测、心率变异性分析,仍然是在MIT-BIH上完成的。哪怕你做的是自己的临床数据,也常需要先用MIT-BIH验证一下方法的可靠性,再迁移到自己的场景里。

1.2 一句话看懂数据存储逻辑:记录、导联、采样率

从存储逻辑上看,MIT-BIH非常好理解。每一条记录,比如“100号记录”,对应一位受试者半小时的动态心电图。记录里有两个导联的信号,绝大多数记录是MLII和V1,少数是MLII和V5等其他导联组合。两个导联是同时采集的,采样率固定为360Hz,每个导联采样30分钟,算下来大约65万个采样点。

打开下载好的文件夹,你会看到每个记录名都对应好几个文件,核心就是三个:.hea头文件、.dat数据文件、.atr注释文件。我习惯用一个类比来说明它们的关系:.hea是档案袋上的标签,写清楚这份档案的基本信息;.dat是里面那张原始记录纸,波形全在上面;.atr是红笔写的批注,标记了每个心拍的位置和类型。三者配合,才是一份完整可用的心电图数据。

2. 各文件作用全拆解:.hea/.dat/.atr一个都不能少

2.1 .hea头文件:全记录的“说明书”与“地图”

.hea文件是纯文本格式,用记事本就能打开。以100号记录为例,开头大概是这样的:

100 2 360 650000 100.dat 212 200 11 1024 995 0 MLII 100.dat 212 200 11 1024 1004 0 V5 # 69 M 1085 1629

第一行的三个数字分别是记录名、导联数、采样率、采样点数。100 2 360 650000的意思就是:这条记录叫100,有两个导联,采样率360Hz,总共65万个采样点。采样点数不需要死记,.hea里写多少就是多少,读取数据时最好以它为准来截断文件,避免把多出来的字节也读进来。

第二行和第三行描述两个导联的细节,这行信息的价值经常被忽略。我们来逐段拆解:

  • 100.dat:信号存储在哪个数据文件里。
  • 212:数据编码格式,MIT-BIH用的是“212”格式,后面讲.dat时细说。
  • 200:ADC增益,单位是ADC单位/mV,意思是1mV的心电信号会被量化为200个ADC单位。
  • 11:ADC分辨率,11位,所以数字量范围大致在0到2047之间。
  • 1024:基线值,即信号为0mV时对应的数字量。
  • 995:第一个样本的值,可以用来校验读取是否正确。
  • 0:校验和,用于检查文件是否损坏。
  • MLII、V5:导联名称。

手动读取.dat的时候,最关键的参数就是基线和增益。因为文件里存的不是以毫伏为单位的物理值,而是一个整数。要把整数变成真正的电压值,公式是:

物理值(mV) = (数字量 - 基线) / 增益

比如数字量是1024,基线也是1024,那电压就是0mV;数字量是1224,电压就是(1224-1024)/200=1.0mV。这个公式看起来简单,但如果读数据时忘记减基线,波形整体就会“飘”得很高或很低,后面无论做滤波还是做心拍检测,结果都会受影响。

2.2 .dat数据文件:按3字节打包的二进制心电图

.dat是二进制文件,不能直接用文本编辑器打开。MIT-BIH采用了一种叫“format 212”的编码方式,名字里的212其实来自“12位采样、两个通道、3字节存储”的缩写。为什么要这么设计?早期存储和传输资源都很紧张,如果每个12位样本用一个16位整数存,会浪费4位空间;如果直接按每个样本3字节存,又会把两倍数据量写在盘上。212格式巧妙地用3个字节存下两个通道的各一个样本,正好每个通道占12位。

具体规则是:每3个字节为一组,存放两个样本。假设三个字节是b0、b1、b2,那么:

  • 通道1的样本值 = (b0 << 4) | (b1 >> 4)
  • 通道2的样本值 = ((b1 & 0x0F) << 8) | b2

用大白话说,第一个字节的8位加上第二个字节的高4位,拼出通道1的12位值;第二个字节的低4位加上第三个字节的8位,拼出通道2的12位值。位运算看起来略繁琐,但这是理解.dat文件的关键。如果你用现成的wfdb库,根本不需要自己写这些;但如果你想排查奇怪的问题,或者做自定义工具,就必须懂这个底层格式。

细心的朋友会发现,这样读出来的数值是“数字量”,范围大概在0到2047之间,距离真正的心电信号还有一步。所以读取.dat之后,一定要记得做基线校正和单位换算:

physical_signal = (digital_signal - baseline) / gain

做完这步,你看到的波形幅值就基本落在-5mV到+5mV之间,这才符合真实心电信号的物理范围。

2.3 .atr注释文件:逐心拍标注的二进制协议

如果说.dat是“波形原始记录”,那.atr就是“专家批注”。文件里记录了每一个被人工识别出的心拍位置,以及这个心拍的类型。MIT-BIH的标注非常细致,常用符号有N(正常心搏)、L(左束支传导阻滞)、R(右束支传导阻滞)、A(房性早搏)、V(室性早搏)、F(融合心搏)、J(交界性早搏)等。做心律失常分类任务时,人们通常会把它们归并为五大类:N、S、V、F、Q,分别对应正常、室上性、室性、融合和未知/不可分类。

.atr也是二进制格式,结构比.dat复杂一些。每个注释记录通常会包含一个标志字节、两个字节的时间增量、一个字节的类型码,可能还有一些辅助字节。时间增量的单位是“采样点”,不是“秒”。比如一个注释说某个心拍在“第1024个采样点”,那在360Hz的采样率下,对应的时刻就是1024/360≈2.84秒。

如果想手动解析.atr,工作量不大但很琐碎,要处理边缘情况,比如符号为0的伪注释、节奏类型注释等。我的建议是:日常使用直接用wfdb库的rdann函数,它会帮你把类型码映射成可读的符号,把采样点索引也解析好。但你仍然需要理解一个核心概念——annotation.sample给出的是第几个采样点,不是秒,也不是某个心拍在整个记录里的“第几拍”。所有后续对齐操作都要基于采样点索引来做。

3. 数据读取实操:从手动解析到一行代码

3.1 手动读取.dat,搞懂底层字节流

先别急着装库,我建议至少手动读一次.dat,这样以后遇到问题心里有底。下面是一段可以直接运行的Python代码,用numpy读一个记录的前N个样本:

import numpy as np def read_mit_bih_dat(record_name, num_samples=3600): """ 手动读取MIT-BIH的.dat文件(format 212) 返回两个导联的数字量,shape=(num_samples, 2) """ with open(record_name + '.dat', 'rb') as f: # 每3个字节存两个12位样本 raw = f.read((num_samples * 3) // 2) byte_arr = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 3) ch1 = (byte_arr[:, 0].astype(np.int16) << 4) | (byte_arr[:, 1].astype(np.int16) >> 4) ch2 = ((byte_arr[:, 1].astype(np.int16) & 0x0F) << 8) | byte_arr[:, 2].astype(np.int16) return np.column_stack([ch1, ch2])

注意我这里的读取长度先按num_samples * 3 // 2换算成字节数,因为3个字节对应2个样本。如果你想读整条记录,直接用.hea文件里的采样点数来计算字节数更稳妥。读完数字量后,再配合基线1024和增益200做物理单位换算,就能得到类似这样的输出:

第0个样本: 995 -> 物理值 -0.145 mV 第1个样本: 1005 -> 物理值 -0.095 mV 第2个样本: 1011 -> 物理值 -0.065 mV

看到这些数值在0附近缓慢波动,说明读取基本正确。如果出来几百几千的数值,或者直接乱码,大概率是字节拼接方向搞反了。

3.2 处理注释文件,理解时间增量

注释文件的解析虽然琐碎,但理解一下时间增量的原理很有必要。每个注释记录的大致结构是:先出现一个0x00标志字节,然后是两个字节的时间增量(16位整数,单位是采样点),再往后是一个类型字节,某些类型后面还会带辅助数据。

实际解析时,你会碰到几个麻烦点:

  • 注释流里有“伪注释”,它们的类型码是0,不表示真实心拍,主要用于帮助定位,处理时要跳过。
  • 节奏类型(比如心动过速开始、结束)也混在注释流里,后缀可能是“+”、“x”等,不是所有符号都对应一个心拍。
  • 文件末尾可能有填充字节,不能硬当成注释来解。

所以手动解析.atr可以当作一次练习,但生产环境里真的没必要。下面这段只展示核心逻辑,方便你理解采样点索引是怎么来的:

def read_ann_basic(atr_path): samples = [] symbols = [] with open(atr_path, 'rb') as f: data = f.read() i = 0 time_counter = 0 while i < len(data): if data[i] == 0x00: # 时间增量占2字节 delta = int.from_bytes(data[i+1:i+3], 'little') ann_type = data[i+3] i += 4 time_counter += delta # 这里省略辅助字节处理,完整实现需参考WFDB格式文档 if ann_type not in [0, 1]: samples.append(time_counter) symbols.append(chr(ann_type) if ann_type < 128 else '?') else: i += 1 return samples, symbols

这段代码只是为了让你感受注释文件的解析逻辑,不能直接用在实际项目中,因为辅助字节和特殊时间标志都被跳过了。遇到复杂的注释记录,老老实实用成熟的库解析,别自己造轮子。

3.3 用wfdb库一行读取,傻瓜式落地

手动解析可以让你明白原理,但日常开发我更推荐用wfdb这个Python库。它是PhysioNet官方维护的WFDB软件包的Python绑定,读取MIT-BIH就是几行代码的事:

pip install wfdb numpy matplotlib
import wfdb record = wfdb.rdrecord('100', sampto=3600, physical=True) annotation = wfdb.rdann('100', 'atr', sampto=3600) # record.p_signal 保存物理单位信号,shape=(3600, 2) # annotation.sample 保存心拍位置索引 # annotation.symbol 保存心拍类型符号 print(record.p_signal[:5]) print(annotation.sample[:10]) print(annotation.symbol[:10]) wfdb.plot_wfdb(record=record, annotation=annotation)

rdrecord负责读.hea和.dat,rdann负责读.atr。你不需要自己处理字节拼接、基线、增益,库内部全部搞定。渲染出来的图会同时显示两个导联的波形和每个心拍的标注符号,立刻能看出读取是否正确。实测下来,wfdb库对MIT-BIH的兼容性很好,几百行数据的读取也就是毫秒级。

如果你是从网上下载的.mat版本(即信号被保存为MATLAB格式),也不要慌。wfdb库同样支持读取,只是底层逻辑稍有不同,核心API差别不大。但如果你要做标准的MIT-BIH流程,建议还是用原生的.hea/.dat/.atr三件套。

4. 读取后的验证与预处理要点

4.1 怎么确认读取结果没出错

读取代码跑通了,不代表数据没问题。我见过太多人读出来的“心电信号”幅值稳定在500以上,波形直奔顶格,还拿着一堆错误数据去训练模型。所以验证这一步不能省。

最直接的验证方法有三个。第一,打印信号的统计信息,正常心电信号在0mV附近波动,标准差一般在0.1到0.5之间;第二,画出前几秒的波形,肉眼找一下QRS波群,正常波形里应该能明显看到P波、QRS波、T波的形态;第三,把.atr里标注的心拍位置叠到波形上,如果标注的点正好落在R波峰值附近,说明读取和标注都对齐了。

wfdb.plot_wfdb可以帮你一次性完成第二、第三步。如果你读出来的波形“糊成一片”或者标注和波峰对不上,先回去看基线、增益、字节拼接这几项,90%的问题都出在这几个地方。

4.2 标签对齐、重采样与心拍切分

很多任务需要把原始信号处理成固定长度的样本,比如“以R峰为中心,向前取0.2秒,向后取0.4秒”。MIT-BIH的采样率是360Hz,所以0.2秒就是72个采样点,0.4秒就是144个采样点。切分时要注意,注释里的采样点索引是原始的整数索引,如果你做了重采样或滤波,原始索引可能不再适用于新信号。

如果必须重采样,比如要统一到250Hz,我建议先用插值把信号变到新采样率,同时把标注的采样点也按比例缩放,并四舍五入取整。这里的“按比例缩放”指的是乘以新采样率与旧采样率的比值,不是简单加减。标签一偏移,后面训练出来的模型再准也是错的,这个问题很容易被忽略。

预处理环节还有一个常见操作是滤波。MIT-BIH原始信号里有基线漂移和工频干扰,做心拍检测前建议先用带通滤波器过滤掉0.5Hz以下和50Hz以上的成分。滤波时要注意,滤波器是有群延迟的,如果做的是零相位滤波(比如scipy.signal.filtfilt),信号延迟会被补偿,标签位置基本不需要调整;如果用了普通IIR滤波,相位延迟会导致R峰位置偏移,标签也要相应微调。

4.3 训练集/测试集划分:患者级分割的必要性

MIT-BIH的48条记录来自47位受试者,其中有两份记录(201和202)来自同一位受试者。做心律失常分类时,如果把同一位受试者的记录同时放进训练集和测试集,模型等于提前见过了这个人的心电形态,测试指标会虚高。这种“信息泄露”在跨患者评估中特别致命。

合理的做法是按患者划分数据,训练集和测试集不要包含同一位受试者的任何记录。很多论文采用的做法是取一部分记录做训练,另一部分记录做测试,比如用前40条记录训练,后8条记录测试,但前提仍然是要确认这些记录确实来自不同受试者。所以在实验设计阶段,建议先查一下官方受试者列表,把同一受试者的记录标记出来,再决定怎么划分。这个细节直接决定你论文结果的可信度。

5. 常见问题与排查技巧实录

5.1 信号变成乱码或幅值异常

这是读取MIT-BIH时遇上最多的问题,症状是画出来的波形完全不像是心电图,或者数值非常大。核心原因有三类:

  • 字节拼接方向错误。format 212的位运算规则如果写反,读出来的数字会完全错位,波形像打乱的马赛克。
  • 没有做基线校正。数字量直接当物理值用,波形整体上移几百个单位,看起来像一条粗大的直线在顶部。
  • 增益参数用错。MIT-BIH的增益通常是200,如果你用了别的数值,幅值会被等比放大或缩小。

排查方法也很简单:打印前10个数字量,先看是不是在0到2047之间;再看计算后的物理值是不是在正负5mV量级。两步能排除掉绝大多数问题。

5.2 注释解析失败或标签漂移

.atr解析最容易踩的坑是符号处理。注释里那些“+”和“x”符号其实不表示普通心拍,前者通常代表起搏心拍,后者代表伪迹或质量差的片段。如果做分类任务时把它们也当成普通标签,数据集就会被污染。

标签漂移则多见于重采样之后。原始标注索引是360Hz下的整数值,重采样后如果你只是粗暴地round(downsample_ratio * sample_index),某些边缘位置的索引会偏移一两个点,导致切出来的样本窗口错位。我的习惯是重采样后再验证一次R峰对齐效果,抽查十几个心拍,看波峰是否仍在窗口中心附近。

5.3 高频踩坑清单速查

下面这张表是实际使用中最常见的几个问题和对应解法,建议收藏备用。

症状可能原因处理方式
文件找不到或读取失败路径大小写错误、目录不对使用绝对路径,核对记录名如“100”是否带前缀
读取出的信号长度不对没有按.hea文件中采样点数截断读取时限定sampto或按头部声明手动截断
波形幅值异常大未减去基线或未除以增益使用(数字量-基线)/增益换算物理值
波形有断崖式跳变字节拼接时位运算顺序错误核对format 212的拼接规则
注释看起来对不上波形重采样后标签索引未调整按采样率比例重新计算索引并四舍五入
分类时只用一个导联数据忽略了“.hea里其实有两个导联”根据任务需求选择channels=[0]或[1]

除了上面这些,还有一个容易被忽略的小问题:如果你把.dat整个文件读进内存,它会占掉不少空间,因为650000个样本乘以2个导联,转成float64后大约是10MB左右。听起来不大,但如果你同时读48条记录,内存也是几十MB起步。读取后及时转成float32,或者按需分段读取,实验会流畅很多。

6. 从读取到建模:MIT-BIH还能这么用

6.1 心拍级分类的标准流程

读通MIT-BIH之后,下一步通常就是做心拍级分类。最常见的流程是:先用wfdb.rdrecord读信号,用wfdb.rdann读标注;接着做带通滤波去噪;然后在每个标注位置附近切出固定长度的心拍窗口;最后把标注符号映射成类别标签,输入分类模型。

窗口长度需要根据任务调整,太短可能漏掉P波或T波,太长又会带入无关噪声。我的经验是取R峰前0.25秒到R峰后0.4秒,既能看到完整的QRS波群,又能包含一部分ST段和T波,对判别室性早搏和正常心拍很有帮助。样本切好后,要检查一下各类别的样本数量分布。MIT-BIH中正常心拍远多于室性早搏和房性早搏,不做类平衡处理的话,模型会严重偏向多数类,导致少数类几乎识别不出来。

6.2 RR间期、心率变异性等扩展特征

标注文件的价值不止用来生成分类标签,它本身就是宝贵的特征来源。通过annotation.sample可以轻松算出相邻心拍之间的间隔,也就是RR间期。RR间期序列反映的是心率变化趋势,对检测房颤、早搏等异常非常有用。

用简单的numpy代码就能计算:

import numpy as np rr_intervals = np.diff(annotation.sample) / record.fs # 单位:秒 mean_hr = 60.0 / np.mean(rr_intervals)

有了RR间期序列,还可以继续统计标准差、RMSSD等时域指标,或者做频域分析。很多心率变异性研究就是在这种基础上开展的。也就是说,一个.atr文件不光是“答案”,同时也是特征工程的一座富矿。

6.3 把自己的数据对齐成MIT-BIH格式

最后再分享一个很实用的技巧:WFDB工具链不仅支持读取,也支持写入。也就是说,你可以把自己采集的心电数据转换成MIT-BIH的.hea/.dat/.atr格式,这样就能直接复用整个WFDB生态里的工具和脚本。

在wfdb库里,写入一条记录大致是这样:

import wfdb import numpy as np # signal shape=(n_samples, n_channels),单位mV signal = np.random.randn(3600, 2).astype(np.float64) wfdb.wrsamp( record_name='my_record', fs=360, units=['mV', 'mV'], sig_name=['MLII', 'V5'], p_signal=signal, fmt=['212', '212'], adc_gain=[200, 200], baseline=[1024, 1024], )

写出来的文件会自动生成.hea和.dat,如果你还想写.atr,就需要构造标注数组再调用对应的写注释接口。这个过程稍微复杂一些,但能帮你建立起一套“私有数据也能用公开工具处理”的流程,对于团队协作和实验复现都很有价值。

我在实际使用中的最大感触是:MIT-BIH的读取难点从来不在代码本身,而在对文件格式底层逻辑的理解。先花半小时把.hea里的参数、.dat的位运算、.atr的含义搞明白,后面所有实验都会顺畅很多。就算你决定用wfdb库偷懒,也别完全放弃这些底层知识,否则遇到数据异常时你会连从哪里排查看起都不知道。另一个小技巧是,读好一份数据后立刻保存成.npy或.npz缓存,后续实验反复读取会快得多,尤其是做深度学习时,数据的I/O时间经常被严重低估。

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

【Java开发MCP】CSDN发帖MCP开发与使用:从Stdio到SpringAI的完整落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 7:12:49

Linux虚拟网络三剑客:Bridge、Tun/Tap与Veth Pair详解

做Linux网络方向久了&#xff0c;你会发现无论KVM虚拟机、Docker容器&#xff0c;还是各种自定义组网方案&#xff0c;底层都绕不开三个内核虚拟设备&#xff1a;Linux Bridge、Tun/Tap、Veth Pair。很多人分开理解时觉得每个都简单&#xff0c;可一旦要自己搭一套虚拟网络&…

作者头像 李华
网站建设 2026/10/1 7:12:42

opencode CLI:本地AI开发者的命令行协同工作台

1. OpenRig 并非一个真实存在的开源项目——从热词误传到技术认知纠偏最近在多个开发者社区、技术问答平台和 CLI 工具讨论区&#xff0c;频繁出现“openrig”这个关键词&#xff0c;常与 Node.js、tmux、codex、CLI 等术语并列出现在搜索建议、错误日志或安装失败提示中。但如…

作者头像 李华
网站建设 2026/10/1 7:12:18

Linux终端特殊符号详解:从通配符到管道重定向的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 7:12:12

/etc/passwd提权原理与四种实战路径详解

1. 为什么修改/etc/passwd能提权&#xff1f;这不是“改个密码”那么简单很多人第一次听说“通过/etc/passwd提权”&#xff0c;第一反应是&#xff1a;“这文件不就是存用户名和密码哈希的吗&#xff1f;改它有啥用&#xff1f;现在密码都存在/etc/shadow里&#xff0c;普通用…

作者头像 李华