Skip to content

Commit c9d30be

Browse files
committed
[ENH] New ComputeDVARS interface
1 parent 99d191a commit c9d30be

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed

nipype/algorithms/confounds.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
'''
5+
Algorithms to compute confounds in :abbr:`fMRI (functional MRI)`
6+
7+
Change directory to provide relative paths for doctests
8+
>>> import os
9+
>>> filepath = os.path.dirname(os.path.realpath(__file__))
10+
>>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data'))
11+
>>> os.chdir(datadir)
12+
13+
'''
14+
from __future__ import print_function, division, unicode_literals, absolute_import
15+
from builtins import str, zip, range, open
16+
17+
import os
18+
import os.path as op
19+
20+
import nibabel as nb
21+
import numpy as np
22+
23+
from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
24+
BaseInterfaceInputSpec, File)
25+
26+
27+
class ComputeDVARSInputSpec(BaseInterfaceInputSpec):
28+
in_file = File(exists=True, mandatory=True, desc='functional data, after HMC')
29+
in_mask = File(exists=True, mandatory=True, desc='a brain mask')
30+
save_std = traits.Bool(True, usedefault=True,
31+
desc='save standardized DVARS')
32+
save_nstd = traits.Bool(False, usedefault=True,
33+
desc='save non-standardized DVARS')
34+
save_vxstd = traits.Bool(False, usedefault=True,
35+
desc='save voxel-wise standardized DVARS')
36+
save_all = traits.Bool(False, usedefault=True, desc='output all DVARS')
37+
38+
39+
class ComputeDVARSOutputSpec(TraitedSpec):
40+
out_std = File(exists=True, desc='output text file')
41+
out_nstd = File(exists=True, desc='output text file')
42+
out_vxstd = File(exists=True, desc='output text file')
43+
out_all = File(exists=True, desc='output text file')
44+
45+
46+
class ComputeDVARS(BaseInterface):
47+
"""
48+
Computes the DVARS.
49+
"""
50+
input_spec = ComputeDVARSInputSpec
51+
output_spec = ComputeDVARSOutputSpec
52+
53+
def __init__(self, **inputs):
54+
self._results = {}
55+
super(ComputeDVARS, self).__init__(**inputs)
56+
57+
def _gen_fname(self, suffix, ext=None):
58+
fname, in_ext = op.splitext(op.basename(
59+
self.inputs.in_file))
60+
61+
if in_ext == '.gz':
62+
fname, in_ext2 = op.splitext(fname)
63+
in_ext = in_ext2 + in_ext
64+
65+
if ext is None:
66+
ext = in_ext
67+
68+
if ext.startswith('.'):
69+
ext = ext[1:]
70+
71+
return op.abspath('{}_{},{}'.format(fname, suffix, ext))
72+
73+
def _parse_inputs(self):
74+
if (self.inputs.save_std or self.inputs.save_nstd or
75+
self.inputs.save_vxstd or self.inputs.save_all):
76+
return super(ComputeDVARS, self)._parse_inputs()
77+
else:
78+
raise RuntimeError('At least one of the save_* options must be True')
79+
80+
def _run_interface(self, runtime):
81+
dvars = compute_dvars(self.inputs.in_file, self.inputs.in_mask)
82+
83+
if self.inputs.save_std:
84+
out_file = self._gen_fname('dvars_std', ext='tsv')
85+
np.savetxt(out_file, dvars[0], fmt=b'%.12f')
86+
self._results['out_std'] = out_file
87+
88+
if self.inputs.save_nstd:
89+
out_file = self._gen_fname('dvars_nstd', ext='tsv')
90+
np.savetxt(out_file, dvars[1], fmt=b'%.12f')
91+
self._results['out_nstd'] = out_file
92+
93+
if self.inputs.save_vxstd:
94+
out_file = self._gen_fname('dvars_vxstd', ext='tsv')
95+
np.savetxt(out_file, dvars[2], fmt=b'%.12f')
96+
self._results['out_vxstd'] = out_file
97+
98+
if self.inputs.save_all:
99+
out_file = self._gen_fname('dvars', ext='tsv')
100+
np.savetxt(out_file, np.vstack(dvars), fmt=b'%.12f', delimiter=b'\t',
101+
header='# std DVARS\tnon-std DVARS\tvx-wise std DVARS')
102+
self._results['out_all'] = out_file
103+
104+
return runtime
105+
106+
def _list_outputs(self):
107+
return self._results
108+
109+
110+
def compute_dvars(in_file, in_mask):
111+
"""
112+
Compute the :abbr:`DVARS (D referring to temporal
113+
derivative of timecourses, VARS referring to RMS variance over voxels)`
114+
[Power2012]_.
115+
116+
Particularly, the *standardized* :abbr:`DVARS (D referring to temporal
117+
derivative of timecourses, VARS referring to RMS variance over voxels)`
118+
[Nichols2013]_ are computed.
119+
120+
.. note:: Implementation details
121+
122+
Uses the implementation of the `Yule-Walker equations
123+
from nitime
124+
<http://nipy.org/nitime/api/generated/nitime.algorithms.autoregressive.html\
125+
#nitime.algorithms.autoregressive.AR_est_YW>`_
126+
for the :abbr:`AR (auto-regressive)` filtering of the fMRI signal.
127+
128+
:param numpy.ndarray func: functional data, after head-motion-correction.
129+
:param numpy.ndarray mask: a 3D mask of the brain
130+
:param bool output_all: write out all dvars
131+
:param str out_file: a path to which the standardized dvars should be saved.
132+
:return: the standardized DVARS
133+
134+
"""
135+
import os.path as op
136+
import numpy as np
137+
import nibabel as nb
138+
from nitime.algorithms import AR_est_YW
139+
140+
func = nb.load(in_file).get_data().astype(np.float32)
141+
mask = nb.load(in_mask).get_data().astype(np.uint8)
142+
143+
if len(func.shape) != 4:
144+
raise RuntimeError(
145+
"Input fMRI dataset should be 4-dimensional")
146+
147+
# Remove zero-variance voxels across time axis
148+
zv_mask = zero_variance(func, mask)
149+
idx = np.where(zv_mask > 0)
150+
mfunc = func[idx[0], idx[1], idx[2], :]
151+
152+
# Robust standard deviation
153+
func_sd = (np.percentile(mfunc, 75) -
154+
np.percentile(mfunc, 25)) / 1.349
155+
156+
# Demean
157+
mfunc -= mfunc.mean(axis=1).astype(np.float32)[..., np.newaxis]
158+
159+
# AR1
160+
ak_coeffs = np.apply_along_axis(AR_est_YW, 1, mfunc, 1)
161+
162+
# Predicted standard deviation of temporal derivative
163+
func_sd_pd = np.squeeze(np.sqrt((2. * (1. - ak_coeffs[:, 0])).tolist()) * func_sd)
164+
diff_sd_mean = func_sd_pd[func_sd_pd > 0].mean()
165+
166+
# Compute temporal difference time series
167+
func_diff = np.diff(mfunc, axis=1)
168+
169+
# DVARS (no standardization)
170+
dvars_nstd = func_diff.std(axis=0)
171+
172+
# standardization
173+
dvars_stdz = dvars_nstd / diff_sd_mean
174+
175+
# voxelwise standardization
176+
diff_vx_stdz = func_diff / np.array([func_sd_pd] * func_diff.shape[-1]).T
177+
dvars_vx_stdz = diff_vx_stdz.std(1, ddof=1)
178+
179+
return (dvars_stdz, dvars_nstd, dvars_vx_stdz)
180+
181+
def zero_variance(func, mask):
182+
"""
183+
Mask out voxels with zero variance across t-axis
184+
185+
:param numpy.ndarray func: input fMRI dataset, after motion correction
186+
:param numpy.ndarray mask: 3D brain mask
187+
:return: the 3D mask of voxels with nonzero variance across :math:`t`.
188+
:rtype: numpy.ndarray
189+
190+
"""
191+
idx = np.where(mask > 0)
192+
func = func[idx[0], idx[1], idx[2], :]
193+
tvariance = func.var(axis=1)
194+
tv_mask = np.zeros_like(tvariance, dtype=np.uint8)
195+
tv_mask[tvariance > 0] = 1
196+
197+
newmask = np.zeros_like(mask, dtype=np.uint8)
198+
newmask[idx] = tv_mask
199+
return newmask

0 commit comments

Comments
 (0)