Skip to content

Commit 962ce99

Browse files
authored
Merge pull request #408 from tsalo/sbref
ENH: Allow bold_reference_wf to accept multiple EPIs/SBRefs
2 parents f963b2c + 6fe250c commit 962ce99

File tree

6 files changed

+261
-93
lines changed

6 files changed

+261
-93
lines changed

.circleci/config.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ jobs:
151151
steps:
152152
- restore_cache:
153153
keys:
154-
- regression-v4-{{ .Revision }}
155-
- regression-v4-
154+
- regression-v5-{{ .Revision }}
155+
- regression-v5-
156156
- run:
157157
name: Get truncated BOLD series
158158
command: |
@@ -175,7 +175,7 @@ jobs:
175175
echo "Pre-computed masks were cached"
176176
fi
177177
- save_cache:
178-
key: regression-v4-{{ .Revision }}-{{ epoch }}
178+
key: regression-v5-{{ .Revision }}-{{ epoch }}
179179
paths:
180180
- /tmp/data
181181

@@ -284,7 +284,7 @@ jobs:
284284
285285
- restore_cache:
286286
keys:
287-
- regression-v4-{{ .Revision }}
287+
- regression-v5-{{ .Revision }}
288288
- restore_cache:
289289
keys:
290290
- masks-workdir-v2-{{ .Branch }}-{{epoch}}

niworkflows/anat/ants.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from nipype.interfaces.ants import N4BiasFieldCorrection, Atropos, MultiplyImages
1717

1818
from ..utils.misc import get_template_specs
19+
from ..utils.connections import pop_file as _pop
1920

2021
# niworkflows
2122
from ..interfaces.ants import (
@@ -897,12 +898,6 @@ def init_n4_only_wf(
897898
return wf
898899

899900

900-
def _pop(in_files):
901-
if isinstance(in_files, (list, tuple)):
902-
return in_files[0]
903-
return in_files
904-
905-
906901
def _select_labels(in_segm, labels):
907902
from os import getcwd
908903
import numpy as np

niworkflows/func/tests/test_util.py

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" Testing module for fmriprep.workflows.bold.util """
1+
"""Testing module for fmriprep.workflows.bold.util."""
22
import pytest
33
import os
44
from pathlib import Path
@@ -8,10 +8,41 @@
88
from nipype.utils.filemanip import fname_presuffix, copyfile
99
from nilearn.image import load_img
1010

11+
from ...utils.connections import listify
1112
from niworkflows.interfaces.masks import ROIsPlot
1213

1314
from ..util import init_bold_reference_wf
1415

16+
# Multi-echo datasets
17+
bold_datasets = ["""\
18+
ds000210/sub-06_task-rest_run-01_echo-1_bold.nii.gz
19+
ds000210/sub-06_task-rest_run-01_echo-2_bold.nii.gz
20+
ds000210/sub-06_task-rest_run-01_echo-3_bold.nii.gz\
21+
""".splitlines(), """\
22+
ds000216/sub-03_task-rest_echo-1_bold.nii.gz
23+
ds000216/sub-03_task-rest_echo-2_bold.nii.gz
24+
ds000216/sub-03_task-rest_echo-3_bold.nii.gz
25+
ds000216/sub-03_task-rest_echo-4_bold.nii.gz""".splitlines()]
26+
27+
# Single-echo datasets
28+
bold_datasets += """\
29+
ds000116/sub-12_task-visualoddballwithbuttonresponsetotargetstimuli_run-02_bold.nii.gz
30+
ds000133/sub-06_ses-post_task-rest_run-01_bold.nii.gz
31+
ds000140/sub-32_task-heatpainwithregulationandratings_run-02_bold.nii.gz
32+
ds000157/sub-23_task-passiveimageviewing_bold.nii.gz
33+
ds000237/sub-03_task-MemorySpan_acq-multiband_run-01_bold.nii.gz
34+
ds000237/sub-06_task-MemorySpan_acq-multiband_run-01_bold.nii.gz
35+
ds001240/sub-26_task-localizerimagination_bold.nii.gz
36+
ds001240/sub-26_task-localizerviewing_bold.nii.gz
37+
ds001240/sub-26_task-molencoding_run-01_bold.nii.gz
38+
ds001240/sub-26_task-molencoding_run-02_bold.nii.gz
39+
ds001240/sub-26_task-molretrieval_run-01_bold.nii.gz
40+
ds001240/sub-26_task-molretrieval_run-02_bold.nii.gz
41+
ds001240/sub-26_task-rest_bold.nii.gz
42+
ds001362/sub-01_task-taskname_run-01_bold.nii.gz""".splitlines()
43+
44+
bold_datasets = [listify(d) for d in bold_datasets]
45+
1546

1647
def symmetric_overlap(img1, img2):
1748
mask1 = load_img(img1).get_fdata() > 0
@@ -32,60 +63,42 @@ def symmetric_overlap(img1, img2):
3263
"input_fname,expected_fname",
3364
[
3465
(
35-
os.path.join(os.getenv("FMRIPREP_REGRESSION_SOURCE", ""), base_fname),
66+
[os.path.join(os.getenv("FMRIPREP_REGRESSION_SOURCE", ""), bf)
67+
for bf in base_fname],
3668
fname_presuffix(
37-
base_fname,
69+
base_fname[0].replace("_echo-1", ""),
3870
suffix="_mask",
3971
use_ext=True,
4072
newpath=os.path.join(
4173
os.getenv("FMRIPREP_REGRESSION_TARGETS", ""),
42-
os.path.dirname(base_fname),
74+
os.path.dirname(base_fname[0]),
4375
),
4476
),
4577
)
46-
for base_fname in """\
47-
ds000116/sub-12_task-visualoddballwithbuttonresponsetotargetstimuli_run-02_bold.nii.gz
48-
ds000133/sub-06_ses-post_task-rest_run-01_bold.nii.gz
49-
ds000140/sub-32_task-heatpainwithregulationandratings_run-02_bold.nii.gz
50-
ds000157/sub-23_task-passiveimageviewing_bold.nii.gz
51-
ds000210/sub-06_task-rest_run-01_echo-1_bold.nii.gz
52-
ds000210/sub-06_task-rest_run-01_echo-2_bold.nii.gz
53-
ds000210/sub-06_task-rest_run-01_echo-3_bold.nii.gz
54-
ds000216/sub-03_task-rest_echo-1_bold.nii.gz
55-
ds000216/sub-03_task-rest_echo-2_bold.nii.gz
56-
ds000216/sub-03_task-rest_echo-3_bold.nii.gz
57-
ds000216/sub-03_task-rest_echo-4_bold.nii.gz
58-
ds000237/sub-03_task-MemorySpan_acq-multiband_run-01_bold.nii.gz
59-
ds000237/sub-06_task-MemorySpan_acq-multiband_run-01_bold.nii.gz
60-
ds001240/sub-26_task-localizerimagination_bold.nii.gz
61-
ds001240/sub-26_task-localizerviewing_bold.nii.gz
62-
ds001240/sub-26_task-molencoding_run-01_bold.nii.gz
63-
ds001240/sub-26_task-molencoding_run-02_bold.nii.gz
64-
ds001240/sub-26_task-molretrieval_run-01_bold.nii.gz
65-
ds001240/sub-26_task-molretrieval_run-02_bold.nii.gz
66-
ds001240/sub-26_task-rest_bold.nii.gz
67-
ds001362/sub-01_task-taskname_run-01_bold.nii.gz""".splitlines()
78+
for base_fname in bold_datasets
6879
],
6980
)
7081
def test_masking(input_fname, expected_fname):
71-
basename = Path(input_fname).name
82+
basename = Path(input_fname[0]).name
7283
dsname = Path(expected_fname).parent.name
7384

7485
# Reconstruct base_fname from above
7586
reports_dir = Path(os.getenv("FMRIPREP_REGRESSION_REPORTS", ""))
7687
newpath = reports_dir / dsname
7788

7889
name = basename.rstrip("_bold.nii.gz").replace("-", "_")
79-
bold_reference_wf = init_bold_reference_wf(omp_nthreads=1, name=name)
80-
bold_reference_wf.inputs.inputnode.bold_file = input_fname
90+
bold_reference_wf = init_bold_reference_wf(omp_nthreads=1, name=name,
91+
multiecho=len(input_fname) > 1)
92+
bold_reference_wf.inputs.inputnode.bold_file = input_fname[0] if len(input_fname) == 1 \
93+
else input_fname
8194
base_dir = os.getenv("CACHED_WORK_DIRECTORY")
8295
if base_dir:
8396
base_dir = Path(base_dir) / dsname
8497
base_dir.mkdir(parents=True, exist_ok=True)
8598
bold_reference_wf.base_dir = str(base_dir)
8699

87100
out_fname = fname_presuffix(
88-
basename, suffix="_mask.svg", use_ext=False, newpath=str(newpath)
101+
Path(expected_fname).name, suffix=".svg", use_ext=False, newpath=str(newpath)
89102
)
90103
newpath.mkdir(parents=True, exist_ok=True)
91104

@@ -117,7 +130,7 @@ def test_masking(input_fname, expected_fname):
117130
mask_dir.mkdir(parents=True, exist_ok=True)
118131
copyfile(
119132
combine_masks.result.outputs.out_file,
120-
fname_presuffix(basename, suffix="_mask", use_ext=True, newpath=str(mask_dir)),
133+
str(mask_dir / Path(expected_fname).name),
121134
copy=True,
122135
)
123136

niworkflows/func/util.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ..interfaces.masks import SimpleShowMaskRPT
2121
from ..interfaces.registration import EstimateReferenceImage
2222
from ..interfaces.utils import CopyXForm
23+
from ..utils.connections import listify
2324
from ..utils.misc import pass_dummy_scans as _pass_dummy_scans
2425

2526

@@ -29,8 +30,10 @@
2930
def init_bold_reference_wf(
3031
omp_nthreads,
3132
bold_file=None,
33+
sbref_files=None,
3234
brainmask_thresh=0.85,
3335
pre_mask=False,
36+
multiecho=False,
3437
name="bold_reference_wf",
3538
gen_report=False,
3639
):
@@ -51,19 +54,26 @@ def init_bold_reference_wf(
5154
5255
Parameters
5356
----------
54-
omp_nthreads : int
57+
omp_nthreads : :obj:`int`
5558
Maximum number of threads an individual process may use
56-
bold_file : str
59+
bold_file : :obj:`str`
5760
BOLD series NIfTI file
61+
sbref_files : :obj:`list` or :obj:`bool`
62+
Single band (as opposed to multi band) reference NIfTI file.
63+
If ``True`` is passed, the workflow is built to accommodate SBRefs,
64+
but the input is left undefined (i.e., it is left open for connection)
5865
brainmask_thresh: :obj:`float`
5966
Lower threshold for the probabilistic brainmask to obtain
6067
the final binary mask (default: 0.85).
61-
pre_mask : bool
68+
pre_mask : :obj:`bool`
6269
Indicates whether the ``pre_mask`` input will be set (and thus, step 1
6370
should be skipped).
64-
name : str
71+
multiecho : :obj:`bool`
72+
If multiecho data was supplied, data from the first echo
73+
will be selected
74+
name : :obj:`str`
6575
Name of workflow (default: ``bold_reference_wf``)
66-
gen_report : bool
76+
gen_report : :obj:`bool`
6777
Whether a mask report node should be appended in the end
6878
6979
Inputs
@@ -104,10 +114,12 @@ def init_bold_reference_wf(
104114
105115
"""
106116
workflow = Workflow(name=name)
107-
workflow.__desc__ = """\
117+
workflow.__desc__ = f"""\
108118
First, a reference volume and its skull-stripped version were generated
109-
using a custom methodology of *fMRIPrep*.
119+
{'from the shortest echo of the BOLD run' * multiecho} using a custom
120+
methodology of *fMRIPrep*.
110121
"""
122+
111123
inputnode = pe.Node(
112124
niu.IdentityInterface(
113125
fields=["bold_file", "bold_mask", "dummy_scans", "sbref_file"]
@@ -125,6 +137,7 @@ def init_bold_reference_wf(
125137
"ref_image_brain",
126138
"bold_mask",
127139
"validation_report",
140+
"mask_report",
128141
]
129142
),
130143
name="outputnode",
@@ -134,10 +147,15 @@ def init_bold_reference_wf(
134147
if bold_file is not None:
135148
inputnode.inputs.bold_file = bold_file
136149

137-
validate = pe.Node(ValidateImage(), name="validate", mem_gb=DEFAULT_MEMORY_MIN_GB)
150+
val_bold = pe.MapNode(
151+
ValidateImage(),
152+
name="val_bold",
153+
mem_gb=DEFAULT_MEMORY_MIN_GB,
154+
iterfield=["in_file"],
155+
)
138156

139157
gen_ref = pe.Node(
140-
EstimateReferenceImage(), name="gen_ref", mem_gb=1
158+
EstimateReferenceImage(multiecho=multiecho), name="gen_ref", mem_gb=1
141159
) # OE: 128x128x128x50 * 64 / 8 ~ 900MB.
142160
enhance_and_skullstrip_bold_wf = init_enhance_and_skullstrip_bold_wf(
143161
brainmask_thresh=brainmask_thresh,
@@ -151,23 +169,23 @@ def init_bold_reference_wf(
151169
run_without_submitting=True,
152170
mem_gb=DEFAULT_MEMORY_MIN_GB,
153171
)
172+
bold_1st = pe.Node(niu.Select(index=[0]),
173+
name="bold_1st", run_without_submitting=True)
174+
validate_1st = pe.Node(niu.Select(index=[0]),
175+
name="validate_1st", run_without_submitting=True)
154176

155177
# fmt: off
156178
workflow.connect([
179+
(inputnode, val_bold, [(("bold_file", listify), "in_file")]),
157180
(inputnode, enhance_and_skullstrip_bold_wf, [
158181
("bold_mask", "inputnode.pre_mask"),
159182
]),
160-
(inputnode, validate, [("bold_file", "in_file")]),
161-
(inputnode, gen_ref, [("sbref_file", "sbref_file")]),
162183
(inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]),
163-
(validate, gen_ref, [("out_file", "in_file")]),
184+
(val_bold, gen_ref, [("out_file", "in_file")]),
164185
(gen_ref, enhance_and_skullstrip_bold_wf, [
165186
("ref_image", "inputnode.in_file"),
166187
]),
167-
(validate, outputnode, [
168-
("out_file", "bold_file"),
169-
("out_report", "validation_report"),
170-
]),
188+
(val_bold, bold_1st, [(("out_file", listify), "inlist")]),
171189
(gen_ref, calc_dummy_scans, [("n_volumes_to_discard", "algo_dummy_scans")]),
172190
(calc_dummy_scans, outputnode, [("skip_vols_num", "skip_vols")]),
173191
(gen_ref, outputnode, [
@@ -179,9 +197,39 @@ def init_bold_reference_wf(
179197
("outputnode.mask_file", "bold_mask"),
180198
("outputnode.skull_stripped_file", "ref_image_brain"),
181199
]),
200+
(val_bold, validate_1st, [(("out_report", listify), "inlist")]),
201+
(bold_1st, outputnode, [("out", "bold_file")]),
202+
(validate_1st, outputnode, [("out", "validation_report")]),
182203
])
183204
# fmt: on
184205

206+
if sbref_files:
207+
nsbrefs = 0
208+
if sbref_files is not True:
209+
# If not boolean, then it is a list-of or pathlike.
210+
inputnode.inputs.sbref_file = sbref_files
211+
nsbrefs = 1 if isinstance(sbref_files, str) else len(sbref_files)
212+
213+
val_sbref = pe.MapNode(
214+
ValidateImage(),
215+
name="val_sbref",
216+
mem_gb=DEFAULT_MEMORY_MIN_GB,
217+
iterfield=["in_file"],
218+
)
219+
# fmt: off
220+
workflow.connect([
221+
(inputnode, val_sbref, [(("sbref_file", listify), "in_file")]),
222+
(val_sbref, gen_ref, [("out_file", "sbref_file")]),
223+
])
224+
# fmt: on
225+
226+
# Edit the boilerplate as the SBRef will be the reference
227+
workflow.__desc__ = f"""\
228+
First, a reference volume and its skull-stripped version were generated
229+
by aligning and averaging{' the first echo of' * multiecho}
230+
{nsbrefs or ''} single-band references (SBRefs).
231+
"""
232+
185233
if gen_report:
186234
mask_reportlet = pe.Node(SimpleShowMaskRPT(), name="mask_reportlet")
187235
# fmt: off

0 commit comments

Comments
 (0)