简介:面向生物医学信号处理开发者的Matlab EDF读取工具包,解决Matlab中直接读取欧洲数据格式(EDF)心电、脑电等生理信号的常见痛点。包内包含一个可直接运行或改写的edfread脚本,以及文本格式的导入使用说明,共两个文件,压缩包仅三KB,轻量易用。该资源重点演示了不依赖外部工具箱、通过底层文件读写函数解析EDF头结构与通道数据的方法,同时附带了BioSig工具箱中edfread函数的备选调用思路,帮助读者根据需求选择路径。已有两千三百八十七人学习下载,适合需要掌握EDF文件解析、为后续滤波分析与特征提取打基础的Matlab初、中级工程人员。阅读脚本与说明后,可快速理解文件头元数据、多通道样本读取、采样率处理等关键环节,直接应用于项目开发或科研场景,提升数据预处理效率。 在医学信号处理这个圈子里,EDF文件几乎是绕不开的标准格式。只要你跟多导睡眠监测、脑电记录、生理信号采集打过交道,八成都会遇到这类后缀为 .edf 的文件。很多同学第一次拿到 EDF 文件就往 Matlab 里拖,结果 load 读不了、fopen 手动解析又不知道从哪下手,最后卡在最基础的读取环节。这篇文章我会用完整的实操视角,把 Matlab 读取 EDF 文件的原理、工具选型、代码实现、常见报错和实际项目里的处理流程一次性讲透。不管你只是临时看一个文件,还是要批量处理几百份睡眠数据,都能从这里找到直接能用的方案。
1. EDF文件到底装了什么:先摸清格式再动手
1.1 EDF和EDF+,别再当成同一种东西
EDF 全称 European Data Format,最早是 1992 年欧洲的一些睡眠研究机构为了交换多导生理记录数据而设计的标准格式。设计目标很纯粹:把多通道、不同采样率的信号连同患者信息、记录参数打包在一个文件里,跨设备、跨软件都能无损交换。
后来到了 2002 年,标准升级成了 EDF+。EDF+ 主要解决两个老版本遗留的问题:一是多段记录之间的时间连续性,二是事件标注的标准化。解决办法是在文件末尾或者通道列表中增加一种特殊的注解通道(Annotation Channel),用时间戳和文本事件记录入睡时间、呼吸事件、肢体运动等标记。所以你拿到手的一个 .edf 文件,内部可能是 EDF 也可能是 EDF+,只靠后缀看不出区别。读取之前先确认版本,能省掉后面一大半的调试时间。
1.2 header结构拆解:为什么不能直接load
EDF 不是文本格式,也不是 Matlab 原生的 .mat 格式,所以 load、importdata 这些函数全都用不了。它本质上是一个二进制文件,由头部信息区和数据区两大部分组成。
头部信息区分为主头部和每个通道的子头部。主头部固定 256 字节,包含版本号、患者 ID、记录 ID、开始日期、开始时间、头部长、记录数、记录时长、信号数量等字段。其中日期和时间以 ASCII 字符串形式存储,格式是 dd.MM.yy 和 HH.MM.SS,不少人在手动解析时是在这里踩坑的。
每个通道子头部也是固定 256 字节,包含信号标签、传感器类型、物理单位、物理量程、数字量程、滤波设置、每记录段采样点数等字段。数据区则严格按照每个记录段(record)内各通道采样点数的顺序交织存储。
主头 256 字节 + 信号头 N*256 字节 + 数据区(按记录段存储)手动解析的代码逻辑并不复杂,但字段偏移量、字节顺序、长度解释只要错一位,整个数据就全乱了。这也是我强烈建议优先用现成工具的原因——格式解析这种成熟工作,没必要从造轮子开始。
2. Matlab读取EDF的主流方案:从官方工具到开源生态
2.1 新版edfread:零依赖,直接出timetable
现在处理 EDF 文件,我首选的是 File Exchange 上重构后的新版 edfread 函数。这批新版本读取结果直接是 timetable 格式,每一列是一个信号通道,第一列是自动生成的绝对时间轴,用起来非常顺手。
data = edfread('sleep_study.edf'); % 查看所有通道名称 data.Properties.VariableNames % 查看前几行,确认时间轴和数据是否正常 head(data) % 直接画出某个通道的前30秒波形 plot(data.Time, data.C3_M2) xlim([0 30]) xlabel('Time (s)') ylabel('Amplitude (uV)')新版 edfread 最大的优势在于它内部把 EDF+ 的注解通道处理掉了,返回的 timetable 里通常只保留真正的信号通道,注解事件会单独提取出来。对于大部分只关心波形数据的分析任务,这段代码拿过来就能用。
不过要注意一点,新版 edfread 对 Matlab 版本有要求,timetable 需要 R2016b 之后的版本。如果你的环境比较老,或者需要更精细的分段读取控制,那就得看下一节的老版方案。
2.2 老版edfread与分段读取:大数据量的救命稻草
在 File Exchange 上流传最广的那一版 edfread,返回结果是两个变量:hdr 是所有头部信息的结构体,record 是读取到的实际数据。老版本虽然 API 不够现代,但它支持通过参数指定要读取的记录段范围和目标通道,这在处理动辄几个 GB 的 24 小时多导数据时非常实用。
% 只读取第1到第10个记录段 [hdr, record] = edfread('long_record.edf', 'records', 1:10); % 只读取特定通道,减小内存压力 [hdr, record] = edfread('long_record.edf', 'targetSignals', {'C3-M2', 'EOG-L'});实际操作中我是这样用的:先用一次不带任何参数的 edfread 把 hdr 读出来,看清楚总记录段数、每段时长、通道数量和采样率,然后再根据分析需要分段读取或者只挑关键通道。这样可以精确控制内存占用,避免一次性把几个 GB 的数据全部压进内存把机器搞死。
老版 edfread 还存在一个变体叫 edfreadUntilDone,常见于 EEGLAB 生态里。它解决的问题是有些 EDF 文件的记录段数量在头部标注不正确,读到中间就报错了,edfreadUntilDone 会一直读到文件末尾才停止。遇到厂商导出的不规范文件,这个函数反而比标准版更可靠。
2.3 EEGLAB和Biosig:科研党绕不开的组合
如果你做的是脑电或睡眠相关的科研分析,不太可能只用裸 Matlab 来读取和处理数据。EEGLAB 是脑电领域最常用的工具箱,内部集成了 Biosig 工具包,通过 pop_biosig 函数就能把 EDF 文件读成 EEGLAB 的 EEG 结构体。
EEG = pop_biosig('sleep_study.edf'); [ALLEEG, EEG, CURRENTSET] = pop_newset(ALLEEG, 0, 'setname', 'sleep_raw', 'gui', 'off'); eeglab redraw;pop_biosig 的好处是不仅能读数据,还会把 EDF+ 注解通道里的事件自动映射到 EEG.event 字段,睡眠分期、呼吸事件、肢体运动标记直接就能在 EEGLAB 的界面里看到。后续做滤波、重参考、跑 ICA,整个流程是无缝衔接的。
Biosig 本身也有独立的 Matlab 接口,核心函数是 sload。它比较复杂,但兼容性极强,很多医院设备导出的非标准 EDF,标准 edfread 读不了的情况下,sload 往往能硬撑过去。建议把它当成一个兜底方案,遇到常规工具读不懂的文件时再拿出来用。
3. 文件读进来之后怎么办:信号解析与通道管理
3.1 输出结构里到底有什么
很多新手读完之后,对着工作区里的变量不知道下一步该看什么。我先说新版 edfread 的情况:返回的 data 是 timetable,每一列的名字就是通道标签,行是采样点或记录段,具体情况取决于文件本身的组织方式。
% 获取信号的采样率 fs = 1 / seconds(data.Time(2) - data.Time(1)); % 查看每个通道的数据类型、范围和缺失情况 summary(data) % 把某一段信号转成普通向量 eeg_channel = data.C4_M1;老版 edfread 返回的 hdr 结构体则包含更底层的元数据:hdr.signal 是一个数组,每个元素对应一个通道,里面有 label、physicalMin、physicalMax、digitalMin、digitalMax、samples 等字段。这些字段在做工程量程转换时非常重要,尤其是遇到一些不返回物理单位的异常文件。
3.2 EDF+注解信号的处理思路
EDF+ 的注解通道不是常规意义上的信号,它内部用 TAL(Timestamped Annotated Lists)格式存储事件。每一段注解记录以+开头,后面跟开始时间,中括号内是事件描述。比如+1800[Sleep Stage W]表示在 1800 秒处出现觉醒期。
新版 edfread 会自动把注解通道从信号列表中分离,老版则会把名为 EDF Annotations 的通道和其他信号混在一起返回。如果你用的是老版,需要自己把标签里包含 Annotation 的通道过滤掉,再解析其中的时间戳和事件文本。
% 老版edfread中分离注解通道 annotation_idx = contains({hdr.signal.label}, 'EDF Annotations'); signal_idx = ~annotation_idx; % 事件提取后的简单处理:将事件和对应发生时间存入表格 % 这一步通常要在读取时输出原始记录文本后自行解析在 EEGLAB 里这个过程是自动完成的,pop_biosig 直接就把事件填到了 EEG.event 结构里。所以如果事件标记对你的分析很重要,强烈建议直接用 EEGLAB 生态来处理,省去手写正则表达式的痛苦。
3.3 通道筛选、重命名与单位换算
EDF 文件里的通道名是设备厂商写入的,同一个导联在不同品牌设备上有各种叫法。C3-M2、C3_A2、C3/M2,虽然指的都是同一个脑电通道,但字符串匹配时就是不一样。我在代码里统一用一个通道映射表,先把原始通道名映射成自己定义的统一名称,再做后续分析。
channel_map = { 'C3-M2', 'C3'; 'C3_A2', 'C3'; 'C3/M2', 'C3'; 'EOG-L', 'EOGL'; 'EOG-R', 'EOGR'; };单位换算方面,正规的 EDF 文件会直接按物理单位存储数据,读出来就是微伏。但有些厂商导出的文件提供的是原始数字量,需要按照通道头里的物理量程和数字量程做线性换算。
换算公式:phys_value = (digital_value - digital_min) * (physical_max - physical_min) / (digital_max - digital_min) + physical_min
这个公式看着简单,但实际文件里经常出现 digital_max 和 digital_min 相等的情况,一算就无穷大。遇到这种情况只能回退到原始 ADC 值,按设备厂商手册里的增益系数处理。所以建议每次读取完都先做一轮快速统计检查,plot 几个通道的波形看看幅值是否在合理范围,比事后发现数据不对再返工高效得多。
4. 我在实际项目中踩过的坑:错误排查与性能优化
4.1 常见报错与问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取后所有信号都是空的或全零 | 数字量程字段异常或读取接口不兼容 | 换 Biosig 的 sload 读取,或者用文本方式检查通道头部信息 |
| 通道数量对不上 | 部分通道被识别成注解通道 | 查看所有变量名,确认是否有 EDF Annotations,手动剔除 |
| 时间轴错乱或开始时间不对 | 文件头部的日期时间字段被厂商写成了非标准格式 | 用 pop_biosig 自动解析,或者手动解析头部后修正时间戳 |
| 文件很大,读取时内存不足 | 一次性读数个 GB 数据 | 老版 edfread 分段读取,或只选目标通道 |
| 中文路径或文件名包含特殊字符导致读取失败 | Matlab 对某些编码支持不完善 | 路径和文件名统一改成纯英文 |
最典型的案例是我处理某医院设备导出的 EDF,文件头部标称 16 个记录段,但实际文件末尾还有大量数据。标准 edfread 读到第 10 段就中断,报段索引越界错误。换成 edfreadUntilDone 后一切正常。后来查资料才知道,不少临床设备的系统时钟在记录中段发生过调整,导致额外多写了几个记录段,但头部统计没有更新。
另一个高频问题是通道顺序错乱。EDF 标准保证的是每个通道的数据块按头部声明的顺序存储,但部分设备在软件升级后导出的文件,头部声明顺序和实际存储顺序不一致。排查方法是用已知信号源测试,比如先记录一段有明显特征的波形(如方波校准信号),读进 Matlab 后检查每个通道的形状,就能定位顺序是否错位。
4.2 大数据量下的性能优化思路
多导睡眠监测动辄十几小时,数据量轻松超过 1 GB,全量读入内存后再处理很容易导致系统卡死。我在项目中总结出来的策略是:分层读取。
第一层,先读头部信息,确认通道数量、采样率、总时长和文件大小,做初步质量评估。第二层,抽取前几个记录段做快速波形预览,确认信号质量。第三层,根据分析需求决定是分段读入还是只读取目标通道。对于必须全量处理的任务,在读取后马上转成 single 类型,并用 tall 数组或分块处理的方式做后续滤波和分析。
% 读取后转成single,能节省近一半内存 data.C3_M2 = single(data.C3_M2); % 分段处理时的常用做法:只保留感兴趣通道 target = data(:, {'C3_M2', 'EOG_L', 'EMG'});如果同一个文件会被反复分析,我通常会先把 EDF 转存成 .mat 文件,后续直接 load,省去每次从二进制重新解析的时间。转换时同步把采样率、通道名、记录时长等元数据打包成结构体一起存,后面所有脚本只需读这一个 .mat,统一又高效。
4.3 波形质量检查不能省
读取只是第一步,真正让人头大的是数据质量问题。电极脱落、导联松动、工频干扰都在 EDF 里原样保存着。如果读取后不先检查波形质量就直接跑分析,后面出来的结果几乎一定会被伪迹污染。
我的习惯是读取后立刻做一个全通道的质量报告:计算每个通道的均值、标准差、过零率和绝对幅值,输出一个简单的文本摘要。某个通道如果标准差异常大,多半是导联松脱或直流偏置严重;如果标准差接近零,十有八九是电极完全脱落或者放大器饱和。这样批量处理几十个文件时,扫一眼摘要就能快速定位坏文件,不用逐个打开看波形。
5. 从零到一:睡眠脑电的前处理小实战
5.1 读取并检查文件完整性
下面我用一个实际睡眠分期的前处理流程来演示整套操作。假设拿到一份标准的多导睡眠 EDF 文件,包含脑电(C3-M2、C4-M1)、眼电(EOG-L、EOG-R)和下颌肌电(EMG)通道。
% 第一步:读取文件 data = edfread('sleep01.edf'); % 第二步:查看通道列表 disp(data.Properties.VariableNames) % 第三步:确认采样率和总时长 fs = round(1 / seconds(data.Time(2) - data.Time(1))); total_seconds = seconds(data.Time(end) - data.Time(1)); fprintf('采样率: %d Hz, 总时长: %.1f 分钟\n', fs, total_seconds/60);读完之后我会用第三小节提到的方法快速检查每个通道的信号质量,把标准差异常的通道标记出来,决定后续分析是剔除还是插值补全。
5.2 滤波和重采样
睡眠脑电分析通常只需要关注 0.5~35 Hz 频段内的信号,所以带通滤波是必须的。我用 Butterworth 零相位滤波,零相位特性对保持睡眠纺锤波和 K 复合波的波形形态非常重要。
% 对脑电通道做0.5~35Hz带通滤波 [b, a] = butter(2, [0.5 35]/(fs/2), 'bandpass'); eeg_filtered = filtfilt(b, a, data.C3_M2); % 如果原始采样率过高,降到256Hz target_fs = 256; if fs > target_fs eeg_resampled = resample(eeg_filtered, target_fs, fs); end滤波时的坑在于边界效应。直接用 filter 会产生边界瞬态,影响首尾几个数据点。用 filtfilt 做零相位滤波虽然计算量稍大,但能保持相位响应平整,这是睡眠分析里不能妥协的地方。另外,EMG 通道和 EOG 通道的处理参数和脑电不同,EMG 往往需要观察高频成分,滤波频段要单独设定。
5.3 按标准窗长切分并导出
睡眠分期标准要求把整夜记录分成长度为 30 秒的 epoch,逐段标注睡眠分期。滤波、重采样完成后,就可以按窗长从连续信号里切分出一个个 epoch。
% 按30秒窗口切分 window_len = target_fs * 30; n_epochs = floor(length(eeg_resampled) / window_len); epochs = reshape(eeg_resampled(1:n_epochs*window_len), window_len, n_epochs)'; % 导出为MAT文件,便于后续分类或打标 save('sleep01_epochs.mat', 'epochs', 'target_fs', 'n_epochs');切分完不要急着跑模型,先把每个 epoch 的功率谱快速画出来看一遍,或者通过计算不同频段比例做个粗略统计。这样能发现滤波参数是否合理,也能在人工标注之前对数据分布有整体感知。
在整个读取和处理链路里,我个人最大的体会是:EDF 读取本身并不难,难的是数据的不可控性。每个设备的导出实现都可能有细微差别,你永远预料不到下一个文件会给你什么惊喜。把读取、检查、转换这一套流程固定成标准脚本,每次拿到新数据先跑一遍质量报告,再进入具体分析,才是真正省时间的方式。
最后再分享一个我自己的使用习惯:所有处理脚本的开头都会读取并保存一份文件信息摘要,包含源文件名、读取时间、通道列表、采样率、总时长、滤波参数等,后面每次分析都从这份摘要开始。这样既方便追溯数据处理链路,也方便同一批数据多人协作时对齐处理标准。EDF 文件沟通的是跨平台、跨设备的数据交换,数据读取只是起步,建立起规范的预处理流程,后面的分析工作才能稳得住。
本文还有配套的精品资源,点击获取