Skip to content

Commit 07ab68e

Browse files
committed
ENH: Add cosine-basis HPF to CompCor
1 parent af2b7aa commit 07ab68e

File tree

1 file changed

+57
-7
lines changed

1 file changed

+57
-7
lines changed

nipype/algorithms/confounds.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
323323
desc=('Position of mask in `mask_files` to use - '
324324
'first is the default.'))
325325
components_file = traits.Str('components_file.txt', usedefault=True,
326-
desc='Filename to store physiological components')
326+
desc='Filename to store physiological components')
327327
num_components = traits.Int(6, usedefault=True) # 6 for BOLD, 4 for ASL
328328
use_regress_poly = traits.Bool(True, usedefault=True,
329329
desc=('use polynomial regression '
@@ -333,11 +333,19 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
333333
header_prefix = traits.Str(desc=('the desired header for the output tsv '
334334
'file (one column). If undefined, will '
335335
'default to "CompCor"'))
336+
high_pass_filter = traits.Bool(
337+
False, usedefault=True,
338+
desc='Use cosine basis to remove low-frequency trends pre-component '
339+
'extraction')
340+
save_hpf_basis = traits.Either(
341+
traits.Bool, File, requires=['high_pass_filter'],
342+
desc='Save high pass filter basis as text file')
336343

337344

338345
class CompCorOutputSpec(TraitedSpec):
339346
components_file = File(exists=True,
340347
desc='text file containing the noise components')
348+
hpf_basis_file = File(desc='text file containing high-pass filter basis')
341349

342350

343351
class CompCor(BaseInterface):
@@ -403,13 +411,18 @@ def _run_interface(self, runtime):
403411

404412
mask_images = self._process_masks(mask_images, imgseries.get_data())
405413

406-
components = compute_noise_components(imgseries.get_data(),
407-
mask_images, degree,
408-
self.inputs.num_components)
414+
components, hpf_basis = compute_noise_components(
415+
imgseries.get_data(), mask_images, degree,
416+
self.inputs.num_components, self.inputs.high_pass_filter)
409417

410418
components_file = os.path.join(os.getcwd(), self.inputs.components_file)
411419
np.savetxt(components_file, components, fmt=b"%.10f", delimiter='\t',
412420
header=self._make_headers(components.shape[1]), comments='')
421+
422+
if self.inputs.save_hpf_basis:
423+
hpf_basis_file = self._list_outputs()['hpf_basis_file']
424+
np.savetxt(hpf_basis_file, hpf_basis, fmt=b'%.10f', delimiter='\t')
425+
413426
return runtime
414427

415428
def _process_masks(self, mask_images, timeseries=None):
@@ -418,6 +431,13 @@ def _process_masks(self, mask_images, timeseries=None):
418431
def _list_outputs(self):
419432
outputs = self._outputs().get()
420433
outputs['components_file'] = os.path.abspath(self.inputs.components_file)
434+
435+
save_hpf_basis = self.inputs.save_hpf_basis
436+
if save_hpf_basis:
437+
if isinstance(save_hpf_basis, bool):
438+
save_hpf_basis = os.path.abspath('hpf_basis.txt')
439+
outputs['save_hpf_basis'] = save_hpf_basis
440+
421441
return outputs
422442

423443
def _make_headers(self, num_col):
@@ -794,6 +814,27 @@ def is_outlier(points, thresh=3.5):
794814
return timepoints_to_discard
795815

796816

817+
def cosine_filter(data, timestep, remove_mean=False, axis=-1):
818+
datashape = data.shape
819+
timepoints = datashape[axis]
820+
821+
data = data.reshape((-1, timepoints))
822+
823+
design_matrix = dmtx_light(timestep * np.arange(timepoints))
824+
825+
X = np.hstack(np.ones((timepoints, 1)), design_matrix)
826+
betas = np.linalg.lstsq(X, data)[0]
827+
828+
if not remove_mean:
829+
X = X[:, 1:]
830+
betas = betas[1:]
831+
832+
residuals = data - X.dot(betas)
833+
834+
return residuals, design_matrix
835+
836+
837+
797838
def regress_poly(degree, data, remove_mean=True, axis=-1):
798839
"""
799840
Returns data with degree polynomial regressed out.
@@ -886,20 +927,23 @@ def combine_mask_files(mask_files, mask_method=None, mask_index=None):
886927
return [img]
887928

888929

889-
def compute_noise_components(imgseries, mask_images, degree, num_components):
930+
def compute_noise_components(imgseries, mask_images, degree, num_components,
931+
high_pass_filter=False):
890932
"""Compute the noise components from the imgseries for each mask
891933
892934
imgseries: a nibabel img
893935
mask_images: a list of nibabel images
894936
degree: order of polynomial used to remove trends from the timeseries
895937
num_components: number of noise components to return
938+
high_pass_filter:
896939
897940
returns:
898941
899942
components: a numpy array
900943
901944
"""
902945
components = None
946+
hpf_basis = None
903947
for img in mask_images:
904948
mask = img.get_data().astype(np.bool)
905949
if imgseries.shape[:3] != mask.shape:
@@ -913,10 +957,16 @@ def compute_noise_components(imgseries, mask_images, degree, num_components):
913957
# Zero-out any bad values
914958
voxel_timecourses[np.isnan(np.sum(voxel_timecourses, axis=1)), :] = 0
915959

960+
if high_pass_filter:
961+
# If degree == 0, remove mean in same pass
962+
voxel_timecourses, hpf_basis = cosine_filter(
963+
voxel_timecourses, 2.5, remove_mean=(degree == 0))
964+
916965
# from paper:
917966
# "The constant and linear trends of the columns in the matrix M were
918967
# removed [prior to ...]"
919-
voxel_timecourses = regress_poly(degree, voxel_timecourses)
968+
if degree > 0 or not high_pass_filter:
969+
voxel_timecourses = regress_poly(degree, voxel_timecourses)
920970

921971
# "Voxel time series from the noise ROI (either anatomical or tSTD) were
922972
# placed in a matrix M of size Nxm, with time along the row dimension
@@ -936,7 +986,7 @@ def compute_noise_components(imgseries, mask_images, degree, num_components):
936986
u[:, :num_components]))
937987
if components is None and num_components > 0:
938988
raise ValueError('No components found')
939-
return components
989+
return components, hpf_basis
940990

941991

942992
def _compute_tSTD(M, x, axis=0):

0 commit comments

Comments
 (0)