
简介这份资源面向神经科学、医学信号处理方向的学习者与研究者聚焦EDF格式脑电数据的读取与癫痫相关分析适合具备Python基础、希望上手真实EEG数据的初中级用户。压缩包共3个文件均为m脚本文件整体约4KB涵盖EDF读取、数据读取与轨迹恢复等核心环节可直接在MATLAB环境中调用帮助读者快速完成从文件解析到信号加载的流程搭建。目前已有1293人学习下载说明其在脑电数据处理入门场景中具有一定参考价值。借助这些脚本读者可以理解EDF文件的时间戳、采样率与多通道结构并在此基础上衔接滤波、去噪、功率谱分析及癫痫发作检测等后续任务为特征提取与分类模型构建打下基础也可作为课程实验或科研预处理的轻量工具参考。1. 从一份 EEG.rar 说起EDF 脑电文件到底怎么读、癫痫数据长什么样如果你拿到过一个名为EEG.rar的压缩包解压后里面是一堆.edf文件每个文件几十到几百 MB双击打不开用文本编辑器打开全是乱码——恭喜你你碰到的是脑电领域最通用的数据格式之一EDF。它全称 European Data Format最初由欧洲几家睡眠实验室在 1992 年提出用来解决多导睡眠图设备之间数据互不通的问题。三十多年过去它依然是癫痫监测、睡眠分期、脑机接口研究里最常被引用的原始信号容器。一个典型的癫痫脑电 EDF 文件里可能同时包含 19 到 64 个电极通道的电压时序、采样率从 256 Hz 到 2000 Hz 不等还有事件标注通道记录着每一次临床发作的起止时刻。很多人第一次处理这类数据时卡住的地方不是算法而是“文件读进来之后那个二维数组到底哪一维是通道、哪一维是时间、单位是伏还是微伏”。这篇笔记就按我实际处理癫痫 EDF 数据的顺序把读取、校验、切片、标注对齐这几步拆开讲顺带把几个让我翻过车的参数坑标出来。适合手里已经有 EDF 文件、准备做发作检测或信号预处理的同学。2. EDF 文件结构拆解与读取工具选型为什么我不推荐一上来就手写解析器2.1 EDF 的头部字段里哪几个决定了你后面会不会翻车EDF 文件分两部分固定 256 字节的头部加上每个信号通道 256 字节的通道头再往后才是数据记录区。固定头里真正影响读取结果的有这几个字段版本号8 字节、患者信息80 字节、记录信息80 字节、开始日期时间16 字节、头部字节数8 字节、保留字段44 字节、记录条数8 字节、每条记录持续时间8 字节、通道数4 字节。通道头里则是每个通道的标签、传感器类型、物理单位、物理上下限、数字上下限、预滤波信息、每记录采样点数。这里最容易踩的是“物理上下限”和“数字上下限”这一对。EDF 存的是 16 位整数设备厂商在采集时会把真实电压线性映射到整数范围。读取时要用公式physical (digital - digital_min) * (physical_max - physical_min) / (digital_max - digital_min) physical_min还原。如果某个通道的 physical_min 和 physical_max 写反了或者 digital_min 和 digital_max 相等还原出来的信号就会整体翻转或直接除零。我见过一个癫痫数据集其中两个通道的 physical_max 被写成 0导致还原后整段信号恒为零如果不做校验直接送进检测模型模型会在这两个通道上学到完全错误的权重。另一个隐蔽字段是“每条记录持续时间”。EDF 允许一条记录持续 1 秒、10 秒甚至 0.1 秒而“每记录采样点数”是每个通道在一条记录内的采样点数。实际采样率等于每记录采样点数 / 记录持续时间。有些设备把记录持续时间写成 1采样点数写成 256那采样率就是 256 Hz但如果写成记录持续时间 0.5、采样点数 128采样率还是 256 Hz可如果你默认按 1 秒去算就会把时间轴拉长一倍。这个坑在跨设备合并数据时特别致命。2.2 用 MNE-Python 读 EDF最小可复现命令与参数说明我一般先用 MNE 把文件读进来做快速体检因为它对 EDF 的兼容性比手写解析稳而且自带通道类型推断和事件读取。下面这段是我处理癫痫 EDF 时的标准开场import mne import numpy as np # 读取 EDF 文件preloadTrue 表示把数据全部载入内存 # 癫痫数据文件通常较大如果内存吃紧可以设 preloadFalse 做惰性加载 raw mne.io.read_raw_edf( seizure_001.edf, preloadTrue, stim_channelNone, # 不自动把标注通道设为 stim避免通道数对不上 exclude[], # 不排除任何通道先全部读进来再手动筛 verboseERROR ) # 打印基本信息通道名、采样率、时长、通道类型 print(raw.info) # 查看前 5 个通道名和采样率 print(raw.ch_names[:5]) print(raw.info[sfreq]) # 获取数据数组单位是伏特 data raw.get_data() print(data.shape) # (n_channels, n_times) # 转成微伏方便和临床报告对照 data_uv data * 1e6这段代码里stim_channelNone是我强烈建议保留的设置。MNE 默认会尝试把名字里带 “stim” 或 “status” 的通道识别为事件通道并单独拎出来但很多癫痫 EDF 的标注通道命名并不规范自动识别要么漏掉要么误判导致通道数和数据维度对不上。先全部读进来再用raw.ch_names去找标注通道心里更有数。exclude[]也是同理MNE 默认会排除一些它认为非脑电的通道但在癫痫数据里眼电、肌电、心电往往对发作检测有辅助价值不该一上来就丢。raw.get_data()返回的是二维数组第一维是通道第二维是时间点单位是伏特。临床报告里常用微伏所以乘 1e6 是常规操作。如果你发现某个通道数值在 1e-3 量级那可能是单位没还原对或者该通道本身就是参考通道。2.3 不用 MNE 时用 pyedflib 做底层校验MNE 方便但它对头部字段的容错有时会掩盖问题。当我要确认一个文件是否被设备写坏时会换 pyedflib 直接读头部import pyedflib # 以只读方式打开 EDF f pyedflib.EdfReader(seizure_001.edf) # 通道数、每条记录持续时间、总记录数 n_channels f.signals_in_file record_duration f.getFileDuration() print(channels:, n_channels) print(duration(s):, record_duration) # 逐通道打印标签、采样率、物理上下限 for i in range(n_channels): label f.getLabel(i) sfreq f.getSampleFrequency(i) pmin, pmax f.getPhysicalMinimum(i), f.getPhysicalMaximum(i) dmin, dmax f.getDigitalMinimum(i), f.getDigitalMaximum(i) print(f{i:02d} {label:12s} sfreq{sfreq:6.1f} fphys[{pmin},{pmax}] dig[{dmin},{dmax}]) # 读取某个通道的原始数字值用于核对还原公式 digital f.readSignal(0, digitalTrue) physical f.readSignal(0, digitalFalse) print(digital[:5], physical[:5]) f.close()readSignal的digitalTrue返回的是未还原的整数digitalFalse返回的是还原后的物理值。把两者对照就能验证还原公式是否按预期工作。如果某个通道的dmax dmin这里会直接暴露出来你可以在后续处理中把该通道标记为无效而不是让它在模型里悄悄污染梯度。2.4 选型建议什么场景用 MNE什么场景用 pyedflib如果你的目标是快速做发作检测、睡眠分期、频谱分析MNE 是首选因为它和 NumPy、SciPy、scikit-learn 的衔接最顺事件读取、滤波、重参考都有现成方法。如果你要做数据质量审计、批量校验几千个文件、或者需要精确控制每个通道的还原过程pyedflib 更透明。我通常两个都用pyedflib 做批量体检MNE 做单文件深入分析。两者读同一个文件如果通道数、采样率、时长对不上那说明文件本身有问题先别往下走。3. 癫痫 EDF 的标注解析与发作片段切片把事件通道变成训练标签3.1 癫痫标注的两种常见存放方式癫痫 EDF 里的发作标注常见有两种放法。一种是单独一个通道通道名类似 “Event” 或 “Annotation”里面用非零值标记事件但不同设备编码方式不同有的用 1 表示发作开始、0 表示结束有的用递增的整数表示不同事件类型。另一种是 EDF 标准里的 TALTime-stamped Annotation List它把标注写在数据记录区的末尾用文本形式记录onset duration annotation。MNE 对 EDF 的 TAL 支持较好read_raw_edf之后可以用raw.annotations拿到事件列表对自定义编码的标注通道则需要手动解析。我处理过的一个模拟项目里标注通道的采样率和脑电通道不一样脑电是 512 Hz标注通道是 1 Hz每个采样点代表 1 秒。这种情况下如果直接把标注通道和脑电通道拼在一起时间轴会对不齐。正确做法是先按时间对齐再把标注扩展到脑电采样率。3.2 从标注通道提取发作区间的完整代码下面这段是我从自定义标注通道提取发作起止时间的常用写法假设标注通道名为 “Event”采样率 1 Hz值从 0 跳到 1 表示发作开始从 1 回到 0 表示发作结束import numpy as np import mne raw mne.io.read_raw_edf(seizure_001.edf, preloadTrue, verboseERROR) # 找到标注通道的索引 event_ch Event if event_ch not in raw.ch_names: raise ValueError(未找到标注通道请检查通道名) event_idx raw.ch_names.index(event_ch) event_data raw.get_data(picks[event_idx])[0] # 标注通道采样率可能和脑电不同先取它自己的时间轴 event_sfreq raw.info[sfreq] # 如果标注通道被重采样过这里需要单独处理 event_times np.arange(len(event_data)) / event_sfreq # 用差分找上升沿和下降沿 binary (event_data 0).astype(int) diff np.diff(binary) starts np.where(diff 1)[0] 1 ends np.where(diff -1)[0] 1 # 处理文件开头就是发作、结尾还没结束的边界情况 if binary[0] 1: starts np.r_[0, starts] if binary[-1] 1: ends np.r_[ends, len(binary)] # 配对成区间单位秒 seizures [(event_times[s], event_times[e]) for s, e in zip(starts, ends)] for i, (s, e) in enumerate(seizures): print(fseizure {i}: {s:.2f}s - {e:.2f}s, duration{e-s:.2f}s)这段代码的关键在边界处理。很多文件开头就是发作状态或者文件结束时发作还没终止如果不补这两个边界starts和ends数量对不上配对就会错位。另外np.diff找的是相邻采样点的变化如果标注通道有抖动比如 0 和 1 之间反复跳会产生大量假区间。实际数据里我遇到过标注通道在发作期间有毛刺解决办法是先对二值序列做形态学闭运算或者设置最小持续时间阈值把短于 2 秒的区间过滤掉。3.3 按发作区间切片并做通道级质控拿到发作区间后下一步是切片。癫痫检测模型通常需要正样本发作期和负样本发作间期负样本一般从发作前或发作后一段时间内取避免取到发作扩散期。我一般会留 30 秒的缓冲import numpy as np # 假设 data_uv 是 (n_channels, n_times) 的微伏数据sfreq 是采样率 sfreq raw.info[sfreq] data_uv raw.get_data() * 1e6 buffer_sec 30 segments [] labels [] for s, e in seizures: # 发作段 s_idx int(s * sfreq) e_idx int(e * sfreq) seg data_uv[:, s_idx:e_idx] segments.append(seg) labels.append(1) # 发作前负样本留 buffer 秒 pre_end max(0, s_idx - int(buffer_sec * sfreq)) pre_start max(0, pre_end - int((e - s) * sfreq)) if pre_end - pre_start sfreq: # 至少 1 秒 segments.append(data_uv[:, pre_start:pre_end]) labels.append(0) # 通道级质控检查每个通道的方差和最大值 for ch in range(data_uv.shape[0]): ch_std np.std(data_uv[ch]) ch_max np.max(np.abs(data_uv[ch])) if ch_std 1e-6: print(f通道 {raw.ch_names[ch]} 方差过小可能为死通道) if ch_max 5000: # 微伏超过 5 mV 可能是伪迹 print(f通道 {raw.ch_names[ch]} 最大值 {ch_max:.1f} uV疑似伪迹)这里的质控阈值不是绝对的。成人头皮脑电的典型幅度在 10 到 100 微伏癫痫发作时可能到几百微伏但超过 5000 微伏通常意味着电极脱落或运动伪迹。死通道的方差阈值设 1e-6 是因为正常脑电即使很安静方差也不会低到浮点误差级别。这些检查应该在切片之后、送模型之前做把有问题的通道标记出来要么插值要么剔除。3.4 采样率不一致时的重采样策略癫痫 EDF 数据经常来自不同设备采样率从 256 Hz 到 2000 Hz 都有。如果要做跨数据集训练必须统一采样率。我一般降到 256 Hz因为大部分发作检测模型在这个采样率下已经够用再高只会增加计算量。用 MNE 重采样raw_resampled raw.copy().resample(256, npadauto, verboseERROR)npadauto是让 MNE 自动处理边界 padding避免重采样后首尾出现振铃。重采样之前一定要先做抗混叠滤波MNE 的resample默认会做但如果你手动用 SciPy 的resample记得先低通到新采样率的一半以下。我见过有人直接把 2000 Hz 的数据隔点抽成 256 Hz结果高频噪声混叠到低频模型在发作间期疯狂误报。4. 避坑与排查EDF 读取和癫痫数据处理里最容易翻车的 5 个点4.1 现象读进来通道数比文件里少或者多出奇怪的通道原因通常是 MNE 自动识别了 stim 通道或排除了非脑电通道。解决方法是读取时显式设置stim_channelNone和exclude[]读完后用raw.ch_names核对再手动决定保留哪些通道。如果文件里确实有标注通道用raw.set_channel_types把它标成 “misc” 或 “stim”而不是让它混在脑电通道里。4.2 现象信号整体为零或恒定值先查该通道的digital_min和digital_max是否相等或者physical_max和physical_min是否写反。用 pyedflib 打印头部字段就能确认。如果头部字段正常再看数据区是否真的全零有些设备在通道未连接时会写全零。这种通道在训练前必须剔除或插值否则模型会学到无意义的常数特征。4.3 现象时间轴对不上发作标注偏移了几秒最常见的原因是标注通道和脑电通道采样率不同但被当成同一时间轴处理。解决方法是分别取各自的时间轴或者先把标注通道重采样到脑电采样率再对齐。另一个原因是 EDF 头部里的开始时间有毫秒级误差跨文件合并时要用绝对时间戳对齐而不是相对时间。4.4 现象发作检测模型在训练集上表现很好换一台设备就崩这通常是通道顺序和通道名不一致导致的。不同设备的 19 导联顺序可能不同有的用 “Fp1” 有的用 “FP1”大小写和空格都有差异。我一般会建一个标准通道名映射表把所有通道名统一成大写、去掉空格再按标准顺序重排。如果某个通道在目标设备上不存在用相邻通道插值或置零并在模型里加通道掩码。4.5 现象切片后正负样本比例极端失衡癫痫发作通常只占整个记录的一小部分有的数据集正样本不到 1%。如果直接按发作区间切片负样本会远多于正样本。我的做法是在负样本里做随机下采样同时保留一部分发作前期的负样本因为发作前期和发作间期的信号特征不同。另外切片长度要统一我一般用 2 到 4 秒的窗口太短抓不到发作的节律特征太长则包含太多发作间期。5. 一个可复用的批量处理脚本与验证习惯5.1 批量读取、质控、切片的脚本骨架把前面几步串起来我通常写一个批处理脚本对目录下所有 EDF 文件做统一处理import os import numpy as np import mne import pyedflib def audit_edf(path): 用 pyedflib 做头部审计返回通道级质量报告 f pyedflib.EdfReader(path) report [] for i in range(f.signals_in_file): dmin, dmax f.getDigitalMinimum(i), f.getDigitalMaximum(i) pmin, pmax f.getPhysicalMinimum(i), f.getPhysicalMaximum(i) ok (dmax ! dmin) and (pmax ! pmin) report.append({ label: f.getLabel(i), sfreq: f.getSampleFrequency(i), ok: ok }) f.close() return report def process_one(path, target_sfreq256, win_sec4): 读取、重采样、切片返回片段和标签 raw mne.io.read_raw_edf(path, preloadTrue, stim_channelNone, exclude[], verboseERROR) # 重采样到统一采样率 if abs(raw.info[sfreq] - target_sfreq) 1: raw raw.copy().resample(target_sfreq, npadauto, verboseERROR) data raw.get_data() * 1e6 # 微伏 sfreq raw.info[sfreq] # 这里假设标注通道名为 Event实际使用时按文件调整 if Event not in raw.ch_names: return None, None event_idx raw.ch_names.index(Event) event_data (raw.get_data(picks[event_idx])[0] 0).astype(int) diff np.diff(event_data) starts np.where(diff 1)[0] 1 ends np.where(diff -1)[0] 1 if event_data[0] 1: starts np.r_[0, starts] if event_data[-1] 1: ends np.r_[ends, len(event_data)] win_len int(win_sec * sfreq) segments, labels [], [] for s, e in zip(starts, ends): s_idx, e_idx int(s), int(e) # 发作段按窗口切 for i in range(s_idx, e_idx - win_len, win_len): segments.append(data[:, i:iwin_len]) labels.append(1) # 发作前负样本 pre_end max(0, s_idx - int(30 * sfreq)) pre_start max(0, pre_end - (e_idx - s_idx)) for i in range(pre_start, pre_end - win_len, win_len): segments.append(data[:, i:iwin_len]) labels.append(0) return np.array(segments), np.array(labels) # 批量处理 root eeg_data all_segs, all_labels [], [] for fname in os.listdir(root): if not fname.endswith(.edf): continue path os.path.join(root, fname) report audit_edf(path) bad [r[label] for r in report if not r[ok]] if bad: print(f{fname} 存在异常通道: {bad}跳过或修复后再用) continue segs, labels process_one(path) if segs is not None: all_segs.append(segs) all_labels.append(labels) if all_segs: X np.concatenate(all_segs, axis0) y np.concatenate(all_labels, axis0) print(片段形状:, X.shape, 标签分布:, np.bincount(y))这个脚本里audit_edf先做头部审计把物理上下限异常的通道找出来避免后面白忙。process_one里重采样、标注解析、切片、负样本采样都串在一起。注意负样本的窗口起点是pre_start终点是pre_end这样保证负样本不会跨到发作段里。实际使用时标注通道名和事件编码方式需要按你的数据调整但整体骨架是通用的。5.2 验证习惯每次处理完先看三个数我处理完一批数据后一定会看三个数片段总数、正负样本比例、每个通道的均值和标准差。片段总数用来判断有没有文件被静默跳过正负比例用来决定要不要调整负采样策略通道统计用来发现死通道或饱和通道。这三个数花不了一分钟但能挡住后面几小时的无效训练。另外我习惯把处理参数写进文件名或日志比如win4s_sfreq256_buffer30s这样过两周回头看还能复现。癫痫数据处理的玄学在于同一个模型换一批切片参数AUC 可能差十几个点没有日志根本说不清是哪一步变了。5.3 一个具体技巧用发作间期放电做弱标签如果你的 EDF 里除了发作标注还有发作间期放电IED标注别浪费。IED 是癫痫诊断的重要依据虽然它不等于发作但可以作为弱标签做预训练。我一般把 IED 片段单独切出来先训练一个二分类模型区分 IED 和正常背景再用这个模型的编码器去初始化发作检测模型。在数据量不大的情况下这个技巧能让收敛快不少。IED 的标注格式和发作类似但持续时间更短通常几十到几百毫秒切片窗口要相应缩小到 1 秒左右。最后说个血泪教训我早期处理癫痫 EDF 时图省事没做通道级质控直接把所有通道送进模型结果模型学到的第一个特征就是“哪个通道是坏的”。后来养成的习惯是任何数据进模型之前先跑一遍通道统计把方差过小、幅度饱和、与其它通道相关性极低的通道标出来要么修要么扔。这个习惯帮我省掉了无数次“模型在验证集上 AUC 0.5”的排查时间。希望帮到你。本文还有配套的精品资源点击获取