简介:这份资源面向神经科学、医学信号处理方向的学习者与研究者,聚焦EDF格式脑电波文件的读取与癫痫数据分析,适合具备一定Python基础、希望上手EEG预处理的初中级用户。压缩包共3个文件,均为m脚本类型,涵盖EDF读取、数据读取与轨迹恢复等核心功能,整体约4KB,体积轻量便于快速集成到现有分析流程中。已有1293人学习下载,说明其在脑电入门与癫痫检测场景中具有一定参考价值。读者可借助其中的脚本完成EDF文件导入、通道与采样率信息查看,并在此基础上开展滤波去噪、眼动与肌电干扰去除等预处理操作,为功率谱分析、事件相关电位研究及癫痫发作预测等任务打下基础。对于需要理解EDF数据结构、搭建脑电分析管线的读者,这份脚本集合提供了可直接参考的实现思路与排错线索。
1. 拿到一份 EEG.rar 里的 EDF 脑电文件,先别急着写模型
你从合作方或公开数据集拿到一个 EEG.rar,解压后是一堆 .edf 文件,文件名里带着 patient、sz、chb 之类的字样,旁边可能还有 .txt 或 .csv 的标注。这就是典型的癫痫脑电数据场景:EDF 是欧洲数据格式(European Data Format),用来存多通道、长时间采样的脑电波信号,癫痫数据往往一录就是几小时,采样率从 256 Hz 到 512 Hz 不等。很多人第一反应是直接上深度学习,结果卡在读取这一步——文件打不开、通道名对不上、时间轴错位、标注和信号对不齐。这篇笔记就按一线做法,把 EDF 读取、通道校验、癫痫标注对齐、最小可跑通流程讲清楚,适合刚拿到脑电数据集的算法同学和做癫痫辅助分析的工程师。
2. EDF 文件结构拆解:为什么不能当普通二进制读
2.1 EDF 头部的固定字段与可变字段
EDF 文件分两部分:固定长度的头部(256 字节)和每个信号的可变头部。固定头部里存了版本号、患者信息、记录开始时间、头部字节数、保留字段、记录数、每条记录时长、信号数量。可变头部按信号逐个存:信号标签(通道名)、 transducer 类型、物理维度、物理最小值/最大值、数字最小值/最大值、预滤波信息、每个记录的采样点数、保留字段。这些字段都是 ASCII 文本,用空格补齐到固定宽度。很多人用open().read()直接读,拿到一堆乱码,就是因为没按 EDF 规范解析这些定长字段。
常见做法是用 Python 的pyedflib或mne读取。pyedflib更底层,能拿到原始头信息;mne封装更好,适合直接进分析流程。我一般先用pyedflib做一次头信息体检,再用mne做后续处理。下面这段代码就是读取 EDF 头部并打印关键字段:
import pyedflib # 打开 EDF 文件,只读模式 f = pyedflib.EdfReader("chb01_03.edf") # 获取信号数量 n_channels = f.signals_in_file print("通道数:", n_channels) # 获取每个通道的标签 labels = f.getSignalLabels() print("通道名:", labels) # 获取采样率(每个通道可能不同) sample_rates = f.getSampleFrequencies() print("采样率:", sample_rates) # 获取记录开始时间 start_time = f.getStartdatetime() print("开始时间:", start_time) # 获取每条记录的时长(秒) record_duration = f.getFileDuration() print("文件总时长(秒):", record_duration) # 读取第一个通道的全部数据 signal = f.readSignal(0) print("通道0数据形状:", signal.shape) f.close()逻辑说明:EdfReader会解析头部,signals_in_file是通道数,getSignalLabels()返回通道名列表,getSampleFrequencies()返回每个通道的采样率(EDF 允许不同通道不同采样率)。readSignal(0)读取第 0 个通道的全部采样点,返回一维 numpy 数组。参数上,readSignal可以指定start和n来分段读取,避免一次性加载几小时数据导致内存爆掉。
2.2 通道名、采样率与物理量纲的校验
拿到头信息后,必须做三件事:通道名是否和标注文件里的通道一致、采样率是否统一、物理量纲是否合理。癫痫数据常见通道名是 FP1-F7、F7-T7、T7-P7 这种双极导联,或者 FP1、F3、C3 这种参考导联。如果标注文件里写的是通道索引,而 EDF 里是通道名,就要建映射表。采样率不统一时,后续做频谱分析会出错,需要重采样到统一频率。物理量纲看getPhysicalDimension(),一般是 uV 或 mV,如果读到的是 V,数值会小得离谱。
# 检查物理量纲和物理最大最小值 for i in range(n_channels): dim = f.getPhysicalDimension(i) pmin, pmax = f.getPhysicalMinimum(i), f.getPhysicalMaximum(i) dmin, dmax = f.getDigitalMinimum(i), f.getDigitalMaximum(i) print(f"通道{i} {labels[i]}: 单位={dim}, 物理范围=[{pmin},{pmax}], 数字范围=[{dmin},{dmax}]")如果物理范围是 [-2048, 2047] 这种整数,说明是数字量未校准,需要按 EDF 规范做线性变换:physical = (digital - dmin) / (dmax - dmin) * (pmax - pmin) + pmin。pyedflib的readSignal默认已经做了这个变换,返回物理值。但如果你用其他库读原始数字量,就要自己算。
提示:EDF 文件里患者信息字段可能包含姓名、生日等隐私信息,做数据共享或发表时记得脱敏,常见做法是把这些字段替换成匿名 ID。
3. 用 MNE 把 EDF 读成可分析对象:通道定位与坏道处理
3.1 MNE 读取 EDF 的最小命令与参数
mne是脑电分析里最常用的库,读取 EDF 只要一行mne.io.read_raw_edf。但它默认会做一些推断,比如根据通道名自动设置通道类型(EEG、EOG、ECG),如果通道名不规范,就会全部标成 EEG 或者 misc。癫痫数据里常有 EKG、EMG 通道,需要手动指定。下面是最小读取命令:
import mne # 读取 EDF,preload=True 表示把数据加载到内存 raw = mne.io.read_raw_edf("chb01_03.edf", preload=True, verbose=False) # 查看基本信息 print(raw.info) # 查看通道名和类型 for ch in raw.info['chs']: print(ch['ch_name'], ch['kind'])preload=True会把数据全部读进内存,方便后续切片和滤波;如果文件很大,可以先preload=False,用raw.load_data()按需加载。verbose=False减少输出。raw.info里包含采样率、通道数、坏道标记等。ch['kind']是通道类型编码,1 是 EEG,2 是 MEG,202 是 EOG,等等。
3.2 通道定位与坏道标记
癫痫分析里,通道定位很重要,尤其是做源定位的时候。MNE 支持设置 montage,常见的是标准 10-20 系统。如果通道名和标准名不一致,可以用映射字典重命名。坏道标记用raw.info['bads'],把明显漂移或噪声大的通道加进去,后续分析会自动排除。
# 设置标准 montage montage = mne.channels.make_standard_montage('standard_1020') raw.set_montage(montage, on_missing='ignore') # 手动标记坏道 raw.info['bads'] = ['FP1-F7', 'T7-P7'] # 根据实际观察填写 # 查看坏道 print("坏道:", raw.info['bads'])on_missing='ignore'表示遇到不认识的通道名就跳过,不报错。raw.info['bads']是一个列表,后续raw.pick_types(eeg=True)会自动排除坏道。如果要做源定位,还需要设置电极位置,set_montage会从标准模板里找对应坐标。如果通道名是双极导联,标准 montage 可能对不上,需要自定义坐标或改用参考导联数据。
注意:双极导联做源定位时,MNE 会提示无法定位,因为双极导联没有绝对位置。常见做法是先把双极导联转成参考导联,或者直接用参考导联数据做源定位。
4. 癫痫标注对齐:把 .txt 里的发作时间段映射到信号上
4.1 标注文件格式与时间基准
癫痫数据集常见标注格式是:每行一个发作,包含开始时间、结束时间、发作类型。时间单位可能是秒、样本点或 HH:MM:SS。CHB-MIT 数据集用的是秒,文件里类似1 299 1表示第 1 个发作从 299 秒开始。TUH 数据集用的是更复杂的 CSV。关键是对齐时间基准:EDF 头里的开始时间是绝对时间,标注文件里的时间是相对文件开始的偏移。如果标注文件用的是绝对时间,就要减去 EDF 开始时间。
import pandas as pd # 读取标注文件,假设是空格分隔,无表头 ann = pd.read_csv("chb01_03.txt", sep=r'\s+', header=None, names=['start', 'end', 'type']) # 查看前几行 print(ann.head()) # 假设时间单位是秒,直接用来切片 sfreq = raw.info['sfreq'] # 采样率 start_sample = int(ann.iloc[0]['start'] * sfreq) end_sample = int(ann.iloc[0]['end'] * sfreq) # 提取发作片段 seizure_data = raw.get_data(start=start_sample, stop=end_sample) print("发作片段形状:", seizure_data.shape)raw.get_data(start, stop)返回指定样本范围的数组,形状是 (通道数, 样本数)。start和stop是样本索引,不是秒。如果标注文件是 HH:MM:SS,需要先转成秒。pd.read_csv的sep=r'\s+'匹配任意空白字符,适合空格或制表符分隔的文件。
4.2 发作间期与发作期的划分
做癫痫检测时,通常把数据分成发作期、发作前期、发作间期。发作前期一般取发作前 5 到 30 分钟,发作间期取远离发作的时间段。划分时要避免数据泄漏:训练集和测试集不能来自同一个患者的同一段连续数据。常见做法是按患者划分,或者按时间顺序划分,前 80% 做训练,后 20% 做测试。
# 定义发作前期窗口(发作前 10 分钟) preictal_window = 10 * 60 # 秒 # 假设 ann 里有多行发作 for idx, row in ann.iterrows(): start = row['start'] end = row['end'] preictal_start = max(0, start - preictal_window) print(f"发作{idx}: 发作期 [{start}, {end}], 发作前期 [{preictal_start}, {start}]")这段代码只是打印划分结果,实际使用时要把这些时间段转成样本索引,然后从raw里切片。注意边界:如果发作前不足 10 分钟,preictal_start要取 0。发作间期可以取发作后 30 分钟到下一个发作前 30 分钟之间的片段。
提示:标注文件里可能有多个发作,也可能有发作类型(如 CPS、GTC),做二分类时可以把所有发作类型合并成正类,做多分类时再细分。
5. 避坑与排查:EDF 读取和癫痫分析里最容易翻车的 5 个点
5.1 现象:读取时提示“文件头损坏”或“不是 EDF 文件”
原因:文件可能是 EDF+ 格式,或者被压缩过,或者下载不完整。EDF+ 在固定头部之后有额外的注释通道,pyedflib能读,但有些老库不支持。解决:先用file命令看文件类型,确认是 EDF 还是 EDF+。如果是 EDF+,用pyedflib或mne都能读。如果文件大小和预期不符,重新下载。
5.2 现象:通道名全是“EEG 1”“EEG 2”这种默认名
原因:EDF 头部里的信号标签字段为空或非标准,读取库自动生成了默认名。解决:打开 EDF 头信息,看getSignalLabels()返回什么。如果是空的,需要从数据集说明文档里找通道顺序,手动重命名。常见做法是建一个列表,按顺序赋名。
5.3 现象:采样率读出来是 0 或负数
原因:EDF 头部里每条记录的采样点数字段解析错误,可能是文件损坏或库的 bug。解决:换一个库读,比如mne读不出来就用pyedflib,反之亦然。如果都不行,用十六进制编辑器看头部对应字段,确认数值是否合理。
5.4 现象:发作片段提取出来全是平线或噪声
原因:时间基准没对齐,标注文件里的时间可能是绝对时间,而raw.get_data用的是相对样本索引。解决:先打印 EDF 的开始时间和标注文件的第一行时间,确认单位。如果是绝对时间,减去 EDF 开始时间再转样本索引。
5.5 现象:做源定位时提示“没有电极位置”
原因:没有设置 montage,或者通道名和标准 montage 不匹配。解决:用raw.set_montage设置标准 montage,on_missing='ignore'跳过不匹配的通道。如果双极导联无法定位,先转成参考导联,或者直接用参考导联数据。
6. 从 EDF 到最小范数估计:源定位的进阶用法与验证
6.1 最小范数估计的输入准备
最小范数估计(Minimum Norm Estimate, MNE)是脑电源定位里最常用的逆问题求解方法之一。它的输入是头皮脑电信号和头模型、源空间、导联场矩阵。做 MNE 之前,需要把 EDF 数据预处理干净:滤波(通常 1-40 Hz)、去伪迹(ICA 或 SSP)、重参考(平均参考或乳突参考)。然后构建源空间,计算导联场,最后求解。
import mne from mne.minimum_norm import make_inverse_operator, apply_inverse # 假设 raw 已经预处理过 # 构建源空间 src = mne.setup_source_space('fsaverage', spacing='oct6', add_dist=False) # 构建 BEM 模型(需要 fsaverage 的 BEM 文件) bem = mne.make_bem_model('fsaverage', ico=4) bem_sol = mne.make_bem_solution(bem) # 计算导联场 trans = 'fsaverage' # 需要配准文件,这里简化 fwd = mne.make_forward_solution(raw.info, trans=trans, src=src, bem=bem_sol, eeg=True) # 构建噪声协方差 noise_cov = mne.compute_covariance(raw, tmax=60, method='shrunk') # 构建逆算子 inv = make_inverse_operator(raw.info, fwd, noise_cov, loose=0.2, depth=0.8) # 应用最小范数估计 stc = apply_inverse(raw, inv, lambda2=1.0/9.0, method='dSPM') # 可视化 stc.plot(hemi='both', subjects_dir='fsaverage')setup_source_space构建源空间,spacing='oct6'是常用的网格密度。make_bem_model和make_bem_solution构建头模型。make_forward_solution计算导联场,trans是头模型和 MRI 的配准文件。compute_covariance计算噪声协方差,tmax=60表示用前 60 秒数据估计噪声。make_inverse_operator构建逆算子,loose=0.2允许源方向偏离法线,depth=0.8做深度加权。apply_inverse求解,lambda2是正则化参数,method='dSPM'输出标准化电流密度。
6.2 验证方法与参数调优
MNE 的结果验证通常看几个指标:峰值位置是否在预期区域、时间过程是否和发作期吻合、不同正则化参数下的稳定性。lambda2越大,解越平滑,但空间分辨率下降;越小,解越尖锐,但噪声敏感。常见做法是扫一组lambda2,看结果是否稳定。另外,loose参数控制源方向约束,0 表示严格法线方向,1 表示自由方向,0.2 是常用折中。
| 参数 | 常用值 | 作用 | 调大影响 | 调小影响 |
|---|---|---|---|---|
| lambda2 | 1/9 | 正则化强度 | 解更平滑,噪声鲁棒 | 解更尖锐,噪声敏感 |
| loose | 0.2 | 源方向约束 | 方向更自由,定位更准 | 方向更受限,计算更快 |
| depth | 0.8 | 深度加权 | 深部源更易检出 | 浅部源更突出 |
| spacing | oct6 | 源空间密度 | 网格更密,计算更慢 | 网格更疏,计算更快 |
我自己的习惯是:先跑一遍默认参数,看结果是否合理;如果噪声大,加大lambda2;如果定位太模糊,减小lambda2或减小loose。源定位不是一步到位的事,通常要结合临床知识反复调。希望帮到你。
本文还有配套的精品资源,点击获取