Skip to content

Commit

Permalink
enh: large overhaul of the functional workflow
Browse files Browse the repository at this point in the history
This commit initiates the refactor to better handle me-epi.
  • Loading branch information
oesteban committed Nov 15, 2023
1 parent 309a54c commit 1afe856
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 231 deletions.
17 changes: 10 additions & 7 deletions mriqc/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def parse_args(args=None, namespace=None):
config.execution.participant_label,
session_id=config.execution.session_id,
task=config.execution.task_id,
group_echos=False,
group_echos=True,
bids_filters=config.execution.bids_filters,
queries={mod: DEFAULT_BIDS_QUERIES[mod] for mod in lc_modalities}
)
Expand Down Expand Up @@ -575,7 +575,7 @@ def parse_args(args=None, namespace=None):
# Estimate the biggest file size / leave 1GB if some file does not exist (datalad)
with suppress(FileNotFoundError):
config.workflow.biggest_file_gb = _get_biggest_file_size_gb(
[i for sublist in config.workflow.inputs.values() for i in sublist]
config.workflow.inputs.values()
)

# set specifics for alternative populations
Expand All @@ -593,11 +593,14 @@ def parse_args(args=None, namespace=None):


def _get_biggest_file_size_gb(files):
"""Identify the largest file size (allows multi-echo groups)."""

import os

max_size = 0
sizes = []
for file in files:
size = os.path.getsize(file) / (1024**3)
if size > max_size:
max_size = size
return max_size
if isinstance(file, (list, tuple)):
sizes.append(_get_biggest_file_size_gb(file))
else:
sizes.append(os.path.getsize(file))
return max(sizes) / (1024**3)
98 changes: 98 additions & 0 deletions mriqc/utils/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# https://www.nipreps.org/community/licensing/
#
"""PyBIDS tooling."""
from __future__ import annotations

import json
import os
from pathlib import Path
Expand Down Expand Up @@ -90,3 +92,99 @@ def write_derivative_description(bids_dir, deriv_dir):
desc["License"] = orig_desc["License"]

Path.write_text(deriv_dir / "dataset_description.json", json.dumps(desc, indent=4))


def derive_bids_fname(
orig_path: str | Path,
entity: str | None = None,
newsuffix: str | None = None,
newpath: str | Path | None = None,
newext: str | None = None,
position: int = -1,
absolute: bool = True,
) -> Path | str:
"""
Derive a new file name from a BIDS-formatted path.
Parameters
----------
orig_path : :obj:`str` or :obj:`os.pathlike`
A filename (may or may not include path).
entity : :obj:`str`, optional
A new BIDS-like key-value pair.
newsuffix : :obj:`str`, optional
Replace the BIDS suffix.
newpath : :obj:`str` or :obj:`os.pathlike`, optional
Path to replace the path of the input orig_path.
newext : :obj:`str`, optional
Replace the extension of the file.
position : :obj:`int`, optional
Position to insert the entity in the filename.
absolute : :obj:`bool`, optional
If True (default), returns the absolute path of the modified filename.
Returns
-------
Absolute path of the modified filename
Examples
--------
>>> derive_bids_fname(
... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz',
... entity='desc-preproc',
... absolute=False,
... )
PosixPath('sub-001/ses-01/anat/sub-001_ses-01_desc-preproc_T1w.nii.gz')
>>> derive_bids_fname(
... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz',
... entity='desc-brain',
... newsuffix='mask',
... newext=".nii",
... absolute=False,
... ) # doctest: +ELLIPSIS
PosixPath('sub-001/ses-01/anat/sub-001_ses-01_desc-brain_mask.nii')
>>> derive_bids_fname(
... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz',
... entity='desc-brain',
... newsuffix='mask',
... newext=".nii",
... newpath="/output/node",
... absolute=True,
... ) # doctest: +ELLIPSIS
PosixPath('/output/node/sub-001_ses-01_desc-brain_mask.nii')
>>> derive_bids_fname(
... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz',
... entity='desc-brain',
... newsuffix='mask',
... newext=".nii",
... newpath=".",
... absolute=False,
... ) # doctest: +ELLIPSIS
PosixPath('sub-001_ses-01_desc-brain_mask.nii')
"""

orig_path = Path(orig_path)
newpath = orig_path.parent if newpath is None else Path(newpath)

ext = "".join(orig_path.suffixes)
newext = newext if newext is not None else ext
orig_stem = orig_path.name.replace(ext, "")

suffix = orig_stem.rsplit("_", maxsplit=1)[-1].strip("_")
newsuffix = newsuffix.strip("_") if newsuffix is not None else suffix

orig_stem = orig_stem.replace(suffix, "").strip("_")
bidts = [bit for bit in orig_stem.split("_") if bit]
if entity:
if position == -1:
bidts.append(entity)
else:
bidts.insert(position, entity.strip("_"))

retval = (newpath / f"{'_'.join(bidts)}_{newsuffix}.{newext.strip('.')}")

return retval.absolute() if absolute else retval
96 changes: 1 addition & 95 deletions mriqc/workflows/anatomical/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def anat_qc_workflow(name="anatMRIQC"):
wf = anat_qc_workflow()
"""
from mriqc.workflows.shared import synthstrip_wf

dataset = config.workflow.inputs.get("t1w", []) + config.workflow.inputs.get("t2w", [])

Expand Down Expand Up @@ -682,101 +683,6 @@ def airmsk_wf(name="AirMaskWorkflow"):
return workflow


def synthstrip_wf(name="synthstrip_wf", omp_nthreads=None):
"""Create a brain-extraction workflow using SynthStrip."""
from nipype.interfaces.ants import N4BiasFieldCorrection
from niworkflows.interfaces.nibabel import IntensityClip, ApplyMask
from mriqc.interfaces.synthstrip import SynthStrip

inputnode = pe.Node(niu.IdentityInterface(fields=["in_files"]), name="inputnode")
outputnode = pe.Node(
niu.IdentityInterface(fields=["out_corrected", "out_brain", "bias_image", "out_mask"]),
name="outputnode",
)

# truncate target intensity for N4 correction
pre_clip = pe.Node(IntensityClip(p_min=10, p_max=99.9), name="pre_clip")

pre_n4 = pe.Node(
N4BiasFieldCorrection(
dimension=3,
num_threads=omp_nthreads,
rescale_intensities=True,
copy_header=True,
),
name="pre_n4",
)

post_n4 = pe.Node(
N4BiasFieldCorrection(
dimension=3,
save_bias=True,
num_threads=omp_nthreads,
n_iterations=[50] * 4,
copy_header=True,
),
name="post_n4",
)

synthstrip = pe.Node(
SynthStrip(num_threads=omp_nthreads),
name="synthstrip",
num_threads=omp_nthreads,
)

final_masked = pe.Node(ApplyMask(), name="final_masked")
final_inu = pe.Node(niu.Function(function=_apply_bias_correction), name="final_inu")

workflow = pe.Workflow(name=name)
# fmt: off
workflow.connect([
(inputnode, final_inu, [("in_files", "in_file")]),
(inputnode, pre_clip, [("in_files", "in_file")]),
(pre_clip, pre_n4, [("out_file", "input_image")]),
(pre_n4, synthstrip, [("output_image", "in_file")]),
(synthstrip, post_n4, [("out_mask", "weight_image")]),
(synthstrip, final_masked, [("out_mask", "in_mask")]),
(pre_clip, post_n4, [("out_file", "input_image")]),
(post_n4, final_inu, [("bias_image", "bias_image")]),
(post_n4, final_masked, [("output_image", "in_file")]),
(final_masked, outputnode, [("out_file", "out_brain")]),
(post_n4, outputnode, [("bias_image", "bias_image")]),
(synthstrip, outputnode, [("out_mask", "out_mask")]),
(post_n4, outputnode, [("output_image", "out_corrected")]),
])
# fmt: on
return workflow


def _apply_bias_correction(in_file, bias_image, out_file=None):
import os.path as op

import numpy as np
import nibabel as nb

img = nb.load(in_file)
data = np.clip(
img.get_fdata() * nb.load(bias_image).get_fdata(),
a_min=0,
a_max=None,
)
out_img = img.__class__(
data.astype(img.get_data_dtype()),
img.affine,
img.header,
)

if out_file is None:
fname, ext = op.splitext(op.basename(in_file))
if ext == ".gz":
fname, ext2 = op.splitext(fname)
ext = ext2 + ext
out_file = op.abspath(f"{fname}_inu{ext}")

out_img.to_filename(out_file)
return out_file


def _binarize(in_file, threshold=0.5, out_file=None):
import os.path as op

Expand Down
65 changes: 1 addition & 64 deletions mriqc/workflows/diffusion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def dmri_qc_workflow(name="dwiMRIQC"):
ReadDWIMetadata,
WeightedStat,
)
from mriqc.workflows.shared import synthstrip_wf as dmri_bmsk_workflow
from mriqc.messages import BUILDING_WORKFLOW

workflow = pe.Workflow(name=name)
Expand Down Expand Up @@ -349,70 +350,6 @@ def compute_iqms(name="ComputeIQMs"):
return workflow


def dmri_bmsk_workflow(name="dmri_brainmask", omp_nthreads=None):
"""
Compute a brain mask for the input :abbr:`dMRI (diffusion MRI)` dataset.
.. workflow::
from mriqc.workflows.diffusion.base import dmri_bmsk_workflow
from mriqc.testing import mock_config
with mock_config():
wf = dmri_bmsk_workflow()
"""

from nipype.interfaces.ants import N4BiasFieldCorrection
from niworkflows.interfaces.nibabel import ApplyMask
from mriqc.interfaces.synthstrip import SynthStrip
from mriqc.workflows.anatomical.base import _apply_bias_correction

inputnode = pe.Node(niu.IdentityInterface(fields=["in_file"]), name="inputnode")
outputnode = pe.Node(
niu.IdentityInterface(fields=["out_corrected", "out_brain", "bias_image", "out_mask"]),
name="outputnode",
)

post_n4 = pe.Node(
N4BiasFieldCorrection(
dimension=3,
save_bias=True,
num_threads=omp_nthreads,
n_iterations=[50] * 4,
copy_header=True,
),
name="post_n4",
)

synthstrip = pe.Node(
SynthStrip(num_threads=omp_nthreads),
name="synthstrip",
num_threads=omp_nthreads,
)

final_masked = pe.Node(ApplyMask(), name="final_masked")
final_inu = pe.Node(niu.Function(function=_apply_bias_correction), name="final_inu")

workflow = pe.Workflow(name=name)
# fmt: off
workflow.connect([
(inputnode, final_inu, [("in_file", "in_file")]),
(inputnode, synthstrip, [("in_file", "in_file")]),
(inputnode, post_n4, [("in_file", "input_image")]),
(synthstrip, post_n4, [("out_mask", "weight_image")]),
(synthstrip, final_masked, [("out_mask", "in_mask")]),
(post_n4, final_inu, [("bias_image", "bias_image")]),
(post_n4, final_masked, [("output_image", "in_file")]),
(final_masked, outputnode, [("out_file", "out_brain")]),
(post_n4, outputnode, [("bias_image", "bias_image")]),
(synthstrip, outputnode, [("out_mask", "out_mask")]),
(post_n4, outputnode, [("output_image", "out_corrected")]),
])
# fmt: on
return workflow


def init_dmriref_wf(
in_file=None,
name="init_dmriref_wf",
Expand Down
Loading

0 comments on commit 1afe856

Please sign in to comment.