Skip to content

Commit aae371d

Browse files
authored
Merge pull request #1606 from oesteban/enh/NewDVARSInterface
[ENH] New ComputeDVARS interface
2 parents 31eb968 + 985d4e3 commit aae371d

File tree

8 files changed

+368
-6
lines changed

8 files changed

+368
-6
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ install:
3131
- conda update --all -y python=$TRAVIS_PYTHON_VERSION
3232
# - if [[ "${INSTALL_DEB_DEPENDECIES}" == "true" && ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]]; then
3333
# conda install -y vtk mayavi; fi
34-
- conda install -y nipype
34+
- conda install -y nipype matplotlib nitime
3535
- pip install python-coveralls coverage doctest-ignore-unicode
3636
- if [ ! -z "$DUECREDIT_ENABLE"]; then pip install duecredit; fi
3737
- rm -r /home/travis/miniconda/lib/python${TRAVIS_PYTHON_VERSION}/site-packages/nipype*

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Upcoming release 0.13
22
=====================
33

4+
* ENH: Add a DVARS calculation interface (https://github.com/nipy/nipype/pull/1606)
45
* ENH: Convenient load/save of interface inputs (https://github.com/nipy/nipype/pull/1591)
56
* ENH: Add a Framewise Displacement calculation interface (https://github.com/nipy/nipype/pull/1604)
67
* FIX: Use builtins open and unicode literals for py3 compatibility (https://github.com/nipy/nipype/pull/1572)

nipype/algorithms/confounds.py

Lines changed: 266 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,181 @@
1414
from __future__ import print_function, division, unicode_literals, absolute_import
1515
from builtins import str, zip, range, open
1616

17+
import os
1718
import os.path as op
19+
20+
import nibabel as nb
1821
import numpy as np
1922

2023
from .. import logging
21-
from ..external.due import due, BibTeX
24+
from ..external.due import due, Doi, BibTeX
2225
from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
2326
BaseInterfaceInputSpec, File, isdefined)
2427
IFLOG = 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\tnon-std DVARS\tvx-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+
27192
class 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+
115378
def 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')
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from ...testing import assert_equal
3+
from ..confounds import ComputeDVARS
4+
5+
6+
def test_ComputeDVARS_inputs():
7+
input_map = dict(figdpi=dict(usedefault=True,
8+
),
9+
figformat=dict(usedefault=True,
10+
),
11+
figsize=dict(usedefault=True,
12+
),
13+
ignore_exception=dict(nohash=True,
14+
usedefault=True,
15+
),
16+
in_file=dict(mandatory=True,
17+
),
18+
in_mask=dict(mandatory=True,
19+
),
20+
remove_zerovariance=dict(usedefault=True,
21+
),
22+
save_all=dict(usedefault=True,
23+
),
24+
save_nstd=dict(usedefault=True,
25+
),
26+
save_plot=dict(usedefault=True,
27+
),
28+
save_std=dict(usedefault=True,
29+
),
30+
save_vxstd=dict(usedefault=True,
31+
),
32+
series_tr=dict(),
33+
)
34+
inputs = ComputeDVARS.input_spec()
35+
36+
for key, metadata in list(input_map.items()):
37+
for metakey, value in list(metadata.items()):
38+
yield assert_equal, getattr(inputs.traits()[key], metakey), value
39+
40+
41+
def test_ComputeDVARS_outputs():
42+
output_map = dict(avg_nstd=dict(),
43+
avg_std=dict(),
44+
avg_vxstd=dict(),
45+
fig_nstd=dict(),
46+
fig_std=dict(),
47+
fig_vxstd=dict(),
48+
out_all=dict(),
49+
out_nstd=dict(),
50+
out_std=dict(),
51+
out_vxstd=dict(),
52+
)
53+
outputs = ComputeDVARS.output_spec()
54+
55+
for key, metadata in list(output_map.items()):
56+
for metakey, value in list(metadata.items()):
57+
yield assert_equal, getattr(outputs.traits()[key], metakey), value

0 commit comments

Comments
 (0)