26
26
from ..external .due import BibTeX
27
27
from ..interfaces .base import (traits , TraitedSpec , BaseInterface ,
28
28
BaseInterfaceInputSpec , File , isdefined ,
29
- InputMultiPath )
29
+ InputMultiPath , OutputMultiPath )
30
30
from ..utils import NUMPY_MMAP
31
31
from ..utils .misc import normalize_mc_params
32
32
@@ -301,23 +301,29 @@ def _list_outputs(self):
301
301
302
302
class CompCorInputSpec (BaseInterfaceInputSpec ):
303
303
realigned_file = File (exists = True , mandatory = True ,
304
- desc = 'already realigned brain image (4D)' )
305
- mask_file = InputMultiPath (File (exists = True , desc = 'mask file(s) that determines ROI (3D)' ))
306
- components_file = File ('components_file.txt' , exists = False ,
307
- usedefault = True ,
308
- desc = 'filename to store physiological components' )
304
+ desc = 'already realigned brain image (4D)' )
305
+ mask_files = InputMultiPath (File (exists = True ,
306
+ desc = 'One or more mask files that determines ROI (3D)' ))
307
+ merge_method = traits .Enum ('union' , 'intersect' , 'none' , xor = ['mask_index' ],
308
+ desc = 'Merge method if multiple masks are present - `union` aggregates '
309
+ 'all masks, `intersect` computes the truth value of all masks, `none` '
310
+ 'performs CompCor on each mask individually' )
311
+ mask_index = traits .Range (0 , value = 0 , xor = ['merge_method' ], usedefault = True ,
312
+ desc = 'Position of mask in `mask_files` to use - first is the default' )
313
+ components_file = File ('components_file.txt' , exists = False , usedefault = True ,
314
+ desc = 'filename to store physiological components' )
309
315
num_components = traits .Int (6 , usedefault = True ) # 6 for BOLD, 4 for ASL
310
316
use_regress_poly = traits .Bool (True , usedefault = True ,
311
- desc = 'use polynomial regression'
312
- 'pre-component extraction' )
317
+ desc = 'use polynomial regression pre-component extraction' )
313
318
regress_poly_degree = traits .Range (low = 1 , default = 1 , usedefault = True ,
314
- desc = 'the degree polynomial to use' )
315
- header = traits .Str (desc = 'the desired header for the output tsv file (one column).'
316
- 'If undefined, will default to "CompCor"' )
319
+ desc = 'the degree polynomial to use' )
320
+ header = traits .Str (
321
+ desc = 'the desired header for the output tsv file (one column).'
322
+ 'If undefined, will default to "CompCor"' )
317
323
318
324
class CompCorOutputSpec (TraitedSpec ):
319
325
components_file = File (exists = True ,
320
- desc = 'text file containing the noise components' )
326
+ desc = 'text file containing the noise components' )
321
327
322
328
class CompCor (BaseInterface ):
323
329
'''
@@ -328,7 +334,7 @@ class CompCor(BaseInterface):
328
334
329
335
>>> ccinterface = CompCor()
330
336
>>> ccinterface.inputs.realigned_file = 'functional.nii'
331
- >>> ccinterface.inputs.mask_file = 'mask.nii'
337
+ >>> ccinterface.inputs.mask_files = 'mask.nii'
332
338
>>> ccinterface.inputs.num_components = 1
333
339
>>> ccinterface.inputs.use_regress_poly = True
334
340
>>> ccinterface.inputs.regress_poly_degree = 2
@@ -350,17 +356,46 @@ class CompCor(BaseInterface):
350
356
}]
351
357
352
358
def _run_interface (self , runtime ):
353
- imgseries = nb .load (self .inputs .realigned_file , mmap = NUMPY_MMAP ).get_data ()
359
+ imgseries = nb .load (self .inputs .realigned_file ,
360
+ mmap = NUMPY_MMAP ).get_data ()
354
361
components = None
355
362
356
- for mask_file in self .inputs .mask_file :
357
- mask = nb .load (mask_file , mmap = NUMPY_MMAP ).get_data ()
358
-
359
- if imgseries .shape [:3 ] != mask .shape :
360
- raise ValueError ('Inputs for CompCor, func {} and mask {}, do not have matching '
361
- 'spatial dimensions ({} and {}, respectively)'
362
- .format (self .inputs .realigned_file , mask_file ,
363
- imgseries .shape [:3 ], mask .shape ))
363
+ if isdefined (self .inputs .mask_index ) and self .inputs .mask_index :
364
+ if self .inputs .mask_index < len (self .inputs .mask_files ):
365
+ self .inputs .mask_files = [
366
+ self .inputs .mask_files [self .inputs .mask_index ]]
367
+ else :
368
+ RuntimeWarning ('Mask index exceeded number of masks, using mask'
369
+ ' {} instead' ).format (self .inputs .mask_files [0 ])
370
+ self .inputs .mask_files = self .inputs .mask_files [0 ]
371
+
372
+ if isdefined (self .inputs .mask_files ):
373
+ for mask_file in self .inputs .mask_files :
374
+ mask = nb .load (mask_file , mmap = NUMPY_MMAP ).get_data ()
375
+
376
+ if imgseries .shape [:3 ] != mask .shape :
377
+ raise ValueError ('Inputs for CompCor, func {} and mask {}, '
378
+ 'do not have matching spatial dimensions '
379
+ '({} and {}, respectively)' .format (
380
+ self .inputs .realigned_file , mask_file ,
381
+ imgseries .shape [:3 ], mask .shape ))
382
+
383
+ if (isdefined (self .inputs .merge_method ) and
384
+ self .merge_method != 'none' and
385
+ len (self .inputs .mask_files > 1 )):
386
+ if mask_file == self .inputs .mask_files [0 ]:
387
+ new_mask = mask
388
+ continue
389
+ else :
390
+ if self .inputs .merge_method == 'union' :
391
+ new_mask = np .logical_or (new_mask , mask ).astype (int )
392
+ elif self .inputs .merge_method == 'intersect' :
393
+ new_mask = np .logical_and (new_mask , mask ).astype (int )
394
+
395
+ if mask_file != self .inputs .mask_files [- 1 ]:
396
+ continue
397
+ else : # merge complete
398
+ mask = new_mask
364
399
365
400
voxel_timecourses = imgseries [mask > 0 ]
366
401
# Zero-out any bad values
@@ -369,7 +404,8 @@ def _run_interface(self, runtime):
369
404
# from paper:
370
405
# "The constant and linear trends of the columns in the matrix M were
371
406
# removed [prior to ...]"
372
- degree = self .inputs .regress_poly_degree if self .inputs .use_regress_poly else 0
407
+ degree = (self .inputs .regress_poly_degree if
408
+ self .inputs .use_regress_poly else 0 )
373
409
voxel_timecourses = regress_poly (degree , voxel_timecourses )
374
410
375
411
# "Voxel time series from the noise ROI (either anatomical or tSTD) were
@@ -386,18 +422,18 @@ def _run_interface(self, runtime):
386
422
if components is None :
387
423
components = u [:, :self .inputs .num_components ]
388
424
else :
389
- components = np .hstack ((components , u [:, :self .inputs .num_components ]))
425
+ components = np .hstack ((components ,
426
+ u [:, :self .inputs .num_components ]))
390
427
391
428
components_file = os .path .join (os .getcwd (), self .inputs .components_file )
392
-
393
429
self ._set_header ()
394
430
np .savetxt (components_file , components , fmt = b"%.10f" , delimiter = '\t ' ,
395
431
header = self ._make_headers (components .shape [1 ]), comments = '' )
396
432
return runtime
397
433
398
434
def _list_outputs (self ):
399
435
outputs = self ._outputs ().get ()
400
- outputs ['components_file' ] = os . path . abspath ( self .inputs .components_file )
436
+ outputs ['components_file' ] = self .inputs .components_file
401
437
return outputs
402
438
403
439
def _compute_tSTD (self , M , x , axis = 0 ):
@@ -408,7 +444,8 @@ def _compute_tSTD(self, M, x, axis=0):
408
444
return stdM
409
445
410
446
def _set_header (self , header = 'CompCor' ):
411
- self .inputs .header = self .inputs .header if isdefined (self .inputs .header ) else header
447
+ self .inputs .header = (self .inputs .header if isdefined (self .inputs .header )
448
+ else header )
412
449
413
450
def _make_headers (self , num_col ):
414
451
headers = []
@@ -441,7 +478,8 @@ class TCompCorInputSpec(CompCorInputSpec):
441
478
442
479
class TCompCorOutputSpec (CompCorInputSpec ):
443
480
# and all the fields in CompCorInputSpec
444
- high_variance_mask = InputMultiPath (File (exists = True , desc = "voxels excedding the variance threshold" ))
481
+ high_variance_masks = OutputMultiPath (File (exists = True ,
482
+ desc = "voxels excedding the variance threshold" ))
445
483
446
484
class TCompCor (CompCor ):
447
485
'''
@@ -452,7 +490,7 @@ class TCompCor(CompCor):
452
490
453
491
>>> ccinterface = TCompCor()
454
492
>>> ccinterface.inputs.realigned_file = 'functional.nii'
455
- >>> ccinterface.inputs.mask_file = 'mask.nii'
493
+ >>> ccinterface.inputs.mask_files = 'mask.nii'
456
494
>>> ccinterface.inputs.num_components = 1
457
495
>>> ccinterface.inputs.use_regress_poly = True
458
496
>>> ccinterface.inputs.regress_poly_degree = 2
@@ -461,54 +499,99 @@ class TCompCor(CompCor):
461
499
462
500
input_spec = TCompCorInputSpec
463
501
output_spec = TCompCorOutputSpec
502
+ _out_masks = []
464
503
465
504
def _run_interface (self , runtime ):
466
- imgseries = nb .load (self .inputs .realigned_file , mmap = NUMPY_MMAP ).get_data ()
505
+ imgseries = nb .load (self .inputs .realigned_file ,
506
+ mmap = NUMPY_MMAP ).get_data ()
467
507
468
508
if imgseries .ndim != 4 :
469
- raise ValueError ('tCompCor expected a 4-D nifti file. Input {} has {} dimensions '
470
- '(shape {})'
471
- .format (self .inputs .realigned_file , imgseries .ndim , imgseries .shape ))
472
-
473
- if isdefined (self .inputs .mask_file ):
474
- in_mask_data = nb .load (self .inputs .mask_file [0 ], mmap = NUMPY_MMAP ).get_data ()
475
- imgseries = imgseries [in_mask_data != 0 , :]
476
-
477
- # From the paper:
478
- # "For each voxel time series, the temporal standard deviation is
479
- # defined as the standard deviation of the time series after the removal
480
- # of low-frequency nuisance terms (e.g., linear and quadratic drift)."
481
- imgseries = regress_poly (2 , imgseries )
482
-
483
- # "To construct the tSTD noise ROI, we sorted the voxels by their
484
- # temporal standard deviation ..."
485
- tSTD = self ._compute_tSTD (imgseries , 0 , axis = - 1 )
486
-
487
- # use percentile_threshold to pick voxels
488
- threshold_std = np .percentile (tSTD , 100. * (1. - self .inputs .percentile_threshold ))
489
- mask = tSTD >= threshold_std
490
-
491
- if isdefined (self .inputs .mask_file ):
492
- mask_data = np .zeros_like (in_mask_data )
493
- mask_data [in_mask_data != 0 ] = mask
509
+ raise ValueError ('tCompCor expected a 4-D nifti file. Input {} has '
510
+ '{} dimensions (shape {})' .format (
511
+ self .inputs .realigned_file , imgseries .ndim ,
512
+ imgseries .shape ))
513
+
514
+ if isdefined (self .inputs .mask_files ):
515
+ if isdefined (self .inputs .mask_index ) and self .inputs .mask_index :
516
+ if self .inputs .mask_index < len (self .inputs .mask_files ):
517
+ self .inputs .mask_files = [
518
+ self .inputs .mask_files [self .inputs .mask_index ]]
519
+ else :
520
+ RuntimeWarning ('Mask index exceeded number of masks, using '
521
+ 'mask {} instead' ).format (self .inputs .mask_files [0 ])
522
+ self .inputs .mask_files = self .inputs .mask_files [0 ]
523
+
524
+ for i , mask_file in enumerate (self .inputs .mask_files , 1 ):
525
+ in_mask = nb .load (mask_file , mmap = NUMPY_MMAP ).get_data ()
526
+ if (isdefined (self .inputs .merge_method ) and
527
+ self .merge_method != 'none' and
528
+ len (self .inputs .mask_files > 1 )):
529
+ if mask_file == self .inputs .mask_files [0 ]:
530
+ new_mask = in_mask
531
+ continue
532
+ else :
533
+ if self .inputs .merge_method == 'union' :
534
+ new_mask = np .logical_or (new_mask , in_mask ).astype (int )
535
+ elif self .inputs .merge_method == 'intersect' :
536
+ new_mask = np .logical_and (new_mask , in_mask ).astype (int )
537
+
538
+ if mask_file != self .inputs .mask_files [- 1 ]:
539
+ continue
540
+ else : # merge complete
541
+ in_mask = new_mask
542
+
543
+ imgseries = imgseries [in_mask != 0 , :]
544
+
545
+ # From the paper:
546
+ # "For each voxel time series, the temporal standard deviation is
547
+ # defined as the standard deviation of the time series after the removal
548
+ # of low-frequency nuisance terms (e.g., linear and quadratic drift)."
549
+ imgseries = regress_poly (2 , imgseries )
550
+
551
+ # "To construct the tSTD noise ROI, we sorted the voxels by their
552
+ # temporal standard deviation ..."
553
+ tSTD = self ._compute_tSTD (imgseries , 0 , axis = - 1 )
554
+
555
+ # use percentile_threshold to pick voxels
556
+ threshold_std = np .percentile (tSTD , 100. *
557
+ (1. - self .inputs .percentile_threshold ))
558
+ mask = tSTD >= threshold_std
559
+
560
+ mask_data = np .zeros_like (in_mask )
561
+ mask_data [in_mask != 0 ] = mask
562
+ # save mask
563
+ mask_file = os .path .abspath ('mask{}.nii' .format (i ))
564
+ nb .Nifti1Image (mask_data , nb .load (
565
+ self .inputs .realigned_file ).affine ).to_filename (mask_file )
566
+ IFLOG .debug ('tCompcor computed and saved mask of shape {} to '
567
+ 'mask_file {}' .format (mask .shape , mask_file ))
568
+ self ._out_masks .append (mask_file )
569
+ self ._set_header ('tCompCor' )
570
+
494
571
else :
572
+ imgseries = regress_poly (2 , imgseries )
573
+ tSTD = self ._compute_tSTD (imgseries , 0 , axis = - 1 )
574
+ threshold_std = np .percentile (tSTD , 100. *
575
+ (1. - self .inputs .percentile_threshold ))
576
+ mask = tSTD >= threshold_std
495
577
mask_data = mask .astype (int )
496
578
497
- # save mask
498
- mask_file = os .path .abspath ('mask.nii' )
499
- nb .Nifti1Image (mask_data ,
500
- nb .load (self .inputs .realigned_file ).affine ).to_filename (mask_file )
501
- IFLOG .debug ('tCompcor computed and saved mask of shape {} to mask_file {} '
502
- .format (mask .shape , mask_file ))
503
- self .inputs . mask_file = mask_file
504
- self ._set_header ('tCompCor' )
579
+ # save mask
580
+ mask_file = os .path .abspath ('mask.nii' )
581
+ nb .Nifti1Image (mask_data ,
582
+ nb .load (self .inputs .realigned_file ).affine ).to_filename (mask_file )
583
+ IFLOG .debug ('tCompcor computed and saved mask of shape {} to mask_file '
584
+ '{}' .format (mask .shape , mask_file ))
585
+ self ._out_masks . append ( mask_file )
586
+ self ._set_header ('tCompCor' )
505
587
588
+ self .inputs .mask_files = self ._out_masks
506
589
super (TCompCor , self )._run_interface (runtime )
507
590
return runtime
508
591
509
592
def _list_outputs (self ):
510
593
outputs = super (TCompCor , self )._list_outputs ()
511
- outputs ['high_variance_mask ' ] = self .inputs .mask_file
594
+ outputs ['high_variance_masks ' ] = self .inputs .mask_files
512
595
return outputs
513
596
514
597
class TSNRInputSpec (BaseInterfaceInputSpec ):
0 commit comments