Skip to content

Commit b3f34a5

Browse files
committed
[skip tests][skip docs][skip ds210][skip ds054] wip: remove non steady state volumes from aroma calculation
1 parent c9bfe4f commit b3f34a5

File tree

3 files changed

+96
-23
lines changed

3 files changed

+96
-23
lines changed

fmriprep/interfaces/confounds.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def _run_interface(self, runtime):
9494

9595
class ICAConfoundsInputSpec(BaseInterfaceInputSpec):
9696
in_directory = Directory(mandatory=True, desc='directory where ICA derivatives are found')
97+
n_volumes = traits.Int(desc='number of non steady state volumes identified')
9798
ignore_aroma_err = traits.Bool(False, usedefault=True, desc='ignore ICA-AROMA errors')
9899

99100

@@ -111,7 +112,7 @@ class ICAConfounds(SimpleInterface):
111112

112113
def _run_interface(self, runtime):
113114
aroma_confounds, motion_ics_out, melodic_mix_out = _get_ica_confounds(
114-
self.inputs.in_directory, newpath=runtime.cwd)
115+
self.inputs.in_directory, self.inputs.n_volumes, newpath=runtime.cwd)
115116

116117
if aroma_confounds is not None:
117118
self._results['aroma_confounds'] = aroma_confounds
@@ -198,7 +199,7 @@ def _adjust_indices(left_df, right_df):
198199
return combined_out, confounds_list
199200

200201

201-
def _get_ica_confounds(ica_out_dir, newpath=None):
202+
def _get_ica_confounds(ica_out_dir, n_volumes, newpath=None):
202203
if newpath is None:
203204
newpath = os.getcwd()
204205

@@ -210,20 +211,21 @@ def _get_ica_confounds(ica_out_dir, newpath=None):
210211
melodic_mix_out = os.path.join(newpath, 'MELODICmix.tsv')
211212
motion_ics_out = os.path.join(newpath, 'AROMAnoiseICs.csv')
212213

213-
# melodic_mix replace spaces with tabs
214-
with open(melodic_mix, 'r') as melodic_file:
215-
melodic_mix_out_char = melodic_file.read().replace(' ', '\t')
216-
# write to output file
217-
with open(melodic_mix_out, 'w+') as melodic_file_out:
218-
melodic_file_out.write(melodic_mix_out_char)
219-
220214
# copy metion_ics file to derivatives name
221215
shutil.copyfile(motion_ics, motion_ics_out)
222216

223217
# -1 since python lists start at index 0
224218
motion_ic_indices = np.loadtxt(motion_ics, dtype=int, delimiter=',', ndmin=1) - 1
225219
melodic_mix_arr = np.loadtxt(melodic_mix, ndmin=2)
226220

221+
# pad melodic_mix_arr with rows of zeros corresponding to number non steadystate volumes
222+
if n_volumes > 0:
223+
zeros = np.zeros([n_volumes, melodic_mix_arr.shape[1]])
224+
melodic_mix_arr = np.vstack([zeros, melodic_mix_arr])
225+
226+
# save melodic_mix_arr
227+
np.savetxt(melodic_mix_out, melodic_mix_arr, delimiter='\t')
228+
227229
# Return dummy list of ones if no noise compnents were found
228230
if motion_ic_indices.size == 0:
229231
LOGGER.warning('No noise components were classified')

fmriprep/workflows/bold/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from nipype.interfaces.fsl import Split as FSLSplit
1919
from nipype.pipeline import engine as pe
2020
from nipype.interfaces import utility as niu
21+
from nipype.algorithms import confounds as nac
2122

2223
from ...interfaces import (
2324
DerivativesDataSink,
@@ -424,6 +425,9 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
424425
omp_nthreads=omp_nthreads,
425426
use_compression=False)
426427

428+
# get non steady-state volumes
429+
non_steady_state = pe.Node(nac.NonSteadyStateDetector(), name='non_steady_state')
430+
427431
# get confounds
428432
bold_confounds_wf = init_bold_confs_wf(
429433
mem_gb=mem_gb['largemem'],
@@ -525,13 +529,18 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
525529
('outputnode.out_warp', 'inputnode.fieldwarp'),
526530
('outputnode.bold_mask', 'inputnode.bold_mask')]),
527531
(bold_sdc_wf, summary, [('outputnode.method', 'distortion_correction')]),
532+
# Calculate nonsteady state
533+
(bold_bold_trans_wf, non_steady_state, [
534+
('outputnode.bold', 'in_file')]),
528535
# Connect bold_confounds_wf
529536
(inputnode, bold_confounds_wf, [('t1_tpms', 'inputnode.t1_tpms'),
530537
('t1_mask', 'inputnode.t1_mask')]),
531538
(bold_hmc_wf, bold_confounds_wf, [
532539
('outputnode.movpar_file', 'inputnode.movpar_file')]),
533540
(bold_reg_wf, bold_confounds_wf, [
534541
('outputnode.itk_t1_to_bold', 'inputnode.t1_bold_xform')]),
542+
(non_steady_state, bold_confounds_wf, [
543+
('n_volumes_to_discard', 'inputnode.n_volumes_to_discard')]),
535544
(bold_confounds_wf, outputnode, [
536545
('outputnode.confounds_file', 'confounds'),
537546
]),
@@ -730,6 +739,8 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
730739
('outputnode.bold_mask', 'inputnode.bold_mask')]),
731740
(bold_sdc_wf, ica_aroma_wf, [
732741
('outputnode.out_warp', 'inputnode.fieldwarp')]),
742+
(non_steady_state, ica_aroma_wf, [
743+
('n_volumes_to_discard', 'inputnode.n_volumes_to_discard')]),
733744
(bold_confounds_wf, join, [
734745
('outputnode.confounds_file', 'in_file')]),
735746
(ica_aroma_wf, join,

fmriprep/workflows/bold/confounds.py

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ def init_bold_confs_wf(mem_gb, metadata, name="bold_confs_wf"):
9292
BOLD series mask
9393
movpar_file
9494
SPM-formatted motion parameters file
95+
n_volumes_to_discard
96+
number of non steady state volumes
9597
t1_mask
9698
Mask of the skull-stripped template image
9799
t1_tpms
@@ -136,8 +138,8 @@ def init_bold_confs_wf(mem_gb, metadata, name="bold_confs_wf"):
136138
placed within the corresponding confounds file.
137139
"""
138140
inputnode = pe.Node(niu.IdentityInterface(
139-
fields=['bold', 'bold_mask', 'movpar_file', 't1_mask', 't1_tpms',
140-
't1_bold_xform']),
141+
fields=['bold', 'bold_mask', 'movpar_file', 'n_volumes_to_discard',
142+
't1_mask', 't1_tpms', 't1_bold_xform']),
141143
name='inputnode')
142144
outputnode = pe.Node(niu.IdentityInterface(
143145
fields=['confounds_file']),
@@ -178,7 +180,6 @@ def init_bold_confs_wf(mem_gb, metadata, name="bold_confs_wf"):
178180
name="fdisp", mem_gb=mem_gb)
179181

180182
# a/t-CompCor
181-
non_steady_state = pe.Node(nac.NonSteadyStateDetector(), name='non_steady_state')
182183
tcompcor = pe.Node(TCompCor(
183184
components_file='tcompcor.tsv', pre_filter='cosine', save_pre_filter=True,
184185
percentile_threshold=.05), name="tcompcor", mem_gb=mem_gb)
@@ -250,18 +251,15 @@ def _pick_wm(files):
250251
('bold_mask', 'in_mask')]),
251252
(inputnode, fdisp, [('movpar_file', 'in_file')]),
252253

253-
# Calculate nonsteady state
254-
(inputnode, non_steady_state, [('bold', 'in_file')]),
255-
256254
# tCompCor
257255
(inputnode, tcompcor, [('bold', 'realigned_file')]),
258-
(non_steady_state, tcompcor, [('n_volumes_to_discard', 'ignore_initial_volumes')]),
256+
(inputnode, tcompcor, [('n_volumes_to_discard', 'ignore_initial_volumes')]),
259257
(tcc_tfm, tcc_msk, [('output_image', 'roi_file')]),
260258
(tcc_msk, tcompcor, [('out', 'mask_files')]),
261259

262260
# aCompCor
263261
(inputnode, acompcor, [('bold', 'realigned_file')]),
264-
(non_steady_state, acompcor, [('n_volumes_to_discard', 'ignore_initial_volumes')]),
262+
(inputnode, acompcor, [('n_volumes_to_discard', 'ignore_initial_volumes')]),
265263
(acc_tfm, acc_msk, [('output_image', 'roi_file')]),
266264
(acc_msk, acompcor, [('out', 'mask_files')]),
267265

@@ -494,6 +492,7 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
494492
'itk_bold_to_t1',
495493
't1_2_mni_forward_transform',
496494
'name_source',
495+
'n_volumes_to_discard',
497496
'bold_split',
498497
'bold_mask',
499498
'hmc_xforms',
@@ -516,6 +515,10 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
516515
)
517516
bold_mni_trans_wf.__desc__ = None
518517

518+
rm_non_steady_state = pe.Node(niu.Function(function=_remove_volumes,
519+
output_names=['bold_cut']),
520+
name='rm_nonsteady')
521+
519522
calc_median_val = pe.Node(fsl.ImageStats(op_string='-k %s -p 50'), name='calc_median_val')
520523
calc_bold_mean = pe.Node(fsl.MeanImage(), name='calc_bold_mean')
521524

@@ -539,6 +542,10 @@ def _getusans_func(image, thresh):
539542
denoise_type='nonaggr', generate_report=True, TR=metadata['RepetitionTime']),
540543
name='ica_aroma')
541544

545+
add_non_steady_state = pe.Node(niu.Function(function=_add_volumes,
546+
output_names=['bold_add']),
547+
name='add_nonsteady')
548+
542549
# extract the confound ICs from the results
543550
ica_aroma_confound_extraction = pe.Node(ICAConfounds(ignore_aroma_err=ignore_aroma_err),
544551
name='ica_aroma_confound_extraction')
@@ -562,16 +569,21 @@ def _getbtthresh(medianval):
562569
('t1_2_mni_forward_transform', 'inputnode.t1_2_mni_forward_transform'),
563570
('fieldwarp', 'inputnode.fieldwarp')]),
564571
(inputnode, ica_aroma, [('movpar_file', 'motion_parameters')]),
572+
(inputnode, rm_non_steady_state, [
573+
('n_volumes_to_discard', 'n_volumes')]),
574+
(bold_mni_trans_wf, rm_non_steady_state, [
575+
('outputnode.bold_mni', 'bold_file')]),
565576
(bold_mni_trans_wf, calc_median_val, [
566-
('outputnode.bold_mni', 'in_file'),
567577
('outputnode.bold_mask_mni', 'mask_file')]),
568-
(bold_mni_trans_wf, calc_bold_mean, [
569-
('outputnode.bold_mni', 'in_file')]),
578+
(rm_non_steady_state, calc_median_val, [
579+
('bold_cut', 'in_file')]),
580+
(rm_non_steady_state, calc_bold_mean, [
581+
('bold_cut', 'in_file')]),
570582
(calc_bold_mean, getusans, [('out_file', 'image')]),
571583
(calc_median_val, getusans, [('out_stat', 'thresh')]),
572584
# Connect input nodes to complete smoothing
573-
(bold_mni_trans_wf, smooth, [
574-
('outputnode.bold_mni', 'in_file')]),
585+
(rm_non_steady_state, smooth, [
586+
('bold_cut', 'in_file')]),
575587
(getusans, smooth, [('usans', 'usans')]),
576588
(calc_median_val, smooth, [(('out_stat', _getbtthresh), 'brightness_threshold')]),
577589
# connect smooth to melodic
@@ -585,17 +597,65 @@ def _getbtthresh(medianval):
585597
(melodic, ica_aroma, [('out_dir', 'melodic_dir')]),
586598
# generate tsvs from ICA-AROMA
587599
(ica_aroma, ica_aroma_confound_extraction, [('out_dir', 'in_directory')]),
600+
(inputnode, ica_aroma_confound_extraction, [
601+
('n_volumes_to_discard', 'n_volumes')]),
588602
# output for processing and reporting
589603
(ica_aroma_confound_extraction, outputnode, [('aroma_confounds', 'aroma_confounds'),
590604
('aroma_noise_ics', 'aroma_noise_ics'),
591605
('melodic_mix', 'melodic_mix')]),
592606
# TODO change melodic report to reflect noise and non-noise components
593-
(ica_aroma, outputnode, [('nonaggr_denoised_file', 'nonaggr_denoised_file')]),
607+
(ica_aroma, add_non_steady_state, [
608+
('nonaggr_denoised_file', 'bold_cut_file')]),
609+
(bold_mni_trans_wf, add_non_steady_state, [
610+
('outputnode.bold_mni', 'bold_file')]),
611+
(inputnode, add_non_steady_state, [
612+
('n_volumes_to_discard', 'n_volumes')]),
613+
(add_non_steady_state, outputnode, [('bold_add', 'nonaggr_denoised_file')]),
594614
(ica_aroma, ds_report_ica_aroma, [('out_report', 'in_file')]),
595615
])
596616

597617
return workflow
598618

619+
def _remove_volumes(bold_file, n_volumes):
620+
import nibabel as nb
621+
from nipype.utils.filemanip import fname_presuffix
622+
623+
# load the bold file and get the 4d matrix
624+
bold_img = nb.load(bold_file)
625+
bold_data = bold_img.get_data()
626+
627+
# cut off the beginning volumes
628+
bold_data_cut = bold_data[..., n_volumes:]
629+
630+
# modify header with new shape (fewer volumes)
631+
data_shape = list(bold_img.header.get_data_shape())
632+
data_shape[-1] -= n_volumes
633+
bold_img.header.set_data_shape(tuple(data_shape))
634+
635+
# save the resulting bold file
636+
out = fname_presuffix(bold_file, suffix='_cut')
637+
bold_img.__class__(bold_data_cut, bold_img.affine, bold_img.header).to_filename(out)
638+
return out
639+
640+
641+
def _add_volumes(bold_file, bold_cut_file, n_volumes):
642+
"""prepend n_volumes from bold_file onto bold_cut_file"""
643+
import nibabel as nb
644+
from nipype.utils.filemanip import fname_presuffix
645+
646+
# load the data
647+
bold_img = nb.load(bold_file)
648+
bold_data = bold_img.get_data()
649+
bold_cut_img = nb.load(bold_cut_file)
650+
bold_cut_data = bold_cut_img.get_data()
651+
652+
# assign everything from n_volumes foward to bold_cut_data
653+
bold_data[..., n_volumes:] = bold_cut_data
654+
655+
out = fname_presuffix(bold_cut_file, suffix='_addnonsteady')
656+
bold_img.__class__(bold_data, bold_img.affine, bold_img.header).to_filename(out)
657+
return out
658+
599659

600660
def _maskroi(in_mask, roi_file):
601661
import numpy as np

0 commit comments

Comments
 (0)