Skip to content

Commit fcfa192

Browse files
author
Shoshana Berleant
committed
move TSNR to confounds
1 parent 4d2f10a commit fcfa192

File tree

3 files changed

+127
-106
lines changed

3 files changed

+127
-106
lines changed

nipype/algorithms/confounds.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
import nibabel as nb
2121
import numpy as np
2222
from scipy import linalg
23+
from scipy.special import legendre
2324

2425
from .. import logging
2526
from ..external.due import due, Doi, BibTeX
2627
from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
27-
BaseInterfaceInputSpec, File, isdefined)
28-
from .misc import regress_poly
28+
BaseInterfaceInputSpec, File, isdefined,
29+
InputMultiPath)
2930
IFLOG = logging.getLogger('interface')
3031

3132
class ComputeDVARSInputSpec(BaseInterfaceInputSpec):
@@ -424,6 +425,108 @@ def _run_interface(self, runtime):
424425

425426
ACompCor = CompCor
426427

428+
class TSNRInputSpec(BaseInterfaceInputSpec):
429+
in_file = InputMultiPath(File(exists=True), mandatory=True,
430+
desc='realigned 4D file or a list of 3D files')
431+
regress_poly = traits.Range(low=1, desc='Remove polynomials')
432+
tsnr_file = File('tsnr.nii.gz', usedefault=True, hash_files=False,
433+
desc='output tSNR file')
434+
mean_file = File('mean.nii.gz', usedefault=True, hash_files=False,
435+
desc='output mean file')
436+
stddev_file = File('stdev.nii.gz', usedefault=True, hash_files=False,
437+
desc='output tSNR file')
438+
detrended_file = File('detrend.nii.gz', usedefault=True, hash_files=False,
439+
desc='input file after detrending')
440+
441+
442+
class TSNROutputSpec(TraitedSpec):
443+
tsnr_file = File(exists=True, desc='tsnr image file')
444+
mean_file = File(exists=True, desc='mean image file')
445+
stddev_file = File(exists=True, desc='std dev image file')
446+
detrended_file = File(desc='detrended input file')
447+
448+
449+
class TSNR(BaseInterface):
450+
"""Computes the time-course SNR for a time series
451+
452+
Typically you want to run this on a realigned time-series.
453+
454+
Example
455+
-------
456+
457+
>>> tsnr = TSNR()
458+
>>> tsnr.inputs.in_file = 'functional.nii'
459+
>>> res = tsnr.run() # doctest: +SKIP
460+
461+
"""
462+
input_spec = TSNRInputSpec
463+
output_spec = TSNROutputSpec
464+
465+
def _run_interface(self, runtime):
466+
img = nb.load(self.inputs.in_file[0])
467+
header = img.header.copy()
468+
vollist = [nb.load(filename) for filename in self.inputs.in_file]
469+
data = np.concatenate([vol.get_data().reshape(
470+
vol.get_shape()[:3] + (-1,)) for vol in vollist], axis=3)
471+
data = np.nan_to_num(data)
472+
473+
if data.dtype.kind == 'i':
474+
header.set_data_dtype(np.float32)
475+
data = data.astype(np.float32)
476+
477+
if isdefined(self.inputs.regress_poly):
478+
data = regress_poly(self.inputs.regress_poly, data)
479+
img = nb.Nifti1Image(data, img.get_affine(), header)
480+
nb.save(img, op.abspath(self.inputs.detrended_file))
481+
482+
meanimg = np.mean(data, axis=3)
483+
stddevimg = np.std(data, axis=3)
484+
tsnr = np.zeros_like(meanimg)
485+
tsnr[stddevimg > 1.e-3] = meanimg[stddevimg > 1.e-3] / stddevimg[stddevimg > 1.e-3]
486+
img = nb.Nifti1Image(tsnr, img.get_affine(), header)
487+
nb.save(img, op.abspath(self.inputs.tsnr_file))
488+
img = nb.Nifti1Image(meanimg, img.get_affine(), header)
489+
nb.save(img, op.abspath(self.inputs.mean_file))
490+
img = nb.Nifti1Image(stddevimg, img.get_affine(), header)
491+
nb.save(img, op.abspath(self.inputs.stddev_file))
492+
return runtime
493+
494+
def _list_outputs(self):
495+
outputs = self._outputs().get()
496+
for k in ['tsnr_file', 'mean_file', 'stddev_file']:
497+
outputs[k] = op.abspath(getattr(self.inputs, k))
498+
499+
if isdefined(self.inputs.regress_poly):
500+
outputs['detrended_file'] = op.abspath(self.inputs.detrended_file)
501+
return outputs
502+
503+
def regress_poly(degree, data):
504+
''' returns data with degree polynomial regressed out.
505+
The last dimension (i.e. data.shape[-1]) should be time.
506+
'''
507+
datashape = data.shape
508+
timepoints = datashape[-1]
509+
510+
# Rearrange all voxel-wise time-series in rows
511+
data = data.reshape((-1, timepoints))
512+
513+
# Generate design matrix
514+
X = np.ones((timepoints, 1))
515+
for i in range(degree):
516+
polynomial_func = legendre(i+1)
517+
value_array = np.linspace(-1, 1, timepoints)
518+
X = np.hstack((X, polynomial_func(value_array)[:, np.newaxis]))
519+
520+
# Calculate coefficients
521+
betas = np.linalg.pinv(X).dot(data.T)
522+
523+
# Estimation
524+
datahat = X[:, 1:].dot(betas[1:, ...]).T
525+
regressed_data = data - datahat
526+
527+
# Back to original shape
528+
return regressed_data.reshape(datashape)
529+
427530
def compute_dvars(in_file, in_mask, remove_zerovariance=False):
428531
"""
429532
Compute the :abbr:`DVARS (D referring to temporal

nipype/algorithms/misc.py

Lines changed: 12 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import numpy as np
2323
from math import floor, ceil
2424
from scipy.ndimage.morphology import grey_dilation
25-
from scipy.special import legendre
2625
import scipy.io as sio
2726
import itertools
2827
import scipy.stats as stats
@@ -35,6 +34,7 @@
3534
BaseInterfaceInputSpec, isdefined,
3635
DynamicTraitedSpec, Undefined)
3736
from ..utils.filemanip import fname_presuffix, split_filename
37+
from . import confounds
3838

3939
iflogger = logging.getLogger('interface')
4040

@@ -258,108 +258,6 @@ def _list_outputs(self):
258258
outputs['nifti_file'] = self._gen_output_file_name()
259259
return outputs
260260

261-
class TSNRInputSpec(BaseInterfaceInputSpec):
262-
in_file = InputMultiPath(File(exists=True), mandatory=True,
263-
desc='realigned 4D file or a list of 3D files')
264-
regress_poly = traits.Range(low=1, desc='Remove polynomials')
265-
tsnr_file = File('tsnr.nii.gz', usedefault=True, hash_files=False,
266-
desc='output tSNR file')
267-
mean_file = File('mean.nii.gz', usedefault=True, hash_files=False,
268-
desc='output mean file')
269-
stddev_file = File('stdev.nii.gz', usedefault=True, hash_files=False,
270-
desc='output tSNR file')
271-
detrended_file = File('detrend.nii.gz', usedefault=True, hash_files=False,
272-
desc='input file after detrending')
273-
274-
275-
class TSNROutputSpec(TraitedSpec):
276-
tsnr_file = File(exists=True, desc='tsnr image file')
277-
mean_file = File(exists=True, desc='mean image file')
278-
stddev_file = File(exists=True, desc='std dev image file')
279-
detrended_file = File(desc='detrended input file')
280-
281-
282-
class TSNR(BaseInterface):
283-
"""Computes the time-course SNR for a time series
284-
285-
Typically you want to run this on a realigned time-series.
286-
287-
Example
288-
-------
289-
290-
>>> tsnr = TSNR()
291-
>>> tsnr.inputs.in_file = 'functional.nii'
292-
>>> res = tsnr.run() # doctest: +SKIP
293-
294-
"""
295-
input_spec = TSNRInputSpec
296-
output_spec = TSNROutputSpec
297-
298-
def _run_interface(self, runtime):
299-
img = nb.load(self.inputs.in_file[0])
300-
header = img.header.copy()
301-
vollist = [nb.load(filename) for filename in self.inputs.in_file]
302-
data = np.concatenate([vol.get_data().reshape(
303-
vol.get_shape()[:3] + (-1,)) for vol in vollist], axis=3)
304-
data = np.nan_to_num(data)
305-
306-
if data.dtype.kind == 'i':
307-
header.set_data_dtype(np.float32)
308-
data = data.astype(np.float32)
309-
310-
if isdefined(self.inputs.regress_poly):
311-
data = regress_poly(self.inputs.regress_poly, data)
312-
img = nb.Nifti1Image(data, img.get_affine(), header)
313-
nb.save(img, op.abspath(self.inputs.detrended_file))
314-
315-
meanimg = np.mean(data, axis=3)
316-
stddevimg = np.std(data, axis=3)
317-
tsnr = np.zeros_like(meanimg)
318-
tsnr[stddevimg > 1.e-3] = meanimg[stddevimg > 1.e-3] / stddevimg[stddevimg > 1.e-3]
319-
img = nb.Nifti1Image(tsnr, img.get_affine(), header)
320-
nb.save(img, op.abspath(self.inputs.tsnr_file))
321-
img = nb.Nifti1Image(meanimg, img.get_affine(), header)
322-
nb.save(img, op.abspath(self.inputs.mean_file))
323-
img = nb.Nifti1Image(stddevimg, img.get_affine(), header)
324-
nb.save(img, op.abspath(self.inputs.stddev_file))
325-
return runtime
326-
327-
def _list_outputs(self):
328-
outputs = self._outputs().get()
329-
for k in ['tsnr_file', 'mean_file', 'stddev_file']:
330-
outputs[k] = op.abspath(getattr(self.inputs, k))
331-
332-
if isdefined(self.inputs.regress_poly):
333-
outputs['detrended_file'] = op.abspath(self.inputs.detrended_file)
334-
return outputs
335-
336-
def regress_poly(degree, data):
337-
''' returns data with degree polynomial regressed out.
338-
The last dimension (i.e. data.shape[-1]) should be time.
339-
'''
340-
datashape = data.shape
341-
timepoints = datashape[-1]
342-
343-
# Rearrange all voxel-wise time-series in rows
344-
data = data.reshape((-1, timepoints))
345-
346-
# Generate design matrix
347-
X = np.ones((timepoints, 1))
348-
for i in range(degree):
349-
polynomial_func = legendre(i+1)
350-
value_array = np.linspace(-1, 1, timepoints)
351-
X = np.hstack((X, polynomial_func(value_array)[:, np.newaxis]))
352-
353-
# Calculate coefficients
354-
betas = np.linalg.pinv(X).dot(data.T)
355-
356-
# Estimation
357-
datahat = X[:, 1:].dot(betas[1:, ...]).T
358-
regressed_data = data - datahat
359-
360-
# Back to original shape
361-
return regressed_data.reshape(datashape)
362-
363261
class GunzipInputSpec(BaseInterfaceInputSpec):
364262
in_file = File(exists=True, mandatory=True)
365263

@@ -1520,3 +1418,14 @@ def __init__(self, **inputs):
15201418
warnings.warn(("This interface has been deprecated since 0.10.0,"
15211419
" please use nipype.algorithms.metrics.FuzzyOverlap"),
15221420
DeprecationWarning)
1421+
1422+
class TSNR(confounds.TSNR):
1423+
"""
1424+
.. deprecated:: 0.12.1
1425+
Use :py:class:`nipype.algorithms.confounds.TSNR` instead
1426+
"""
1427+
def __init__(self, **inputs):
1428+
super(confounds.TSNR, self).__init__(**inputs)
1429+
warnings.warn(("This interface has been deprecated since 0.12.0,"
1430+
" please use nipype.algorithms.confounds.TSNR"),
1431+
DeprecationWarning)

nipype/algorithms/tests/test_tsnr.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from ...testing import (assert_equal, assert_true, assert_almost_equal,
55
skipif, utils)
6-
from ..misc import TSNR
6+
from ..confounds import TSNR
7+
from .. import misc
78

89
import unittest
910
import mock
@@ -85,6 +86,14 @@ def test_tsnr_withpoly3(self):
8586
'tsnr_file': (2.6, 57.3)
8687
})
8788

89+
@mock.patch('warnings.warn')
90+
def test_deprecation_warning(self, mock_warn):
91+
# run
92+
misc.TSNR(in_file=self.in_filenames['in_file'])
93+
94+
# assert
95+
mock_warn.assert_called_once_with(mock.ANY, DeprecationWarning)
96+
8897
def assert_expected_outputs_poly(self, tsnrresult, expected_ranges):
8998
assert_equal(os.path.basename(tsnrresult.outputs.detrended_file),
9099
self.out_filenames['detrended_file'])

0 commit comments

Comments
 (0)