Skip to content

Commit 279f79c

Browse files
committed
[ENH] Add a FramewiseDisplacement interface
Calculates the FD following the implementation of fsl_motion_outliers, that theoretically follows that of Power et al. 2012. If ```save_plot``` is ```True```, then it will save an FD plot using seaborn (but this is optional). Includes a test that assess that this implementation is equivalent to the one in fsl_motion_outliers
1 parent 99d191a commit 279f79c

File tree

4 files changed

+844
-0
lines changed

4 files changed

+844
-0
lines changed

nipype/algorithms/misc.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,69 @@
3838
iflogger = logging.getLogger('interface')
3939

4040

41+
class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec):
42+
in_plots = File(exists=True, desc='motion parameters as written by FSL MCFLIRT')
43+
radius = traits.Float(50, usedefault=True,
44+
desc='radius in mm to calculate angular FDs, 50mm is the '
45+
'default since it is used in Power et al. 2012')
46+
out_file = File('fd_power_2012.txt', usedefault=True, desc='output file name')
47+
out_figure = File('fd_power_2012.pdf', usedefault=True, desc='output figure name')
48+
series_tr = traits.Float(desc='repetition time in sec.')
49+
save_plot = traits.Bool(False, usedefault=True, desc='write FD plot')
50+
normalize = traits.Bool(False, usedefault=True, desc='calculate FD in mm/s',
51+
requires=['series_tr'])
52+
figdpi = traits.Int(100, usedefault=True, desc='output dpi for the FD plot')
53+
figsize = traits.Tuple(traits.Float(11.7), traits.Float(2.3), usedefault=True,
54+
desc='output figure size')
55+
56+
class FramewiseDisplacementOutputSpec(TraitedSpec):
57+
out_file = File(desc='calculated FD per timestep')
58+
out_figure = File(desc='output image file')
59+
fd_average = traits.Float(desc='average FD')
60+
61+
class FramewiseDisplacement(BaseInterface):
62+
"""
63+
Calculate the :abbr:`FD (framewise displacement)` as in [Power2012]_.
64+
This implementation reproduces the calculation in fsl_motion_outliers
65+
66+
.. [Power2012] Power et al., Spurious but systematic correlations in functional
67+
connectivity MRI networks arise from subject motion, NeuroImage 59(3),
68+
2012. doi:`10.1016/j.neuroimage.2011.10.018
69+
<http://dx.doi.org/10.1016/j.neuroimage.2011.10.018>`_.
70+
71+
72+
"""
73+
74+
input_spec = FramewiseDisplacementInputSpec
75+
output_spec = FramewiseDisplacementOutputSpec
76+
77+
def _run_interface(self, runtime):
78+
mpars = np.loadtxt(self.inputs.in_plots) # mpars is N_t x 6
79+
diff = mpars[:-1, :] - mpars[1:, :]
80+
diff[:, :3] *= self.inputs.radius
81+
fd_res = np.abs(diff).sum(axis=1)
82+
83+
self._results = {
84+
'out_file': op.abspath(self.inputs.out_file),
85+
'fd_average': float(fd_res.mean())
86+
}
87+
np.savetxt(self.inputs.out_file, fd_res)
88+
89+
if self.inputs.save_plot:
90+
self._results['out_figure'] = op.abspath(self.inputs.out_figure)
91+
tr = None
92+
if self.inputs.normalize:
93+
tr = self.inputs.series_tr
94+
fig = plot_fd(fd_res, self.inputs.figsize, series_tr=tr)
95+
fig.savefig(self._results['out_figure'], dpi=float(self.inputs.figdpi),
96+
format=self.inputs.out_figure[-3:],
97+
bbox_inches='tight')
98+
return runtime
99+
100+
def _list_outputs(self):
101+
return self._results
102+
103+
41104
class PickAtlasInputSpec(BaseInterfaceInputSpec):
42105
atlas = File(exists=True, desc="Location of the atlas that will be used.",
43106
mandatory=True)
@@ -1460,6 +1523,40 @@ def merge_rois(in_files, in_idxs, in_ref,
14601523
return out_file
14611524

14621525

1526+
def plot_fd(fd_values, figsize, series_tr=None):
1527+
"""
1528+
A helper function to plot the framewise displacement
1529+
"""
1530+
import matplotlib
1531+
matplotlib.use('Agg')
1532+
import matplotlib.pyplot as plt
1533+
from matplotlib.gridspec import GridSpec
1534+
from matplotlib.backends.backend_pdf import FigureCanvasPdf as FigureCanvas
1535+
import seaborn as sns
1536+
1537+
fig = plt.Figure(figsize=figsize)
1538+
FigureCanvas(fig)
1539+
grid = GridSpec(1, 2, width_ratios=[3, 1], wspace=0.025)
1540+
grid.update(hspace=1.0, right=0.95, left=0.1, bottom=0.2)
1541+
1542+
ax = fig.add_subplot(grid[0, :-1])
1543+
ax.plot(fd_values)
1544+
ax.set_xlim((0, len(fd_values)))
1545+
ax.set_ylabel('FD [{}]'.format('mm/s' if series_tr is not None else 'mm'))
1546+
1547+
xlabel = 'Frame #'
1548+
if series_tr is not None:
1549+
xlabel = 'Frame # ({} sec TR)'.format(series_tr)
1550+
ax.set_xlabel(xlabel)
1551+
ylim = ax.get_ylim()
1552+
1553+
ax = fig.add_subplot(grid[0, -1])
1554+
sns.distplot(fd_values, vertical=True, ax=ax)
1555+
ax.set_xlabel('Frames')
1556+
ax.set_ylim(ylim)
1557+
ax.set_yticklabels([])
1558+
return fig
1559+
14631560
# Deprecated interfaces ------------------------------------------------------
14641561

14651562
class Distance(nam.Distance):

nipype/algorithms/tests/test_fd.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from nipype.testing import (assert_equal, example_data)
5+
from nipype.algorithms.misc import FramewiseDisplacement
6+
import numpy as np
7+
from tempfile import mkdtemp
8+
from shutil import rmtree
9+
10+
def test_fd():
11+
tempdir = mkdtemp()
12+
ground_truth = np.loadtxt(example_data('fsl_motion_outliers_fd.txt'))
13+
fd = FramewiseDisplacement(in_plots=example_data('fsl_mcflirt_movpar.txt'),
14+
out_file=tempdir + '/fd.txt')
15+
res = fd.run()
16+
yield assert_equal, np.allclose(ground_truth, np.loadtxt(res.outputs.out_file)), True
17+
yield assert_equal, np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-4, True
18+
rmtree(tempdir)

0 commit comments

Comments
 (0)