
简介这套MATLAB实现专门针对脑电EEGEDF格式文件的读取与初步恢复面向神经科学、医学诊断和信号处理方向的科研人员、学生以及需要分析癫痫等脑电数据的技术开发者。包体极其精简仅含3个.m脚本总大小约4KB以EDF读取函数为主配套一个调用入口和一个信号轨迹恢复工具能够完成多通道EDF数据的导入、通道信息解析与波形重建为后续滤波、去噪、功率谱分析或机器学习分类提供标准化的数据输入。已有1292人学习下载是目前快速上手EDF读取的轻量级参考。读者可借此理解EDF文件的通道组织方式掌握MATLAB中读取采样率、信号值和时间戳的基本方法并结合轨迹恢复功能直观检查数据质量节省从零造轮子的时间适合用于小规模实验验证或作为课程设计的基础代码。1. EEG.rar里躺着一堆EDF脑电文件这套数据怎么读看这一篇就够了做癫痫检测和睡眠分期的人大概率都下载过类似 EEG.rar 这种明晃晃写着“癫痫数据”的压缩包。解开之后里面的文件名通常是 patient_01.edf、seizure_02.edf后缀一样大小从几十兆到几个G不等。EDF不是某厂商的私有格式而是欧洲临床脑电的标准交换格式全称 European Data Format它把多通道EEG信号、采样率、量程、发作标注全封装在一个文件里。这篇文章主要讲清楚三件事EDF文件里的信息是怎么排布的、用Python怎么正确读取、以及把发作标注变成训练标签时那些容易翻车的细节。适合要做癫痫自动检测、脑电分类或脑机接口预处理的人读完能直接照着自己解包、读取、切片和存数据。2. EDF与EDF格式拆解读懂头文件就掌握了读取的主动权很多人第一次读EDF文件时习惯直接用库一把梭读出数组就去算特征这其实是在赌运气。EDF的信号区本质上是一串16位小端整数没有头文件里的量程、采样率和通道标签这堆字节就是黑匣子。我一般会先把头文件完整拉出来看一遍确认字段值合理再继续。EEG临床数据里尤其来自不同医院的记录头文件写法差异很大最常见的坑就是物理单位写成V、uV、mV不一致导致后面幅度全偏。所以这一章先把格式本身拆开。2.1 EDF头文件的关键字段为什么必须先读头文件标准的EDF文件头部固定是256字节的ASCII文本从第256字节之后才是信号数据区。数据区不是直接可读的波形数组而是“按通道顺序排列的16位采样点”比如8个通道每个数据记录里每通道采样100点那么数据记录里就是100点通道1、100点通道2……依次排下去。要正确解释这段字节流必须依赖头文件里的通道数目、每通道采样点数、数据记录时长、物理量程与数字量程这几个字段。偏移(字节)字段含义常见示例0-7版本号固定ASCII字符串0 8-51患者标识患者编码与性别年龄P001 M 3452-131记录标识记录时间、医院信息SEIZURE_V1132-139开始日期格式dd.mm.yy01.03.24140-147开始时间格式hh.mm.ss14.23.05148-155头文件字节数256通道数*2562304156-199保留区通常空空格200-207数据记录总数决定文件时长1200208-215每个记录时长(秒)采样时间基准1216-219信号通道数含EEG和ECG等8220起通道描述块每通道256字节见下每个通道的256字节描述块里依次是16字节通道标签如 EEG Fpz-Cz、80字节换能器类型、8字节物理单位uV、8字节物理最小值、8字节物理最大值、8字节数字最小值、8字节数字最大值、80字节滤波信息、8字节每记录采样点数和32字节保留区。这里最有价值的是物理量程和数字量程它们直接决定了原始采样值换算成微伏时的系数。读任何EDF文件我习惯先打出一个通道的这组数量程异常的文件后面处理时常会出幺蛾子。2.2 数据记录与标定公式16位整数如何变成微伏EDF存储的原始采样值是有符号16位整数范围通常在 -32768 到 32767 之间但真正对应到脑电信号的范围由头文件里的物理量程决定。比如某个参考电极通道的物理最小值为 -1000、物理最大值为 1000单位是uV数字最小值为 -32768、数字最大值为 32767那么读到数字值 DV 时换算物理值 PV 的标准线性映射是PV PMin (DV - DMin) * (PMax - PMin) / (DMax - DMin)举个例子DV0 时PV -1000 (0 - (-32768)) * 2000 / 65535约等于 -0.03uV接近0。如果某个解析库偷懒不做标定直接把int16数组返回给你那数据看起来就离谱幅度几千几万“uV”后面做特征全废。正规的EDF读取库都会自动做这个标定但如果你是自己用C或MATLAB解析这条公式就是核心。此外还有一个容易忽略的点EDF的数据记录时长不一定是1秒常见的有0.5秒、2秒甚至5秒。采样率是通过“每记录采样点数 / 记录时长”算出来的不是头文件里直接写的。所以读取时不要自以为通道一定采样250Hz先算再决定后续滤波的截止频率。2.3 EDF的注解通道癫痫发作标注藏在这里传统EDF没有注解能力纯信号加头文件。EDF在此基础上增加了一个特殊通道标签固定为 EDF Annotations它的数据区不是采样点而是一次次时间戳文本格式叫TALTime-stamped Annotation List。每个TAL包含起始时间相对于记录开始、持续时间、注解字符串比如“seizure start 14:23:05”最后以控制字符结束。癫痫数据集的发作起止区间就靠这个通道标注。另有一种情况标注不在EDF里而在同目录的CSV或Excel里这在去中心化的公开数据分享里也很常见。遇到时先把外部标注读出来再和EDF的时间轴对齐别急着删文件。理解了这一层后面用MNE和pyedflib时就不会奇怪“为什么别人读得到s刺激事件我这全是空白”。3. 用Python读取EDF文件pyedflib与MNE的最小可跑通命令这一章直接上手。环境建议用Python 3.9以上先装两个库pyedflib用于底层解析mne用于信号浏览和标注提取。pyedflib轻量、读取速度快、返回的是普通NumPy数组MNE功能强自带滤波、可视化和事件提取但内存开销大。两个库对EDF的读取逻辑都是先解析头文件再做标定差异主要在API和返回结构上。我通常两个都装排查问题时互相印证。3.1 拿到EEG.rar后先做一步测试压缩包完整性解压前先测试压缩包是否完整这步很多人跳过直到读到数据中途报错才回头找原因。尤其EEG.rar这种大文件下载过程中断概率不低分卷压缩的还要确认所有分卷都齐了。# 用7-Zip测试压缩包完整性t参数只测试不解压 7z t EEG.rar # 如果有多卷测试第一个分卷即可工具会自动检查后续分卷 7z t EEG.rar.001 # 完整性通过后再解压到指定目录 7z x EEG.rar -o./EEG_dataset这里7z t会逐个校验压缩包内文件的CRC32输出“全部正常”或“有错误”的结论。如果CRC报错别急着用各种恢复工具去“救”先在原始下载地址重新拉一遍医学数据缺几兆尾部头文件里记录总数不变但实际采样点少了后面算法的统计特征都会受影响。解压完成后还要留意目录里是否混有exe、scr这类可执行文件来源不明的压缩包里偶尔会夹带广告程序只保留edf和标注文件就够了。3.2 用pyedflib拉出头信息和第一通道信号pyedflib的API很直白先建EdfReader对象再通过几个get方法拿元数据最后readSignal取通道数组。import pyedflib import numpy as np # 打开EDF文件路径换成你实际解压出来的文件 edf pyedflib.EdfReader(./EEG_dataset/patient_01.edf) # 头文件核心信息 print(通道数:, edf.signals_in_file) print(通道标签:, edf.getSignalLabels()) print(采样率:, edf.getSampleFrequencies()) print(物理单位:, edf.getPhysicalDimensions()) print(每通道采样点数:, edf.getNSamples()) # 读第0通道完整信号返回浮点数组单位是标定后的物理单位(通常是uV) signal_uV edf.readSignal(0) print(第一通道长度:, signal_uV.size) print(幅度范围:, signal_uV.min(), signal_uV.max()) # 一定要记得关闭文件 edf.close()getSampleFrequencies()返回的是每个通道的采样率数组不是单个数值。多通道EDF里参考通道和EEG通道的采样率可能本来就不同这点要注意。readSignal(通道索引)默认从头读到尾返回的是经过标定的浮点值如果只需要某个区间可以用readSignal(0, start100, stop200)参数是采样点索引不是秒。对于几个G的大文件不要一次性把所有通道都读进内存按需读通道能省不少内存。3.3 用MNE读取EDF并可视化顺手看到发作标注MNE读取EDF会自动把注解通道转换成Annotations对象这对癫痫数据特别友好。import mne # preloadTrue会把全部数据载入内存文件过大时改成preloadFalse raw mne.io.read_raw_edf(./EEG_dataset/patient_01.edf, preloadTrue) print(raw.info[ch_names]) # 通道列表 print(采样率:, raw.info[sfreq]) # MNE取所有通道的公共采样率 print(时间长度(秒):, raw.times[-1]) # 总时长 # 查看注解EDF里的发作标注会出现在这里 annot raw.annotations print(annot.onset) # 发作起始时间单位秒相对文件起点 print(annot.duration) # 持续时间单位秒 print(annot.description) # 标注文本如“seizure” # 画前8个通道20秒波形 raw.plot(n_channels8, duration20, scalingsauto, blockTrue)这里有个细节MNE的sfreq是全通道取公倍数或最大值如果原始EDF里各通道采样率不一致MNE会内部重采样对齐。遇到重采样提示时要注意这会让原始数据发生改变。看波形时如果发现某个通道幅度特别大或完全是直线大概率是坏道或参考电极问题先在记录里记下来不要当场删通道。3.4 把全部通道导出为NumPy数组给后续训练一个干净入口如果你只是要数据做分类模型不依赖MNE的后续信号处理可以直接把全部通道拼成数组再存成npz。这一步的关键是保持通道顺序和MNE一致。import numpy as np data raw.get_data() # 返回形状 (n_channels, n_samples) print(数据形状:, data.shape) # 转成 [样本数, 通道数] 便于后续模型输入 data_t data.T print(转置后形状:, data_t.shape) # 保存为npz标注信息也一并存放 ann_onset raw.annotations.onset ann_desc raw.annotations.description np.savez(./EEG_dataset/patient_01_processed.npz, datadata_t, sfreqraw.info[sfreq], ann_onsetann_onset, ann_descann_desc)raw.get_data()返回的通道顺序与raw.info[ch_names]一致理论上直接拼数组没问题。但有一个隐含风险如果某个通道类型是ECG或EOG它也会原样进入数组后面做模型时如果没过滤模型会学到眼电伪迹。我一般在导出前先用raw.pick_types(eegTrue)只保留EEG通道这样数组里全是真实脑电信号。4. 癫痫数据提取与脑电去噪从原始波形到训练样本数据读出来只是开始真正的重头戏是把发作标注变成能训练的样本。EDF里的癫痫数据通常是连续记录包含发作期和发作间期两者时长比例可以相差几十倍。直接整段喂给模型有两个问题类不平衡太严重以及模型不知道从什么时候开始算发作。这一章解决这两个问题。4.1 发作标注解析MNE的annotations到底在说什么先看标注内容长什么样再决定怎么用。import mne raw mne.io.read_raw_edf(./EEG_dataset/patient_01.edf, preloadTrue) onset raw.annotations.onset duration raw.annotations.duration description raw.annotations.description for o, d, desc in zip(onset, duration, description): print(f起始:{o:.2f}s 持续:{d:.2f}s 描述:{desc})输出里你会看到类似 “seizure”、epilepsy、start、end 这类字符串。不同数据集标注风格差别很大有的标“sz”有的标spike持续时间0需要先统计所有description的值再做映射别想当然把所有都当作发作。另一个注意点是onset的单位是秒且相对文件起始点不是绝对时钟时间。如果EDF头文件里记录的开始时间是 14:23:05那onset120.5 就表示 14:25:05.5。外部标注文件里如果写的是绝对时间就得用开始时间做一次减法对齐。4.2 滑动窗口切片窗长、步长和类别标签怎么定滑动窗口是脑电分类最常用的切片方式。窗口长度一般取2到5秒癫痫检测常用4秒窗。步长取更小值如0.5到1秒能增强样本量但相邻窗口重叠太大会引入标签泄漏模型看起来精度很高实际是记位置。通常重合50%到75%是可用区间。sfreq int(raw.info[sfreq]) window_sec 4 hop_sec 1 win_len window_sec * sfreq hop_len hop_sec * sfreq # 先把标注区间转成 (start秒, end秒) 二元组 seizure_windows [] for o, d, desc in zip(raw.annotations.onset, raw.annotations.duration, raw.annotations.description): if seizure in desc.lower() or sz in desc.lower(): seizure_windows.append((o, o d)) X_parts [] y_parts [] for start in range(0, raw.n_times - win_len, hop_len): window_data raw.get_data(startstart, stopstart win_len) t_start start / sfreq t_end (start win_len) / sfreq label 0 for sz_start, sz_end in seizure_windows: # 窗口与发作区间有重叠就标为正样本 if t_start sz_end and t_end sz_start: label 1 break X_parts.append(window_data) y_parts.append(label)标注重叠判定的边界是t_start sz_end and t_end sz_start这个写法覆盖了窗口完全落在发作段内和跨边界两种情况比简单判断中点是否在区间内更稳。类不平衡方面如果正样本比例低于5%可以后续用过采样复制正样本窗口或者改用滑窗步长只挪0.5秒补正样本千万别直接删负样本脑电的静息段形态信息也有价值。4.3 脑电去噪三板斧带通滤波、陷波、坏通道剔除滤波是脑电去噪里性价比最高的一步。EEG有效信号集中在0.5到40Hz50Hz工频干扰是每个做脑电的人都要面对的欧洲是50Hz北美是60Hz看数据来源选。# MNE自带的FIR带通滤波截止频率0.5和40Hz raw.filter(0.5, 40, fir_designfirwin) # 50Hz工频陷波带宽设为4Hz raw.notch_filter(50, pickseeg, notch_widths4) # 坏通道粗筛标准差过小说明信号无变化过大说明饱和或噪声 bad_chans [] for ch in raw.ch_names: ch_data raw.copy().pick([ch]).get_data()[0] ch_std np.std(ch_data) if len(ch_data) else 0 if ch_std 0.5 or np.isnan(ch_data).sum() 0: bad_chans.append(ch) raw.info[bads] bad_chans print(剔除了这些通道:, bad_chans)raw.filter的截止频率是按Hz写的注意它不会改变采样率只做滤波。notch_filter的notch_widths参数影响陷波带宽太窄可能只滤掉一个频点太宽会连累40Hz附近的脑电成分。坏通道阈值我给的0.5是经验值实际以你数据的单位为准uV量级下正常脑电标准差一般在5到20之间偏差过大就要排查记录质量问题。这步做完后再切片你会明显发现滤波器边缘的震荡少了很多模型输入也更干净。5. EDF读取与RAR分发的5个踩坑现象、原因、解决方案这一章写的是我在实际数据集上摸出来的坑每一条都对应过真实的翻车现场按“现象 → 原因 → 解决”记录排查时按顺序对照。5.1 坑一解压时CRC校验失败读到的EDF数据尾部不完整现象7z解压到95%时报CRC错误强制解压出来的EDF文件能被打开读出来的数据前边正常后边波形突然断掉或者重复一段。原因下载文件不完整或者压缩包本身在存储过程中损坏而强制解压把损坏区域填充了占位数据。解决先跑一遍7z t EEG.rar确认哪些文件CRC错误。CRC错误的分卷不要用修复功能硬救重新下载原始分卷。如果实在找不到完整源那就只用文件前部完好的数据做分析并且在论文或报告里明确说明数据时长打了折扣。5.2 坑二pyedflib和MNE读出来的同一通道数值差几百倍现象用pyedflib的readSignal读出的信号幅度在 ±500uV 之间看起来正常但用MNE的raw.get_data()读同一个通道数值到了 ±50000完全不合理。原因MNE读取EDF时默认把标定公式用错了单位或者该EDF头文件里物理单位写的是“V”而不是“uV”MNE按V输出pyedflib按uV输出差了1000倍。解决先打印edf.getPhysicalDimensions()看官方头文件单位。如果是V就统一除以1000转成uV如果是uV直接和pyedflib对比确认一致。写代码时把单位换算显式写出来别依赖库默认行为。5.3 坑三MNE读EDF后raw.annotations.onset全是偏移量外部标注完全对不上现象EDF里没有注解通道两个标注文件的起始时间写的是 2024-01-09 14:23:05 这种绝对时间而MNE的annotations字段是空的或者读出来的onset是0到几百秒。原因文件是纯EDF没有“EDF Annotations”通道标注放在单独的CSV里或者EDF的注解通道里写的是绝对时间戳需要减掉头文件的开始时间才是相对秒数。解决先看同目录有没有疑似标注文件把CSV读进来用头文件的开始时间raw.info[meas_date]做差值。差值那步一定要自己写测试验证标注绝对时间减开始时间必须落在记录时长范围内。5.4 坑四不同患者EDF的采样率不一致拼接数组时直接崩现象patient_01采样率250Hzpatient_02是512Hz你按索引拼数组触发了维度不一致。原因公开癫痫数据来源杂采集设备不同采样率天然不一致。解决在读取阶段就统一重采样到同一频率比如128Hz或256Hz。MNE里直接用raw.resample(256)但要注意先滤波再降采样不然有混叠。我习惯在读取循环外面加一个配置变量TARGET_SFREQ 256每个文件读完都检查raw.info[sfreq]不一致就resample。这样后面的窗口切片代码不用各自为政。5.5 坑五加密的EEG.rar打不开在线密码恢复工具越跑越偏现象压缩包设置了密码你在下载页只找到没用的提示试几个常见口令都不对。原因医疗数据有伦理审查要求发布方往往在数据说明文档里单独写密码而不是放在压缩包文件名附近也有人把密码写在个人信息脱敏的那一行里。解决先翻发布页的说明文件、README、CSV表头。都找不到就联系发布者索取这类数据通常有使用协议。对来源不明的包不建议用暴力恢复工具去试一是耗时不可控二是无法确认数据授权范围。密码本身也不是EDF读取的核心核心永远是拿到合规数据后再解析。6. 读取结果自检两步标定校核与标注对齐最后这步最容易偷懒但跳过它出了错别人发现不了你自己也察觉不到。我会在数据入库前做两道自检全程不过十行代码。import pyedflib import mne import numpy as np file_path ./EEG_dataset/patient_01.edf # 第一步pyedflib和MNE对拍前100点应几乎一致 edf pyedflib.EdfReader(file_path) signal_py edf.readSignal(0)[:100] edf.close() raw mne.io.read_raw_edf(file_path, preloadTrue) signal_mne raw.get_data(picksraw.info[ch_names][0])[0, :100] diff np.abs(signal_py - signal_mne).max() print(两种读取方式最大差值(uV):, diff) # 第二步标注时间轴合理性检查 assert raw.annotations.onset.max() raw.times[-1], 标注越界时间对齐有问题 print(时间轴检查通过标注最大起始秒:, round(raw.annotations.onset.max(), 2))对拍结果里差值小于0.01uV基本可以认为读取正确差值大到几uV甚至上百uV说明某个库内部做了额外处理或单位换算不一致必须查清楚再继续。标注对齐是更关键的检查发作起始时间必须早于总时长且遍历所有发作窗口时不能出现负值或NaN。这两步通过后再把数据按第3.4节存成npz后续模型训练就都在一个统一格式上。做EDF数据的这半年我最后悔的事就是刚开始跳过了自检单靠MNE一条路线读数据做出来的模型在本地测试精度很高换了台机器跑就崩。后来才发现是单位不一致导致特征分布全变了。从那以后我养成了两个习惯一是所有EDF读取代码必须双库对拍一遍二是标注对齐检查写进脚本开头。希望帮到你。本文还有配套的精品资源点击获取