Skip to content

Commit aa56aaa

Browse files
authored
Merge pull request #1335 from jdkent/remove_init_vol_aroma
[FIX] Remove non-steady-state volumes prior to ICA-AROMA
2 parents 8f73452 + a44b9b4 commit aa56aaa

File tree

4 files changed

+111
-31
lines changed

4 files changed

+111
-31
lines changed

docs/workflows.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,10 @@ be generated, so non-aggressive denoising can be manually performed in the T1w s
588588
-d sub-<subject_label>_task-<task_id>_bold_MELODICmix.tsv \
589589
-o sub-<subject_label>_task-<task_id>_bold_space-<space>_AromaNonAggressiveDenoised.nii.gz
590590

591+
*Note*: The non-steady state volumes are removed for the determination of components in melodic.
592+
Therefore ``*MELODICmix.tsv`` may have zero padded rows to account for the volumes not used in
593+
melodic's estimation of components.
594+
591595
A visualization of the AROMA component classification is also included in the HTML reports.
592596

593597
.. figure:: _static/aroma.svg

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+
skip_vols = 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.skip_vols, 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, skip_vols, 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 skip_vols > 0:
223+
zeros = np.zeros([skip_vols, 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
531531
('outputnode.movpar_file', 'inputnode.movpar_file')]),
532532
(bold_reg_wf, bold_confounds_wf, [
533533
('outputnode.itk_t1_to_bold', 'inputnode.t1_bold_xform')]),
534+
(bold_reference_wf, bold_confounds_wf, [
535+
('outputnode.skip_vols', 'inputnode.skip_vols')]),
534536
(bold_confounds_wf, outputnode, [
535537
('outputnode.confounds_file', 'confounds'),
536538
]),
@@ -729,6 +731,8 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
729731
('outputnode.bold_mask', 'inputnode.bold_mask')]),
730732
(bold_sdc_wf, ica_aroma_wf, [
731733
('outputnode.out_warp', 'inputnode.fieldwarp')]),
734+
(bold_reference_wf, ica_aroma_wf, [
735+
('outputnode.skip_vols', 'inputnode.skip_vols')]),
732736
(bold_confounds_wf, join, [
733737
('outputnode.confounds_file', 'in_file')]),
734738
(ica_aroma_wf, join,

fmriprep/workflows/bold/confounds.py

Lines changed: 92 additions & 22 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+
skip_vols
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', 'skip_vols',
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, [('skip_vols', '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, [('skip_vols', 'ignore_initial_volumes')]),
265263
(acc_tfm, acc_msk, [('output_image', 'roi_file')]),
266264
(acc_msk, acompcor, [('out', 'mask_files')]),
267265

@@ -399,6 +397,7 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
399397
400398
The following steps are performed:
401399
400+
#. Remove non-steady state volumes from the bold series.
402401
#. Smooth data using FSL `susan`, with a kernel width FWHM=6.0mm.
403402
#. Run FSL `melodic` outside of ICA-AROMA to generate the report
404403
#. Run ICA-AROMA
@@ -453,15 +452,27 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
453452
Set the dimensionality of the Melodic ICA decomposition
454453
If None, MELODIC automatically estimates dimensionality.
455454
456-
457455
**Inputs**
458456
459-
bold_mni
460-
BOLD series, resampled to template space
457+
itk_bold_to_t1
458+
Affine transform from ``ref_bold_brain`` to T1 space (ITK format)
459+
t1_2_mni_forward_transform
460+
ANTs-compatible affine-and-warp transform file
461+
name_source
462+
BOLD series NIfTI file
463+
Used to recover original information lost during processing
464+
skip_vols
465+
number of non steady state volumes
466+
bold_split
467+
Individual 3D BOLD volumes, not motion corrected
468+
bold_mask
469+
BOLD series mask in template space
470+
hmc_xforms
471+
List of affine transforms aligning each volume to ``ref_image`` in ITK format
472+
fieldwarp
473+
a :abbr:`DFM (displacements field map)` in ITK format
461474
movpar_file
462475
SPM-formatted motion parameters file
463-
bold_mask_mni
464-
BOLD series mask in template space
465476
466477
**Outputs**
467478
@@ -474,15 +485,15 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
474485
nonaggr_denoised_file
475486
BOLD series with non-aggressive ICA-AROMA denoising applied
476487
477-
.. _ICA-AROMA: https://github.com/rhr-pruim/ICA-AROMA
488+
.. _ICA-AROMA: https://github.com/maartenmennes/ICA-AROMA
478489
479490
"""
480491
workflow = Workflow(name=name)
481492
workflow.__postdesc__ = """\
482493
Automatic removal of motion artifacts using independent component analysis
483494
[ICA-AROMA, @aroma] was performed on the *preprocessed BOLD on MNI space*
484-
time-series after a spatial smoothing with an isotropic, Gaussian kernel
485-
of 6mm FWHM (full-width half-maximum).
495+
time-series after removal of non-steady state volumes and spatial smoothing
496+
with an isotropic, Gaussian kernel of 6mm FWHM (full-width half-maximum).
486497
Corresponding "non-aggresively" denoised runs were produced after such
487498
smoothing.
488499
Additionally, the "aggressive" noise-regressors were collected and placed
@@ -494,6 +505,7 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
494505
'itk_bold_to_t1',
495506
't1_2_mni_forward_transform',
496507
'name_source',
508+
'skip_vols',
497509
'bold_split',
498510
'bold_mask',
499511
'hmc_xforms',
@@ -516,6 +528,10 @@ def init_ica_aroma_wf(template, metadata, mem_gb, omp_nthreads,
516528
)
517529
bold_mni_trans_wf.__desc__ = None
518530

531+
rm_non_steady_state = pe.Node(niu.Function(function=_remove_volumes,
532+
output_names=['bold_cut']),
533+
name='rm_nonsteady')
534+
519535
calc_median_val = pe.Node(fsl.ImageStats(op_string='-k %s -p 50'), name='calc_median_val')
520536
calc_bold_mean = pe.Node(fsl.MeanImage(), name='calc_bold_mean')
521537

@@ -539,6 +555,10 @@ def _getusans_func(image, thresh):
539555
denoise_type='nonaggr', generate_report=True, TR=metadata['RepetitionTime']),
540556
name='ica_aroma')
541557

558+
add_non_steady_state = pe.Node(niu.Function(function=_add_volumes,
559+
output_names=['bold_add']),
560+
name='add_nonsteady')
561+
542562
# extract the confound ICs from the results
543563
ica_aroma_confound_extraction = pe.Node(ICAConfounds(ignore_aroma_err=ignore_aroma_err),
544564
name='ica_aroma_confound_extraction')
@@ -562,16 +582,21 @@ def _getbtthresh(medianval):
562582
('t1_2_mni_forward_transform', 'inputnode.t1_2_mni_forward_transform'),
563583
('fieldwarp', 'inputnode.fieldwarp')]),
564584
(inputnode, ica_aroma, [('movpar_file', 'motion_parameters')]),
585+
(inputnode, rm_non_steady_state, [
586+
('skip_vols', 'skip_vols')]),
587+
(bold_mni_trans_wf, rm_non_steady_state, [
588+
('outputnode.bold_mni', 'bold_file')]),
565589
(bold_mni_trans_wf, calc_median_val, [
566-
('outputnode.bold_mni', 'in_file'),
567590
('outputnode.bold_mask_mni', 'mask_file')]),
568-
(bold_mni_trans_wf, calc_bold_mean, [
569-
('outputnode.bold_mni', 'in_file')]),
591+
(rm_non_steady_state, calc_median_val, [
592+
('bold_cut', 'in_file')]),
593+
(rm_non_steady_state, calc_bold_mean, [
594+
('bold_cut', 'in_file')]),
570595
(calc_bold_mean, getusans, [('out_file', 'image')]),
571596
(calc_median_val, getusans, [('out_stat', 'thresh')]),
572597
# Connect input nodes to complete smoothing
573-
(bold_mni_trans_wf, smooth, [
574-
('outputnode.bold_mni', 'in_file')]),
598+
(rm_non_steady_state, smooth, [
599+
('bold_cut', 'in_file')]),
575600
(getusans, smooth, [('usans', 'usans')]),
576601
(calc_median_val, smooth, [(('out_stat', _getbtthresh), 'brightness_threshold')]),
577602
# connect smooth to melodic
@@ -586,18 +611,63 @@ def _getbtthresh(medianval):
586611
(melodic, ica_aroma, [('out_dir', 'melodic_dir')]),
587612
# generate tsvs from ICA-AROMA
588613
(ica_aroma, ica_aroma_confound_extraction, [('out_dir', 'in_directory')]),
614+
(inputnode, ica_aroma_confound_extraction, [
615+
('skip_vols', 'skip_vols')]),
589616
# output for processing and reporting
590617
(ica_aroma_confound_extraction, outputnode, [('aroma_confounds', 'aroma_confounds'),
591618
('aroma_noise_ics', 'aroma_noise_ics'),
592619
('melodic_mix', 'melodic_mix')]),
593620
# TODO change melodic report to reflect noise and non-noise components
594-
(ica_aroma, outputnode, [('nonaggr_denoised_file', 'nonaggr_denoised_file')]),
621+
(ica_aroma, add_non_steady_state, [
622+
('nonaggr_denoised_file', 'bold_cut_file')]),
623+
(bold_mni_trans_wf, add_non_steady_state, [
624+
('outputnode.bold_mni', 'bold_file')]),
625+
(inputnode, add_non_steady_state, [
626+
('skip_vols', 'skip_vols')]),
627+
(add_non_steady_state, outputnode, [('bold_add', 'nonaggr_denoised_file')]),
595628
(ica_aroma, ds_report_ica_aroma, [('out_report', 'in_file')]),
596629
])
597630

598631
return workflow
599632

600633

634+
def _remove_volumes(bold_file, skip_vols):
635+
"""remove skip_vols from bold_file"""
636+
import nibabel as nb
637+
from nipype.utils.filemanip import fname_presuffix
638+
639+
if skip_vols == 0:
640+
return bold_file
641+
642+
out = fname_presuffix(bold_file, suffix='_cut')
643+
bold_img = nb.load(bold_file)
644+
bold_img.__class__(bold_img.dataobj[..., skip_vols:],
645+
bold_img.affine, bold_img.header).to_filename(out)
646+
647+
return out
648+
649+
650+
def _add_volumes(bold_file, bold_cut_file, skip_vols):
651+
"""prepend skip_vols from bold_file onto bold_cut_file"""
652+
import nibabel as nb
653+
import numpy as np
654+
from nipype.utils.filemanip import fname_presuffix
655+
656+
if skip_vols == 0:
657+
return bold_cut_file
658+
659+
bold_img = nb.load(bold_file)
660+
bold_cut_img = nb.load(bold_cut_file)
661+
662+
bold_data = np.concatenate((bold_img.dataobj[..., :skip_vols],
663+
bold_cut_img.dataobj), axis=3)
664+
665+
out = fname_presuffix(bold_cut_file, suffix='_addnonsteady')
666+
bold_img.__class__(bold_data, bold_img.affine, bold_img.header).to_filename(out)
667+
668+
return out
669+
670+
601671
def _maskroi(in_mask, roi_file):
602672
import numpy as np
603673
import nibabel as nb

0 commit comments

Comments
 (0)