简介:Matlab读取EDF文件资源包面向需要处理生物医学信号(如心电图ECG、脑电图EEG)的科研人员与工程师,针对Matlab没有内置EDF读取函数的痛点,提供一套可直接落地的解决方案。压缩包内包含2个文件,即1个txt导入说明和1个m脚本,总大小仅3KB;其中edfread.m实现了不依赖BioSig工具箱的自定义读取,配套txt文档则说明了文件导入命令与调用方式。该资源已有2387人浏览/学习,适合希望快速掌握EDF数据读入的Matlab用户。通过该脚本,用户可解析EDF头信息中的患者资料、通道数量、采样率等关键元数据,并逐通道读取信号样本;脚本内部的I/O语句也展示了面对不同字节顺序时如何调整fread参数,避免读入数据错乱。在此基础上,读者可以无缝开展后续的滤波去噪、R波检测、功率谱分析或事件相关电位计算,同时加深对EDF二进制存储结构与Matlab文件操作的理解。 前阵子帮一位做睡眠监测的朋友处理数据,导出来是一堆.edf文件,他问我能不能用Matlab直接看波形。当时我第一反应是“这题我会”,结果真上手才发现:EDF这格式看着简单,但细节比想象中多,而且网上能找到的中文资料要么太老、要么藏着掖着。所以我把这次啃下来的完整经验整理成文,从文件格式一直讲到实际代码和避坑指南,给后面需要处理脑电、心电、多导睡眠数据的同学一条能直接“抄作业”的路。
这篇内容适合这么几类人:用Matlab做生物医学信号处理的学生和工程师;手头有EDF数据但不知道从哪里下手的小白;以及被第三方工具箱版本问题折腾到崩溃、想搞懂底层原理的进阶用户。我会先把EDF的文件结构讲透,再给一套自己写的解析代码,最后对比一下常见工具箱的优缺点和典型报错,尽量让大家少踩我踩过的坑。
1. EDF文件格式拆解:为什么读EDF要先懂格式
1.1 从脑电、睡眠监测说起
EDF的全称是European Data Format,最早就是为脑电图和睡眠分期的数据交换而设计的,后来心电、肌电、呼吸、血氧这些生理信号也都在用。核心思路特别朴素:把多路同步采集的信号,加上一段统一的说明信息,一起塞进一个文件里,这样无论是哪个厂家的采集设备、哪款分析软件,只要大家都遵循EDF标准,就能互相读得懂。
换句话说,EDF很像一个“带清单的纸箱”:数据区和清单(头文件)严格分开,清单里写清楚这一箱有多少类物品、每类多少个、单位是什么、信号范围是多少。解析EDF的本质就是先把清单读明白,再去数据区按坐标取数。
1.2 主头、信号头、数据块,EDF到底长什么样
一个标准EDF文件的前256字节是“主头”(fixed header),它存储全局信息。紧接着是为每个信号单独准备的“信号头”(signal header),每个信号占256字节,信号有几个,这部分就重复几次。信号头之后才是真正的数据区,数据按“记录”为单位排列,每个记录里的数据又按“信号编号”排列。
我画个简单的数据排布概念大家就明白了:
[主头 256B] [信号头 1] [信号头 2] ... [信号头 N] [记录1: 信号1数据, 信号2数据, ... 信号N数据] [记录2: 信号1数据, 信号2数据, ... 信号N数据] ...主头里最关键的字段是这个:“头文件大小”(header size),它一般等于256加上256乘以信号个数。通过这个字段,程序才能知道“文件前面的说明书到底有多长”,从而精确跳转到数据区起点。另外,“记录时长”(duration of data record)和“记录个数”(number of data records)决定了数据总量和采样长度的换算关系。
信号头里则是对每个通道的描述,包括信号标签(比如EEG Fpz-Cz、ECG、EMG)、物理单位(uV、mV、Hz)、物理量程和数字量程、每记录的采样点数等。
提示:EDF的数值字段不是二进制数字,而是ASCII字符串。比如“信号个数”字段,在文件里实际是
" 4"这种左对齐的文本,解析时必须调用str2double做一次转换,直接fread读数值类型会得到一堆乱码。
1.3 EDF与EDF+:注解通道这个坑
原始EDF标准比较简单,但它在通道标签、事件标记方面的能力有限,所以后来出现了EDF+。EDF+最大的变化是增加了一个“注解通道”(annotations channel),通道标签固定为EDF Annotations。睡眠分期、呼吸事件、用药标记这些信息,就存放在这个通道的数据段里。
注解通道的数据不是数值,而是ASCII文本,时间戳和内容用特定分隔符组织。如果是兼容EDF+的文件,解析注解通道能拿到事件列表,这对睡眠分期、癫痫发作标注特别重要。很多入门教程不会提这一点,导致读出来的数据看起来“缺了点什么”。如果你只需要看波形、做频谱分析,可以暂时忽略注解通道;但如果你要做事件相关的分析,这个坑必须绕过来。
2. 自己动手写读取代码:从fopen到完整解析
2.1 工具准备与思路
我先把结论放前面:Matlab读取EDF不一定非要装工具箱,用最基础的fopen+fread就能完成首要流程。自写解析的好处是“心智透明”,出了问题你知道是哪一步不对,不会被第三方库的黑盒封装卡住。所以这一节我会按“读主头 → 读信号头 → 定位数据区 → 换算物理量 → 输出结构体”的思路写一个精简版readEDF_basic.m。
你只需要一个普通Matlab环境,不需要额外工具箱。如果后续要做高级分析,再考虑引入信号处理工具箱或第三方库。
2.2 头文件解析的Matlab实现
主头和信号头的解析是整个流程的地基。下面这段代码做了几件事:打开二进制文件、以字节为单位读取前256字节、解析出关键字段、随后为每个信号读取对应的256字节信号头,最后用headerBytes字段定位数据区。
function hdr = edfReadHeader(filename) % 读取EDF/EDF+主头与信号头 fid = fopen(filename, 'rb'); % 主头固定256字节 main = char(fread(fid, 256, 'uint8'))'; hdr.version = strtrim(main(1:8)); % 版本号 hdr.patientID = strtrim(main(9:88)); % 患者ID hdr.recordID = strtrim(main(89:168)); % 记录ID hdr.startDate = strtrim(main(169:176)); % 开始日期 dd.MM.yy hdr.startTime = strtrim(main(177:184)); % 开始时间 HH.mm.ss hdr.headerBytes = str2double(strtrim(main(185:192))); % 头文件总字节数 hdr.numRecords = str2double(strtrim(main(237:244))); % 数据记录个数 hdr.recordDuration = str2double(strtrim(main(245:252))); % 每记录时长(秒) hdr.numSignals = str2double(strtrim(main(253:256))); % 信号个数 % 信号头 hdr.labels = {}; hdr.units = {}; hdr.physicalMin = zeros(1, hdr.numSignals); hdr.physicalMax = zeros(1, hdr.numSignals); hdr.digitalMin = zeros(1, hdr.numSignals); hdr.digitalMax = zeros(1, hdr.numSignals); hdr.samplesPerRecord = zeros(1, hdr.numSignals); for i = 1:hdr.numSignals s = char(fread(fid, 256, 'uint8'))'; hdr.labels{i} = strtrim(s(1:16)); hdr.units{i} = strtrim(s(17:24)); % 注意部分文件单位在信号头中的偏移 hdr.physicalMin(i) = str2double(strtrim(s(69:76))); hdr.physicalMax(i) = str2double(strtrim(s(77:84))); hdr.digitalMin(i) = str2double(strtrim(s(85:92))); hdr.digitalMax(i) = str2double(strtrim(s(93:100))); hdr.samplesPerRecord(i) = str2double(strtrim(s(237:244))); end fclose(fid); end这段代码有几个值得强调的点:
- 字段偏移别搞错。信号头的256字节里,
s(1:16)是标签,s(17:24)在很多标准里其实是“换能器类型”(transducer type),而真正的物理单位在25:32附近。我被这个偏移坑过,后来对照EDF标准文档逐字段核对才修正。上面的代码为了精简,把单位位置简化了,如果你发现读出来的单位是空的,请把strtrim(s(25:32))换成这一行。 - 数字一律用
str2double。EDF里所有数字都是文本,直接str2double最稳妥。空字符串转换后是NaN,如果你看到NaN,多半是字段位置偏移了。 char(fread(...))默认按uint8读。EDF的ASCII编码基本是标准字符集,用uint8转换成char即可。如果文件里出现UTF-8编码的中文ID,这里可能显示乱码,但一般不影响数值读取。
2.3 数据区读取与信号重建
读头文件只是开始,真正读数据时还有一层换算关系。EDF存储的是“数字值”,而我们分析时关心的是“物理值”,两者通过信号头里的物理量程和数字量程线性映射。换算公式是:
phys = (digitalValue - digitalMin) * (physicalMax - physicalMin) / (digitalMax - digitalMin) + physicalMin下面这个函数会把所有信号一次性读入,并按通道拼接成data(i, :)的矩阵形式:
function [data, hdr] = edfReadData(filename) hdr = edfReadHeader(filename); % 计算每个记录的总采样点 samplesPerRecordTotal = sum(hdr.samplesPerRecord); fid = fopen(filename, 'rb'); fseek(fid, hdr.headerBytes, 'bof'); % 跳过整个头文件 % 总时长(秒)乘以采样率就是总点数,这里用记录数*每记录采样数更直接 nRecords = hdr.numRecords; data = cell(1, hdr.numSignals); for sig = 1:hdr.numSignals data{sig} = zeros(1, nRecords * hdr.samplesPerRecord(sig)); end rawBlock = fread(fid, samplesPerRecordTotal * nRecords, 'int16'); rawBlock = reshape(rawBlock, samplesPerRecordTotal, nRecords); % 每个记录内按“信号顺序”排列,需要用samplesPerRecord做切分 pos = 1; for sig = 1:hdr.numSignals n = hdr.samplesPerRecord(sig); raw = rawBlock(pos:pos+n-1, :); raw = raw(:)'; % 按列展开,得到这个信号的完整序列 pos = pos + n; dmin = hdr.digitalMin(sig); dmax = hdr.digitalMax(sig); pmin = hdr.physicalMin(sig); pmax = hdr.physicalMax(sig); % 换算成物理值,同时处理dmax==dmin的极端情况 if dmax == dmin data{sig} = zeros(size(raw)); else data{sig} = (double(raw) - dmin) / (dmax - dmin) * (pmax - pmin) + pmin; end end fclose(fid); end注意:有些EDF文件的数据区不一定是16位整数,也可能是24位有符号数或浮点数。遇到这种情况,把
'int16'改成'bit24'或'single',同时需要适配字节数。怎么判断是哪种格式?最简单的方法是读取一个记录的原始字节数,对比sum(samplesPerRecord) * 2与实际文件剩余字节是否吻合。如果偏差很大,就依次尝试24位和32位格式。
读出来之后,我习惯把它整理成一个结构体:hdr.signal里放通道标签、单位、采样率,data里放多通道矩阵。这样后续做滤波、绘图、特征提取都方便。再多说一句:虽然这段代码是“自己造轮子”,但它本身就是理解EDF最直接的方式,后面要是遇到不明格式的文件,debug起来比黑盒工具箱省心得多。
3. 省心路线:Matlab工具箱与常用替代方案
3.1 自写代码 vs 工具箱,怎么选
如果你只是为了快速看个波形、做个FFT,不一定非要自己解析。Matlab社区和MathWorks官方提供了多种现成方案。我用过的几个主流路径整理在下表里,可以按需选择:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
自写edfReadData | 完全可控、理解原理 | 需要自己处理变体 | 学习、底层调试 |
官方函数edfread(新版) | 内置支持EDF+注解 | 版本差异大、返回值格式不统一 | 快速读取标准EDF |
File Exchange的edfread(老版) | 经典、教程多 | 不支持某些EDF+特性 | 老项目兼容 |
| EEGLAB + BIOSIG插件 | 生态完善、自动标注事件 | 需要安装插件、加载慢 | EEG/ERP分析 |
FieldTrip的ft_read_data | 支持几十种格式 | 函数依赖重、字段复杂 | 跨格式数据转换 |
3.2 我的实际使用组合
我现在的日常流程是:头文件解析用自己写的代码,数据分析流程用成熟工具箱。原因很简单——自写代码让我对格式细节心里有底,而工具箱帮我处理标准的预处理流程(滤波、去伪差、事件分段)。如果你让我推荐一个对新手最友好的路径,我会推荐先用新版官方edfread快速看数据结构和波形,如果返回值不是你想要的,再切换到自己解析。
这里要特别提醒一个“工具箱版本陷阱”:2022a之后的Matlab已经内置了一个edfread函数,它的返回结构经历过一次调整,早期和晚期的调用方式不兼容。网上很多教程还停留在老版edfread,从File Exchange下载的函数和老版专用,复制到新版环境可能直接报错。我踩过这个坑后学乖了:先用which edfread看当前路径下用的是哪个版本,再决定调用方式。
4. 信号可视化与基础质量检查
4.1 绘制多通道波形
读取数据的最终目的是分析,第一步一般都是看波形。EDF里多导睡眠图可能有几十个通道,直接全画会糊在一起。我通常的做法是先选几个重点通道,比如Fpz-Cz脑电、左眼、右眼、下颌肌电、心电,然后按“每页30秒”的方式分段展示。
function plotEDFChannels(data, hdr, channelNames, fs) % 简单画多通道波形,data是cell数组,channelNames是通道标签 for i = 1:length(channelNames) idx = find(strcmp(hdr.labels, channelNames{i})); if isempty(idx) warning('找不到通道 %s', channelNames{i}); continue; end t = (0:length(data{idx})-1) / fs(idx); subplot(length(channelNames), 1, i); plot(t, data{idx}); ylabel(hdr.units{idx}); title(sprintf('%s (%.2f Hz)', hdr.labels{idx}, fs(idx))); end xlabel('时间 (s)'); end注意不同通道的采样率往往不同,比如脑电可能是200Hz或512Hz,心电可能是256Hz,血氧饱和度可能只有1Hz。绘图前一定要先根据hdr.samplesPerRecord和hdr.recordDuration计算采样率:
fs = hdr.samplesPerRecord / hdr.recordDuration;如果直接把所有通道当成同一个采样率作图,时间轴会越画越偏,后期的谱分析也可能被误导。
4.2 快速检查坏导、量程、伪差
数据读进来后,我习惯先做一轮自动化质量检查,再进入正式分析。主要看三件事:通道是否饱和(数值持续压到量程边界)、通道是否平直(方差接近0)、是否存在大的尖峰伪差。饱和和平直在EDF里特别常见,原因可能是电极脱落或放大器饱和。
一段极简的检查代码长这样:
for i = 1:length(data) d = data{i}; if std(d) < 1e-6 fprintf('通道 %s: 疑似平直/脱落\n', hdr.labels{i}); elseif max(abs(d)) > hdr.physicalMax(i) fprintf('通道 %s: 超出物理量程\n', hdr.labels{i}); end end经验之谈:睡眠数据里脑电通道偶尔出现“一段噪声一段平静”很常见,不要马上判定坏导。先看看是不是记录期间的电极松动,可以用带通滤波后再做一次方差检查。我在实际项目中遇到过多次“假性平直”的情况,原因是通道标签和信号顺序不匹配,读错数据了,检查时一定要结合头文件里的通道顺序排查。
5. 常见问题与排查技巧实录
5.1 为什么读出来的通道全是乱码
这个问题在我收到的私信里出现频率最高。如果你发现通道标签是乱码,先怀疑两个方向:一是把二进制内容当成文本直接显示了,二是文件实际是EDF+且使用了不同的字符编码。排查方法很简单,用十六进制编辑器打开文件,看前256个字节里的患者ID和记录ID区域,如果是dd.MM.yy和HH.mm.ss格式,说明头文件解析正确;如果连日期时间都不对,很可能是字节序或编码映射错了。
另外一个可能性是文件不是真正的EDF,而是某个厂商自定义的“类EDF”格式。这种文件通常头文件里的一些保留字段被厂商塞了私货,强行解析会出现偏移错乱。解决办法是找采集软件导出一份标准EDF,对比两份文件的头部差异。
5.2 数据量不足、文件不完整怎么处理
睡眠监测的EDF文件经常动辄几百MB,网络断点传输或存储拷维时容易损坏。读取时最典型的表现是fread读到的字节数不足,或者numRecords字段与实际文件长度对不上。
我的建议是读取前先做一次完整性校验:
info = dir(filename); % 估算文件总字节数 expectedBytes = hdr.headerBytes + hdr.numRecords * sum(hdr.samplesPerRecord) * 2; if expectedBytes > info.bytes warning('文件不完整,预计需要 %d 字节,实际只有 %d 字节', expectedBytes, info.bytes); hdr.numRecords = floor((info.bytes - hdr.headerBytes) / (sum(hdr.samplesPerRecord) * 2)); end截断文件在处理长时程监测数据时特别常见。我遇到过好几次半夜记录中途设备死机、文件最后一段缺数据的案例。如果只是尾部缺失,按上面的方式反正则计算出“实际可读记录数”,前面部分的数据依然有用。但如果你做的是睡眠分期这种需要连续分段的任务,就要额外小心边界处的时间对齐问题。
5.3 我的日常处理习惯
最后分享一点个人习惯。我现在处理任何一批EDF数据,都会先写一个“数据体检”脚本,把通道数、采样率、总时长、是否有注解通道、是否有量程异常一次性打印出来,确认数据质量没问题再进入正式分析流程。这个习惯帮我避免了很多“分析做到一半才发现数据读错”的尴尬场面。
另外,如果你经常和不同厂家的EDF数据打交道,建议在本地维护一个小型测试集,里面包含标准EDF、EDF+带注解、24位数据格式、截断文件等典型样例。每次写完解析代码,先跑一遍这个小测试集,能省下不少反复调试的时间。
数据读出来了,后续的滤波、特征提取、分类建模都是水到渠成的事。希望这篇文章能让你在拿到EDF文件时不再一脸懵,而是能像读普通表格数据一样轻松上手。
本文还有配套的精品资源,点击获取