简介:这份资源是一套面向fMRI影像分析与RSA研究的MATLAB脚本工具,用于从HCP-MMP1.0分区图谱中便捷提取特定体积二进制掩码文件。它适合具备一定MATLAB基础、从事脑影像ROI分析的研究生与科研人员,可解决手动从大图谱中裁剪目标脑区、生成掩码效率低且易出错的问题。资源包共7个文件,约834KB,包含MATLAB脚本、mmp.csv ROI查找表、HCP_atlas压缩包、示例nii数据、LICENSE及README说明文档,覆盖脚本、数据与使用说明三类内容。目前已有545人学习下载。使用者可通过ROI查找表定位感兴趣脑区代码,调用create_HCP_mask()函数,按单个或合并方式输出掩码,并灵活选择左、右或双侧半球及是否膨胀处理;输出掩码保持MNI空间,便于后续配准与统计分析,为脑区定位与RSA流程提供可直接复用的基础工具。
1. 从 HCP-MMP1.0 图谱里抠出你要的那块脑区掩码
做静息态或任务态 fMRI 的人,几乎都会遇到同一个卡点:手头有一份 HCP-MMP1.0 分区图谱,180 个皮层区域,每个区域左右半球各一份,可你只想要其中某一个或某几个区域,做成一张二值掩码,拿去提时间序列、算功能连接、做 ROI-wise 分析。问题是这份图谱的原始文件动辄几百 MB,标签是离散的整数编号,直接丢进流程里既浪费内存又容易把编号搞错。标题里说的这个脚本,干的就是这件事:从 HCP-MMP1.0 的拆分文件里,按你指定的区域编号,抽出一张干净的二进制掩码。它解决的不是什么高深算法,而是把「图谱 → 目标区域 → 可用掩码」这条链路里最容易出错的一步标准化。适合谁?适合正在用 HCP 图谱做 ROI 分析、又不想每次手动在可视化软件里描区域的研究生和工程师。下面我按自己实际跑通的顺序,把选型、命令、参数和踩过的坑讲清楚。
2. 先搞懂 HCP-MMP1.0 的文件结构和编号逻辑
2.1 为什么不能直接拿原始图谱当掩码用
HCP-MMP1.0 的官方发布形式,通常是一个体积不小的 NIfTI 文件,体素值就是区域标签。比如左侧某个区域是 1,右侧对应区域是 2,依次排到 180。这种「一张图装下全部区域」的设计,对可视化很友好,对批处理却很不友好。你拿它当掩码,等于告诉后续流程「所有非零体素都是感兴趣区」,那 180 个区域会被一锅端。更麻烦的是,很多工具在读取时会把标签当连续灰度处理,插值一开,边界立刻糊掉,编号也跟着漂。所以正确做法是:先明确你要哪几个标签值,再把它们单独抠成 0/1 掩码。这一步不做,后面所有 ROI 信号都是脏的。
2.2 拆分文件、标签值和左右半球的关系
HCP-MMP1.0 的「拆分」通常指按半球或按区域拆出来的子文件。常见约定是:左侧皮层标签为奇数或从 1 开始的一段,右侧为对应偏移。不同发布版本编号规则可能不同,所以第一件事不是写代码,而是把标签表读出来核对。我一般会先用工具打印唯一值,确认目标区域到底对应哪个整数。这里有个血泪经验:有人直接按文献里的编号去提,结果文献用的是另一套重编号方案,提出来的是隔壁区域,做完一圈分析才发现,只能返工。核对标签这一步花五分钟,能省你两天。
2.3 掩码为什么必须是二值且和功能像对齐
掩码的本质是布尔索引。它只有两个状态:在区域内、不在区域内。如果你保留原始标签值,后续做均值时会把标签值本身当权重,等于给不同体素偷偷加了系数,结果完全不可解释。另外,掩码必须和你的功能像处在同一空间、同一网格。HCP 图谱常见是 32k 表面或体积模板,而你的 fMRI 可能是 2mm 或 3mm 体素。空间没对齐就提信号,等于拿错地图找路。所以流程里一定要有重采样或配准这一步,且重采样时用最近邻插值,别用线性,否则 0/1 边界会被插出 0.3、0.7 这种中间值。
3. 用脚本从拆分文件提取指定区域掩码的完整流程
3.1 环境准备与依赖确认
这个脚本本质是围绕 NIfTI 读写和数组操作,Python 环境下最稳的组合是 nibabel 加 numpy。先确认版本能对上,避免读文件时报头信息错误。
# 建议在独立虚拟环境里操作,避免和现有神经影像工具链冲突 python -m venv env_mask source env_mask/bin/activate # 核心依赖:nibabel 负责读写 NIfTI,numpy 负责数组运算 pip install nibabel numpy逻辑说明:nibabel 是神经影像领域读 NIfTI 的事实标准,能保留仿射矩阵和头信息,这对后面空间对齐至关重要。numpy 负责把体素数据转成数组做布尔运算。参数上没有太多可调的,重点是别在系统 Python 里乱装,很多人的环境里已经有一套老版本 nibabel,混用会出现仿射矩阵读出来是单位阵的玄学问题。
3.2 读取拆分文件并确认目标标签
先别急着提掩码,把文件读进来,看看里面到底有哪些标签值,这一步是后悔药。
import nibabel as nib import numpy as np # 替换成你的 HCP-MMP1.0 拆分文件路径 atlas_path = "HCPMMP1_split.nii.gz" atlas_img = nib.load(atlas_path) atlas_data = atlas_img.get_fdata().astype(np.int32) # 打印所有唯一标签值,确认目标区域编号 labels = np.unique(atlas_data) print("唯一标签值:", labels) print("标签总数:", len(labels))逻辑说明:get_fdata()返回浮点数组,这里强制转成整型,是因为标签本质是离散编号,浮点比较容易出现 1.0000001 不等于 1 的翻车。np.unique把出现过的标签全列出来,你对着标签表找目标值。参数上注意:如果文件很大,get_fdata会占内存,可以改用np.asanyarray(atlas_img.dataobj)做惰性读取,但多数拆分文件不至于。确认标签后,把目标值记下来,比如左侧某区是 7,右侧是 8。
3.3 生成二值掩码并保存
确认标签后,提取就是一行布尔运算的事,但保存时的细节决定后面能不能用。
# 假设目标区域标签为 7 和 8 target_labels = [7, 8] # 生成二值掩码:在目标标签内为 1,否则为 0 mask_data = np.isin(atlas_data, target_labels).astype(np.uint8) # 用原图谱的仿射矩阵和头信息构建新图像,保证空间一致 mask_img = nib.Nifti1Image(mask_data, atlas_img.affine, atlas_img.header) mask_img.set_data_dtype(np.uint8) # 保存掩码 nib.save(mask_img, "roi_mask.nii.gz") print("掩码体素总数:", int(mask_data.sum()))逻辑说明:np.isin一次性处理多个标签,比循环快且不易错。astype(np.uint8)把布尔转成 0/1,节省空间。关键在atlas_img.affine和atlas_img.header:仿射矩阵决定体素坐标到世界坐标的映射,头信息里的体素尺寸、方向也一并继承,这样掩码和原图谱严格对齐。set_data_dtype显式声明数据类型,避免保存时被默认转成 float。最后打印体素数,是给自己一个 sanity check:如果体素数为 0,说明标签选错了;如果大得离谱,说明把整个半球都框进去了。
3.4 把掩码对齐到功能像空间
掩码和图谱对齐了,但你的 fMRI 可能是另一个空间。这一步不能省。
# 常见做法是用 FSL 的 flirt 做配准,最近邻插值保住 0/1 边界 flirt -in roi_mask.nii.gz \ -ref func_img.nii.gz \ -out roi_mask_func_space.nii.gz \ -applyxfm -usesqform \ -interp nearestneighbour逻辑说明:-ref指定功能像作为参考空间,-applyxfm -usesqform表示用文件自带的 sform 做变换,适合已经配准过的场景。-interp nearestneighbour是重点,最近邻插值保证输出只有 0 和 1,不会出现中间值。如果你的图谱和功能像还没配准,得先做一步仿射或非线性配准,再套这个命令。参数上,-usesqform和-applyxfm的组合在头信息规范时很省事,但头信息乱的文件会翻车,这时改用-init指定变换矩阵更稳。
4. 提取掩码时最容易翻车的几个地方
4.1 标签编号对不上,提出来是隔壁区域
现象:掩码体素数看着正常,但提出来的时间序列和预期脑区功能对不上,做统计时结果诡异。原因:HCP-MMP1.0 存在多套编号方案,文献、可视化软件、拆分文件三者可能不一致,直接照搬文献编号就会错位。解决:永远先用np.unique打印实际标签,再对照你手上那份拆分文件自带的标签表核对,别信记忆。我一般会把标签表和唯一值并排打印,肉眼过一遍再动手。
4.2 重采样用了线性插值,掩码边界糊掉
现象:掩码里出现 0.3、0.5 这类值,后续做均值时边界体素被算进去,信号被稀释。原因:线性或样条插值会把 0/1 边界平滑成中间值。解决:所有涉及掩码的重采样一律用最近邻插值。FSL 里是-interp nearestneighbour,其他工具里找 nearest neighbor 选项。已经糊掉的掩码可以重新二值化,但边界已经漂了,不如重做。
4.3 左右半球标签混用,掩码跨了中线
现象:掩码看起来覆盖了目标区域,但体素数比单侧预期多一倍,或者形状跨了中线。原因:左侧和右侧标签是不同整数,如果只填了一个却以为覆盖双侧,或者把左右标签都填进去却没意识到,就会出问题。解决:明确你要单侧还是双侧。单侧只填对应标签,双侧把左右两个标签都放进target_labels。做完后把掩码叠到图谱上目视检查一次,这一步别偷懒。
4.4 头信息丢失导致空间错位
现象:掩码单独打开看着没问题,一叠到功能像上就偏到别处。原因:保存时没继承原图谱的仿射矩阵,或者中间某步用了默认头信息重建图像。解决:构建新图像时始终传入原图的affine和header,保存前打印一下仿射矩阵,和原图对比。如果已经丢了,用原图的仿射矩阵重新赋值再保存。
4.5 体素尺寸不一致直接提信号
现象:掩码和功能像体素尺寸不同,提信号时工具自动重采样,结果不可控。原因:图谱常见 1mm 或表面模板,功能像常是 2mm、3mm,直接混用会触发隐式重采样。解决:显式做一步配准或重采样,把掩码变到功能像网格,用最近邻插值,做完检查两者体素尺寸和方向是否一致。别依赖工具的默认行为,默认行为是黑匣子。
5. 进阶:批量提取多区域与掩码质量自检
当你不是提一个区域,而是要把 180 个区域全提一遍做全脑连接时,逐个手改标签就不现实了。我一般会写一个循环,把标签表读进来,按区域名和标签的映射批量生成掩码,每个掩码单独存文件,文件名带上区域名,方便后续索引。
import nibabel as nib import numpy as np import os atlas_img = nib.load("HCPMMP1_split.nii.gz") atlas_data = atlas_img.get_fdata().astype(np.int32) # 区域名到标签的映射,按你手上的标签表填写 region_map = { "area_A_left": 1, "area_A_right": 2, "area_B_left": 3, "area_B_right": 4, } out_dir = "masks" os.makedirs(out_dir, exist_ok=True) for name, label in region_map.items(): mask_data = (atlas_data == label).astype(np.uint8) voxel_count = int(mask_data.sum()) # 体素数为 0 说明标签错了,直接跳过并报警 if voxel_count == 0: print(f"警告:{name} 标签 {label} 未匹配到体素,请核对标签表") continue mask_img = nib.Nifti1Image(mask_data, atlas_img.affine, atlas_img.header) mask_img.set_data_dtype(np.uint8) nib.save(mask_img, os.path.join(out_dir, f"{name}_mask.nii.gz")) print(f"{name}: 标签 {label}, 体素 {voxel_count}")逻辑说明:循环里对每个标签做等值判断,比np.isin更适合单标签场景。体素数为 0 时直接报警并跳过,避免生成空掩码污染后续流程。文件名带区域名,后续做连接矩阵时可以直接按名字排序,不用记编号。参数上,region_map建议从标签表自动生成,减少手抄错误;如果标签表是 CSV,用 pandas 读进来转字典即可。
批量生成后,质量自检不能省。我习惯做三件事:一是统计每个掩码的体素数,和该区域在文献里的近似体积对比,数量级差太多就回头查;二是随机抽几个掩码叠到图谱上目视,看边界是否贴合;三是检查所有掩码的仿射矩阵是否一致,不一致的说明中间某步头信息被改了。这三步做完,再往流程里灌数据,心里才有底。
最后说个我自己的习惯:每次提完掩码,先拿一个已知功能的区域跑一遍种子点连接,看看连接模式是否符合预期。如果连出来的图和教科书里的默认网络对不上,八成是掩码或空间对齐出了问题。这个自检比任何日志都直观。希望帮到你。
本文还有配套的精品资源,点击获取