From 89f1704ec15da31115124ba8d55c794822959d8a Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 30 Sep 2019 16:47:49 -0400 Subject: [PATCH 01/19] Allow bold_reference_wf to accept lists of EPI/SBRef files. --- niworkflows/func/util.py | 12 ++++++-- niworkflows/interfaces/registration.py | 42 ++++++++++++++++++++++---- niworkflows/utils/misc.py | 13 ++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index c2c4496268b..5f8f6aa8466 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -20,7 +20,7 @@ from ..interfaces.masks import SimpleShowMaskRPT from ..interfaces.registration import EstimateReferenceImage from ..interfaces.utils import CopyXForm -from ..utils.misc import pass_dummy_scans as _pass_dummy_scans +from ..utils.misc import select_first, pass_dummy_scans as _pass_dummy_scans DEFAULT_MEMORY_MIN_GB = 0.01 @@ -125,6 +125,7 @@ def init_bold_reference_wf( "ref_image_brain", "bold_mask", "validation_report", + "mask_report", ] ), name="outputnode", @@ -134,7 +135,12 @@ def init_bold_reference_wf( if bold_file is not None: inputnode.inputs.bold_file = bold_file - validate = pe.Node(ValidateImage(), name="validate", mem_gb=DEFAULT_MEMORY_MIN_GB) + validate = pe.MapNode( + ValidateImage(), + name="validate", + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=["in_file"], + ) gen_ref = pe.Node( EstimateReferenceImage(), name="gen_ref", mem_gb=1 @@ -165,7 +171,7 @@ def init_bold_reference_wf( ("ref_image", "inputnode.in_file"), ]), (validate, outputnode, [ - ("out_file", "bold_file"), + (("out_file", select_first), "bold_file"), ("out_report", "validation_report"), ]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 60a072c6757..6f58e9fb0d0 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -16,6 +16,7 @@ BaseInterfaceInputSpec, File, SimpleInterface, + InputMultiPath, ) from nipype.interfaces.mixins import reporting from nipype.interfaces import freesurfer as fs @@ -398,8 +399,27 @@ def _post_run_hook(self, runtime): class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc="4D EPI file") - sbref_file = File(exists=True, desc="Single band reference image") + in_file = traits.Either( + File(exists=True), + InputMultiPath(File(exists=True)), + mandatory=True, + desc=( + "4D EPI file. If multiple files " + "are provided, they are assumed " + "to represent multiple echoes " + "from the same run." + ), + ) + sbref_file = traits.Either( + File(exists=True), + InputMultiPath(File(exists=True)), + desc=( + "Single band reference image. " + "If multiple files are provided, " + "they are assumed to represent " + "multiple echoes." + ), + ) mc_method = traits.Enum( "AFNI", "FSL", @@ -415,6 +435,9 @@ class _EstimateReferenceImageOutputSpec(TraitedSpec): "state volumes in the beginning of " "the input file" ) + description = traits.Str( + desc="Description of reference image " "identification steps taken." + ) class EstimateReferenceImage(SimpleInterface): @@ -429,7 +452,11 @@ class EstimateReferenceImage(SimpleInterface): output_spec = _EstimateReferenceImageOutputSpec def _run_interface(self, runtime): - ref_name = self.inputs.in_file + # Select first EPI file + if not isinstance(self.inputs.in_file, File): + ref_name = self.inputs.in_file[0] + else: + ref_name = self.inputs.in_file ref_nii = nb.load(ref_name) n_volumes_to_discard = _get_vols_to_discard(ref_nii) @@ -437,9 +464,13 @@ def _run_interface(self, runtime): out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") if isdefined(self.inputs.sbref_file): - out_ref_fname = os.path.join(runtime.cwd, "ref_sbref.nii.gz") - ref_name = self.inputs.sbref_file + # Select first SBRef file + if not isinstance(self.inputs.sbref_file, File): + ref_name = self.inputs.sbref_file[0] + else: + ref_name = self.inputs.sbref_file ref_nii = nb.squeeze_image(nb.load(ref_name)) + out_ref_fname = os.path.join(runtime.cwd, "ref_sbref.nii.gz") # If reference is only 1 volume, return it directly if len(ref_nii.shape) == 3: @@ -488,7 +519,6 @@ def _run_interface(self, runtime): ) self._results["ref_image"] = out_ref_fname - return runtime diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py index 9cfbdc06175..1274e587d76 100644 --- a/niworkflows/utils/misc.py +++ b/niworkflows/utils/misc.py @@ -212,6 +212,19 @@ def splitext(fname): return stem, basename[len(stem):] +def select_first(in_files): + """ + Select the first file from a list of filenames. + Used to grab the first echo's file when processing + multi-echo data through workflows that only accept + a single file. + """ + if isinstance(in_files, list): + return in_files[0] + else: + return in_files + + def _copy_any(src, dst): import os import gzip From a6adb3cf0d28fa357a1953d6859da4421ccbb088 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 1 Oct 2019 08:01:51 -0400 Subject: [PATCH 02/19] Remove output from failed attempt to track description. --- niworkflows/interfaces/registration.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 6f58e9fb0d0..bbe03deb4dc 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -435,9 +435,6 @@ class _EstimateReferenceImageOutputSpec(TraitedSpec): "state volumes in the beginning of " "the input file" ) - description = traits.Str( - desc="Description of reference image " "identification steps taken." - ) class EstimateReferenceImage(SimpleInterface): From fd1eb59fceb32392cbefd83a0c782924954650f6 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 2 Oct 2019 08:40:34 -0400 Subject: [PATCH 03/19] Add doctest for select_first. --- niworkflows/utils/misc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py index 1274e587d76..9b2b0c9ba86 100644 --- a/niworkflows/utils/misc.py +++ b/niworkflows/utils/misc.py @@ -218,6 +218,12 @@ def select_first(in_files): Used to grab the first echo's file when processing multi-echo data through workflows that only accept a single file. + + >>> select_first('some/file.nii.gz') + 'some/file.nii.gz' + >>> select_first(['some/file1.nii.gz', 'some/file2.nii.gz']) + 'some/file1.nii.gz' + """ if isinstance(in_files, list): return in_files[0] From 74010b733746664f67d508b60daab93e76a960d1 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 2 Oct 2019 08:41:20 -0400 Subject: [PATCH 04/19] Replace traits.Either with InputMultiPath. --- niworkflows/interfaces/registration.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index bbe03deb4dc..13be4189750 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -399,9 +399,8 @@ def _post_run_hook(self, runtime): class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): - in_file = traits.Either( + in_file = InputMultiPath( File(exists=True), - InputMultiPath(File(exists=True)), mandatory=True, desc=( "4D EPI file. If multiple files " @@ -410,9 +409,8 @@ class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): "from the same run." ), ) - sbref_file = traits.Either( + sbref_file = InputMultiPath( File(exists=True), - InputMultiPath(File(exists=True)), desc=( "Single band reference image. " "If multiple files are provided, " @@ -450,10 +448,7 @@ class EstimateReferenceImage(SimpleInterface): def _run_interface(self, runtime): # Select first EPI file - if not isinstance(self.inputs.in_file, File): - ref_name = self.inputs.in_file[0] - else: - ref_name = self.inputs.in_file + ref_name = self.inputs.in_file[0] ref_nii = nb.load(ref_name) n_volumes_to_discard = _get_vols_to_discard(ref_nii) @@ -462,10 +457,7 @@ def _run_interface(self, runtime): out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") if isdefined(self.inputs.sbref_file): # Select first SBRef file - if not isinstance(self.inputs.sbref_file, File): - ref_name = self.inputs.sbref_file[0] - else: - ref_name = self.inputs.sbref_file + ref_name = self.inputs.sbref_file[0] ref_nii = nb.squeeze_image(nb.load(ref_name)) out_ref_fname = os.path.join(runtime.cwd, "ref_sbref.nii.gz") From b429b9d857a2a2ac8ef1df7975ec8e76ff956eab Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 2 Oct 2019 09:22:34 -0400 Subject: [PATCH 05/19] Use ensure_list for backwards compatibility. --- niworkflows/func/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index 5f8f6aa8466..8049e15e269 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -6,6 +6,7 @@ from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu, fsl, afni +from nipype.utils.filemanip import ensure_list from templateflow.api import get as get_template @@ -163,8 +164,8 @@ def init_bold_reference_wf( (inputnode, enhance_and_skullstrip_bold_wf, [ ("bold_mask", "inputnode.pre_mask"), ]), - (inputnode, validate, [("bold_file", "in_file")]), - (inputnode, gen_ref, [("sbref_file", "sbref_file")]), + (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), + (inputnode, gen_ref, [(("sbref_file", ensure_list), "sbref_file")]), (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), (validate, gen_ref, [("out_file", "in_file")]), (gen_ref, enhance_and_skullstrip_bold_wf, [ From 21c753ab3648d2817a80cb844da2267f62f29493 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 2 Oct 2019 09:29:16 -0400 Subject: [PATCH 06/19] Update niworkflows/func/util.py Co-Authored-By: Chris Markiewicz --- niworkflows/func/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index 8049e15e269..42aaa7f6e25 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -165,7 +165,7 @@ def init_bold_reference_wf( ("bold_mask", "inputnode.pre_mask"), ]), (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), - (inputnode, gen_ref, [(("sbref_file", ensure_list), "sbref_file")]), + (inputnode, gen_ref, [("sbref_file", "sbref_file")]), (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), (validate, gen_ref, [("out_file", "in_file")]), (gen_ref, enhance_and_skullstrip_bold_wf, [ From 61e7f24e449bc4b57d3654600f63836e2ff413ad Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 2 Oct 2019 09:29:29 -0400 Subject: [PATCH 07/19] Update niworkflows/interfaces/registration.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/registration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 13be4189750..6552c4c1542 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -16,7 +16,7 @@ BaseInterfaceInputSpec, File, SimpleInterface, - InputMultiPath, + InputMultiObject, ) from nipype.interfaces.mixins import reporting from nipype.interfaces import freesurfer as fs @@ -399,7 +399,7 @@ def _post_run_hook(self, runtime): class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): - in_file = InputMultiPath( + in_file = InputMultiObject( File(exists=True), mandatory=True, desc=( @@ -409,7 +409,7 @@ class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): "from the same run." ), ) - sbref_file = InputMultiPath( + sbref_file = InputMultiObject( File(exists=True), desc=( "Single band reference image. " From aaa565fc73fddd6ac81ec5a4133897a178427a40 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 7 Oct 2019 08:42:52 -0400 Subject: [PATCH 08/19] Add multiecho argument to init_bold_reference_wf. Also updates the workflow description based on whether multiecho is True or False. --- niworkflows/func/util.py | 57 ++++++++++++++++++-------- niworkflows/interfaces/registration.py | 8 ++++ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index 42aaa7f6e25..4f8b18a84a2 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -32,6 +32,7 @@ def init_bold_reference_wf( bold_file=None, brainmask_thresh=0.85, pre_mask=False, + multiecho=False, name="bold_reference_wf", gen_report=False, ): @@ -52,19 +53,22 @@ def init_bold_reference_wf( Parameters ---------- - omp_nthreads : int + omp_nthreads : :obj:`int` Maximum number of threads an individual process may use - bold_file : str + bold_file : :obj:`str` BOLD series NIfTI file brainmask_thresh: :obj:`float` Lower threshold for the probabilistic brainmask to obtain the final binary mask (default: 0.85). - pre_mask : bool + pre_mask : :obj:`bool` Indicates whether the ``pre_mask`` input will be set (and thus, step 1 should be skipped). - name : str + multiecho : :obj:`bool` + If multiecho data was supplied, data from the first echo + will be selected + name : :obj:`str` Name of workflow (default: ``bold_reference_wf``) - gen_report : bool + gen_report : :obj:`bool` Whether a mask report node should be appended in the end Inputs @@ -105,7 +109,13 @@ def init_bold_reference_wf( """ workflow = Workflow(name=name) - workflow.__desc__ = """\ + if multiecho: + workflow.__desc__ = """\ +First, a reference volume and its skull-stripped version were generated +from the first echo using a custom methodology of *fMRIPrep*. +""" + else: + workflow.__desc__ = """\ First, a reference volume and its skull-stripped version were generated using a custom methodology of *fMRIPrep*. """ @@ -136,12 +146,17 @@ def init_bold_reference_wf( if bold_file is not None: inputnode.inputs.bold_file = bold_file - validate = pe.MapNode( - ValidateImage(), - name="validate", - mem_gb=DEFAULT_MEMORY_MIN_GB, - iterfield=["in_file"], - ) + if multiecho: + validate = pe.MapNode( + ValidateImage(), + name="validate", + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=["in_file"], + ) + else: + validate = pe.Node( + ValidateImage(), name="validate", mem_gb=DEFAULT_MEMORY_MIN_GB + ) gen_ref = pe.Node( EstimateReferenceImage(), name="gen_ref", mem_gb=1 @@ -164,17 +179,12 @@ def init_bold_reference_wf( (inputnode, enhance_and_skullstrip_bold_wf, [ ("bold_mask", "inputnode.pre_mask"), ]), - (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), (inputnode, gen_ref, [("sbref_file", "sbref_file")]), (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), (validate, gen_ref, [("out_file", "in_file")]), (gen_ref, enhance_and_skullstrip_bold_wf, [ ("ref_image", "inputnode.in_file"), ]), - (validate, outputnode, [ - (("out_file", select_first), "bold_file"), - ("out_report", "validation_report"), - ]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_ref, outputnode, [ @@ -187,6 +197,19 @@ def init_bold_reference_wf( ("outputnode.skull_stripped_file", "ref_image_brain"), ]), ]) + + if multiecho: + workflow.connect([ + (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), + (validate, outputnode, [(("out_file", select_first), "bold_file"), + ("out_report", "validation_report")]), + ]) + else: + workflow.connect([ + (inputnode, validate, [("bold_file", "in_file")]), + (validate, outputnode, [("out_file", "bold_file"), + ("out_report", "validation_report")]), + ]) # fmt: on if gen_report: diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 6552c4c1542..4495bbaa948 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -424,6 +424,14 @@ class _EstimateReferenceImageInputSpec(BaseInterfaceInputSpec): usedefault=True, desc="Which software to use to perform motion correction", ) + multiecho = traits.Bool( + False, + usedefault=True, + desc=( + "If multiecho data was supplied, data from " + "the first echo will be selected." + ), + ) class _EstimateReferenceImageOutputSpec(TraitedSpec): From 64d36e2144e74b26e6cb351bd5a86afdbf43e4e0 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 7 Oct 2019 08:53:57 -0400 Subject: [PATCH 09/19] Check inputs against multiecho argument in EstimateReferenceImage. --- niworkflows/interfaces/registration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 4495bbaa948..841802f8ea9 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -462,8 +462,16 @@ def _run_interface(self, runtime): self._results["n_volumes_to_discard"] = n_volumes_to_discard + if multiecho and (len(self.inputs.in_file) < 2): + raise ValueError("Argument 'multiecho' is True but " + "'in_file' has only one element") + out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") if isdefined(self.inputs.sbref_file): + if multiecho and (len(self.inputs.sbref_file) < 2): + raise ValueError("Argument 'multiecho' is True but " + "'sbref_file' has only one element") + # Select first SBRef file ref_name = self.inputs.sbref_file[0] ref_nii = nb.squeeze_image(nb.load(ref_name)) From 27dceed9980fa3224c24d855603d8eef6fe6ab10 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 7 Oct 2019 09:20:43 -0400 Subject: [PATCH 10/19] Fix that mistake. --- niworkflows/interfaces/registration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 841802f8ea9..d88c18a75c0 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -462,13 +462,13 @@ def _run_interface(self, runtime): self._results["n_volumes_to_discard"] = n_volumes_to_discard - if multiecho and (len(self.inputs.in_file) < 2): + if self.inputs.multiecho and (len(self.inputs.in_file) < 2): raise ValueError("Argument 'multiecho' is True but " "'in_file' has only one element") out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") if isdefined(self.inputs.sbref_file): - if multiecho and (len(self.inputs.sbref_file) < 2): + if self.inputs.multiecho and (len(self.inputs.sbref_file) < 2): raise ValueError("Argument 'multiecho' is True but " "'sbref_file' has only one element") From 7c781af19338bb05aa4550ee10b836194351c2b7 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 3 Jun 2020 11:19:17 -0400 Subject: [PATCH 11/19] Update niworkflows/utils/misc.py Co-authored-by: Oscar Esteban --- niworkflows/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py index 9b2b0c9ba86..cb6686e0422 100644 --- a/niworkflows/utils/misc.py +++ b/niworkflows/utils/misc.py @@ -225,7 +225,7 @@ def select_first(in_files): 'some/file1.nii.gz' """ - if isinstance(in_files, list): + if isinstance(in_files, (list, tuple)): return in_files[0] else: return in_files From c08127b4b7f72060f85b5c62b24e2acebd7c7e54 Mon Sep 17 00:00:00 2001 From: oesteban Date: Mon, 1 Jun 2020 15:37:03 -0700 Subject: [PATCH 12/19] FIX: Update mask-regression tests for #408 --- niworkflows/func/tests/test_util.py | 76 +++++++++++++++++------------ 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/niworkflows/func/tests/test_util.py b/niworkflows/func/tests/test_util.py index ef59d3f517e..48afbd983db 100755 --- a/niworkflows/func/tests/test_util.py +++ b/niworkflows/func/tests/test_util.py @@ -1,17 +1,47 @@ -""" Testing module for fmriprep.workflows.bold.util """ +"""Testing module for fmriprep.workflows.bold.util.""" import pytest import os from pathlib import Path import numpy as np from nipype.pipeline import engine as pe -from nipype.utils.filemanip import fname_presuffix, copyfile +from nipype.utils.filemanip import fname_presuffix, copyfile, ensure_list from nilearn.image import load_img from niworkflows.interfaces.masks import ROIsPlot from ..util import init_bold_reference_wf +# Multi-echo datasets +bold_datasets = ["""\ +ds000210/sub-06_task-rest_run-01_echo-1_bold.nii.gz +ds000210/sub-06_task-rest_run-01_echo-2_bold.nii.gz +ds000210/sub-06_task-rest_run-01_echo-3_bold.nii.gz\ +""".splitlines(), """\ +ds000216/sub-03_task-rest_echo-1_bold.nii.gz +ds000216/sub-03_task-rest_echo-2_bold.nii.gz +ds000216/sub-03_task-rest_echo-3_bold.nii.gz +ds000216/sub-03_task-rest_echo-4_bold.nii.gz""".splitlines()] + +# Single-echo datasets +bold_datasets += """\ +ds000116/sub-12_task-visualoddballwithbuttonresponsetotargetstimuli_run-02_bold.nii.gz +ds000133/sub-06_ses-post_task-rest_run-01_bold.nii.gz +ds000140/sub-32_task-heatpainwithregulationandratings_run-02_bold.nii.gz +ds000157/sub-23_task-passiveimageviewing_bold.nii.gz +ds000237/sub-03_task-MemorySpan_acq-multiband_run-01_bold.nii.gz +ds000237/sub-06_task-MemorySpan_acq-multiband_run-01_bold.nii.gz +ds001240/sub-26_task-localizerimagination_bold.nii.gz +ds001240/sub-26_task-localizerviewing_bold.nii.gz +ds001240/sub-26_task-molencoding_run-01_bold.nii.gz +ds001240/sub-26_task-molencoding_run-02_bold.nii.gz +ds001240/sub-26_task-molretrieval_run-01_bold.nii.gz +ds001240/sub-26_task-molretrieval_run-02_bold.nii.gz +ds001240/sub-26_task-rest_bold.nii.gz +ds001362/sub-01_task-taskname_run-01_bold.nii.gz""".splitlines() + +bold_datasets = [ensure_list(d) for d in bold_datasets] + def symmetric_overlap(img1, img2): mask1 = load_img(img1).get_fdata() > 0 @@ -32,43 +62,23 @@ def symmetric_overlap(img1, img2): "input_fname,expected_fname", [ ( - os.path.join(os.getenv("FMRIPREP_REGRESSION_SOURCE", ""), base_fname), + [os.path.join(os.getenv("FMRIPREP_REGRESSION_SOURCE", ""), bf) + for bf in base_fname], fname_presuffix( - base_fname, + base_fname[0].replace("_echo-1", ""), suffix="_mask", use_ext=True, newpath=os.path.join( os.getenv("FMRIPREP_REGRESSION_TARGETS", ""), - os.path.dirname(base_fname), + os.path.dirname(base_fname[0]), ), ), ) - for base_fname in """\ -ds000116/sub-12_task-visualoddballwithbuttonresponsetotargetstimuli_run-02_bold.nii.gz -ds000133/sub-06_ses-post_task-rest_run-01_bold.nii.gz -ds000140/sub-32_task-heatpainwithregulationandratings_run-02_bold.nii.gz -ds000157/sub-23_task-passiveimageviewing_bold.nii.gz -ds000210/sub-06_task-rest_run-01_echo-1_bold.nii.gz -ds000210/sub-06_task-rest_run-01_echo-2_bold.nii.gz -ds000210/sub-06_task-rest_run-01_echo-3_bold.nii.gz -ds000216/sub-03_task-rest_echo-1_bold.nii.gz -ds000216/sub-03_task-rest_echo-2_bold.nii.gz -ds000216/sub-03_task-rest_echo-3_bold.nii.gz -ds000216/sub-03_task-rest_echo-4_bold.nii.gz -ds000237/sub-03_task-MemorySpan_acq-multiband_run-01_bold.nii.gz -ds000237/sub-06_task-MemorySpan_acq-multiband_run-01_bold.nii.gz -ds001240/sub-26_task-localizerimagination_bold.nii.gz -ds001240/sub-26_task-localizerviewing_bold.nii.gz -ds001240/sub-26_task-molencoding_run-01_bold.nii.gz -ds001240/sub-26_task-molencoding_run-02_bold.nii.gz -ds001240/sub-26_task-molretrieval_run-01_bold.nii.gz -ds001240/sub-26_task-molretrieval_run-02_bold.nii.gz -ds001240/sub-26_task-rest_bold.nii.gz -ds001362/sub-01_task-taskname_run-01_bold.nii.gz""".splitlines() + for base_fname in bold_datasets ], ) def test_masking(input_fname, expected_fname): - basename = Path(input_fname).name + basename = Path(input_fname[0]).name dsname = Path(expected_fname).parent.name # Reconstruct base_fname from above @@ -76,8 +86,10 @@ def test_masking(input_fname, expected_fname): newpath = reports_dir / dsname name = basename.rstrip("_bold.nii.gz").replace("-", "_") - bold_reference_wf = init_bold_reference_wf(omp_nthreads=1, name=name) - bold_reference_wf.inputs.inputnode.bold_file = input_fname + bold_reference_wf = init_bold_reference_wf(omp_nthreads=1, name=name, + multiecho=len(input_fname) > 1) + bold_reference_wf.inputs.inputnode.bold_file = input_fname[0] if len(input_fname) == 1 \ + else input_fname base_dir = os.getenv("CACHED_WORK_DIRECTORY") if base_dir: base_dir = Path(base_dir) / dsname @@ -85,7 +97,7 @@ def test_masking(input_fname, expected_fname): bold_reference_wf.base_dir = str(base_dir) out_fname = fname_presuffix( - basename, suffix="_mask.svg", use_ext=False, newpath=str(newpath) + Path(expected_fname).name, suffix=".svg", use_ext=False, newpath=str(newpath) ) newpath.mkdir(parents=True, exist_ok=True) @@ -117,7 +129,7 @@ def test_masking(input_fname, expected_fname): mask_dir.mkdir(parents=True, exist_ok=True) copyfile( combine_masks.result.outputs.out_file, - fname_presuffix(basename, suffix="_mask", use_ext=True, newpath=str(mask_dir)), + str(mask_dir / Path(expected_fname).name), copy=True, ) From a2d838d34c9674cc6d4bea88a511ac96c51bf732 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 3 Jun 2020 11:55:44 -0400 Subject: [PATCH 13/19] Apply suggestions from code review Thanks @oesteban! Co-authored-by: Oscar Esteban --- niworkflows/func/util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index 4f8b18a84a2..fabbfa2ad8b 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -159,7 +159,7 @@ def init_bold_reference_wf( ) gen_ref = pe.Node( - EstimateReferenceImage(), name="gen_ref", mem_gb=1 + EstimateReferenceImage(multiecho=multiecho), name="gen_ref", mem_gb=1 ) # OE: 128x128x128x50 * 64 / 8 ~ 900MB. enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf( brainmask_thresh=brainmask_thresh, @@ -185,6 +185,10 @@ def init_bold_reference_wf( (gen_ref, enhance_and_skullstrip_bold_wf, [ ("ref_image", "inputnode.in_file"), ]), + (validate, outputnode, [ + (("out_file", select_first), "bold_file"), + ("out_report", "validation_report"), + ]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_ref, outputnode, [ @@ -201,14 +205,10 @@ def init_bold_reference_wf( if multiecho: workflow.connect([ (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), - (validate, outputnode, [(("out_file", select_first), "bold_file"), - ("out_report", "validation_report")]), ]) else: workflow.connect([ (inputnode, validate, [("bold_file", "in_file")]), - (validate, outputnode, [("out_file", "bold_file"), - ("out_report", "validation_report")]), ]) # fmt: on From 01f6474024c44d41af26ac83d25581a02f0f1826 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 5 Jun 2020 13:39:10 -0700 Subject: [PATCH 14/19] enh: do not use inline connections when feeding outputnodes --- niworkflows/func/util.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index fabbfa2ad8b..e360d0d9033 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -21,7 +21,7 @@ from ..interfaces.masks import SimpleShowMaskRPT from ..interfaces.registration import EstimateReferenceImage from ..interfaces.utils import CopyXForm -from ..utils.misc import select_first, pass_dummy_scans as _pass_dummy_scans +from ..utils.misc import pass_dummy_scans as _pass_dummy_scans DEFAULT_MEMORY_MIN_GB = 0.01 @@ -173,9 +173,15 @@ def init_bold_reference_wf( run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, ) + sel_1st = pe.Node(niu.Select(index=[0]), + name="sel_1st", run_without_submitting=True) # fmt: off workflow.connect([ + (inputnode, validate, [ + (("bold_file", ensure_list) if multiecho else "bold_file", + "in_file"), + ]), (inputnode, enhance_and_skullstrip_bold_wf, [ ("bold_mask", "inputnode.pre_mask"), ]), @@ -185,10 +191,7 @@ def init_bold_reference_wf( (gen_ref, enhance_and_skullstrip_bold_wf, [ ("ref_image", "inputnode.in_file"), ]), - (validate, outputnode, [ - (("out_file", select_first), "bold_file"), - ("out_report", "validation_report"), - ]), + (validate, sel_1st, [(("out_file", ensure_list), "inlist")]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_ref, outputnode, [ @@ -200,16 +203,9 @@ def init_bold_reference_wf( ("outputnode.mask_file", "bold_mask"), ("outputnode.skull_stripped_file", "ref_image_brain"), ]), + (validate, outputnode, [("out_report", "validation_report")]), + (sel_1st, outputnode, [("out", "bold_file")]), ]) - - if multiecho: - workflow.connect([ - (inputnode, validate, [(("bold_file", ensure_list), "in_file")]), - ]) - else: - workflow.connect([ - (inputnode, validate, [("bold_file", "in_file")]), - ]) # fmt: on if gen_report: From 0334ac828c4f93cbf9219c1d679b3a431ad9e39e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 5 Jun 2020 15:16:12 -0700 Subject: [PATCH 15/19] enh: deep revision of ``EstimateReferenceImage`` --- niworkflows/interfaces/registration.py | 105 +++++++++++++++---------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index d88c18a75c0..0c7b0677efe 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -8,7 +8,7 @@ import numpy as np from nilearn import image as nli from nilearn.image import index_img -from nipype.utils.filemanip import fname_presuffix +from nipype.utils.filemanip import fname_presuffix, ensure_list from nipype.interfaces.base import ( traits, isdefined, @@ -445,61 +445,84 @@ class _EstimateReferenceImageOutputSpec(TraitedSpec): class EstimateReferenceImage(SimpleInterface): """ - Given an 4D EPI file estimate an optimal reference image that could be later - used for motion estimation and coregistration purposes. If detected uses - T1 saturated volumes (non-steady state) otherwise a median of + Generate a reference 3D map from BOLD and SBRef EPI images for BOLD datasets. + + Given a 4D BOLD file or one or more 3/4D SBRefs, estimate a reference + image for subsequent motion estimation and coregistration steps. + For the case of BOLD datasets, it estimates a number of T1w saturated volumes + (non-steady state at the beginning of the scan) and calculates the median + across them. + Otherwise (SBRefs or detected zero non-steady state frames), a median of of a subset of motion corrected volumes is used. + If the input reference (BOLD or SBRef) is 3D already, it just returns a + copy of the image with the NIfTI header extensions removed. + + LIMITATION: If one wants to extract the reference from several SBRefs + with several echoes each, the first echo should be selected elsewhere + and run this interface in ``multiecho = False`` mode. """ input_spec = _EstimateReferenceImageInputSpec output_spec = _EstimateReferenceImageOutputSpec def _run_interface(self, runtime): - # Select first EPI file - ref_name = self.inputs.in_file[0] - ref_nii = nb.load(ref_name) - n_volumes_to_discard = _get_vols_to_discard(ref_nii) - - self._results["n_volumes_to_discard"] = n_volumes_to_discard - - if self.inputs.multiecho and (len(self.inputs.in_file) < 2): - raise ValueError("Argument 'multiecho' is True but " - "'in_file' has only one element") + is_sbref = isdefined(self.inputs.sbref_file) + ref_input = ensure_list( + self.inputs.sbref_file if is_sbref else self.inputs.in_file + ) - out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") - if isdefined(self.inputs.sbref_file): - if self.inputs.multiecho and (len(self.inputs.sbref_file) < 2): + if self.inputs.multiecho: + if len(ref_input) < 2: + input_name = "sbref_file" if is_sbref else "in_file" raise ValueError("Argument 'multiecho' is True but " - "'sbref_file' has only one element") - - # Select first SBRef file - ref_name = self.inputs.sbref_file[0] - ref_nii = nb.squeeze_image(nb.load(ref_name)) + f"'{input_name}' has only one element.") + else: + # Select only the first echo (see LIMITATION above for SBRefs) + ref_input = ref_input[:1] + elif not is_sbref and len(ref_input) > 1: + raise ValueError("Input 'in_file' cannot point to more than one file " + "for single-echo BOLD datasets.") + + # Build the nibabel spatial image we will work with + ref_im = [] + for im_i in ref_input: + nib_i = nb.squeeze_image(nb.load(im_i)) + if nib_i.dataobj.ndim == 3: + ref_im.append(nib_i) + elif nib_i.dataobj.ndim == 4: + ref_im += nb.four_to_three(nib_i) + ref_im = nb.squeeze_image(nb.concat_images(ref_im)) + + # Volumes to discard only makes sense with BOLD inputs. + if not is_sbref: + n_volumes_to_discard = _get_vols_to_discard(ref_im) + out_ref_fname = os.path.join(runtime.cwd, "ref_bold.nii.gz") + else: + n_volumes_to_discard = 0 out_ref_fname = os.path.join(runtime.cwd, "ref_sbref.nii.gz") - # If reference is only 1 volume, return it directly - if len(ref_nii.shape) == 3: - ref_nii.header.extensions.clear() - ref_nii.to_filename(out_ref_fname) - self._results["ref_image"] = out_ref_fname - return runtime - else: - # Reset this variable as it no longer applies - # and value for the output is stored in self._results - n_volumes_to_discard = 0 + # Set interface outputs + self._results["n_volumes_to_discard"] = n_volumes_to_discard + self._results["ref_image"] = out_ref_fname # Slicing may induce inconsistencies with shape-dependent values in extensions. # For now, remove all. If this turns out to be a mistake, we can select extensions # that don't break pipeline stages. - ref_nii.header.extensions.clear() + ref_im.header.extensions.clear() + + # If reference is only 1 volume, return it directly + if ref_im.dataobj.ndim == 3: + ref_im.to_filename(out_ref_fname) + return runtime if n_volumes_to_discard == 0: - if ref_nii.shape[-1] > 40: - ref_name = os.path.join(runtime.cwd, "slice.nii.gz") - nb.Nifti1Image( - ref_nii.dataobj[:, :, :, 20:40], ref_nii.affine, ref_nii.header - ).to_filename(ref_name) + if ref_im.shape[-1] > 40: + ref_im = nb.Nifti1Image( + ref_im.dataobj[:, :, :, 20:40], ref_im.affine, ref_im.header + ) + ref_name = os.path.join(runtime.cwd, "slice.nii.gz") + ref_im.to_filename(ref_name) if self.inputs.mc_method == "AFNI": res = afni.Volreg( in_file=ref_name, @@ -516,14 +539,12 @@ def _run_interface(self, runtime): median_image_data = np.median(mc_slice_nii.get_fdata(), axis=3) else: median_image_data = np.median( - ref_nii.dataobj[:, :, :, :n_volumes_to_discard], axis=3 + ref_im.dataobj[:, :, :, :n_volumes_to_discard], axis=3 ) - nb.Nifti1Image(median_image_data, ref_nii.affine, ref_nii.header).to_filename( + nb.Nifti1Image(median_image_data, ref_im.affine, ref_im.header).to_filename( out_ref_fname ) - - self._results["ref_image"] = out_ref_fname return runtime From d8f63660bf904f66e7307742e788b27c25600398 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 5 Jun 2020 15:33:23 -0700 Subject: [PATCH 16/19] chore: cosmetic changes & deduplicate ``_pop`` definition in ants cc/ https://github.com/nipreps/niworkflows/pull/408/files#r330252779 --- niworkflows/anat/ants.py | 8 +------- niworkflows/utils/misc.py | 6 ++++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/niworkflows/anat/ants.py b/niworkflows/anat/ants.py index c32b3042504..3130aec9a98 100644 --- a/niworkflows/anat/ants.py +++ b/niworkflows/anat/ants.py @@ -15,7 +15,7 @@ from nipype.interfaces.fsl.maths import ApplyMask from nipype.interfaces.ants import N4BiasFieldCorrection, Atropos, MultiplyImages -from ..utils.misc import get_template_specs +from ..utils.misc import get_template_specs, select_first as _pop # niworkflows from ..interfaces.ants import ( @@ -897,12 +897,6 @@ def init_n4_only_wf( return wf -def _pop(in_files): - if isinstance(in_files, (list, tuple)): - return in_files[0] - return in_files - - def _select_labels(in_segm, labels): from os import getcwd import numpy as np diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py index cb6686e0422..c2751ad7334 100644 --- a/niworkflows/utils/misc.py +++ b/niworkflows/utils/misc.py @@ -215,10 +215,13 @@ def splitext(fname): def select_first(in_files): """ Select the first file from a list of filenames. + Used to grab the first echo's file when processing multi-echo data through workflows that only accept a single file. + Examples + -------- >>> select_first('some/file.nii.gz') 'some/file.nii.gz' >>> select_first(['some/file1.nii.gz', 'some/file2.nii.gz']) @@ -227,8 +230,7 @@ def select_first(in_files): """ if isinstance(in_files, (list, tuple)): return in_files[0] - else: - return in_files + return in_files def _copy_any(src, dst): From d3fb528dbb145f2598d616ab3ca1b085e428d67e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 5 Jun 2020 15:37:44 -0700 Subject: [PATCH 17/19] critical: bump regression data cache-id on CircleCI [skip ci] This is necessary as now the masks regressions will only be run on the first echo of ME datasets. This id bump will only be applied after merging into master, as the regression tests are run only on master or branches with the prefix ``masks?/``. Reference: tsalo/niworkflows#1. --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fedb9e92932..4a44a61b9c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -151,8 +151,8 @@ jobs: steps: - restore_cache: keys: - - regression-v4-{{ .Revision }} - - regression-v4- + - regression-v5-{{ .Revision }} + - regression-v5- - run: name: Get truncated BOLD series command: | @@ -175,7 +175,7 @@ jobs: echo "Pre-computed masks were cached" fi - save_cache: - key: regression-v4-{{ .Revision }}-{{ epoch }} + key: regression-v5-{{ .Revision }}-{{ epoch }} paths: - /tmp/data @@ -284,7 +284,7 @@ jobs: - restore_cache: keys: - - regression-v4-{{ .Revision }} + - regression-v5-{{ .Revision }} - restore_cache: keys: - masks-workdir-v2-{{ .Branch }}-{{epoch}} From 9d78796373163d2881f80407ff9d5aee9c65f77e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 5 Jun 2020 17:06:41 -0700 Subject: [PATCH 18/19] enh: better handling of sbrefs and the workflow's description validates incoming sbrefs as suggested in https://github.com/poldracklab/fmriprep/pull/1803#discussion_r436204506 --- niworkflows/func/util.py | 82 +++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index e360d0d9033..7676c23f65a 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -30,6 +30,7 @@ def init_bold_reference_wf( omp_nthreads, bold_file=None, + sbref_files=None, brainmask_thresh=0.85, pre_mask=False, multiecho=False, @@ -57,6 +58,10 @@ def init_bold_reference_wf( Maximum number of threads an individual process may use bold_file : :obj:`str` BOLD series NIfTI file + sbref_files : :obj:`list` or :obj:`bool` + Single band (as opposed to multi band) reference NIfTI file. + If ``True`` is passed, the workflow is built to accommodate SBRefs, + but the input is left undefined (i.e., it is left open for connection) brainmask_thresh: :obj:`float` Lower threshold for the probabilistic brainmask to obtain the final binary mask (default: 0.85). @@ -109,16 +114,12 @@ def init_bold_reference_wf( """ workflow = Workflow(name=name) - if multiecho: - workflow.__desc__ = """\ + workflow.__desc__ = f"""\ First, a reference volume and its skull-stripped version were generated -from the first echo using a custom methodology of *fMRIPrep*. -""" - else: - workflow.__desc__ = """\ -First, a reference volume and its skull-stripped version were generated -using a custom methodology of *fMRIPrep*. +{'from the shortest echo of the BOLD run' * multiecho} using a custom +methodology of *fMRIPrep*. """ + inputnode = pe.Node( niu.IdentityInterface( fields=["bold_file", "bold_mask", "dummy_scans", "sbref_file"] @@ -146,17 +147,12 @@ def init_bold_reference_wf( if bold_file is not None: inputnode.inputs.bold_file = bold_file - if multiecho: - validate = pe.MapNode( - ValidateImage(), - name="validate", - mem_gb=DEFAULT_MEMORY_MIN_GB, - iterfield=["in_file"], - ) - else: - validate = pe.Node( - ValidateImage(), name="validate", mem_gb=DEFAULT_MEMORY_MIN_GB - ) + val_bold = pe.MapNode( + ValidateImage(), + name="val_bold", + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=["in_file"], + ) gen_ref = pe.Node( EstimateReferenceImage(multiecho=multiecho), name="gen_ref", mem_gb=1 @@ -173,25 +169,23 @@ def init_bold_reference_wf( run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, ) - sel_1st = pe.Node(niu.Select(index=[0]), - name="sel_1st", run_without_submitting=True) + bold_1st = pe.Node(niu.Select(index=[0]), + name="bold_1st", run_without_submitting=True) + validate_1st = pe.Node(niu.Select(index=[0]), + name="validate_1st", run_without_submitting=True) # fmt: off workflow.connect([ - (inputnode, validate, [ - (("bold_file", ensure_list) if multiecho else "bold_file", - "in_file"), - ]), + (inputnode, val_bold, [(("bold_file", ensure_list), "in_file")]), (inputnode, enhance_and_skullstrip_bold_wf, [ ("bold_mask", "inputnode.pre_mask"), ]), - (inputnode, gen_ref, [("sbref_file", "sbref_file")]), (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), - (validate, gen_ref, [("out_file", "in_file")]), + (val_bold, gen_ref, [("out_file", "in_file")]), (gen_ref, enhance_and_skullstrip_bold_wf, [ ("ref_image", "inputnode.in_file"), ]), - (validate, sel_1st, [(("out_file", ensure_list), "inlist")]), + (val_bold, bold_1st, [(("out_file", ensure_list), "inlist")]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_ref, outputnode, [ @@ -203,11 +197,39 @@ def init_bold_reference_wf( ("outputnode.mask_file", "bold_mask"), ("outputnode.skull_stripped_file", "ref_image_brain"), ]), - (validate, outputnode, [("out_report", "validation_report")]), - (sel_1st, outputnode, [("out", "bold_file")]), + (val_bold, validate_1st, [(("out_report", ensure_list), "inlist")]), + (bold_1st, outputnode, [("out", "bold_file")]), + (validate_1st, outputnode, [("out", "validation_report")]), ]) # fmt: on + if sbref_files: + nsbrefs = 0 + if sbref_files is not True: + # If not boolean, then it is a list-of or pathlike. + inputnode.inputs.sbref_file = sbref_files + nsbrefs = 1 if isinstance(sbref_files, str) else len(sbref_files) + + val_sbref = pe.MapNode( + ValidateImage(), + name="val_sbref", + mem_gb=DEFAULT_MEMORY_MIN_GB, + iterfield=["in_file"], + ) + # fmt: off + workflow.connect([ + (inputnode, val_sbref, [(("sbref_file", ensure_list), "in_file")]), + (val_sbref, gen_ref, [("out_file", "sbref_file")]), + ]) + # fmt: on + + # Edit the boilerplate as the SBRef will be the reference + workflow.__desc__ = f"""\ +First, a reference volume and its skull-stripped version were generated +by aligning and averaging{' the first echo of' * multiecho} +{nsbrefs or ''} single-band references (SBRefs). +""" + if gen_report: mask_reportlet = pe.Node(SimpleShowMaskRPT(), name="mask_reportlet") # fmt: off From 6fe250cb8db9b79672ba9d28a4adb15fc83e2f0f Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 6 Jun 2020 19:09:57 -0700 Subject: [PATCH 19/19] fix: do not use ``nipype.utils.filemanip.ensure_list`` Because that function utilizes ``is_container``, which is imported at global context, nipype could not evaluate ``ensure_list`` calls inlined inside ``connect``. This would end up failing with: ``` fMRIPrep failed: name 'is_container' is not defined ``` A new module for nipype connections is created to provide this kind of commodity functions for all NiPreps. --- niworkflows/anat/ants.py | 3 +- niworkflows/func/tests/test_util.py | 5 ++- niworkflows/func/util.py | 10 ++--- niworkflows/interfaces/registration.py | 6 +-- niworkflows/utils/connections.py | 58 ++++++++++++++++++++++++++ niworkflows/utils/misc.py | 21 ---------- 6 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 niworkflows/utils/connections.py diff --git a/niworkflows/anat/ants.py b/niworkflows/anat/ants.py index 3130aec9a98..09ec9330246 100644 --- a/niworkflows/anat/ants.py +++ b/niworkflows/anat/ants.py @@ -15,7 +15,8 @@ from nipype.interfaces.fsl.maths import ApplyMask from nipype.interfaces.ants import N4BiasFieldCorrection, Atropos, MultiplyImages -from ..utils.misc import get_template_specs, select_first as _pop +from ..utils.misc import get_template_specs +from ..utils.connections import pop_file as _pop # niworkflows from ..interfaces.ants import ( diff --git a/niworkflows/func/tests/test_util.py b/niworkflows/func/tests/test_util.py index 48afbd983db..2151fb976a4 100755 --- a/niworkflows/func/tests/test_util.py +++ b/niworkflows/func/tests/test_util.py @@ -5,9 +5,10 @@ import numpy as np from nipype.pipeline import engine as pe -from nipype.utils.filemanip import fname_presuffix, copyfile, ensure_list +from nipype.utils.filemanip import fname_presuffix, copyfile from nilearn.image import load_img +from ...utils.connections import listify from niworkflows.interfaces.masks import ROIsPlot from ..util import init_bold_reference_wf @@ -40,7 +41,7 @@ ds001240/sub-26_task-rest_bold.nii.gz ds001362/sub-01_task-taskname_run-01_bold.nii.gz""".splitlines() -bold_datasets = [ensure_list(d) for d in bold_datasets] +bold_datasets = [listify(d) for d in bold_datasets] def symmetric_overlap(img1, img2): diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index 7676c23f65a..cd9140425da 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -6,7 +6,6 @@ from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu, fsl, afni -from nipype.utils.filemanip import ensure_list from templateflow.api import get as get_template @@ -21,6 +20,7 @@ from ..interfaces.masks import SimpleShowMaskRPT from ..interfaces.registration import EstimateReferenceImage from ..interfaces.utils import CopyXForm +from ..utils.connections import listify from ..utils.misc import pass_dummy_scans as _pass_dummy_scans @@ -176,7 +176,7 @@ def init_bold_reference_wf( # fmt: off workflow.connect([ - (inputnode, val_bold, [(("bold_file", ensure_list), "in_file")]), + (inputnode, val_bold, [(("bold_file", listify), "in_file")]), (inputnode, enhance_and_skullstrip_bold_wf, [ ("bold_mask", "inputnode.pre_mask"), ]), @@ -185,7 +185,7 @@ def init_bold_reference_wf( (gen_ref, enhance_and_skullstrip_bold_wf, [ ("ref_image", "inputnode.in_file"), ]), - (val_bold, bold_1st, [(("out_file", ensure_list), "inlist")]), + (val_bold, bold_1st, [(("out_file", listify), "inlist")]), (gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]), (calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]), (gen_ref, outputnode, [ @@ -197,7 +197,7 @@ def init_bold_reference_wf( ("outputnode.mask_file", "bold_mask"), ("outputnode.skull_stripped_file", "ref_image_brain"), ]), - (val_bold, validate_1st, [(("out_report", ensure_list), "inlist")]), + (val_bold, validate_1st, [(("out_report", listify), "inlist")]), (bold_1st, outputnode, [("out", "bold_file")]), (validate_1st, outputnode, [("out", "validation_report")]), ]) @@ -218,7 +218,7 @@ def init_bold_reference_wf( ) # fmt: off workflow.connect([ - (inputnode, val_sbref, [(("sbref_file", ensure_list), "in_file")]), + (inputnode, val_sbref, [(("sbref_file", listify), "in_file")]), (val_sbref, gen_ref, [("out_file", "sbref_file")]), ]) # fmt: on diff --git a/niworkflows/interfaces/registration.py b/niworkflows/interfaces/registration.py index 0c7b0677efe..5a71eee4aec 100644 --- a/niworkflows/interfaces/registration.py +++ b/niworkflows/interfaces/registration.py @@ -8,7 +8,7 @@ import numpy as np from nilearn import image as nli from nilearn.image import index_img -from nipype.utils.filemanip import fname_presuffix, ensure_list +from nipype.utils.filemanip import fname_presuffix from nipype.interfaces.base import ( traits, isdefined, @@ -467,9 +467,7 @@ class EstimateReferenceImage(SimpleInterface): def _run_interface(self, runtime): is_sbref = isdefined(self.inputs.sbref_file) - ref_input = ensure_list( - self.inputs.sbref_file if is_sbref else self.inputs.in_file - ) + ref_input = self.inputs.sbref_file if is_sbref else self.inputs.in_file if self.inputs.multiecho: if len(ref_input) < 2: diff --git a/niworkflows/utils/connections.py b/niworkflows/utils/connections.py new file mode 100644 index 00000000000..7862cf4fd74 --- /dev/null +++ b/niworkflows/utils/connections.py @@ -0,0 +1,58 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" +Utilities for the creation of nipype workflows. + +Because these functions are meant to be inlined in nipype's ``connect`` invocations, +all the imports MUST be done in each function's context. + +""" + +__all__ = [ + "listify", + "pop_file", +] + + +def pop_file(in_files): + """ + Select the first file from a list of filenames. + + Used to grab the first echo's file when processing + multi-echo data through workflows that only accept + a single file. + + Examples + -------- + >>> pop_file('some/file.nii.gz') + 'some/file.nii.gz' + >>> pop_file(['some/file1.nii.gz', 'some/file2.nii.gz']) + 'some/file1.nii.gz' + + """ + if isinstance(in_files, (list, tuple)): + return in_files[0] + return in_files + + +def listify(value): + """ + Convert to a list (inspired by bids.utils.listify). + + Examples + -------- + >>> listify('some/file.nii.gz') + ['some/file.nii.gz'] + >>> listify((0.1, 0.2)) + [0.1, 0.2] + >>> listify(None) is None + True + + """ + from pathlib import Path + from nipype.interfaces.base import isdefined + if not isdefined(value) or isinstance(value, type(None)): + return value + if isinstance(value, (str, bytes, Path)): + return [str(value)] + return list(value) diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py index c2751ad7334..9cfbdc06175 100644 --- a/niworkflows/utils/misc.py +++ b/niworkflows/utils/misc.py @@ -212,27 +212,6 @@ def splitext(fname): return stem, basename[len(stem):] -def select_first(in_files): - """ - Select the first file from a list of filenames. - - Used to grab the first echo's file when processing - multi-echo data through workflows that only accept - a single file. - - Examples - -------- - >>> select_first('some/file.nii.gz') - 'some/file.nii.gz' - >>> select_first(['some/file1.nii.gz', 'some/file2.nii.gz']) - 'some/file1.nii.gz' - - """ - if isinstance(in_files, (list, tuple)): - return in_files[0] - return in_files - - def _copy_any(src, dst): import os import gzip