Skip to content

Commit acff08d

Browse files
authored
Merge pull request #1651 from shoshber/regresspoly
ENH Make regresspoly more universal
2 parents 1ce3095 + 012d334 commit acff08d

File tree

2 files changed

+48
-42
lines changed

2 files changed

+48
-42
lines changed

nipype/algorithms/confounds.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
1313
'''
1414
from __future__ import print_function, division, unicode_literals, absolute_import
15-
from builtins import str, zip, range, open
15+
from builtins import range
1616

1717
import os
1818
import os.path as op
1919

2020
import nibabel as nb
2121
import numpy as np
22-
from scipy import linalg
23-
from scipy.special import legendre
22+
from numpy.polynomial import Legendre
23+
from scipy import linalg, signal
2424

2525
from .. import logging
26-
from ..external.due import due, Doi, BibTeX
26+
from ..external.due import BibTeX
2727
from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
2828
BaseInterfaceInputSpec, File, isdefined,
2929
InputMultiPath)
@@ -160,8 +160,8 @@ def _run_interface(self, runtime):
160160
'dvars_nstd', ext=self.inputs.figformat)
161161
fig = plot_confound(dvars[1], self.inputs.figsize, 'DVARS', series_tr=tr)
162162
fig.savefig(self._results['fig_nstd'], dpi=float(self.inputs.figdpi),
163-
format=self.inputs.figformat,
164-
bbox_inches='tight')
163+
format=self.inputs.figformat,
164+
bbox_inches='tight')
165165
fig.clf()
166166

167167
if self.inputs.save_vxstd:
@@ -175,8 +175,8 @@ def _run_interface(self, runtime):
175175
fig = plot_confound(dvars[2], self.inputs.figsize, 'Voxelwise std DVARS',
176176
series_tr=tr)
177177
fig.savefig(self._results['fig_vxstd'], dpi=float(self.inputs.figdpi),
178-
format=self.inputs.figformat,
179-
bbox_inches='tight')
178+
format=self.inputs.figformat,
179+
bbox_inches='tight')
180180
fig.clf()
181181

182182
if self.inputs.save_all:
@@ -323,9 +323,9 @@ class CompCor(BaseInterface):
323323
"author = {Behzadi, Yashar and Restom, Khaled and Liau, Joy and Liu, Thomas T.},"
324324
"year = {2007},"
325325
"pages = {90-101},}"
326-
),
326+
),
327327
'tags': ['method', 'implementation']
328-
}]
328+
}]
329329

330330
def _run_interface(self, runtime):
331331
imgseries = nb.load(self.inputs.realigned_file).get_data()
@@ -337,18 +337,13 @@ def _run_interface(self, runtime):
337337
# from paper:
338338
# "The constant and linear trends of the columns in the matrix M were
339339
# removed [prior to ...]"
340-
if self.inputs.use_regress_poly:
341-
voxel_timecourses = regress_poly(self.inputs.regress_poly_degree,
342-
voxel_timecourses)
343-
voxel_timecourses = voxel_timecourses - np.mean(voxel_timecourses,
344-
axis=1)[:, np.newaxis]
340+
degree = self.inputs.regress_poly_degree if self.inputs.use_regress_poly else 0
341+
voxel_timecourses = regress_poly(degree, voxel_timecourses)
345342

346343
# "Voxel time series from the noise ROI (either anatomical or tSTD) were
347344
# placed in a matrix M of size Nxm, with time along the row dimension
348345
# and voxels along the column dimension."
349346
M = voxel_timecourses.T
350-
numvols = M.shape[0]
351-
numvoxels = M.shape[1]
352347

353348
# "[... were removed] prior to column-wise variance normalization."
354349
M = M / self._compute_tSTD(M, 1.)
@@ -412,7 +407,6 @@ def _run_interface(self, runtime):
412407
# defined as the standard deviation of the time series after the removal
413408
# of low-frequency nuisance terms (e.g., linear and quadratic drift)."
414409
imgseries = regress_poly(2, imgseries)
415-
imgseries = imgseries - np.mean(imgseries, axis=1)[:, np.newaxis]
416410

417411
time_voxels = imgseries.T
418412
num_voxels = np.prod(time_voxels.shape[1:])
@@ -488,7 +482,7 @@ def _run_interface(self, runtime):
488482
data = data.astype(np.float32)
489483

490484
if isdefined(self.inputs.regress_poly):
491-
data = regress_poly(self.inputs.regress_poly, data)
485+
data = regress_poly(self.inputs.regress_poly, data, remove_mean=False)
492486
img = nb.Nifti1Image(data, img.get_affine(), header)
493487
nb.save(img, op.abspath(self.inputs.detrended_file))
494488

@@ -513,28 +507,33 @@ def _list_outputs(self):
513507
outputs['detrended_file'] = op.abspath(self.inputs.detrended_file)
514508
return outputs
515509

516-
def regress_poly(degree, data):
510+
def regress_poly(degree, data, remove_mean=True, axis=-1):
517511
''' returns data with degree polynomial regressed out.
518-
The last dimension (i.e. data.shape[-1]) should be time.
512+
Be default it is calculated along the last axis (usu. time).
513+
If remove_mean is True (default), the data is demeaned (i.e. degree 0).
514+
If remove_mean is false, the data is not.
519515
'''
520516
datashape = data.shape
521-
timepoints = datashape[-1]
517+
timepoints = datashape[axis]
522518

523519
# Rearrange all voxel-wise time-series in rows
524520
data = data.reshape((-1, timepoints))
525521

526522
# Generate design matrix
527-
X = np.ones((timepoints, 1))
523+
X = np.ones((timepoints, 1)) # quick way to calc degree 0
528524
for i in range(degree):
529-
polynomial_func = legendre(i+1)
525+
polynomial_func = Legendre.basis(i + 1)
530526
value_array = np.linspace(-1, 1, timepoints)
531527
X = np.hstack((X, polynomial_func(value_array)[:, np.newaxis]))
532528

533529
# Calculate coefficients
534530
betas = np.linalg.pinv(X).dot(data.T)
535531

536532
# Estimation
537-
datahat = X[:, 1:].dot(betas[1:, ...]).T
533+
if remove_mean:
534+
datahat = X.dot(betas).T
535+
else: # disregard the first layer of X, which is degree 0
536+
datahat = X[:, 1:].dot(betas[1:, ...]).T
538537
regressed_data = data - datahat
539538

540539
# Back to original shape
@@ -569,7 +568,6 @@ def compute_dvars(in_file, in_mask, remove_zerovariance=False):
569568
:return: the standardized DVARS
570569
571570
"""
572-
import os.path as op
573571
import numpy as np
574572
import nibabel as nb
575573
from nitime.algorithms import AR_est_YW
@@ -594,7 +592,7 @@ def compute_dvars(in_file, in_mask, remove_zerovariance=False):
594592
mfunc = func[idx[0], idx[1], idx[2], :]
595593

596594
# Demean
597-
mfunc -= mfunc.mean(axis=1).astype(np.float32)[..., np.newaxis]
595+
mfunc = regress_poly(0, mfunc, remove_mean=True).astype(np.float32)
598596

599597
# Compute (non-robust) estimate of lag-1 autocorrelation
600598
ar1 = np.apply_along_axis(AR_est_YW, 1, mfunc, 1)[:, 0]

nipype/algorithms/tests/test_compcor.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
3-
import nipype
4-
from ...testing import assert_equal, assert_true, assert_false, skipif, utils
5-
from ..confounds import CompCor, TCompCor, ACompCor
6-
7-
import unittest
8-
import nibabel as nb
9-
import numpy as np
103
import os
114
import tempfile
125
import shutil
6+
import unittest
7+
8+
import nibabel as nb
9+
import numpy as np
10+
11+
from ...testing import assert_equal, assert_true, utils
12+
from ..confounds import CompCor, TCompCor, ACompCor
1313

1414
class TestCompCor(unittest.TestCase):
1515
''' Note: Tests currently do a poor job of testing functionality '''
@@ -27,8 +27,8 @@ def setUp(self):
2727
self.realigned_file = utils.save_toy_nii(self.fake_data + noise,
2828
self.filenames['functionalnii'])
2929
mask = np.ones(self.fake_data.shape[:3])
30-
mask[0,0,0] = 0
31-
mask[0,0,1] = 0
30+
mask[0, 0, 0] = 0
31+
mask[0, 0, 1] = 0
3232
self.mask_file = utils.save_toy_nii(mask, self.filenames['masknii'])
3333

3434
def test_compcor(self):
@@ -52,20 +52,28 @@ def test_compcor(self):
5252

5353
def test_tcompcor(self):
5454
ccinterface = TCompCor(realigned_file=self.realigned_file, percentile_threshold=0.75)
55-
self.run_cc(ccinterface, [['-0.1535587949', '-0.4318584065'],
56-
['0.4286265207', '0.7162580102'],
57-
['-0.6288093202', '0.0465452630'],
58-
['0.5859742580', '-0.5144309306'],
59-
['-0.2322326636', '0.1834860639']])
55+
self.run_cc(ccinterface, [['-0.1114536190', '-0.4632908609'],
56+
['0.4566907310', '0.6983205193'],
57+
['-0.7132557407', '0.1340170559'],
58+
['0.5022537643', '-0.5098322262'],
59+
['-0.1342351356', '0.1407855119']])
6060

61-
def test_tcompcor_with_default_percentile(self):
61+
def test_tcompcor_no_percentile(self):
6262
ccinterface = TCompCor(realigned_file=self.realigned_file)
6363
ccinterface.run()
6464

6565
mask = nb.load('mask.nii').get_data()
6666
num_nonmasked_voxels = np.count_nonzero(mask)
6767
assert_equal(num_nonmasked_voxels, 1)
6868

69+
def test_compcor_no_regress_poly(self):
70+
self.run_cc(CompCor(realigned_file=self.realigned_file, mask_file=self.mask_file,
71+
use_regress_poly=False), [['0.4451946442', '-0.7683311482'],
72+
['-0.4285129505', '-0.0926034137'],
73+
['0.5721540256', '0.5608764842'],
74+
['-0.5367548139', '0.0059943226'],
75+
['-0.0520809054', '0.2940637551']])
76+
6977
def run_cc(self, ccinterface, expected_components):
7078
# run
7179
ccresult = ccinterface.run()

0 commit comments

Comments
 (0)