简介RoiConnect 是面向脑电研究与神经影像分析者的 ROI 连接分析工具箱基于 EEGLAB 接口并与主流头模型工具兼容可在表面或体积图谱上开展 TRGC、CS 等感兴趣区连通性计算适合从通道分析推进到脑区网络分析的研究生与科研人员。压缩包共 371 个文件约 46.31MB以 210 个 .m 源码和 117 个 HTML 帮助文档为主体辅以 png 示意图、mat 示例数据、mex 平台二进制及配置说明便于查阅函数实现、示例流程与参数用法。包内目录组织清晰便于按模块检索、对照学习与二次开发具有参考价值。现有 655 人学习下载说明其在脑电分析社区具有一定关注度。读者可获得可运行的连接分析流程、图谱与头模型适配思路、两类连接指标调用及排错线索用于复现实验或搭建脑区网络分析脚本。1. 结论先说ROI 连接矩阵里最粗的那条边通常最不可信一份脑电图 ROI 连接分析region of interest connectivity的结果如果最终以单文件 HTML 交付第一眼看到的往往是热图上对角线附近那一片深色——相邻脑区、甚至同一块皮层被两个标签切开的位置连接值高得离谱。直觉会说“这两个区真的很同步”实际上这多半是体积传导留下的特征同一个电场被相邻电极和相邻源点同时拾取相位差接近零任何对零延迟敏感的量都会把它算成强连接。搞懂这条链路上每一步在做什么比会拖一个阈值滑块重要得多因为阈值之下的每个数字都是从电极电位一路投影、对齐、加权推出来的哪一步偷懒报告里都看不出来。2. 电极看到的是混合物从传感器空间到 ROI 时间序列的三次投影头皮电极记录的从来不是某个脑区的活动而是整块皮层和伪迹在那一刻叠加出来的电位。ROI 连接分析要做的第一件事就是把这个混合物拆成“每个脑区一根时间序列”的形式。这个过程要经过三次投影正向建模把源映到电极逆投影把电极映回源分区归并再把上千个源点压成几十个 ROI。三次投影里任何一次的定义含糊后面的连接值就只是把误差重新排列了一遍。2.1 头模型与导联场源和电极之间的关系是被算出来的正向问题的形式很简单V L·s nV是电极电位向量s是源活动L就是导联场矩阵形状为n_ch × n_src。导联场不是配置项它是几何量头模型的形状球模型还是分层的边界元、电极在头皮上的三维坐标、源点被放在白质边界还是皮层中面这三件事一旦定下来L的每一列就确定了——它描述“第 j 个源点放 1 个单位电流时每个电极上会出现多少电位”。逆投影是解V L·s的反问题而这个方程是欠定的源点动辄上万个电极只有几十个。常见的实现思路有两类最小范数估计和波束形成。前者写成ŝ W·V其中W Lᵀ(LLᵀ λI)⁻¹λ是正则化项后者对每个源点单独求一个空域滤波器w (LᵀC⁻¹L)⁻¹LᵀC⁻¹C是数据的协方差矩阵。两者共同的后果是解出来的源活动在空间上被“抹开”了一个真实的局灶源会在邻近源点上留下拖尾。这件事对 ROI 级分析的含义比大多数人以为的更重。你选的不是电极数量而是有效空间分辨率。电极越多L的行数越多逆问题的约束越强相邻源点才越可能被分开电极少的时候两个相邻 ROI 的源时间序列本来就高度相似算出来的高连接有相当一部分是算法造成的不是大脑造成的。提示把逆算子缓存下来重复用没问题但每次换电极坐标或换头模型都要重算不要拿上一个被试的W直接套。2.2 分区图谱与时间序列提取一个 ROI 到底代表哪一根线分区图谱把皮层划成 N 个标签每个标签覆盖若干源点。从“若干根源时间序列”到“一根 ROI 时间序列”中间那一步看着无害实际是最容易出问题的地方。问题出在极性上。逆投影得到的每个源点都有自己的偶极子朝向两个解剖上属于同一脑区、活动完全同步的源点可能因为朝向相反而解出符号相反的时间序列。直接取平均两者互相抵消剩下的是一根幅值很小、相位混乱的线——然后你会看到这个 ROI 跟谁都不连或者跟谁都乱连。合格的实现通常这么做先在整个 ROI 里挑能量最大的源点作为极性参考再把每个源点与它做内积结果为负就整体翻号对齐之后再平均。下面这段是核心逻辑# src_ts: 形状 (n_src_in_roi, n_times)逆投影后属于同一个 ROI 的源时间序列 # 目标合成一根无极性抵消的 ROI 代表序列 def roi_time_series(src_ts): # 1. 选能量最大的源点当极性参考避免随机选到噪声主导的源 energy (src_ts ** 2).sum(axis1) ref src_ts[energy.argmax()] aligned [] for s in src_ts: # 2. 与参考做内积负相关说明偶极子朝向相反整体取反 if (s * ref).sum() 0: s -s aligned.append(s) # 3. 极性统一之后再做平均幅值才不会被抵消 return sum(aligned) / len(aligned)energy.argmax()挑的是幅值最强的源点理由是这样一根序列的信噪比最高拿它当符号基准比拿随机源稳定得多(s * ref).sum() 0是极性判据等价于相关系数为负计算量是 O(n_times)对上千个源点也不慢最后的sum(...) / len(...)是等权平均也有人用第一主成分代替但主成分的符号本身是随机的还得再做一次对齐而且它会把强源的权重放大空间特异性反而下降。还有两个容易忽略的参数。一是图谱的配准方式把模板图谱硬套到个体头上和先做个体化配准再映射得到的 ROI 边界能差出一大截尤其是内侧结构和颞叶。二是概率图谱的阈值阈值调高ROI 内源点变少平均后噪声更大阈值调低ROI 会溢到邻近区域空间特异性变差。这两个方向都不“更对”只有跟你的科学问题匹配不匹配。2.3 采样与窗口连接不是瞬时量它需要一个时间尺度到这一步手里是n_roi × n_times的矩阵采样率还停留在原始数据的几百赫兹。直接拿去算连接是没意义的因为连接描述的是两段信号在某个时间尺度上的统计关系必须有窗口。常见的两条路子。一条是先带通到目标频段比如 8–12 Hz再做 Hilbert 变换取包络把包络降采样到 50–100 Hz然后在包络上算相关或相干——这条路适合描述“两个区的能量起伏是否同步”。另一条是不取包络直接在滑动窗口里对原始信号做频谱估计再算交叉谱——这条路保留了相位信息wPLI 这类度量必须走它。窗口长度不是随便定的它受时频不确定性约束频带宽度B和窗口长度T大致满足T ≈ 1/B的量级关系。想分析 2 Hz 宽的窄带窗口就得接近 500 ms窗口取 200 ms频率分辨率只剩 5 Hz你根本分不清 8 Hz 和 12 Hz 的活动。更麻烦的是自由度一个窗口内的有效独立样本数大约是2·T·BT0.5 s、B2 Hz时只有 2 个自由度单个窗口的连接估计几乎全是噪声必须跨 trial 或跨窗口平均。注意降采样要在带通之后做顺序反了会把高频折叠进低频包络里混进一堆原本不存在的慢波。3. 零延迟是问题关键相干、PLI、wPLI 之间的取舍连接度量有一大类翻车不是因为代码写错了而是因为选了一个对零相位差没有抵抗力的量。要理解这件事得先看清楚体积传导是怎么凭空制造出“强连接”的。3.1 体积传导如何制造出并不存在的连接把观测写成V M·sM是混合矩阵s是真实源。假设两个源s1、s2完全独立但它们在头皮上的投影有重叠那么电极x记录到a1·s1 a2·s2电极y记录到b1·s1 b2·s2。两个电极的观测信号里都含有s1的成分相干自然不为零而且这个虚假的相干有个标志性特征相位差接近 0。头皮是导体电磁波传播在这个尺度上的延迟可以忽略一个源到两个电极几乎是同时到达。所以判据很直接如果一个度量对零相位差敏感它在 EEG 上就不可信。幅值平方相干就是典型它对零延迟完全无免疫相邻电极之间的相干常年被抬到 0.6 以上而真实的功能连接往往只有 0.1 量级。这也是为什么在 EEG 上谈连接几乎必须在源空间做而且即便到了源空间逆问题带来的空间模糊依然会残留一部分零延迟成分。3.2 三种去零延迟的思路与它们的代价第一类是只取交叉谱的虚部。设Sxy(f)是x、y在频率f上的交叉谱零延迟成分只贡献实部虚部对应的是有相位滞后的成分于是虚部相干天然把体积传导压下去了。代价是方差变大而且当真实连接的相位差接近 0 或 π 时虚部本身接近 0真信号也被一起丢掉。第二类是相位滞后指数只看相位差的符号PLI |E[sign(Δφ)]|。它把每个样本的相位差压成 ±1完全无视幅值。好处是对幅值噪声不敏感坏处同样来自这一点——一段几乎没有连接、只有随机小抖动的数据符号照样是 ±1PLI的期望不是一个稳定的零而是一个随样本数衰减的正偏置量级大概在√(2/(πN))。trial 数 40 的时候这个偏置约 0.13和很多真实效应一样大。第三类是加权相位滞后指数它保留了虚部的幅值作为权重# X, Y: 形状 (n_trials, n_times) 的两根 ROI 时间序列已带通、已去均值 # fft_band() 返回目标频带内的复系数形状 (n_trials, n_freq) def wpli(X, Y): zx fft_band(X) zy fft_band(Y) # 交叉谱的虚部对应有相位滞后的那一部分能量 im (zx * zy.conj()).imag # 分子先跨 trial 平均再取模保留符号一致性 num abs(im.mean(axis0)) # 分母先取模再平均作为总能量归一化 den abs(im).mean(axis0) # 平方形式取值落在 [0, 1]避免分母趋零时的数值抖动 return (num ** 2) / (den ** 2 1e-12)分子分母的顺序是整个度量的灵魂。num先平均再取模如果各 trial 的相位滞后方向一致平均之后不会互相抵消值就大den先取模再平均衡量的是“总的虚部能量有多少”。两者的比值本质上是在问这些有相位滞后的能量里有多大比例是方向一致的。随机噪声在分子里会互相抵消所以 wPLI 在无连接数据上的正偏置比 PLI 小得多。提示1e-12不是为了好看分母接近零时比如某个频点几乎没有能量会直接给你 inf 或 nan这个保护必须留。代价也要说清楚。wPLI 对 trial 数依然敏感trial 少的时候估计的方差大、系统性偏高它对相位差接近 0 的真实连接同样不敏感因为那部分能量被虚部丢掉了。所以它不是“更好的 PLI”而是“在零延迟污染和统计偏置之间换了一个平衡点”。度量对零延迟的处理无连接时的偏置对 trial 数的敏感度主要短板幅值平方相干无很高体积传导直接抬高低无法区分真假连接虚部相干只保留虚部低但方差大高相位差接近 0 或 π 时丢真信号PLI只看相位差符号正偏置约√(2/(πN))中微小幅值也计满噪声敏感wPLI按虚部幅值加权明显更小高分母趋零时不稳定3.3 有向连接什么时候值得多花这一步wPLI、PLI、虚部相干都是无向的它们只知道“这两个区有关系”不知道谁先谁后。想看方向就得上 Granger 因果或转移熵前者基于 VAR 模型后者基于状态转移概率。在 ROI 级 EEG 上做有向分析有两个硬门槛一是采样率VAR 模型对时间分辨率敏感降采样太狠会把滞后结构压没二是样本量N个 ROI 的 VAR 模型参数量随N²增长几十个 ROI、几百个时间点的情况下过拟合几乎是必然的。比较务实的做法是分两步先用 wPLI 这类无向度量在全脑范围筛出候选边再对少数几对 ROI 单独做有向分析并配 surrogates 检验。反过来一上来就对整张矩阵跑有向连接得到的箭头方向大概率是噪声拟合出来的。4. 把结果压进一个 HTML单文件报告的取舍分析跑完交付物是一个.html文件双击就能打开里面有热图、有网络图、有阈值滑块断网也能用。这个选择看着像是纯工程问题实际上它反过来约束了分析阶段的数据结构。4.1 为什么是单文件而不是一堆图和 CSV理由不是“好看”是可复核。连接分析的报告要给不装环境的人看要给几个月后的自己看还要能被别人拿着原始数据重跑一遍做对照。散装交付一张 PNG 热图、一份 CSV 矩阵、一份 README 记录参数在这三件事上都会掉链子图片不能查数值CSV 不知道对应哪次运行README 里的参数和实际跑的那次未必一致。单文件的好处是把数据和视图绑死矩阵、ROI 名称、频带、窗口长度、trial 数、正则化参数、生成时间全在同一个文件里改了任何一个都能看出来。代价也很实在文件体积随 ROI 数的平方增长浏览器渲染能力有限为了压体积必然要做精度或分辨率上的让步。这些让步必须有记录否则报告就变成了新的黑匣子。4.2 数据载荷矩阵怎么塞进 HTML 而不把它撑大ROI 数 60 的时候一个 60×60 的 float64 矩阵是 28 KB压缩后更小但真实报告里往往不止一张矩阵——多个频段、多个被试、多组对比加起来的体积是乘法关系。三个常规手段只存上三角对称矩阵省一半、定点量化float32 转 int16精度 1e-4 足够显示、按需下采样视图用低分辨率原始矩阵单独存。import base64 import numpy as np def build_payload(conn, labels, meta, q4): # 连接矩阵对称只取严格上三角体积直接减半 iu np.triu_indices_from(conn, k1) vals conn[iu] # 定点量化float 乘 10^q 取整存成 int16精度 1e-q scale 10 ** q qvals np.round(vals * scale).astype(np.int16) return { meta: meta, # 频带、窗口、trial 数、逆算子参数等 labels: labels, # ROI 名称顺序即矩阵下标 edges: base64.b64encode(qvals.tobytes()).decode(), scale: scale, # 解码时用来还原浮点值 n: len(labels), # 前端重建方阵需要 }np.triu_indices_from(conn, k1)拿到上三角的行列索引k1表示排除对角线——自连接恒为 1存了也是浪费。10 ** q里q4表示保留四位小数对 0–1 范围的连接值绰绰有余超过 32767 会溢出所以q不能随便往 5、6 调。base64编码会让体积涨约三分之一但换来的是纯文本可嵌入、不会被 HTML 转义破坏比直接写二进制安全。解码端做三件事atob拿到二进制串、Int16Array视图还原整数、除以scale还原浮点再按上三角索引填回方阵。整个过程在前端几十毫秒内完成不需要任何后端。4.3 渲染层canvas 画矩阵SVG 画网络60×60 的热图是 3600 个格子用 DOM 元素画会直接卡死必须用 canvas一次性把矩阵画到离屏画布缩放和 hover 只做坐标映射不重绘。网络图反过来节点通常只有几十个边上要挂 hover 事件、要拖动、要高亮邻居用 SVG 更省事每个circle和line都是独立元素事件委托一行就能写。视图参数和分析参数必须分开这是单文件报告里最容易被混淆的边界。阈值滑块调的是“显示哪些边”它不应该改变矩阵里的任何数值也不应该影响报告里记录的统计结论。可行的做法是在meta里写死两套字段analysis下面放不可变的参数频带、窗口、trial 数、置换次数、显著性校正方式view下面放可变的当前阈值、颜色映射范围、选中的 ROI。导出截图或者分享时只带analysis。注意滑块拖动时不要重算矩阵。把阈值当过滤条件在渲染层做一次遍历边列表就够重算会把交互拖成幻灯片。5. 踩坑记录五条来自真实排查现场5.1 配准与标签类现象同一批被试左半球某 ROI 的连接值系统性偏高右半球对应区域几乎为零跨被试一致得像真的效应。原因电极坐标和模板空间用了两套不同的坐标约定一套是影像学方向一套是头表方向配准矩阵里少了一次左右翻转。ROI 标签本身没错错的是电极被映射到了镜像位置。解决在流水线里加一步独立的坐标一致性检查——把配准后的电极位置画到模板头皮上人工看一眼鼻尖和左右耳前点是否落在预期位置。这一步花两分钟能省掉两周的返工。现象个别被试的某些 ROI 时间序列接近常数方差几乎为零连接值全是 nan。原因概率图谱的阈值定得太严加上个体配准偏差该标签下没有落进任何源点提取环节返回了空集或者全零。解决提取时对每个标签检查源点数量低于阈值就退化到“取最近源点”而不是静默返回零同时在报告meta里记录每个 ROI 的源点数让这类问题在交付时就暴露出来。5.2 度量与统计类现象两组被试的 PLI 做独立样本 t 检验多处“显著”但把两组标签打乱重跑显著性几乎不变。原因PLI 在无连接数据上有正偏置而且这个偏置随 trial 数变化。两组 trial 数不一致时偏置差异直接被当成了组间差异。解决不要对原始 PLI 做参数检验。用置换检验并且用相位随机化的 surrogates 估计每个被试自己的零分布报告里给出“观测值减零分布均值”的校正后指标而不是原始值。现象trial 数从 80 降到 30 之后全脑平均连接强度涨了一截看起来像是“任务难度提高了”。原因wPLI 是小样本偏高的估计量trial 少的时候方差大、期望上移。解决把 trial 数作为协变量记录进meta跨条件比较时保持 trial 数一致随机下采样到最小值并在报告里显式标注这一处理。5.3 报告与交付类现象HTML 报告在生成机上打开正常发给合作方之后浏览器直接无响应文件 200 MB 出头。原因把每个被试、每个频段、每个窗口的完整连接矩阵都以 float64 塞进了 payload还带上了所有 trial 级别的中间结果。解决payload 只保留可视图层需要的数据上三角 定点量化trial 级中间结果单独存成二进制附件单文件超过 20 MB 就该拆分了。6. 设计取舍可复现和可交互之间的那条线回到最初那个问题一份 ROI 连接报告要可信靠的到底是什么。做过几轮之后我的判断是靠的不是度量选得有多新而是每一个数字都能被追溯到一个明确的参数组合。wPLI 比 PLI 抗噪但如果报告里没写清窗口长度、trial 数、图谱阈值、逆算子的正则化系数换个人重跑一遍照样得到不一样的结果这时候度量再讲究也没用。所以设计上有一条我越来越坚持的线凡是影响数值的东西一律固化进 payload 的analysis字段且渲染层只读不写凡是只影响看的方式全部归到view随便拖。这条线的好处是一份报告同时满足两个互相拉扯的需求——合作方能拖着阈值滑块自己探索你自己能在三个月后拿着同一份文件复现出完全相同的矩阵因为矩阵本身根本没被交互碰过。验证这件事有个笨办法但很有效同一份原始数据跑两次流水线把两次生成的 payload 里的edges字段解码出来逐元素比对。差异应该是严格为零。如果出现了 1e-6 量级的漂移多半是某处用了未固定种子的随机过程比如 trial 级的下采样、或者 surrogates 的抽样顺序这类不确定性必须在meta里显式声明而不是让它悄悄混进结果。再往上一层是精度和体积的取舍。定点量化到四位小数对连接值的显示完全够用但如果有人想拿这份 HTML 里的数据去做二次统计四位小数可能不够。我的处理是分层HTML 里存量化后的可视图层同时用meta里的一个字段指向原始精度数据的校验值比如哈希需要精确复算的人拿原始数据和参数自己重跑报告只负责自证“我这次跑的是什么”。这样既不把文件撑爆也不给“报告里的数和论文里的数对不上”留后门。进阶一点的延伸是把 surrogates 的零分布也塞进报告。现在大多数报告只给观测值和 p 值读者没法判断这个 p 值是置换 200 次得到的还是 5000 次得到的也看不出观测值离零分布有多远。把零分布的直方图和观测值标在同一条数轴上一眼就能看出效应是“稳稳在分布之外”还是“擦着尾巴过去的”。代价是 payload 又要大一圈但如果只对显著的边存零分布的分位数比如 5%、50%、95%体积增加很有限信息量却翻倍。踩过的坑里最贵的一次是拿一份没记录图谱阈值的报告去做了组间比较两组用了不同阈值差异显著得离谱查了两周才发现问题出在提取环节而不是统计环节。从那以后我每次生成报告都会先打开 HTML 检查analysis字段是否齐全——频带、窗口、trial 数、图谱阈值、逆算子参数、随机种子缺一项就重新跑不管结果看起来多合理。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?