3、数据格式与协议:MIT-BIH格式、WFDB工具包、自定义数据格式设计
聊到心电数据,绕不开一个经典——MIT-BIH格式。说实话,这玩意儿在圈子里就跟“普通话”一样普及。我最早接触它的时候,差点被它的文件结构搞晕。今天咱们就把它掰开揉碎,讲清楚。
3.1 MIT-BIH格式:心电界的“通用语言”
MIT-BIH格式诞生于上世纪80年代,由麻省理工和贝斯以色列医院联合推出。别看它老,至今仍是心电算法验证的黄金标准。为什么?因为它的标注数据太全了,每个心跳类型都标得清清楚楚。
一个完整的MIT-BIH记录包含三个文件:
| 文件后缀 | 文件名示例 | 存储内容 |
|---|---|---|
| .dat | 100.dat | 原始心电信号数据(二进制) |
| .hea | 100.hea | 头文件,描述信号参数 |
| .atr | 100.atr | 注释文件,记录心跳类型和位置 |
我刚开始做心电分析时,犯过一个低级错误——只拿了.dat文件就跑,结果解析出来的数据全是乱码。后来才明白,.hea文件才是“钥匙”。它里面记录了采样率、增益、ADC分辨率这些关键参数。
举个例子,一个典型的.hea文件长这样:
100 2 360 650000
100.dat 212 200 11 1024 995 -22131 0 MLII
100.dat 212 200 11 1024 995 22131 0 V5
# 69 M 1085 1629 x1
# 记录开始时间:1984-01-01 00:00:00
我来解释一下:第一行表示记录名、导联数、采样率(360Hz)、信号点数。第二行和第三行分别描述两个导联的信号格式。212代表一种特定的二进制编码方式,200是增益(单位是ADC单位/mV),11是ADC位数。
3.2 WFDB工具包:别自己造轮子
既然MIT-BIH格式这么复杂,难道每次都要手写解析器?当然不用。WFDB(WaveForm DataBase)工具包就是专门干这个的。它由MIT开发,支持C、Python、MATLAB等多种语言。
我个人习惯用Python版本的wfdb库,安装就一行命令:
pip install wfdb
读取MIT-BIH数据,核心代码其实就几行:
import wfdb
# 读取记录
record = wfdb.rdrecord('100', sampto=3600) # 只读前10秒
annotation = wfdb.rdann('100', 'atr', sampto=3600)
# 获取信号数据
signal = record.p_signal # 二维数组,每列是一个导联
fs = record.fs # 采样率
# 获取心跳标注
sample_indices = annotation.sample # 心跳位置
symbols = annotation.symbol # 心跳类型(N、V、A等)
你看,就这么简单。rdrecord读信号,rdann读标注。sampto参数可以控制读取长度,避免一次加载整个文件——这在处理长程记录时特别有用。
WFDB还提供了很多实用功能。比如绘制心电波形:
wfdb.plot_wfdb(record=record, annotation=annotation,
time_units='seconds', figsize=(12, 4))
嗯,这里要注意,plot_wfdb默认会显示所有导联。如果你只想看某个导联,可以手动提取信号后用matplotlib画。
3.3 自定义数据格式:什么时候需要自己设计?
MIT-BIH格式虽好,但有些场景下它并不适用。我遇到过几个典型情况:
- 存储空间受限: MIT-BIH用16位整数存储,对于嵌入式设备来说,12位甚至8位就够了
- 需要附加信息: 比如患者ID、采集时间、设备状态等,MIT-BIH的头文件扩展性有限
- 实时流式传输: MIT-BIH是文件级别的格式,不适合逐包传输
我自己设计过一套轻量级格式,结构很简单:
# 自定义心电数据格式(二进制)
# 文件结构:
# [文件头] [数据块1] [数据块2] ...
# 文件头(固定64字节):
# 4字节:魔数(0xECG1)
# 4字节:版本号
# 2字节:采样率(Hz)
# 1字节:导联数
# 1字节:ADC位数
# 2字节:增益(ADC单位/mV)
# 8字节:记录开始时间(Unix时间戳)
# 42字节:保留字段
# 数据块(可变长度):
# 4字节:数据块长度(字节数)
# 2字节:数据块类型(0=原始信号,1=标注,2=事件)
# N字节:数据内容
为什么这么设计?说白了,就是为了解析效率。固定长度的文件头可以一次读取,数据块的分块设计支持随机访问——你想跳转到第10分钟的数据,直接计算偏移量就行,不用从头读到尾。
3.4 格式转换:打通数据孤岛
实际项目中,你往往需要在不同格式之间来回切换。比如从MIT-BIH转成自定义格式,或者从自定义格式转成WFDB可读的格式。
我写过一个转换工具,核心逻辑是这样的:
def mitbih_to_custom(input_path, output_path, sampto=None):
# 1. 读取MIT-BIH数据
record = wfdb.rdrecord(input_path, sampto=sampto)
ann = wfdb.rdann(input_path, 'atr', sampto=sampto)
# 2. 提取关键信息
signal = record.p_signal
fs = record.fs
gain = record.adc_gain[0]
bits = record.adc_res[0]
# 3. 写入自定义格式
with open(output_path, 'wb') as f:
# 写文件头
header = struct.pack('<4sIHBBHq42s',
b'ECG1', 1, fs,
record.n_sig, bits,
int(gain), 0, b'')
f.write(header)
# 写数据块
for i in range(0, len(signal), 360): # 每秒一个块
chunk = signal[i:i+360].tobytes()
block_header = struct.pack('<IH', len(chunk), 0)
f.write(block_header)
f.write(chunk)
这段代码里有个细节:我按每秒一个数据块来组织数据。为什么是每秒?因为360Hz采样率下,每秒正好360个点,方便后续按秒检索。你想想看,如果按任意长度分块,查找特定时间点的数据就得遍历所有块,效率就低了。
3.5 实战建议:选型决策
讲了这么多,到底该用哪种格式?我根据经验总结了一个决策表:
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| 算法验证、学术研究 | MIT-BIH + WFDB | 生态完善,有大量公开数据集 |
| 嵌入式设备采集 | 自定义轻量格式 | 存储小、解析快、可扩展 |
| 云端存储与分析 | WFDB或Parquet | WFDB兼容性好,Parquet适合大数据分析 |
| 实时流式传输 | 自定义协议(如基于Protobuf) | 支持增量传输,带宽利用率高 |
最后说一句:不管你选哪种格式,一定要写清楚文档。我见过太多项目,格式设计得挺漂亮,但半年后连设计者自己都忘了字段含义。嗯,这可不是段子,是真事。