Skip to content

Commit

Permalink
[CI] Swap synthstrip for bet when sloppy mode is chosen (#817)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcieslak authored Sep 2, 2024
1 parent 634483f commit aa43d3b
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 126 deletions.
196 changes: 110 additions & 86 deletions .circleci/config.yml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions qsiprep/data/intramodal_ACPC_sloppy.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"write_composite_transform": false,
"use_histogram_matching": [true, true],
"transforms": [ "Similarity", "Affine"],
"number_of_iterations": [ [ 100, 100 ], [ 10, 10 ] ],
"number_of_iterations": [ [ 10, 10 ], [ 10, 10 ] ],
"output_warped_image": true,
"transform_parameters": [ [ 0.4 ], [ 0.2 ] ],
"convergence_threshold": [ 1e-6, 1e-6 ],
"convergence_window_size": [ 10, 10 ],
"convergence_window_size": [ 4, 4 ],
"metric": [ "MI", "MI" ],
"sampling_percentage": [ 0.3, 0.1 ],
"sampling_percentage": [ 0.2, 0.05 ],
"sampling_strategy": [ "Regular", "Regular" ],
"shrink_factors": [ [ 8, 4 ], [ 4, 2 ] ],
"sigma_units": [ "vox", "vox" ],
Expand Down
44 changes: 24 additions & 20 deletions qsiprep/interfaces/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"""
import shutil
from pathlib import Path

import nibabel as nb
import nilearn.image as nim
Expand Down Expand Up @@ -220,28 +222,30 @@ def _run_interface(self, runtime):
LOGGER.info("Using T1w modality template for ACPC alignment")
anatomical_contrast = "T1w"

template_file = str(
get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc=None,
suffix=anatomical_contrast,
extension=".nii.gz",
),
template_path = get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc=None,
suffix=anatomical_contrast,
extension=".nii.gz",
)
mask_file = str(
get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc="brain",
suffix="mask",
extension=".nii.gz",
),
mask_path = get_template(
self.inputs.template_name,
cohort=[None, "2"],
resolution="1",
desc="brain",
suffix="mask",
extension=".nii.gz",
)

self._results["template_file"] = template_file
self._results["mask_file"] = mask_file
local_template = Path(runtime.cwd) / template_path.name
local_mask = Path(runtime.cwd) / mask_path.name

shutil.copy(template_path, local_template)
shutil.copy(mask_path, local_mask)

self._results["template_file"] = str(local_template)
self._results["mask_file"] = str(local_mask)

return runtime
55 changes: 55 additions & 0 deletions qsiprep/interfaces/freesurfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from nipype.interfaces.freesurfer.utils import LTAConvert
from nipype.utils.filemanip import copyfile, filename_to_list, fname_presuffix
from niworkflows.utils.images import _copyxform
from scipy import ndimage
from scipy.ndimage.morphology import binary_fill_holes
from skimage import morphology as sim

Expand Down Expand Up @@ -573,6 +574,25 @@ def _run_interface(self, runtime, correct_return_codes=(0,)):
return runtime


class MockSynthStrip(SimpleInterface):
input_spec = _SynthStripInputSpec
output_spec = _SynthStripOutputSpec

def _run_interface(self, runtime):
from nipype.interfaces.fsl import BET

this_bet = BET(
mask=True,
in_file=self.inputs.input_image,
output_type="NIFTI_GZ",
)
result = this_bet.run()
self._results["out_brain"] = result.outputs.out_file
self._results["out_brain_mask"] = result.outputs.mask_file

return runtime


class _SynthSegInputSpec(FSTraitedSpecOpenMP):
input_image = File(argstr="--i %s", exists=True, mandatory=True)
num_threads = traits.Int(
Expand Down Expand Up @@ -626,3 +646,38 @@ def _format_arg(self, name, trait_spec, value):
def _num_threads_update(self):
if self.inputs.num_threads:
self.inputs.environ.update({"OMP_NUM_THREADS": "1"})


class MockSynthSeg(SimpleInterface):
"""A fake version of synthseg for testing."""

input_spec = _SynthSegInputSpec
output_spec = _SynthSegOutputSpec

def _run_interface(self, runtime):
from nipype.interfaces.fsl import BET

output_qc = op.join(runtime.cwd, "fake_synthseg_qc.csv")
with open(output_qc, "w") as qcf:
qcf.write("Test QC file\n")

# Get a brain mask
this_bet = BET(
mask=True,
in_file=self.inputs.input_image,
output_type="NIFTI_GZ",
)
result = this_bet.run()
self._results["out_post"] = result.outputs.out_file

# Make a fake segmentation
img = nb.load(result.outputs.mask_file)
orig_mask = img.get_fdata() > 0
eroded1 = ndimage.binary_erosion(orig_mask, iterations=3)
eroded2 = ndimage.binary_erosion(eroded1, iterations=3)
final = orig_mask.astype(int) + eroded1 + eroded2
out_img = nb.Nifti1Image(final, img.affine, header=img.header)
out_fname = fname_presuffix(self.inputs.input_image, suffix="_dseg", newpath=runtime.cwd)
out_img.to_filename(out_fname)
self._results["out_seg"] = out_fname
return runtime
7 changes: 6 additions & 1 deletion qsiprep/interfaces/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,10 +477,11 @@ def _run_interface(self, runtime):
class _ChooseInterpolatorInputSpec(BaseInterfaceInputSpec):
dwi_files = InputMultiObject(File(exists=True), mandatory=True)
output_resolution = traits.Float(mandatory=True)
sloppy = traits.Bool(False, usedefault=True)


class _ChooseInterpolatorOutputSpec(TraitedSpec):
interpolation_method = traits.Enum("LanczosWindowedSinc", "BSpline")
interpolation_method = traits.Enum("LanczosWindowedSinc", "BSpline", "NearestNeighbor")


class ChooseInterpolator(SimpleInterface):
Expand All @@ -490,6 +491,10 @@ class ChooseInterpolator(SimpleInterface):
output_spec = _ChooseInterpolatorOutputSpec

def _run_interface(self, runtime):
if self.inputs.sloppy:
self._results["interpolation_method"] = "NearestNeighbor"
LOGGER.warning("Using NN interpolation for sloppy mode")
return runtime
output_resolution = np.array([self.inputs.output_resolution] * 3)
interpolator = "LanczosWindowedSinc"
for input_file in self.inputs.dwi_files:
Expand Down
2 changes: 1 addition & 1 deletion qsiprep/interfaces/tortoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

SLOPPY_DRBUDDI = (
"--DRBUDDI_stage "
"\[learning_rate=\{0.3\},cfs=\{100:8:4\},field_smoothing=\{9:0\},"
"\[learning_rate=\{0.4\},cfs=\{4:2:1\},field_smoothing=\{9:0\},"
"metrics=\{MSJac:CC\},restrict_constrain=\{1:1\}\] "
)

Expand Down
8 changes: 8 additions & 0 deletions qsiprep/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def test_dscsdsi_fmap(data_dir, output_dir, working_dir):
f"-w={work_dir}",
"--boilerplate",
"--sloppy",
"--b0-motion-corr-to=first",
"--write-graph",
"--mem_mb=4096",
"--output-resolution=5",
Expand Down Expand Up @@ -185,6 +186,7 @@ def test_drbuddi_rpe(data_dir, output_dir, working_dir):
"--sloppy",
"--anat-modality=none",
"--denoise-method=none",
"--b0-motion-corr-to=first",
"--b1_biascorrect_stage=none",
"--pepolar-method=DRBUDDI",
f"--eddy_config={eddy_config}",
Expand Down Expand Up @@ -220,6 +222,7 @@ def test_drbuddi_shoreline_epi(data_dir, output_dir, working_dir):
"--sloppy",
"--anat-modality=none",
"--denoise-method=none",
"--b0-motion-corr-to=first",
"--b1-biascorrect-stage=none",
"--pepolar-method=DRBUDDI",
"--hmc-model=none",
Expand Down Expand Up @@ -256,6 +259,7 @@ def test_drbuddi_tensorline_epi(data_dir, output_dir, working_dir):
"--sloppy",
"--anat-modality=none",
"--denoise-method=none",
"--b0-motion-corr-to=first",
"--b1-biascorrect-stage=none",
"--pepolar-method=DRBUDDI",
"--hmc-model=tensor",
Expand Down Expand Up @@ -497,6 +501,8 @@ def test_maternal_brain_project(data_dir, output_dir, working_dir):
"participant",
f"-w={work_dir}",
"--sloppy",
"--denoise-method=none",
"--b1-biascorrect-stage=none",
"--write-graph",
"--output-resolution=5",
"--hmc-model=3dSHORE",
Expand Down Expand Up @@ -532,6 +538,8 @@ def test_forrest_gump(data_dir, output_dir, working_dir):
"participant",
f"-w={work_dir}",
"--sloppy",
"--denoise-method=none",
"--b1-biascorrect-stage=none",
"--write-graph",
"--output-resolution=5",
f"--bids-filter-file={bids_filter}",
Expand Down
36 changes: 26 additions & 10 deletions qsiprep/workflows/anatomical/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
from ...interfaces.anatomical import DesaturateSkull, GetTemplate, VoxelSizeChooser
from ...interfaces.freesurfer import (
FixHeaderSynthStrip,
MockSynthSeg,
MockSynthStrip,
PrepareSynthStripGrid,
SynthSeg,
)
Expand Down Expand Up @@ -911,11 +913,18 @@ def init_synthstrip_wf(do_padding=False, unfatsat=False, name="synthstrip_wf") -
name="outputnode",
)

synthstrip = pe.Node(
FixHeaderSynthStrip(), # Threads are always fixed to 1 in the run
name="synthstrip",
n_procs=config.nipype.omp_nthreads,
)
if not config.execution.sloppy:
synthstrip = pe.Node(
FixHeaderSynthStrip(), # Threads are always fixed to 1 in the run
name="synthstrip",
n_procs=config.nipype.omp_nthreads,
)
else:
synthstrip = pe.Node(
MockSynthStrip(),
name="mocksynthstrip",
)

mask_to_original_grid = pe.Node(
ants.ApplyTransforms(
dimension=3, transforms=["identity"], interpolation="NearestNeighbor"
Expand Down Expand Up @@ -969,11 +978,18 @@ def init_synthseg_wf() -> Workflow:
name="outputnode",
)

synthseg = pe.Node(
SynthSeg(fast=config.execution.sloppy, num_threads=1), # Hard code to 1
n_procs=config.nipype.omp_nthreads,
name="synthseg",
)
if not config.execution.sloppy:
synthseg = pe.Node(
SynthSeg(fast=config.execution.sloppy, num_threads=1), # Hard code to 1
n_procs=config.nipype.omp_nthreads,
name="synthseg",
)
else:
synthseg = pe.Node(
MockSynthSeg(fast=config.execution.sloppy, num_threads=1), # Hard code to 1
n_procs=config.nipype.omp_nthreads,
name="mocksynthseg",
)

workflow.connect([
(inputnode, synthseg, [('padded_image', 'input_image')]),
Expand Down
11 changes: 8 additions & 3 deletions qsiprep/workflows/dwi/hmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,12 @@ def init_dwi_hmc_wf(

# Warp the modeled images into non-motion-corrected space
uncorrect_model_images = pe.MapNode(
ants.ApplyTransforms(invert_transform_flags=[True], interpolation="LanczosWindowedSinc"),
ants.ApplyTransforms(
invert_transform_flags=[True],
interpolation=(
"LanczosWindowedSinc" if not config.execution.sloppy else "NearestNeighbor"
),
),
iterfield=["input_image", "reference_image", "transforms"],
name="uncorrect_model_images",
)
Expand Down Expand Up @@ -238,7 +243,7 @@ def linear_alignment_workflow(transform="Rigid", iternum=0, omp_nthreads=1):
),
name="outputnode",
)
precision = "coarse" if config.execution.sloppy else "precise"
precision = "sloppy" if config.execution.sloppy else "precise"
ants_settings = pkgrf(
"qsiprep",
"data/shoreline_{precision}_{transform}.json".format(
Expand Down Expand Up @@ -484,7 +489,7 @@ def init_hmc_model_iteration_wf(name="hmc_model_iter0"):
),
name="outputnode",
)
precision = "coarse" if config.execution.sloppy else "precise"
precision = "sloppy" if config.execution.sloppy else "precise"
ants_settings = pkgrf(
"qsiprep",
"data/shoreline_{precision}_{transform}.json".format(
Expand Down
7 changes: 6 additions & 1 deletion qsiprep/workflows/dwi/hmc_sdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ def init_qsiprep_hmcsdc_wf(

# apply the head motion correction transforms
apply_hmc_transforms = pe.MapNode(
ants.ApplyTransforms(dimension=3, interpolation="LanczosWindowedSinc"),
ants.ApplyTransforms(
dimension=3,
interpolation=(
"LanczosWindowedSinc" if not config.execution.sloppy else "NearestNeighbor"
),
),
iterfield=["input_image", "reference_image", "transforms"],
name="uncorrect_model_images",
)
Expand Down
3 changes: 2 additions & 1 deletion qsiprep/workflows/dwi/resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ def init_dwi_trans_wf(
# get composite warps and composed affines for warping and rotating
compose_transforms = pe.Node(ComposeTransforms(), name="compose_transforms")
get_interpolation = pe.Node(
ChooseInterpolator(output_resolution=output_resolution), name="get_interpolation"
ChooseInterpolator(sloppy=config.execution.sloppy, output_resolution=output_resolution),
name="get_interpolation",
)
dwi_transform = pe.MapNode(
ants.ApplyTransforms(float=True),
Expand Down

0 comments on commit aa43d3b

Please sign in to comment.