-
Notifications
You must be signed in to change notification settings - Fork 532
ENH CompCor #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH CompCor #1599
Changes from 64 commits
50da63f
9e0d327
9f7886c
55aec65
b45b7b1
f454cbe
ca930d3
414f11a
bca197e
124d346
ef50858
5e8b997
6127924
ac8dbfe
a5402f7
8f5eabc
d87ac71
757bd0e
a54120f
c1b0a87
9a88a11
61da367
89b530a
87108d9
33d2f30
1511bea
b66db5f
4b9dbe4
bd9113a
e78b0ae
5bf2d54
fd45c05
62bb3bd
da1e59a
6f76129
045bfdc
ee726a1
0a5bc7a
61c96ad
4fd169a
dfdd6b0
1995a01
13838bd
027d47e
5464902
f8cea61
1c655f8
08bccfa
4401bb8
29c8b74
402b6b1
c18a780
083c16f
be215f5
5f39bfc
6869a5f
c9aa6d6
9aa2300
276fb57
ea60782
6a1497d
e245073
6a88b3a
5347881
cb2faa2
bb45785
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
A dataset for use with these scripts can be downloaded from the nipype | ||
website. At the time of writing, it's at: | ||
|
||
http://nipy.org/nipype/users/pipeline_tutorial.html | ||
http://nipype.readthedocs.io/en/0.12.0/users/pipeline_tutorial.html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
# -*- coding: utf-8 -*- | ||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
''' | ||
Miscellaneous algorithms | ||
|
||
Change directory to provide relative paths for doctests | ||
>>> import os | ||
>>> filepath = os.path.dirname(os.path.realpath(__file__)) | ||
>>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data')) | ||
>>> os.chdir(datadir) | ||
|
||
''' | ||
from ..interfaces.base import (BaseInterfaceInputSpec, TraitedSpec, | ||
BaseInterface, traits, File) | ||
from ..pipeline import engine as pe | ||
from ..interfaces.utility import IdentityInterface | ||
from .misc import regress_poly | ||
|
||
import nibabel as nb | ||
import numpy as np | ||
from scipy import linalg, stats | ||
import os | ||
|
||
class CompCorInputSpec(BaseInterfaceInputSpec): | ||
realigned_file = File(exists=True, mandatory=True, | ||
desc='already realigned brain image (4D)') | ||
mask_file = File(exists=True, mandatory=False, | ||
desc='mask file that determines ROI (3D)') | ||
components_file = File('components_file.txt', exists=False, | ||
mandatory=False, usedefault=True, | ||
desc='filename to store physiological components') | ||
num_components = traits.Int(6, usedefault=True) # 6 for BOLD, 4 for ASL | ||
use_regress_poly = traits.Bool(True, usedefault=True, | ||
desc='use polynomial regression' | ||
'pre-component extraction') | ||
regress_poly_degree = traits.Range(low=1, default=1, usedefault=True, | ||
desc='the degree polynomial to use') | ||
extra_regressors = File(exists=True, mandatory=False, | ||
desc='additional regressors to add') | ||
|
||
class CompCorOutputSpec(TraitedSpec): | ||
components_file = File(exists=True, | ||
desc='text file containing the noise components') | ||
|
||
class CompCor(BaseInterface): | ||
''' | ||
Interface with core CompCor computation, used in aCompCor and tCompCor | ||
|
||
Example | ||
------- | ||
|
||
>>> ccinterface = CompCor() | ||
>>> ccinterface.inputs.realigned_file = 'functional.nii' | ||
>>> ccinterface.inputs.mask_file = 'mask.nii' | ||
>>> ccinterface.inputs.num_components = 1 | ||
>>> ccinterface.inputs.use_regress_poly = True | ||
>>> ccinterface.inputs.regress_poly_degree = 2 | ||
''' | ||
input_spec = CompCorInputSpec | ||
output_spec = CompCorOutputSpec | ||
|
||
def _run_interface(self, runtime): | ||
imgseries = nb.load(self.inputs.realigned_file).get_data() | ||
mask = nb.load(self.inputs.mask_file).get_data() | ||
voxel_timecourses = imgseries[mask > 0] | ||
# Zero-out any bad values | ||
voxel_timecourses[np.isnan(np.sum(voxel_timecourses, axis=1)), :] = 0 | ||
|
||
# from paper: | ||
# "The constant and linear trends of the columns in the matrix M were | ||
# removed [prior to ...]" | ||
if self.inputs.use_regress_poly: | ||
voxel_timecourses = regress_poly(self.inputs.regress_poly_degree, | ||
voxel_timecourses) | ||
voxel_timecourses = voxel_timecourses - np.mean(voxel_timecourses, | ||
axis=1)[:, np.newaxis] | ||
|
||
# "Voxel time series from the noise ROI (either anatomical or tSTD) were | ||
# placed in a matrix M of size Nxm, with time along the row dimension | ||
# and voxels along the column dimension." | ||
M = voxel_timecourses.T | ||
numvols = M.shape[0] | ||
numvoxels = M.shape[1] | ||
|
||
# "[... were removed] prior to column-wise variance normalization." | ||
M = M / self._compute_tSTD(M, 1.) | ||
|
||
# "The covariance matrix C = MMT was constructed and decomposed into its | ||
# principal components using a singular value decomposition." | ||
u, _, _ = linalg.svd(M, full_matrices=False) | ||
components = u[:, :self.inputs.num_components] | ||
if self.inputs.extra_regressors: | ||
components = self._add_extras(components, | ||
self.inputs.extra_regressors) | ||
|
||
components_file = os.path.join(os.getcwd(), self.inputs.components_file) | ||
np.savetxt(components_file, components, fmt="%.10f") | ||
return runtime | ||
|
||
def _list_outputs(self): | ||
outputs = self._outputs().get() | ||
outputs['components_file'] = os.path.abspath(self.inputs.components_file) | ||
return outputs | ||
|
||
def _compute_tSTD(self, M, x): | ||
stdM = np.std(M, axis=0) | ||
# set bad values to x | ||
stdM[stdM == 0] = x | ||
stdM[np.isnan(stdM)] = x | ||
stdM[np.isinf(stdM)] = x | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When does this happen? Why are those set to zero? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know! This was in the original implementation for aCompCor, though there they are set to 1. In tCompCor ROI selection, I set them to 0 because setting them to 0 guaranteed that they would not be selected for the ROI. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit counter intuitive. If the SD is infinite (as a numerical approximation of very very big) the voxel should be included. For the sake of safety I would remove this line. |
||
return stdM | ||
|
||
def _add_extras(self, components, extra_regressors): | ||
regressors = np.genfromtxt(self.inputs.extra_regressors) | ||
return np.hstack((components, regressors)) | ||
|
||
class TCompCorInputSpec(CompCorInputSpec): | ||
# and all the fields in CompCorInputSpec | ||
percentile_threshold = traits.Range(low=0., high=1., value=.02, | ||
exclude_low=True, exclude_high=True, | ||
usedefault=True, desc='the percentile ' | ||
'used to select highest-variance ' | ||
'voxels. By default the 2% of voxels ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default value is ".02" not "2%". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there should be some clarity that this should be expressed as value ranging from 0.0-1.0 rather than 0-100 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll fix the documentation. |
||
'with the highest variance are used.') | ||
|
||
class TCompCor(CompCor): | ||
''' | ||
Interface for tCompCor. Computes a ROI mask based on variance of voxels. | ||
|
||
Example | ||
------- | ||
|
||
>>> ccinterface = TCompCor() | ||
>>> ccinterface.inputs.realigned_file = 'functional.nii' | ||
>>> ccinterface.inputs.mask_file = 'mask.nii' | ||
>>> ccinterface.inputs.num_components = 1 | ||
>>> ccinterface.inputs.use_regress_poly = True | ||
>>> ccinterface.inputs.regress_poly_degree = 2 | ||
>>> ccinterface.inputs.percentile_threshold = .03 | ||
''' | ||
|
||
input_spec = TCompCorInputSpec | ||
output_spec = CompCorOutputSpec | ||
|
||
def _run_interface(self, runtime): | ||
imgseries = nb.load(self.inputs.realigned_file).get_data() | ||
|
||
# From the paper: | ||
# "For each voxel time series, the temporal standard deviation is | ||
# defined as the standard deviation of the time series after the removal | ||
# of low-frequency nuisance terms (e.g., linear and quadratic drift)." | ||
imgseries = regress_poly(2, imgseries) | ||
imgseries = imgseries - np.mean(imgseries, axis=1)[:, np.newaxis] | ||
|
||
time_voxels = imgseries.T | ||
num_voxels = np.prod(time_voxels.shape[1:]) | ||
|
||
# "To construct the tSTD noise ROI, we sorted the voxels by their | ||
# temporal standard deviation ..." | ||
tSTD = self._compute_tSTD(time_voxels, 0) | ||
sortSTD = np.sort(tSTD, axis=None) # flattened sorted matrix | ||
|
||
# use percentile_threshold to pick voxels | ||
threshold_index = int(num_voxels * (1. - self.inputs.percentile_threshold)) | ||
threshold_std = sortSTD[threshold_index] | ||
mask = tSTD >= threshold_std | ||
mask = mask.astype(int) | ||
|
||
# save mask | ||
mask_file = 'mask.nii' | ||
nb.nifti1.save(nb.Nifti1Image(mask, np.eye(4)), mask_file) | ||
self.inputs.mask_file = mask_file | ||
|
||
super(TCompCor, self)._run_interface(runtime) | ||
return runtime | ||
|
||
ACompCor = CompCor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this? In preprocessing usually regressors from different sources are combined in a later step.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added
extra_regressors
here because https://github.com/nipy/nipype/blob/master/examples/rsfmri_vol_surface_preprocessing_nipy.pyhad an
extra_regressors
input to it's compcor implementation. I assumed that was normal ... I'm not averse to leaving it out, but then I'm not sure to refactor the above file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it it "lean and mean" and remove this.