2626from ..external .due import BibTeX
2727from ..interfaces .base import (traits , TraitedSpec , BaseInterface ,
2828 BaseInterfaceInputSpec , File , isdefined ,
29- InputMultiPath )
29+ InputMultiPath , OutputMultiPath )
3030from ..utils import NUMPY_MMAP
3131from ..utils .misc import normalize_mc_params
3232
@@ -301,23 +301,33 @@ def _list_outputs(self):
301301
302302class CompCorInputSpec (BaseInterfaceInputSpec ):
303303 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' )
309319 num_components = traits .Int (6 , usedefault = True ) # 6 for BOLD, 4 for ASL
310320 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' )
313322 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"' )
317327
318328class CompCorOutputSpec (TraitedSpec ):
319329 components_file = File (exists = True ,
320- desc = 'text file containing the noise components' )
330+ desc = 'text file containing the noise components' )
321331
322332class CompCor (BaseInterface ):
323333 '''
@@ -328,7 +338,7 @@ class CompCor(BaseInterface):
328338
329339 >>> ccinterface = CompCor()
330340 >>> ccinterface.inputs.realigned_file = 'functional.nii'
331- >>> ccinterface.inputs.mask_file = 'mask.nii'
341+ >>> ccinterface.inputs.mask_files = 'mask.nii'
332342 >>> ccinterface.inputs.num_components = 1
333343 >>> ccinterface.inputs.use_regress_poly = True
334344 >>> ccinterface.inputs.regress_poly_degree = 2
@@ -349,15 +359,53 @@ class CompCor(BaseInterface):
349359 'tags' : ['method' , 'implementation' ]
350360 }]
351361
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
361409
362410 voxel_timecourses = imgseries [mask > 0 ]
363411 # Zero-out any bad values
@@ -366,7 +414,8 @@ def _run_interface(self, runtime):
366414 # from paper:
367415 # "The constant and linear trends of the columns in the matrix M were
368416 # 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 )
370419 voxel_timecourses = regress_poly (degree , voxel_timecourses )
371420
372421 # "Voxel time series from the noise ROI (either anatomical or tSTD) were
@@ -380,9 +429,13 @@ def _run_interface(self, runtime):
380429 # "The covariance matrix C = MMT was constructed and decomposed into its
381430 # principal components using a singular value decomposition."
382431 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 ]))
385437
438+ components_file = os .path .join (os .getcwd (), self .inputs .components_file )
386439 self ._set_header ()
387440 np .savetxt (components_file , components , fmt = b"%.10f" , delimiter = '\t ' ,
388441 header = self ._make_headers (components .shape [1 ]), comments = '' )
@@ -401,7 +454,8 @@ def _compute_tSTD(self, M, x, axis=0):
401454 return stdM
402455
403456 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 )
405459
406460 def _make_headers (self , num_col ):
407461 headers = []
@@ -434,7 +488,8 @@ class TCompCorInputSpec(CompCorInputSpec):
434488
435489class TCompCorOutputSpec (CompCorInputSpec ):
436490 # 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" ))
438493
439494class TCompCor (CompCor ):
440495 '''
@@ -445,7 +500,7 @@ class TCompCor(CompCor):
445500
446501 >>> ccinterface = TCompCor()
447502 >>> ccinterface.inputs.realigned_file = 'functional.nii'
448- >>> ccinterface.inputs.mask_file = 'mask.nii'
503+ >>> ccinterface.inputs.mask_files = 'mask.nii'
449504 >>> ccinterface.inputs.num_components = 1
450505 >>> ccinterface.inputs.use_regress_poly = True
451506 >>> ccinterface.inputs.regress_poly_degree = 2
@@ -456,52 +511,105 @@ class TCompCor(CompCor):
456511 output_spec = TCompCorOutputSpec
457512
458513 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+
460520
461521 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+
487590 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
488596 mask_data = mask .astype (int )
489597
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' )
498605
499- super (TCompCor , self )._run_interface (runtime )
606+ self .inputs .mask_files = _out_masks
607+ super (TCompCor , self )._run_interface (runtime , tCompCor_mask = True )
500608 return runtime
501609
502610 def _list_outputs (self ):
503611 outputs = super (TCompCor , self )._list_outputs ()
504- outputs ['high_variance_mask ' ] = self .inputs .mask_file
612+ outputs ['high_variance_masks ' ] = self .inputs .mask_files
505613 return outputs
506614
507615class TSNRInputSpec (BaseInterfaceInputSpec ):
0 commit comments