Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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

Expand Down Expand Up @@ -284,7 +284,7 @@ jobs:

- restore_cache:
keys:
- regression-v4-{{ .Revision }}
- regression-v5-{{ .Revision }}
- restore_cache:
keys:
- masks-workdir-v2-{{ .Branch }}-{{epoch}}
Expand Down
8 changes: 1 addition & 7 deletions niworkflows/anat/ants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
76 changes: 44 additions & 32 deletions niworkflows/func/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -32,60 +62,42 @@ 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
reports_dir = Path(os.getenv("FMRIPREP_REGRESSION_REPORTS", ""))
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
base_dir.mkdir(parents=True, exist_ok=True)
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)

Expand Down Expand Up @@ -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,
)

Expand Down
80 changes: 64 additions & 16 deletions niworkflows/func/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -29,8 +30,10 @@
def init_bold_reference_wf(
omp_nthreads,
bold_file=None,
sbref_files=None,
brainmask_thresh=0.85,
pre_mask=False,
multiecho=False,
name="bold_reference_wf",
gen_report=False,
):
Expand All @@ -51,19 +54,26 @@ 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
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).
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
Expand Down Expand Up @@ -104,10 +114,12 @@ def init_bold_reference_wf(

"""
workflow = Workflow(name=name)
workflow.__desc__ = """\
workflow.__desc__ = f"""\
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"]
Expand All @@ -125,6 +137,7 @@ def init_bold_reference_wf(
"ref_image_brain",
"bold_mask",
"validation_report",
"mask_report",
]
),
name="outputnode",
Expand All @@ -134,10 +147,15 @@ 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)
val_bold = pe.MapNode(
ValidateImage(),
name="val_bold",
mem_gb=DEFAULT_MEMORY_MIN_GB,
iterfield=["in_file"],
)

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,
Expand All @@ -151,23 +169,23 @@ def init_bold_reference_wf(
run_without_submitting=True,
mem_gb=DEFAULT_MEMORY_MIN_GB,
)
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, val_bold, [(("bold_file", ensure_list), "in_file")]),
(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, 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, outputnode, [
("out_file", "bold_file"),
("out_report", "validation_report"),
]),
(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, [
Expand All @@ -179,9 +197,39 @@ 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")]),
(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
Expand Down
Loading