阶段三 · 工具进阶 · 模块 5 · 第 13 课 / 共 28 课

FieldTrip 安装与时域 / 频域 / 时频分析

在学术界另一大主流框架里把整个流程重做一遍:没有菜单、没有主界面, 一切靠一个 cfg 和一个 data——这是通往论文级统计(下一课)的必经之路。

🕐 75 分钟🎯 难度:进阶✅ 前置:模块 2、4

🎯 本节学习目标

  • 说清 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:两种设计哲学

两个工具包解决的问题高度重叠,但设计哲学几乎相反:

维度EEGLABFieldTrip
交互方式GUI 优先;菜单点完可用 eegh 回放成脚本纯脚本;没有主界面,一切靠配置结构 cfg
数据组织一个 EEG 结构体,GUI 围着它转一个 data 结构走天下:函数吃 cfg、吐 data
学习曲线上手快,一周能点通全流程入门慢,但学会一个函数就等于学会所有函数
强项ICA 生态成熟、插件多、社区大统计(下一课的 cluster 置换检验)、时频、溯源深度最强
典型用法预处理、探索性浏览批量分析、论文级统计

两者的数据可以互转(EEGLAB 的 File > Export,或转换函数 eeglab2fieldtrip)。 很多论文的流水线正是:EEGLAB 做 ICA 去伪迹,再转到 FieldTrip 做时频与统计——各取所长,不必二选一。

三、安装与路径配置:ft_defaults 是唯一正确入口

  1. 到官网 fieldtriptoolbox.org 的 download 页面下载压缩包(release 稳定版);
  2. 解压到纯英文、无空格路径,例如 D:\fieldtrip
  3. 在 Matlab 中只把主目录加入路径:addpath('D:\fieldtrip')
  4. 执行 ft_defaults——它会自动把需要的子目录补进路径并设置默认行为;
  5. 验证:输入 ft_version,能正常打印版本号即安装成功。

每次重启 Matlab 后重新执行一遍 ft_defaults 即可;写进 startup.m 可以一劳永逸。

⚠️ 千万不要 addpath(genpath(...))

FieldTrip 仓库有几百个子目录,其中捆绑的 SPM、各种工具函数可能与你已安装的其他工具包同名冲突——症状是"函数行为诡异""变量莫名消失",极难排查。官方 FAQ 的建议非常明确:只 addpath 主目录,剩下的事交给 ft_defaults

四、解剖 FieldTrip 的数据结构:一个 data 走天下

FieldTrip 的所有分析函数吃的、吐的都是同一种结构,习惯上叫 data。 一份分段好的 EEG 数据长这样:

Matlab
% 假设 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]
📖 概念:cfg —— FieldTrip 的万能遥控器

FieldTrip 每个函数都是同一个样子:结果 = ft_函数名(cfg, 输入数据)。先 cfg = [] 清空,再逐项赋值,最后连同数据一起喂进去。学 FieldTrip 就是学"每件事该配哪些字段",忘了就 help ft_函数名

特别注意 data.trial元胞数组:FieldTrip 允许各试次长度不同 (比如按反应时分段时),所以一个试次存一个矩阵。这与 EEGLAB"所有试次等长、拼成一个大矩阵" 是两种取舍——前者灵活,后者紧凑。

五、定义试次与预处理:ft_definetrial 与 ft_preprocessing

FieldTrip 读数据的套路是两步走:先用 ft_definetrial 从事件表算出一张试次表 (此时尚未读数据),再让 ft_preprocessing 按表把数据读进来、顺手做完滤波等处理。 两步共用同一个 cfg——这正是官方教程的标准写法:

Matlab
%% 第 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 的对应行,原始数据文件永远不被改动。
💡 范式复杂?写自己的 trialfun

如果试次要按"刺激类型 × 反应正误"交叉定义,通用的 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 浏览每个试次、手工点选。
Matlab
% 面孔一:数值阈值(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 里最常用的条件选择方式:

Matlab
% 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' 即可,思路完全一样。

Matlab
%% 面孔一: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);
📖 概念:多窗(multitaper)估计

单窗频谱噪声很大(想想模块 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)结构存进元胞数组(对不等长字段最友好), 最后一次性保存。这些按被试、按条件保存的结构,正是下一课统计的直接输入。

Matlab · ft_batch.m
%% ============ 参数区:只改这里 ============
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,让单个被试的失败不中断整批。 每个被试的试次保留数要随手看一眼——保留率低于七成的被试值得回去检查原始数据。

十、本课检查清单

  1. ft_version 正常输出,且没有用 genpath 加过路径;
  2. data 五个核心字段(trial / label / time / fsample / trialinfo)各自存什么,能脱口而出;
  3. cfg.trl 三列的含义说得清,知道 offset 如何决定时间轴的 0 点;
  4. 滤波、陷波、去均值、平均参考都能用 ft_preprocessing 的 cfg 表达出来;
  5. ERP 三件套与功率谱、时频图都亲手画出来过,layout 用的是标准模板;
  6. 批处理先在 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 统计的直接输入

✏️ 课后练习

  1. 完成 FieldTrip 安装,运行 ft_version 验证,再用 which ft_preprocessing 确认函数来自 FieldTrip 主目录。
  2. 对一份公开数据跑通 definetrial → preprocessing → rejectartifact → timelockanalysis 全流程,保存 data 与 timelock 两个 .mat,各出一张图。
  3. 把上面的流程改写成 3 个被试的批处理循环,保存每人 timelock,最后把各被试 Cz 电极的 ERP 曲线画到同一张图上对比。