news 2026/10/19 8:19:30

Matlab读取EDF文件:从二进制解析到脑电信号处理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matlab读取EDF文件:从二进制解析到脑电信号处理实战

简介:这份资源面向需要在Matlab环境中处理生物医学信号的科研人员、工程师与学生,聚焦EDF(European Data Format)文件的读取与解析。EDF是心电、脑电等生理信号记录的通用格式,Matlab本身没有内置读取函数,资源围绕这一痛点提供了可落地的解决思路。压缩包共2个文件,包含1个m脚本与1个txt说明文档,整体约3KB,其中m文件为可直接调用的读取函数,txt则给出导入命令与使用提示,便于快速上手。资源从EDF文件头结构讲起,涉及通道数量、采样率、患者信息等元数据的解析,并演示如何借助edfread函数或自定义文件I/O代码读取多通道数据,为后续滤波、特征提取、R波检测、功率谱分析等处理打下基础。目前已有2399人学习下载,适合希望掌握Matlab读取EDF文件完整流程、理解文件结构并具备一定Matlab基础的读者参考。

1. 从一堆脑电数据里把信号捞出来:Matlab 读取 EDF 文件到底能解决什么

如果你拿到过医院或实验室导出的脑电、心电、睡眠监测数据,大概率会看到一堆后缀为.edf的文件。双击打不开,拖进 Excel 是乱码,用文本编辑器打开全是二进制。这不是文件坏了,而是 EDF(European Data Format)本身就是一种为生理信号设计的二进制容器格式,它把多通道时序信号、采样率、单位、患者信息全部打包在一个文件里,普通工具根本读不懂。

这个项目要解决的就是这件事:用 Matlab 把 EDF 文件里的信号完整读出来,变成可以直接画图、滤波、做频谱分析的数值矩阵。适合两类人:一类是手里有生理信号数据、需要快速做算法验证的开发者;另一类是刚接触生物医学信号处理、想找一个能跑通的最小示例的新手。它不涉及复杂的工具箱依赖,核心就是解析文件头、按通道读取数据、还原物理量纲这三步。

2. EDF 文件结构拆解:为什么不能直接 fread 一把梭

2.1 文件头的固定字段与可变字段

EDF 文件的结构其实很朴素,分两大块:固定长度的文件头(256 字节)和每个信号通道各自的头部信息,然后是连续排列的数据记录。固定头里存的是版本号、患者信息、记录开始时间、记录条数、每条记录的持续时间、信号通道数量。这些字段长度是固定的,但内容格式有讲究,比如患者信息字段里用空格填充,日期格式是dd.mm.yy,时间格式是hh.mm.ss。

很多新手第一次读 EDF 会翻车,就是因为直接按字符数去切,结果遇到字段里有空格或者数字位数不固定就错位了。常见做法是先把整个 256 字节读进来,再按 EDF 规范里定义的偏移量逐段截取,每段读完做一次strtrim去掉尾部空格。

fid = fopen('sample.edf', 'r', 'ieee-le'); header = fread(fid, 256, '*char')'; version = strtrim(header(1:8)); patient_info = strtrim(header(9:88)); record_start_date = strtrim(header(169:176)); record_start_time = strtrim(header(177:184)); num_records = str2double(strtrim(header(237:244))); num_signals = str2double(strtrim(header(253:256)));

这段代码里ieee-le是小端模式,EDF 规范要求用小端存储,但有些设备导出时会用大端,读出来全是乱码,这时候换成ieee-be再试一次。num_signals决定了后面每个通道的头部信息要读多少组,这个值直接影响到后续循环的次数。

2.2 每个通道的头部信息怎么对齐

固定头之后,是num_signals个通道的头部信息,每个通道的头部又分成了 8 个字段,每个字段的长度等于num_signals乘以对应的字节数。比如通道标签字段,总长度是num_signals * 16,第 i 个通道的标签就是从(i-1)*16+1到i*16这个区间。

这里有个容易踩的坑:很多人以为每个通道的头部是连续排列的,其实不是。EDF 把所有通道的同一个字段放在一起,读完所有通道的标签,再读所有通道的物理量纲,再读所有通道的物理最小值、最大值、数字最小值、最大值、预滤波信息、采样点数。这个排列方式叫“按字段分组”,不是“按通道分组”。

signal_labels = cell(1, num_signals); physical_dims = cell(1, num_signals); physical_min = zeros(1, num_signals); physical_max = zeros(1, num_signals); digital_min = zeros(1, num_signals); digital_max = zeros(1, num_signals); samples_per_record = zeros(1, num_signals); offset = 256; for i = 1:num_signals signal_labels{i} = strtrim(header(offset + (i-1)*16 + 1 : offset + i*16)); end offset = offset + num_signals * 16; for i = 1:num_signals physical_dims{i} = strtrim(header(offset + (i-1)*8 + 1 : offset + i*8)); end offset = offset + num_signals * 8; for i = 1:num_signals physical_min(i) = str2double(header(offset + (i-1)*8 + 1 : offset + i*8)); end offset = offset + num_signals * 8;

这段代码的逻辑是按字段逐段推进偏移量,每读完一个字段,offset就往后移对应的总字节数。samples_per_record这个数组特别关键,它决定了每个通道在一条记录里有多少个采样点,后面读取数据时就是按这个数来分配矩阵的列数。

2.3 数据区的读取与物理量纲还原

数据区是从第 256 字节加上所有通道头部长度之后开始的。每条记录里,所有通道的数据依次排列,每个采样点用 2 字节的有符号整数存储。读取的时候要按通道顺序把数据切出来,然后做一次线性变换,把数字量转换成物理量。

转换公式是:physical_value = (digital_value - digital_min) * (physical_max - physical_min) / (digital_max - digital_min) + physical_min。这个公式看着简单,但digital_min和digital_max如果读错了,还原出来的信号要么幅值不对,要么直接溢出。

data_offset = offset; total_samples_per_record = sum(samples_per_record); signal_data = cell(1, num_signals); for rec = 1:num_records record_data = fread(fid, total_samples_per_record, 'int16'); idx = 1; for ch = 1:num_signals n = samples_per_record(ch); raw = record_data(idx : idx + n - 1); idx = idx + n; physical = (raw - digital_min(ch)) * ... (physical_max(ch) - physical_min(ch)) / ... (digital_max(ch) - digital_min(ch)) + physical_min(ch); signal_data{ch} = [signal_data{ch}; physical]; end end fclose(fid);

这里用cell数组存每个通道的完整信号,是因为不同通道的采样率可能不一样,没法直接拼成一个矩阵。fread读的是int16,对应 EDF 规范里的 2 字节有符号整数。如果文件里某个通道的digital_max和digital_min相等,除法会出Inf,这种情况一般是文件头损坏,需要在读之前加一个判断。

3. 从零跑通:一个能直接用的 EDF 读取脚本

3.1 环境准备与文件放置

这个脚本不需要任何额外的工具箱,Matlab 基础版本就能跑。把要读的.edf文件和脚本放在同一个目录下,或者把文件路径写绝对路径。如果你用的是 Octave,大部分语法也兼容,但strtrim和fread的行为基本一致,问题不大。

常见做法是建一个data文件夹专门放 EDF 文件,脚本里用fullfile拼路径,这样换电脑跑的时候只需要改一个根目录变量。

data_dir = fullfile(pwd, 'data'); edf_file = fullfile(data_dir, 'sample.edf'); if ~isfile(edf_file) error('EDF 文件不存在,请检查路径:%s', edf_file); end

3.2 完整读取流程与信号可视化

把前面几段代码串起来,就是一个完整的读取流程。读完之后,用plot把每个通道的信号画出来,检查一下有没有明显的漂移或者饱和。生理信号经常有基线漂移,画图的时候可以顺手做一个去均值。

fs = samples_per_record ./ (num_records * record_duration); t = (0 : size(signal_data{1}, 1) - 1) / fs(1); figure; for ch = 1:num_signals subplot(num_signals, 1, ch); plot(t, signal_data{ch} - mean(signal_data{ch})); title(signal_labels{ch}); xlabel('时间 (s)'); ylabel(physical_dims{ch}); end

record_duration是从固定头里读出来的,一般是 1 秒。fs算出来是每个通道的采样率,不同通道可能不同,所以画图时横轴时间要按各自采样率算。如果所有通道采样率一样,可以直接用一个时间向量。

3.3 参数怎么改:采样率、通道选择与时间段截取

实际用的时候,往往不需要读全部通道和全部时间。比如只关心 Fz、Cz、Pz 三个电极,或者只看第 10 分钟到第 20 分钟的数据。这时候可以在读取前先筛选通道索引,读取时只保留需要的记录段。

target_channels = [1, 3, 5]; target_start_sec = 600; target_end_sec = 1200; start_rec = floor(target_start_sec / record_duration) + 1; end_rec = min(floor(target_end_sec / record_duration), num_records); for rec = start_rec:end_rec record_data = fread(fid, total_samples_per_record, 'int16'); idx = 1; for ch = 1:num_signals n = samples_per_record(ch); raw = record_data(idx : idx + n - 1); idx = idx + n; if ismember(ch, target_channels) physical = (raw - digital_min(ch)) * ... (physical_max(ch) - physical_min(ch)) / ... (digital_max(ch) - digital_min(ch)) + physical_min(ch); signal_data{ch} = [signal_data{ch}; physical]; end end end

这里用ismember判断当前通道是否在目标列表里,不在就跳过转换,省计算量。start_rec和end_rec把时间换算成记录索引,注意索引从 1 开始,所以要加 1。

4. 避坑与排查:EDF 读取时最容易翻车的几个地方

4.1 读出来全是零或者乱码

现象:信号画出来是一条直线,或者数值大得离谱。原因通常是字节序搞反了,或者digital_min/digital_max读成了字符串没转数字。解决方法是先检查fread的精度参数,小端用ieee-le,大端用ieee-be。然后确认所有str2double的字段都转换成功,没有变成NaN。

4.2 通道数对不上或者标签错位

现象:读出来的通道标签和实际电极对不上,或者通道数比预期少。原因多半是固定头里的num_signals读错了,或者偏移量计算时漏掉了某个字段。解决方法是把num_signals打印出来,和文件属性里的通道数对比。如果不对,检查header(253:256)这一段是不是被空格填充了。

4.3 数据长度和预期不一致

现象:读出来的信号长度不是num_records * samples_per_record。原因可能是文件在写入时没有写满最后一条记录,或者读取时fread提前遇到了文件末尾。解决方法是先算一下文件总大小,减去头部长度,再除以 2,看能容纳多少个采样点。如果对不上,说明文件本身不完整。

4.4 物理量纲还原后幅值不对

现象:信号形状对,但幅值差了一个数量级。原因通常是physical_min和physical_max的单位和digital_min/digital_max不匹配。比如物理量纲是微伏,但数字量是毫伏。解决方法是把physical_dims打印出来,确认单位,然后检查转换公式里的分子分母有没有写反。

4.5 大文件读取内存爆掉

现象:读一个几百兆的 EDF 文件,Matlab 直接卡死或者报内存不足。原因是把所有记录一次性读进内存,而且用cell数组不断拼接。解决方法是预分配矩阵,或者分块读取,每次只读一部分记录,处理完再读下一块。如果只是做频谱分析,可以边读边做 FFT,不用保存完整信号。

5. 二次开发与改造:把读取脚本变成自己的信号处理流水线

5.1 封装成函数并支持批量处理

把读取逻辑封装成一个函数,输入是文件路径和可选参数,输出是信号矩阵、采样率、通道标签。这样以后换文件只需要改一个参数。批量处理的时候,用dir列出所有.edf文件,循环调用。

function [signals, fs, labels, dims] = read_edf(filepath, varargin) p = inputParser; addParameter(p, 'Channels', []); addParameter(p, 'StartSec', 0); addParameter(p, 'EndSec', Inf); parse(p, varargin{:}); % ... 读取逻辑 ... if ~isempty(p.Results.Channels) signals = signals(p.Results.Channels); labels = labels(p.Results.Channels); dims = dims(p.Results.Channels); fs = fs(p.Results.Channels); end end

inputParser是 Matlab 里做参数解析的标准做法,支持默认值和名称-值对。Channels为空时读全部通道,指定时只读目标通道。StartSec和EndSec控制读取的时间范围。

5.2 接入滤波与频谱分析

读出来的信号通常需要做带通滤波,比如脑电常用 0.5 到 45 Hz。Matlab 的bandpass函数可以直接用,但要注意采样率要匹配。滤波之后做pwelch算功率谱,看看各个频段的能量分布。

[b, a] = butter(4, [0.5, 45] / (fs(1) / 2), 'bandpass'); filtered = filtfilt(b, a, signal_data{1}); [pxx, f] = pwelch(filtered, [], [], [], fs(1)); plot(f, 10*log10(pxx)); xlabel('频率 (Hz)'); ylabel('功率谱密度 (dB/Hz)');

butter生成滤波器系数,filtfilt做零相位滤波,避免相位失真。pwelch的第二个参数是窗函数,空着用默认的汉明窗。如果信号很长,可以分段做pwelch再平均,结果更平滑。

5.3 一个我踩过的坑:别在循环里动态扩容

刚开始写的时候,我在记录循环里用signal_data{ch} = [signal_data{ch}; physical]不断拼接,读一个 10 分钟的文件要好几秒。后来改成先算总长度,预分配矩阵,再按索引填充,速度直接快了一个数量级。从那以后我每次读大文件都强制走一遍预分配,哪怕代码看起来啰嗦一点。

total_samples = num_records * samples_per_record(ch); signal_data{ch} = zeros(total_samples, 1); pos = 1; for rec = 1:num_records % ... 读取 ... signal_data{ch}(pos : pos + n - 1) = physical; pos = pos + n; end

预分配之后,内存不会反复申请和释放,Matlab 的 JIT 也能更好地优化循环。如果通道数多,可以只预分配需要的通道,不需要的通道直接跳过读取。

5.4 验证读取结果是否正确的几个方法

读完之后怎么确认没读错?我一般做三件事:第一,检查信号长度是否等于num_records * samples_per_record;第二,画一段原始信号,看有没有明显的削顶或者死区;第三,用已知的校准信号(比如方波)验证幅值还原是否正确。如果这三步都过了,基本可以放心往下做分析。

希望这些能帮到你,少走一点我当年走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 8:19:20

如何搭建并发布个人技术博客:从Hexo到Vercel的完整指南

说实话,现在回头看我自己写的“第一篇博客”,排版凌乱、思路跳跃、连代码块高亮都没配,但就是那篇粗糙得不行的小文章,让我把“写博客”这个念头真正变成了习惯,也让我接下来一年的学习路径清晰了很多。很多人卡在第一…

作者头像 李华
网站建设 2026/10/19 8:17:27

Context Pruning实战:语义相似度驱动的上下文精简与Token成本优化

如果你在一个知识库问答系统上跑过几轮真实对话,大概会懂那种憋屈感:用户问了一个看似简单的问题,你的程序却把过去几轮里塞进来的旧文档、无关日志、重复列表一股脑全带上了。结果Prompt越长,模型回答越慢,Token账单越…

作者头像 李华
网站建设 2026/10/19 8:17:27

Kafka消费组Rebalance:积压、重复、丢失的共同根源与治理

1. 半夜的连环告警:积压、重复、丢数据,全指向了同一个“惯犯”半夜两点被电话叫醒,线上 Kafka 消费延迟从几百条飙到几十万条,重启消费者之后更离谱——同一个订单被处理了三次,还有几个订单在日志里彻底没了下文。第…

作者头像 李华
网站建设 2026/10/19 8:16:58

LangChain 1.0架构瘦身:从全家桶到按需取用,迁移实操指南

LangChain 1.0 发布的消息一出来,做 AI 应用的朋友们分成了两派:一派兴奋地表示终于等到了 API 稳定,另一派则吐槽那些年被架构折腾掉的头发。作为从 0.8 时代一路用过来的开发者,我第一反应是赶紧把线上一个基于 LangChain 的文档…

作者头像 李华
网站建设 2026/10/19 8:16:27

大彩串口屏与STM32F103 UART通信实战:协议解析、帧校验与状态机设计

简介:本资源是一套面向嵌入式初学者与STM32开发者的「大彩串口屏STM32F103」通信实战模板,聚焦解决串口屏快速接入、协议适配与显示控制等典型工程问题,特别适配野火/正点原子系开发板用户。压缩包共262个文件,含36个C源码、38个头…

作者头像 李华
网站建设 2026/10/19 8:16:25

OPC DA转MQTT实战:搞定DCOM权限与数据桥接

做了十几年工业数据集成,这类需求几乎成了我的家常便饭:车间里那套十多年前上线的上位机系统还在用 OPC DA 老协议,集团层面却要求数据尽快接入统一的工业物联网平台,而平台那边只认 MQTT。设备不能停,旧系统不能拆&am…

作者头像 李华