简介这份资源面向需要在Matlab环境中处理生物医学信号的科研人员、工程师与学生聚焦EDF欧洲数据格式文件的读取与解析。EDF常用于心电图、脑电图等生理信号记录而Matlab并无内置读取函数资源正是为解决这一痛点而整理。压缩包共2个文件包含1个m脚本与1个txt说明文档整体约3KB其中m文件提供可直接调用的读取实现txt则给出导入命令与使用提示便于快速上手。目前已有2399人学习下载说明该方案在相关领域具有一定参考价值。读者可借此理解EDF文件头结构、通道数据组织方式掌握借助第三方工具箱或自定义代码完成读取的思路并在此基础上开展滤波、特征提取、R波检测、功率谱分析等后续处理适合作为信号处理入门与工程实践的轻量参考。1. 为什么我劝你先别急着写edfreadMatlab 读取 EDF 文件的三层认知如果你手头有一批 EDF 文件第一反应大概率是打开 Matlab敲edfread然后被返回的一堆结构体字段搞懵。EDF 全称 European Data Format是脑电、心电、睡眠监测设备最常用的交换格式结构简单到用文本头就能描述但真正读起来坑全在细节里。Matlab 读取 EDF 这件事表面是调一个函数实际要过三关头信息解析、通道标定、事件对齐。很多人卡在第二关读出来的信号幅值全是错的还以为是设备问题。这篇笔记面向需要批量处理生理信号、做睡眠分期或事件相关电位分析的从业者把 Matlab 读取 EDF 的完整链路拆开从文件结构讲到批量脚本再到踩过的坑争取让你一次跑通。2. EDF 文件结构拆解头信息、数据记录与通道标定2.1 EDF 头信息的固定字段与可变字段EDF 文件分两部分固定长度的 ASCII 头256 字节和每个信号的可变头然后是数据区。固定头里最关键的是这几个字段版本号8 字节、患者信息80 字节、记录信息80 字节、开始日期8 字节、开始时间8 字节、头字节数8 字节、保留字段44 字节、记录数8 字节、每记录时长8 字节、信号数4 字节。这些字段都是定长的用fread按字节读出来再转字符就行。可变头是每个信号一组按信号顺序排列。每个信号有标签16 字节、 transducer 类型80 字节、物理维度8 字节、物理最小值8 字节、物理最大值8 字节、数字最小值8 字节、数字最大值8 字节、预滤波80 字节、每记录采样数8 字节、保留字段32 字节。注意物理最小/最大值和数字最小/最大值决定了后面标定公式的系数。我一般会先写一个parse_edf_header函数把这两部分读出来存成结构体。下面这段代码是核心逻辑用fread配合char转换注意字节偏移要算准。function hdr parse_edf_header(filename) fid fopen(filename, r, ieee-le); if fid -1 error(无法打开文件: %s, filename); end % 固定头 256 字节 hdr.version strtrim(char(fread(fid, 8, char))); hdr.patient strtrim(char(fread(fid, 80, char))); hdr.record_info strtrim(char(fread(fid, 80, char))); hdr.start_date strtrim(char(fread(fid, 8, char))); hdr.start_time strtrim(char(fread(fid, 8, char))); hdr.header_bytes str2double(char(fread(fid, 8, char))); fread(fid, 44, char); % 保留字段 hdr.num_records str2double(char(fread(fid, 8, char))); hdr.record_duration str2double(char(fread(fid, 8, char))); hdr.num_signals str2double(char(fread(fid, 4, char))); % 可变头每个信号一组按字段顺序读 for i 1:hdr.num_signals hdr.labels{i} strtrim(char(fread(fid, 16, char))); hdr.transducer{i} strtrim(char(fread(fid, 80, char))); hdr.phys_dim{i} strtrim(char(fread(fid, 8, char))); hdr.phys_min(i) str2double(char(fread(fid, 8, char))); hdr.phys_max(i) str2double(char(fread(fid, 8, char))); hdr.dig_min(i) str2double(char(fread(fid, 8, char))); hdr.dig_max(i) str2double(char(fread(fid, 8, char))); hdr.prefilter{i} strtrim(char(fread(fid, 80, char))); hdr.samples_per_record(i) str2double(char(fread(fid, 8, char))); fread(fid, 32, char); % 每个信号的保留字段 end fclose(fid); end这段代码里fread的第三个参数char表示按字符读strtrim去掉尾部空格。注意header_bytes这个字段它告诉你数据区从第几个字节开始后面读数据时要fseek到这个位置。num_records和record_duration决定了总时长samples_per_record是每个信号在每个记录里的采样点数。这些参数后面标定和分帧都要用。2.2 数据记录与通道标定公式数据区是按记录连续存放的。每个记录里所有信号的采样点依次排列每个采样点是一个 2 字节的整数小端序。读的时候要按信号顺序、按记录循环。标定公式是物理值 (数字值 - 数字最小值) * (物理最大值 - 物理最小值) / (数字最大值 - 数字最小值) 物理最小值这个公式必须用每个信号自己的参数不能混用。我见过有人直接用全局的 min/max结果幅值全错。下面是把数据读成物理值的代码假设你已经有了hdr。function data read_edf_data(filename, hdr) fid fopen(filename, r, ieee-le); fseek(fid, hdr.header_bytes, bof); % 跳到数据区 % 预分配每个信号一个矩阵行是采样点列是记录 for i 1:hdr.num_signals data{i} zeros(hdr.samples_per_record(i), hdr.num_records); end for rec 1:hdr.num_records for i 1:hdr.num_signals n hdr.samples_per_record(i); raw fread(fid, n, int16); % 标定 phys (raw - hdr.dig_min(i)) * (hdr.phys_max(i) - hdr.phys_min(i)) / ... (hdr.dig_max(i) - hdr.dig_min(i)) hdr.phys_min(i); data{i}(:, rec) phys; end end fclose(fid); end这里fread(fid, n, int16)读的是有符号 16 位整数EDF 标准规定数据是 2 字节补码。如果你的文件是 24 位或 32 位需要改int32或自定义读取但绝大多数 EDF 是 16 位。标定后的data是一个 cell 数组每个 cell 对应一个通道行是采样点列是记录。你可以用cell2mat或直接拼接成连续时间序列。注意有些设备导出的 EDF 文件里dig_min和dig_max可能是 0 和 0这时候标定会除零。遇到这种情况直接跳过标定用原始值或者检查文件是否损坏。3. 用 Matlab 内置函数与第三方工具读取 EDF 的选型对比3.1edfread的适用场景与局限Matlab 从 R2015b 开始提供了edfread函数在 Signal Processing Toolbox 里。它的优点是开箱即用返回一个 timetable 或结构体自动处理头信息和标定。但局限也很明显第一它只支持 EDF不支持 EDF 的某些扩展比如注释通道第二它把整个文件读进内存大文件容易爆第三返回的 timetable 时间轴是均匀的如果文件里有事件标记它不会自动对齐。我一般用edfread做快速预览确认文件能读、通道名对得上然后再用自己写的解析函数做批量处理。% 快速预览 [data, header] edfread(sample.edf); disp(header.labels); plot(data.Var1); % 假设第一个通道是 EEG这段代码里data是 timetableheader包含通道信息。edfread会自动把物理值算好你直接画图就行。但如果你要处理几百个文件每个都调edfread内存和速度都是问题。3.2 第三方工具箱edfread与edfread2的差异社区里还有几个第三方实现比如edfread同名但不同作者和edfread2。edfread2支持 EDF 的注释通道能把事件标记读出来。如果你做睡眠分期需要用到事件标记比如呼吸事件、腿动就得用edfread2或者自己解析注释通道。注释通道的标签通常是EDF Annotations数据区里存的是 TALTime-stamped Annotation List格式是onset duration annotation。解析起来比普通信号麻烦但逻辑不复杂。我一般会先判断文件里有没有注释通道遍历hdr.labels找EDF Annotations。如果有就单独读那个通道按 TAL 格式解析。下面是一个简化的解析片段。% 假设 hdr.labels 里找到了注释通道的索引 idx fseek(fid, hdr.header_bytes, bof); % 先跳过前面所有信号的数据定位到注释通道 for rec 1:hdr.num_records for i 1:hdr.num_signals n hdr.samples_per_record(i); if i idx raw fread(fid, n, int16); % 注释通道的数据是字符按字节转 txt char(raw); % 解析 TALonset duration annotation tokens regexp(txt, \(\d\.?\d*)\x15(\d*)\x14([^\x14\x15]*), tokens); for k 1:numel(tokens) onset str2double(tokens{k}{1}); duration str2double(tokens{k}{2}); annotation tokens{k}{3}; fprintf(事件: %s, 起始: %.2f, 时长: %.2f\n, annotation, onset, duration); end else fseek(fid, n * 2, cof); % 跳过其他信号 end end end这里\x15和\x14是 TAL 里的分隔符\x15是 onset 和 duration 的分隔\x14是 annotation 的结束。fseek的cof表示从当前位置偏移。这段代码能读出事件标记但要注意注释通道的采样数可能和其他通道不同samples_per_record要单独处理。3.3 选型建议按文件规模和任务类型决定如果你的文件小于 100 MB通道数少于 32直接用edfread最省事。如果文件大、通道多、需要事件标记或者要批量处理自己写解析函数更可控。我通常的做法是先用edfread读一个样本确认通道名和采样率然后用自定义函数批量跑。这样既快又稳。4. 批量读取与预处理从单文件到数据集4.1 批量遍历文件夹与并行加速实际项目里你面对的是一个文件夹里几百个 EDF 文件。用dir遍历循环调用解析函数。如果文件多可以用parfor并行。但要注意parfor里不能直接写文件每个 worker 要独立打开文件。下面是一个批量读取的框架。files dir(*.edf); num_files numel(files); all_data cell(num_files, 1); all_hdr cell(num_files, 1); parfor i 1:num_files fname files(i).name; hdr parse_edf_header(fname); data read_edf_data(fname, hdr); all_data{i} data; all_hdr{i} hdr; end % 保存结果 save(edf_dataset.mat, all_data, all_hdr, -v7.3);这里parfor会自动分配文件给多个 worker速度取决于 CPU 核数。-v7.3是保存大文件用的支持大于 2 GB 的变量。注意parfor里不能有fprintf之类的输出否则会乱序。4.2 通道筛选与重采样读出来的数据可能有很多通道你只关心其中几个。比如睡眠监测里你只要 EEG、EOG、EMG、ECG。可以在解析后按标签筛选。下面是一个筛选和重采样的例子。% 假设 all_data{1} 是第一个文件的数据all_hdr{1} 是头信息 target_labels {EEG, EOG, EMG, ECG}; idx find(ismember(all_hdr{1}.labels, target_labels)); selected all_data{1}(idx); % 重采样到 256 Hz假设原始采样率是 512 Hz fs_orig all_hdr{1}.samples_per_record(idx(1)) / all_hdr{1}.record_duration; fs_new 256; for k 1:numel(selected) selected{k} resample(selected{k}(:), fs_new, fs_orig); end这里resample是 Signal Processing Toolbox 的函数做抗混叠重采样。fs_orig的计算是每个记录的采样数除以记录时长。注意如果不同通道的采样率不同要分别算。重采样后所有通道的时间轴就统一了方便后续做多通道分析。4.3 时间轴对齐与事件标记合并如果你有事件标记需要把事件的时间戳对齐到重采样后的时间轴。假设事件起始时间是秒重采样后采样率是fs_new那么事件对应的样本索引是round(onset * fs_new) 1。我一般会建一个事件表每行是一个事件列是 onset、duration、type。然后根据 onset 找到对应的样本点在信号上打标签。% 假设 events 是一个结构体数组每个有 onset, duration, type event_table struct2table(events); event_table.sample_idx round(event_table.onset * fs_new) 1; % 筛选出在信号长度范围内的事件 valid event_table.sample_idx 0 event_table.sample_idx length(selected{1}); event_table event_table(valid, :);这样事件和信号就对齐了。后面做 epoch 切分时直接按sample_idx取窗口就行。5. 避坑指南Matlab 读取 EDF 的五个血泪教训5.1 字节序与数据类型读错信号全是噪声现象读出来的信号幅值巨大或者全是零画图像噪声。原因EDF 标准规定数据是小端序little-endian但有些设备导出的是大端序或者用了int32而不是int16。解决先用fread读几个字节用typecast试一下确认字节序。Matlab 的fopen里指定ieee-le或ieee-be。如果不确定用edfread读一个样本对比。5.2 标定公式用错幅值单位不对现象读出来的信号幅值在 0 到 1 之间但实际应该是微伏级。原因标定公式里用了错误的dig_min或phys_min或者把不同通道的参数混用。解决每个通道单独标定检查hdr.phys_dim是否是uV或mV。如果dig_min和dig_max相等说明文件有问题直接跳过标定。5.3 注释通道解析失败事件丢失现象用edfread读出来的 timetable 里没有事件标记。原因edfread不解析注释通道或者注释通道的标签不是EDF Annotations。解决自己遍历hdr.labels找到注释通道按 TAL 格式解析。注意 TAL 里的分隔符是\x15和\x14不是普通逗号。5.4 大文件内存溢出程序崩溃现象读一个 500 MB 的 EDF 文件Matlab 报Out of memory。原因edfread把整个文件读进内存或者你的预分配矩阵太大。解决用memmapfile做内存映射或者分块读取。我一般会先算一下文件大小和总采样点数如果超过可用内存的一半就分块处理每块存成单独的文件。5.5 时间轴不均匀事件对齐错位现象事件标记的时间戳和信号对不上差几秒。原因EDF 文件的记录时长可能不是整数秒或者record_duration有小数。解决用hdr.record_duration精确计算每个记录的起始时间而不是假设每个记录 1 秒。如果record_duration是 0.5 秒那么第 n 个记录的起始时间是(n-1) * 0.5秒。6. 进阶技巧用memmapfile做内存映射与增量读取当你需要处理几十 GB 的 EDF 文件时一次性读进内存不现实。Matlab 的memmapfile可以把文件映射到内存按需读取。下面是一个用memmapfile读取 EDF 数据区的例子。% 假设 hdr 已经解析好header_bytes 是数据区起始字节 % 每个采样点 2 字节总采样点数 sum(samples_per_record) * num_records total_samples sum(hdr.samples_per_record) * hdr.num_records; m memmapfile(filename, ... Format, {int16, [total_samples, 1], raw}, ... Offset, hdr.header_bytes); % 读取第 1 个记录的第 1 个通道 n1 hdr.samples_per_record(1); ch1_rec1 m.Data.raw(1:n1); % 标定 phys (ch1_rec1 - hdr.dig_min(1)) * (hdr.phys_max(1) - hdr.phys_min(1)) / ... (hdr.dig_max(1) - hdr.dig_min(1)) hdr.phys_min(1);这里memmapfile把整个数据区映射成一个int16数组Offset指定从header_bytes开始。m.Data.raw就是原始数据你可以按索引取任意片段。这样内存占用极小只在你访问时才读磁盘。注意memmapfile的Format里[total_samples, 1]表示一列如果数据是交错排列的你需要自己算索引。增量读取的思路是先算每个记录的总字节数然后循环fseek和fread每次只读一个记录处理完就丢弃。这样内存里始终只有一个记录的数据。我一般会写一个read_edf_chunk函数接受起始记录和结束记录返回对应片段。function chunk read_edf_chunk(filename, hdr, start_rec, end_rec) fid fopen(filename, r, ieee-le); fseek(fid, hdr.header_bytes, bof); % 跳过 start_rec 之前的记录 bytes_per_rec sum(hdr.samples_per_record) * 2; fseek(fid, (start_rec - 1) * bytes_per_rec, cof); num_recs end_rec - start_rec 1; chunk cell(hdr.num_signals, 1); for i 1:hdr.num_signals chunk{i} zeros(hdr.samples_per_record(i), num_recs); end for rec 1:num_recs for i 1:hdr.num_signals n hdr.samples_per_record(i); raw fread(fid, n, int16); phys (raw - hdr.dig_min(i)) * (hdr.phys_max(i) - hdr.phys_min(i)) / ... (hdr.dig_max(i) - hdr.dig_min(i)) hdr.phys_min(i); chunk{i}(:, rec) phys; end end fclose(fid); end这个函数可以按记录块读取适合做流式处理。比如你要做实时睡眠分期可以每 30 秒读一个记录提取特征然后丢弃。这样内存永远不涨。验证读取是否正确我一般会做三件事第一用edfread读同一个文件对比前 1000 个采样点的数值误差应该在浮点精度内第二画一段 10 秒的信号看波形是否合理有没有削顶或基线漂移第三检查事件标记的时间戳是否落在信号范围内。这三步走完基本不会翻车。从那以后我每次读 EDF 都强制走一遍「头信息校验 → 标定对比 → 事件对齐」的流程再也没出现过幅值错误。希望帮到你。本文还有配套的精品资源点击获取