Tutorial 7: Defining Custom Parcellations#
While NeuroCAPs leverages Nilearn’s fetch functions for the Schaefer and AAL, additional parcellations (lateralized and non-lateralized) can be manually defined. For custom parcellation approaches, three subkeys are recognized: “maps”, “nodes”, and “regions”. For additional details on these subkeys, refer to the “Custom Parcellations” sub-section.
Note: Non-lateralized parcellations are supported in versions >= 0.30.0.
There are three methods to create the “Custom” parcel_approach.
[8]:
# Download packages
try:
import neurocaps
except:
!pip install neurocaps[windows,demo]
# Set headless display for google colab
import os, sys
if "google.colab" in sys.modules:
os.environ["DISPLAY"] = ":0.0"
!apt-get install -y xvfb
!Xvfb :0 -screen 0 1024x768x24 &> /dev/null &
!Xvfb :0 -screen 0 1024x768x24 &> /dev/null &
1. Manual Creation#
[9]:
# Fetching atlas NiFTI image and labels from Github
import os, subprocess, sys
demo_dir = "neurocaps_demo"
os.makedirs(demo_dir, exist_ok=True)
if sys.platform != "win32":
cmd = [
[
"wget",
"-q",
"-P",
demo_dir,
"https://github.com/wayalan/HCPex/raw/main/HCPex_v1.1/HCPex_LookUpTable.txt",
],
[
"wget",
"-q",
"-P",
demo_dir,
"https://github.com/wayalan/HCPex/raw/main/HCPex_v1.1/HCPex_2mm.nii",
],
]
else:
cmd = [
[
"curl",
"-L",
"-o",
f"{demo_dir}\\HCPex_LookUpTable.txt",
"https://github.com/wayalan/HCPex/raw/main/HCPex_v1.1/HCPex_LookUpTable.txt",
],
[
"curl",
"-L",
"-o",
f"{demo_dir}\\HCPex.nii.gz",
"https://github.com/wayalan/HCPex/raw/main/HCPex_v1.1/HCPex_2mm.nii",
],
]
for command in cmd:
subprocess.run(command, check=True)
[10]:
import pandas as pd
parcel_approach = {"Custom": {}}
# Set path to parcellation NifTI image
parcel_approach["Custom"]["maps"] = os.path.join(demo_dir, "HCPex.nii.gz")
# Get the nodes and ensure that the first node (index 0) is the first non-background node
parcel_approach["Custom"]["nodes"] = pd.read_csv(
os.path.join(demo_dir, "HCPex_LookUpTable.txt"),
sep=None,
engine="python",
)["Label"].values[1:]
# Setting the region names and their corresponding indices in the "nodes" list
# in this case it is just the label id - 1
parcel_approach["Custom"]["regions"] = {
"Primary Visual": {"lh": [0], "rh": [180]},
"Early Visual": {"lh": [1, 2, 3], "rh": [181, 182, 183]},
"Dorsal Stream Visual": {"lh": range(4, 10), "rh": range(184, 190)},
"Ventral Stream Visual": {"lh": range(10, 17), "rh": range(190, 197)},
"MT+ Complex": {"lh": range(17, 26), "rh": range(197, 206)},
"SomaSens Motor": {"lh": range(26, 31), "rh": range(206, 211)},
"ParaCentral MidCing": {"lh": range(31, 40), "rh": range(211, 220)},
"Premotor": {"lh": range(40, 47), "rh": range(220, 227)},
"Posterior Opercular": {"lh": range(47, 52), "rh": range(227, 232)},
"Early Auditory": {"lh": range(52, 59), "rh": range(232, 239)},
"Auditory Association": {"lh": range(59, 67), "rh": range(239, 247)},
"Insula FrontalOperc": {"lh": range(67, 79), "rh": range(247, 259)},
"Medial Temporal": {"lh": range(79, 87), "rh": range(259, 267)},
"Lateral Temporal": {"lh": range(87, 95), "rh": range(267, 275)},
"TPO": {"lh": range(95, 100), "rh": range(275, 280)},
"Superior Parietal": {"lh": range(100, 110), "rh": range(280, 290)},
"Inferior Parietal": {"lh": range(110, 120), "rh": range(290, 300)},
"Posterior Cingulate": {"lh": range(120, 133), "rh": range(300, 313)},
"AntCing MedPFC": {"lh": range(133, 149), "rh": range(313, 329)},
"OrbPolaFrontal": {"lh": range(149, 158), "rh": range(329, 338)},
"Inferior Frontal": {"lh": range(158, 167), "rh": range(338, 347)},
"Dorsolateral Prefrontal": {"lh": range(167, 180), "rh": range(347, 360)},
"Subcortical Regions": {"lh": range(360, 393), "rh": range(393, 426)},
}
The “lh” and “rh” subkeys aren’t required. The following configurations are also acceptable.
[11]:
# Non-lateralized regions
regions_non_lateralized = {
"Primary Visual": [0, 180],
"Early Visual": [1, 2, 3, 181, 182, 183],
"Dorsal Stream Visual": [*range(4, 10), *range(184, 190)],
"Ventral Stream Visual": [*range(10, 17), *range(190, 197)],
"MT+ Complex": [*range(17, 26), *range(197, 206)],
"SomaSens Motor": [*range(26, 31), *range(206, 211)],
"ParaCentral MidCing": [*range(31, 40), *range(211, 220)],
"Premotor": [*range(40, 47), *range(220, 227)],
"Posterior Opercular": [*range(47, 52), *range(227, 232)],
"Early Auditory": [*range(52, 59), *range(232, 239)],
"Auditory Association": [*range(59, 67), *range(239, 247)],
"Insula FrontalOperc": [*range(67, 79), *range(247, 259)],
"Medial Temporal": [*range(79, 87), *range(259, 267)],
"Lateral Temporal": [*range(87, 95), *range(267, 275)],
"TPO": [*range(95, 100), *range(275, 280)],
"Superior Parietal": [*range(100, 110), *range(280, 290)],
"Inferior Parietal": [*range(110, 120), *range(290, 300)],
"Posterior Cingulate": [*range(120, 133), *range(300, 313)],
"AntCing MedPFC": [*range(133, 149), *range(313, 329)],
"OrbPolaFrontal": [*range(149, 158), *range(329, 338)],
"Inferior Frontal": [*range(158, 167), *range(338, 347)],
"Dorsolateral Prefrontal": [*range(167, 180), *range(347, 360)],
"Subcortical Regions": [*range(360, 393), *range(393, 426)],
}
# Mix of lateralized and non-lateralized regions
regions_mixed = {
# Non-Lateralized Regions
"Primary Visual": [*[0], *[180]],
"Early Visual": [*[1, 2, 3], *[181, 182, 183]],
"Dorsal Stream Visual": [*range(4, 10), *range(184, 190)],
"Ventral Stream Visual": [*range(10, 17), *range(190, 197)],
"ParaCentral MidCing": [*range(31, 40), *range(211, 220)],
"Posterior Cingulate": [*range(120, 133), *range(300, 313)],
"AntCing MedPFC": [*range(133, 149), *range(313, 329)],
"Subcortical Regions": [*range(360, 393), *range(393, 426)],
# Lateralized Regions
"MT+ Complex": {"lh": range(17, 26), "rh": range(197, 206)},
"SomaSens Motor": {"lh": range(26, 31), "rh": range(206, 211)},
"Premotor": {"lh": range(40, 47), "rh": range(220, 227)},
"Posterior Opercular": {"lh": range(47, 52), "rh": range(227, 232)},
"Early Auditory": {"lh": range(52, 59), "rh": range(232, 239)},
"Auditory Association": {"lh": range(59, 67), "rh": range(239, 247)},
"Insula FrontalOperc": {"lh": range(67, 79), "rh": range(247, 259)},
"Medial Temporal": {"lh": range(79, 87), "rh": range(259, 267)},
"Lateral Temporal": {"lh": range(87, 95), "rh": range(267, 275)},
"TPO": {"lh": range(95, 100), "rh": range(275, 280)},
"Superior Parietal": {"lh": range(100, 110), "rh": range(280, 290)},
"Inferior Parietal": {"lh": range(110, 120), "rh": range(290, 300)},
"OrbPolaFrontal": {"lh": range(149, 158), "rh": range(329, 338)},
"Inferior Frontal": {"lh": range(158, 167), "rh": range(338, 347)},
"Dorsolateral Prefrontal": {"lh": range(167, 180), "rh": range(347, 360)},
}
2. Generate from a tabular metadata file#
[12]:
import pandas as pd, numpy as np, sys, subprocess
from neurocaps.utils import generate_custom_parcel_approach
# Fetching atlas NiFTI image and labels from Github
if sys.platform != "win32":
cmd = [
[
"wget",
"-q",
"-P",
"neurocaps_demo",
"https://github.com/PennLINC/AtlasPack/raw/main/atlas-4S156Parcels_dseg.tsv",
],
]
else:
cmd = [
[
"curl",
"-L",
"-o",
"neurocaps_demo\\atlas-4S156Parcels_dseg.tsv",
"https://github.com/PennLINC/AtlasPack/raw/main/atlas-4S156Parcels_dseg.tsv",
],
]
for command in cmd:
subprocess.run(command, check=True)
# For this parcellation, the metadata contains the labels and the network mappings though
# certain nodes in the Cerebellum, Subcortical, and Thalamus have NaN values in the
# column denoting network affiliation
df = pd.read_csv(
r"neurocaps_demo\atlas-4S156Parcels_dseg.tsv",
sep="\t",
)
# Replacing null values in the "network_label" column with values in "atlas_name"
df["network_label"] = np.where(df["network_label"].isnull(), df["atlas_name"], df["network_label"])
# Simplifying names for for certain names in "network_label"
df.loc[df["network_label"].str.contains("Subcortical", na=False), "network_label"] = "Subcortical"
df.loc[df["network_label"].str.contains("Thalamus", na=False), "network_label"] = "Thalamus"
# Create empty file for demonstration purposes
with open(r"neurocaps_demo\temp_parc_map.nii.gz", "w") as f:
pass
# Creating custom parcel approach dictionary
parcel_approach = generate_custom_parcel_approach(
df,
maps_path=r"neurocaps_demo\temp_parc_map.nii.gz",
column_map={"nodes": "label", "regions": "network_label"},
)
The following code creates a lateralized version of the parcel_approach. Note that the lateralization information is specific case in CAP.caps2plot when visual_scope is set to “nodes” and the add_custom_node_labels kwarg is True.
[13]:
# Create a hemisphere column
df["hemisphere_labels"] = df["hemisphere_labels"] = df["label"].str.extract(r"^(LH|RH)")
# Creating custom parcel approach dictionary
parcel_approach = generate_custom_parcel_approach(
df,
maps_path=r"neurocaps_demo\temp_parc_map.nii.gz",
column_map={"nodes": "label", "regions": "network_label", "hemispheres": "hemisphere_labels"},
hemisphere_map={"lh": ["LH"], "rh": ["RH"]},
)
3. Fetching a preset “Custom” parcel_approach#
Note: Currently only “HCPex”, “4S”, and “Gordon” are supported.
[14]:
from neurocaps.utils import fetch_preset_parcel_approach
parcel_approach = fetch_preset_parcel_approach("HCPex")
parcel_approach = fetch_preset_parcel_approach("4S", n_nodes=456)
2025-07-21 14:31:59,242 neurocaps.utils._io [WARNING] Creating the following non-existent path: C:\Users\donis\neurocaps_data.
2025-07-21 14:31:59,245 neurocaps.utils.datasets._fetch [INFO] Downloading the following files from OSF: 'atlas-HCPex_desc-CustomParcelApproach.json', 'tpl-MNI152NLin2009cAsym_atlas-HCPex_2mm.nii.gz'
[fetch_single_file] Downloading data from https://osf.io/rdbfv/download ...
[fetch_single_file] ...done. (3 seconds, 0 min)
[fetch_single_file] Downloading data from https://osf.io/mx4d6/download ...
[fetch_single_file] ...done. (1 seconds, 0 min)
2025-07-21 14:32:03,459 neurocaps.utils.datasets._fetch [INFO] Downloading the following files from OSF: 'atlas-4S456Parcels_desc-CustomParcelApproach.json', 'tpl-MNI152NLin2009cAsym_atlas-4S456Parcels_res-01_dseg.nii.gz'
[fetch_single_file] Downloading data from https://osf.io/juyac/download ...
[fetch_single_file] ...done. (2 seconds, 0 min)
[fetch_single_file] Downloading data from https://osf.io/tpz6y/download ...
[fetch_single_file] ...done. (3 seconds, 0 min)