FieldTrip 安装与时域 / 频域 / 时频分析
在学术界另一大主流框架里把整个流程重做一遍:没有菜单、没有主界面, 一切靠一个 cfg 和一个 data——这是通往论文级统计(下一课)的必经之路。
🎯 本节学习目标
- 说清 FieldTrip 与 EEGLAB 的定位差异,知道什么任务值得切到 FieldTrip
- 独立完成安装与路径配置,理解为什么入口是
ft_defaults而不是addpath(genpath(...)) - 讲得出 data 结构五个核心字段的含义:
trial / label / time / fsample / trialinfo - 会用
ft_definetrial+ft_preprocessing+ft_rejectartifact完成从原始文件到干净试次的流水线 - 能在同一份数据上完成时域(ERP)、频域(功率谱)、时频(TFR)三类分析并正确绘图,再扩展到多被试批处理
一、FieldTrip 是什么:另一条主流路线
模块 2 里你已经用 EEGLAB 把预处理流水线跑通了;这一课换一套装备: FieldTrip——荷兰 Donders 研究所 Robert Oostenveld 团队维护的开源 Matlab 工具包, 与 EEGLAB 并称脑电/脑磁图分析的两大事实标准。你读到的 MEG/EEG 论文, 方法部分出现频率最高的两个词,基本就是这两个。
两者最核心的差别一句话就能说清:EEGLAB 是"菜单优先",FieldTrip 是"脚本优先"。 FieldTrip 没有主界面、没有菜单树,从第一天起就要写代码——听起来更劝退, 但换来的正是科研最需要的能力:批处理、版本管理、论文复现,全都天然成立。
本课的目标也很直接:把你在模块 2、3、4 做过的整条流水线——定义试次、预处理、ERP、功率谱、时频—— 在 FieldTrip 里用脚本重做一遍。换一个框架做同一件事,你对流程本身的理解会深一层。
二、EEGLAB vs FieldTrip:两种设计哲学
两个工具包解决的问题高度重叠,但设计哲学几乎相反:
| 维度 | EEGLAB | FieldTrip |
|---|---|---|
| 交互方式 | GUI 优先;菜单点完可用 eegh 回放成脚本 | 纯脚本;没有主界面,一切靠配置结构 cfg |
| 数据组织 | 一个 EEG 结构体,GUI 围着它转 | 一个 data 结构走天下:函数吃 cfg、吐 data |
| 学习曲线 | 上手快,一周能点通全流程 | 入门慢,但学会一个函数就等于学会所有函数 |
| 强项 | ICA 生态成熟、插件多、社区大 | 统计(下一课的 cluster 置换检验)、时频、溯源深度最强 |
| 典型用法 | 预处理、探索性浏览 | 批量分析、论文级统计 |
两者的数据可以互转(EEGLAB 的 File > Export,或转换函数 eeglab2fieldtrip)。
很多论文的流水线正是:EEGLAB 做 ICA 去伪迹,再转到 FieldTrip 做时频与统计——各取所长,不必二选一。
三、安装与路径配置:ft_defaults 是唯一正确入口
- 到官网
fieldtriptoolbox.org的 download 页面下载压缩包(release 稳定版); - 解压到纯英文、无空格路径,例如
D:\fieldtrip; - 在 Matlab 中只把主目录加入路径:
addpath('D:\fieldtrip'); - 执行
ft_defaults——它会自动把需要的子目录补进路径并设置默认行为; - 验证:输入
ft_version,能正常打印版本号即安装成功。
每次重启 Matlab 后重新执行一遍 ft_defaults 即可;写进 startup.m 可以一劳永逸。
FieldTrip 仓库有几百个子目录,其中捆绑的 SPM、各种工具函数可能与你已安装的其他工具包同名冲突——症状是"函数行为诡异""变量莫名消失",极难排查。官方 FAQ 的建议非常明确:只 addpath 主目录,剩下的事交给 ft_defaults。
四、解剖 FieldTrip 的数据结构:一个 data 走天下
FieldTrip 的所有分析函数吃的、吐的都是同一种结构,习惯上叫 data。
一份分段好的 EEG 数据长这样:
% 假设 data 是 ft_preprocessing 的输出,先整体看一眼
data % 直接打印结构体
data.trial % 元胞数组:1 个元胞 = 1 个试次
data.trial{1} % 第 1 个试次的矩阵:通道 x 时间点,例如 64 x 1000
data.label % Nx1 元胞数组:通道名,如 Fp1、Cz、O2
data.time % 1x1 元胞数组:时间轴(单位是秒),对齐后一般从 -0.5 开始
data.fsample % 采样率,例如 500 (Hz)
data.trialinfo % 每个试次一行的附加信息:条件编号、反应时等
% 三个自检动作(拿到任何新数据都先做)
size(data.trial{1}) % 通道数 x 每试次采样点数,与采集记录核对
numel(data.trial) % 试次总数
unique(data.trialinfo(:,1)) % 出现过哪些条件编号,例如 [1; 2]
FieldTrip 每个函数都是同一个样子:结果 = ft_函数名(cfg, 输入数据)。先 cfg = [] 清空,再逐项赋值,最后连同数据一起喂进去。学 FieldTrip 就是学"每件事该配哪些字段",忘了就 help ft_函数名。
特别注意 data.trial 是元胞数组:FieldTrip 允许各试次长度不同
(比如按反应时分段时),所以一个试次存一个矩阵。这与 EEGLAB"所有试次等长、拼成一个大矩阵"
是两种取舍——前者灵活,后者紧凑。
五、定义试次与预处理:ft_definetrial 与 ft_preprocessing
FieldTrip 读数据的套路是两步走:先用 ft_definetrial 从事件表算出一张试次表
(此时尚未读数据),再让 ft_preprocessing 按表把数据读进来、顺手做完滤波等处理。
两步共用同一个 cfg——这正是官方教程的标准写法:
%% 第 1 步:定义试次(只算试次表,还没读数据)
cfg = [];
cfg.dataset = 'D:\project\sub01.vhdr'; % 原始数据(英文路径!)
cfg.trialfun = 'ft_trialfun_general'; % 通用试次定义函数
cfg.trialdef.prestim = 0.5; % 事件前 0.5 秒
cfg.trialdef.poststim = 1.5; % 事件后 1.5 秒
cfg.trialdef.eventtype = 'Stimulus'; % 事件类型名(取决于采集软件)
cfg.trialdef.eventvalue = [1 2]; % 触发值:1=条件A,2=条件B
cfg = ft_definetrial(cfg); % 注意:输出还是 cfg,trl 已填好
cfg.trl(1:3, :) % 每行三列:[起始采样点 结束采样点 事件偏移]
%% 第 2 步:读入数据并完成预处理(继续往同一个 cfg 上加)
cfg.channel = {'EEG'}; % 只读脑电通道,自动排除 EOG/ECG
cfg.hpfilter = 'yes'; cfg.hpfreq = 0.1; % 高通 0.1 Hz:去慢漂移(理由同 2.3)
cfg.lpfilter = 'yes'; cfg.lpfreq = 45; % 低通 45 Hz:挡肌电、留 ERP 主能量
cfg.dftfilter = 'yes'; cfg.dftfreq = 50; % 50 Hz 工频陷波
cfg.reref = 'yes'; cfg.refchannel = {'all'}; % all = 平均参考
cfg.demean = 'yes'; cfg.baselinewindow = [-0.5 0]; % 基线校正(单位秒)
cfg.padding = 1; % 滤波时两侧各补 1 秒,压制边缘振铃
data = ft_preprocessing(cfg); % 一口气:读数据 + 上面全部处理
cfg.trl 的三列是理解 FieldTrip 时间概念的关键:
- 第 1、2 列:试次在连续记录中的起止采样点(是编号,不是秒);
- 第 3 列 offset:事件落在试次内第几个采样点。0.5 秒基线、500 Hz 采样时它等于 250——时间轴靠它换算,刺激开始 = 第 0 秒;
- 剔除某个试次只需删掉 trl 的对应行,原始数据文件永远不被改动。
如果试次要按"刺激类型 × 反应正误"交叉定义,通用的 ft_trialfun_general 不够用了:复制官方模板写一个 my_trialfun(函数里自己读事件、自己拼 trl),然后 cfg.trialfun = 'my_trialfun'。这是 FieldTrip 进阶的必经之路,好在模板非常短。
六、伪迹拒绝:ft_rejectartifact 的两副面孔
模块 2.3 你在 EEGLAB 里做过视觉拒绝;FieldTrip 把这件事拆成检测与拒绝两层, 对应两副面孔:
- 数值阈值(自动、可复现、能进批处理):检测方法由
cfg.artfctdef.type指定,各方法的参数写在cfg.artfctdef.方法名下面,ft_rejectartifact一并完成检测与剔除; - 视觉拒绝(交互、适合小数据集和抽查):用配套的
ft_rejectvisual浏览每个试次、手工点选。
% 面孔一:数值阈值(zvalue 法)——把数据先 z 变换,超过 4 个标准差算伪迹
cfg = [];
cfg.artfctdef.type = {'zvalue'}; % 用哪些检测方法,可叠加多种
cfg.artfctdef.zvalue.channel = {'EEG'}; % 在哪些通道上检测
cfg.artfctdef.zvalue.cutoff = 4; % 超过 4 个标准差视为伪迹
data_clean = ft_rejectartifact(cfg, data); % 默认整试次剔除(reject='complete')
% 好习惯:把拒绝率打出来,写进实验日志
fprintf('剔除 %d / %d 个试次\n', ...
numel(data.trial) - numel(data_clean.trial), numel(data.trial));
% 面孔二:视觉拒绝——交互浏览、手工点选(小数据集与抽查用)
cfg = [];
cfg.method = 'summary'; % 汇总指标视图;也可选 'trial' / 'channel'
cfg.channel = 'EEG';
data_clean = ft_rejectvisual(cfg, data); % 弹出浏览窗口,勾选要删的试次/通道
数值阈值的好处是规则先于数据确定:z 分数阈值不依赖数据单位,换一台放大器、 换一批被试,规则照样成立。视觉拒绝更聪明,但不可复现——论文里通常两者结合: 数值阈值做主力,视觉只用来抽查数值法的漏网之鱼。
七、时域分析:ft_timelockanalysis 与 ERP 绘图三件套
有了干净的 data,算 ERP 只需要一个 ft_timelockanalysis:
它对选定试次逐点求平均,顺手管理方差等统计时用得上的量。
用 cfg.trials 配合 data.trialinfo 按条件分别平均——
这是 FieldTrip 里最常用的条件选择方式:
% 1) 时域分析:按条件分别平均
cfg = [];
cfg.trials = find(data.trialinfo(:,1) == 1); % 条件 A 的试次编号
tlk_A = ft_timelockanalysis(cfg, data);
cfg = [];
cfg.trials = find(data.trialinfo(:,1) == 2); % 条件 B
tlk_B = ft_timelockanalysis(cfg, data);
% 输出结构:tlk_A.avg(通道x时间的平均)、tlk_A.var、tlk_A.time、tlk_A.label
% 2) ERP 绘图三件套
cfg = [];
cfg.channel = 'Cz'; % 指定一个通道
cfg.xlim = [-0.5 1.5];
ft_singleplotER(cfg, tlk_A); % 单通道 ERP 曲线:论文主图的高频操作
cfg = [];
cfg.layout = 'elec1005'; % 标准 10-05 系统电极布局(EEG 通用)
ft_multiplotER(cfg, tlk_A); % 全通道阵列图:一眼看全局形态
cfg = [];
cfg.layout = 'elec1005';
cfg.xlim = [0.3 0.5]; % 显示 300-500 ms 的时间窗(如 P3 窗口)
cfg.zlim = 'maxabs';
cfg.colorbar = 'yes';
ft_topoplotER(cfg, tlk_A); % 头皮地形图
三件套分工明确:ft_singleplotER 画单通道波形,
ft_multiplotER 把全部通道排成一版阵列图(快速总览),
ft_topoplotER 画指定时间窗的头皮分布。
后两者需要电极布局模板 cfg.layout,标准 10-05 系统 EEG 用 elec1005 即可。
八、频域与时频分析:ft_freqanalysis 的两副面孔
模块 4 你手写过滑动窗时频;在 FieldTrip 里这件事由 ft_freqanalysis 统一负责,
靠 cfg.method 切换两副面孔:mtmfft 对整个试次做一次多窗傅里叶变换
(得到功率谱,没有时间维度);mtmconvol 用滑动窗逐点计算(得到时频表示 TFR)。
想用模块 4 讲过的 Morlet 小波,换成 cfg.method = 'wavelet' 即可,思路完全一样。
%% 面孔一:mtmfft 算功率谱(PSD)——整段试次一次变换
cfg = [];
cfg.method = 'mtmfft'; % 多窗 FFT
cfg.output = 'pow'; % 输出功率
cfg.taper = 'dpss'; % 多窗(Slepian 序列),比单窗谱更平滑
cfg.foilim = [1 45]; % 1-45 Hz
cfg.tapsmofrq = 2; % 谱平滑带宽 2 Hz:平滑与分辨率的交换
cfg.trials = find(data.trialinfo(:,1) == 1);
freqA = ft_freqanalysis(cfg, data);
% 输出:freqA.powspctrm(通道x频率)、freqA.freq
% 画功率谱:x 轴是频率(复用了 TFR 绘图函数)
cfg = [];
cfg.channel = 'Cz';
cfg.xlim = [1 45]; % 这里 xlim 指频率范围
ft_singleplotTFR(cfg, freqA);
%% 面孔二:mtmconvol 滑动窗算时频(TFR)
cfg = [];
cfg.method = 'mtmconvol'; % 多窗卷积时频
cfg.output = 'pow';
cfg.foi = 2:1:40; % 感兴趣频率
cfg.t_ftimwin = 5 ./ cfg.foi; % 窗长随频率变:每个频率取 5 个周期
cfg.tapsmofrq = 0.4 .* cfg.foi; % 平滑带宽随频率变
cfg.toi = -0.5:0.05:1.5; % 时间中心点,50 ms 一步
cfg.trials = find(data.trialinfo(:,1) == 1);
tfrA = ft_freqanalysis(cfg, data);
% 输出:tfrA.powspctrm(通道x频率x时间)
% 画时频图:单通道的"热图"
cfg = [];
cfg.channel = 'Cz';
cfg.xlim = [-0.5 1.5]; % 时间轴
cfg.ylim = [2 40]; % 频率轴
cfg.zlim = 'maxabs';
ft_singleplotTFR(cfg, tfrA);
% 画时频地形图:某个时间-频率窗的空间分布
cfg = [];
cfg.layout = 'elec1005';
cfg.xlim = [0.3 0.8]; % 时间窗
cfg.ylim = [8 13]; % 频率窗:alpha 频段
ft_topoplotTFR(cfg, tfrA);
单窗频谱噪声很大(想想模块 4 学 Welch 的动机)。多窗法用一组 DPSS(Slepian)窗各算一次谱再平均,得到更稳定的估计,代价是频率分辨率下降。带宽由 cfg.tapsmofrq 控制:4 Hz 平滑指 ±4 Hz——平滑与分辨率永远在做交换。
窗长怎么选?cfg.t_ftimwin = 5 ./ cfg.foi 这个经验式给每个频率分配 5 个周期的窗:
10 Hz 时窗长 0.5 秒、40 Hz 时 0.125 秒——低频要长窗保分辨率、高频用短窗保时间定位,
这正是模块 4 讲过的时频不确定性原理,FieldTrip 用一行 cfg 就把它实现了。
九、批处理思路:从单个被试到整个课题组
FieldTrip 本来就是脚本框架,批处理不需要任何额外技巧——把模块 2.4 学到的 "参数区 + 被试循环 + 结果集中保存"原样搬过来即可。每个被试跑完的 timelock(以及按需的 TFR)结构存进元胞数组(对不等长字段最友好), 最后一次性保存。这些按被试、按条件保存的结构,正是下一课统计的直接输入。
%% ============ 参数区:只改这里 ============
subjects = {'sub01','sub02','sub03'}; % 被试列表
dataDir = 'D:\project\raw\'; % 原始数据目录(英文路径!)
outDir = 'D:\project\ft_result\'; % 输出目录
conds = [1 2]; % 两个条件的触发值
if ~exist(outDir, 'dir'); mkdir(outDir); end
tlkA = cell(1, numel(subjects)); % 每被试每条件一个 timelock 结构
tlkB = cell(1, numel(subjects));
%% ============ 循环区:所有被试同一流程 ============
for k = 1:numel(subjects)
fprintf('==== [%d/%d] %s ====\n', k, numel(subjects), subjects{k});
% --- 试次定义 + 预处理(每人重配一份新 cfg,防止残留字段捣乱)---
cfg = [];
cfg.dataset = [dataDir subjects{k} '.vhdr'];
cfg.trialfun = 'ft_trialfun_general';
cfg.trialdef.prestim = 0.5;
cfg.trialdef.poststim = 1.5;
cfg.trialdef.eventvalue = conds;
cfg = ft_definetrial(cfg);
cfg.channel = {'EEG'};
cfg.hpfilter = 'yes'; cfg.hpfreq = 0.1;
cfg.lpfilter = 'yes'; cfg.lpfreq = 45;
cfg.reref = 'yes'; cfg.refchannel = {'all'};
cfg.demean = 'yes'; cfg.baselinewindow = [-0.5 0];
data = ft_preprocessing(cfg);
% --- 伪迹拒绝(数值阈值,可复现)---
cfg = [];
cfg.artfctdef.type = {'zvalue'};
cfg.artfctdef.zvalue.channel = {'EEG'};
cfg.artfctdef.zvalue.cutoff = 4;
data = ft_rejectartifact(cfg, data);
% --- 按条件分别算 ERP ---
cfg = []; cfg.trials = find(data.trialinfo(:,1) == conds(1));
tlkA{k} = ft_timelockanalysis(cfg, data);
cfg = []; cfg.trials = find(data.trialinfo(:,1) == conds(2));
tlkB{k} = ft_timelockanalysis(cfg, data);
nA = sum(data.trialinfo(:,1) == conds(1));
nB = sum(data.trialinfo(:,1) == conds(2));
fprintf(' 保留试次:A %d 个,B %d 个\n', nA, nB);
end
%% ============ 保存:5.2 统计的直接输入 ============
save(fullfile(outDir, 'timelock_all.mat'), 'subjects', 'tlkA', 'tlkB');
和模块 2.4 一样:先在 1 个被试上跑通全流程,确认输出无误后再放全量;
数据量大时同样建议给循环套一层 try-catch,让单个被试的失败不中断整批。
每个被试的试次保留数要随手看一眼——保留率低于七成的被试值得回去检查原始数据。
十、本课检查清单
ft_version正常输出,且没有用genpath加过路径;- data 五个核心字段(trial / label / time / fsample / trialinfo)各自存什么,能脱口而出;
- cfg.trl 三列的含义说得清,知道 offset 如何决定时间轴的 0 点;
- 滤波、陷波、去均值、平均参考都能用
ft_preprocessing的 cfg 表达出来; - ERP 三件套与功率谱、时频图都亲手画出来过,layout 用的是标准模板;
- 批处理先在 1 个被试上跑通,确认输出无误后再放全量数据。
一批按被试、按条件保存的 timelock(必要时加上 TFR)结构。下一课我们就在它们之上做 cluster-based 置换检验——你会发现统计的 cfg 和本课的分析 cfg 长得一模一样。
📌 本节小结
- FieldTrip 的范式:
cfg = []起手、逐项赋值、函数带 cfg 调用——学会一个函数就等于学会所有函数 - 安装只 addpath 主目录 +
ft_defaults;genpath 会带来同名函数冲突的诡异 bug cfg.trl三列 = 起始采样点、结束采样点、事件偏移;offset 决定时间轴的 0 点- 三类分析一个套路:
ft_timelockanalysis管 ERP,ft_freqanalysis的 mtmfft 管功率谱、mtmconvol 管时频 - 批处理 = 被试循环 + 元胞数组保存每被试结果;这些结构就是 5.2 统计的直接输入
✏️ 课后练习
- 完成 FieldTrip 安装,运行
ft_version验证,再用which ft_preprocessing确认函数来自 FieldTrip 主目录。 - 对一份公开数据跑通 definetrial → preprocessing → rejectartifact → timelockanalysis 全流程,保存 data 与 timelock 两个 .mat,各出一张图。
- 把上面的流程改写成 3 个被试的批处理循环,保存每人 timelock,最后把各被试 Cz 电极的 ERP 曲线画到同一张图上对比。