NiWrap and Connectome Workbench#
Surface-Based Visualization Workflow for Single-Subject CIFTI#
Author: Monika Doerig
Date: 23 July 2025
Citation and Resources:#
Tools included in this workflow#
NiWrap:
@software{niwrap,
author = {The NiWrap Contributors},
title = {NiWrap: Type-Safe Neuroimaging Tool Wrappers},
url = {https://github.com/styx-api/niwrap},
note = {Preprint in preparation},
year = {2025}
Connectome Workbench:
Marcus, D. S., Harwell, J., Olsen, T., Hodge, M., Glasser, M. F., Prior, F., Jenkinson, M., Laumann, T., Curtiss, S. W., & Van Essen, D. C. (2011). Informatics and data mining tools and strategies for the human connectome project. Frontiers in neuroinformatics, 5, 4. https://doi.org/10.3389/fninf.2011.00004
Dataset#
Zhengxin Gong and Ming Zhou and Yuxuan Dai and Yushan Wen and Youyi Liu and Zonglei Zhen (2023). A large-scale fMRI dataset for the visual processing of naturalistic scenes. OpenNeuro. [Dataset] doi: doi:10.18112/openneuro.ds004496.v2.1.2
Load software tools and import python libraries#
# Load connectomeworkbench
import module
await module.load('connectomeworkbench/1.5.0')
await module.list()
['connectomeworkbench/1.5.0']
%%capture
!pip install niwrap==0.6.1 nilearn==0.12.0 nibabel==5.3.2 pandas==2.2.3
# Import the necessary libraries
import matplotlib.pyplot as plt
import os
from pathlib import Path
from niwrap_workbench.workbench import (cifti_math, cifti_math_var_params, cifti_separate, cifti_separate_metric_params,
cifti_stats, metric_smoothing, cifti_create_dense_scalar, cifti_create_dense_scalar_left_metric_params, cifti_create_dense_scalar_right_metric_params,
cifti_parcellate, file_information, cifti_label_export_table)
import re
import pandas as pd
from nilearn import plotting, surface
import nibabel as nib
import numpy as np
Data download and preparation#
Datalad#
%%bash
# --- Configuration ---
REPO_URL="https://github.com/OpenNeuroDatasets/ds004496.git"
DATASET_DIR="ds004496"
SUBJECT_ID="sub-01"
# --- Datalad Installation ---
if [ ! -d "$DATASET_DIR" ]; then
datalad install $REPO_URL
else
echo "Dataset directory '$DATASET_DIR' already exists. Skipping install."
fi
cd $DATASET_DIR
# --- Datalad Data Download ---
echo "Downloading Ciftify derivative files for $SUBJECT_ID from ds004496..."
# 1. Get the beta map from the floc task.
datalad get "derivatives/ciftify/$SUBJECT_ID/results/ses-floc_task-floc/ses-floc_task-floc_beta.dscalar.nii"
# 2. Get the corresponding T1 surface geometry files.
#datalad get "derivatives/ciftify/$SUBJECT_ID/T1w_fsLR_surface"
# 2. Get the corresponding standard surface geometry files.
datalad get "derivatives/ciftify/$SUBJECT_ID/standard_fsLR_surface"
echo "Datalad download complete for ds004496."
install(ok): /home/jovyan/workspace/books/examples/functional_imaging/ds004496 (dataset)
Downloading Ciftify derivative files for sub-01 from ds004496...
get(ok): derivatives/ciftify/sub-01/results/ses-floc_task-floc/ses-floc_task-floc_beta.dscalar.nii (
file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.ArealDistortion_FS.32k_fs_LR.dscala
r.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.ArealDistortion_MSMSulc.32k_fs_LR.d
scalar.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-
01.BA_exvivo.32k_fs_LR.dlabel.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/sta
ndard_fsLR_surface/sub-01.EdgeDistortion_MSMSulc.32k_fs_LR.dscalar.nii (file) [from s3-PUBLIC...]
ge
t(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.L.inflated.32k_fs_LR.surf.gii (file)
[from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.L.midthickness.
32k_fs_LR.surf.gii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surf
ace/sub-01.L.pial.32k_fs_LR.surf.gii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/
standard_fsLR_surface/sub-01.L.very_inflated.32k_fs_LR.surf.gii (file) [from s3-PUBLIC...]
get(ok):
derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.L.white.32k_fs_LR.surf.gii (file) [from s3-P
UBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.R.inflated.32k_fs_LR.surf
.gii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.R.m
idthickness.32k_fs_LR.surf.gii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standa
rd_fsLR_surface/sub-01.R.pial.32k_fs_LR.surf.gii (file) [from s3-PUBLIC...]
get(ok): derivatives/cif
tify/sub-01/standard_fsLR_surface/sub-01.R.very_inflated.32k_fs_LR.surf.gii (file) [from s3-PUBLIC..
.]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.R.white.32k_fs_LR.surf.gii (file
) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.aparc.32k_fs_
LR.dlabel.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/s
ub-01.aparc.DKTatlas.32k_fs_LR.dlabel.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/su
b-01/standard_fsLR_surface/sub-01.aparc.a2009s.32k_fs_LR.dlabel.nii (file) [from s3-PUBLIC...]
get(o
k): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.curvature.32k_fs_LR.dscalar.nii (file) [
from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.sulc.32k_fs_LR.d
scalar.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/standard_fsLR_surface/sub-
01.thickness.32k_fs_LR.dscalar.nii (file) [from s3-PUBLIC...]
get(ok): derivatives/ciftify/sub-01/st
andard_fsLR_surface (directory)
action summary:
get (ok: 21)
Datalad download complete for ds004496.
[INFO] Attempting a clone into /home/jovyan/workspace/books/examples/functional_imaging/ds004496
[I
NFO] Attempting to clone from https://github.com/OpenNeuroDatasets/ds004496.git to /home/jovyan/work
space/books/examples/functional_imaging/ds004496
[INFO] Start enumerating objects
[INFO] Start counting objects
[INFO] Start compressing objects
[INFO] Start receiving objects
[INFO] Start resolving deltas
[INFO] Completed clone attempts for Dataset(/home/jovyan/workspace/books/examples/functional_imaging
/ds004496)
[INFO] Remote origin not usable by git-annex; setting annex-ignore
[INFO] https://github.com/OpenNeuroDatasets/ds004496.git/config download failed: Not Found
# --- Define File Paths ---
DATASET_DIR = Path("ds004496")
SUBJECT_ID = "sub-01"
# The root directory of the Ciftify derivatives
deriv_dir = DATASET_DIR / "derivatives" / "ciftify"
# 1. Input File: The statistical map
beta_map_path = (
deriv_dir
/ SUBJECT_ID
/ "results"
/ "ses-floc_task-floc"
/ "ses-floc_task-floc_beta.dscalar.nii"
)
# 2. Input Files: The surface geometry files.
surface_dir = deriv_dir / SUBJECT_ID / "standard_fsLR_surface"
left_surf_path = surface_dir / f"{SUBJECT_ID}.L.midthickness.32k_fs_LR.surf.gii"
right_surf_path = surface_dir / f"{SUBJECT_ID}.R.midthickness.32k_fs_LR.surf.gii"
left_infl_path = surface_dir / f"{SUBJECT_ID}.L.inflated.32k_fs_LR.surf.gii"
right_infl_path = surface_dir / f"{SUBJECT_ID}.R.inflated.32k_fs_LR.surf.gii"
sulcal_depth_path = surface_dir / f"{SUBJECT_ID}.sulc.32k_fs_LR.dscalar.nii"
# --- Verify files exist before we start ---
assert left_surf_path.exists(), f"Left mid-surface not found: {left_surf_path}"
assert right_surf_path.exists(), f"Right mid-surface not found: {right_surf_path}"
assert left_infl_path.exists(), f"Left inflated surface not found: {left_surf_path}"
assert right_infl_path.exists(), f"Right inflated surface not found: {right_surf_path}"
assert sulcal_depth_path.exists(), f"Sulcal depth not found: {sulcal_depth_path}"
print("✅ All necessary files found. Ready for analysis.")
✅ All necessary files found. Ready for analysis.
# Download Schaefer atlas (skip if exists)
! wget -nc -q --show-progress https://github.com/ThomasYeoLab/CBIG/raw/master/stable_projects/brain_parcellation/Schaefer2018_LocalGlobal/Parcellations/HCP/fslr32k/cifti/Schaefer2018_400Parcels_17Networks_order.dlabel.nii
Schaefer2 0%[ ] 0 --.-KB/s
Schaefer2018_400Par 100%[===================>] 667.06K --.-KB/s in 0.01s
Surface-Based Visualization Workflow#
This workflow demonstrates a surface-based approach for processing and visualizing single-subject CIFTI contrast data using niwrap and Connectome Workbench. These cifti files consist of 91,282 grayordinates: 32,492 cortical vertices per hemisphere and 26,298 subcortical voxels with approximately 2 mm spatial resolution. In this notebook we are focusing on the cortical vertices.
The goal is to showcase how multi-contrast CIFTI data can be extracted, thresholded, separated by hemisphere, smoothed, and parcellated for comprehensive visualization and analysis on the cortical surface.
⚠️ Important Notice: This tutorial uses niwrap, which is in very early release stage. It is not recommended for production use unless you are willing to debug, fix, and contribute descriptors. Due to version compatibility issues between niwrap and different Connectome Workbench installations, some steps in this workflow fall back to direct wb_command calls where niwrap functions were not compatible with the available Workbench version.
Therefore, this example is not intended for statistical inference but serves to demonstrate how niwrap can be used alongside Connectome Workbench for surface-based processing and visualization. The selected steps highlight a practical path for extracting, thresholding, separating, and smoothing single-subject contrast data to create interpretable visual outputs:
Split Maps
Thresholding
Surface Separation
Sulcal Depth Separation
Statistical Summary
Surface Smoothing
Parcellation
1. Split Maps#
A multi-map statistical image containing 5 different contrast maps is separated into individual maps using wb_command -cifti-merge. In this example, the “Face - others” contrast (column 3) is extracted from the original multi-map CIFTI file to create a single contrast map for focused analysis and visualization.
#create output directory
output_dir = Path("./niwrap_results").absolute()
output_dir.mkdir(parents=True, exist_ok=True)
Note: These steps use direct wb_command due to compatibility issues with the current niwrap implementation.
! wb_command -file-information $beta_map_path -only-map-names
Character - others
Body - others
Face - others
Place - others
Object - others
! wb_command -cifti-merge ./niwrap_results/beta_face.dscalar.nii -cifti $beta_map_path -column 3
! wb_command -file-information ./niwrap_results/beta_face.dscalar.nii -only-map-names
Face - others
2. Thresholding a CIFTI map - cifti_math#
The “Face - others” contrast map is thresholded using a data-driven approach rather than a fixed value. The 95th percentile of non-NaN values is computed and used as the threshold, retaining only the strongest contrast values while preserving their original magnitudes (rather than creating a binary mask). This approach adapts to the actual data distribution and highlights the top 5% of contrast values.
# Load the beta map
img = nib.load("./niwrap_results/beta_face.dscalar.nii")
data = img.get_fdata()
# Mask out NaNs (some CIFTI data may have them)
masked_data = data[~np.isnan(data)]
# Compute 95th percentile
percentile_95 = np.percentile(masked_data, 95)
print(f"95th percentile value: {percentile_95}")
95th percentile value: 1.081139940023422
output_file = output_dir / "beta_face_thresholded.dscalar.nii"
result_cifti = cifti_math(
expression=f"x * (x > {percentile_95})", # Retain original values > 95th percentile
cifti_out=str(output_file),
var=[
cifti_math_var_params(
name="x",
cifti=str("./niwrap_results/beta_face.dscalar.nii")
)
]
)
[D] Running command: wb_command -cifti-math 'x * (x > 1.081139940023422)' /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/beta_face_thresholded.dscalar.nii -var x /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/beta_face.dscalar.nii
[I] parsed 'x * (x > 1.081139940023422)' as 'x * (x > 1.08113994002342)'
[I] Executed cifti-math in 0:00:01.464513
3. Surface separation - cifti_separate#
The thresholded CIFTI map is separated into left and right hemisphere GIFTI metric files. This step allows independent processing and visualization of each cortical hemisphere using standard surface templates.
metric = [
cifti_separate_metric_params(
structure="CORTEX_LEFT",
metric_out=str(output_dir / "sub-01_thresh_lh.func.gii"),
),
cifti_separate_metric_params(
structure="CORTEX_RIGHT",
metric_out=str(output_dir / "sub-01_thresh_rh.func.gii"),
)
]
result_separate = cifti_separate(
cifti_in=str(result_cifti.cifti_out), #thresholded image
direction="COLUMN",
metric=metric
)
[D] Running command: wb_command -cifti-separate /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/beta_face_thresholded.dscalar.nii COLUMN -metric CORTEX_LEFT /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/sub-01_thresh_lh.func.gii -metric CORTEX_RIGHT /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/sub-01_thresh_rh.func.gii
[I] Executed cifti-separate in 0:00:01.304817
4. Sulcal Depth Separation#
A sulcal depth map in CIFTI format is split into left and right hemisphere GIFTI metric files. These depth maps are used as anatomical underlays to provide context for the overlaid activation maps during surface visualization.
metric = [
cifti_separate_metric_params(
structure="CORTEX_LEFT",
metric_out=str(output_dir / "sub-01.L.sulc.func.gii"),
),
cifti_separate_metric_params(
structure="CORTEX_RIGHT",
metric_out=str(output_dir / "sub-01.R.sulc.func.gii"),
)
]
result_separate_sulc = cifti_separate(
cifti_in=str(sulcal_depth_path),
direction="COLUMN",
metric=metric
)
[D] Running command: wb_command -cifti-separate /home/jovyan/workspace/books/examples/functional_imaging/ds004496/derivatives/ciftify/sub-01/standard_fsLR_surface/sub-01.sulc.32k_fs_LR.dscalar.nii COLUMN -metric CORTEX_LEFT /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/sub-01.L.sulc.func.gii -metric CORTEX_RIGHT /home/jovyan/workspace/books/examples/functional_imaging/niwrap_results/sub-01.R.sulc.func.gii
[I] Executed cifti-separate in 0:00:00.986667
Surface Plotting with Nilearn#
1. Interactive Visualization with view_surf#
# Plot thresholde maps on cortical surfaces with interactive rotation and zooming
# Load meshes and concatenate coordinates
mesh_lh = surface.load_surf_mesh(str(left_surf_path))
mesh_rh = surface.load_surf_mesh(str(right_surf_path))
coords_combined = np.vstack([mesh_lh.coordinates, mesh_rh.coordinates])
# Offset RH face indices by LH vertex count
offset = mesh_lh.coordinates.shape[0]
faces_rh_offset = mesh_rh.faces + offset
# Concatenate faces
faces_combined = np.vstack([mesh_lh.faces, faces_rh_offset])
# Combine into mesh tuple
combined_mesh_mid = (coords_combined, faces_combined)
# Load and concatenate data
data_lh = nib.load(str(result_separate.metric[0].metric_out)).darrays[0].data
data_rh = nib.load(str(result_separate.metric[1].metric_out)).darrays[0].data
data_combined = np.concatenate([data_lh, data_rh])
# Plot both hemispheres
view = plotting.view_surf(
surf_mesh=combined_mesh_mid,
surf_map=data_combined,
hemi='both',
threshold=0.0001, #for better contrast of the brain surface
title="Face > Others: fLoc Functional Localizer Results (Top 5% Activation)",
title_fontsize=18,
colorbar=False,
darkness=None,
)
view