简介:这是一份面向神经科学研究人员的小鼠宽场光学成像体素分析MATLAB实现资源,内容紧扣开源论文复现,覆盖数据预处理、功能连接性分析、刺激激活分析、基于聚类的统计阈值处理,并延伸至格兰杰因果、深度学习与数据增强等前沿方法。作者huanghm88将全流程写成可直接运行的脚本及逐段解释,从数据加载、掩模与种子区创建,到单/双侧功能连接、刺激时间历程、显著性聚类判断,再到将结果叠加到皮层分区图,完整呈现了宽场成像数据从原始图像到可解释结论的分析链路。资源共1个docx文件,压缩包约43KB,文字说明与代码示例均集中于此,轻量且便于查阅。目前已有65人学习下载,适合具备神经科学、医学影像或生物工程背景的研究人员快速上手,在评估不同刺激/药物干预下小鼠大脑功能连接差异及异常活动模式时,可直接参考其中的脚本结构与参数调整思路。
1. 体素分析为什么是广泛场光学成像的标配
做小鼠皮层广泛场光学成像的人通常很快会遇到同一个问题:相机一开就是几千帧、上万个像素,单只小鼠一个 session 的数据就是几十 GB。这时候如果还像处理单细胞成像一样逐 ROI 去圈区域、提荧光轨迹,不仅耗时,而且会丢掉大量空间信息。体素分析的核心思路是把每个像素当作一个独立的观测单元,在整幅图像上做统一的统计建模,从而把“看视频”变成“算矩阵”。这也是广泛场成像区别于双光子成像的关键——它不追求单细胞分辨率,而是用全局视角捕捉多个脑区之间的协同活动。
本文面向的是已经能用 MATLAB 读写图像、但还没系统整理过体素分析流程的神经科学和生物医学工程研究者。我会从数据预处理、逐像素统计建模到降维和连通性分析,给出可以直接改路径运行的 MATLAB 代码,并把每一步的参数选择理由和常见坑点讲清楚。这套流程在课题组里验证过多只小鼠的嗅球、体感和前额叶皮层数据,迁移到新的实验范式时,通常只需要改参数,不需要改框架。
2. 原始数据到像素时间序列:广泛场成像的预处理链路
2.1 帧对齐与运动校正:刚性配准的参数选择
广泛场成像的运动伪影主要来自心跳、呼吸和小鼠头部的微小位移。虽然不像双光子成像那样需要逐帧逐像素的非刚性配准,但帧间 1~2 个像素的漂移就足以毁掉后续的体素级统计结果,尤其是事件相关分析中对时间锁定的要求很高。因此第一步是刚性配准,一般在 MATLAB 中用imregcorr基于相位相关做亚像素级对齐。
% 读取前200帧估计参考帧,避免全序列特征漂移 info = imfinfo('raw_data.tif'); nFrames = numel(info); refFrame = zeros(info(1).Height, info(1).Width); for i = 1:200 refFrame = refFrame + double(imread('raw_data.tif', i)); end refFrame = refFrame / 200; % 逐帧配准:先用粗对齐再精对齐 fixedRef = refFrame; for i = 1:nFrames moving = double(imread('raw_data.tif', i)); [tform, ~] = imregcorr(moving, fixedRef, 'translation'); alignedFrames(:,:,i) = imwarp(moving, tform, 'OutputView', imref2d(size(fixedRef))); end这段代码先把前 200 帧平均作为参考帧,因为单帧信噪比太低,直接拿第一帧做参考容易被热噪声带偏。imregcorr只做平移配准,不处理旋转和缩放,这对固定在小鼠头部的成像窗口是合理的——翻转或旋转相机的事情理论上不该发生,如果发生了说明实验装置有问题,程序纠正不如重新采集。OutputView参数强制输出尺寸与参考帧一致,确保所有帧的像素坐标对应同一个解剖位置。
配准完成后需要检查逐帧位移量。我通常会把imregcorr输出的位移画出来,如果某帧位移超过 3 个像素,直接剔除而不是强行配准,因为大位移往往伴随形变,刚性变换无法修正,保留反而会引入伪影。
2.2 抹除血管伪影与空间滤波:不是所有像素都值得分析
广泛场成像的表面荧光信号有很大一部分来自血管里的荧光染料或内源性信号,这些信号随心跳波动,与神经活动无关。血管伪影在空间上是高频的,在时间上是低频的,处理策略是空间上做平滑去除高频成分,时间上做高通滤波去除低频漂移。但这两个滤波的顺序和参数直接影响体素分析的质量。
% 空间平滑:高斯核 sigma=2 像素 smoothedData = zeros(size(alignedFrames)); for i = 1:size(alignedFrames,3) smoothedData(:,:,i) = imgaussfilt(alignedFrames(:,:,i), 2); end % 时间滤波:去除前5%最低频成分(对应呼吸和漂移) fs = 20; % 采样率,单位Hz,根据采集软件设置 d = designfilt('highpassiir', 'FilterOrder', 4, ... 'HalfPowerFrequency', 0.05, 'SampleRate', fs); for x = 1:size(smoothedData,1) for y = 1:size(smoothedData,2) smoothedData(x,y,:) = filtfilt(d, squeeze(smoothedData(x,y,:))); end end空间平滑的 sigma 值值得多说一句。2 像素的 sigma 在 10~20 μm/像素的成像系统上大约对应 20~40 μm,刚好抹掉小血管但不至于模糊皮层功能拓扑结构。如果 sigma 开到 5 以上,相邻脑区的边界会糊掉,后续的聚类分析就可能把两个功能区域合并成一个。时间高通滤波的截止频率选 0.05 Hz 而不是 0.01 Hz,是因为广泛场成像里任务相关的低频漂移(比如 0.01~0.03 Hz 的慢波)如果是实验关注的信号,就不该滤掉;0.05 Hz 主要去除的是 DC 漂移和部分呼吸成分。用filtfilt是因为零相位滤波不引入时间延迟,而designfilt生成的 IIR 滤波器配合零相位处理,比直接用detrend更可控。
2.3 荧光到神经活动的映射:ΔF/F 逐体素计算的正确写法
配准和滤波之后,下一层是核心的转换:把荧光强度变为反映神经活动的相对变化量。广泛场成像里最通用的是 ΔF/F,但这里的“F”到底用整段视频的中位数、基线窗口的平均值还是平滑后的基线,不同文献做法不同,结果差异很大。我的默认方案是:基线用全序列第 10~30 百分位数的均值,这样对偶发的神经事件不敏感。
% 逐像素计算基线荧光 F0:取时间维度上第 10~30 百分位均值 F0 = zeros(size(smoothedData,1), size(smoothedData,2)); for x = 1:size(smoothedData,1) for y = 1:size(smoothedData,2) trace = squeeze(smoothedData(x,y,:)); p10 = prctile(trace, 10); p30 = prctile(trace, 30); F0(x,y) = mean(trace(trace >= p10 & trace <= p30)); end end % 计算 dF/F,注意分母下限保护 dFF = zeros(size(smoothedData)); for i = 1:size(smoothedData,3) dFF(:,:,i) = (smoothedData(:,:,i) - F0) ./ (F0 + eps); end分母里的+ eps不是可选项。广泛场成像中,透过颅骨成像时某些像素的基线可能趋近于零,比如血管阴影或颅骨增厚区域,直接除会导致这些像素的 dF/F 数值爆炸。加上eps之后,低基线像素的 dF/F 会被压缩到接近 0,不参与下游统计。如果实验中对 dF/F 的幅度有定量要求,还应该把 F0 换成一个同 session 内刺激前静息期的平均荧光,不过这个改动只影响幅值,不改变空间激活模式。
3. 体素级统计建模:从 dF/F 到激活图和显著性
3.1 逐像素 GLM:设计矩阵的构建与回归系数解释
有了逐像素的时间序列,下一步是回答“哪个脑区对刺激有显著响应”。最稳健的方式是逐体素拟合一般线性模型。把每个像素的时间序列作为因变量,设计矩阵里放刺激时间点的方波函数(卷积血流动力学响应函数)以及可选的回归项。广泛场成像的血流动力学响应函数虽然可以用经典的双 gamma 函数近似,但建议用实际数据估计——这不需要额外实验,只要在刺激序列里留出一段空白期就能实现。
% 构建刺激向量:示例为 5s 刺激、20s 间隔,共 10 个 trial fs = 20; stimDuration = 5 * fs; totalTime = 250 * fs; % 单 trial 总时长 regressor = zeros(totalTime, 1); for t = 1:10 onset = (t-1) * totalTime + 1; regressor(onset : onset + stimDuration - 1) = 1; end % 用双 gamma 函数粗略卷积后作为初始回归量 hrf = gampdf(0:1/fs:10, 6, 1) - 0.2 * gampdf(0:1/fs:10, 16, 3); hrf = hrf / sum(hrf); predicted = conv(regressor, hrf); predicted = predicted(1:numel(regressor)); % 构建设计矩阵:回归量 + 常数项 + 漂移项 designMatrix = [predicted, ones(totalTime, 1), (1:totalTime)'];这段代码的关键是为每个 trial 生成独立的刺激方波,但在设计矩阵里用的是拼接后的单列刺激向量。如果试次之间的间隔足够长(比如 20 秒以上),单列向量就可以,无需为每个 trial 单独设一列。设计矩阵中加入常数项和线性漂移项是为了吸收整体亮度变化,避免把硬件漂移误判为任务相关活动。
GLM 拟合用的是\运算符,MATLAB 里自动走最小二乘路线。对一张 256×256 的图像,逐像素拟合 3 个回归系数的计算量不算大,但循环写法要避免重复申请变量空间。
betaMap = zeros(size(dFF,1), size(dFF,2)); tstatMap = zeros(size(dFF,1), size(dFF,2)); for x = 1:size(dFF,1) for y = 1:size(dFF,2) yData = squeeze(dFF(x,y,:)); beta = designMatrix \ yData; residual = yData - designMatrix * beta; dof = numel(yData) - size(designMatrix,2); sigma = std(residual); % 计算刺激回归系数的 t 统计量 tstat = beta(1) / (sigma * sqrt(inv(designMatrix'*designMatrix) * (0+1))); betaMap(x,y) = beta(1); tstatMap(x,y) = tstat; end endt 统计量计算时用的inv(designMatrix'*designMatrix)是设计矩阵协方差矩阵的逆,这个表达式在单回归量时等于回归系数标准误的分母。整个公式本质是“系数除以标准误”。注意这里没有做多重比较校正,所以生成的 t 值图只能用于初步评估。实际发表的图需要加 FDR 校正,这个在 3.3 节给出实现。
3.2 事件相关平均:逐像素响应幅值与时间窗选择
GLM 回答“有没有显著响应”,事件相关平均回答“响应长什么样”。逐像素做刺激前的基线校正,把每个 trial 的刺激前 2 秒均值当作基线,将刺激后的响应标准化为该基线之上的变化百分比。这个计算在体素层面进行,但需要先把数据按 trial 切片再平均。
preTime = 2 * fs; % 刺激前窗口,2秒 postTime = 10 * fs; % 刺激后窗口,10秒 nTrials = 10; trialMatrix = zeros(size(dFF,1), size(dFF,2), postTime, nTrials); for t = 1:nTrials onset = (t-1) * totalTime + 1; baseline = mean(dFF(:,:, max(1,onset-preTime):onset), 3); for i = 1:postTime idx = onset + i - 1; trialMatrix(:,:,i,t) = dFF(:,:,idx) - baseline; % 逐像素减去该trial自己的基线 end end avgResponse = mean(trialMatrix, 4);这段代码有个容易忽略的细节:baseline是逐像素的二维矩阵,所以减基线时用的是dFF(:,:,idx) - baseline,而不是全局标量。广泛场成像中不同脑区的基线荧光水平本来就不一样,使用全局减基线会让腹侧和背侧区域的相对激活幅度失真。时间窗的选择上,10 秒的刺激后窗口对大多数感觉刺激足够,但如果做的是奖赏或社交任务,响应会延伸到刺激结束后 10~15 秒,需要把 postTime 加大到 600 帧以上。
逐像素事件相关得到的是一个 4 维矩阵(x, y, time, trial),可以按兴趣区域把空间维度压缩后画平均时间曲线,也可以选特定时间点生成激活图。一般我会先看全脑像素中响应峰值出现的平均时间,再把激活图的时间点选在峰值附近 ±2 帧内,而不是固定在刺激后某个硬编码的时间点。
3.3 多重比较校正:FDR 体素级阈值怎么算
逐体素做统计检验必然遇到多重比较问题。一帧 256×256 的图像上有 65,536 个像素,即使没有真实效应,也有约 3,276 个像素会在 p<0.05 的阈值下被误判为显著。广泛场成像的空间平滑使得邻近像素高度相关,Bonferroni 校正过于保守,业界常用的是 Benjamini-Hochberg FDR 控制。
% 输入:tstatMap 是逐像素 t 统计量矩阵,dof 是自由度 pMap = 2 * tcdf(-abs(tstatMap), dof); % 双尾检验 p 值 pVec = pMap(:); pVec = pVec(~isnan(pVec)); sortedP = sort(pVec); n = numel(sortedP); fdrThreshold = 0.05; k = find(sortedP <= (1:n)' * fdrThreshold / n, 1, 'last'); if isempty(k) threshold = 0; % 无显著像素 else threshold = sortedP(k); end significantMask = pMap <= threshold;FDR 的计算逻辑并不复杂:把 p 值排序后,找到满足p(i) <= i * q / n的最大的 i,以该 p 值为阈值。这里的 q=0.05 表示控制的是“假发现比例”而非“假阳性概率”,意味着显著像素里允许有 5% 是假的。广泛场成像场景下 q 取 0.05 是常规选择,但如果用于筛选后续追踪的目标脑区,建议把 q 降到 0.01,因为后续单脑区的信号提取需要比较可靠的空间种子点。
4. 降维与连通性:体素分析的高级应用
4.1 主成分分析压缩体素维度:预处理还是信号提取
当体素维度过高而 trial 数相对有限时,逐体素 GLM 的统计效力会打折扣。PCA 在广泛场成像中主要有两种用途,一是作为下游分析的预处理来降噪——把 65,536 个像素压缩到前 20 个主成分,丢掉的主要是随机噪声;二是作为信号提取手段,把主成分得分映射回像素空间后得到空间模式。两者的区别在于是否用得分序列做后续回归,前者不关心成分含义,后者必须在意。
% 将 dFF 三维矩阵展开为 [时间 x 像素] [nRows, nCols, nTime] = size(dFF); pixelTime = reshape(dFF, nRows*nCols, nTime)'; % 注意:转置后有 NaN 的行需要剔除,用 mean 而非 nanmean 时的默认处理 reduced = pixelTime; reduced(isnan(reduced)) = 0; % 对数据居中化 reduced = reduced - mean(reduced, 1); % 计算主成分。这里用 svd 而不是 pca 函数,便于直接观察解释方差 [U, S, V] = svd(reduced, 'econ'); explainedVar = diag(S).^2 / sum(diag(S).^2); topN = find(cumsum(explainedVar) > 0.8, 1, 'first');SVD 计算完成后,U 的列是主成分时间序列,V 的列是空间加载。解释方差累计 80% 的主成分数量通常只有 10~30 个,这比直接用 65,536 维体素做后续聚类要稳定得多。把主成分得分重塑回二维图像就是空间模式图,它反映了该成分主要在哪几个脑区表达。注意svd之前必须先做像素级去均值,否则第一主成分会很大程度上对应整体亮度波动背景。
4.2 种子点相关分析:从像素到功能连接图的实现
广泛场成像的一个常见问题是各脑区之间的协同活动如何刻画,种子点相关分析是其中最直接的体素级方法。选一个感兴趣的种子区域,取该区域所有像素的平均时间序列,然后计算它与全脑每个像素时间序列的相关系数,最终生成一个功能连接图。这里的细节在于种子区域的定义方式——用解剖坐标还是用激活图聚类。
% 假设 seedMask 是二值掩膜,已通过 ROI 工具或 GLM 激活图获得 seedTrace = squeeze(mean(mean(dFF .* seedMask, 1), 2)); seedTrace = seedTrace(:); corrMap = zeros(nRows, nCols); for x = 1:nRows for y = 1:nCols pixelTrace = squeeze(dFF(x,y,:)); pixelTrace = pixelTrace(:); if std(pixelTrace) < 1e-6 || std(seedTrace) < 1e-6 corrMap(x,y) = 0; continue; end R = corrcoef(seedTrace, pixelTrace); corrMap(x,y) = R(1,2); end end相关系数图通常需要经过 Fisher z 变换后再做统计检验,因为皮尔逊相关系数的分布不是正态的,尤其当相关值接近 ±1 时。变换公式是z = 0.5 * log((1+r)/(1-r)),后续的组间比较用 z 值而非原始 r 值。另外,逐体素做相关计算开销不小,256×256 分辨率的图像要跑 65,536 次corrcoef,在普通工作站上大约需要几分钟。如果需要提速,可以把像素矩阵一次性标准化后做矩阵乘法,用pixelNorm' * seedNorm代替循环。
4.3 静息态广泛场成像的体素级频段分解
静息态数据分析中,dF/F 信号需要先按频段拆分,再在特定频段上做体素间连接。广泛场成像中最常见的是把 0.01~0.1 Hz 的频带作为低频波动的关注区间,这对应神经血管耦合中较慢的振荡成分。逐体素做小波分解太慢,工程化的做法是设计带通滤波器后对每个像素滤波,再进行种子点分析,也就是把 4.2 节的输入信号换成带通滤波后的版本。
fs = 20; flow = 0.01; fhigh = 0.1; d = designfilt('bandpassiir', 'FilterOrder', 6, ... 'HalfPowerFrequency1', flow, 'HalfPowerFrequency2', fhigh, ... 'SampleRate', fs); bandData = zeros(size(dFF)); for x = 1:nRows for y = 1:nCols bandData(x,y,:) = filtfilt(d, squeeze(dFF(x,y,:))); end end带通滤波器的阶数选 6,在 0.01 Hz 和 0.1 Hz 之间有足够的过渡带衰减,又不至于产生相位畸变——配合filtfilt使用后相位响应为零。这里的一个常见误区是直接对原始 dF/F 滤波而不是对去趋势后的信号滤波。如果数据里有明显漂移,先做时间高通或去趋势再带通,否则低频成分会被线性拟合的部分吸收掉。
5. 验证体素分析结果的三个实用技巧
5.1 置换检验确定时空聚类阈值
前面 FDR 校正只校正了像素维度上的多重比较,但没有考虑空间邻近性。广泛场成像的平滑特性使得相邻像素的统计量高度相关,一个真实的激活区域会在图像上形成一块连续区域,而零散的孤立像素更可能是假阳性。因此更稳健的做法是置换检验与聚类阈值结合:打乱 trial 标签,记录最大聚类大小作为零分布,再以 95% 分位数为阈值筛选真实聚类。
nPerm = 1000; maxClusterSize = zeros(nPerm, 1); for perm = 1:nPerm permOrder = randperm(nTrials); % 对每个 trial 内的时间点做整体置换,避免破坏时间自相关 permData = trialMatrix(:,:,:,permOrder); permMean = mean(permData, 4); % 用简单的阈值法提取聚类 permStat = max(abs(permMean), [], 3); bw = permStat > prctile(permStat(:), 95); cc = bwconncomp(bw, 8); if numel(cc.PixelIdxList) > 0 maxClusterSize(perm) = max(cellfun(@numel, cc.PixelIdxList)); end end thresholdSize = prctile(maxClusterSize, 95);置换次数 1000 次足够获得稳定的 95% 分位数估计。置换单位是 trial 而不是单帧,因为相邻帧之间存在高自相关,逐帧置换会严重低估真实聚类大小。bwconncomp使用 8 连通判断相邻像素,这比 4 连通更容易把斜向连接的像素聚合在一起,适合成像数据天然具有的空间平滑特性。
5.2 信号相关性检查:逐像素时间序列与种子点的一致性度量
在向组里汇报结果之前,我总是会做一步人为检查:在激活图中随机挑 5 个像素,把它们的时间序列和 GLM 预测的响应曲线叠加画出来,目测拟合质量。这里有一个容易被忽略的坑:逐体素 GLM 的 t 值图只反映信噪比,不能直接当作“激活强度”去解释。两个像素的 t 值相同,完全可能是因为一个激活强但噪声大,另一个激活弱但噪声小。因此如果要报告响应幅度,必须返回去看 betaMap,而不是 tstatMap。
% 在显著区域中挑选 5 个高 t 值像素,打印其 beta 估计值 idx = find(significantMask); [~, sortedIdx] = sort(tstatMap(idx), 'descend'); sampleIdx = idx(sortedIdx(1:5)); for i = 1:5 fprintf('Pixel %d: beta=%.3f, t=%.2f\n', ... sampleIdx(i), betaMap(sampleIdx(i)), tstatMap(sampleIdx(i))); end这个检查在 MATLAB 命令行里就能完成,不需要写文件。输出里如果出现 beta 值很小但 t 值很高的像素,说明该位置的噪声极低,信号虽然小但稳定;如果 beta 大但 t 值一般,则提示该区域激活幅度可观、但 trial-to-trial 波动也大。两者分别适合不同的后续解释场景。
5.3 组水平分析的标准化策略:共配准与参数映射表
多只小鼠的数据做组水平分析前,需要把每只小鼠的图像配准到一个共同模板上。广泛场成像没有类似 fMRI 那种标准脑模板,常见的做法是用解剖学 landmarks 做仿射配准,或者用每只小鼠的血管模式图做特征匹配。MATLAB 里fitgeotrans支持'affine'变换,输入两组对应的控制点坐标即可。控制点的选取通常基于嗅球、前囟、lambda 缝等解剖标志。
组水平统计的最小单位是每只小鼠的体素级 beta 图或 t 图。把这些图配准到模板空间后,逐体素做单样本 t 检验,得到组水平的激活图。此时需要注意的是,每只小鼠的 trial 数目可能不同,自由度也不同,所以做组水平检验时应该把每只小鼠的对比图像先标准化为单位方差,避免高 trial 数的小鼠主导结果。常用的标准化因子是sqrt(nTrials),直接除以该因子后再进组水平 t 检验。
本文还有配套的精品资源,点击获取