ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EDF/EDF+ 医学信号格式解析与读取实践指南

EDF/EDF+ 医学信号格式解析与读取实践指南 简介这是一份面向MATLAB用户的EDF欧洲数据格式文件读取工具包适用于生物医学信号分析、睡眠研究及临床神经生理学等场景。压缩包内共3个m文件整体大小仅6KB完整封装了打开、读取与关闭EDF文件的函数流程。edfopen.m负责检查文件是否存在、解析头部信息并初始化结构体记录采样率、信号数量、物理单位转换参数等关键元数据edfread.m根据这些元数据定位数据范围处理二进制字节对齐、整数到浮点的转换及采样率调整最终返回各通道的时间序列edfclose.m实现文件句柄释放防止资源泄漏。三个函数配合即可快速提取脑电、心电、肌电等多类生理信号为后续频谱分析、事件检测或信号质量评估提供可靠数据基础。该工具包已有554人学习使用尤其适合想理解EDF底层结构的MATLAB初学者也可作为科研人员二次开发的轻量起点。通过直接调用上述接口使用者可跳过繁琐的底层解析步骤将精力集中于信号分析研究。1. 从一段没法直接看的生理信号说起做多导睡眠监测或心电分析的人大概率遇到过这种文件后缀是 .edf双击用记事本打开是满屏乱码用 Excel 读是行列错位的数字到 GitHub 搜“EDF 读取”能翻出几十个 repo但真正能一次把 edf format 的事件注释一并读出来的不多。EDFEuropean Data Format是三十多年前为了在设备之间交换多导生理信号设计的文件格式EDF 是它的扩展。edfread 这一类读取函数的任务就是把里面的信号矩阵、采样率和事件标注完整还原出来。这篇文讲清楚格式边界、读取工具和几个只有踩过坑才会注意的参数。2. 先拆开 EDF 文件本身头文件、信号头和数据记录在写任何读取代码之前值得花十分钟用一个十六进制工具看一眼文件。EDF 不是普通的二进制对象序列而是“ASCII 头 二进制数据区”的混合体。几乎所有你以为麻烦的问题——文件里有多少个通道、每个通道的采样率、数据从哪个字节开始——头部字段里都写清楚了读不出来通常不是格式问题而是偏移没对齐。xxd -l 256 sleep.edf | head -20xxd -l 256只读取文件前 256 字节head -20限制终端输出行数。一个规范的 EDF 文件输出第一行的 ASCII 部分通常就是版本号字符“0”紧接着能看到患者 ID、记录 ID 和开始日期。如果第 244 字节附近有EDFC或EDFD字样说明这是 EDF 文件。不要小看这一步很多“读到一半数据全乱”的文件从这里就能判断是头长度算错还是记录数写错。2.1 文件头的固定偏移字段读 EDF 前先背下来EDF 的文件头固定为 256 字节之后每增加一个信号通道文件头部分就追加 256 字节的信号头。文件头字段全部是 ASCII 数字或字符串读取时必须按固定偏移解析这是 EDF 格式为数不多的硬性约束。下表是解析顺序中用到的主要字段偏移长度字段说明08版本号固定 ASCII “0”EDF 不改这里8152患者 IDEDF 用空格分隔多个子字段1608记录 ID设备号或检查编号1688开始日期dd.MM.yy 格式1768开始时间HH.mm.ss 格式1848头文件字节数必须能被 (NS1)×256 整除1928数据记录数EDF 允许为 -1表示未知1968单条记录时长单位秒通常为 12002信号数量 NS决定后续信号头数量24412格式标记EDF 在这里写 EDFC 或 EDFD偏移 192 处的数据记录数如果等于 -1说明这是 EDF 的断续记录不能靠这个字段做循环终止后面专门讲。偏移 196 处的单条记录时长配合信号数决定了数据区如何切块读取。2.1.1 每个信号头的 256 字节在描述什么文件头之后是 NS 个信号头每个固定 256 字节按顺序存储各通道的 label、传感器类型、物理单位、物理量程、数字量程、滤波信息以及每条数据记录中该通道的采样点数。其中 label 从偏移 0 开始取 16 字节物理单位在偏移 96 处取 8 字节数字最小值和最大值在偏移 128 和 136 处采样点数在偏移 216 处。这些字段共同决定了数据区如何解释物理量程是放大器标定的真实世界值数字量程是 ADC 原始整数范围。两者之间的换算关系是整个 EDF 读取的核心所有读取库最终都在做同一件事import numpy as np def digital_to_physical(raw, dig_min, dig_max, phys_min, phys_max): if dig_max dig_min: return np.zeros_like(raw, dtypefloat) return (raw - dig_min) / (dig_max - dig_min) * (phys_max - phys_min) phys_minraw是从文件数据区直接读出的整数数组dig_min和dig_max对应 ADC 量程通常就是信号头里 digital minimum 和 digital maximum 的值phys_min和phys_max是物理量程。一条经验是如果读取结果整体反相先检查这两个极值是不是填反了而不是怀疑文件损坏。2.2 EDF 与 EDF 的核心差异不只是多了注释EDF 的定位很单纯多导信号逐记录存放不支持事件。EDF 做了三处扩展新增专用的 EDF Annotations 通道记录时间戳和文本事件支持断续记录并用事件时间戳表达记录间隙在头部保留字段写入格式标记。下面这张对比表在实际选型时很有用项目EDFEDF事件标注不支持EDF Annotations 通道记录连续性必须连续支持 EDFD 断续患者信息单段文本结构化子字段数据记录数实际数量可为 -1时间基准靠开始时间推算记录内自带时间戳EDF 对旧 EDF 完全向下兼容读取器只需识别 label 为EDF Annotations的信号并对该通道做文本解析不影响其他通道的数据排布。这也意味着处理现代采集设备的数据时可以直接按 EDF 读取不需要额外判断。2.3 为什么选现成读取库而不是自己写一个市面上能看到的 edfread、pyedflib、EDFlib 做的事情完全一样解析文件头、计算每条记录的字节偏移、按信号头参数做物理量换算、识别注解通道。自己写解析器大约需要两百行代码踩坑点集中在字节序、记录边界和注解字符编码上。除非是嵌入式环境或离线交付场景否则直接用成熟库更稳妥。3. 落地读取edfread 的几种调用方式和参数3.1 先确认你拿到的 edfread 是哪个版本“edfread”这个名字在 MATLAB 生态里流传了很多年至少存在两个常见分支新版 MATLAB 内置的官方函数以及社区流传的独立脚本。两者的返回结构并不一致最省事的判断方式是运行help edfread查看函数签名。新版内置版本的常见签名类似[header, data] edfread(filename)返回的header是结构化对象包含信号标签、采样率、物理单位和极值社区版本的常见签名则是[hdr, rec] edfread(filename)返回的头信息和数据记录排布方式各不相同。无论用哪个版本第一次读取时都先只打印 header确认信号数量和采样率与采集软件里的设置一致再继续处理数据。3.2 MATLAB 环境下读取 EDF 的最小命令以常见的内置调用方式为例读取一个 EDF 文件只需要几行% 读取 EDF/EDF 文件返回头部结构和信号数据 [header, data] edfread(sleep.edf); % 打印信号标签和采样率确认通道顺序 disp(header.SignalLabels) disp(header.SamplesPerRecord) % 只读取指定导联节省内存 [header, data] edfread(sleep.edf, TargetSignals, EEG Fpz-Cz);代码里的header.SamplesPerRecord对应每个通道在每条数据记录中的采样点数不是采样率本身实际的采样率需要用它除以单条记录时长。TargetSignals参数按信号标签过滤适合通道很多但只关心某几个导联的场景。注意不同版本对TargetSignals的取值写法有差异建议先用help edfread确认参数名和匹配规则再写进正式脚本。社区版本的 edfread 调用方式通常是[hdr, rec] edfread(sleep.edf)这里rec的维度排布在不同实现里可能是“通道数 × 总采样点”也可能是“记录数 × 通道数 × 每记录采样点数”拿到后先size(rec)确认维度不要凭经验直接索引。3.3 用 pyedflib 在 Python 里读取和验证Python 生态里最常用的是 pyedflib它封装了 EDFlib C 库同时支持 EDF 和 EDF。最小读取流程如下import pyedflib # 打开文件读取信号头信息 f pyedflib.EdfReader(sleep.edf) labels f.getSignalLabels() fs f.getSampleFrequencies() n_records f.datarecords_in_file # 读取第 0 个通道的全部数据返回值是物理量 signal f.readSignal(0) print(labels[0], fs[0], signal.shape) # 读取 EDF 事件注释onset 单位为秒 annotations f.readAnnotations() f.close()getSampleFrequencies()返回的数组长度与信号数量一致顺序与getSignalLabels()对应。readSignal(index)接收的是信号索引不是标签字符串返回的数组是 float32已经完成数字量到物理量的换算。readAnnotations()返回三个数组事件起始时间、持续时间和事件文本。如果文件里没有注解通道第三个数组就是空的调用前不必单独判断。3.4 用 digital/physical 换算做交叉检查有时候读取结果和采集软件显示的对不上先别怀疑工具手动验证一次换算是更高效的做法import pyedflib import numpy as np f pyedflib.EdfReader(sleep.edf) # 取数字量和物理量程参数 dig_min f.getDigitalMinimum(0) dig_max f.getDigitalMaximum(0) phys_min f.getPhysicalMinimum(0) phys_max f.getPhysicalMaximum(0) # 读取原始整数数组不做缩放的读取方式 raw f.readSignal(0, digitalTrue) # 手动换算并与 readSignal 默认返回值对比 scaled (raw - dig_min) / (dig_max - dig_min) * (phys_max - phys_min) phys_min f.close()readSignal(0, digitalTrue)返回的是原始整数未做缩放默认返回的是换算后的物理量。两者对比如果偏差超过 float 精度范围常见原因有两个一个是物理量程和数字量程的极值在信号头里填反了另一个是字节序解释错误后者会在 5.3 小节展开。这个交叉检查可以在接入任何新数据源时跑一次能省掉后续一整轮的排查时间。4. EDF 的注解通道、断续记录与文件校验4.1 手工解析 EDF Annotations 的 TAL 内容EDF 的注解通道内部使用 TALTime-stamped Annotation List格式存储事件。每条注解由事件起始时间、可选持续时间和事件文本组成字段之间用 ASCII 控制字符 0x14 分隔整条注解以 0x00 结束。pyedflib 的readAnnotations()已经封装好了解析逻辑但排查乱码事件时手工解析一次能定位问题import numpy as np # 从注解通道读取原始字节按 TAL 规则拆分 raw bytearray([0x31, 0x32, 0x2E, 0x35, 0x14, 0x53, 0x6C, 0x65, 0x65, 0x70, 0x14, 0x00]) parts bytes(raw).split(b\x00)[0].split(b\x14) onset float(parts[0]) # 事件起始时间单位秒 text parts[-1].decode(utf-8, errorsreplace) # 事件文本上面代码里的0x31 0x32 0x2E 0x35是 ASCII 数字“12.5”0x14是字段分隔符0x53到0x70是文本“Sleep”。实际读取时注解通道的信号样本本身就是这些 ASCII 字节直接按通道数据读出再做同样的拆分即可。文本部分默认是 UTF-8但国产部分设备可能直接写 GBK解密失败时优先尝试gb18030。4.2 数据记录数为 -1 时怎么安全读取EDF 的断续格式EDFD允许在头部把记录数写成 -1表示文件中实际有多少条记录未知。此时用for i in range(record_count)的方式读文件必定出错正确做法是循环读取直到文件末尾并在每次读取前检查剩余字节数是否足够一条记录import os import pyedflib f pyedflib.EdfReader(discontinuous.edf) ns f.signals_in_file record_bytes sum(f.getSamplesPerRecord()) * 2 # 假设数据区为 int16 fsize os.path.getsize(discontinuous.edf) offset f.header[header_size] records [] while offset record_bytes fsize: # 按记录读取所有通道的原始数据块 block f.readPhysicalSignal(0) records.append(block) offset record_bytes f.close()getSamplesPerRecord()返回各通道在每条记录中的采样点数乘以 2 得到字节数前提是数据区按 int16 存储。如果文件里混有 int8 或 float 类型字节数计算方式会不同需要以信号头里的存储格式为准。循环终止条件用的是剩余文件大小而不是头部记录数字段这是处理断续记录最稳妥的方式。4.2.1 EDFD 断续记录的事件时间戳怎么和信号对齐断续记录里注解通道的时间戳表达的是“事件相对文件开始时间的绝对时间”而不是相对当前记录内偏移的时间。对齐时不能简单用“记录的索引 × 单条记录时长”来推算事件在信号中的位置因为中间缺失的记录段会造成累积偏移。常见做法是先把所有注解事件读出来再根据事件起始时间在信号时间轴上做插值定位事件时间乘以对应通道的采样率得到事件在总信号序列中的采样点索引。这样即使中间跳过几段记录信号总长度和事件位置依然能对应上。4.3 读取 EDF 文件的一分钟校验法拿到一个新 EDF 文件数据读出来之前先做三个快速检查能过滤掉大部分损坏文件import os import pyedflib f pyedflib.EdfReader(verify.edf) header_size f.header[header_size] ns f.signals_in_file # 1. 头文件字节数必须能被 (通道数 1) 整除 assert header_size % ((ns 1) * 256) 0 # 2. 文件大小减去头大小后应能被单条记录字节数整除 record_bytes sum(f.getSamplesPerRecord()) * 2 fsize os.path.getsize(verify.edf) assert (fsize - header_size) % record_bytes 0 # 3. 抽查第一通道的最大值是否落在数字量程内 values f.readSignal(0, digitalTrue) assert values.max() f.getDigitalMaximum(0) f.close()第一项检查头文件偏移对齐第二项检查记录切分完整性第三项检查数值范围是否可信。三个断言都通过的前提下如果读取结果仍然异常才需要考虑字节序和设备标定问题。4.4 别把 FPGA 的 EDIF 网表当成 EDF 来读搜索“EDF 读取”的时候很容易串到另一个完全不同的领域Vivado 里生成网表文件的后缀也是 .edf属于 EDIFElectronic Design Interchange Format网表在 zcu208 这类 FPGA 开发板上做 IP 移植时会频繁出现。那个格式与本文讨论的 European Data Format 没有任何关系混淆后按医学信号解析必然失败。判断方法很简单用文本编辑器打开文件开头。如果开头是(EDIF这样的括号表达式那是 FPGA 网表如果前 256 字节是规整的 ASCII 字段且偏移 192 处能解析出数字记录数才是本文讨论的医学 EDF 文件。搜索时加上“生理信号”“脑电”“睡眠”之类的限定词能明显降低误入其他领域的概率。5. 大文件读取的落地技巧用头部参数省掉十几分钟5.1 读取前先算总时长和总样本数EDF 头部已经给出记录数和单条记录时长两者相乘就是总秒数。总样本数等于记录数乘以每条记录各通道采样点数之和。读取前先算这两个值能判断文件是否值得整读还是应该分段读取import pyedflib f pyedflib.EdfReader(long.edf) n_records f.datarecords_in_file record_duration f.header[record_duration] samples_per_record f.getSamplesPerRecord() total_seconds n_records * record_duration total_samples n_records * sum(samples_per_record) print(f共 {total_seconds} 秒{total_samples} 个采样点) f.close()如果总样本数换算成数组后超过内存可接受范围建议改用按记录索引跳跃读取的方式只读取需要的区间避免整个文件载入内存。5.2 采样率不一致的通道不要拼成大矩阵睡眠监测文件里很常见的情况是EEG 通道 256 HzSpO2 通道只有 1 Hz。强行把所有通道 resample 到同一采样率再拼成矩阵不仅浪费内存还会引入插值误差。更合理的做法是保持按通道存储后续分析时按各自采样率独立处理。需要做多通道统一时优先以最高采样率通道为基准低采样率通道做线性插值或前向填充。处理前先打印所有通道的getSampleFrequencies()确认哪些通道采样率不一致再决定是否需要重采样。EDF 的所有通道在每条记录内的采样点数都写在信号头里这个值天然允许不同通道采用不同采样率读取时不要默认所有通道等长。5.3 字节序与越界检查的最后一招EDF 数据区通常按小端序存储 int16但少数采集设备会写出大端序。如果读取结果数值整体异常偏大或偏小最大值接近 32767 或 -32768先做一个字节序检查import numpy as np raw np.fromfile(sleep.edf, dtypei2) # 显式小端 max_val np.abs(raw).max() if max_val 30000: raw_le raw.byteswap() # 尝试字节交换后重新解析 print(原始数据疑似字节序异常)dtypei2显式声明小端有符号 16 位整数byteswap()将字节序反转。这个检查放在读取流程的最后一步因为前两步的头部校验和记录切分能先排除大多数结构性问题剩下的才需要怀疑字节序。养成新数据源接入时先跑一次完整校验的习惯比临时找 bug 高效得多。本文还有配套的精品资源点击获取
返回列表