(advanced) using custom subcortical atlas¶
this notebook demonstrates how to convert a custom subcortical volumetric atlas (such as part of the AAL3 atlas) into the 3D surface mesh format required for yabplot.
why convert?
unlike the cerebral cortex (which is mapped as a continuous 2D sheet), subcortical regions like the thalamus, amygdala, and putamen are 3D structures. yabplot visualizes these structures as beautiful 3D surface meshes, but most standard MRI atlases are distributed as 3D volumes made of blocky voxels.
to plot them, we cannot simply pass the NIfTI file directly. instead, we use the marching cubes algorithm to mathematically wrap a tight 3D mesh around the outer boundary of the voxels. we then apply laplacian surface smoothing to remove the jagged edges, and export them as plot-ready surface files.
inputs and outputs¶
we start with:
- atlas volume (
.nii.gz): the 3D NIfTI file where each voxel contains an integer region ID. - atlas metadata (
.txt,.csv, etc.): a file listing the string names for each integer ID. (note: because every atlas creator formats this differently, we will write a quick loop to parse this into a python dictionary before feeding it to the builder).
we need to generate:
- surface meshes (
.vtk): a dedicated folder containing individual 3D mesh files for every extracted region. these files are fully smoothed, and ready to be loaded via thecustom_atlas_pathparameter inplot_subcortical.
step 1: configuration & file paths¶
import os
import yabplot as yab
# define where your source NIfTI and text files are located
# you can download the same atlas for this tutorial in here:
# https://www.gin.cnrs.fr/wp-content/uploads/AAL3v2_for_SPM12.tar.gz
aal_txt = '/Users/to8050an/Downloads/AAL3/AAL3v1_1mm.nii.txt'
aal_nii = '/Users/to8050an/Downloads/AAL3/AAL3v1_1mm.nii.gz'
# we will create two different output folders for two separate atlases
# one for full AAL3v1 subcortical atlas and one where we exclude cerebellum
dir_full_subcortical = 'dev/my_custom_atlases/subcortical/AAL3v1'
dir_no_cerebellum = 'dev/my_custom_atlases/subcortical/AAL3v1_nocerebellum'
step 2: parse the atlas metadata¶
first, we write a quick loop to parse the atlas' text file into a standard python dictionary mapping integer IDs to region names.
atlas_labels = {}
with open(aal_txt, 'r') as f_in:
for line in f_in:
parts = line.strip().split()
if len(parts) >= 2:
try:
rid = int(parts[0])
name = parts[1].replace(' ', '_').replace('/', '-')
atlas_labels[rid] = name
except ValueError:
continue
print(f"successfully parsed {len(atlas_labels)} total regions from text file.")
print(atlas_labels)
successfully parsed 170 total regions from text file.
{1: 'Precentral_L', 2: 'Precentral_R', 3: 'Frontal_Sup_2_L', 4: 'Frontal_Sup_2_R', 5: 'Frontal_Mid_2_L', 6: 'Frontal_Mid_2_R', 7: 'Frontal_Inf_Oper_L', 8: 'Frontal_Inf_Oper_R', 9: 'Frontal_Inf_Tri_L', 10: 'Frontal_Inf_Tri_R', 11: 'Frontal_Inf_Orb_2_L', 12: 'Frontal_Inf_Orb_2_R', 13: 'Rolandic_Oper_L', 14: 'Rolandic_Oper_R', 15: 'Supp_Motor_Area_L', 16: 'Supp_Motor_Area_R', 17: 'Olfactory_L', 18: 'Olfactory_R', 19: 'Frontal_Sup_Medial_L', 20: 'Frontal_Sup_Medial_R', 21: 'Frontal_Med_Orb_L', 22: 'Frontal_Med_Orb_R', 23: 'Rectus_L', 24: 'Rectus_R', 25: 'OFCmed_L', 26: 'OFCmed_R', 27: 'OFCant_L', 28: 'OFCant_R', 29: 'OFCpost_L', 30: 'OFCpost_R', 31: 'OFClat_L', 32: 'OFClat_R', 33: 'Insula_L', 34: 'Insula_R', 35: 'Cingulate_Ant_L', 36: 'Cingulate_Ant_R', 37: 'Cingulate_Mid_L', 38: 'Cingulate_Mid_R', 39: 'Cingulate_Post_L', 40: 'Cingulate_Post_R', 41: 'Hippocampus_L', 42: 'Hippocampus_R', 43: 'ParaHippocampal_L', 44: 'ParaHippocampal_R', 45: 'Amygdala_L', 46: 'Amygdala_R', 47: 'Calcarine_L', 48: 'Calcarine_R', 49: 'Cuneus_L', 50: 'Cuneus_R', 51: 'Lingual_L', 52: 'Lingual_R', 53: 'Occipital_Sup_L', 54: 'Occipital_Sup_R', 55: 'Occipital_Mid_L', 56: 'Occipital_Mid_R', 57: 'Occipital_Inf_L', 58: 'Occipital_Inf_R', 59: 'Fusiform_L', 60: 'Fusiform_R', 61: 'Postcentral_L', 62: 'Postcentral_R', 63: 'Parietal_Sup_L', 64: 'Parietal_Sup_R', 65: 'Parietal_Inf_L', 66: 'Parietal_Inf_R', 67: 'SupraMarginal_L', 68: 'SupraMarginal_R', 69: 'Angular_L', 70: 'Angular_R', 71: 'Precuneus_L', 72: 'Precuneus_R', 73: 'Paracentral_Lobule_L', 74: 'Paracentral_Lobule_R', 75: 'Caudate_L', 76: 'Caudate_R', 77: 'Putamen_L', 78: 'Putamen_R', 79: 'Pallidum_L', 80: 'Pallidum_R', 81: 'Thalamus_L', 82: 'Thalamus_R', 83: 'Heschl_L', 84: 'Heschl_R', 85: 'Temporal_Sup_L', 86: 'Temporal_Sup_R', 87: 'Temporal_Pole_Sup_L', 88: 'Temporal_Pole_Sup_R', 89: 'Temporal_Mid_L', 90: 'Temporal_Mid_R', 91: 'Temporal_Pole_Mid_L', 92: 'Temporal_Pole_Mid_R', 93: 'Temporal_Inf_L', 94: 'Temporal_Inf_R', 95: 'Cerebellum_Crus1_L', 96: 'Cerebellum_Crus1_R', 97: 'Cerebellum_Crus2_L', 98: 'Cerebellum_Crus2_R', 99: 'Cerebellum_3_L', 100: 'Cerebellum_3_R', 101: 'Cerebellum_4_5_L', 102: 'Cerebellum_4_5_R', 103: 'Cerebellum_6_L', 104: 'Cerebellum_6_R', 105: 'Cerebellum_7b_L', 106: 'Cerebellum_7b_R', 107: 'Cerebellum_8_L', 108: 'Cerebellum_8_R', 109: 'Cerebellum_9_L', 110: 'Cerebellum_9_R', 111: 'Cerebellum_10_L', 112: 'Cerebellum_10_R', 113: 'Vermis_1_2', 114: 'Vermis_3', 115: 'Vermis_4_5', 116: 'Vermis_6', 117: 'Vermis_7', 118: 'Vermis_8', 119: 'Vermis_9', 120: 'Vermis_10', 121: 'Thal_AV_L', 122: 'Thal_AV_R', 123: 'Thal_LP_L', 124: 'Thal_LP_R', 125: 'Thal_VA_L', 126: 'Thal_VA_R', 127: 'Thal_VL_L', 128: 'Thal_VL_R', 129: 'Thal_VPL_L', 130: 'Thal_VPL_R', 131: 'Thal_IL_L', 132: 'Thal_IL_R', 133: 'Thal_Re_L', 134: 'Thal_Re_R', 135: 'Thal_MDm_L', 136: 'Thal_MDm_R', 137: 'Thal_MDl_L', 138: 'Thal_MDl_R', 139: 'Thal_LGN_L', 140: 'Thal_LGN_R', 141: 'Thal_MGN_L', 142: 'Thal_MGN_R', 143: 'Thal_PuI_L', 144: 'Thal_PuI_R', 145: 'Thal_PuM_L', 146: 'Thal_PuM_R', 147: 'Thal_PuA_L', 148: 'Thal_PuA_R', 149: 'Thal_PuL_L', 150: 'Thal_PuL_R', 151: 'ACC_sub_L', 152: 'ACC_sub_R', 153: 'ACC_pre_L', 154: 'ACC_pre_R', 155: 'ACC_sup_L', 156: 'ACC_sup_R', 157: 'N_Acc_L', 158: 'N_Acc_R', 159: 'VTA_L', 160: 'VTA_R', 161: 'SN_pc_L', 162: 'SN_pc_R', 163: 'SN_pr_L', 164: 'SN_pr_R', 165: 'Red_N_L', 166: 'Red_N_R', 167: 'LC_L', 168: 'LC_R', 169: 'Raphe_D', 170: 'Raphe_M'}
step 3: filtering strategies (include vs. exclude)¶
many NIfTI atlases (like AAL3 or Brainnetome) contain both cortical and subcortical regions mixed together. to build a subcortical mesh atlas, we don't want to the entire cortex, but only regions within the subcortex.
yabplot provides optional include_list and exclude_list parameters.
- mixed atlases: use
include_listto grab only the deep structures. - pure subcortical atlases: if your nifti file only contains subcortical regions, you can leave these parameters blank.
here, we will create two versions of the atlas to demonstrate the flexibility.
step 3a: creating full subcortical AAL3 atlas (with cerebellum)¶
# define all subcortical keywords present in the mixed AAL3 atlas
subcortical_keywords = [
'Hippocampus', 'Amygdala', 'Caudate', 'Putamen', 'Pallidum', 'Thalamus', 'Thal',
'Cerebellum', 'Vermis', 'N_Acc', 'VTA', 'SN', 'Red_N', 'LC', 'Raphe'
]
print("--- building atlas 1: full subcortical (using include_list) ---")
yab.build_subcortical_atlas(
nii_path=aal_nii,
labels_dict=atlas_labels,
out_dir=dir_full_subcortical,
include_list=subcortical_keywords,
smooth_i=20, smooth_f=0.7
)
# check the amount of regions
regions_full = yab.get_atlas_regions(atlas=None, category='subcortical', custom_atlas_path=dir_full_subcortical)
print(f"full atlas: found {len(regions_full)} meshes.")
# plot the full subcortical atlaas
yab.plot_subcortical(
custom_atlas_path=dir_full_subcortical,
figsize=(1000, 300),
views=['superior', 'anterior', 'left_lateral']
)
--- building atlas 1: full subcortical (using include_list) --- filtered down to 82 subcortical regions to extract. extracting: Hippocampus_L (id 41)... extracting: Hippocampus_R (id 42)... extracting: Amygdala_L (id 45)... extracting: Amygdala_R (id 46)... extracting: Caudate_L (id 75)... extracting: Caudate_R (id 76)... extracting: Putamen_L (id 77)... extracting: Putamen_R (id 78)... extracting: Pallidum_L (id 79)... extracting: Pallidum_R (id 80)... [WARNING] Thalamus_L is empty in the volume! [WARNING] Thalamus_R is empty in the volume! extracting: Cerebellum_Crus1_L (id 95)... extracting: Cerebellum_Crus1_R (id 96)... extracting: Cerebellum_Crus2_L (id 97)... extracting: Cerebellum_Crus2_R (id 98)... extracting: Cerebellum_3_L (id 99)... extracting: Cerebellum_3_R (id 100)... extracting: Cerebellum_4_5_L (id 101)... extracting: Cerebellum_4_5_R (id 102)... extracting: Cerebellum_6_L (id 103)... extracting: Cerebellum_6_R (id 104)... extracting: Cerebellum_7b_L (id 105)... extracting: Cerebellum_7b_R (id 106)... extracting: Cerebellum_8_L (id 107)... extracting: Cerebellum_8_R (id 108)... extracting: Cerebellum_9_L (id 109)... extracting: Cerebellum_9_R (id 110)... extracting: Cerebellum_10_L (id 111)... extracting: Cerebellum_10_R (id 112)... extracting: Vermis_1_2 (id 113)... extracting: Vermis_3 (id 114)... extracting: Vermis_4_5 (id 115)... extracting: Vermis_6 (id 116)... extracting: Vermis_7 (id 117)... extracting: Vermis_8 (id 118)... extracting: Vermis_9 (id 119)... extracting: Vermis_10 (id 120)... extracting: Thal_AV_L (id 121)... extracting: Thal_AV_R (id 122)... extracting: Thal_LP_L (id 123)... extracting: Thal_LP_R (id 124)... extracting: Thal_VA_L (id 125)... extracting: Thal_VA_R (id 126)... extracting: Thal_VL_L (id 127)... extracting: Thal_VL_R (id 128)... extracting: Thal_VPL_L (id 129)... extracting: Thal_VPL_R (id 130)... extracting: Thal_IL_L (id 131)... extracting: Thal_IL_R (id 132)... extracting: Thal_Re_L (id 133)... [WARNING] Thal_Re_L is too small to form a 3D mesh (volume: 0.0000 mm³). dropping from atlas. extracting: Thal_Re_R (id 134)... [WARNING] Thal_Re_R is too small to form a 3D mesh (volume: 0.0000 mm³). dropping from atlas. extracting: Thal_MDm_L (id 135)... extracting: Thal_MDm_R (id 136)... extracting: Thal_MDl_L (id 137)... extracting: Thal_MDl_R (id 138)... extracting: Thal_LGN_L (id 139)... extracting: Thal_LGN_R (id 140)... extracting: Thal_MGN_L (id 141)... extracting: Thal_MGN_R (id 142)... extracting: Thal_PuI_L (id 143)... extracting: Thal_PuI_R (id 144)... extracting: Thal_PuM_L (id 145)... extracting: Thal_PuM_R (id 146)... extracting: Thal_PuA_L (id 147)... extracting: Thal_PuA_R (id 148)... extracting: Thal_PuL_L (id 149)... extracting: Thal_PuL_R (id 150)... extracting: N_Acc_L (id 157)... extracting: N_Acc_R (id 158)... extracting: VTA_L (id 159)... extracting: VTA_R (id 160)... extracting: SN_pc_L (id 161)... extracting: SN_pc_R (id 162)... extracting: SN_pr_L (id 163)... extracting: SN_pr_R (id 164)... extracting: Red_N_L (id 165)... extracting: Red_N_R (id 166)... extracting: LC_L (id 167)... extracting: LC_R (id 168)... extracting: Raphe_D (id 169)... extracting: Raphe_M (id 170)... subcortical atlas successfully saved to: dev/my_custom_atlases/subcortical/AAL3v1 full atlas: found 78 meshes.
step 3b: creating a subcortical AAL3 atlas without cerebellum¶
the cerebellum is massive and often visually blocks the smaller, deeper nuclei. what if we want an atlas without it?
we can easily pre-filter our python dictionary to only contain subcortical regions,
and then use the exclude_list parameter to drop the cerebellum.
# pre-filter dictionary so no cortical regions remain
only_subcortical_dict = {
rid: name for rid, name in atlas_labels.items()
if any(sub in name for sub in subcortical_keywords)
}
print("\n--- building atlas 2: no cerebellum (using exclude_list) ---")
yab.build_subcortical_atlas(
nii_path=aal_nii,
labels_dict=only_subcortical_dict,
out_dir=dir_no_cerebellum,
exclude_list=['Cerebellum', 'Vermis'], # ignore these specific keywords
smooth_i=20, smooth_f=0.5
)
# check the second atlas (no cerebellum)
regions_nocer = yab.get_atlas_regions(atlas=None, category='subcortical', custom_atlas_path=dir_no_cerebellum)
print(f"no cerebellum atlas: found {len(regions_nocer)} meshes.")
# plot atlas without cerebellum
yab.plot_subcortical(
custom_atlas_path=dir_no_cerebellum,
figsize=(1000, 300),
views=['superior', 'anterior', 'left_lateral']
)
--- building atlas 2: no cerebellum (using exclude_list) --- filtered down to 56 subcortical regions to extract. extracting: Hippocampus_L (id 41)... extracting: Hippocampus_R (id 42)... extracting: Amygdala_L (id 45)... extracting: Amygdala_R (id 46)... extracting: Caudate_L (id 75)... extracting: Caudate_R (id 76)... extracting: Putamen_L (id 77)... extracting: Putamen_R (id 78)... extracting: Pallidum_L (id 79)... extracting: Pallidum_R (id 80)... [WARNING] Thalamus_L is empty in the volume! [WARNING] Thalamus_R is empty in the volume! extracting: Thal_AV_L (id 121)... extracting: Thal_AV_R (id 122)... extracting: Thal_LP_L (id 123)... extracting: Thal_LP_R (id 124)... extracting: Thal_VA_L (id 125)... extracting: Thal_VA_R (id 126)... extracting: Thal_VL_L (id 127)... extracting: Thal_VL_R (id 128)... extracting: Thal_VPL_L (id 129)... extracting: Thal_VPL_R (id 130)... extracting: Thal_IL_L (id 131)... extracting: Thal_IL_R (id 132)... extracting: Thal_Re_L (id 133)... [WARNING] Thal_Re_L is too small to form a 3D mesh (volume: 0.0000 mm³). dropping from atlas. extracting: Thal_Re_R (id 134)... [WARNING] Thal_Re_R is too small to form a 3D mesh (volume: 0.0005 mm³). dropping from atlas. extracting: Thal_MDm_L (id 135)... extracting: Thal_MDm_R (id 136)... extracting: Thal_MDl_L (id 137)... extracting: Thal_MDl_R (id 138)... extracting: Thal_LGN_L (id 139)... extracting: Thal_LGN_R (id 140)... extracting: Thal_MGN_L (id 141)... extracting: Thal_MGN_R (id 142)... extracting: Thal_PuI_L (id 143)... extracting: Thal_PuI_R (id 144)... extracting: Thal_PuM_L (id 145)... extracting: Thal_PuM_R (id 146)... extracting: Thal_PuA_L (id 147)... extracting: Thal_PuA_R (id 148)... extracting: Thal_PuL_L (id 149)... extracting: Thal_PuL_R (id 150)... extracting: N_Acc_L (id 157)... extracting: N_Acc_R (id 158)... extracting: VTA_L (id 159)... extracting: VTA_R (id 160)... extracting: SN_pc_L (id 161)... extracting: SN_pc_R (id 162)... extracting: SN_pr_L (id 163)... extracting: SN_pr_R (id 164)... extracting: Red_N_L (id 165)... extracting: Red_N_R (id 166)... extracting: LC_L (id 167)... extracting: LC_R (id 168)... extracting: Raphe_D (id 169)... extracting: Raphe_M (id 170)... subcortical atlas successfully saved to: dev/my_custom_atlases/subcortical/AAL3v1_nocerebellum no cerebellum atlas: found 52 meshes.
step 6: generate quality control (qc) report¶
building 3D meshes from voxels can occasionally result in artifacts if a region in the nifti file is extremely small or fragmented.
the qc_custom_subcortical_atlas tool automatically scans your new .vtk files,
calculates their physical properties (vertex count, face count, and 3d volume in mm³),
and saves a .txt summary. it also loops through the atlas and saves a
picture of every individual structure so you can quickly scan a folder to ensure
the smoothing algorithms worked correctly!
from yabplot.atlas_builder import qc_custom_subcortical_atlas
print("\ngenerating qc report for the no-cerebellum atlas...")
qc_custom_subcortical_atlas(atlas_dir=dir_no_cerebellum)
# print("generating qc report for the full subcortical atlas...")
# qc_custom_subcortical_atlas(atlas_dir=dir_full_subcortical)
generating qc report for the no-cerebellum atlas... starting qc for 52 subcortical meshes... [Amygdala_L] vertices: 1264 | volume: 1298.6 mm³
[Amygdala_R] vertices: 1432 | volume: 1463.9 mm³
[Caudate_L] vertices: 3702 | volume: 5228.7 mm³
[Caudate_R] vertices: 3769 | volume: 5740.8 mm³
[Hippocampus_L] vertices: 4570 | volume: 6297.0 mm³
[Hippocampus_R] vertices: 4676 | volume: 6366.9 mm³
[LC_L] vertices: 152 | volume: 0.6 mm³
[LC_R] vertices: 160 | volume: 0.4 mm³
[N_Acc_L] vertices: 1064 | volume: 929.0 mm³
[N_Acc_R] vertices: 994 | volume: 798.6 mm³
[Pallidum_L] vertices: 1545 | volume: 1770.5 mm³
[Pallidum_R] vertices: 1472 | volume: 1727.5 mm³
[Putamen_L] vertices: 4019 | volume: 7042.6 mm³
[Putamen_R] vertices: 4038 | volume: 7604.2 mm³
[Raphe_D] vertices: 234 | volume: 35.9 mm³
[Raphe_M] vertices: 126 | volume: 5.0 mm³
[Red_N_L] vertices: 456 | volume: 226.1 mm³
[Red_N_R] vertices: 470 | volume: 240.2 mm³
[SN_pc_L] vertices: 479 | volume: 101.9 mm³
[SN_pc_R] vertices: 482 | volume: 116.1 mm³
[SN_pr_L] vertices: 721 | volume: 218.9 mm³
[SN_pr_R] vertices: 714 | volume: 232.6 mm³
[Thal_AV_L] vertices: 290 | volume: 36.3 mm³
[Thal_AV_R] vertices: 328 | volume: 49.0 mm³
[Thal_IL_L] vertices: 691 | volume: 154.7 mm³
[Thal_IL_R] vertices: 739 | volume: 160.3 mm³
[Thal_LGN_L] vertices: 513 | volume: 150.5 mm³
[Thal_LGN_R] vertices: 499 | volume: 150.8 mm³
[Thal_LP_L] vertices: 368 | volume: 63.1 mm³
[Thal_LP_R] vertices: 343 | volume: 62.0 mm³
[Thal_MDl_L] vertices: 548 | volume: 119.9 mm³
[Thal_MDl_R] vertices: 554 | volume: 123.0 mm³
[Thal_MDm_L] vertices: 1043 | volume: 678.0 mm³
[Thal_MDm_R] vertices: 1091 | volume: 701.7 mm³
[Thal_MGN_L] vertices: 281 | volume: 30.9 mm³
[Thal_MGN_R] vertices: 235 | volume: 22.0 mm³
[Thal_PuA_L] vertices: 528 | volume: 92.1 mm³
[Thal_PuA_R] vertices: 579 | volume: 95.5 mm³
[Thal_PuI_L] vertices: 480 | volume: 92.8 mm³
[Thal_PuI_R] vertices: 507 | volume: 108.5 mm³
[Thal_PuL_L] vertices: 391 | volume: 59.5 mm³
[Thal_PuL_R] vertices: 406 | volume: 44.3 mm³
[Thal_PuM_L] vertices: 1555 | volume: 1117.8 mm³
[Thal_PuM_R] vertices: 1548 | volume: 1128.8 mm³
[Thal_VA_L] vertices: 776 | volume: 373.4 mm³
[Thal_VA_R] vertices: 746 | volume: 374.6 mm³
[Thal_VL_L] vertices: 1596 | volume: 1748.2 mm³
Context leak detected, msgtracer returned -1
[Thal_VL_R] vertices: 1576 | volume: 1622.9 mm³
[Thal_VPL_L] vertices: 1152 | volume: 957.3 mm³
[Thal_VPL_R] vertices: 1189 | volume: 925.7 mm³
[VTA_L] vertices: 179 | volume: 3.7 mm³
[VTA_R] vertices: 171 | volume: 3.7 mm³
qc complete! check the 'dev/my_custom_atlases/subcortical/AAL3v1_nocerebellum/qc_report' folder for the report and images.
extra: using custom atlases for white matter tracts¶
while this tutorial focused on converting and visualizing subcortical volumes, yabplot supports custom atlases for white matter tracts as well.
this works exactly like the subcortical example above. you simply point custom_atlas_path to a folder, and yabplot will visualize every tract file it finds there.
- requirement: a directory containing
.trkor.tckfiles. - naming: the filename determines the region name (e.g.,
CST_Left.trkbecomes'CST_Left'). - alignment: ensure your tract files are in the same space as the background brain (standard MNI152 by default) so they align correctly.