
心电信号处理这个系列写到第四篇前几篇聊了不少滤波、基线漂移校正、R峰检测这些算法层面的东西。今天这篇想换个视角聊一个看起来最不起眼、实则最容易卡住新手的环节——心电数据格式。如果你从公开数据集下载过MIT-BIH对着.hea、.dat、.atr这些后缀一脸懵如果你在自己设备上采了一堆数据却不知道怎么转成能跑算法的格式如果你解析波形时发现幅值动辄几千、时间轴完全对不上那这篇就是写给你的。我会从数字化原理讲清楚格式为什么长这样再把WFDB、SCP-ECG、HL7 aECG这些主流格式挨个拆开最后用Python手把手把MIT-BIH的212格式从裸字节解析成心电波形。不管你是做科研、搞医疗器械还是自己在捣鼓可穿戴设备这份笔记都能帮你省下不少弯路。1. 格式问题为什么值得花一整篇讲先理解数字化那一刻发生了什么很多初学者拿到心电数据的第一反应是打开文件直接画图结果画出来一团乱麻。这不奇怪——你看到的文本、二进制、XML其实都是模拟心电信号在数字化那一刻被“硬编码”下来的结果。想读懂各种格式得先回到源头。1.1 模拟信号变成数字数据到底变了什么心电图机或采集设备的电极贴在皮肤上测量到的是毫伏级别的电位差这是一个随时间连续变化的模拟信号。要让计算机处理它必须先做两件事采样和量化。采样决定了时间分辨率常见心电采样率有250Hz、360Hz、500Hz甚至1000Hz。360Hz这个数字有点特殊它是MIT-BIH数据集的采样率因为当年磁带记录和数字化设备的限制后来成了科研界的“默认值”。理论上根据奈奎斯特定理要保留心电信号里常见的100Hz以内的频率成分采样率至少得200Hz360Hz留了足够裕量还能照顾到QRS波群里的高频细节。量化决定了幅度分辨率。模拟信号是连续的数字化后只能用有限个离散等级来表示。心电采集通常用12位、16位或24位的ADC模数转换器。12位意味着整个量程被切成了4096个等级16位则是65536个等级。举例来说假如ADC的量程是±5mV12位精度下每个最小刻度代表约2.44μV而16位精度下约0.15μV。这就是为什么有些高精度心电设备能用16位甚至24位ADC——为了捕捉微弱的P波细节和ST段变化。除了采样率和位深还有两个概念必须搞清楚增益gain和基线baseline。心电信号本身只有毫伏级ADC的输入量程通常是伏特级或几百毫伏级所以模拟前端必须先放大信号。这个放大倍数就是增益单位通常是“ADC码值/mV”比如200、250、1000。基线则代表“零电位”在ADC输出中对应的码值常见的是1024或2048也就是ADC量程的中点。这里有个很经典的坑文件里存的是ADC码值不是毫伏。如果你不做(raw - baseline) / gain这一步换算画出来的波形数值就会是几千甚至上万看起来像噪音而不是心电。后面我会专门演示换算过程。1.2 设计一种数据格式本质上是在平衡哪几件事理解了数字化参数之后再看各种心电数据格式就容易多了。你会发现不管格式怎么变它都要解决同样几个核心问题第一可恢复性。格式里必须保留采样率、位深、增益、基线这些元数据否则拿到波形数据的人无法还原真实的物理信号。MIT-BIH的.hea头文件专门干这个SCP-ECG里有专门的“ECG Acquisition”章节HL7 aECG则用字段标签来记录。第二压缩与存储效率。心电数据动辄几小时甚至24小时连续记录12位精度、360Hz采样率下一小时单通道就是约1.27MB多通道加上注释会更庞大。SCP-ECG用了差分编码和专用压缩算法体积能做到原始数据的几分之一。而MIT-BIH的212格式本质上也是一种压缩——用12位存储却能把两个通道塞进3个字节省掉了25%的空间。第三注释与事件信息。一份可用的心电记录不能只有波形还得有R峰位置、心拍类型正常、室早、房早等、起搏器事件这些标注。MIT-BIH的.atr注释文件、SCP-ECG的Reference Beat和Beat Rhythm章节都是为了解决这个问题。这也是心电格式区别于普通波形文件的关键。第四可互操作性。医院里的心电数据可能来自不同厂商的设备要做电子病历共享、远程会诊格式必须能跨系统流通。DICOM和HL7 aECG就是为了这个目的出现的标准。明白了这四个维度再去看各种格式的文档你会发现自己能快速抓住它设计的核心意图。2. 主流心电数据格式全景图从科研机房到临床设备再到可穿戴心电数据格式不是一个“标准答案”而是按场景分层的。科研界、医疗设备厂商、可穿戴生态各有各的惯例。2.1 WFDB/MIT-BIH科研界默认的“硬通货”如果你在心电信号处理领域待过一定绕不开MIT-BIH心律失常数据库。它由MIT和Beth Israel Hospital在1980年代发布包含48条半小时双通道心电记录每条记录都带有专业医生标注的心拍类型。几十年来它成了算法评测的“考卷”几乎所有论文都会在MIT-BIH上报告准确率。MIT-BIH采用的格式统称WFDBWaveForm DataBase格式由三部分组成.hea头文件文本格式记录采样率、通道数、位深、增益、基线、样本数等信息.dat数据文件二进制存储原始波形数据常见212格式、16位格式、8位格式.atr注释文件二进制存储R峰位置和心拍类型标签WFDB是一套完整工具链的名字它不仅有数据格式还包括rdsamp、rdann、wrann等命令行工具和C语言库后来社区又封装了Python的wfdb库。说它是科研界的事实标准一点也不夸张。除了MIT-BIH很多其他公开数据集也采用或兼容WFDB格式比如欧洲ST-T数据库、QT数据库等。初学者从WFDB入手等于同时打开了整个心电科研资源库。2.2 SCP-ECG与HL7 aECG医疗互操作的两条路线如果说WFDB是科学家们“够用就好”的产物那SCP-ECG和HL7 aECG就是医疗标准化组织的正经作品。SCP-ECGStandard Communication Protocol for Computer-Assisted Electrocardiography是欧洲标准同时也是ISO 11073-91064。它的设计目标很明确在医疗系统之间传输心电记录并且要完整保留诊断信息。SCP-ECG的存储方式是“分章节”的患者信息、采集参数、参考心拍、节律数据、测量结果各自放在独立章节里还可以用专用算法压缩波形数据。这种结构在传输效率和信息完整性上做了非常好的平衡。HL7 aECG则是基于XML的心电标注格式把波形数据以十六进制字符串嵌在XML里同时用大量标签描述采样率、导联配置、滤波状态等细节。它的最大优点是纯文本、可读性好、跨平台性强缺点是文件偏大适合数据量不算太大的应用场景。两者对比的话SCP-ECG更接近“文件存储格式”HL7 aECG更接近“消息交换格式”。临床心电网络建设里用SCP-ECG存档案、用HL7 aECG做接口传输是常见的组合。2.3 EDF、CSV和厂商私有格式工程落地里的“现实选择”EDFEuropean Data Format原本是为多导睡眠监测设计的通用生物信号格式好处是头文件统一、支持多采样率很多脑电、肌电、心电混合采集设备都支持它。如果你做的是多模态生理信号采集EDF经常是首选。CSV则是工程里最常见的“逃生通道”。自己拿MCU采集心电、或者读取某款手环导出的数据最通用、最省事的方式就是存成CSV每列一个导联、每行一个采样点再加一列时间戳。它的缺点是文件大、没有标准元数据规范、注释信息难附加但它够简单、够通用。很多算法原型开发阶段我都会先把数据转成CSV或NPY来调试跑通逻辑后再回到正式格式做工程化。厂商私有格式则是绕不开的现实。Philips、GE、Mortara这些心电图机厂商都有自家的存储格式用于保存原始波形和诊断结论。这类格式通常不开放完整文档需要通过厂商SDK或中间软件导出标准格式。这也是很多医院数据科研化利用的第一道门槛。做个小结几种格式的定位差异很大格式典型场景优点缺点WFDB/MIT-BIH科研算法研发、公开数据集生态成熟、配套工具齐全医疗设备原生支持较少SCP-ECG欧洲医疗心电网络存储标准化程度高、支持压缩和诊断信息解析复杂度较高HL7 aECG医疗系统间数据交换XML可读性好、跨平台体积大、不适合原始波形存档EDF多导生理信号采集通用性强、支持多路信号心电专用功能弱CSV/自定义嵌入式设备、算法原型简单直观、开发快缺乏标准、元数据易丢3. 手把手解析MIT-BIH数据从裸字节到心电波形了解了全景之后我们选一个最常见的场景做深度实操解析MIT-BIH数据库。我以经典记录100为例从.hea头文件一路解析到位把212格式的.dat数据变成能画图的心电波形。3.1 读头文件.hea里每一个字段都不是摆设打开100.hea内容长这样100.dat 2 212 360 650000 0 11 0 0 0 0 0 100.dat 200 11 1024 995 - 0 0 0 0 0第一行是全局信息逐字段含义如下100.dat数据文件名2通道数量212数据存储格式编号360采样率单位Hz650000每条通道的样本总数0信号计数基线该记录为011每个样本的位深实际使用12位中的11位后续的0和某个特殊值代表分组信息一般用默认值第二、三行是每个通道的详细信息100.dat该通道数据所在文件双通道共用同一个文件200增益单位是ADC码值/mV。这里表示每1mV心电信号对应200个ADC码值11该通道位深同样是11位1024基线值对应0mV电位995信号第一个采样点的ADC码值这个数值本身没多大含义只是记录初始状态-无符号修正标记若为表示需要对码值做额外的符号扩展后面的字段按需使用实际操作中直接用wfdb.rdsamp()读取最省事import wfdb signals, fields wfdb.rdsamp(100, sampto3600) print(fields[fs]) # 采样率360 print(fields[sig_name]) # [MLII, V5] print(fields[adcgain]) # [200., 200.] print(fields[baseline]) # [1024, 1024] print(fields[units]) # [mV, mV] print(signals[:5])signals已经是换算成mV的物理量fields把元数据都装好了。但如果你想理解底层原理、或者未来需要解析非标准记录我建议还是手动读一次头文件搞清楚信息流是怎么走的。3.2 解析212格式每3个字节装下2个样本212格式是MIT-BIH中最常用的格式也是初学者最容易读错的地方。它的核心特点是每个样本用12位bit存储但为了节省空间两个通道的12位数据被交叉打包在3个字节24位里。拆开看是这样的逻辑每3个字节组成一组这一组里承载两个采样点——第一个点使用第1个字节的全部8位加上第2个字节的低4位第二个点使用第3个字节的全部8位加上第2个字节的高4位。用Python手动解析的代码如下import numpy as np def read_212(dat_path, nsamp_per_channel): raw np.fromfile(dat_path, dtypenp.uint8) total_samples nsamp_per_channel * 2 n_bytes_needed (total_samples * 12) // 8 raw raw[:n_bytes_needed].reshape(-1, 3) # 第一个通道第0字节 第1字节的低4位作为高4位 ch1 raw[:, 0].astype(np.int16) | ((raw[:, 1] 0x0F).astype(np.int16) 8) # 第二个通道第2字节 第1字节的高4位作为高4位 ch2 raw[:, 2].astype(np.int16) | ((raw[:, 1] 4).astype(np.int16) 8) # 11位有符号数超出2048表示负数做符号扩展 ch1[ch1 2048] - 4096 ch2[ch2 2048] - 4096 return ch1, ch2 ch1_adc, ch2_adc read_212(100.dat, 650000) # 物理单位换算 ch1_mv (ch1_adc - 1024) / 200.0 ch2_mv (ch2_adc - 1024) / 200.0这段代码里有几个细节值得说明每个组里第1个字节和第3个字节存的都是12位值的低8位而中间那个字节被拆成了两半低4位给第一通道、高4位给第二通道。网上有些解析代码把中间字节高低4位的分配弄反了画出来的两个通道互相串扰QRS波会同时出现在两个导联上排查起来非常痛苦。符号扩展也是个关键点。212格式实际用的是11位有符号数范围是-1024到1023。如果不做 2048的判断负的采样值会被读成几千的正整数波形看起来就像满屏的尖峰。换算成物理单位后ch1_mv的范围应该在±2mV左右画出来能清晰看到P波、QRS波群和T波这才是正常的。3.3 注释文件.atr心拍位置与类型标签只有波形没有标注MIT-BIH的价值会大打折扣。.atr文件用紧凑的二进制格式存储注释每个注释对应一个采样点位置和一个类型码。用wfdb库读取注释非常简单ann wfdb.rdann(100, atr) ann_sample ann.sample[:10] # 前10个注释的采样点索引 ann_symbol ann.symbol[:10] # 对应的心拍类型 print(ann_sample) print(ann_symbol)常见的心拍类型符号包括N正常窦性心拍L左束支传导阻滞R右束支传导阻滞A房性早搏V室性早搏Q未分类心拍R峰标注位置的意义在于它可以作为算法评估的“金标准”。你训练QRS检测网络输出结果和ann_sample对比就能算出灵敏度、阳性预测率这些指标。做心拍分类时ann_symbol就是标签来源。如果想自己解析.atr底层逻辑是每两个字节为一组第一个字节的高4位表示注释类型标志低4位和第二个字节组合成时间增量。解释这个格式确实需要读MIT-BIH的注解文档我的建议是——除非你有特殊需求否则直接复用wfdb.rdann就好这部分的轮子已经足够成熟。3.4 实际跑一遍用Python画出带标注的心电波形把前面的内容串起来用wfdb一行指令加载数据和标注再用matplotlib画出第一个通道前5秒的波形和R峰位置import wfdb import matplotlib.pyplot as plt import numpy as np record wfdb.rdrecord(100, sampto5*360) ann wfdb.rdann(100, atr, sampto5*360) t np.arange(len(record.p_signal)) / record.fs sig record.p_signal[:, 0] plt.figure(figsize(12, 4)) plt.plot(t, sig, linewidth0.8) for s in ann.sample: if s len(record.p_signal): plt.axvline(xs/record.fs, colorred, linestyle--, linewidth0.6) plt.xlabel(Time (s)) plt.ylabel(ECG (mV)) plt.title(MIT-BIH Record 100, Channel 1 R peaks) plt.tight_layout() plt.show()实际运行这段代码你会看到清晰的、稳定的QRS波群R峰位置对齐在每个波峰上。如果你看到波形幅值在0附近波动、R峰标注整体偏移那就要往前排查——大概率是采样率或增益换算出了问题。4. 格式转换、工具链与真实项目中的选型取舍读数据和可视化只是第一步。真正做算法研发或产品落地时你会频繁遇到格式转换和工具选型的问题。4.1 值得记住的工具和库Python生态里最核心的是wfdb官方维护的WFDB Python接口。它能读几乎所有WFDB格式的记录和注释也能写数据生成自己的数据集。配合numpy和matplotlib常规科研流程完全够用。heartpy是一个更偏应用的心电分析库内置了滤波、心率计算、HRV心率变异性分析等功能适合快速出指标。但要注意它背后的核心其实是读入标准格式后的算法流程数据加载模块的灵活性不如wfdb。如果你是处理自己采集的数据pyEDFlib用来写EDF文件scipy.io或pandas用来处理CSV再加上wfdb.wrsamp把CSV转成WFDB格式一套流程就闭环了import pandas as pd import wfdb df pd.read_csv(my_ecg.csv, header0) samples df[[ch1_mv, ch2_mv]].values n samples.shape[0] wfdb.wrsamp( record_namemy_record, fs500, units[mV, mV], sig_name[I, II], p_signalsamples, base_time12:00:00, base_date2023-01-01 )这样一来你自己采集的数据就能用wfdb库、甚至用各种临床算法直接处理了。4.2 三种典型场景的格式选型建议做算法研究直接用WFDB/MIT-BIH格式保持和公开数据集一致。减少格式转换的坑评测指标才可信。做医疗产品对接优先支持SCP-ECG和HL7 aECG。虽然解析麻烦但医院系统的数据交换就吃这套标准。建议把解析层封装成独立服务用协议缓冲区或数据库表来中转避免核心业务被格式细节拖累。做可穿戴设备或IoT不建议在端侧用复杂格式。端侧记录用轻量二进制元数据头或者直接用CSV片段云端再统一转成标准格式做分析。压缩算法优先参考SCP-ECG的差分编码思路它把“相邻采样点差值往往很小”这个信号特性利用得非常充分。5. 常见问题与排查技巧实录最后分享几个我在实际项目中踩过的坑这些场景很多是文档里查不到的但几乎每隔几个月就会有人问一次。5.1 波形幅值几千甚至上万如果你画出来的波形数值在几千级别几乎可以断定没有做增益和基线换算。记住标准公式mv (adc - baseline) / gain。使用wfdb.rdsamp()时返回的已经是物理单位但如果你自己解析.dat这个换算必须自己做一遍。另外注意基线和增益是“每通道一个值”别只用一个通道的参数去换算所有通道。5.2 时间轴和采样率对不上心电记录每条通道的采样率可能不同读取时要用各自的fs。MIT-BIH这类标准数据通常两个通道采样率一致但EDF文件里多信号不同采样率很常见。统一处理时要么重采样到同一频率要么按时间戳对齐不能想当然认为同一行索引代表同一时间。5.3 两个通道波形完全一样或互相串扰这种情况九成是212格式字节拆包时高低位分配写反了。第1个通道用第2个字节的低4位第2个通道用第2个字节的高4位——反了就串道。排查方法很简单分别把两个通道画出来观察QRS主波的极性是否一致。如果两个通道几乎一模一样优先检查字节拆包逻辑。5.4 注释文件读取后R峰位置整体偏移R峰标注偏移通常是采样率传递错误导致的。比如解析器默认采样率是250Hz但实际是360Hz那么所有标注的时间索引都会系统性地偏移。用ann.sample与波形索引对比时一定要用同一个采样率口径。做一个速查表收着遇到问题查一查现象常见原因检查方向幅值为几千级别未减基线、未除增益检查baseline和adcgain时间轴错位采样率口径不一致确认文件标注的fs双通道波形相同212格式高低位拆包错误重查字节分配逻辑注释与波形对不上采样率或样本数不一致对齐fs和nsamp数据量大、读取慢格式未压缩考虑转SCP-ECG或差分压缩格式跨系统传输乱码格式不受支持转EDF或HL7 aECG5.5 转换过程丢信息wfdb2edf这类工具能完成格式转换但转换途中有些隐性损耗你要清楚SCP-ECG里的诊断结论、起搏器标记、某些厂商的专有测量参数在转换后可能丢失。转换前最好列一个字段清单确认哪些信息必须保留必要时自己写转换代码而不是依赖通用工具。根据我个人经验处理心电数据格式最重要的是养成“先读元数据再读波形”的习惯。拿到任何一份数据第一件事不是画图而是打开头文件或看fields字典确认采样率、通道数、位深、增益、基线五个核心参数。这五个性价比极高的字段能帮你规避掉绝大多数解析陷阱剩下的问题基本都能靠搜索和读官方文档解决。最后再分享一个实用技巧如果你经常跟MIT-BIH打交道建议把几个常用记录的头文件打印出来贴在工位边上。100、105、109、118这些记录它们的通道配置、增益值、注释分布其实各不相同。多熟悉几份典型的头文件你对“格式”这件事的理解会比看十篇文档都来得快。