14
14
from __future__ import print_function , division , unicode_literals , absolute_import
15
15
from builtins import str , zip , range , open
16
16
17
+ import os
17
18
import os .path as op
19
+
20
+ import nibabel as nb
18
21
import numpy as np
19
22
20
23
from .. import logging
21
- from ..external .due import due , BibTeX
24
+ from ..external .due import due , Doi , BibTeX
22
25
from ..interfaces .base import (traits , TraitedSpec , BaseInterface ,
23
26
BaseInterfaceInputSpec , File , isdefined )
24
27
IFLOG = logging .getLogger ('interface' )
25
28
26
29
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
+
27
192
class FramewiseDisplacementInputSpec (BaseInterfaceInputSpec ):
28
193
in_plots = File (exists = True , desc = 'motion parameters as written by FSL MCFLIRT' )
29
194
radius = traits .Float (50 , usedefault = True ,
@@ -90,7 +255,6 @@ def _run_interface(self, runtime):
90
255
}
91
256
np .savetxt (self .inputs .out_file , fd_res )
92
257
93
-
94
258
if self .inputs .save_plot :
95
259
tr = None
96
260
if isdefined (self .inputs .series_tr ):
@@ -106,16 +270,116 @@ def _run_interface(self, runtime):
106
270
format = self .inputs .out_figure [- 3 :],
107
271
bbox_inches = 'tight' )
108
272
fig .clf ()
273
+
109
274
return runtime
110
275
111
276
def _list_outputs (self ):
112
277
return self ._results
113
278
114
279
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
+
115
378
def plot_confound (tseries , figsize , name , units = None ,
116
379
series_tr = None , normalize = False ):
117
380
"""
118
381
A helper function to plot :abbr:`fMRI (functional MRI)` confounds.
382
+
119
383
"""
120
384
import matplotlib
121
385
matplotlib .use ('Agg' )
0 commit comments