1
1
#!/usr/bin/env python3
2
2
# -*- coding: utf-8 -*-
3
3
"""
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.
11
5
"""
12
6
13
- import os , warnings , inspect , copy
7
+ import os , inspect , copy
14
8
import numpy as np
15
9
import nibabel as nib
16
10
@@ -126,15 +120,49 @@ class CiftiHandler(object):
126
120
* get_all_data : Convenience function for extracting surface and volume data
127
121
* create_new_cifti : Create new CIFTI image from provided data
128
122
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')
129
159
"""
130
160
def __init__ (self , img , full_surface = False ):
131
161
# Load cifti
132
162
if isinstance (img , nib .Cifti2Image ):
133
163
self .cifti = img
134
164
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 )
138
166
else :
139
167
raise ValueError ('img must be valid filepath or Cifti2Image object' )
140
168
@@ -154,7 +182,7 @@ def _get_struct_info(self, struct_name):
154
182
# Loop structures, return matching one
155
183
for name , struct_indices , model in self .axis1 .iter_structures ():
156
184
if name == struct_name :
157
- return struct_indices , model
185
+ return struct_indices , model , struct_name
158
186
159
187
# If we reach here then structure doesn't exist - raise error
160
188
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):
240
268
241
269
Arguments
242
270
---------
243
- hemi : str { lh | rh }
271
+ hemi : str { lh | rh | L | R }
244
272
Name of hemisphere to load data from.
245
273
246
274
dtype : None or valid dtype
@@ -256,9 +284,9 @@ def get_surface_data(self, hemi, dtype=None, squeeze_data=False):
256
284
[samples x vertices] numpy array containing data values.
257
285
"""
258
286
# Work out surface name
259
- if hemi == 'lh' :
287
+ if hemi . lower () in [ 'lh' , 'l' ] :
260
288
surf_name = 'left_cortex'
261
- elif hemi == 'rh' :
289
+ elif hemi . lower () in [ 'rh' , 'r' ] :
262
290
surf_name = 'right_cortex'
263
291
else :
264
292
raise ValueError (f'Unrecognised hemisphere: \' { hemi } \' ' )
@@ -267,17 +295,17 @@ def get_surface_data(self, hemi, dtype=None, squeeze_data=False):
267
295
n_samp = self .axis0 .size
268
296
269
297
# 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 )
271
299
272
300
# Extract surface data, pad to full set of vertices if necessary
273
301
data = self .cifti .get_fdata ()
274
302
if self .full_surface :
275
303
vtx_indices = model .vertex
276
- n_vtx = vtx_indices . max () + 1
304
+ n_vtx = model . nvertices [ struct_name ]
277
305
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_ ]
279
307
else :
280
- surf_data = data [:, struct_indices ]
308
+ surf_data = data [:, slice_ ]
281
309
282
310
# Convert dtype?
283
311
if dtype is not None :
@@ -331,25 +359,18 @@ def create_new_cifti(self, left_surface_data=None, right_surface_data=None,
331
359
data = np .zeros ([n_samp , n_grayordinates ])
332
360
333
361
# Process left surface
334
- indices , model = self .get_struct_info ('left_cortex' )
335
362
if left_surface_data is not None :
363
+ slice_ , model = self ._get_struct_info ('left_cortex' )[:2 ]
336
364
if self .full_surface :
337
365
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
340
367
341
368
# Process right surface
342
- indices , model = self .get_struct_info ('right_cortex' )
343
369
if right_surface_data is not None :
370
+ slice_ , model = self ._get_struct_info ('right_cortex' )[:2 ]
344
371
if self .full_surface :
345
372
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
353
374
354
375
# Allocate volume data to array
355
376
if volume_data is not None :
@@ -364,7 +385,7 @@ def create_new_cifti(self, left_surface_data=None, right_surface_data=None,
364
385
365
386
def uncache (self ):
366
387
"""
367
- Uncache data from memory - good idea to call this when done .
388
+ Uncache data from memory.
368
389
"""
369
390
self .cifti .uncache ()
370
391
@@ -385,11 +406,87 @@ class CiftiMasker(object):
385
406
Methods
386
407
-------
387
408
* fit : Load mask
388
- * transform() : Load dataset, applying mask
409
+ * transform : Load dataset, applying mask
389
410
* transform_multiple : Load multiple datasets, applying mask
390
411
* fit_transform[_multiple] : Fit and transform in one go
391
412
* inverse_transform : Create new CIFTI image from masked data
392
413
* 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')
393
490
"""
394
491
def __init__ (self , mask_img ):
395
492
self .mask_img = mask_img
@@ -408,48 +505,40 @@ def _resample_to_data(self, dict_, data_handler, block='all'):
408
505
409
506
dict_ : Dictionary returned by CiftiHandler.get_all_data
410
507
data_handler : CiftiHandler for data image
411
- block : 'all', 'surface', 'lh', 'rh', or 'volume'
508
+ block : 'all', 'surface', 'lh'/'L' , 'rh'/'R ', or 'volume'
412
509
"""
413
510
# Error check
414
511
if not any (X .size > 0 for X in dict_ .values ()):
415
512
raise ValueError ('CIFTI does not contain any data structures' )
416
513
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
422
515
for X in dict_ .values ():
423
516
if X .size > 0 :
424
517
nSamp = X .shape [0 ]
425
518
dtype = X .dtype
426
519
break
427
520
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
+
428
525
# 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 ]
435
529
436
530
# 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 ]
443
534
444
535
# 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' ]
450
539
451
- # Concat arrays and return
452
- return np . hstack ( array )
540
+ # Return
541
+ return array
453
542
454
543
def _parse_mapN (self , mapN ):
455
544
"""
@@ -513,7 +602,7 @@ def transform(self, img, mask_block='all', labelID=1, mapN=0, dtype=None):
513
602
Path to CIFTI data file (likely a dscalar or dtseries), or a
514
603
Cifti2Image or CiftiHandler object containing the data.
515
604
516
- mask_block : str {all | lh | rh | surface | volume}
605
+ mask_block : str { all | lh | rh | L | R | surface | volume }
517
606
Which blocks from the CIFTI array to return data from. For example,
518
607
could use to select data from only one hemisphere. Ignored if mask
519
608
is a CIFTI structure. Default is 'all'.
0 commit comments