-
Notifications
You must be signed in to change notification settings - Fork 535
WIP logging and validations in CompCor, SignalExtraction, ApplyTopUp #1676
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
Changes from all commits
73b9a0e
e811b96
0c7c649
8313923
974a42b
741405f
d38562c
a512faf
6448ee8
e98bd95
4f27943
b04d4e7
7c7dcd2
8cfa00f
dd8dfb4
a0a31c4
7f84dff
b3187f3
bba591b
8a660dc
89b9856
a42439f
b85fd5f
35a0bb3
4f80b2a
c217a23
cd5385e
9239f7b
d5e1a0b
b48395d
fb3c550
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 |
---|---|---|
|
@@ -32,7 +32,7 @@ | |
class ComputeDVARSInputSpec(BaseInterfaceInputSpec): | ||
in_file = File(exists=True, mandatory=True, desc='functional data, after HMC') | ||
in_mask = File(exists=True, mandatory=True, desc='a brain mask') | ||
remove_zerovariance = traits.Bool(False, usedefault=True, | ||
remove_zerovariance = traits.Bool(True, usedefault=True, | ||
desc='remove voxels with zero variance') | ||
save_std = traits.Bool(True, usedefault=True, | ||
desc='save standardized DVARS') | ||
|
@@ -255,7 +255,7 @@ def _run_interface(self, runtime): | |
'out_file': op.abspath(self.inputs.out_file), | ||
'fd_average': float(fd_res.mean()) | ||
} | ||
np.savetxt(self.inputs.out_file, fd_res) | ||
np.savetxt(self.inputs.out_file, fd_res, header='framewise_displacement') | ||
|
||
if self.inputs.save_plot: | ||
tr = None | ||
|
@@ -291,6 +291,8 @@ class CompCorInputSpec(BaseInterfaceInputSpec): | |
'pre-component extraction') | ||
regress_poly_degree = traits.Range(low=1, default=1, usedefault=True, | ||
desc='the degree polynomial to use') | ||
header = traits.Str(desc='the desired header for the output tsv file (one column).' | ||
'If undefined, will default to "CompCor"') | ||
|
||
class CompCorOutputSpec(TraitedSpec): | ||
components_file = File(exists=True, | ||
|
@@ -329,6 +331,13 @@ class CompCor(BaseInterface): | |
def _run_interface(self, runtime): | ||
imgseries = nb.load(self.inputs.realigned_file).get_data() | ||
mask = nb.load(self.inputs.mask_file).get_data() | ||
|
||
if imgseries.shape[:3] != mask.shape: | ||
raise ValueError('Inputs for CompCor, func {} and mask {}, do not have matching ' | ||
'spatial dimensions ({} and {}, respectively)' | ||
.format(self.inputs.realigned_file, self.inputs.mask_file, | ||
imgseries.shape[:3], mask.shape)) | ||
|
||
voxel_timecourses = imgseries[mask > 0] | ||
# Zero-out any bad values | ||
voxel_timecourses[np.isnan(np.sum(voxel_timecourses, axis=1)), :] = 0 | ||
|
@@ -352,7 +361,10 @@ def _run_interface(self, runtime): | |
u, _, _ = linalg.svd(M, full_matrices=False) | ||
components = u[:, :self.inputs.num_components] | ||
components_file = os.path.join(os.getcwd(), self.inputs.components_file) | ||
np.savetxt(components_file, components, fmt=b"%.10f") | ||
|
||
self._set_header() | ||
np.savetxt(components_file, components, fmt=b"%.10f", delimiter='\t', | ||
header=self._make_headers(components.shape[1])) | ||
return runtime | ||
|
||
def _list_outputs(self): | ||
|
@@ -367,6 +379,26 @@ def _compute_tSTD(self, M, x): | |
stdM[np.isnan(stdM)] = x | ||
return stdM | ||
|
||
def _set_header(self, header='CompCor'): | ||
self.inputs.header = self.inputs.header if isdefined(self.inputs.header) else header | ||
|
||
def _make_headers(self, num_col): | ||
headers = [] | ||
for i in range(num_col): | ||
headers.append(self.inputs.header + str(i)) | ||
return '\t'.join(headers) | ||
|
||
|
||
class ACompCor(CompCor): | ||
''' Anatomical compcor; for input/output, see CompCor. | ||
If the mask provided is an anatomical mask, CompCor == ACompCor ''' | ||
|
||
def __init__(self, *args, **kwargs): | ||
''' exactly the same as compcor except the header ''' | ||
super(ACompCor, self).__init__(*args, **kwargs) | ||
self._set_header('aCompCor') | ||
|
||
|
||
class TCompCorInputSpec(CompCorInputSpec): | ||
# and all the fields in CompCorInputSpec | ||
percentile_threshold = traits.Range(low=0., high=1., value=.02, | ||
|
@@ -401,6 +433,11 @@ class TCompCor(CompCor): | |
def _run_interface(self, runtime): | ||
imgseries = nb.load(self.inputs.realigned_file).get_data() | ||
|
||
if imgseries.ndim != 4: | ||
raise ValueError('tCompCor expected a 4-D nifti file. Input {} has {} dimensions ' | ||
'(shape {})' | ||
.format(self.inputs.realigned_file, imgseries.ndim, imgseries.shape)) | ||
|
||
# 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 | ||
|
@@ -419,18 +456,19 @@ def _run_interface(self, runtime): | |
threshold_index = int(num_voxels * (1. - self.inputs.percentile_threshold)) | ||
threshold_std = sortSTD[threshold_index] | ||
mask = tSTD >= threshold_std | ||
mask = mask.astype(int) | ||
mask = mask.astype(int).T | ||
|
||
# save mask | ||
mask_file = 'mask.nii' | ||
mask_file = os.path.abspath('mask.nii') | ||
nb.nifti1.save(nb.Nifti1Image(mask, np.eye(4)), mask_file) | ||
IFLOG.debug('tCompcor computed and saved mask of shape {} to mask_file {}' | ||
.format(mask.shape, mask_file)) | ||
self.inputs.mask_file = mask_file | ||
self._set_header('tCompCor') | ||
|
||
super(TCompCor, self)._run_interface(runtime) | ||
return runtime | ||
|
||
ACompCor = CompCor | ||
|
||
class TSNRInputSpec(BaseInterfaceInputSpec): | ||
in_file = InputMultiPath(File(exists=True), mandatory=True, | ||
desc='realigned 4D file or a list of 3D files') | ||
|
@@ -512,6 +550,8 @@ def regress_poly(degree, data, remove_mean=True, axis=-1): | |
If remove_mean is True (default), the data is demeaned (i.e. degree 0). | ||
If remove_mean is false, the data is not. | ||
''' | ||
IFLOG.debug('Performing polynomial regression on data of shape ' + str(data.shape)) | ||
|
||
datashape = data.shape | ||
timepoints = datashape[axis] | ||
|
||
|
@@ -570,6 +610,7 @@ def compute_dvars(in_file, in_mask, remove_zerovariance=False): | |
import numpy as np | ||
import nibabel as nb | ||
from nitime.algorithms import AR_est_YW | ||
import warnings | ||
|
||
func = nb.load(in_file).get_data().astype(np.float32) | ||
mask = nb.load(in_mask).get_data().astype(np.uint8) | ||
|
@@ -585,7 +626,7 @@ def compute_dvars(in_file, in_mask, remove_zerovariance=False): | |
|
||
if remove_zerovariance: | ||
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. We took a closer look on this (w. @chrisfilo) and I realized that you were 100% right about this. Sorry about that. Since this Again, my apologies for not looking at this more thoroughly before. |
||
# Remove zero-variance voxels across time axis | ||
mask = zero_variance(func, mask) | ||
mask = zero_remove(func_sd, mask) | ||
|
||
idx = np.where(mask > 0) | ||
mfunc = func[idx[0], idx[1], idx[2], :] | ||
|
@@ -609,31 +650,28 @@ def compute_dvars(in_file, in_mask, remove_zerovariance=False): | |
# standardization | ||
dvars_stdz = dvars_nstd / diff_sd_mean | ||
|
||
# voxelwise standardization | ||
diff_vx_stdz = func_diff / np.array([diff_sdhat] * func_diff.shape[-1]).T | ||
dvars_vx_stdz = diff_vx_stdz.std(axis=0, ddof=1) | ||
with warnings.catch_warnings(): # catch, e.g., divide by zero errors | ||
warnings.filterwarnings('error') | ||
|
||
# voxelwise standardization | ||
diff_vx_stdz = func_diff / np.array([diff_sdhat] * func_diff.shape[-1]).T | ||
dvars_vx_stdz = diff_vx_stdz.std(axis=0, ddof=1) | ||
|
||
return (dvars_stdz, dvars_nstd, dvars_vx_stdz) | ||
|
||
def zero_variance(func, mask): | ||
def zero_remove(data, mask): | ||
""" | ||
Mask out voxels with zero variance across t-axis | ||
Modify inputted mask to also mask out zero values | ||
|
||
:param numpy.ndarray func: input fMRI dataset, after motion correction | ||
:param numpy.ndarray mask: 3D brain mask | ||
:return: the 3D mask of voxels with nonzero variance across :math:`t`. | ||
:param numpy.ndarray data: e.g. voxelwise stddev of fMRI dataset, after motion correction | ||
:param numpy.ndarray mask: brain mask (same dimensions as data) | ||
:return: the mask with any additional zero voxels removed (same dimensions as inputs) | ||
:rtype: numpy.ndarray | ||
|
||
""" | ||
idx = np.where(mask > 0) | ||
func = func[idx[0], idx[1], idx[2], :] | ||
tvariance = func.var(axis=1) | ||
tv_mask = np.zeros_like(tvariance, dtype=np.uint8) | ||
tv_mask[tvariance > 0] = 1 | ||
|
||
newmask = np.zeros_like(mask, dtype=np.uint8) | ||
newmask[idx] = tv_mask | ||
return newmask | ||
new_mask = mask.copy() | ||
new_mask[data == 0] = 0 | ||
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 think we can just do this directly on line 627 |
||
return new_mask | ||
|
||
def plot_confound(tseries, figsize, name, units=None, | ||
series_tr=None, normalize=False): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,9 @@ | |
from tempfile import mkdtemp | ||
from shutil import rmtree | ||
|
||
from nipype.testing import (assert_equal, example_data, skipif, assert_true) | ||
from io import open | ||
|
||
from nipype.testing import (assert_equal, example_data, skipif, assert_true, assert_in) | ||
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. 👍 |
||
from nipype.algorithms.confounds import FramewiseDisplacement, ComputeDVARS | ||
import numpy as np | ||
|
||
|
@@ -24,8 +26,14 @@ def test_fd(): | |
out_file=tempdir + '/fd.txt') | ||
res = fdisplacement.run() | ||
|
||
with open(res.outputs.out_file) as all_lines: | ||
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. Please add |
||
for line in all_lines: | ||
yield assert_in, 'framewise_displacement', line | ||
break | ||
|
||
yield assert_true, np.allclose(ground_truth, np.loadtxt(res.outputs.out_file), atol=.16) | ||
yield assert_true, np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 | ||
|
||
rmtree(tempdir) | ||
|
||
@skipif(nonitime) | ||
|
@@ -35,8 +43,14 @@ def test_dvars(): | |
dvars = ComputeDVARS(in_file=example_data('ds003_sub-01_mc.nii.gz'), | ||
in_mask=example_data('ds003_sub-01_mc_brainmask.nii.gz'), | ||
save_all=True) | ||
|
||
origdir = os.getcwd() | ||
os.chdir(tempdir) | ||
|
||
res = dvars.run() | ||
|
||
dv1 = np.loadtxt(res.outputs.out_std) | ||
yield assert_equal, (np.abs(dv1 - ground_truth).sum()/ len(dv1)) < 0.05, True | ||
|
||
os.chdir(origdir) | ||
rmtree(tempdir) |
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.
Why here you are using the numpy array
.ndim
property, and you have changed the same in your modification of ApplyTopup to use the nibabel header object? Which one is the appropriate option? If both are equivalent, I prefer this (numpy.ndim) rather than your change to ApplyTopup.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.
The difference is that using numpy requires loading the data into numpy memory, which is expensive. In fact, in ApplyTopUp, it was erroring out. Here, in TCompCor we are loading data into numpy memory anyway, so it is not an additional cost.
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.
Thanks! That's very sound.