(advanced) using custom cortical atlas¶
[note] this pipeline requires Connectome Workbench (wb_command) to be installed on your system to run the anatomical projections.
sometimes you have a standard volumetric atlas (like the AAL3 atlas) that you want to visualize on the cortical surface using yabplot.plot_cortical.
since plot_cortical works with continuous surface vertices rather than blocky 3D voxels, we cannot simply pass the nifti file directly. instead, we need to project the 3D volume onto the 2d cortical sheet.
this tutorial uses yabplot's built-in wrapper for Connectome Workbench to perform highly accurate "ribbon-constrained" mapping, resulting in smooth, anatomically precise borders.
inputs and outputs¶
we start with:
- atlas volume (
.nii.gz): the 3D nifti file where each voxel has an integer region id. - atlas metadata (
.txt,.xml, etc.): a file listing what each region id represents (e.g., id 1 = "Precentral_L"). (note: because every atlas publisher formats this differently, the parsing step always requires a custom script for your specific atlas)
we need to generate:
- vertex map (
.csv): a single column array of integers merging the left and right hemispheres. row 0 is the region id for vertex 0, row 1 for vertex 1, etc. - lookup table (
.txt): a file that tellsyabplotthe exact string name and rgb color to use for every integer id found in the csv map.
the workflow¶
to achieve this conversion, our script will:
- parse the custom metadata into the strict Connectome Workbench label format.
- fetch standard high-resolution surface meshes (
fsLR32k). - project the nifti volume strictly into the cortical ribbon (between the white matter and pial surfaces).
- refine the map by erasing deep subcortical structures (if there is any) and masking out the medial wall.
- apply iterative surface smoothing and hole-filling to finalize the borders.
step 1: configuration & file paths¶
import os
import numpy as np
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'
# define where the new atlas will be saved
workdir_wb = 'dev/my_custom_atlases/cortical/AAL3v1'
wb_label_list = os.path.join(workdir_wb, 'wb_labels.txt')
os.makedirs(workdir_wb, exist_ok=True)
step 2: format the lookup table (LUT)¶
connectome workbench requires a highly specific text format. because every atlas
publisher formats their .txt or .csv files differently, you must write a short loop
to translate your specific atlas into the standard format required by wb_command.
the format requires two lines per region:
region_name
id r g b a (where 'a' is alpha opacity, usually 255)
with open(aal_txt, 'r') as f_in, open(wb_label_list, 'w') as f_out:
for line in f_in:
parts = line.strip().split()
if len(parts) >= 2:
try:
rid = int(parts[0])
# clean up names to prevent plotting errors downstream
name = parts[1].replace(' ', '_').replace('/', '-')
# seed random RGB colors so they remain identical every time you run the script
np.random.seed(rid)
r, g, b = np.random.randint(50, 255, 3)
# write the two required lines
f_out.write(f"{name}\n{rid} {r} {g} {b} 255\n")
except ValueError:
continue
step 3: exclude subcortical structures¶
deep brain structures (like the thalamus or amygdala) are 3D volumes that do not belong on the 2D cortical sheet. if left unchecked, the projection algorithm could smear them across the medial wall of the brain. we define them here so the builder can actively delete them from the final map.
exclude_keywords = [
'Hippocampus', 'Amygdala', 'Caudate', 'Putamen', 'Pallidum', 'Thalamus', 'Thal',
'Cerebellum', 'Vermis', 'N_Acc', 'VTA', 'SN', 'Red_N', 'LC', 'Raphe'
]
step 4: run the atlas builder¶
this function manages the heavy lifting: downloading standard fsLR32k templates, wrapping the terminal commands, masking the medial wall, filling gaps, and aggressively smoothing boundaries to remove jagged voxel artifacts.
print("building cortical atlas...")
yab.build_cortical_atlas(
nii_path=aal_nii,
wb_txt_path=wb_label_list,
out_dir=os.path.join(workdir_wb, "atlas"),
exclude_list=exclude_keywords
)
building cortical atlas... fetching standard surfaces... running volume-to-surface projection... found 88 initial cortical regions. mapping and cleaning... building surface adjacency and filling holes... smoothing boundaries... [WARNING] Cingulate_Ant_L (id 35) lost during smoothing/masking. dropping from lut. [WARNING] Cingulate_Ant_R (id 36) lost during smoothing/masking. dropping from lut. final polished atlas saved to: dev/my_custom_atlases/cortical/AAL3v1/atlas saved 86 regions (2 empty regions dropped).
step 5: visualize and verify¶
the pipeline automatically creates the required .csv and .txt files in your
output directory. you can now pass this folder directly into yabplot!
yab.plot_cortical(custom_atlas_path=os.path.join(workdir_wb, "atlas"))
step 6: quality control (QC)¶
converting 3D volumes to 2D surfaces can sometimes result in dropouts (where regions are too small to hit the surface mesh) or anatomical bleed.
the qc_custom_cortical_atlas function automatically counts the exact number of vertices assigned
to each region and saves a .txt summary. it also silently loops through the atlas
and saves a picture of every individual region so you can quickly scan a folder
to ensure everything is anatomically correct!
from yabplot.atlas_builder import qc_custom_cortical_atlas
print("generating qc report...")
qc_custom_cortical_atlas(atlas_dir=os.path.join(workdir_wb, "atlas"))
generating qc report... starting qc for 86 regions... [Precentral_L] id: 1 | vertices: 1286
[Precentral_R] id: 2 | vertices: 1596
[Frontal_Sup_2_L] id: 3 | vertices: 1490
[Frontal_Sup_2_R] id: 4 | vertices: 1570
[Frontal_Mid_2_L] id: 5 | vertices: 1370
[Frontal_Mid_2_R] id: 6 | vertices: 1471
[Frontal_Inf_Oper_L] id: 7 | vertices: 349
[Frontal_Inf_Oper_R] id: 8 | vertices: 484
[Frontal_Inf_Tri_L] id: 9 | vertices: 872
[Frontal_Inf_Tri_R] id: 10 | vertices: 673
[Frontal_Inf_Orb_2_L] id: 11 | vertices: 232
[Frontal_Inf_Orb_2_R] id: 12 | vertices: 243
[Rolandic_Oper_L] id: 13 | vertices: 577
[Rolandic_Oper_R] id: 14 | vertices: 668
[Supp_Motor_Area_L] id: 15 | vertices: 702
[Supp_Motor_Area_R] id: 16 | vertices: 940
[Olfactory_L] id: 17 | vertices: 105
[Olfactory_R] id: 18 | vertices: 177
[Frontal_Sup_Medial_L] id: 19 | vertices: 749
[Frontal_Sup_Medial_R] id: 20 | vertices: 640
[Frontal_Med_Orb_L] id: 21 | vertices: 220
[Frontal_Med_Orb_R] id: 22 | vertices: 269
[Rectus_L] id: 23 | vertices: 267
[Rectus_R] id: 24 | vertices: 305
[OFCmed_L] id: 25 | vertices: 212
[OFCmed_R] id: 26 | vertices: 205
[OFCant_L] id: 27 | vertices: 78
[OFCant_R] id: 28 | vertices: 120
[OFCpost_L] id: 29 | vertices: 204
[OFCpost_R] id: 30 | vertices: 128
[OFClat_L] id: 31 | vertices: 62
[OFClat_R] id: 32 | vertices: 34
[Insula_L] id: 33 | vertices: 1156
[Insula_R] id: 34 | vertices: 1271
[Cingulate_Mid_L] id: 37 | vertices: 1223
[Cingulate_Mid_R] id: 38 | vertices: 1287
[Cingulate_Post_L] id: 39 | vertices: 159
[Cingulate_Post_R] id: 40 | vertices: 94
[ParaHippocampal_L] id: 43 | vertices: 611
[ParaHippocampal_R] id: 44 | vertices: 724
[Calcarine_L] id: 47 | vertices: 859
[Calcarine_R] id: 48 | vertices: 708
[Cuneus_L] id: 49 | vertices: 462
[Cuneus_R] id: 50 | vertices: 546
[Lingual_L] id: 51 | vertices: 867
[Lingual_R] id: 52 | vertices: 1013
[Occipital_Sup_L] id: 53 | vertices: 380
[Occipital_Sup_R] id: 54 | vertices: 370
Context leak detected, msgtracer returned -1
[Occipital_Mid_L] id: 55 | vertices: 1278
[Occipital_Mid_R] id: 56 | vertices: 771
[Occipital_Inf_L] id: 57 | vertices: 319
[Occipital_Inf_R] id: 58 | vertices: 272
[Fusiform_L] id: 59 | vertices: 769
[Fusiform_R] id: 60 | vertices: 833
[Postcentral_L] id: 61 | vertices: 2017
[Postcentral_R] id: 62 | vertices: 2053
[Parietal_Sup_L] id: 63 | vertices: 938
[Parietal_Sup_R] id: 64 | vertices: 606
[Parietal_Inf_L] id: 65 | vertices: 1537
[Parietal_Inf_R] id: 66 | vertices: 774
[SupraMarginal_L] id: 67 | vertices: 674
[SupraMarginal_R] id: 68 | vertices: 1115
[Angular_L] id: 69 | vertices: 560
[Angular_R] id: 70 | vertices: 677
[Precuneus_L] id: 71 | vertices: 1480
[Precuneus_R] id: 72 | vertices: 1430
[Paracentral_Lobule_L] id: 73 | vertices: 549
[Paracentral_Lobule_R] id: 74 | vertices: 440
[Heschl_L] id: 83 | vertices: 102
[Heschl_R] id: 84 | vertices: 147
[Temporal_Sup_L] id: 85 | vertices: 1103
[Temporal_Sup_R] id: 86 | vertices: 1359
[Temporal_Pole_Sup_L] id: 87 | vertices: 348
Context leak detected, msgtracer returned -1
[Temporal_Pole_Sup_R] id: 88 | vertices: 467
[Temporal_Mid_L] id: 89 | vertices: 2033
[Temporal_Mid_R] id: 90 | vertices: 1561
[Temporal_Pole_Mid_L] id: 91 | vertices: 131
[Temporal_Pole_Mid_R] id: 92 | vertices: 218
[Temporal_Inf_L] id: 93 | vertices: 1029
[Temporal_Inf_R] id: 94 | vertices: 987
[ACC_sub_L] id: 151 | vertices: 33
[ACC_sub_R] id: 152 | vertices: 30
[ACC_pre_L] id: 153 | vertices: 182
[ACC_pre_R] id: 154 | vertices: 241
[ACC_sup_L] id: 155 | vertices: 156
[ACC_sup_R] id: 156 | vertices: 130
qc complete! check the 'dev/my_custom_atlases/cortical/AAL3v1/atlas/qc_report' folder for the report and images.