1414from __future__ import print_function , division , unicode_literals , absolute_import
1515from builtins import str , zip , range , open
1616
17+ import os
1718import os .path as op
19+
20+ import nibabel as nb
1821import numpy as np
1922
2023from .. import logging
21- from ..external .due import due , BibTeX
24+ from ..external .due import due , Doi , BibTeX
2225from ..interfaces .base import (traits , TraitedSpec , BaseInterface ,
2326 BaseInterfaceInputSpec , File , isdefined )
2427IFLOG = logging .getLogger ('interface' )
2528
2629
30+ class ComputeDVARSInputSpec (BaseInterfaceInputSpec ):
31+ in_file = File (exists = True , mandatory = True , desc = 'functional data, after HMC' )
32+ in_mask = File (exists = True , mandatory = True , desc = 'a brain mask' )
33+ remove_zerovariance = traits .Bool (False , usedefault = True ,
34+ desc = 'remove voxels with zero variance' )
35+ save_std = traits .Bool (True , usedefault = True ,
36+ desc = 'save standardized DVARS' )
37+ save_nstd = traits .Bool (False , usedefault = True ,
38+ desc = 'save non-standardized DVARS' )
39+ save_vxstd = traits .Bool (False , usedefault = True ,
40+ desc = 'save voxel-wise standardized DVARS' )
41+ save_all = traits .Bool (False , usedefault = True , desc = 'output all DVARS' )
42+
43+ series_tr = traits .Float (desc = 'repetition time in sec.' )
44+ save_plot = traits .Bool (False , usedefault = True , desc = 'write DVARS plot' )
45+ figdpi = traits .Int (100 , usedefault = True , desc = 'output dpi for the plot' )
46+ figsize = traits .Tuple (traits .Float (11.7 ), traits .Float (2.3 ), usedefault = True ,
47+ desc = 'output figure size' )
48+ figformat = traits .Enum ('png' , 'pdf' , 'svg' , usedefault = True ,
49+ desc = 'output format for figures' )
50+
51+
52+
53+ class ComputeDVARSOutputSpec (TraitedSpec ):
54+ out_std = File (exists = True , desc = 'output text file' )
55+ out_nstd = File (exists = True , desc = 'output text file' )
56+ out_vxstd = File (exists = True , desc = 'output text file' )
57+ out_all = File (exists = True , desc = 'output text file' )
58+ avg_std = traits .Float ()
59+ avg_nstd = traits .Float ()
60+ avg_vxstd = traits .Float ()
61+ fig_std = File (exists = True , desc = 'output DVARS plot' )
62+ fig_nstd = File (exists = True , desc = 'output DVARS plot' )
63+ fig_vxstd = File (exists = True , desc = 'output DVARS plot' )
64+
65+
66+ class ComputeDVARS (BaseInterface ):
67+ """
68+ Computes the DVARS.
69+ """
70+ input_spec = ComputeDVARSInputSpec
71+ output_spec = ComputeDVARSOutputSpec
72+ references_ = [{
73+ 'entry' : BibTeX ("""\
74+ @techreport{nichols_notes_2013,
75+ address = {Coventry, UK},
76+ title = {Notes on {Creating} a {Standardized} {Version} of {DVARS}},
77+ url = {http://www2.warwick.ac.uk/fac/sci/statistics/staff/academic-\
78+ research/nichols/scripts/fsl/standardizeddvars.pdf},
79+ urldate = {2016-08-16},
80+ institution = {University of Warwick},
81+ author = {Nichols, Thomas},
82+ year = {2013}
83+ }""" ),
84+ 'tags' : ['method' ]
85+ }, {
86+ 'entry' : BibTeX ("""\
87+ @article{power_spurious_2012,
88+ title = {Spurious but systematic correlations in functional connectivity {MRI} networks \
89+ arise from subject motion},
90+ volume = {59},
91+ doi = {10.1016/j.neuroimage.2011.10.018},
92+ number = {3},
93+ urldate = {2016-08-16},
94+ journal = {NeuroImage},
95+ author = {Power, Jonathan D. and Barnes, Kelly A. and Snyder, Abraham Z. and Schlaggar, \
96+ Bradley L. and Petersen, Steven E.},
97+ year = {2012},
98+ pages = {2142--2154},
99+ }
100+ """ ),
101+ 'tags' : ['method' ]
102+ }]
103+
104+ def __init__ (self , ** inputs ):
105+ self ._results = {}
106+ super (ComputeDVARS , self ).__init__ (** inputs )
107+
108+ def _gen_fname (self , suffix , ext = None ):
109+ fname , in_ext = op .splitext (op .basename (
110+ self .inputs .in_file ))
111+
112+ if in_ext == '.gz' :
113+ fname , in_ext2 = op .splitext (fname )
114+ in_ext = in_ext2 + in_ext
115+
116+ if ext is None :
117+ ext = in_ext
118+
119+ if ext .startswith ('.' ):
120+ ext = ext [1 :]
121+
122+ return op .abspath ('{}_{}.{}' .format (fname , suffix , ext ))
123+
124+ def _run_interface (self , runtime ):
125+ dvars = compute_dvars (self .inputs .in_file , self .inputs .in_mask ,
126+ remove_zerovariance = self .inputs .remove_zerovariance )
127+
128+ self ._results ['avg_std' ] = dvars [0 ].mean ()
129+ self ._results ['avg_nstd' ] = dvars [1 ].mean ()
130+ self ._results ['avg_vxstd' ] = dvars [2 ].mean ()
131+
132+ tr = None
133+ if isdefined (self .inputs .series_tr ):
134+ tr = self .inputs .series_tr
135+
136+ if self .inputs .save_std :
137+ out_file = self ._gen_fname ('dvars_std' , ext = 'tsv' )
138+ np .savetxt (out_file , dvars [0 ], fmt = b'%0.6f' )
139+ self ._results ['out_std' ] = out_file
140+
141+ if self .inputs .save_plot :
142+ self ._results ['fig_std' ] = self ._gen_fname (
143+ 'dvars_std' , ext = self .inputs .figformat )
144+ fig = plot_confound (dvars [0 ], self .inputs .figsize , 'Standardized DVARS' ,
145+ series_tr = tr )
146+ fig .savefig (self ._results ['fig_std' ], dpi = float (self .inputs .figdpi ),
147+ format = self .inputs .figformat ,
148+ bbox_inches = 'tight' )
149+ fig .clf ()
150+
151+ if self .inputs .save_nstd :
152+ out_file = self ._gen_fname ('dvars_nstd' , ext = 'tsv' )
153+ np .savetxt (out_file , dvars [1 ], fmt = b'%0.6f' )
154+ self ._results ['out_nstd' ] = out_file
155+
156+ if self .inputs .save_plot :
157+ self ._results ['fig_nstd' ] = self ._gen_fname (
158+ 'dvars_nstd' , ext = self .inputs .figformat )
159+ fig = plot_confound (dvars [1 ], self .inputs .figsize , 'DVARS' , series_tr = tr )
160+ fig .savefig (self ._results ['fig_nstd' ], dpi = float (self .inputs .figdpi ),
161+ format = self .inputs .figformat ,
162+ bbox_inches = 'tight' )
163+ fig .clf ()
164+
165+ if self .inputs .save_vxstd :
166+ out_file = self ._gen_fname ('dvars_vxstd' , ext = 'tsv' )
167+ np .savetxt (out_file , dvars [2 ], fmt = b'%0.6f' )
168+ self ._results ['out_vxstd' ] = out_file
169+
170+ if self .inputs .save_plot :
171+ self ._results ['fig_vxstd' ] = self ._gen_fname (
172+ 'dvars_vxstd' , ext = self .inputs .figformat )
173+ fig = plot_confound (dvars [2 ], self .inputs .figsize , 'Voxelwise std DVARS' ,
174+ series_tr = tr )
175+ fig .savefig (self ._results ['fig_vxstd' ], dpi = float (self .inputs .figdpi ),
176+ format = self .inputs .figformat ,
177+ bbox_inches = 'tight' )
178+ fig .clf ()
179+
180+ if self .inputs .save_all :
181+ out_file = self ._gen_fname ('dvars' , ext = 'tsv' )
182+ np .savetxt (out_file , np .vstack (dvars ).T , fmt = b'%0.8f' , delimiter = b'\t ' ,
183+ header = 'std DVARS\t non-std DVARS\t vx-wise std DVARS' )
184+ self ._results ['out_all' ] = out_file
185+
186+ return runtime
187+
188+ def _list_outputs (self ):
189+ return self ._results
190+
191+
27192class FramewiseDisplacementInputSpec (BaseInterfaceInputSpec ):
28193 in_plots = File (exists = True , desc = 'motion parameters as written by FSL MCFLIRT' )
29194 radius = traits .Float (50 , usedefault = True ,
@@ -90,7 +255,6 @@ def _run_interface(self, runtime):
90255 }
91256 np .savetxt (self .inputs .out_file , fd_res )
92257
93-
94258 if self .inputs .save_plot :
95259 tr = None
96260 if isdefined (self .inputs .series_tr ):
@@ -106,16 +270,116 @@ def _run_interface(self, runtime):
106270 format = self .inputs .out_figure [- 3 :],
107271 bbox_inches = 'tight' )
108272 fig .clf ()
273+
109274 return runtime
110275
111276 def _list_outputs (self ):
112277 return self ._results
113278
114279
280+ def compute_dvars (in_file , in_mask , remove_zerovariance = False ):
281+ """
282+ Compute the :abbr:`DVARS (D referring to temporal
283+ derivative of timecourses, VARS referring to RMS variance over voxels)`
284+ [Power2012]_.
285+
286+ Particularly, the *standardized* :abbr:`DVARS (D referring to temporal
287+ derivative of timecourses, VARS referring to RMS variance over voxels)`
288+ [Nichols2013]_ are computed.
289+
290+ .. [Nichols2013] Nichols T, `Notes on creating a standardized version of
291+ DVARS <http://www2.warwick.ac.uk/fac/sci/statistics/staff/academic-\
292+ research/nichols/scripts/fsl/standardizeddvars.pdf>`_, 2013.
293+
294+ .. note:: Implementation details
295+
296+ Uses the implementation of the `Yule-Walker equations
297+ from nitime
298+ <http://nipy.org/nitime/api/generated/nitime.algorithms.autoregressive.html\
299+ #nitime.algorithms.autoregressive.AR_est_YW>`_
300+ for the :abbr:`AR (auto-regressive)` filtering of the fMRI signal.
301+
302+ :param numpy.ndarray func: functional data, after head-motion-correction.
303+ :param numpy.ndarray mask: a 3D mask of the brain
304+ :param bool output_all: write out all dvars
305+ :param str out_file: a path to which the standardized dvars should be saved.
306+ :return: the standardized DVARS
307+
308+ """
309+ import os .path as op
310+ import numpy as np
311+ import nibabel as nb
312+ from nitime .algorithms import AR_est_YW
313+
314+ func = nb .load (in_file ).get_data ().astype (np .float32 )
315+ mask = nb .load (in_mask ).get_data ().astype (np .uint8 )
316+
317+ if len (func .shape ) != 4 :
318+ raise RuntimeError (
319+ "Input fMRI dataset should be 4-dimensional" )
320+
321+ # Robust standard deviation
322+ func_sd = (np .percentile (func , 75 , axis = 3 ) -
323+ np .percentile (func , 25 , axis = 3 )) / 1.349
324+ func_sd [mask <= 0 ] = 0
325+
326+ if remove_zerovariance :
327+ # Remove zero-variance voxels across time axis
328+ mask = zero_variance (func , mask )
329+
330+ idx = np .where (mask > 0 )
331+ mfunc = func [idx [0 ], idx [1 ], idx [2 ], :]
332+
333+ # Demean
334+ mfunc -= mfunc .mean (axis = 1 ).astype (np .float32 )[..., np .newaxis ]
335+
336+ # Compute (non-robust) estimate of lag-1 autocorrelation
337+ ar1 = np .apply_along_axis (AR_est_YW , 1 , mfunc , 1 )[:, 0 ]
338+
339+ # Compute (predicted) standard deviation of temporal difference time series
340+ diff_sdhat = np .squeeze (np .sqrt (((1 - ar1 ) * 2 ).tolist ())) * func_sd [mask > 0 ].reshape (- 1 )
341+ diff_sd_mean = diff_sdhat .mean ()
342+
343+ # Compute temporal difference time series
344+ func_diff = np .diff (mfunc , axis = 1 )
345+
346+ # DVARS (no standardization)
347+ dvars_nstd = func_diff .std (axis = 0 )
348+
349+ # standardization
350+ dvars_stdz = dvars_nstd / diff_sd_mean
351+
352+ # voxelwise standardization
353+ diff_vx_stdz = func_diff / np .array ([diff_sdhat ] * func_diff .shape [- 1 ]).T
354+ dvars_vx_stdz = diff_vx_stdz .std (axis = 0 , ddof = 1 )
355+
356+ return (dvars_stdz , dvars_nstd , dvars_vx_stdz )
357+
358+ def zero_variance (func , mask ):
359+ """
360+ Mask out voxels with zero variance across t-axis
361+
362+ :param numpy.ndarray func: input fMRI dataset, after motion correction
363+ :param numpy.ndarray mask: 3D brain mask
364+ :return: the 3D mask of voxels with nonzero variance across :math:`t`.
365+ :rtype: numpy.ndarray
366+
367+ """
368+ idx = np .where (mask > 0 )
369+ func = func [idx [0 ], idx [1 ], idx [2 ], :]
370+ tvariance = func .var (axis = 1 )
371+ tv_mask = np .zeros_like (tvariance , dtype = np .uint8 )
372+ tv_mask [tvariance > 0 ] = 1
373+
374+ newmask = np .zeros_like (mask , dtype = np .uint8 )
375+ newmask [idx ] = tv_mask
376+ return newmask
377+
115378def plot_confound (tseries , figsize , name , units = None ,
116379 series_tr = None , normalize = False ):
117380 """
118381 A helper function to plot :abbr:`fMRI (functional MRI)` confounds.
382+
119383 """
120384 import matplotlib
121385 matplotlib .use ('Agg' )
0 commit comments