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