做了这么多年心电信号处理和 MIT-BIH 数据集打交道的时间不算短。最近带几个新人入门几乎每个人都会卡在同一个环节数据下载下来打开文件夹一看.hea、.dat、.atr一堆不认识的文件网上代码跑不通也不知道每个文件到底干嘛用的。这篇文章就把我这些年读 MIT-BIH 数据的经验完整写一遍从文件作用讲到二进制解析再到实际项目里最容易踩的坑你照着做基本能一次跑通。1. 拿到 MIT-BIH 数据集先看清楚一个记录由哪几类文件组成MIT-BIH 心律失常数据库里的每条记录说白了一个病人 30 分钟左右的动态心电但为了把这份数据完整描述出来PhysioNet 把它拆成了好几个配套文件。你不搞清楚这些文件的分工后面写代码就是在黑箱里瞎试。1.1 .hea 头文件整条记录的说明书.hea文件是纯文本体积很小但它是读取整条记录的入口。里面写明了记录名、采样点数、采样率、信号通道数、每个信号对应的数据文件名、增益、基线、ADC 分辨率等等。以常见的 100 号记录为例.hea内容大概是这个样子100 650000 360 200 0 100.dat 200 1024 200 11 1024 861 0 0 x 0 100.dat 200 1024 200 11 1024 961 0 0 x 0第一行是记录总览关键信息是记录名100总共 650000 个采样点采样率 360 Hz后面两个数字属于历史遗留的附加字段普通使用场景下基本可以不用关心。后面两行分别描述两个导联。以第一个信号行举例字段含义大致如下100.dat该通道波形数据存放在这个文件里200ADC 增益200 counts/mV1024基线值200单位相关的校准信息11ADC 有效分辨率 11 位1024ADC 零点861校验和等辅助字段解析时可忽略。不同年化版本的 MIT-BIH 头文件书写格式会有一点点差异但关键信息都在这几个字段里。核心重点就两个采样率 360 Hz以及增益 200 counts/mV。这两个数值后面会频繁用到。1.2 .dat 数据文件真正的波形二进制.dat文件是真正的心电波形数据二进制格式。MIT-BIH 采用的是一种叫 format 212 的压缩格式每 3 个字节存放两个 12 位采样值。只从文件名完全看不出来这个压缩规则所以必须先读.hea再根据其中的格式信息去解析.dat。100 号记录两个通道共 130 万个采样值.dat文件大小大约为 650000 × 3 ≈ 1.95 MB。数据本身是整数不是直接以 mV 为单位的浮点数这一点初学的人特别容易搞混。读出原始整数之后还必须结合增益和基线做一次换算才能得到真实的电压值。1.3 .atr 注释文件标注心跳和节律事件.atr是注释文件保存的是由心内科医生手工标注的心拍位置、节律类型、信号质量等信息。所谓MIT-BIH 心律失常数据库最重要的价值恰恰就在这些注释里。注释常见符号包括符号含义N正常心拍V室性早搏A房性早搏F融合心拍L左束支传导阻滞R右束支传导阻滞·无法判断的干扰段.atr也是二进制格式正常使用中由函数库负责解析。你只要通过接口拿到第几个采样点是一个心拍、这个心拍属于什么类型就可以了。真实项目里训练心律失常分类模型就是拿.dat里的波形片段作为输入拿.atr里的心拍类型作为标签。读懂了.atr你才有资格做后续的分类、检测、分割。2. 文件格式的核心数值采样率、增益、ADC 零点到底做什么用很多人用 wfdb 库读数据读出来一个 numpy 数组就直接拿去训练了但中间有一层物理单位还原的逻辑被库封装掉了。如果哪天你必须脱离库自己解析这层逻辑就必须搞清楚。2.1 采样率 360 Hz 和记录时长怎么换算360 Hz 意味着每秒采样 360 个点。一条记录的650000是采样点数换算成时长就是650000 / 360 ≈ 1805.56 秒 ≈ 30.09 分钟所以每条 MIT-BIH 记录大约半小时。这个换算关系在做时间轴定位时很重要。比如注释文件告诉你第 100 号心拍在采样点12345处那么对应的时间是12345 / 360 ≈ 34.29 秒这也是为什么很多画图代码里 x 轴喜欢用sample index / 360来标注秒数。看起来很简单但如果你用的是别的数据集采样率是 250、512 甚至 1000这个换算就必须跟着变硬编码 360 是新手最容易犯的错误。2.2 200 counts/mV 增益与 1024 零点怎么还原真实电压ADC 采样得到的原始整数不是毫伏数。MIT-BIH 的常见配置是设备每 1 mV 的电压变化量化成 200 个最小单位也就是增益 200 counts/mV。而 ADC 零点一般在 1024 附近对应 0 mV 时的原始读数。真实电压的换算公式是voltage_mV (raw_value - baseline) / gain基线通常取零点的近似值。比如原始读数 1224基线 1024增益 200那么(1224 - 1024) / 200 1.0 mV说明这个采样点对应的电压是 1 mV。这里的基线在wfdb库里的字段名是base或baseline在.hea里对应信号行的第三个字段。不同记录的这个值可能有细微差别所以不要全局硬编码为一个固定值最好每次从头文件里读。2.3 为什么即使只有三个文件也不建议完全从零解析理论上掌握了采样率、增益、格式 212 的打包规则你完全可以自己写代码把.dat读出来把.atr也读出来。但实际做一遍你会发现格式 212 的位操作很容易出错.atr的注释结构还有变长字段解析到一半输出全乱的情况非常常见。我的建议是自己在小规模数据上写一遍解析逻辑加深理解真正做项目时直接使用库。后面第四节我会给出一个不依赖库的最小解析代码但要记住那只是学习用的简化版生产环境请无条件相信wfdb。3. 用 Python 读 MIT-BIH 的推荐路线wfdb 库如何安排目前 Python 生态里读 MIT-BIH 最省事的库就是wfdb。它不是 PhysioNet 官方维护的唯一工具但文档齐全函数接口简单社区使用者最多。3.1 环境安装与数据获取安装只需要一条命令pip install wfdb我建议同时装好numpy、matplotlib一个负责计算一个负责画波形。数据有两种获取方式。第一种是到 PhysioNet 官网下载mit-bih-arrhythmia-database-1.0.0.zip解压后你会看到100.dat、100.hea、100.atr这样的文件。第二种是用wfdb库自带的数据库下载工具import wfdb wfdb.dl_database(mitdb, dl_dir./mitdb)这种方式会自动下载整个数据库包括所有记录。如果你只要 100 号记录测试用官网下载单个记录的文件就够了。注意一个细节wgdb的dl_database返回的下载目录结构里文件名是100不带扩展名而.hea、.dat、.atr会以同名前缀存在于同一目录。wfdb库读取时传入记录名100就行不需要手动补扩展名。3.2 读取头文件、波形、注释的三条核心命令读取波形的标准写法import wfdb record wfdb.rdsamp(100, pb_dirmitdb) signals record[0] # 二维数组形状为 (650000, 2) fields record[1] # 头文件信息字典record[1]里常用的字段包括fs采样率360sig_name通道名列表units单位列表baseline基线值列表adc_gain增益列表。读取注释的标准写法ann wfdb.rdann(100, atr, pb_dirmitdb) sample_indices ann.sample # 心拍所在的采样点索引 symbols ann.symbol # 对应心拍类型符号ann.sample、ann.symbol、ann.subtype、ann.chan覆盖了绝大多数论文里你需要的信息。拿到sample和symbol你就可以在心拍位置打标签做分类了。3.3 一口气完成从记录到可分析的数组下面这段代码是我给新人的标准模板读100号记录把两个导联的波形和注释一起整理出来import wfdb import numpy as np signals, fields wfdb.rdsamp(100, pb_dirmitdb) ann wfdb.rdann(100, atr, pb_dirmitdb) fs fields[fs] signal_0 signals[:, 0] signal_1 signals[:, 1] beat_positions ann.sample beat_types ann.symbol print(f采样率: {fs} Hz) print(f信号形状: {signals.shape}) print(f心拍数量: {len(beat_positions)})输出大致是这样采样率: 360 Hz 信号形状: (650000, 2) 心拍数量: 2273100 号记录有 2273 个心拍这个数字可以作为验证读取是否正常的参考值。如果读出来的心拍数量差很多十有八九是数据文件下载不完整或者路径传错了。3.4 可视化确认波形是否正常读数据成功的第一个直观标志就是波形图。画前 5 秒的波形import matplotlib.pyplot as plt duration_sec 5 n_show int(duration_sec * fs) t np.arange(n_show) / fs plt.figure(figsize(12, 4)) plt.plot(t, signal_0[:n_show], labelMLII) plt.plot(t, signal_1[:n_show], labelV5) plt.xlabel(时间 (秒)) plt.ylabel(电压 (mV)) plt.legend() plt.grid(True) plt.show()正常波形应该是清晰的心电图形态QRS 波群肉眼可见基线基本平稳。如果画出来是一片锯齿或者数值范围巨大检查一下是不是把原始 ADC 整数当成了 mV 直接用。rdsamp默认已经做了增益换算返回的数据单位就是 mV如果你自己解析就要手动补上上一步换算。4. 如果不想依赖 wfdb二进制层怎么读wfdb确实方便但理解底层对排查问题极有帮助。尤其是你把代码部署到无网络环境或者换用其他语言时必须知道 212 格式的位规则。4.1 逐字节解析 .dat 文件格式 212 的打包规则格式 212 的核心是每 3 个字节存放两个 12 位采样值。第一个采样值取第 1 字节的全部 8 位加第 2 字节的低 4 位第二个采样值取第 2 字节的高 4 位加第 3 字节的全部 8 位。用位运算表示import numpy as np def read_mit_bih_212(file_path, n_samples): raw np.fromfile(file_path, dtypenp.uint8) samples [] for i in range(0, n_samples * 3, 3): b0 int(raw[i]) b1 int(raw[i 1]) b2 int(raw[i 2]) val1 b0 | ((b1 0x0F) 8) val2 ((b1 0xF0) 4) | (b2 4) samples.append(val1) samples.append(val2) return np.array(samples[:n_samples])注意val2的高位来自b2的全部 8 位所以左移 4 位后得到的是 12 位值。上面代码是简化版没有处理符号位因为 MIT-BIH 的基线通常在 1024原始值以无符号形式存储减基线后就得到实际信号偏移。如果换到别的数据库比如欧标数据签名格式可能会不同需要先看.hea里的格式描述。你的数据文件如果读出来乱码最常见就是字节顺序搞反了。212 格式是小端风格的低位在前逐字节解析时必须按照上面规则取位不能直接把 3 字节解释成一个大整数。4.2 头文件增益、基线和单位换算的代码手动读.hea时只要取信号行的前几个关键字段。解析代码参考def parse_hea(file_path): with open(file_path) as f: lines f.readlines() record_name, n_samples, fs, *_ lines[0].split() signal_info [] for line in lines[1:]: if not line.strip(): continue parts line.split() signal_info.append({ file: parts[0], adc_gain: float(parts[1]), baseline: float(parts[2]), resolution: int(parts[4]), adc_zero: float(parts[5]), }) return record_name, int(n_samples), float(fs), signal_info拿到这些字段后把原始整数转换为毫伏voltage (raw_value - info[baseline]) / info[adc_gain]如果.hea里的增益是 200基线 1024那么raw_value1224时电压恰好是 1 mV。我在实际项目中碰到过一个很隐蔽的问题有些记录的baseline字段是 1024但adc_zero也是 1024二者含义不同。ADC 零点是硬件零输入时的读数基线的概念更偏信号处理中的参考水平。日常换算减法时用哪个都行因为两者数值几乎一样但如果你做精确算法对比实验务必统一使用wfdb返回的baseline。4.3 手动解析的限制与坑自己解析.atr是非常不推荐的。.atr有 16 字节的固定头、可变长度的注释块还涉及时间戳和类型映射。网上有少量解析代码但几乎都是针对某一版本的记录写的换个记录可能就挂。我做过一个实测手写 212 格式解析 100 号记录波形与wfdb读出来的一致但一旦换到某些含格式 16 的记录就出错。格式 16 是另一个存储方案每 2 字节一个采样值属于带符号的原始格式解析方式完全不一样。这也是为什么.hea里必须写明格式号写代码时不能假设全库都是 212。5. 实际项目里我反复踩过的 7 个坑这一节全部来自真实经历。下面这些问题每个都让团队里至少一位同学卡过一整天。5.1 路径与文件名问题wfdb.rdsamp传入的记录名是100而不是100.hea。如果你写wfdb.rdsamp(100.hea)大概率报错File not found。另外pb_dir参数指向数据库根目录如果你的文件直接放在当前目录就省略pb_dir。Windows 环境下文件路径里的反斜杠容易在字符串里被转义推荐统一用正斜杠或pathlib.Path。以前我不注意写./mitdb\100导致\1被转成不可见字符排查了很久。5.2 读取注释时的索引和时间戳问题ann.sample返回的是采样点索引而不是时间。后续画图时一定记得除以采样率fs。另一个容易犯的错是用ann.sample直接做切片时把数组边界超了因为最后几个心拍可能落在信号末尾附近切片时最好对n_show做一次min保护。wfdb.rdann还有一个return_label_elements参数返回的symbol数组标签类型更完整但符号数量可能比sample短。排序对齐时提前做好长度检查不然会得到奇怪的维度不匹配错误。5.3 Python 包版本差异wfdb这个包在早期版本和 4.x 系列之间接口有变化。有些老代码用wfdb.io.rdsamp新版本里wfdb.io依然存在但更推荐直接wfdb.rdsamp。如果你在网上找到的教程同时出现rdann和rdrecord注意它们返回类型不同rdsamp返回(信号数组, 字段字典)信号数组是 numpy 二维数组rdrecord返回一个Record对象调用.p_signal或.d_signal获取数据。新手最懵的就是这两种接口混用。我的建议是统一用rdsamp打印输出最直观调试成本低。5.4 内存和批量读取整个 MIT-BIH 数据库 48 条记录全部加载到内存大概需要 90 MB 左右看起来不大但如果你做滑动窗口采样一次性把 130 万采样点切成几十万个片段内存就爆了。正确做法是先读取记录只保留需要的通道生成心拍位置列表根据心拍位置在线切窗不保留全量片段。每批只保留几百条片段训练更稳定内存占用控制在 2 GB 以内。5.5 注释质量问题的干扰.atr里有大量噪声段和不完全定义的注释。做评估指标时如果用N、V两种类型做二分类训练集和测试集的类别分布还好。但如果你把?、R、L之类全当真标签模型会学得很痛苦。我自己常用的一组筛选规则是只保留N、V、A、F、L、R六类其他注释全部丢弃。这样得到的样本更干净论文结果也更可复现。5.6 手动解析格式 212 时的符号问题有些记录不是纯正向偏移原始值减去基线后可能出现负数。你如果直接按无符号路径解析就会出错。如果你不得不用手动解析建议以.hea里记录的baseline为中点raw_value - baseline如果大于 2047再手动减 4096。这种偏移处理能从 100 号记录验证但不保证所有记录都适用。再次强调能用库就别自己解析。5.7 从 0 开始索引 vs 从 1 开始索引wfdb的ann.sample使用的是 0 起始索引和 numpy 一致直接用没问题。但 PhysioNet 官方文档的某些描述用的是 1 起始索引网上零星代码里也可能混用。之前我在对比算法时因为没注意到这一点结果整体偏移了一个点导致心拍检测精度差了一个采样点在 360 Hz 下相当于 2.8 ms 的偏差对某些评测指标影响不小。调试时可以打印ann.sample[:10]心拍位置应该在几十到几百的范围内如果整体都是 1 偏大就要留意索引体系是否统一。6. 把读取做成一类可复用的小工具项目只要碰过 MIT-BIH 一次后面大概率还会再碰第二次、第三次。与其每次重写读取代码不如第一次就把读取和预处理封装好。6.1 一个覆盖主要需求的工具函数下面这个函数我用了挺久核心功能是按记录名读取波形和注释并返回筛选后的心拍片段import wfdb import numpy as np def load_mit_record(record_name, channelsNone, keep_types(N, V, A, F, L, R)): signals, fields wfdb.rdsamp(record_name) ann wfdb.rdann(record_name, atr) fs fields[fs] if channels is not None: ch_indices [fields[sig_name].index(c) for c in channels] signals signals[:, ch_indices] valid_flag np.isin(ann.symbol, list(keep_types)) return signals, ann.sample[valid_flag], np.array(ann.symbol)[valid_flag], fs使用示例signals, beats, symbols, fs load_mit_record(100, channels[MLII])这样你就拿到了一个单导联的信号数组、心拍位置列表、心拍类型列表和采样率。绝大多数深度学习模型训练都只需要这几个量。6.2 配合 NumPy 做基本心电预处理的流程读取只是第一步。真实业务里你还需要去基线漂移、去工频干扰。这里我一般分三步第一步整段信号做高通滤波截止频率 0.5 Hz 左右去掉基线漂移 第二步用陷波器或小波变换处理 50 Hz 工频干扰 第三步以每个心拍位置为中心截取前后 0.6 秒的片段作为模型输入。截取片段时注意索引边界def extract_beats(signal, beat_positions, fs, before0.6, after0.6): n_before int(before * fs) n_after int(after * fs) segments [] for pos in beat_positions: start pos - n_before end pos n_after if start 0 and end len(signal): segments.append(signal[start:end]) return np.array(segments)这个流程对大部分对比实验够用也不复杂可以快速验证你从.dat里读出的波形质量。6.3 面向后续扩展的方向一旦把读取和预处理做成工具函数后面就可以很自然往几个方向延伸心拍分类提取每个心拍片段的时域、频域特征或者直接给卷积网络输入原始波形节律分析结合.atr里更粗粒度的节律注释做房颤、心室颤动等长程事件检测多记录交叉验证用 47 条记录做训练留 1 条做验证保证训练集和测试集样本不互相污染。我在实际项目里踩过最重的一次坑就是忘了做按记录划分数据集结果同一条记录的心拍碎片同时出现在训练集和测试集模型指标漂亮得离谱部署后彻底崩盘。MIT-BIH 虽然只有 48 条记录但正确划分后依然能做出稳定可靠的心律失常分类模型。读取数据本身不是终点把数据理解透、文件格式弄明白后面的每个实验才能稳稳落地。
阅读完成 · 觉得有帮助?