第3章 MNE库入门:Raw对象创建与加载、数据可视化、通道选择与重命名、事件提取与Epoch生成
好,咱们进入MNE库的实战环节。说实话,MNE这个库我用了快十年了,从最早的0.9版本一路跟到现在。它就像一把瑞士军刀,处理脑电数据时几乎什么活儿都能干。这一章,咱们就把它最核心的几个功能摸透。
3.1 Raw对象:脑电数据的"容器"
Raw对象是什么?说白了,它就是MNE里用来装原始脑电数据的容器。你采集到的所有通道数据、采样频率、电极位置等信息,全塞在这个对象里。
3.1.1 从零创建Raw对象
有时候我们需要自己造数据来测试算法。比如你写了个滤波函数,想看看效果对不对,这时候手动创建Raw对象就很有用。
import mne
import numpy as np
# 模拟数据:5秒,250Hz采样,19个通道
sfreq = 250 # 采样频率
duration = 5 # 秒
n_channels = 19
data = np.random.randn(n_channels, sfreq * duration)
# 创建Info对象
info = mne.create_info(
ch_names=['Fz', 'Cz', 'Pz', 'F3', 'F4', 'C3', 'C4',
'P3', 'P4', 'O1', 'O2', 'F7', 'F8', 'T7',
'T8', 'P7', 'P8', 'Fp1', 'Fp2'],
sfreq=sfreq,
ch_types='eeg'
)
# 创建Raw对象
raw = mne.io.RawArray(data, info)
print(raw)
print(f"数据形状: {raw.get_data().shape}")
嗯,这里要注意一点:ch_types参数我习惯写成'eeg',但如果你有眼电、心电等其他类型的信号,可以传一个列表,比如['eeg', 'eeg', 'eog', ...]。我在项目里就踩过这个坑——把所有通道都标成'eeg',结果做ICA去眼电时死活找不到EOG通道。
3.1.2 加载真实数据
实际项目中,我们更多是从文件加载数据。MNE支持几十种格式,我常用的就三种:
| 格式 | 加载函数 | 适用场景 |
|---|---|---|
| EDF/EDF+ | mne.io.read_raw_edf() | 医院、公开数据集最常见 |
| BrainVision | mne.io.read_raw_brainvision() | 德国Brain Products设备 |
| FIF | mne.io.read_raw_fif() | MNE原生格式,速度最快 |
# 加载EDF文件(以公开数据集为例)
raw = mne.io.read_raw_edf('sample_data.edf', preload=True)
print(f"采样频率: {raw.info['sfreq']} Hz")
print(f"通道数: {len(raw.ch_names)}")
print(f"时间长度: {raw.times[-1]:.2f} 秒")
我个人建议,只要内存够用,就把preload=True加上。为什么?因为后续做滤波、重参考这些操作,如果数据没加载到内存,MNE会反复读硬盘,慢得让人抓狂。我曾经处理一个2小时的EEG数据,没加preload,一个简单的带通滤波跑了快10分钟……加了之后秒出。
3.2 数据可视化:先看看你的信号长什么样
拿到数据第一件事是什么?我反正不会直接跑分析。先画图看看,有没有坏段、有没有大噪声、基线漂不漂。这一步省不了。
3.2.1 原始信号绘图
# 绘制原始信号(前10秒)
raw.plot(duration=10, n_channels=10, scalings='auto')
# 绘制功率谱密度
raw.plot_psd(fmin=0.5, fmax=45, tmin=0, tmax=60)
scalings='auto'这个参数很贴心,它会根据每个通道的信号幅度自动调整显示比例。但有时候自动调整反而会掩盖问题——比如某个通道信号特别小,自动缩放后看起来跟正常通道一样,你就发现不了它坏了。所以我习惯先设成统一尺度看看:
# 统一尺度,更容易发现异常通道
raw.plot(duration=10, scalings=dict(eeg=100e-6))
3.2.2 通道布局图
电极位置对不对?这个一定要检查。我遇到过好几次,数据文件里的电极位置标错了,结果做源定位时出来的结果完全不对。
# 绘制电极位置
raw.plot_sensors(show_names=True)
# 如果电极位置信息缺失,可以手动设置
montage = mne.channels.make_standard_montage('standard_1020')
raw.set_montage(montage)
raw.plot_sensors(show_names=True)
这里有个坑:set_montage()之前,一定要确保通道名称跟模板里的名称能对上。比如你的数据里通道叫'Fz',模板里也叫'Fz',那就没问题。但如果你的数据里叫'FZ'(大写),那就对不上了。我一般会先打印montage.ch_names看看。
3.3 通道选择与重命名
实际数据很少能直接用。通道顺序乱七八糟、命名不规范、有些通道根本没用……这些都得处理。
3.3.1 选择通道子集
# 选择特定通道
raw_picked = raw.pick_channels(['Fz', 'Cz', 'Pz', 'O1', 'O2'])
# 按类型选择
raw_eeg = raw.pick_types(meg=False, eeg=True, eog=False)
# 排除坏通道
raw.drop_channels(['Fp1', 'Fp2']) # 额极通道容易受眨眼污染
你想想看,为什么我经常把Fp1和Fp2丢掉?因为这两个通道离眼睛太近了,眨眼产生的伪迹幅度比脑电信号大几十倍。虽然可以用ICA去除,但有时候直接丢掉更省事——尤其是你关注的脑区不在额叶时。
3.3.2 重命名通道
不同设备厂商的命名规则五花八门。有的叫'Cz',有的叫'C_Z',还有的叫'Channel_12'。统一命名是第一步。
# 批量重命名
rename_dict = {
'C_Z': 'Cz',
'P_Z': 'Pz',
'Channel_12': 'Fz',
'Channel_14': 'Cz'
}
raw.rename_channels(rename_dict)
# 或者用函数批量处理
raw.rename_channels(lambda x: x.upper().replace(' ', ''))
嗯,这里有个小技巧:用lambda函数做批量处理特别灵活。比如你的通道名是'EEG Fz'、'EEG Cz'这种带前缀的,一行代码就能去掉前缀:raw.rename_channels(lambda x: x.replace('EEG ', ''))。
3.4 事件提取与Epoch生成
终于到了最关键的部分。原始脑电信号是连续的,但我们做分析时,通常只关心刺激出现前后那一段。这就需要把连续信号切成一个个小片段——也就是Epoch。
3.4.1 从刺激通道提取事件
大多数脑电设备在采集数据时,会用一个专门的通道记录刺激标记。比如每次屏幕出现一个图片,这个通道的电压就会跳变一下。
# 从STIM通道提取事件
events = mne.find_events(raw, stim_channel='STI 014')
print(f"找到 {len(events)} 个事件")
print(events[:5]) # 前5个事件
events是一个n×3的数组,每一行代表一个事件。三列分别是:事件发生的采样点序号、前一个事件的持续时间(这个一般用不到)、事件类型(比如1代表目标刺激,2代表非目标刺激)。
raw.ch_names看看有没有类似名字的通道。实在不行,就手动查看原始信号,找到那个有脉冲信号的通道。
3.4.2 事件ID映射
原始事件ID通常是数字,比如1、2、3、4。但每个数字代表什么含义,需要你自己定义。
# 定义事件ID映射
event_id = {
'标准刺激': 1,
'偏差刺激': 2,
'目标刺激': 3
}
# 也可以反过来,从数字到名称
event_id_inv = {v: k for k, v in event_id.items()}
print(event_id_inv) # {1: '标准刺激', 2: '偏差刺激', 3: '目标刺激'}
3.4.3 生成Epoch
有了事件,就可以切Epoch了。这里有两个关键参数:tmin和tmax,决定了每个Epoch的时间范围。
# 生成Epoch:刺激前0.2秒到刺激后0.8秒
tmin, tmax = -0.2, 0.8
epochs = mne.Epochs(
raw,
events,
event_id=event_id,
tmin=tmin,
tmax=tmax,
baseline=(-0.2, 0), # 基线校正:用刺激前200ms做基线
preload=True,
reject=dict(eeg=150e-6) # 拒绝幅度超过150μV的试次
)
print(epochs)
print(f"Epoch数据形状: {epochs.get_data().shape}")
数据形状是(试次数, 通道数, 时间点)。比如你有100个试次、19个通道、每个Epoch有251个时间点(0.2+0.8秒 × 250Hz),那形状就是(100, 19, 251)。
reject参数一定要设。不设的话,那些被眨眼、肌电污染的试次会混进来,把平均结果搞得一塌糊涂。150μV这个阈值是我做视觉ERP时常用的,但不同实验可以调整。我一般先画几个Epoch看看噪声水平,再定阈值。
3.4.4 Epoch的可视化与筛选
# 绘制所有Epoch(蝴蝶图)
epochs.plot(n_epochs=20, scalings='auto')
# 绘制平均波形
epochs['标准刺激'].average().plot()
epochs['偏差刺激'].average().plot()
# 按条件筛选Epoch
epochs_clean = epochs.copy().drop_bad() # 去掉被拒绝的试次
print(f"保留试次: {len(epochs_clean)} / {len(epochs)}")
你可能会问:为什么用copy()?因为drop_bad()会直接修改原对象。我习惯先复制一份,万一删错了还能找回原始数据。
知识体系总览
这一章的内容比较多,我画了张图帮你理清脉络:
这张图把整个流程串起来了。从Raw对象开始,经过可视化检查、通道处理,再到事件提取和Epoch生成,每一步都有对应的MNE函数。你按照这个顺序走,基本不会出错。
核心要点回顾:
- Raw对象是MNE的数据基石,支持多种格式加载
- 可视化是数据质量检查的第一道防线
- 通道重命名和选择是数据标准化的必要步骤
- 事件提取要确认刺激通道名称和事件ID含义
- Epoch生成时,基线校正和伪迹拒绝是标配
好了,这一章的内容就到这儿。代码不多,但每一步都是实战中绕不开的。你把这些基础打牢了,后面做特征提取、分类分析才会顺手。