|
| 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.path as op |
| 18 | +import numpy as np |
| 19 | + |
| 20 | +from .. import logging |
| 21 | +from ..external.due import due, BibTeX |
| 22 | +from ..interfaces.base import (traits, TraitedSpec, BaseInterface, |
| 23 | + BaseInterfaceInputSpec, File, isdefined) |
| 24 | +IFLOG = logging.getLogger('interface') |
| 25 | + |
| 26 | + |
| 27 | +class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec): |
| 28 | + in_plots = File(exists=True, desc='motion parameters as written by FSL MCFLIRT') |
| 29 | + radius = traits.Float(50, usedefault=True, |
| 30 | + desc='radius in mm to calculate angular FDs, 50mm is the ' |
| 31 | + 'default since it is used in Power et al. 2012') |
| 32 | + out_file = File('fd_power_2012.txt', usedefault=True, desc='output file name') |
| 33 | + out_figure = File('fd_power_2012.pdf', usedefault=True, desc='output figure name') |
| 34 | + series_tr = traits.Float(desc='repetition time in sec.') |
| 35 | + save_plot = traits.Bool(False, usedefault=True, desc='write FD plot') |
| 36 | + normalize = traits.Bool(False, usedefault=True, desc='calculate FD in mm/s') |
| 37 | + figdpi = traits.Int(100, usedefault=True, desc='output dpi for the FD plot') |
| 38 | + figsize = traits.Tuple(traits.Float(11.7), traits.Float(2.3), usedefault=True, |
| 39 | + desc='output figure size') |
| 40 | + |
| 41 | +class FramewiseDisplacementOutputSpec(TraitedSpec): |
| 42 | + out_file = File(desc='calculated FD per timestep') |
| 43 | + out_figure = File(desc='output image file') |
| 44 | + fd_average = traits.Float(desc='average FD') |
| 45 | + |
| 46 | +class FramewiseDisplacement(BaseInterface): |
| 47 | + """ |
| 48 | + Calculate the :abbr:`FD (framewise displacement)` as in [Power2012]_. |
| 49 | + This implementation reproduces the calculation in fsl_motion_outliers |
| 50 | +
|
| 51 | + .. [Power2012] Power et al., Spurious but systematic correlations in functional |
| 52 | + connectivity MRI networks arise from subject motion, NeuroImage 59(3), |
| 53 | + 2012. doi:`10.1016/j.neuroimage.2011.10.018 |
| 54 | + <http://dx.doi.org/10.1016/j.neuroimage.2011.10.018>`_. |
| 55 | +
|
| 56 | +
|
| 57 | + """ |
| 58 | + |
| 59 | + input_spec = FramewiseDisplacementInputSpec |
| 60 | + output_spec = FramewiseDisplacementOutputSpec |
| 61 | + |
| 62 | + references_ = [{ |
| 63 | + 'entry': BibTeX("""\ |
| 64 | +@article{power_spurious_2012, |
| 65 | + title = {Spurious but systematic correlations in functional connectivity {MRI} networks \ |
| 66 | +arise from subject motion}, |
| 67 | + volume = {59}, |
| 68 | + doi = {10.1016/j.neuroimage.2011.10.018}, |
| 69 | + number = {3}, |
| 70 | + urldate = {2016-08-16}, |
| 71 | + journal = {NeuroImage}, |
| 72 | + author = {Power, Jonathan D. and Barnes, Kelly A. and Snyder, Abraham Z. and Schlaggar, \ |
| 73 | +Bradley L. and Petersen, Steven E.}, |
| 74 | + year = {2012}, |
| 75 | + pages = {2142--2154}, |
| 76 | +} |
| 77 | +"""), |
| 78 | + 'tags': ['method'] |
| 79 | + }] |
| 80 | + |
| 81 | + def _run_interface(self, runtime): |
| 82 | + mpars = np.loadtxt(self.inputs.in_plots) # mpars is N_t x 6 |
| 83 | + diff = mpars[:-1, :] - mpars[1:, :] |
| 84 | + diff[:, :3] *= self.inputs.radius |
| 85 | + fd_res = np.abs(diff).sum(axis=1) |
| 86 | + |
| 87 | + self._results = { |
| 88 | + 'out_file': op.abspath(self.inputs.out_file), |
| 89 | + 'fd_average': float(fd_res.mean()) |
| 90 | + } |
| 91 | + np.savetxt(self.inputs.out_file, fd_res) |
| 92 | + |
| 93 | + |
| 94 | + if self.inputs.save_plot: |
| 95 | + tr = None |
| 96 | + if isdefined(self.inputs.series_tr): |
| 97 | + tr = self.inputs.series_tr |
| 98 | + |
| 99 | + if self.inputs.normalize and tr is None: |
| 100 | + IFLOG.warn('FD plot cannot be normalized if TR is not set') |
| 101 | + |
| 102 | + self._results['out_figure'] = op.abspath(self.inputs.out_figure) |
| 103 | + fig = plot_confound(fd_res, self.inputs.figsize, 'FD', units='mm', |
| 104 | + series_tr=tr, normalize=self.inputs.normalize) |
| 105 | + fig.savefig(self._results['out_figure'], dpi=float(self.inputs.figdpi), |
| 106 | + format=self.inputs.out_figure[-3:], |
| 107 | + bbox_inches='tight') |
| 108 | + fig.clf() |
| 109 | + return runtime |
| 110 | + |
| 111 | + def _list_outputs(self): |
| 112 | + return self._results |
| 113 | + |
| 114 | + |
| 115 | +def plot_confound(tseries, figsize, name, units=None, |
| 116 | + series_tr=None, normalize=False): |
| 117 | + """ |
| 118 | + A helper function to plot :abbr:`fMRI (functional MRI)` confounds. |
| 119 | + """ |
| 120 | + import matplotlib |
| 121 | + matplotlib.use('Agg') |
| 122 | + import matplotlib.pyplot as plt |
| 123 | + from matplotlib.gridspec import GridSpec |
| 124 | + from matplotlib.backends.backend_pdf import FigureCanvasPdf as FigureCanvas |
| 125 | + import seaborn as sns |
| 126 | + |
| 127 | + fig = plt.Figure(figsize=figsize) |
| 128 | + FigureCanvas(fig) |
| 129 | + grid = GridSpec(1, 2, width_ratios=[3, 1], wspace=0.025) |
| 130 | + grid.update(hspace=1.0, right=0.95, left=0.1, bottom=0.2) |
| 131 | + |
| 132 | + ax = fig.add_subplot(grid[0, :-1]) |
| 133 | + if normalize and series_tr is not None: |
| 134 | + tseries /= series_tr |
| 135 | + |
| 136 | + ax.plot(tseries) |
| 137 | + ax.set_xlim((0, len(tseries))) |
| 138 | + ylabel = name |
| 139 | + if units is not None: |
| 140 | + ylabel += (' speed [{}/s]' if normalize else ' [{}]').format(units) |
| 141 | + ax.set_ylabel(ylabel) |
| 142 | + |
| 143 | + xlabel = 'Frame #' |
| 144 | + if series_tr is not None: |
| 145 | + xlabel = 'Frame # ({} sec TR)'.format(series_tr) |
| 146 | + ax.set_xlabel(xlabel) |
| 147 | + ylim = ax.get_ylim() |
| 148 | + |
| 149 | + ax = fig.add_subplot(grid[0, -1]) |
| 150 | + sns.distplot(tseries, vertical=True, ax=ax) |
| 151 | + ax.set_xlabel('Frames') |
| 152 | + ax.set_ylim(ylim) |
| 153 | + ax.set_yticklabels([]) |
| 154 | + return fig |
0 commit comments