diff --git a/docs/requirements.txt b/docs/requirements.txt index 5e52cc136db..8031585b9f7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ git+https://github.com/AleksandarPetrov/napoleon.git@0dc3f28a309ad602be5f44a9049785a1026451b3#egg=sphinxcontrib-napoleon git+https://github.com/rwblair/sphinxcontrib-versioning.git@39b40b0b84bf872fc398feff05344051bbce0f63#egg=sphinxcontrib-versioning nbsphinx -nipype>=1.3.1 +nipype>=1.5.0 nitransforms >= 20.0.0rc3,<20.2 packaging pydot>=1.2.3 diff --git a/niworkflows/anat/ants.py b/niworkflows/anat/ants.py index 09ec9330246..21212a74662 100644 --- a/niworkflows/anat/ants.py +++ b/niworkflows/anat/ants.py @@ -12,25 +12,25 @@ # nipype from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu -from nipype.interfaces.fsl.maths import ApplyMask -from nipype.interfaces.ants import N4BiasFieldCorrection, Atropos, MultiplyImages +from nipype.interfaces.ants import ( + Atropos, + MultiplyImages, + N4BiasFieldCorrection, + ThresholdImage, +) +from nipype.interfaces.ants.utils import AI from ..utils.misc import get_template_specs from ..utils.connections import pop_file as _pop # niworkflows -from ..interfaces.ants import ( - ImageMath, - ResampleImageBySpacing, - AI, - ThresholdImage, -) +from ..interfaces.ants import ImageMath from ..interfaces.fixes import ( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, ) -from ..interfaces.utils import CopyXForm -from ..interfaces.nibabel import Binarize +from ..interfaces.images import RegridToZooms +from ..interfaces.nibabel import ApplyMask, Binarize ATROPOS_MODELS = { @@ -84,7 +84,7 @@ def init_brain_extraction_wf( Parameters ---------- in_template : str - Name of the skull-stripping template ('OASIS30ANTs', 'NKI', or + Name of the skull-stripping template ("OASIS30ANTs", "NKI", or path). The brain template from which regions will be projected Anatomical template created using e.g. LPBA40 data set with @@ -201,12 +201,6 @@ def init_brain_extraction_wf( name="outputnode", ) - copy_xform = pe.Node( - CopyXForm(fields=["out_file", "out_mask", "bias_corrected", "bias_image"]), - name="copy_xform", - run_without_submitting=True, - ) - trunc = pe.MapNode( ImageMath(operation="TruncateImageIntensity", op2="0.01 0.999 256"), name="truncate_images", @@ -227,15 +221,9 @@ def init_brain_extraction_wf( iterfield=["input_image"], ) - res_tmpl = pe.Node( - ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), - name="res_tmpl", - ) - res_tmpl.inputs.input_image = tpl_target_path - res_target = pe.Node( - ResampleImageBySpacing(out_spacing=(4, 4, 4), apply_smoothing=True), - name="res_target", - ) + res_tmpl = pe.Node(RegridToZooms(in_file=tpl_target_path, zooms=(4, 4, 4), smooth=True), + name="res_tmpl") + res_target = pe.Node(RegridToZooms(zooms=(4, 4, 4), smooth=True), name="res_target") lap_tmpl = pe.Node(ImageMath(operation="Laplacian", op2="1.5 1"), name="lap_tmpl") lap_tmpl.inputs.op1 = tpl_target_path @@ -338,15 +326,14 @@ def init_brain_extraction_wf( # fmt: off wf.connect([ (inputnode, trunc, [("in_files", "op1")]), - (inputnode, copy_xform, [(("in_files", _pop), "hdr_file")]), (inputnode, inu_n4_final, [("in_files", "input_image")]), (inputnode, init_aff, [("in_mask", "fixed_image_mask")]), (inputnode, norm, [("in_mask", fixed_mask_trait)]), (inputnode, map_brainmask, [(("in_files", _pop), "reference_image")]), (trunc, inu_n4, [("output_image", "input_image")]), - (inu_n4, res_target, [(("output_image", _pop), "input_image")]), - (res_tmpl, init_aff, [("output_image", "fixed_image")]), - (res_target, init_aff, [("output_image", "moving_image")]), + (inu_n4, res_target, [(("output_image", _pop), "in_file")]), + (res_tmpl, init_aff, [("out_file", "fixed_image")]), + (res_target, init_aff, [("out_file", "moving_image")]), (init_aff, norm, [("output_transform", "initial_moving_transform")]), (norm, map_brainmask, [ ("reverse_transforms", "transforms"), @@ -356,19 +343,11 @@ def init_brain_extraction_wf( (thr_brainmask, dil_brainmask, [("output_image", "op1")]), (dil_brainmask, get_brainmask, [("output_image", "op1")]), (inu_n4_final, apply_mask, [("output_image", "in_file")]), - (get_brainmask, apply_mask, [("output_image", "mask_file")]), - (get_brainmask, copy_xform, [("output_image", "out_mask")]), - (apply_mask, copy_xform, [("out_file", "out_file")]), - (inu_n4_final, copy_xform, [ - ("output_image", "bias_corrected"), - ("bias_image", "bias_image"), - ]), - (copy_xform, outputnode, [ - ("out_file", "out_file"), - ("out_mask", "out_mask"), - ("bias_corrected", "bias_corrected"), - ("bias_image", "bias_image"), - ]), + (get_brainmask, apply_mask, [("output_image", "in_mask")]), + (get_brainmask, outputnode, [("output_image", "out_mask")]), + (inu_n4_final, outputnode, [("output_image", "bias_corrected"), + ("bias_image", "bias_image")]), + (apply_mask, outputnode, [("out_file", "out_file")]), ]) # fmt: on @@ -418,8 +397,8 @@ def init_brain_extraction_wf( # fmt: off wf.disconnect([ - (get_brainmask, apply_mask, [("output_image", "mask_file")]), - (copy_xform, outputnode, [("out_mask", "out_mask")]), + (get_brainmask, apply_mask, [("output_image", "in_mask")]), + (get_brainmask, outputnode, [("output_image", "out_mask")]), ]) wf.connect([ (inu_n4, atropos_wf, [("output_image", "inputnode.in_files")]), @@ -429,7 +408,7 @@ def init_brain_extraction_wf( ]), (atropos_wf, sel_wm, [("outputnode.out_tpms", "inlist")]), (sel_wm, inu_n4_final, [("out", "weight_image")]), - (atropos_wf, apply_mask, [("outputnode.out_mask", "mask_file")]), + (atropos_wf, apply_mask, [("outputnode.out_mask", "in_mask")]), (atropos_wf, outputnode, [ ("outputnode.out_mask", "out_mask"), ("outputnode.out_segm", "out_segm"), @@ -508,7 +487,6 @@ def init_atropos_wf( out_tpms : str Output :abbr:`TPMs (tissue probability maps)` - """ wf = pe.Workflow(name) @@ -521,12 +499,6 @@ def init_atropos_wf( name="outputnode", ) - copy_xform = pe.Node( - CopyXForm(fields=["out_mask", "out_segm", "out_tpms"]), - name="copy_xform", - run_without_submitting=True, - ) - # Run atropos (core node) atropos = pe.Node( Atropos( @@ -645,17 +617,16 @@ def init_atropos_wf( ImageMath(operation="PadImage", op2="-%d" % padding), name="27_depad_csf" ) - msk_conform = pe.Node(niu.Function(function=_conform_mask), name="msk_conform") + msk_dtype = pe.Node(niu.Function(function=_ensure_dtype), name="msk_dtype") + msk_dtype.inputs.dtype = "uint8" merge_tpms = pe.Node(niu.Merge(in_segmentation_model[0]), name="merge_tpms") # fmt: off wf.connect([ - (inputnode, copy_xform, [(("in_files", _pop), "hdr_file")]), (inputnode, pad_mask, [("in_mask", "op1")]), (inputnode, atropos, [ ("in_files", "intensity_images"), ("in_mask_dilated", "mask_image"), ]), - (inputnode, msk_conform, [(("in_files", _pop), "in_reference")]), (atropos, pad_segm, [("classified_image", "op1")]), (pad_segm, sel_labels, [("output_image", "in_segm")]), (sel_labels, get_wm, [("out_wm", "op1")]), @@ -688,15 +659,10 @@ def init_atropos_wf( (depad_csf, merge_tpms, [("output_image", "in1")]), (depad_gm, merge_tpms, [("output_image", "in2")]), (depad_wm, merge_tpms, [("output_image", "in3")]), - (depad_mask, msk_conform, [("output_image", "in_mask")]), - (msk_conform, copy_xform, [("out", "out_mask")]), - (depad_segm, copy_xform, [("output_image", "out_segm")]), - (merge_tpms, copy_xform, [("out", "out_tpms")]), - (copy_xform, outputnode, [ - ("out_mask", "out_mask"), - ("out_segm", "out_segm"), - ("out_tpms", "out_tpms"), - ]), + (depad_mask, msk_dtype, [("output_image", "in_mask")]), + (msk_dtype, outputnode, [("out", "out_mask")]), + (depad_segm, outputnode, [("output_image", "out_segm")]), + (merge_tpms, outputnode, [("out", "out_tpms")]), ]) # fmt: on return wf @@ -748,7 +714,7 @@ def init_n4_only_wf( Allows to specify a particular segmentation model, overwriting the defaults based on ``bids_suffix`` name : str, optional - Workflow name (default: ``'n4_only_wf'``). + Workflow name (default: ``"n4_only_wf"``). Inputs ------ @@ -918,27 +884,18 @@ def _select_labels(in_segm, labels): return out_files -def _conform_mask(in_mask, in_reference): - """Ensures the mask headers make sense and match those of the T1w""" +def _ensure_dtype(in_mask, dtype="uint8"): + """Ensure the mask headers make sense and match those of the T1w.""" from pathlib import Path import numpy as np import nibabel as nb from nipype.utils.filemanip import fname_presuffix - ref = nb.load(in_reference) nii = nb.load(in_mask) hdr = nii.header.copy() - hdr.set_data_dtype("int16") + hdr.set_data_dtype(dtype) hdr.set_slope_inter(1, 0) - qform, qcode = ref.header.get_qform(coded=True) - if qcode is not None: - hdr.set_qform(qform, int(qcode)) - - sform, scode = ref.header.get_sform(coded=True) - if scode is not None: - hdr.set_sform(sform, int(scode)) - if "_maths" in in_mask: # Cut the name at first _maths occurrence ext = "".join(Path(in_mask).suffixes) basename = Path(in_mask).name @@ -946,6 +903,6 @@ def _conform_mask(in_mask, in_reference): out_file = fname_presuffix(in_mask, suffix="_mask", newpath=str(Path())) nii.__class__( - np.asanyarray(nii.dataobj).astype("int16"), ref.affine, hdr + np.asanyarray(nii.dataobj).astype(dtype), nii.affine, hdr ).to_filename(out_file) return out_file diff --git a/niworkflows/func/util.py b/niworkflows/func/util.py index cd9140425da..0992b09df9a 100644 --- a/niworkflows/func/util.py +++ b/niworkflows/func/util.py @@ -6,11 +6,11 @@ from nipype.pipeline import engine as pe from nipype.interfaces import utility as niu, fsl, afni +from nipype.interfaces.ants.utils import AI from templateflow.api import get as get_template from ..engine.workflows import LiterateWorkflow as Workflow -from ..interfaces.ants import AI from ..interfaces.fixes import ( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, diff --git a/niworkflows/interfaces/ants.py b/niworkflows/interfaces/ants.py index 985431f8ce0..44070f9fe4a 100644 --- a/niworkflows/interfaces/ants.py +++ b/niworkflows/interfaces/ants.py @@ -1,660 +1,18 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: -"""Nipype interfaces for ANTs' commands.""" -import os -from glob import glob -from nipype.interfaces import base -from nipype.interfaces.ants.base import ANTSCommandInputSpec, ANTSCommand -from nipype.interfaces.base import traits, isdefined +"""Patch ImageMath until https://github.com/nipy/nipype/pull/3210 is merged.""" +from nipype.interfaces.base import Str +from nipype.interfaces.ants.utils import ( + ImageMath as _ImageMath, + ImageMathInputSpec as _InputSpec +) -class _ImageMathInputSpec(ANTSCommandInputSpec): - dimension = traits.Int( - 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" - ) - output_image = base.File( - position=2, - argstr="%s", - name_source=["op1"], - name_template="%s_maths", - desc="output image file", - keep_extension=True, - ) - operation = base.Str( +class _ImageMathInputSpec(_InputSpec): + operation = Str( mandatory=True, position=3, argstr="%s", desc="operations and intputs" ) - op1 = base.File( - exists=True, mandatory=True, position=-2, argstr="%s", desc="first operator" - ) - op2 = traits.Either( - base.File(exists=True), - base.Str, - position=-1, - argstr="%s", - desc="second operator", - ) - copy_header = traits.Bool( - True, - usedefault=True, - desc="copy headers of the original image into the output (corrected) file", - ) - - -class _ImageMathOuputSpec(base.TraitedSpec): - output_image = base.File(exists=True, desc="output image file") - -class ImageMath(ANTSCommand): - """ - Operations over images. - Example - ------- - >>> maths = ImageMath(dimension=3, op1=nifti_fname, operation='+', op2='2') - >>> result = maths.run() - >>> np.all(nb.load(result.outputs.output_image).get_sform() == - ... nb.load(nifti_fname).get_sform()) - True - - """ - - _cmd = "ImageMath" +class ImageMath(_ImageMath): input_spec = _ImageMathInputSpec - output_spec = _ImageMathOuputSpec - - def _list_outputs(self): - outputs = super(ImageMath, self)._list_outputs() - if self.inputs.copy_header: # Fix headers - _copy_header(self.inputs.op1, outputs["output_image"], set_dtype=False) - return outputs - - -class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): - dimension = traits.Int( - 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" - ) - input_image = base.File( - exists=True, mandatory=True, position=2, argstr="%s", desc="input image file" - ) - output_image = base.File( - position=3, - argstr="%s", - name_source=["input_image"], - name_template="%s_resampled", - desc="output image file", - keep_extension=True, - ) - out_spacing = traits.Either( - traits.List(traits.Float, minlen=2, maxlen=3), - traits.Tuple(traits.Float, traits.Float, traits.Float), - traits.Tuple(traits.Float, traits.Float), - position=4, - argstr="%s", - mandatory=True, - desc="output spacing", - ) - apply_smoothing = traits.Bool( - False, argstr="%d", position=5, desc="smooth before resampling" - ) - addvox = traits.Int( - argstr="%d", - position=6, - requires=["apply_smoothing"], - desc="addvox pads each dimension by addvox", - ) - nn_interp = traits.Bool( - argstr="%d", desc="nn interpolation", position=-1, requires=["addvox"] - ) - - -class _ResampleImageBySpacingOutputSpec(base.TraitedSpec): - output_image = traits.File(exists=True, desc="resampled file") - - -class ResampleImageBySpacing(ANTSCommand): - """ - Resample an image with a given spacing. - - Examples - -------- - >>> res = ResampleImageBySpacing(dimension=3) - >>> res.inputs.input_image = nifti_fname - >>> res.inputs.output_image = 'output.nii.gz' - >>> res.inputs.out_spacing = (4, 4, 4) - >>> res.cmdline #doctest: +ELLIPSIS - 'ResampleImageBySpacing 3 .../test.nii.gz output.nii.gz 4 4 4' - - >>> res = ResampleImageBySpacing(dimension=3) - >>> res.inputs.input_image = nifti_fname - >>> res.inputs.output_image = 'output.nii.gz' - >>> res.inputs.out_spacing = (4, 4, 4) - >>> res.inputs.apply_smoothing = True - >>> res.cmdline #doctest: +ELLIPSIS - 'ResampleImageBySpacing 3 .../test.nii.gz output.nii.gz 4 4 4 1' - - >>> res = ResampleImageBySpacing(dimension=3) - >>> res.inputs.input_image = nifti_fname - >>> res.inputs.output_image = 'output.nii.gz' - >>> res.inputs.out_spacing = (0.4, 0.4, 0.4) - >>> res.inputs.apply_smoothing = True - >>> res.inputs.addvox = 2 - >>> res.inputs.nn_interp = False - >>> res.cmdline #doctest: +ELLIPSIS - 'ResampleImageBySpacing 3 .../test.nii.gz output.nii.gz 0.4 0.4 0.4 1 2 0' - - """ - - _cmd = "ResampleImageBySpacing" - input_spec = _ResampleImageBySpacingInputSpec - output_spec = _ResampleImageBySpacingOutputSpec - - def _format_arg(self, name, trait_spec, value): - if name == "out_spacing": - if len(value) != self.inputs.dimension: - raise ValueError("out_spacing dimensions should match dimension") - - value = " ".join(["%g" % d for d in value]) - - return super(ResampleImageBySpacing, self)._format_arg(name, trait_spec, value) - - -class _ThresholdImageInputSpec(ANTSCommandInputSpec): - dimension = traits.Int( - 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" - ) - input_image = base.File( - exists=True, mandatory=True, position=2, argstr="%s", desc="input image file" - ) - output_image = base.File( - position=3, - argstr="%s", - name_source=["input_image"], - name_template="%s_resampled", - desc="output image file", - keep_extension=True, - ) - - mode = traits.Enum( - "Otsu", - "Kmeans", - argstr="%s", - position=4, - requires=["num_thresholds"], - xor=["th_low", "th_high"], - desc="whether to run Otsu / Kmeans thresholding", - ) - num_thresholds = traits.Int(position=5, argstr="%d", desc="number of thresholds") - input_mask = base.File( - exists=True, - requires=["num_thresholds"], - argstr="%s", - desc="input mask for Otsu, Kmeans", - ) - - th_low = traits.Float(position=4, argstr="%f", xor=["mode"], desc="lower threshold") - th_high = traits.Float( - position=5, argstr="%f", xor=["mode"], desc="upper threshold" - ) - inside_value = traits.Float( - 1, position=6, argstr="%f", requires=["th_low"], desc="inside value" - ) - outside_value = traits.Float( - 0, position=7, argstr="%f", requires=["th_low"], desc="outside value" - ) - copy_header = traits.Bool( - True, - mandatory=True, - usedefault=True, - desc="copy headers of the original image into the output (corrected) file", - ) - - -class _ThresholdImageOutputSpec(base.TraitedSpec): - output_image = traits.File(exists=True, desc="resampled file") - - -class ThresholdImage(ANTSCommand): - """ - Apply thresholds on images. - - Examples - -------- - >>> thres = ThresholdImage(dimension=3) - >>> thres.inputs.input_image = nifti_fname - >>> thres.inputs.output_image = 'output.nii.gz' - >>> thres.inputs.th_low = 0.5 - >>> thres.inputs.th_high = 1.0 - >>> thres.inputs.inside_value = 1.0 - >>> thres.inputs.outside_value = 0.0 - >>> thres.cmdline #doctest: +ELLIPSIS - 'ThresholdImage 3 .../test.nii.gz output.nii.gz 0.500000 1.000000 1.000000 0.000000' - - >>> result = thres.run() - >>> os.path.exists(result.outputs.output_image) - True - - >>> thres = ThresholdImage(dimension=3) - >>> thres.inputs.input_image = nifti_fname - >>> thres.inputs.output_image = 'output.nii.gz' - >>> thres.inputs.mode = 'Kmeans' - >>> thres.inputs.num_thresholds = 4 - >>> thres.cmdline #doctest: +ELLIPSIS - 'ThresholdImage 3 .../test.nii.gz output.nii.gz Kmeans 4' - - """ - - _cmd = "ThresholdImage" - input_spec = _ThresholdImageInputSpec - output_spec = _ThresholdImageOutputSpec - - def _list_outputs(self): - outputs = super(ThresholdImage, self)._list_outputs() - if self.inputs.copy_header: # Fix headers - _copy_header( - self.inputs.input_image, outputs["output_image"], set_dtype=False - ) - return outputs - - -class _AIInputSpec(ANTSCommandInputSpec): - dimension = traits.Int( - 3, usedefault=True, argstr="-d %d", desc="dimension of output image" - ) - verbose = traits.Bool( - False, usedefault=True, argstr="-v %d", desc="enable verbosity" - ) - - fixed_image = traits.File( - exists=True, - mandatory=True, - desc="Image to which the moving_image should be transformed", - ) - moving_image = traits.File( - exists=True, - mandatory=True, - desc="Image that will be transformed to fixed_image", - ) - - fixed_image_mask = traits.File(exists=True, argstr="-x %s", desc="fixed mage mask") - moving_image_mask = traits.File( - exists=True, requires=["fixed_image_mask"], desc="moving mage mask" - ) - - metric_trait = ( - traits.Enum("Mattes", "GC", "MI"), - traits.Int(32), - traits.Enum("Regular", "Random", "None"), - traits.Range(value=0.2, low=0.0, high=1.0), - ) - metric = traits.Tuple( - *metric_trait, argstr="-m %s", mandatory=True, desc="the metric(s) to use." - ) - - transform = traits.Tuple( - traits.Enum("Affine", "Rigid", "Similarity"), - traits.Range(value=0.1, low=0.0, exclude_low=True), - argstr="-t %s[%f]", - usedefault=True, - desc="Several transform options are available", - ) - - principal_axes = traits.Bool( - False, - usedefault=True, - argstr="-p %d", - xor=["blobs"], - desc="align using principal axes", - ) - search_factor = traits.Tuple( - traits.Float(20), - traits.Range(value=0.12, low=0.0, high=1.0), - usedefault=True, - argstr="-s [%f,%f]", - desc="search factor", - ) - - search_grid = traits.Either( - traits.Tuple( - traits.Float, traits.Tuple(traits.Float, traits.Float, traits.Float) - ), - traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float)), - argstr="-g %s", - desc="Translation search grid in mm", - ) - - convergence = traits.Tuple( - traits.Range(low=1, high=10000, value=10), - traits.Float(1e-6), - traits.Range(low=1, high=100, value=10), - usedefault=True, - argstr="-c [%d,%f,%d]", - desc="convergence", - ) - - output_transform = traits.File( - "initialization.mat", usedefault=True, argstr="-o %s", desc="output file name" - ) - - -class _AIOuputSpec(base.TraitedSpec): - output_transform = traits.File(exists=True, desc="output file name") - - -class AI(ANTSCommand): - """Replaces ``AffineInitializer``.""" - - _cmd = "antsAI" - input_spec = _AIInputSpec - output_spec = _AIOuputSpec - - def _run_interface(self, runtime, correct_return_codes=(0,)): - runtime = super(AI, self)._run_interface(runtime, correct_return_codes) - - setattr( - self, - "_output", - { - "output_transform": os.path.join( - runtime.cwd, os.path.basename(self.inputs.output_transform) - ) - }, - ) - return runtime - - def _format_arg(self, opt, spec, val): - if opt == "metric": - val = "%s[{fixed_image},{moving_image},%d,%s,%f]" % val - val = val.format( - fixed_image=self.inputs.fixed_image, - moving_image=self.inputs.moving_image, - ) - return spec.argstr % val - - if opt == "search_grid": - val1 = "x".join(["%f" % v for v in val[1]]) - fmtval = "[%s]" % ",".join([str(val[0]), val1]) - return spec.argstr % fmtval - - if opt == "fixed_image_mask": - if isdefined(self.inputs.moving_image_mask): - return spec.argstr % ("[%s,%s]" % (val, self.inputs.moving_image_mask)) - - return super(AI, self)._format_arg(opt, spec, val) - - def _list_outputs(self): - return getattr(self, "_output") - - -class _AntsJointFusionInputSpec(ANTSCommandInputSpec): - dimension = traits.Enum( - 3, - 2, - 4, - argstr="-d %d", - desc="This option forces the image to be treated " - "as a specified-dimensional image. If not " - "specified, the program tries to infer the " - "dimensionality from the input image.", - ) - target_image = traits.List( - base.InputMultiPath(base.File(exists=True)), - argstr="-t %s", - mandatory=True, - desc="The target image (or " - "multimodal target images) assumed to be " - "aligned to a common image domain.", - ) - atlas_image = traits.List( - base.InputMultiPath(base.File(exists=True)), - argstr="-g %s...", - mandatory=True, - desc="The atlas image (or " - "multimodal atlas images) assumed to be " - "aligned to a common image domain.", - ) - atlas_segmentation_image = base.InputMultiPath( - base.File(exists=True), - argstr="-l %s...", - mandatory=True, - desc="The atlas segmentation " - "images. For performing label fusion the number " - "of specified segmentations should be identical " - "to the number of atlas image sets.", - ) - alpha = traits.Float( - default_value=0.1, - usedefault=True, - argstr="-a %s", - desc=( - "Regularization " - "term added to matrix Mx for calculating the inverse. Default = 0.1" - ), - ) - beta = traits.Float( - default_value=2.0, - usedefault=True, - argstr="-b %s", - desc=( - "Exponent for mapping " - "intensity difference to the joint error. Default = 2.0" - ), - ) - retain_label_posterior_images = traits.Bool( - False, - argstr="-r", - usedefault=True, - requires=["atlas_segmentation_image"], - desc=( - "Retain label posterior probability images. Requires " - "atlas segmentations to be specified. Default = false" - ), - ) - retain_atlas_voting_images = traits.Bool( - False, - argstr="-f", - usedefault=True, - desc=("Retain atlas voting images. Default = false"), - ) - constrain_nonnegative = traits.Bool( - False, - argstr="-c", - usedefault=True, - desc=("Constrain solution to non-negative weights."), - ) - patch_radius = traits.ListInt( - minlen=3, - maxlen=3, - argstr="-p %s", - desc=("Patch radius for similarity measures." "Default: 2x2x2"), - ) - patch_metric = traits.Enum( - "PC", - "MSQ", - argstr="-m %s", - desc=( - "Metric to be used in determining the most similar " - "neighborhood patch. Options include Pearson's " - "correlation (PC) and mean squares (MSQ). Default = " - "PC (Pearson correlation)." - ), - ) - search_radius = traits.List( - [3, 3, 3], - minlen=1, - maxlen=3, - argstr="-s %s", - usedefault=True, - desc=( - "Search radius for similarity measures. Default = 3x3x3. " - "One can also specify an image where the value at the " - "voxel specifies the isotropic search radius at that voxel." - ), - ) - exclusion_image_label = traits.List( - traits.Str(), - argstr="-e %s", - requires=["exclusion_image"], - desc=("Specify a label for the exclusion region."), - ) - exclusion_image = traits.List( - base.File(exists=True), - desc=("Specify an exclusion region for the given label."), - ) - mask_image = base.File( - argstr="-x %s", - exists=True, - desc="If a mask image " - "is specified, fusion is only performed in the mask region.", - ) - out_label_fusion = base.File( - argstr="%s", hash_files=False, desc="The output label fusion image." - ) - out_intensity_fusion_name_format = traits.Str( - argstr="", - desc="Optional intensity fusion " - "image file name format. " - '(e.g. "antsJointFusionIntensity_%d.nii.gz")', - ) - out_label_post_prob_name_format = traits.Str( - "antsJointFusionPosterior_%d.nii.gz", - requires=["out_label_fusion", "out_intensity_fusion_name_format"], - desc="Optional label posterior probability " "image file name format.", - ) - out_atlas_voting_weight_name_format = traits.Str( - "antsJointFusionVotingWeight_%d.nii.gz", - requires=[ - "out_label_fusion", - "out_intensity_fusion_name_format", - "out_label_post_prob_name_format", - ], - desc="Optional atlas voting weight image " "file name format.", - ) - verbose = traits.Bool(False, argstr="-v", desc=("Verbose output.")) - - -class _AntsJointFusionOutputSpec(base.TraitedSpec): - out_label_fusion = base.File(exists=True) - out_intensity_fusion = base.OutputMultiPath(base.File(exists=True)) - out_label_post_prob = base.OutputMultiPath(base.File(exists=True)) - out_atlas_voting_weight = base.OutputMultiPath(base.File(exists=True)) - - -class AntsJointFusion(ANTSCommand): - """Run ``antsJoinFusion`` (finds the consensus segmentation).""" - - input_spec = _AntsJointFusionInputSpec - output_spec = _AntsJointFusionOutputSpec - _cmd = "antsJointFusion" - - def _format_arg(self, opt, spec, val): - if opt == "exclusion_image_label": - retval = [] - for ii in range(len(self.inputs.exclusion_image_label)): - retval.append( - "-e {0}[{1}]".format( - self.inputs.exclusion_image_label[ii], - self.inputs.exclusion_image[ii], - ) - ) - retval = " ".join(retval) - elif opt == "patch_radius": - retval = "-p {0}".format(self._format_xarray(val)) - elif opt == "search_radius": - retval = "-s {0}".format(self._format_xarray(val)) - elif opt == "out_label_fusion": - if isdefined(self.inputs.out_intensity_fusion_name_format): - if isdefined(self.inputs.out_label_post_prob_name_format): - if isdefined(self.inputs.out_atlas_voting_weight_name_format): - retval = "-o [{0}, {1}, {2}, {3}]".format( - self.inputs.out_label_fusion, - self.inputs.out_intensity_fusion_name_format, - self.inputs.out_label_post_prob_name_format, - self.inputs.out_atlas_voting_weight_name_format, - ) - else: - retval = "-o [{0}, {1}, {2}]".format( - self.inputs.out_label_fusion, - self.inputs.out_intensity_fusion_name_format, - self.inputs.out_label_post_prob_name_format, - ) - else: - retval = "-o [{0}, {1}]".format( - self.inputs.out_label_fusion, - self.inputs.out_intensity_fusion_name_format, - ) - else: - retval = "-o {0}".format(self.inputs.out_label_fusion) - elif opt == "out_intensity_fusion_name_format": - retval = "" - if not isdefined(self.inputs.out_label_fusion): - retval = "-o {0}".format(self.inputs.out_intensity_fusion_name_format) - elif opt == "atlas_image": - atlas_image_cmd = " ".join( - [ - "-g [{0}]".format(", ".join("'%s'" % fn for fn in ai)) - for ai in self.inputs.atlas_image - ] - ) - retval = atlas_image_cmd - elif opt == "target_image": - target_image_cmd = " ".join( - [ - "-t [{0}]".format(", ".join("'%s'" % fn for fn in ai)) - for ai in self.inputs.target_image - ] - ) - retval = target_image_cmd - elif opt == "atlas_segmentation_image": - if len(val) != len(self.inputs.atlas_image): - raise ValueError( - "Number of specified segmentations should be identical to the number " - "of atlas image sets {0}!={1}".format( - len(val), len(self.inputs.atlas_image) - ) - ) - - atlas_segmentation_image_cmd = " ".join( - ["-l {0}".format(fn) for fn in self.inputs.atlas_segmentation_image] - ) - retval = atlas_segmentation_image_cmd - else: - - return super(AntsJointFusion, self)._format_arg(opt, spec, val) - return retval - - def _list_outputs(self): - outputs = self._outputs().get() - if isdefined(self.inputs.out_label_fusion): - outputs["out_label_fusion"] = os.path.abspath(self.inputs.out_label_fusion) - if isdefined(self.inputs.out_intensity_fusion_name_format): - outputs["out_intensity_fusion"] = glob( - os.path.abspath( - self.inputs.out_intensity_fusion_name_format.replace("%d", "*") - ) - ) - if isdefined(self.inputs.out_label_post_prob_name_format): - outputs["out_label_post_prob"] = glob( - os.path.abspath( - self.inputs.out_label_post_prob_name_format.replace("%d", "*") - ) - ) - if isdefined(self.inputs.out_atlas_voting_weight_name_format): - outputs["out_atlas_voting_weight"] = glob( - os.path.abspath( - self.inputs.out_atlas_voting_weight_name_format.replace("%d", "*") - ) - ) - return outputs - - -def _copy_header(header_file, in_file, set_dtype=True): - """Copy header from input image to an output image.""" - import nibabel as nb - - in_img = nb.load(header_file) - out_img = nb.load(in_file, mmap=False) - new_img = out_img.__class__(out_img.dataobj, in_img.affine, in_img.header) - if set_dtype: - new_img.set_data_dtype(out_img.get_data_dtype()) - new_img.to_filename(in_file) - return in_file diff --git a/niworkflows/interfaces/images.py b/niworkflows/interfaces/images.py index e625ef7fbde..4535bc4cadc 100644 --- a/niworkflows/interfaces/images.py +++ b/niworkflows/interfaces/images.py @@ -42,6 +42,13 @@ class _RegridToZoomsInputSpec(BaseInterfaceInputSpec): usedefault=True, desc="clip the data array within the original image's range", ) + smooth = traits.Either( + False, + traits.Bool(), + traits.Float(), + usedefault=True, + desc="apply gaussian smoothing before resampling" + ) class _RegridToZoomsOutputSpec(TraitedSpec): @@ -65,6 +72,7 @@ def _run_interface(self, runtime): self.inputs.zooms, order=self.inputs.order, clip=self.inputs.clip, + smooth=self.inputs.smooth, ).to_filename(self._results["out_file"]) return runtime diff --git a/niworkflows/utils/images.py b/niworkflows/utils/images.py index b67747dcd08..950e4beb9be 100644 --- a/niworkflows/utils/images.py +++ b/niworkflows/utils/images.py @@ -127,7 +127,7 @@ def dseg_label(in_seg, label, newpath=None): return out_file -def resample_by_spacing(in_file, zooms, order=3, clip=True): +def resample_by_spacing(in_file, zooms, order=3, clip=True, smooth=False): """Regrid the input image to match the new zooms.""" from pathlib import Path import numpy as np @@ -143,7 +143,6 @@ def resample_by_spacing(in_file, zooms, order=3, clip=True): hdr = in_file.header.copy() dtype = hdr.get_data_dtype() - data = np.asanyarray(in_file.dataobj) zooms = np.array(zooms) # Calculate the factors to normalize voxel size to the specific zooms @@ -174,6 +173,13 @@ def resample_by_spacing(in_file, zooms, order=3, clip=True): new_card.dot(np.vstack((new_grid, np.ones((1, new_grid.shape[1]))))) ) + if smooth: + from scipy.ndimage import gaussian_filter + data = gaussian_filter(in_file.get_fdata(), + 2 if smooth is True else smooth).astype(dtype) + else: + data = np.asanyarray(in_file.dataobj) + # Resample data in the new grid resampled = map_coordinates( data, diff --git a/setup.cfg b/setup.cfg index f69e37c42db..8b1e84b5edf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ install_requires = matplotlib >= 2.2.0 nibabel >= 3.0.1 nilearn >= 0.2.6, != 0.5.0, != 0.5.1 - nipype >= 1.3.1 + nipype >= 1.5.0 nitransforms >= 20.0.0rc3,<20.2 packaging pandas