ERP 波形计算与地形图绘制
把模块 2 洗好的试次一层层叠起来,得到一条属于你自己的 ERP 波形。这一课先讲清 "平均"背后的信噪比直觉,再用 EEGLAB 把波形、ERPimage 和地形图全部画出来。
🎯 本节学习目标
- 能用"信号锁时、噪声随机"的信噪比直觉,解释平均为什么能提取出 ERP
- 理解 epochs 数据从"通道 × 时间 × 试次"到 ERP 的"通道 × 时间"发生了什么
- 会用 EEGLAB GUI 与脚本两种方式计算并绘制单被试 ERP 波形
- 理解 grand average 的意义,能写出跨被试的组平均脚本
- 会画并解读 ERPimage 与头皮地形图,掌握 topoplot 的关键参数
一、为什么要平均:一场信号和噪声的拔河
先面对一个让人沮丧的事实:单看一个试次,你几乎看不出 ERP。 ERP 成分的典型幅度只有几个微伏,而自发脑电、残留肌电动辄是它的五到十倍—— 信号是被噪声淹没的。
但 ERP 有一个噪声没有的特质:它和刺激锁时。刺激一出现,P300 就在 300ms 左右冒头,一百个试次里次次如此;噪声则与刺激无关,这一试次偏正、 下一试次偏负,完全随机。把试次对齐刺激点后逐点平均:信号每次都往同一个 方向使劲,被完整保留;噪声时正时负,互相抵消。
一个类比:教室里有人小声念课文,全班在随机聊天。只录一遍,念书声听不见; 把一百遍录音的起点对齐、逐点叠加,聊天声正负抵消,念书声一遍遍加强—— 它自己就浮出来了。
平均 N 个试次,信号不变,噪声的标准差缩小为原来的 1/√N——100 个试次能把噪声 压到十分之一。这就是"多收试次"永远不过时的原因:它是提升信噪比最笨、也最有效的手段。
% 用模拟数据直观感受"平均"的威力
% 每个试次 = 固定形状的"ERP"(信号) + 随机噪声
fs = 500; % 采样率 (Hz)
t = -0.2 : 1/fs : 0.8; % -200 ~ 800 ms 时间轴
sig = 6 * exp(-((t - 0.3) / 0.05).^2); % 模拟 300ms 的正波,峰值 6 uV
nTrial = 100;
data = zeros(length(t), nTrial); % 时间点 x 试次
for k = 1:nTrial
data(:, k) = sig + 8 * randn(length(t), 1); % 噪声比信号还大——真实脑电的常态
end
figure;
subplot(3, 1, 1); plot(t, data(:, 1)); title('单个试次:信号被噪声淹没');
subplot(3, 1, 2); plot(t, mean(data(:, 1:10), 2)); title('平均 10 个试次:隐约成形');
subplot(3, 1, 3); plot(t, mean(data, 2)); title('平均 100 个试次:ERP 浮出水面');
xlabel('时间 (s)'); ylabel('幅度 (\muV)');
二、从 epochs 到 ERP:维度发生了什么
模块 2 结束时,你手里的 EEG.data 是一个三维矩阵:
通道 × 时间点 × 试次(例如 64 × 501 × 80)。
所谓"算 ERP",就是沿第 3 维(试次维)求平均,得到一个
通道 × 时间的二维矩阵——试次维被平均掉了,剩下的就是
"这堆试次的典型反应"。
理解这一步的另一个好处:你会明白 ERP 是描述性的——它是平均的结果, 不是某一次"真实发生"的波形。单试次里信号长什么样,要靠 ERPimage(第六节)去看。
三、单被试 ERP:EEGLAB GUI 路径
加载预处理好的 .set 文件后,画 ERP 主要走 Plot 菜单下的三条路:
- 带地形图的波形:
Plot > Channel ERPs > With scalp maps——所有通道的 ERP 排成一页,附一张最大方差时刻的地形图;点任意通道的波形,可查看对应时刻的地形图; - 按头皮位置排波形:
Plot > Channel ERPs > In scalp array——每个通道的波形画在它所在的头皮位置上,一眼看出哪个区域在什么时候"动"了; - 一串时刻的地形图:
Plot > ERP map series > In 2-D——输入0:100:500这样的时刻列表,画出各时刻的地形图序列。
看图时盯住三件事:极性(正波还是负波)、潜伏期(波峰在刺激后多少毫秒)、 头皮分布(集中在哪个区域)。三件事合起来,才是一个 ERP 成分的"身份"。
(旧版本 EEGLAB 里这些菜单叫 Channel ERPs (popup),功能相同。)
四、单被试 ERP:脚本方式
GUI 适合看图,脚本适合复现。核心只有一行,剩下的都是画图:
% 前提:EEG 是模块 2 结束时的分段数据(已基线校正)
ERP = mean(EEG.data, 3); % 沿第 3 维(试次维)平均 -> 通道 x 时间
[~, chIdx] = ismember({'Pz'}, {EEG.chanlocs.labels}); % 找到 Pz 的通道编号
figure; plot(EEG.times, ERP(chIdx, :), 'LineWidth', 1.2); hold on;
xline(0, '--'); % 刺激 onset 参考线
xlabel('时间 (ms)'); ylabel('幅度 (\muV)'); title('单被试 Pz 的 ERP 波形');
% EEGLAB 自带的画 ERP 函数:GUI 中 Plot > Channel ERPs 的脚本等价形式
pop_erpplot(EEG, 1, [chIdx], 0); % 通道列表;0 表示不叠加标准差
想不起 pop_erpplot 的参数顺序?在 GUI 里点一遍 Plot > Channel ERPs,
再到命令行敲 eegh——你在界面里做的每一步都会变成一条可直接复制的命令
(模块 2.4 讲过的老技巧,永远好用)。
五、组平均 grand average:一条属于"这群人"的波形
单被试的 ERP 只是"这个人这一次实验"的结果,被试间的个体差异(头型、皮层折叠、 注意状态……)会原样留在波形里。grand average(组平均)先给每个被试算出 他自己的 ERP,再跨被试平均——个体间的随机差异像试次间的噪声一样被抵消, 剩下的是这群人的典型反应。论文结果图里那条粗线,通常就是它。
"平均的平均":先每人一条 ERP(试次级平均),再跨被试平均(被试级平均)。 两次平均对付的是两种不同的随机性——试次间的噪声,和被试间的个体差异。
%% 组平均:先算每个人的 ERP,再跨被试平均
clear; clc; close all;
dataDir = 'D:\project\processed\'; % 预处理输出目录(英文路径)
subs = {'sub01','sub02','sub03','sub04','sub05'};
erpAll = []; times = [];
for s = 1:numel(subs)
EEG = pop_loadset('filename', [subs{s} '_pp.set'], 'filepath', dataDir);
erp = mean(EEG.data, 3); % 这名被试的 ERP:通道 x 时间
if isempty(erpAll)
% 用第一名被试初始化尺寸与通道编号
[~, chIdx] = ismember({'Pz'}, {EEG.chanlocs.labels});
erpAll = zeros(numel(subs), numel(EEG.times));
times = EEG.times; % 预处理一致 -> 时间轴一致
end
erpAll(s, :) = erp(chIdx, :); % 收集 Pz 波形:每行一个被试
fprintf('%s:剩余 %d 个试次\n', subs{s}, EEG.trials); % 顺手做质控
end
GA = mean(erpAll, 1); % 跨被试平均 -> 组平均
figure; hold on;
plot(times, erpAll, 'Color', [0.75 0.75 0.75]); % 灰色:每个被试
plot(times, GA, 'r', 'LineWidth', 2); % 红色:组平均
xlabel('时间 (ms)'); ylabel('幅度 (\muV)');
title('灰色为单被试,红色为 grand average');
一是把所有被试的试次直接混在一起平均(伪被试):条件间试次数不等时结果被扭曲, 也做不了被试级统计;二是无视试次数差异:某人只剩 20 个试次、别人有 80 个, 他的 ERP 噪声更大,会把组平均"拉毛"。先看每人的试次数(上面的 fprintf 就是在做这件事),再进组平均。
六、ERPimage:把每个试次摊开看
平均是把双刃剑:它把噪声压下去的同时,也把试次间的差异抹掉了—— 而这些差异可能正是大脑实时工作的线索。ERPimage 把单个通道的所有试次 排成一张试次 × 时间的图像:每一行是一个试次的完整波形,颜色代表电位。 默认按采集顺序排列,也可以按反应时、窗口内幅值等变量排序。
GUI 路径:Plot > Channel ERP image(对应 pop_erpimage),
填通道号和 smoothing(相邻试次平滑宽度)即可;排序可以在弹窗里选事件字段(如反应时 rt)。
%% 手工画 ERPimage:imagesc 就够了
X = squeeze(EEG.data(chIdx, :, :))'; % 原为 1 x 时间 x 试次,转置成 试次 x 时间
figure;
subplot(2, 1, 1);
imagesc(EEG.times, 1:EEG.trials, X); % 每一行 = 一个试次的完整波形
set(gca, 'YDir', 'normal'); colorbar; axis tight;
xlabel('时间 (ms)'); ylabel('试次编号'); title('按采集顺序排列');
% 换一种排法:按 250-350ms 窗口内的平均幅值从小到大排序
subplot(2, 1, 2);
idxWin = EEG.times >= 250 & EEG.times <= 350;
[~, order] = sort(mean(X(:, idxWin), 2)); % 每个试次在窗口内的幅值
imagesc(EEG.times, 1:EEG.trials, X(order, :));
set(gca, 'YDir', 'normal'); colorbar; axis tight;
xlabel('时间 (ms)'); ylabel('试次(按幅值排序)'); title('同一份数据,按窗口幅值排序');
怎么读:竖着的色带 = 与刺激锁时的成分(所有试次同时变红/变蓝); 排序后出现的斜纹 = 成分的幅度或潜伏期随排序变量系统变化—— 这正是平均波形里看不到、却可能最有故事的信息。
七、地形图:电极值如何"铺满"头皮
电极只是头皮上的几十个采样点,像在草坪上插了几十根测湿度的探针。 地形图做的事,是用插值(球面样条)从这些离散点推算整个头皮表面的电位分布, 再用红(正)蓝(负)上色。它回答的问题是:某个时刻,电活动集中在头皮哪里?
% 画 300ms 时刻的地形图:给每个电极一个值
[~, tIdx] = min(abs(EEG.times - 300)); % 离 300ms 最近的时间点下标
vals = ERP(:, tIdx); % 所有电极在该时刻的平均电位 (uV)
figure;
topoplot(vals, EEG.chanlocs, ...
'maplimits', 'maxabs', ... % 颜色范围对称:红=正,蓝=负
'electrodes', 'labels', ... % 在头皮图上标出电极名
'style', 'both', ... % 填色 + 等高线
'numcontours', 6); % 等高线条数
title('300 ms 的头皮电位分布');
| 参数 | 作用 | 常用取值与说明 |
|---|---|---|
maplimits | 颜色范围 | [min max] 或 'maxabs'(对称);多张图对比时必须手动统一,否则颜色不可比 |
electrodes | 电极标注方式 | 'off' / 'labels' / 'numbers' |
style | 绘图风格 | 'both'(填色+等高线,最常用)/ 'straight' / 'fill' |
numcontours | 等高线条数 | 6–10;太多会显得杂乱 |
GUI 里画一串时刻的地形图用 Plot > ERP map series > In 2-D。
注意地形图的前置条件是电极定位正确(模块 2.2)——没有坐标,插值无从谈起。
八、本节交付物
一张单被试 ERP 波形图(Pz,带 0ms 参考线)、一张组平均图 (灰色单被试 + 红色 grand average)、一张300ms 地形图。 三张图齐了,你的数据才算"算得出、画得好"——下一课开始"测得准"。
📌 本节小结
- 平均的逻辑:信号随刺激锁时出现、噪声随机起伏,平均后噪声按 1/√N 缩小
ERP = mean(EEG.data, 3):试次维被平均掉,三维 epochs 变成二维波形- grand average 是"先每人一条 ERP,再跨被试平均";切勿把所有人的试次混在一起
- ERPimage 是平均的"背面":用排序暴露试次间的差异,检查平均掩盖了什么
- 地形图 = 电极采样值在头皮表面的插值;多张图对比时必须统一色标
✏️ 课后练习
- 把模拟代码里的试次数依次改成 25 / 100 / 400,各画一张平均波形;量一量基线期(0ms 之前)的标准差,验证噪声是否按 1/√N 缩小。
- 对
lesson23_preprocessed.set计算单被试 ERP:画 Pz 波形图(带 0ms 参考线)和 300ms 地形图,分别保存为 PNG。 - 用 ERPimage 把同一通道的试次按 250-350ms 幅值排序,与未排序版本对比,写一两句你看到了什么。