Skip to content

Commit

Permalink
Merge pull request #1708 from rciric/confmeta
Browse files Browse the repository at this point in the history
ENH: Confounds metadata
  • Loading branch information
oesteban committed Sep 24, 2019
2 parents 243b227 + b39bd20 commit c7c428c
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 10 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ RUN mkdir -p /opt/ICA-AROMA && \
curl -sSL "https://github.com/maartenmennes/ICA-AROMA/archive/v0.4.4-beta.tar.gz" \
| tar -xzC /opt/ICA-AROMA --strip-components 1 && \
chmod +x /opt/ICA-AROMA/ICA_AROMA.py
ENV PATH=/opt/ICA-AROMA:$PATH
ENV PATH="/opt/ICA-AROMA:$PATH" \
AROMA_VERSION="0.4.4-beta"

# Installing and setting up miniconda
RUN curl -sSLO https://repo.continuum.io/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh && \
Expand Down
27 changes: 22 additions & 5 deletions fmriprep/interfaces/confounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class ICAConfoundsOutputSpec(TraitedSpec):
File(exists=True, desc='output confounds file extracted from ICA-AROMA'))
aroma_noise_ics = File(exists=True, desc='ICA-AROMA noise components')
melodic_mix = File(exists=True, desc='melodic mix file')
aroma_metadata = File(exists=True, desc='tabulated ICA-AROMA metadata')


class ICAConfounds(SimpleInterface):
Expand All @@ -116,8 +117,12 @@ class ICAConfounds(SimpleInterface):
output_spec = ICAConfoundsOutputSpec

def _run_interface(self, runtime):
aroma_confounds, motion_ics_out, melodic_mix_out = _get_ica_confounds(
self.inputs.in_directory, self.inputs.skip_vols, newpath=runtime.cwd)
(aroma_confounds,
motion_ics_out,
melodic_mix_out,
aroma_metadata) = _get_ica_confounds(self.inputs.in_directory,
self.inputs.skip_vols,
newpath=runtime.cwd)

if self.inputs.err_on_aroma_warn and aroma_confounds is None:
raise RuntimeError('ICA-AROMA failed')
Expand All @@ -126,6 +131,7 @@ def _run_interface(self, runtime):

self._results['aroma_noise_ics'] = motion_ics_out
self._results['melodic_mix'] = melodic_mix_out
self._results['aroma_metadata'] = aroma_metadata
return runtime


Expand Down Expand Up @@ -218,10 +224,12 @@ def _get_ica_confounds(ica_out_dir, skip_vols, newpath=None):
# load the txt files from ICA-AROMA
melodic_mix = os.path.join(ica_out_dir, 'melodic.ica/melodic_mix')
motion_ics = os.path.join(ica_out_dir, 'classified_motion_ICs.txt')
aroma_metadata = os.path.join(ica_out_dir, 'classification_overview.txt')

# Change names of motion_ics and melodic_mix for output
melodic_mix_out = os.path.join(newpath, 'MELODICmix.tsv')
motion_ics_out = os.path.join(newpath, 'AROMAnoiseICs.csv')
aroma_metadata_out = os.path.join(newpath, 'classification_overview.tsv')

# copy metion_ics file to derivatives name
shutil.copyfile(motion_ics, motion_ics_out)
Expand All @@ -238,18 +246,27 @@ def _get_ica_confounds(ica_out_dir, skip_vols, newpath=None):
# save melodic_mix_arr
np.savetxt(melodic_mix_out, melodic_mix_arr, delimiter='\t')

# process the metadata so that the IC column entries match the BIDS name of
# the regressor
aroma_metadata = pd.read_csv(aroma_metadata, sep='\t')
aroma_metadata['IC'] = [
'aroma_motion_{}'.format(name) for name in aroma_metadata['IC']]
aroma_metadata.columns = [
re.sub('[ |\-|\/]', '_', c) for c in aroma_metadata.columns]
aroma_metadata.to_csv(aroma_metadata_out, sep='\t', index=False)

# Return dummy list of ones if no noise compnents were found
if motion_ic_indices.size == 0:
LOGGER.warning('No noise components were classified')
return None, motion_ics_out, melodic_mix_out
return None, motion_ics_out, melodic_mix_out, aroma_metadata_out

# the "good" ics, (e.g., not motion related)
good_ic_arr = np.delete(melodic_mix_arr, motion_ic_indices, 1).T

# return dummy lists of zeros if no signal components were found
if good_ic_arr.size == 0:
LOGGER.warning('No signal components were classified')
return None, motion_ics_out, melodic_mix_out
return None, motion_ics_out, melodic_mix_out, aroma_metadata_out

# transpose melodic_mix_arr so x refers to the correct dimension
aggr_confounds = np.asarray([melodic_mix_arr.T[x] for x in motion_ic_indices])
Expand All @@ -260,7 +277,7 @@ def _get_ica_confounds(ica_out_dir, skip_vols, newpath=None):
columns=['aroma_motion_%02d' % (x + 1) for x in motion_ic_indices]).to_csv(
aroma_confounds, sep="\t", index=None)

return aroma_confounds, motion_ics_out, melodic_mix_out
return aroma_confounds, motion_ics_out, melodic_mix_out, aroma_metadata_out


class FMRISummaryInputSpec(BaseInterfaceInputSpec):
Expand Down
14 changes: 14 additions & 0 deletions fmriprep/workflows/bold/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from niworkflows.engine.workflows import LiterateWorkflow as Workflow
from niworkflows.interfaces.cifti import GenerateCifti
from niworkflows.interfaces.utils import DictMerge

from ...utils.meepi import combine_meepi_source

Expand Down Expand Up @@ -875,10 +876,17 @@ def init_func_preproc_wf(
function=_to_join),
name='aroma_confounds')

mrg_conf_metadata = pe.Node(niu.Merge(2), name='merge_confound_metadata',
run_without_submitting=True)
mrg_conf_metadata2 = pe.Node(DictMerge(), name='merge_confound_metadata2',
run_without_submitting=True)
workflow.disconnect([
(bold_confounds_wf, outputnode, [
('outputnode.confounds_file', 'confounds'),
]),
(bold_confounds_wf, outputnode, [
('outputnode.confounds_metadata', 'confounds_metadata'),
]),
])
workflow.connect([
(bold_std_trans_wf, ica_aroma_wf, [
Expand All @@ -893,13 +901,19 @@ def init_func_preproc_wf(
('outputnode.skip_vols', 'inputnode.skip_vols')]),
(bold_confounds_wf, join, [
('outputnode.confounds_file', 'in_file')]),
(bold_confounds_wf, mrg_conf_metadata,
[('outputnode.confounds_metadata', 'in1')]),
(ica_aroma_wf, join,
[('outputnode.aroma_confounds', 'join_file')]),
(ica_aroma_wf, mrg_conf_metadata,
[('outputnode.aroma_metadata', 'in2')]),
(mrg_conf_metadata, mrg_conf_metadata2, [('out', 'in_dicts')]),
(ica_aroma_wf, outputnode,
[('outputnode.aroma_noise_ics', 'aroma_noise_ics'),
('outputnode.melodic_mix', 'melodic_mix'),
('outputnode.nonaggr_denoised_file', 'nonaggr_denoised_file')]),
(join, outputnode, [('out_file', 'confounds')]),
(mrg_conf_metadata2, outputnode, [('out_dict', 'confounds_metadata')]),
])

# SURFACES ##################################################################################
Expand Down
20 changes: 17 additions & 3 deletions fmriprep/workflows/bold/confounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.. autofunction:: init_ica_aroma_wf
"""
from os import getenv
from nipype.pipeline import engine as pe
from nipype.interfaces import utility as niu, fsl
from nipype.algorithms import confounds as nac
Expand Down Expand Up @@ -245,8 +246,9 @@ def init_bold_confs_wf(
acompcor.inputs.repetition_time = metadata['RepetitionTime']

# Global and segment regressors
signals_class_labels = ["csf", "white_matter", "global_signal"]
mrg_lbl = pe.Node(niu.Merge(3), name='merge_rois', run_without_submitting=True)
signals = pe.Node(SignalExtraction(class_labels=["csf", "white_matter", "global_signal"]),
signals = pe.Node(SignalExtraction(class_labels=signals_class_labels),
name="signals", mem_gb=mem_gb)

# Arrange confounds
Expand All @@ -270,8 +272,10 @@ def init_bold_confs_wf(
TSV2JSON(index_column='component', output=None,
additional_metadata={'Method': 'aCompCor'}, enforce_case=True),
name='acc_metadata_fmt')
mrg_conf_metadata = pe.Node(niu.Merge(2), name='merge_confound_metadata',
mrg_conf_metadata = pe.Node(niu.Merge(3), name='merge_confound_metadata',
run_without_submitting=True)
mrg_conf_metadata.inputs.in3 = {label: {'Method': 'Mean'}
for label in signals_class_labels}
mrg_conf_metadata2 = pe.Node(DictMerge(), name='merge_confound_metadata2',
run_without_submitting=True)

Expand Down Expand Up @@ -648,7 +652,7 @@ def init_ica_aroma_wf(metadata, mem_gb, omp_nthreads,

outputnode = pe.Node(niu.IdentityInterface(
fields=['aroma_confounds', 'aroma_noise_ics', 'melodic_mix',
'nonaggr_denoised_file']), name='outputnode')
'nonaggr_denoised_file', 'aroma_metadata']), name='outputnode')

select_std = pe.Node(KeySelect(
fields=['bold_mask_std', 'bold_std']),
Expand Down Expand Up @@ -687,6 +691,13 @@ def _getusans_func(image, thresh):
ica_aroma_confound_extraction = pe.Node(ICAConfounds(err_on_aroma_warn=err_on_aroma_warn),
name='ica_aroma_confound_extraction')

ica_aroma_metadata_fmt = pe.Node(
TSV2JSON(index_column='IC', output=None, enforce_case=True,
additional_metadata={'Method': {
'Name': 'ICA-AROMA',
'Version': getenv('AROMA_VERSION', 'n/a')}}),
name='ica_aroma_metadata_fmt')

ds_report_ica_aroma = pe.Node(
DerivativesDataSink(desc='aroma', keep_dtype=True),
name='ds_report_ica_aroma', run_without_submitting=True,
Expand Down Expand Up @@ -732,10 +743,13 @@ def _getbtthresh(medianval):
(ica_aroma, ica_aroma_confound_extraction, [('out_dir', 'in_directory')]),
(inputnode, ica_aroma_confound_extraction, [
('skip_vols', 'skip_vols')]),
(ica_aroma_confound_extraction, ica_aroma_metadata_fmt, [
('aroma_metadata', 'in_file')]),
# output for processing and reporting
(ica_aroma_confound_extraction, outputnode, [('aroma_confounds', 'aroma_confounds'),
('aroma_noise_ics', 'aroma_noise_ics'),
('melodic_mix', 'melodic_mix')]),
(ica_aroma_metadata_fmt, outputnode, [('output', 'aroma_metadata')]),
(ica_aroma, add_non_steady_state, [
('nonaggr_denoised_file', 'bold_cut_file')]),
(select_std, add_non_steady_state, [
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ classifiers =
[options]
python_requires = >=3.5
install_requires =
niworkflows ~= 0.10.3
niworkflows @ git+https://github.com/poldracklab/niworkflows.git@9226e01cf5a7f4c8691870a3f98249c481986e7c
smriprep ~= 0.3.2
templateflow ~= 0.4.1
nibabel >=2.2.1
Expand Down

0 comments on commit c7c428c

Please sign in to comment.