Skip to content

Commit f181016

Browse files
committed
Add script for converting CIFTI labels to FSL-compatible GIFTIs
1 parent 925ada6 commit f181016

File tree

1 file changed

+206
-0
lines changed

1 file changed

+206
-0
lines changed

cifti_label2surf.py

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Modelled on FSL's label2surf command, allows transforming CIFTI or GIFTI label
5+
files to FSL-compatible GIFTI files combining surface and label structures.
6+
These are, for example, suitable for input to probtrackx.
7+
8+
Arguments
9+
---------
10+
-s|--surf
11+
Path to input surface GIFTI (probably white, midthickness, or pial surface)
12+
-o|--out
13+
Path to output surface+label GIFTI
14+
-l|--label
15+
Path to CIFTI/GIFTI label file (can be .dlabel.nii, .dscalar.nii,
16+
.label.gii, or .shape.gii). Can specify flag more than once to supply
17+
multiple label files - in this case, the script will take the union over
18+
all labels.
19+
--hemi
20+
Surface hemisphere. Choose from L, R, lh, or rh. Required for CIFTI labels,
21+
but ignored for GIFTI labels.
22+
--label-ids
23+
Name (for CIFTI dlabels only) or numeric value of specific label to use
24+
if multiple labels are encoded in same file. If multiple values given,
25+
will take union of all corresponding labels. If omitted (default), take
26+
union of all labels. Can also specify flag multiple times to apply
27+
different selection for each input label.
28+
-m|--map
29+
Index of map/darray to select within CIFTI/GIFTI file (zero-indexed).
30+
The default is 0. Can also specify flag multiple times to apply different
31+
index to each input label.
32+
-h|--help
33+
Print help and exit
34+
35+
"""
36+
37+
import sys, argparse
38+
import numpy as np
39+
import nibabel as nib
40+
41+
42+
### Custom funcs ###
43+
44+
def get_struct_info(struct, brain_models_axis):
45+
for this_struct, slice_, model in brain_models_axis.iter_structures():
46+
if this_struct == struct:
47+
return slice_, model
48+
raise Exception(f"No information for structure '{struct}'")
49+
50+
51+
### Parse args ###
52+
53+
parser = argparse.ArgumentParser(
54+
formatter_class=argparse.ArgumentDefaultsHelpFormatter, usage=__doc__
55+
)
56+
57+
parser.add_argument('-s', '--surf', required=True, help='Input surface')
58+
parser.add_argument('-o', '--out', required=True, help='Output surface')
59+
parser.add_argument('-l', '--label', required=True, action='append',
60+
help='CIFTI label file(s)')
61+
parser.add_argument('--hemi', choices=['L','R','lh','rh'],
62+
help='Hemisphere to extract label from')
63+
parser.add_argument('--label-ids', action='append', nargs='+',
64+
help='Numeric ID or name of label')
65+
parser.add_argument('-m', '--map', action='append', type=int,
66+
help='Index of map to extract label from')
67+
68+
if len(sys.argv) < 2:
69+
parser.print_help()
70+
sys.exit()
71+
72+
args = parser.parse_args()
73+
surf_file = args.surf
74+
outfile = args.out
75+
label_files = args.label
76+
hemi = args.hemi
77+
label_ids = args.label_ids
78+
mapNs = args.map
79+
80+
if hemi is None:
81+
hemi_struct = None
82+
elif hemi in ['L','lh']:
83+
hemi_struct = 'CIFTI_STRUCTURE_CORTEX_LEFT'
84+
elif hemi in ['R','rh']:
85+
hemi_struct = 'CIFTI_STRUCTURE_CORTEX_RIGHT'
86+
87+
if label_ids is None:
88+
label_ids = [None] * len(label_files)
89+
elif len(label_ids) == 1:
90+
label_ids *= len(label_files)
91+
92+
if mapNs is None:
93+
mapNs = [0] * len(label_files)
94+
else:
95+
mapNs *= len(label_files)
96+
97+
98+
### Begin ###
99+
100+
# Read surface
101+
surf_gii = nib.load(surf_file)
102+
103+
coords_darray = surf_gii.get_arrays_from_intent('NIFTI_INTENT_POINTSET')
104+
if len(coords_darray) != 1:
105+
raise ValueError('Surface must contain exactly one pointset array')
106+
coords_darray = coords_darray[0]
107+
nVtcs_surf = coords_darray.dims[0]
108+
109+
triangle_darray = surf_gii.get_arrays_from_intent('NIFTI_INTENT_TRIANGLE')
110+
if len(triangle_darray) != 1:
111+
raise ValueError('Surface must contain exactly one triangle array')
112+
triangle_darray = triangle_darray[0]
113+
triangle_darray.coordsys = None # only valid for pointset arrays
114+
115+
# Pre-allocate array for padded labels
116+
pad_label_data = np.zeros(nVtcs_surf, dtype=bool)
117+
118+
# Loop labels
119+
for i, label_file in enumerate(label_files):
120+
these_label_ids = label_ids[i]
121+
mapN = mapNs[i]
122+
123+
# Open handle to label
124+
label_img = nib.load(label_file)
125+
126+
# Handle CIFTI labels
127+
if isinstance(label_img, nib.Cifti2Image):
128+
# Need hemisphere for CIFTIs
129+
if hemi is None:
130+
raise ValueError('Must specify hemisphere for CIFTI labels')
131+
132+
# Read label & data
133+
label_data = label_img.get_fdata()[mapN]
134+
135+
# Convert label ID to numeric if necessary
136+
if these_label_ids is not None:
137+
for j, ID in enumerate(these_label_ids):
138+
try:
139+
these_label_ids[j] = int(ID)
140+
except ValueError:
141+
ax0 = label_img.header.get_axis(0)
142+
if not isinstance(ax0, nib.cifti2.LabelAxis):
143+
raise ValueError(
144+
'Label ID names are only supported for ' \
145+
'CIFTI dlabels. Use integer value instead.'
146+
)
147+
matches = [k for k,v in ax0.label[mapN].items() if v[0] == ID]
148+
if len(matches) != 1:
149+
raise ValueError(f"No matches in {label_file} for label '{ID}'")
150+
these_label_ids[j] = matches[0]
151+
152+
# Get brain models info for hemisphere
153+
ax1 = label_img.header.get_axis(1)
154+
slice_, model = get_struct_info(hemi_struct, ax1)
155+
nVtcs_label = ax1.nvertices[hemi_struct]
156+
if nVtcs_surf != nVtcs_label:
157+
raise Exception(('Number of vertices in label ({}) does not ' \
158+
+ 'match number of vertices on surface ({})') \
159+
.format(nVtcs_label, nVtcs_surf))
160+
161+
# Pad label to full set of vertices, add to array
162+
if these_label_ids is None:
163+
isLabel = label_data[slice_].astype(bool)
164+
else:
165+
isLabel = np.isin(label_data[slice_], these_label_ids)
166+
pad_label_data[model.vertex] |= isLabel
167+
168+
# Handle GIFTI labels
169+
elif isinstance(label_img, nib.GiftiImage):
170+
# Get label data
171+
label_data = label_img.darrays[mapN].data
172+
nVtcs_label = len(label_data)
173+
if nVtcs_surf != nVtcs_label:
174+
raise Exception(('Number of vertices in label ({}) does not ' \
175+
+ 'match number of vertices on surface ({})') \
176+
.format(nVtcs_label, nVtcs_surf))
177+
178+
# Check label IDs
179+
for j, ID in enumerate(these_label_ids):
180+
try:
181+
these_label_ids[j] = int(ID)
182+
except ValueError:
183+
raise ValueError(
184+
'Label ID names are only supported for ' \
185+
'CIFTI dlabels. Use integer value instead.'
186+
)
187+
188+
# Add to array
189+
pad_label_data |= np.isin(label_data[slice_], these_label_ids)
190+
191+
else:
192+
raise TypeError('Label must be CIFTI or GIFTI')
193+
194+
# Convert to darray
195+
label_darray = nib.gifti.GiftiDataArray(
196+
pad_label_data.astype(np.float32), intent='NIFTI_INTENT_SHAPE',
197+
datatype='NIFTI_TYPE_FLOAT32'
198+
)
199+
label_darray.coordsys = None # only valid for pointset arrays
200+
201+
# Create new GIFTI & save
202+
new_gii = nib.GiftiImage(
203+
header=surf_gii.header, extra=surf_gii.extra, meta=surf_gii.meta,
204+
darrays=[coords_darray, triangle_darray, label_darray]
205+
)
206+
new_gii.to_filename(outfile)

0 commit comments

Comments
 (0)