Skip to content

Commit bb92d55

Browse files
committed
Minor code and doc updates
1 parent 3918968 commit bb92d55

File tree

2 files changed

+178
-66
lines changed

2 files changed

+178
-66
lines changed

cifti.py

+147-58
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33
"""
4-
Updated version of CIFTI tools. Simplifies masker by removing ability to
5-
invert mask, while also adding some new functionality such as selecting
6-
specific label IDs or using CIFTI structures as masks.
7-
8-
Requires nibabel >= v3.2, which isn't currently installed at YNiC. You might
9-
need to pip install it yourself:
10-
pip3 install --user --upgrade nibabel==3.2
4+
Assorted tools for reading and writing data from/to CIFTI files.
115
"""
126

13-
import os, warnings, inspect, copy
7+
import os, inspect, copy
148
import numpy as np
159
import nibabel as nib
1610

@@ -126,15 +120,49 @@ class CiftiHandler(object):
126120
* get_all_data : Convenience function for extracting surface and volume data
127121
* create_new_cifti : Create new CIFTI image from provided data
128122
123+
Example usage
124+
-------------
125+
Load all data from example dataset for first HCP subject.
126+
127+
>>> infile = '/mnt/hcpdata/Facelab/100610/MNINonLinear/Results/' \\
128+
... 'rfMRI_REST1_7T_PA/rfMRI_REST1_7T_PA_Atlas_hp2000_clean.dtseries.nii'
129+
>>> handler = CiftiHandler(infile)
130+
>>> data = handler.get_all_data()
131+
>>> for block_name, block_data in data.items():
132+
... print(block_name, block_data.shape)
133+
134+
::
135+
136+
lh (900, 29696)
137+
rh (900, 29716)
138+
volume (900, 31870)
139+
140+
Setting ``full_surface = True`` will return the full set of surface
141+
vertices, filling in missing vertices along the medial wall with zeros.
142+
143+
>>> handler = CiftiHandler(infile, full_surface=True)
144+
>>> data = handler.get_all_data()
145+
>>> for block_name, block_data in data.items():
146+
... print(block_name, block_data.shape)
147+
148+
::
149+
150+
lh (900, 32492)
151+
rh (900, 32492)
152+
volume (900, 31870)
153+
154+
Use the ``create_new_cifti`` method to reverse the process, creating a new
155+
CIFTI object from the data arrays.
156+
157+
>>> newImg = handler.create_new_cifti(data['lh'], data['rh'], data['volume'])
158+
>>> newImg.to_filename('my_data.dtseries.nii')
129159
"""
130160
def __init__(self, img, full_surface=False):
131161
# Load cifti
132162
if isinstance(img, nib.Cifti2Image):
133163
self.cifti = img
134164
elif isinstance(img, str) and os.path.isfile(img):
135-
with warnings.catch_warnings():
136-
warnings.simplefilter('ignore')
137-
self.cifti = nib.load(img)
165+
self.cifti = nib.load(img)
138166
else:
139167
raise ValueError('img must be valid filepath or Cifti2Image object')
140168

@@ -154,7 +182,7 @@ def _get_struct_info(self, struct_name):
154182
# Loop structures, return matching one
155183
for name, struct_indices, model in self.axis1.iter_structures():
156184
if name == struct_name:
157-
return struct_indices, model
185+
return struct_indices, model, struct_name
158186

159187
# If we reach here then structure doesn't exist - raise error
160188
raise Exception(f'No data found for structure: {struct_name}')
@@ -240,7 +268,7 @@ def get_surface_data(self, hemi, dtype=None, squeeze_data=False):
240268
241269
Arguments
242270
---------
243-
hemi : str { lh | rh }
271+
hemi : str { lh | rh | L | R }
244272
Name of hemisphere to load data from.
245273
246274
dtype : None or valid dtype
@@ -256,9 +284,9 @@ def get_surface_data(self, hemi, dtype=None, squeeze_data=False):
256284
[samples x vertices] numpy array containing data values.
257285
"""
258286
# Work out surface name
259-
if hemi == 'lh':
287+
if hemi.lower() in ['lh', 'l']:
260288
surf_name = 'left_cortex'
261-
elif hemi == 'rh':
289+
elif hemi.lower() in ['rh', 'r']:
262290
surf_name = 'right_cortex'
263291
else:
264292
raise ValueError(f'Unrecognised hemisphere: \'{hemi}\'')
@@ -267,17 +295,17 @@ def get_surface_data(self, hemi, dtype=None, squeeze_data=False):
267295
n_samp = self.axis0.size
268296

269297
# Search axis structures for requested surface
270-
struct_indices, model = self._get_struct_info(surf_name)
298+
slice_, model, struct_name = self._get_struct_info(surf_name)
271299

272300
# Extract surface data, pad to full set of vertices if necessary
273301
data = self.cifti.get_fdata()
274302
if self.full_surface:
275303
vtx_indices = model.vertex
276-
n_vtx = vtx_indices.max() + 1
304+
n_vtx = model.nvertices[struct_name]
277305
surf_data = np.zeros([n_samp, n_vtx], dtype=dtype)
278-
surf_data[:, vtx_indices] = data[:, struct_indices]
306+
surf_data[:, vtx_indices] = data[:, slice_]
279307
else:
280-
surf_data = data[:, struct_indices]
308+
surf_data = data[:, slice_]
281309

282310
# Convert dtype?
283311
if dtype is not None:
@@ -331,25 +359,18 @@ def create_new_cifti(self, left_surface_data=None, right_surface_data=None,
331359
data = np.zeros([n_samp, n_grayordinates])
332360

333361
# Process left surface
334-
indices, model = self.get_struct_info('left_cortex')
335362
if left_surface_data is not None:
363+
slice_, model = self._get_struct_info('left_cortex')[:2]
336364
if self.full_surface:
337365
left_surface_data = left_surface_data[..., model.vertex]
338-
else:
339-
left_surface_data = np.zeros([n_samp, indices.stop - indices.start])
366+
data[..., slice_] = left_surface_data
340367

341368
# Process right surface
342-
indices, model = self.get_struct_info('right_cortex')
343369
if right_surface_data is not None:
370+
slice_, model = self._get_struct_info('right_cortex')[:2]
344371
if self.full_surface:
345372
right_surface_data = right_surface_data[..., model.vertex]
346-
else:
347-
right_surface_data = np.zeros([n_samp, indices.stop - indices.start])
348-
349-
# Concat surface data over hemispheres and add to data array
350-
surface_data = np.hstack([left_surface_data, right_surface_data])
351-
surface_mask = self._get_surface_mask()
352-
data[..., surface_mask] = surface_data
373+
data[..., slice_] = right_surface_data
353374

354375
# Allocate volume data to array
355376
if volume_data is not None:
@@ -364,7 +385,7 @@ def create_new_cifti(self, left_surface_data=None, right_surface_data=None,
364385

365386
def uncache(self):
366387
"""
367-
Uncache data from memory - good idea to call this when done.
388+
Uncache data from memory.
368389
"""
369390
self.cifti.uncache()
370391

@@ -385,11 +406,87 @@ class CiftiMasker(object):
385406
Methods
386407
-------
387408
* fit : Load mask
388-
* transform() : Load dataset, applying mask
409+
* transform : Load dataset, applying mask
389410
* transform_multiple : Load multiple datasets, applying mask
390411
* fit_transform[_multiple] : Fit and transform in one go
391412
* inverse_transform : Create new CIFTI image from masked data
392413
* uncache : Clear cache
414+
415+
Example usage
416+
-------------
417+
Use masks from Freesurfer Desikan-Killiany atlas for example subject.
418+
419+
>>> maskfile = '/mnt/hcpdata/Facelab/100610/MNINonLinear/' \\
420+
... 'fsaverage_LR32k/100610.aparc.32k_fs_LR.dlabel.nii'
421+
>>> masker = CiftiMasker(maskfile).fit()
422+
423+
Use the ``transform`` method to mask data. By default, this will mask by
424+
the label with a numerical ID of 1 - this will work if the dlabel file
425+
contains only one label, but might not give you data for the label you want
426+
if there are multiple labels. In the APARC atlas, the first label is the
427+
``'L_banksts'``. In this example, the masked data contains 900 time points
428+
and 456 vertices.
429+
430+
>>> infile = '/mnt/hcpdata/Facelab/100610/MNINonLinear/Results/' \\
431+
... 'rfMRI_REST1_7T_PA/rfMRI_REST1_7T_PA_Atlas_hp2000_clean.dtseries.nii'
432+
>>> ROI_data = masker.transform(infile)
433+
>>> print(ROI_data.shape)
434+
435+
::
436+
437+
(900, 456)
438+
439+
When the dlabel file contains multiple labels, it is often useful to specify
440+
which label to extract the data from. Here, we mask by the right
441+
parahippocampal region. Now, the masked data contains 900 time points and
442+
313 vertices.
443+
444+
>>> ROI_data = masker.transform(infile, labelID='R_parahippocampal')
445+
>>> print(ROI_data.shape)
446+
447+
::
448+
449+
(900, 313)
450+
451+
The ``mask_block`` argument can be used to restrict the data to specific
452+
blocks of the CIFTI file (left surface, right surface, or subcortical).
453+
For instance, this could be useful if the dlabel file contains bilateral
454+
labels, but you want to only use the label in one hemisphere. However, in
455+
the APARC atlas, the label names already indicate the hemisphere. If we
456+
again mask by the right parahippocampal region, setting
457+
``mask_block = 'surface'`` or ``mask_block = 'rh'`` will have no effect,
458+
but setting ``mask_block = 'lh'`` would prevent any grayordinates being
459+
selected (it would not make sense to do this in practice).
460+
461+
>>> ROI_data = masker.transform(infile, labelID='R_parahippocampal',
462+
... mask_block='lh')
463+
>>> print(ROI_data.shape)
464+
465+
::
466+
467+
(900, 0)
468+
469+
Instead of using a dlabel mask file, you can also mask by one of the
470+
labelled structures contained in the data CIFTI file. This is most useful
471+
for extracting subcortical regions. You can use the full structure name,
472+
or anything recognised by nibabel's ``to_cifti_brain_structure_name``
473+
method. Here, we extract data for the left amygdala, comprising 900
474+
timepoints and 315 voxels.
475+
476+
>>> masker = CiftiMasker('left amygdala').fit()
477+
>>> ROI_data = masker.transform(infile)
478+
>>> print(ROI_data.shape)
479+
480+
::
481+
482+
(900, 315)
483+
484+
Use the ``.inverse_transform`` method to reverse the process, creating a
485+
new CIFTI object from the masked data. In general, you should run this
486+
with the same settings you used for the forward ``transform`` method.
487+
488+
>>> new_img = masker.inverse_transform(ROI_data)
489+
>>> new_img.to_filename('my_masked_data.dtseries.nii')
393490
"""
394491
def __init__(self, mask_img):
395492
self.mask_img = mask_img
@@ -408,48 +505,40 @@ def _resample_to_data(self, dict_, data_handler, block='all'):
408505
409506
dict_ : Dictionary returned by CiftiHandler.get_all_data
410507
data_handler : CiftiHandler for data image
411-
block : 'all', 'surface', 'lh', 'rh', or 'volume'
508+
block : 'all', 'surface', 'lh'/'L', 'rh'/'R', or 'volume'
412509
"""
413510
# Error check
414511
if not any(X.size > 0 for X in dict_.values()):
415512
raise ValueError('CIFTI does not contain any data structures')
416513

417-
# Pre-allocate list for structures
418-
array = []
419-
420-
# Get dtype and number of samples from first available block - needed
421-
# when allocating zeros for missing blocks
514+
# Get dtype and number of samples from first available block
422515
for X in dict_.values():
423516
if X.size > 0:
424517
nSamp = X.shape[0]
425518
dtype = X.dtype
426519
break
427520

521+
# Pre-allocate array of zeros. Any blocks not allocated below will
522+
# remain as zeros.
523+
array = np.zeros([nSamp, data_handler.axis1.size], dtype=dtype)
524+
428525
# Left surface
429-
model = data_handler._get_struct_info('cortex_left')[1]
430-
if block in ['lh','surface','all'] and dict_['lh'].size > 0:
431-
array.append(dict_['lh'][..., model.vertex])
432-
else:
433-
nVtx = len(model.vertex)
434-
array.append(np.zeros([nSamp, nVtx], dtype))
526+
if block.lower() in ['lh','l','surface','all'] and dict_['lh'].size > 0:
527+
slice_, model = data_handler._get_struct_info('cortex_left')[:2]
528+
array[:, slice_] = dict_['lh'][..., model.vertex]
435529

436530
# Right surface
437-
model = data_handler._get_struct_info('cortex_right')[1]
438-
if block in ['rh','surface','all'] and dict_['rh'].size > 0:
439-
array.append(dict_['rh'][..., model.vertex])
440-
else:
441-
nVtx = len(model.vertex)
442-
array.append(np.zeros([nSamp, nVtx], dtype))
531+
if block.lower() in ['rh','r','surface','all'] and dict_['rh'].size > 0:
532+
slice_, model = data_handler._get_struct_info('cortex_right')[:2]
533+
array[:, slice_] = dict_['rh'][..., model.vertex]
443534

444535
# Volume
445-
if block in ['volume','all'] and dict_['volume'].size > 0:
446-
array.append(dict_['volume'])
447-
else:
448-
nVox = data_handler._get_volume_mask().sum()
449-
array.append(np.zeros([nSamp, nVox], dtype))
536+
if block.lower() in ['volume','all'] and dict_['volume'].size > 0:
537+
vol_mask = data_handler._get_volume_mask()
538+
array[:, vol_mask] = dict_['volume']
450539

451-
# Concat arrays and return
452-
return np.hstack(array)
540+
# Return
541+
return array
453542

454543
def _parse_mapN(self, mapN):
455544
"""
@@ -513,7 +602,7 @@ def transform(self, img, mask_block='all', labelID=1, mapN=0, dtype=None):
513602
Path to CIFTI data file (likely a dscalar or dtseries), or a
514603
Cifti2Image or CiftiHandler object containing the data.
515604
516-
mask_block : str {all | lh | rh | surface | volume}
605+
mask_block : str { all | lh | rh | L | R | surface | volume }
517606
Which blocks from the CIFTI array to return data from. For example,
518607
could use to select data from only one hemisphere. Ignored if mask
519608
is a CIFTI structure. Default is 'all'.

0 commit comments

Comments
 (0)