From 909301e982afcf8146406515b5de6dff966f4b41 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 12:16:30 -0800 Subject: [PATCH 01/18] fix #3178: improve the parsing of ``Atropos`` --- nipype/interfaces/ants/segmentation.py | 169 ++++++++++++++++--------- 1 file changed, 111 insertions(+), 58 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index faba90dc82..8f6064b4a0 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -31,7 +31,10 @@ class AtroposInputSpec(ANTSCommandInputSpec): requires=["number_of_tissue_classes"], mandatory=True, ) - prior_probability_images = InputMultiPath(File(exists=True)) + kmeans_init_centers = traits.List(traits.Either(traits.Int, traits.Float), minlen=1) + prior_image = traits.Either( + File(exists=True), traits.Str, + desc="either a string pattern (e.g., 'prior%%02d.nii') or an existing vector-image file.") number_of_tissue_classes = traits.Int(mandatory=True) prior_weighting = traits.Float() prior_probability_threshold = traits.Float(requires=["prior_weighting"]) @@ -65,7 +68,10 @@ class AtroposOutputSpec(TraitedSpec): class Atropos(ANTSCommand): - """A finite mixture modeling (FMM) segmentation approach with possibilities for + """ + A multivariate n-class segmentation algorithm. + + A finite mixture modeling (FMM) segmentation approach with possibilities for specifying prior constraints. These prior constraints include the specification of a prior label image, prior probability images (one for each class), and/or an MRF prior to enforce spatial smoothing of the labels. Similar algorithms include @@ -73,32 +79,69 @@ class Atropos(ANTSCommand): Examples -------- - >>> from nipype.interfaces.ants import Atropos - >>> at = Atropos() - >>> at.inputs.dimension = 3 - >>> at.inputs.intensity_images = 'structural.nii' - >>> at.inputs.mask_image = 'mask.nii' + >>> at = Atropos( + ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', + ... number_of_tissue_classes=2, likelihood_model='Gaussian', save_posteriors=True, + ... mrf_smoothing_factor=0.2, mrf_radius=[1, 1, 1], icm_use_synchronous_update=True, + ... maximum_number_of_icm_terations=1, n_iterations=5, convergence_threshold=0.000001, + ... posterior_formulation='Socrates', use_mixture_model_proportions=True) + >>> at.inputs.initialization = 'Random' + >>> at.cmdline + 'Atropos --image-dimensionality 3 --icm [1,1] \ +--initialization Random[2] --intensity-image structural.nii \ +--likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ +--output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ +--use-random-seed 1' + + >>> at = Atropos( + ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', + ... number_of_tissue_classes=2, likelihood_model='Gaussian', save_posteriors=True, + ... mrf_smoothing_factor=0.2, mrf_radius=[1, 1, 1], icm_use_synchronous_update=True, + ... maximum_number_of_icm_terations=1, n_iterations=5, convergence_threshold=0.000001, + ... posterior_formulation='Socrates', use_mixture_model_proportions=True) + >>> at.inputs.initialization = 'KMeans' + >>> at.inputs.kmeans_init_centers = [100, 200] + >>> at.cmdline + 'Atropos --image-dimensionality 3 --icm [1,1] \ +--initialization KMeans[2,100,200] --intensity-image structural.nii \ +--likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ +--output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ +--use-random-seed 1' + + >>> at = Atropos( + ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', + ... number_of_tissue_classes=2, likelihood_model='Gaussian', save_posteriors=True, + ... mrf_smoothing_factor=0.2, mrf_radius=[1, 1, 1], icm_use_synchronous_update=True, + ... maximum_number_of_icm_terations=1, n_iterations=5, convergence_threshold=0.000001, + ... posterior_formulation='Socrates', use_mixture_model_proportions=True) >>> at.inputs.initialization = 'PriorProbabilityImages' - >>> at.inputs.prior_probability_images = ['rc1s1.nii', 'rc1s2.nii'] - >>> at.inputs.number_of_tissue_classes = 2 + >>> at.inputs.prior_image = 'BrainSegmentationPrior%02d.nii.gz' >>> at.inputs.prior_weighting = 0.8 >>> at.inputs.prior_probability_threshold = 0.0000001 - >>> at.inputs.likelihood_model = 'Gaussian' - >>> at.inputs.mrf_smoothing_factor = 0.2 - >>> at.inputs.mrf_radius = [1, 1, 1] - >>> at.inputs.icm_use_synchronous_update = True - >>> at.inputs.maximum_number_of_icm_terations = 1 - >>> at.inputs.n_iterations = 5 - >>> at.inputs.convergence_threshold = 0.000001 - >>> at.inputs.posterior_formulation = 'Socrates' - >>> at.inputs.use_mixture_model_proportions = True - >>> at.inputs.save_posteriors = True >>> at.cmdline 'Atropos --image-dimensionality 3 --icm [1,1] \ ---initialization PriorProbabilityImages[2,priors/priorProbImages%02d.nii,0.8,1e-07] --intensity-image structural.nii \ +--initialization PriorProbabilityImages[2,BrainSegmentationPrior%02d.nii.gz,0.8,1e-07] \ +--intensity-image structural.nii --likelihood-model Gaussian --mask-image mask.nii \ +--mrf [0.2,1x1x1] --convergence [5,1e-06] --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] \ +--posterior-formulation Socrates[1] --use-random-seed 1' + + >>> at = Atropos( + ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', + ... number_of_tissue_classes=2, likelihood_model='Gaussian', save_posteriors=True, + ... mrf_smoothing_factor=0.2, mrf_radius=[1, 1, 1], icm_use_synchronous_update=True, + ... maximum_number_of_icm_terations=1, n_iterations=5, convergence_threshold=0.000001, + ... posterior_formulation='Socrates', use_mixture_model_proportions=True) + >>> at.inputs.initialization = 'PriorLabelImage' + >>> at.inputs.prior_image = 'segmentation0.nii.gz' + >>> at.inputs.number_of_tissue_classes = 2 + >>> at.inputs.prior_weighting = 0.8 + >>> at.cmdline + 'Atropos --image-dimensionality 3 --icm [1,1] \ +--initialization PriorLabelImage[2,segmentation0.nii.gz,0.8] --intensity-image structural.nii \ --likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ ---output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] --use-random-seed 1' +--output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ +--use-random-seed 1' """ @@ -108,20 +151,53 @@ class Atropos(ANTSCommand): def _format_arg(self, opt, spec, val): if opt == "initialization": - retval = "--initialization %s[%d" % ( - val, - self.inputs.number_of_tissue_classes, - ) - if val == "PriorProbabilityImages": - _, _, ext = split_filename(self.inputs.prior_probability_images[0]) - retval += ( - ",priors/priorProbImages%02d" - + ext - + ",%g" % self.inputs.prior_weighting - ) - if isdefined(self.inputs.prior_probability_threshold): - retval += ",%g" % self.inputs.prior_probability_threshold - return retval + "]" + n_classes = self.inputs.number_of_tissue_classes + brackets = ['%d' % n_classes] + if val == 'KMeans' and isdefined(self.inputs.kmeans_init_centers): + centers = sorted(set(self.inputs.kmeans_init_centers)) + if len(centers) != n_classes: + raise ValueError( + "KMeans initialization with initial cluster centers requires " + "the number of centers to match number_of_tissue_classes" + ) + brackets += ["%g" % c for c in centers] + + if val in ("PriorProbabilityImages", "PriorLabelImage"): + if ( + not isdefined(self.inputs.prior_image) + or not isdefined(self.inputs.prior_weighting) + ): + raise ValueError( + "'%s' initialization requires setting " + "prior_image and prior_weighting" % val + ) + + priors_paths = [self.inputs.prior_image] + if "%02d" in priors_paths[0]: + if val == "PriorLabelImage": + raise ValueError( + "'PriorLabelImage' initialization does not " + "accept patterns for prior_image." + ) + priors_paths = [ + priors_paths[0] % i + for i in range(1, n_classes + 1) + ] + + if not all([os.path.exists(p) for p in priors_paths]): + raise FileNotFoundError( + "One or more prior images do not exist: " + "%s." % ', '.join(priors_paths) + ) + brackets += [self.inputs.prior_image, + "%g" % self.inputs.prior_weighting] + + if ( + val == "PriorProbabilityImages" + and isdefined(self.inputs.prior_probability_threshold) + ): + brackets.append("%g" % self.inputs.prior_probability_threshold) + return "--initialization %s[%s]" % (val, ','.join(brackets)) if opt == "mrf_smoothing_factor": retval = "--mrf [%g" % val if isdefined(self.inputs.mrf_radius): @@ -151,29 +227,6 @@ def _format_arg(self, opt, spec, val): return retval + "]" return super(Atropos, self)._format_arg(opt, spec, val) - def _run_interface(self, runtime, correct_return_codes=[0]): - if self.inputs.initialization == "PriorProbabilityImages": - priors_directory = os.path.join(os.getcwd(), "priors") - if not os.path.exists(priors_directory): - os.makedirs(priors_directory) - _, _, ext = split_filename(self.inputs.prior_probability_images[0]) - for i, f in enumerate(self.inputs.prior_probability_images): - target = os.path.join( - priors_directory, "priorProbImages%02d" % (i + 1) + ext - ) - if not ( - os.path.exists(target) - and os.path.realpath(target) == os.path.abspath(f) - ): - copyfile( - os.path.abspath(f), - os.path.join( - priors_directory, "priorProbImages%02d" % (i + 1) + ext - ), - ) - runtime = super(Atropos, self)._run_interface(runtime) - return runtime - def _gen_filename(self, name): if name == "out_classified_image_name": output = self.inputs.out_classified_image_name From 8165242e61d605029b52d2f1a0189e7270148e90 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 15:00:51 -0800 Subject: [PATCH 02/18] enh: add a few new utilities to ANTs --- nipype/interfaces/ants/utils.py | 346 +++++++++++++++++++++++++- nipype/utils/imagemanip.py | 18 ++ nipype/utils/tests/test_imagemanip.py | 33 +++ 3 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 nipype/utils/imagemanip.py create mode 100644 nipype/utils/tests/test_imagemanip.py diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 5497535609..17415b77aa 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -1,11 +1,341 @@ -# -*- coding: utf-8 -*- -"""ANTS Apply Transforms interface -""" - +"""ANTs' utilities.""" import os +from ...utils.imagemanip import copy_header as _copy_header +from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiPath +from .base import ANTSCommandInputSpec, ANTSCommand + + +class _ImageMathInputSpec(ANTSCommandInputSpec): + dimension = traits.Int(3, usedefault=True, position=1, argstr='%d', + desc='dimension of output image') + output_image = File(position=2, argstr='%s', name_source=['op1'], + name_template='%s_maths', desc='output image file', + keep_extension=True) + operation = traits.Enum( + 'm', 'vm', '+', 'v+', '-', 'v-', '/', '^', 'max', 'exp', 'addtozero', + 'overadd', 'abs', 'total', 'mean', 'vtotal', 'Decision', 'Neg', + 'Project', 'G', 'MD', 'ME', 'MO', 'MC', 'GD', 'GE', 'GO', 'GC', + mandatory=True, position=3, argstr='%s', + desc='mathematical operations') + op1 = File(exists=True, mandatory=True, position=-2, argstr='%s', + desc='first operator') + op2 = traits.Either(File(exists=True), 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(TraitedSpec): + output_image = File(exists=True, desc='output image file') + + +class ImageMath(ANTSCommand): + """ + Operations over images. + + Example + ------- + >>> ImageMath( + ... op1='structural.nii', + ... operation='+', + ... op2='2').cmdline + 'ImageMath 3 structural_maths.nii + structural.nii 2' + + >>> ImageMath( + ... op1='structural.nii', + ... operation='Project', + ... op2='1 2').cmdline + 'ImageMath 3 structural_maths.nii Project structural.nii 1 2' + + >>> ImageMath( + ... op1='structural.nii', + ... operation='G', + ... op2='4').cmdline + 'ImageMath 3 structural_maths.nii G structural.nii 4' + + """ + + _cmd = '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'], + keep_dtype=True) + return outputs + + +class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): + dimension = traits.Int(3, usedefault=True, position=1, argstr='%d', + desc='dimension of output image') + input_image = File(exists=True, mandatory=True, position=2, argstr='%s', + desc='input image file') + output_image = 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(TraitedSpec): + output_image = 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 = 'structural.nii' + >>> res.inputs.output_image = 'output.nii.gz' + >>> res.inputs.out_spacing = (4, 4, 4) + >>> res.cmdline #doctest: +ELLIPSIS + 'ResampleImageBySpacing 3 structural.nii output.nii.gz 4 4 4' + + >>> res = ResampleImageBySpacing(dimension=3) + >>> res.inputs.input_image = 'structural.nii' + >>> 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 structural.nii output.nii.gz 4 4 4 1' + + >>> res = ResampleImageBySpacing(dimension=3) + >>> res.inputs.input_image = 'structural.nii' + >>> 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 structural.nii 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 = File(exists=True, mandatory=True, position=2, argstr='%s', + desc='input image file') + output_image = File(position=3, argstr='%s', name_source=['input_image'], + name_template='%s_resampled', desc='output image file', + keep_extension=True) -from ..base import TraitedSpec, File, traits, InputMultiPath -from .base import ANTSCommand, ANTSCommandInputSpec + 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 = 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(TraitedSpec): + output_image = File(exists=True, desc='resampled file') + + +class ThresholdImage(ANTSCommand): + """ + Apply thresholds on images. + + Examples + -------- + >>> thres = ThresholdImage(dimension=3) + >>> thres.inputs.input_image = 'structural.nii' + >>> 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 structural.nii output.nii.gz 0.500000 1.000000 1.000000 0.000000' + + >>> thres = ThresholdImage(dimension=3) + >>> thres.inputs.input_image = 'structural.nii' + >>> thres.inputs.output_image = 'output.nii.gz' + >>> thres.inputs.mode = 'Kmeans' + >>> thres.inputs.num_thresholds = 4 + >>> thres.cmdline #doctest: +ELLIPSIS + 'ThresholdImage 3 structural.nii 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'], + keep_dtype=True) + return outputs + + +class _AIInputSpec(ANTSCommandInputSpec): + dimension = traits.Enum(3, 2, usedefault=True, argstr='-d %d', + desc='dimension of output image') + verbose = traits.Bool(False, usedefault=True, argstr='-v %d', + desc='enable verbosity') + + fixed_image = File( + exists=True, mandatory=True, + desc='Image to which the moving_image should be transformed') + moving_image = File( + exists=True, mandatory=True, + desc='Image that will be transformed to fixed_image') + + fixed_image_mask = File( + exists=True, argstr='-x %s', desc='fixed mage mask') + moving_image_mask = 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[%g]', 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 [%g,%g]', 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,%g,%d]', desc='convergence') + + output_transform = File( + 'initialization.mat', usedefault=True, argstr='-o %s', + desc='output file name') + + +class _AIOuputSpec(TraitedSpec): + output_transform = File(exists=True, desc='output file name') + + +class AI(ANTSCommand): + """ + Calculate the optimal linear transform parameters for aligning two images. + + Examples + -------- + >>> AI( + ... fixed_image='structural.nii', + ... moving_image='epi.nii', + ... metric=('Mattes', 32, 'Regular', 1), + ... ).cmdline + 'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1] + -o initialization.mat -p 0 -s [20,0.12] -t Affine[0.1] -v 0' + + >>> AI( + ... fixed_image='structural.nii', + ... moving_image='epi.nii', + ... metric=('Mattes', 32, 'Regular', 1), + ... search_grid=(12, (1, 1, 1)), + ... ).cmdline + 'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1] + -o initialization.mat -p 0 -s [20,0.12] -g [12.0,1x1x1] -t Affine[0.1] -v 0' + + """ + + _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,%g]' % 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(['%g' % 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 AverageAffineTransformInputSpec(ANTSCommandInputSpec): @@ -42,6 +372,7 @@ class AverageAffineTransform(ANTSCommand): >>> avg.inputs.output_affine_transform = 'MYtemplatewarp.mat' >>> avg.cmdline 'AverageAffineTransform 3 MYtemplatewarp.mat trans.mat func_to_struct.mat' + """ _cmd = "AverageAffineTransform" @@ -343,7 +674,8 @@ class ComposeMultiTransform(ANTSCommand): >>> compose_transform.inputs.dimension = 3 >>> compose_transform.inputs.transforms = ['struct_to_template.mat', 'func_to_struct.mat'] >>> compose_transform.cmdline - 'ComposeMultiTransform 3 struct_to_template_composed.mat struct_to_template.mat func_to_struct.mat' + 'ComposeMultiTransform 3 struct_to_template_composed.mat + struct_to_template.mat func_to_struct.mat' """ diff --git a/nipype/utils/imagemanip.py b/nipype/utils/imagemanip.py new file mode 100644 index 0000000000..4fe022973b --- /dev/null +++ b/nipype/utils/imagemanip.py @@ -0,0 +1,18 @@ +"""Image manipulation utilities (mostly, NiBabel manipulations).""" +import nibabel as nb + + +def copy_header(header_file, in_file, keep_dtype=True): + """Copy header from a reference image onto another image.""" + hdr_img = nb.load(header_file) + out_img = nb.load(in_file, mmap=False) + hdr = hdr_img.header.copy() + if keep_dtype: + hdr.set_data_dtype(out_img.get_data_dtype()) + + new_img = out_img.__class__(out_img.dataobj, None, hdr) + if not keep_dtype: + new_img.set_data_dtype(hdr_img.get_data_dtype()) + + new_img.to_filename(in_file) + return in_file diff --git a/nipype/utils/tests/test_imagemanip.py b/nipype/utils/tests/test_imagemanip.py new file mode 100644 index 0000000000..922d7681f1 --- /dev/null +++ b/nipype/utils/tests/test_imagemanip.py @@ -0,0 +1,33 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +import numpy as np +import nibabel as nb +import pytest +from ..imagemanip import copy_header + + +@pytest.mark.parametrize('keep_dtype', (True, False)) +def test_copy_header(tmp_path, keep_dtype): + """Cover copy_header.""" + fname1 = tmp_path / 'reference.nii.gz' + fname2 = tmp_path / 'target.nii.gz' + + nii = nb.Nifti1Image( + np.zeros((10, 10, 10), dtype='uint8'), None, None) + nii.set_qform(np.diag((1., 2., 3., 1.)), code=2) + nii.set_sform(np.diag((1., 2., 3., 1.)), code=1) + nii.to_filename(fname1) + + nii.set_data_dtype('float32') + nii.set_qform(np.eye(4), code=1) + nii.to_filename(fname2) + + copied = nb.load( + copy_header(fname1, fname2, keep_dtype=keep_dtype) + ) + ref = nb.load(fname1) + assert np.all(copied.get_qform(coded=False) == ref.get_qform(coded=False)) + assert np.all(copied.get_sform(coded=False) == ref.get_sform(coded=False)) + assert copied.get_qform(coded=True)[1] == ref.get_qform(coded=True)[1] + assert copied.get_sform(coded=True)[1] == ref.get_sform(coded=True)[1] + assert (copied.header.get_data_dtype() == ref.header.get_data_dtype()) != keep_dtype From 167f6c964ab0c1832bc1c80ae947d33779548cac Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 15:01:24 -0800 Subject: [PATCH 03/18] enh: update ants' interfaces API --- nipype/interfaces/ants/__init__.py | 59 ++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/nipype/interfaces/ants/__init__.py b/nipype/interfaces/ants/__init__.py index 389a5f1371..0dd4c66adf 100644 --- a/nipype/interfaces/ants/__init__.py +++ b/nipype/interfaces/ants/__init__.py @@ -3,13 +3,13 @@ # vi: set ft=python sts=4 ts=4 sw=4 et: """Top-level namespace for ants.""" -# Registraiton programs +# RegistratIon programs from .registration import ( ANTS, - Registration, - RegistrationSynQuick, CompositeTransformUtil, MeasureImageSimilarity, + Registration, + RegistrationSynQuick, ) # Resampling Programs @@ -22,14 +22,14 @@ # Segmentation Programs from .segmentation import ( + AntsJointFusion, Atropos, - LaplacianThickness, - N4BiasFieldCorrection, - JointFusion, - CorticalThickness, BrainExtraction, + CorticalThickness, DenoiseImage, - AntsJointFusion, + JointFusion, + LaplacianThickness, + N4BiasFieldCorrection, ) # Visualization Programs @@ -37,11 +37,48 @@ # Utility Programs from .utils import ( + AffineInitializer, + AI, AverageAffineTransform, AverageImages, - MultiplyImages, - CreateJacobianDeterminantImage, - AffineInitializer, ComposeMultiTransform, + CreateJacobianDeterminantImage, + ImageMath, LabelGeometry, + MultiplyImages, + ResampleImageBySpacing, + ThresholdImage, ) + +__all__ = [ + "AffineInitializer", + "AI", + "ANTS", + "AntsJointFusion", + "ApplyTransforms", + "ApplyTransformsToPoints", + "Atropos", + "AverageAffineTransform", + "AverageImages", + "BrainExtraction", + "ComposeMultiTransform", + "CompositeTransformUtil", + "ConvertScalarImageToRGB", + "CorticalThickness", + "CreateJacobianDeterminantImage", + "CreateTiledMosaic", + "DenoiseImage", + "ImageMath", + "JointFusion", + "LabelGeometry", + "LaplacianThickness", + "MeasureImageSimilarity", + "MultiplyImages", + "N4BiasFieldCorrection", + "Registration", + "RegistrationSynQuick", + "ResampleImageBySpacing", + "ThresholdImage", + "WarpImageMultiTransform", + "WarpTimeSeriesImageMultiTransform", +] From 417b8897a116fcded5000e21e2b6ccbe29452a52 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 15:08:42 -0800 Subject: [PATCH 04/18] sty: black --- nipype/interfaces/ants/utils.py | 341 +++++++++++++++++++++----------- 1 file changed, 221 insertions(+), 120 deletions(-) diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 17415b77aa..28f4cbfd97 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -6,28 +6,66 @@ class _ImageMathInputSpec(ANTSCommandInputSpec): - dimension = traits.Int(3, usedefault=True, position=1, argstr='%d', - desc='dimension of output image') - output_image = File(position=2, argstr='%s', name_source=['op1'], - name_template='%s_maths', desc='output image file', - keep_extension=True) + dimension = traits.Int( + 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" + ) + output_image = File( + position=2, + argstr="%s", + name_source=["op1"], + name_template="%s_maths", + desc="output image file", + keep_extension=True, + ) operation = traits.Enum( - 'm', 'vm', '+', 'v+', '-', 'v-', '/', '^', 'max', 'exp', 'addtozero', - 'overadd', 'abs', 'total', 'mean', 'vtotal', 'Decision', 'Neg', - 'Project', 'G', 'MD', 'ME', 'MO', 'MC', 'GD', 'GE', 'GO', 'GC', - mandatory=True, position=3, argstr='%s', - desc='mathematical operations') - op1 = File(exists=True, mandatory=True, position=-2, argstr='%s', - desc='first operator') - op2 = traits.Either(File(exists=True), Str, position=-1, - argstr='%s', desc='second operator') + "m", + "vm", + "+", + "v+", + "-", + "v-", + "/", + "^", + "max", + "exp", + "addtozero", + "overadd", + "abs", + "total", + "mean", + "vtotal", + "Decision", + "Neg", + "Project", + "G", + "MD", + "ME", + "MO", + "MC", + "GD", + "GE", + "GO", + "GC", + mandatory=True, + position=3, + argstr="%s", + desc="mathematical operations", + ) + op1 = File( + exists=True, mandatory=True, position=-2, argstr="%s", desc="first operator" + ) + op2 = traits.Either( + File(exists=True), 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') + True, + usedefault=True, + desc="copy headers of the original image into the output (corrected) file", + ) class _ImageMathOuputSpec(TraitedSpec): - output_image = File(exists=True, desc='output image file') + output_image = File(exists=True, desc="output image file") class ImageMath(ANTSCommand): @@ -56,42 +94,57 @@ class ImageMath(ANTSCommand): """ - _cmd = 'ImageMath' + _cmd = "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'], - keep_dtype=True) + _copy_header(self.inputs.op1, outputs["output_image"], keep_dtype=True) return outputs class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): - dimension = traits.Int(3, usedefault=True, position=1, argstr='%d', - desc='dimension of output image') - input_image = File(exists=True, mandatory=True, position=2, argstr='%s', - desc='input image file') - output_image = File(position=3, argstr='%s', name_source=['input_image'], - name_template='%s_resampled', desc='output image file', - keep_extension=True) + dimension = traits.Int( + 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" + ) + input_image = File( + exists=True, mandatory=True, position=2, argstr="%s", desc="input image file" + ) + output_image = 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' + 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"] ) - 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(TraitedSpec): - output_image = File(exists=True, desc='resampled file') + output_image = File(exists=True, desc="resampled file") class ResampleImageBySpacing(ANTSCommand): @@ -127,53 +180,73 @@ class ResampleImageBySpacing(ANTSCommand): """ - _cmd = 'ResampleImageBySpacing' + _cmd = "ResampleImageBySpacing" input_spec = _ResampleImageBySpacingInputSpec output_spec = _ResampleImageBySpacingOutputSpec def _format_arg(self, name, trait_spec, value): - if name == 'out_spacing': + if name == "out_spacing": if len(value) != self.inputs.dimension: - raise ValueError('out_spacing dimensions should match dimension') + raise ValueError("out_spacing dimensions should match dimension") - value = ' '.join(['%g' % d for d in value]) + value = " ".join(["%g" % d for d in value]) - return super(ResampleImageBySpacing, self)._format_arg( - name, trait_spec, 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 = File(exists=True, mandatory=True, position=2, argstr='%s', - desc='input image file') - output_image = 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 = 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') + dimension = traits.Int( + 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" + ) + input_image = File( + exists=True, mandatory=True, position=2, argstr="%s", desc="input image file" + ) + output_image = 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 = 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') + True, + mandatory=True, + usedefault=True, + desc="copy headers of the original image into the output (corrected) file", + ) class _ThresholdImageOutputSpec(TraitedSpec): - output_image = File(exists=True, desc='resampled file') + output_image = File(exists=True, desc="resampled file") class ThresholdImage(ANTSCommand): @@ -202,76 +275,101 @@ class ThresholdImage(ANTSCommand): """ - _cmd = 'ThresholdImage' + _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'], - keep_dtype=True) + _copy_header( + self.inputs.input_image, outputs["output_image"], keep_dtype=True + ) return outputs class _AIInputSpec(ANTSCommandInputSpec): - dimension = traits.Enum(3, 2, usedefault=True, argstr='-d %d', - desc='dimension of output image') - verbose = traits.Bool(False, usedefault=True, argstr='-v %d', - desc='enable verbosity') + dimension = traits.Enum( + 3, 2, usedefault=True, argstr="-d %d", desc="dimension of output image" + ) + verbose = traits.Bool( + False, usedefault=True, argstr="-v %d", desc="enable verbosity" + ) fixed_image = File( - exists=True, mandatory=True, - desc='Image to which the moving_image should be transformed') + exists=True, + mandatory=True, + desc="Image to which the moving_image should be transformed", + ) moving_image = File( - exists=True, mandatory=True, - desc='Image that will be transformed to fixed_image') + exists=True, + mandatory=True, + desc="Image that will be transformed to fixed_image", + ) - fixed_image_mask = File( - exists=True, argstr='-x %s', desc='fixed mage mask') + fixed_image_mask = File(exists=True, argstr="-x %s", desc="fixed mage mask") moving_image_mask = File( - exists=True, requires=['fixed_image_mask'], - desc='moving mage mask') + 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) + 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." ) - 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.Enum("Affine", "Rigid", "Similarity"), traits.Range(value=0.1, low=0.0, exclude_low=True), - argstr='-t %s[%g]', usedefault=True, - desc='Several transform options are available') + argstr="-t %s[%g]", + 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') + 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 [%g,%g]', desc='search factor') + traits.Float(20), + traits.Range(value=0.12, low=0.0, high=1.0), + usedefault=True, + argstr="-s [%g,%g]", + 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, traits.Float) + ), traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float)), - argstr='-g %s', desc='Translation search grid in mm') + 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,%g,%d]', desc='convergence') + usedefault=True, + argstr="-c [%d,%g,%d]", + desc="convergence", + ) output_transform = File( - 'initialization.mat', usedefault=True, argstr='-o %s', - desc='output file name') + "initialization.mat", usedefault=True, argstr="-o %s", desc="output file name" + ) class _AIOuputSpec(TraitedSpec): - output_transform = File(exists=True, desc='output file name') + output_transform = File(exists=True, desc="output file name") class AI(ANTSCommand): @@ -299,43 +397,46 @@ class AI(ANTSCommand): """ - _cmd = 'antsAI' + _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)) - }) + 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,%g]' % val + if opt == "metric": + val = "%s[{fixed_image},{moving_image},%d,%s,%g]" % val val = val.format( fixed_image=self.inputs.fixed_image, - moving_image=self.inputs.moving_image) + moving_image=self.inputs.moving_image, + ) return spec.argstr % val - if opt == 'search_grid': - val1 = 'x'.join(['%g' % v for v in val[1]]) - fmtval = '[%s]' % ','.join([str(val[0]), val1]) + if opt == "search_grid": + val1 = "x".join(["%g" % v for v in val[1]]) + fmtval = "[%s]" % ",".join([str(val[0]), val1]) return spec.argstr % fmtval - if opt == 'fixed_image_mask': + if opt == "fixed_image_mask": if isdefined(self.inputs.moving_image_mask): - return spec.argstr % ('[%s,%s]' % ( - val, 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') + return getattr(self, "_output") class AverageAffineTransformInputSpec(ANTSCommandInputSpec): From 1c5045d9acc2a26c8222ad84c65e6e60390df508 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 15:35:05 -0800 Subject: [PATCH 05/18] enh,sty: update antsJointFusion + black et al. --- nipype/interfaces/ants/segmentation.py | 454 +++++++----------- .../ants/tests/test_auto_JointFusion.py | 45 -- .../ants/tests/test_spec_JointFusion.py | 92 ---- 3 files changed, 169 insertions(+), 422 deletions(-) delete mode 100644 nipype/interfaces/ants/tests/test_auto_JointFusion.py delete mode 100644 nipype/interfaces/ants/tests/test_spec_JointFusion.py diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 8f6064b4a0..d2d75d900f 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -"""The ants module provides basic functions for interfacing with ants functions. -""" +"""Wrappers for segmentation utilities within ANTs.""" import os +from glob import glob from ...external.due import BibTeX from ...utils.filemanip import split_filename, copyfile, which, fname_presuffix from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined @@ -33,8 +32,10 @@ class AtroposInputSpec(ANTSCommandInputSpec): ) kmeans_init_centers = traits.List(traits.Either(traits.Int, traits.Float), minlen=1) prior_image = traits.Either( - File(exists=True), traits.Str, - desc="either a string pattern (e.g., 'prior%%02d.nii') or an existing vector-image file.") + File(exists=True), + traits.Str, + desc="either a string pattern (e.g., 'prior%%02d.nii') or an existing vector-image file.", + ) number_of_tissue_classes = traits.Int(mandatory=True) prior_weighting = traits.Float() prior_probability_threshold = traits.Float(requires=["prior_weighting"]) @@ -88,11 +89,11 @@ class Atropos(ANTSCommand): ... posterior_formulation='Socrates', use_mixture_model_proportions=True) >>> at.inputs.initialization = 'Random' >>> at.cmdline - 'Atropos --image-dimensionality 3 --icm [1,1] \ ---initialization Random[2] --intensity-image structural.nii \ ---likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ ---output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ ---use-random-seed 1' + 'Atropos --image-dimensionality 3 --icm [1,1] + --initialization Random[2] --intensity-image structural.nii + --likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] + --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] + --use-random-seed 1' >>> at = Atropos( ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', @@ -103,11 +104,11 @@ class Atropos(ANTSCommand): >>> at.inputs.initialization = 'KMeans' >>> at.inputs.kmeans_init_centers = [100, 200] >>> at.cmdline - 'Atropos --image-dimensionality 3 --icm [1,1] \ ---initialization KMeans[2,100,200] --intensity-image structural.nii \ ---likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ ---output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ ---use-random-seed 1' + 'Atropos --image-dimensionality 3 --icm [1,1] + --initialization KMeans[2,100,200] --intensity-image structural.nii + --likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] + --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] + --use-random-seed 1' >>> at = Atropos( ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', @@ -120,11 +121,12 @@ class Atropos(ANTSCommand): >>> at.inputs.prior_weighting = 0.8 >>> at.inputs.prior_probability_threshold = 0.0000001 >>> at.cmdline - 'Atropos --image-dimensionality 3 --icm [1,1] \ ---initialization PriorProbabilityImages[2,BrainSegmentationPrior%02d.nii.gz,0.8,1e-07] \ ---intensity-image structural.nii --likelihood-model Gaussian --mask-image mask.nii \ ---mrf [0.2,1x1x1] --convergence [5,1e-06] --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] \ ---posterior-formulation Socrates[1] --use-random-seed 1' + 'Atropos --image-dimensionality 3 --icm [1,1] + --initialization PriorProbabilityImages[2,BrainSegmentationPrior%02d.nii.gz,0.8,1e-07] + --intensity-image structural.nii --likelihood-model Gaussian --mask-image mask.nii + --mrf [0.2,1x1x1] --convergence [5,1e-06] + --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] + --posterior-formulation Socrates[1] --use-random-seed 1' >>> at = Atropos( ... dimension=3, intensity_images='structural.nii', mask_image='mask.nii', @@ -137,11 +139,11 @@ class Atropos(ANTSCommand): >>> at.inputs.number_of_tissue_classes = 2 >>> at.inputs.prior_weighting = 0.8 >>> at.cmdline - 'Atropos --image-dimensionality 3 --icm [1,1] \ ---initialization PriorLabelImage[2,segmentation0.nii.gz,0.8] --intensity-image structural.nii \ ---likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] \ ---output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] \ ---use-random-seed 1' + 'Atropos --image-dimensionality 3 --icm [1,1] + --initialization PriorLabelImage[2,segmentation0.nii.gz,0.8] --intensity-image structural.nii + --likelihood-model Gaussian --mask-image mask.nii --mrf [0.2,1x1x1] --convergence [5,1e-06] + --output [structural_labeled.nii,POSTERIOR_%02d.nii.gz] --posterior-formulation Socrates[1] + --use-random-seed 1' """ @@ -152,8 +154,8 @@ class Atropos(ANTSCommand): def _format_arg(self, opt, spec, val): if opt == "initialization": n_classes = self.inputs.number_of_tissue_classes - brackets = ['%d' % n_classes] - if val == 'KMeans' and isdefined(self.inputs.kmeans_init_centers): + brackets = ["%d" % n_classes] + if val == "KMeans" and isdefined(self.inputs.kmeans_init_centers): centers = sorted(set(self.inputs.kmeans_init_centers)) if len(centers) != n_classes: raise ValueError( @@ -163,9 +165,8 @@ def _format_arg(self, opt, spec, val): brackets += ["%g" % c for c in centers] if val in ("PriorProbabilityImages", "PriorLabelImage"): - if ( - not isdefined(self.inputs.prior_image) - or not isdefined(self.inputs.prior_weighting) + if not isdefined(self.inputs.prior_image) or not isdefined( + self.inputs.prior_weighting ): raise ValueError( "'%s' initialization requires setting " @@ -180,24 +181,24 @@ def _format_arg(self, opt, spec, val): "accept patterns for prior_image." ) priors_paths = [ - priors_paths[0] % i - for i in range(1, n_classes + 1) + priors_paths[0] % i for i in range(1, n_classes + 1) ] if not all([os.path.exists(p) for p in priors_paths]): raise FileNotFoundError( "One or more prior images do not exist: " - "%s." % ', '.join(priors_paths) + "%s." % ", ".join(priors_paths) ) - brackets += [self.inputs.prior_image, - "%g" % self.inputs.prior_weighting] + brackets += [ + self.inputs.prior_image, + "%g" % self.inputs.prior_weighting, + ] - if ( - val == "PriorProbabilityImages" - and isdefined(self.inputs.prior_probability_threshold) + if val == "PriorProbabilityImages" and isdefined( + self.inputs.prior_probability_threshold ): brackets.append("%g" % self.inputs.prior_probability_threshold) - return "--initialization %s[%s]" % (val, ','.join(brackets)) + return "--initialization %s[%s]" % (val, ",".join(brackets)) if opt == "mrf_smoothing_factor": retval = "--mrf [%g" % val if isdefined(self.inputs.mrf_radius): @@ -448,42 +449,42 @@ class N4BiasFieldCorrection(ANTSCommand): >>> n4.inputs.shrink_factor = 3 >>> n4.inputs.n_iterations = [50,50,30,20] >>> n4.cmdline - 'N4BiasFieldCorrection --bspline-fitting [ 300 ] \ --d 3 --input-image structural.nii \ ---convergence [ 50x50x30x20 ] --output structural_corrected.nii \ ---shrink-factor 3' + 'N4BiasFieldCorrection --bspline-fitting [ 300 ] + -d 3 --input-image structural.nii + --convergence [ 50x50x30x20 ] --output structural_corrected.nii + --shrink-factor 3' >>> n4_2 = copy.deepcopy(n4) >>> n4_2.inputs.convergence_threshold = 1e-6 >>> n4_2.cmdline - 'N4BiasFieldCorrection --bspline-fitting [ 300 ] \ --d 3 --input-image structural.nii \ ---convergence [ 50x50x30x20, 1e-06 ] --output structural_corrected.nii \ ---shrink-factor 3' + 'N4BiasFieldCorrection --bspline-fitting [ 300 ] + -d 3 --input-image structural.nii + --convergence [ 50x50x30x20, 1e-06 ] --output structural_corrected.nii + --shrink-factor 3' >>> n4_3 = copy.deepcopy(n4_2) >>> n4_3.inputs.bspline_order = 5 >>> n4_3.cmdline - 'N4BiasFieldCorrection --bspline-fitting [ 300, 5 ] \ --d 3 --input-image structural.nii \ ---convergence [ 50x50x30x20, 1e-06 ] --output structural_corrected.nii \ ---shrink-factor 3' + 'N4BiasFieldCorrection --bspline-fitting [ 300, 5 ] + -d 3 --input-image structural.nii + --convergence [ 50x50x30x20, 1e-06 ] --output structural_corrected.nii + --shrink-factor 3' >>> n4_4 = N4BiasFieldCorrection() >>> n4_4.inputs.input_image = 'structural.nii' >>> n4_4.inputs.save_bias = True >>> n4_4.inputs.dimension = 3 >>> n4_4.cmdline - 'N4BiasFieldCorrection -d 3 --input-image structural.nii \ ---output [ structural_corrected.nii, structural_bias.nii ]' + 'N4BiasFieldCorrection -d 3 --input-image structural.nii + --output [ structural_corrected.nii, structural_bias.nii ]' >>> n4_5 = N4BiasFieldCorrection() >>> n4_5.inputs.input_image = 'structural.nii' >>> n4_5.inputs.dimension = 3 >>> n4_5.inputs.histogram_sharpening = (0.12, 0.02, 200) >>> n4_5.cmdline - 'N4BiasFieldCorrection -d 3 --histogram-sharpening [0.12,0.02,200] \ ---input-image structural.nii --output structural_corrected.nii' + 'N4BiasFieldCorrection -d 3 --histogram-sharpening [0.12,0.02,200] + --input-image structural.nii --output structural_corrected.nii' """ @@ -749,8 +750,9 @@ class CorticalThickness(ANTSCommand): ... 'BrainSegmentationPrior04.nii.gz'] >>> corticalthickness.inputs.t1_registration_template = 'brain_study_template.nii.gz' >>> corticalthickness.cmdline - 'antsCorticalThickness.sh -a T1.nii.gz -m ProbabilityMaskOfStudyTemplate.nii.gz -e study_template.nii.gz -d 3 \ --s nii.gz -o antsCT_ -p nipype_priors/BrainSegmentationPrior%02d.nii.gz -t brain_study_template.nii.gz' + 'antsCorticalThickness.sh -a T1.nii.gz -m ProbabilityMaskOfStudyTemplate.nii.gz + -e study_template.nii.gz -d 3 -s nii.gz -o antsCT_ + -p nipype_priors/BrainSegmentationPrior%02d.nii.gz -t brain_study_template.nii.gz' """ @@ -990,7 +992,8 @@ class BrainExtraction(ANTSCommand): >>> brainextraction.inputs.brain_template = 'study_template.nii.gz' >>> brainextraction.inputs.brain_probability_mask ='ProbabilityMaskOfStudyTemplate.nii.gz' >>> brainextraction.cmdline - 'antsBrainExtraction.sh -a T1.nii.gz -m ProbabilityMaskOfStudyTemplate.nii.gz -e study_template.nii.gz -d 3 -s nii.gz -o highres001_' + 'antsBrainExtraction.sh -a T1.nii.gz -m ProbabilityMaskOfStudyTemplate.nii.gz + -e study_template.nii.gz -d 3 -s nii.gz -o highres001_' """ @@ -1143,155 +1146,6 @@ def _list_outputs(self): return outputs -class JointFusionInputSpec(ANTSCommandInputSpec): - dimension = traits.Enum( - 3, - 2, - 4, - argstr="%d", - position=0, - usedefault=True, - mandatory=True, - desc="image dimension (2, 3, or 4)", - ) - modalities = traits.Int( - argstr="%d", position=1, mandatory=True, desc="Number of modalities or features" - ) - warped_intensity_images = InputMultiPath( - File(exists=True), argstr="-g %s...", mandatory=True, desc="Warped atlas images" - ) - target_image = InputMultiPath( - File(exists=True), argstr="-tg %s...", mandatory=True, desc="Target image(s)" - ) - warped_label_images = InputMultiPath( - File(exists=True), - argstr="-l %s...", - mandatory=True, - desc="Warped atlas segmentations", - ) - method = traits.Str( - "Joint", - argstr="-m %s", - usedefault=True, - desc=( - "Select voting method. Options: Joint (Joint" - "Label Fusion). May be followed by optional" - "parameters in brackets, e.g., -m Joint[0.1,2]" - ), - ) - alpha = traits.Float( - 0.1, - usedefault=True, - requires=["method"], - desc=("Regularization term added to matrix Mx for inverse"), - ) - beta = traits.Int( - 2, - usedefault=True, - requires=["method"], - desc=("Exponent for mapping intensity difference to joint error"), - ) - output_label_image = File( - argstr="%s", - mandatory=True, - position=-1, - name_template="%s", - output_name="output_label_image", - desc="Output fusion label map image", - ) - patch_radius = traits.ListInt( - minlen=3, - maxlen=3, - argstr="-rp %s", - desc=("Patch radius for similarity measures, scalar or vector. Default: 2x2x2"), - ) - search_radius = traits.ListInt( - minlen=3, maxlen=3, argstr="-rs %s", desc="Local search radius. Default: 3x3x3" - ) - exclusion_region = File( - exists=True, - argstr="-x %s", - desc=("Specify an exclusion region for the given label."), - ) - atlas_group_id = traits.ListInt( - argstr="-gp %d...", desc="Assign a group ID for each atlas" - ) - atlas_group_weights = traits.ListInt( - argstr="-gpw %d...", desc=("Assign the voting weights to each atlas group") - ) - - -class JointFusionOutputSpec(TraitedSpec): - output_label_image = File(exists=True) - # TODO: optional outputs - output_posteriors, output_voting_weights - - -class JointFusion(ANTSCommand): - """ - Segmentation fusion tool. - - Examples - -------- - >>> from nipype.interfaces.ants import JointFusion - >>> at = JointFusion() - >>> at.inputs.dimension = 3 - >>> at.inputs.modalities = 1 - >>> at.inputs.method = 'Joint[0.1,2]' - >>> at.inputs.output_label_image ='fusion_labelimage_output.nii' - >>> at.inputs.warped_intensity_images = ['im1.nii', - ... 'im2.nii', - ... 'im3.nii'] - >>> at.inputs.warped_label_images = ['segmentation0.nii.gz', - ... 'segmentation1.nii.gz', - ... 'segmentation1.nii.gz'] - >>> at.inputs.target_image = 'T1.nii' - >>> at.cmdline - 'jointfusion 3 1 -m Joint[0.1,2] -tg T1.nii -g im1.nii -g im2.nii -g im3.nii -l segmentation0.nii.gz \ --l segmentation1.nii.gz -l segmentation1.nii.gz fusion_labelimage_output.nii' - - >>> at.inputs.method = 'Joint' - >>> at.inputs.alpha = 0.5 - >>> at.inputs.beta = 1 - >>> at.inputs.patch_radius = [3,2,1] - >>> at.inputs.search_radius = [1,2,3] - >>> at.cmdline - 'jointfusion 3 1 -m Joint[0.5,1] -rp 3x2x1 -rs 1x2x3 -tg T1.nii -g im1.nii -g im2.nii -g im3.nii \ --l segmentation0.nii.gz -l segmentation1.nii.gz -l segmentation1.nii.gz fusion_labelimage_output.nii' - - """ - - input_spec = JointFusionInputSpec - output_spec = JointFusionOutputSpec - _cmd = "jointfusion" - - def _format_arg(self, opt, spec, val): - if opt == "method": - if "[" in val: - retval = "-m {0}".format(val) - else: - retval = "-m {0}[{1},{2}]".format( - self.inputs.method, self.inputs.alpha, self.inputs.beta - ) - elif opt == "patch_radius": - retval = "-rp {0}".format(self._format_xarray(val)) - elif opt == "search_radius": - retval = "-rs {0}".format(self._format_xarray(val)) - else: - if opt == "warped_intensity_images": - assert len(val) == self.inputs.modalities * len( - self.inputs.warped_label_images - ), "Number of intensity images and label maps must be the same {0}!={1}".format( - len(val), len(self.inputs.warped_label_images) - ) - return super(JointFusion, self)._format_arg(opt, spec, val) - return retval - - def _list_outputs(self): - outputs = self._outputs().get() - outputs["output_label_image"] = os.path.abspath(self.inputs.output_label_image) - return outputs - - class DenoiseImageInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( 2, @@ -1403,7 +1257,7 @@ def _format_arg(self, name, trait_spec, value): return super(DenoiseImage, self)._format_arg(name, trait_spec, value) -class AntsJointFusionInputSpec(ANTSCommandInputSpec): +class _JointFusionInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( 3, 2, @@ -1549,75 +1403,91 @@ class AntsJointFusionInputSpec(ANTSCommandInputSpec): verbose = traits.Bool(False, argstr="-v", desc=("Verbose output.")) -class AntsJointFusionOutputSpec(TraitedSpec): +class _JointFusionOutputSpec(TraitedSpec): out_label_fusion = File(exists=True) - out_intensity_fusion_name_format = traits.Str() - out_label_post_prob_name_format = traits.Str() - out_atlas_voting_weight_name_format = traits.Str() + out_intensity_fusion = OutputMultiPath(File(exists=True)) + out_label_post_prob = OutputMultiPath(File(exists=True)) + out_atlas_voting_weight = OutputMultiPath(File(exists=True)) -class AntsJointFusion(ANTSCommand): +class JointFusion(ANTSCommand): """ + An image fusion algorithm. + + Developed by Hongzhi Wang and Paul Yushkevich, and it won segmentation challenges + at MICCAI 2012 and MICCAI 2013. + The original label fusion framework was extended to accommodate intensities by Brian + Avants. + This implementation is based on Paul's original ITK-style implementation + and Brian's ANTsR implementation. + + References include 1) H. Wang, J. W. Suh, S. + Das, J. Pluta, C. Craige, P. Yushkevich, Multi-atlas segmentation with joint + label fusion IEEE Trans. on Pattern Analysis and Machine Intelligence, 35(3), + 611-623, 2013. and 2) H. Wang and P. A. Yushkevich, Multi-atlas segmentation + with joint label fusion and corrective learning--an open source implementation, + Front. Neuroinform., 2013. + Examples -------- - >>> from nipype.interfaces.ants import AntsJointFusion - >>> antsjointfusion = AntsJointFusion() - >>> antsjointfusion.inputs.out_label_fusion = 'ants_fusion_label_output.nii' - >>> antsjointfusion.inputs.atlas_image = [ ['rc1s1.nii','rc1s2.nii'] ] - >>> antsjointfusion.inputs.atlas_segmentation_image = ['segmentation0.nii.gz'] - >>> antsjointfusion.inputs.target_image = ['im1.nii'] - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -l segmentation0.nii.gz \ --b 2.0 -o ants_fusion_label_output.nii -s 3x3x3 -t ['im1.nii']" - - >>> antsjointfusion.inputs.target_image = [ ['im1.nii', 'im2.nii'] ] - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -l segmentation0.nii.gz \ --b 2.0 -o ants_fusion_label_output.nii -s 3x3x3 -t ['im1.nii', 'im2.nii']" - - >>> antsjointfusion.inputs.atlas_image = [ ['rc1s1.nii','rc1s2.nii'], + >>> from nipype.interfaces.ants import JointFusion + >>> jf = JointFusion() + >>> jf.inputs.out_label_fusion = 'ants_fusion_label_output.nii' + >>> jf.inputs.atlas_image = [ ['rc1s1.nii','rc1s2.nii'] ] + >>> jf.inputs.atlas_segmentation_image = ['segmentation0.nii.gz'] + >>> jf.inputs.target_image = ['im1.nii'] + >>> jf.cmdline + "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -l segmentation0.nii.gz + -b 2.0 -o ants_fusion_label_output.nii -s 3x3x3 -t ['im1.nii']" + + >>> jf.inputs.target_image = [ ['im1.nii', 'im2.nii'] ] + >>> jf.cmdline + "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -l segmentation0.nii.gz + -b 2.0 -o ants_fusion_label_output.nii -s 3x3x3 -t ['im1.nii', 'im2.nii']" + + >>> jf.inputs.atlas_image = [ ['rc1s1.nii','rc1s2.nii'], ... ['rc2s1.nii','rc2s2.nii'] ] - >>> antsjointfusion.inputs.atlas_segmentation_image = ['segmentation0.nii.gz', + >>> jf.inputs.atlas_segmentation_image = ['segmentation0.nii.gz', ... 'segmentation1.nii.gz'] - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] \ --l segmentation0.nii.gz -l segmentation1.nii.gz -b 2.0 -o ants_fusion_label_output.nii \ --s 3x3x3 -t ['im1.nii', 'im2.nii']" - - >>> antsjointfusion.inputs.dimension = 3 - >>> antsjointfusion.inputs.alpha = 0.5 - >>> antsjointfusion.inputs.beta = 1.0 - >>> antsjointfusion.inputs.patch_radius = [3,2,1] - >>> antsjointfusion.inputs.search_radius = [3] - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] \ --l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -o ants_fusion_label_output.nii \ --p 3x2x1 -s 3 -t ['im1.nii', 'im2.nii']" - - >>> antsjointfusion.inputs.search_radius = ['mask.nii'] - >>> antsjointfusion.inputs.verbose = True - >>> antsjointfusion.inputs.exclusion_image = ['roi01.nii', 'roi02.nii'] - >>> antsjointfusion.inputs.exclusion_image_label = ['1','2'] - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] \ --l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -e 1[roi01.nii] -e 2[roi02.nii] \ --o ants_fusion_label_output.nii -p 3x2x1 -s mask.nii -t ['im1.nii', 'im2.nii'] -v" - - >>> antsjointfusion.inputs.out_label_fusion = 'ants_fusion_label_output.nii' - >>> antsjointfusion.inputs.out_intensity_fusion_name_format = 'ants_joint_fusion_intensity_%d.nii.gz' - >>> antsjointfusion.inputs.out_label_post_prob_name_format = 'ants_joint_fusion_posterior_%d.nii.gz' - >>> antsjointfusion.inputs.out_atlas_voting_weight_name_format = 'ants_joint_fusion_voting_weight_%d.nii.gz' - >>> antsjointfusion.cmdline - "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] \ --l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -e 1[roi01.nii] -e 2[roi02.nii] \ --o [ants_fusion_label_output.nii, ants_joint_fusion_intensity_%d.nii.gz, \ -ants_joint_fusion_posterior_%d.nii.gz, ants_joint_fusion_voting_weight_%d.nii.gz] \ --p 3x2x1 -s mask.nii -t ['im1.nii', 'im2.nii'] -v" + >>> jf.cmdline + "antsJointFusion -a 0.1 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] + -l segmentation0.nii.gz -l segmentation1.nii.gz -b 2.0 -o ants_fusion_label_output.nii + -s 3x3x3 -t ['im1.nii', 'im2.nii']" + + >>> jf.inputs.dimension = 3 + >>> jf.inputs.alpha = 0.5 + >>> jf.inputs.beta = 1.0 + >>> jf.inputs.patch_radius = [3,2,1] + >>> jf.inputs.search_radius = [3] + >>> jf.cmdline + "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] + -l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -o ants_fusion_label_output.nii + -p 3x2x1 -s 3 -t ['im1.nii', 'im2.nii']" + + >>> jf.inputs.search_radius = ['mask.nii'] + >>> jf.inputs.verbose = True + >>> jf.inputs.exclusion_image = ['roi01.nii', 'roi02.nii'] + >>> jf.inputs.exclusion_image_label = ['1','2'] + >>> jf.cmdline + "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] + -l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -e 1[roi01.nii] -e 2[roi02.nii] + -o ants_fusion_label_output.nii -p 3x2x1 -s mask.nii -t ['im1.nii', 'im2.nii'] -v" + + >>> jf.inputs.out_label_fusion = 'ants_fusion_label_output.nii' + >>> jf.inputs.out_intensity_fusion_name_format = 'ants_joint_fusion_intensity_%d.nii.gz' + >>> jf.inputs.out_label_post_prob_name_format = 'ants_joint_fusion_posterior_%d.nii.gz' + >>> jf.inputs.out_atlas_voting_weight_name_format = 'ants_joint_fusion_voting_weight_%d.nii.gz' + >>> jf.cmdline + "antsJointFusion -a 0.5 -g ['rc1s1.nii', 'rc1s2.nii'] -g ['rc2s1.nii', 'rc2s2.nii'] + -l segmentation0.nii.gz -l segmentation1.nii.gz -b 1.0 -d 3 -e 1[roi01.nii] -e 2[roi02.nii] + -o [ants_fusion_label_output.nii, ants_joint_fusion_intensity_%d.nii.gz, + ants_joint_fusion_posterior_%d.nii.gz, ants_joint_fusion_voting_weight_%d.nii.gz] + -p 3x2x1 -s mask.nii -t ['im1.nii', 'im2.nii'] -v" """ - input_spec = AntsJointFusionInputSpec - output_spec = AntsJointFusionOutputSpec + input_spec = _JointFusionInputSpec + output_spec = _JointFusionOutputSpec _cmd = "antsJointFusion" def _format_arg(self, opt, spec, val): @@ -1679,11 +1549,13 @@ def _format_arg(self, opt, spec, val): ) retval = target_image_cmd elif opt == "atlas_segmentation_image": - assert len(val) == len(self.inputs.atlas_image), ( - "Number of specified " - "segmentations should be identical to the number of atlas image " - "sets {0}!={1}".format(len(val), len(self.inputs.atlas_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] @@ -1699,21 +1571,32 @@ def _list_outputs(self): 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_name_format"] = os.path.abspath( - 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_name_format"] = os.path.abspath( - 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_name_format"] = os.path.abspath( - 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 +# For backwards compatibility +AntsJointFusion = JointFusion +AntsJointFusionInputSpec = _JointFusionInputSpec +AntsJointFusionOutputSpec = _JointFusionOutputSpec + + class KellyKapowskiInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( 3, @@ -1850,7 +1733,8 @@ class KellyKapowskiOutputSpec(TraitedSpec): class KellyKapowski(ANTSCommand): - """ Nipype Interface to ANTs' KellyKapowski, also known as DiReCT. + """ + Nipype Interface to ANTs' KellyKapowski, also known as DiReCT. DiReCT is a registration based estimate of cortical thickness. It was published in S. R. Das, B. B. Avants, M. Grossman, and J. C. Gee, Registration based @@ -1865,12 +1749,12 @@ class KellyKapowski(ANTSCommand): >>> kk.inputs.convergence = "[45,0.0,10]" >>> kk.inputs.thickness_prior_estimate = 10 >>> kk.cmdline - 'KellyKapowski --convergence "[45,0.0,10]" \ ---output "[segmentation0_cortical_thickness.nii.gz,segmentation0_warped_white_matter.nii.gz]" \ ---image-dimensionality 3 --gradient-step 0.025000 \ ---maximum-number-of-invert-displacement-field-iterations 20 --number-of-integration-points 10 \ ---segmentation-image "[segmentation0.nii.gz,2,3]" --smoothing-variance 1.000000 \ ---smoothing-velocity-field-parameter 1.500000 --thickness-prior-estimate 10.000000' + 'KellyKapowski --convergence "[45,0.0,10]" + --output "[segmentation0_cortical_thickness.nii.gz,segmentation0_warped_white_matter.nii.gz]" + --image-dimensionality 3 --gradient-step 0.025000 + --maximum-number-of-invert-displacement-field-iterations 20 --number-of-integration-points 10 + --segmentation-image "[segmentation0.nii.gz,2,3]" --smoothing-variance 1.000000 + --smoothing-velocity-field-parameter 1.500000 --thickness-prior-estimate 10.000000' """ diff --git a/nipype/interfaces/ants/tests/test_auto_JointFusion.py b/nipype/interfaces/ants/tests/test_auto_JointFusion.py deleted file mode 100644 index 4919b27a2d..0000000000 --- a/nipype/interfaces/ants/tests/test_auto_JointFusion.py +++ /dev/null @@ -1,45 +0,0 @@ -# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT -from ..segmentation import JointFusion - - -def test_JointFusion_inputs(): - input_map = dict( - alpha=dict(requires=["method"], usedefault=True,), - args=dict(argstr="%s",), - atlas_group_id=dict(argstr="-gp %d...",), - atlas_group_weights=dict(argstr="-gpw %d...",), - beta=dict(requires=["method"], usedefault=True,), - dimension=dict(argstr="%d", mandatory=True, position=0, usedefault=True,), - environ=dict(nohash=True, usedefault=True,), - exclusion_region=dict(argstr="-x %s", extensions=None,), - method=dict(argstr="-m %s", usedefault=True,), - modalities=dict(argstr="%d", mandatory=True, position=1,), - num_threads=dict(nohash=True, usedefault=True,), - output_label_image=dict( - argstr="%s", - extensions=None, - mandatory=True, - name_template="%s", - output_name="output_label_image", - position=-1, - ), - patch_radius=dict(argstr="-rp %s", maxlen=3, minlen=3,), - search_radius=dict(argstr="-rs %s", maxlen=3, minlen=3,), - target_image=dict(argstr="-tg %s...", mandatory=True,), - warped_intensity_images=dict(argstr="-g %s...", mandatory=True,), - warped_label_images=dict(argstr="-l %s...", mandatory=True,), - ) - inputs = JointFusion.input_spec() - - for key, metadata in list(input_map.items()): - for metakey, value in list(metadata.items()): - assert getattr(inputs.traits()[key], metakey) == value - - -def test_JointFusion_outputs(): - output_map = dict(output_label_image=dict(extensions=None,),) - outputs = JointFusion.output_spec() - - for key, metadata in list(output_map.items()): - for metakey, value in list(metadata.items()): - assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_spec_JointFusion.py b/nipype/interfaces/ants/tests/test_spec_JointFusion.py deleted file mode 100644 index a0276afbb0..0000000000 --- a/nipype/interfaces/ants/tests/test_spec_JointFusion.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -from nipype.testing import example_data -from nipype.interfaces.base import InputMultiPath -from traits.trait_errors import TraitError -from nipype.interfaces.ants import JointFusion -import pytest - - -def test_JointFusion_dimension(): - at = JointFusion() - set_dimension = lambda d: setattr(at.inputs, "dimension", int(d)) - for d in range(2, 5): - set_dimension(d) - assert at.inputs.dimension == int(d) - for d in [0, 1, 6, 7]: - with pytest.raises(TraitError): - set_dimension(d) - - -@pytest.mark.parametrize("m", range(1, 5)) -def test_JointFusion_modalities(m): - at = JointFusion() - setattr(at.inputs, "modalities", int(m)) - assert at.inputs.modalities == int(m) - - -@pytest.mark.parametrize("a, b", [(a, b) for a in range(10) for b in range(10)]) -def test_JointFusion_method(a, b): - at = JointFusion() - set_method = lambda a, b: setattr( - at.inputs, "method", "Joint[%.1f,%d]".format(a, b) - ) - _a = a / 10.0 - set_method(_a, b) - # set directly - assert at.inputs.method == "Joint[%.1f,%d]".format(_a, b) - aprime = _a + 0.1 - bprime = b + 1 - at.inputs.alpha = aprime - at.inputs.beta = bprime - # set with alpha/beta - assert at.inputs.method == "Joint[%.1f,%d]".format(aprime, bprime) - - -@pytest.mark.parametrize( - "attr, x", - [(attr, x) for attr in ["patch_radius", "search_radius"] for x in range(5)], -) -def test_JointFusion_radius(attr, x): - at = JointFusion() - setattr(at.inputs, attr, [x, x + 1, x ** x]) - assert at._format_arg(attr, None, getattr(at.inputs, attr))[ - 4: - ] == "{0}x{1}x{2}".format(x, x + 1, x ** x) - - -def test_JointFusion_cmd(): - at = JointFusion() - at.inputs.dimension = 3 - at.inputs.modalities = 1 - at.inputs.method = "Joint[0.1,2]" - at.inputs.output_label_image = "fusion_labelimage_output.nii" - warped_intensity_images = [example_data("im1.nii"), example_data("im2.nii")] - at.inputs.warped_intensity_images = warped_intensity_images - segmentation_images = [ - example_data("segmentation0.nii.gz"), - example_data("segmentation1.nii.gz"), - ] - at.inputs.warped_label_images = segmentation_images - T1_image = example_data("T1.nii") - at.inputs.target_image = T1_image - at.inputs.patch_radius = [3, 2, 1] - at.inputs.search_radius = [1, 2, 3] - expected_command = ( - "jointfusion 3 1 -m Joint[0.1,2] -rp 3x2x1 -rs 1x2x3" - " -tg %s -g %s -g %s -l %s -l %s" - " fusion_labelimage_output.nii" - ) % ( - T1_image, - warped_intensity_images[0], - warped_intensity_images[1], - segmentation_images[0], - segmentation_images[1], - ) - assert at.cmdline == expected_command - # setting intensity or labels with unequal lengths raises error - with pytest.raises(AssertionError): - at._format_arg( - "warped_intensity_images", - InputMultiPath, - warped_intensity_images + [example_data("im3.nii")], - ) From 9e7c99713b78b68f199df141a05b9fd7390eb3d6 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 27 Feb 2020 16:43:47 -0800 Subject: [PATCH 06/18] fix: ``make specs`` --- nipype/interfaces/ants/__init__.py | 2 +- nipype/interfaces/ants/tests/test_auto_AI.py | 37 ++++++++++++ .../ants/tests/test_auto_Atropos.py | 3 +- .../ants/tests/test_auto_ImageMath.py | 37 ++++++++++++ .../ants/tests/test_auto_JointFusion.py | 59 +++++++++++++++++++ .../tests/test_auto_ResampleImageBySpacing.py | 38 ++++++++++++ .../ants/tests/test_auto_ThresholdImage.py | 47 +++++++++++++++ 7 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 nipype/interfaces/ants/tests/test_auto_AI.py create mode 100644 nipype/interfaces/ants/tests/test_auto_ImageMath.py create mode 100644 nipype/interfaces/ants/tests/test_auto_JointFusion.py create mode 100644 nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py create mode 100644 nipype/interfaces/ants/tests/test_auto_ThresholdImage.py diff --git a/nipype/interfaces/ants/__init__.py b/nipype/interfaces/ants/__init__.py index 0dd4c66adf..dc96642f23 100644 --- a/nipype/interfaces/ants/__init__.py +++ b/nipype/interfaces/ants/__init__.py @@ -3,7 +3,7 @@ # vi: set ft=python sts=4 ts=4 sw=4 et: """Top-level namespace for ants.""" -# RegistratIon programs +# Registration programs from .registration import ( ANTS, CompositeTransformUtil, diff --git a/nipype/interfaces/ants/tests/test_auto_AI.py b/nipype/interfaces/ants/tests/test_auto_AI.py new file mode 100644 index 0000000000..3a3e1485d1 --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_AI.py @@ -0,0 +1,37 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..utils import AI + + +def test_AI_inputs(): + input_map = dict( + args=dict(argstr="%s",), + convergence=dict(argstr="-c [%d,%g,%d]", usedefault=True,), + dimension=dict(argstr="-d %d", usedefault=True,), + environ=dict(nohash=True, usedefault=True,), + fixed_image=dict(extensions=None, mandatory=True,), + fixed_image_mask=dict(argstr="-x %s", extensions=None,), + metric=dict(argstr="-m %s", mandatory=True,), + moving_image=dict(extensions=None, mandatory=True,), + moving_image_mask=dict(extensions=None, requires=["fixed_image_mask"],), + num_threads=dict(nohash=True, usedefault=True,), + output_transform=dict(argstr="-o %s", extensions=None, usedefault=True,), + principal_axes=dict(argstr="-p %d", usedefault=True, xor=["blobs"],), + search_factor=dict(argstr="-s [%g,%g]", usedefault=True,), + search_grid=dict(argstr="-g %s",), + transform=dict(argstr="-t %s[%g]", usedefault=True,), + verbose=dict(argstr="-v %d", usedefault=True,), + ) + inputs = AI.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value + + +def test_AI_outputs(): + output_map = dict(output_transform=dict(extensions=None,),) + outputs = AI.output_spec() + + for key, metadata in list(output_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_auto_Atropos.py b/nipype/interfaces/ants/tests/test_auto_Atropos.py index 021348bba0..09644907ab 100644 --- a/nipype/interfaces/ants/tests/test_auto_Atropos.py +++ b/nipype/interfaces/ants/tests/test_auto_Atropos.py @@ -13,6 +13,7 @@ def test_Atropos_inputs(): argstr="%s", mandatory=True, requires=["number_of_tissue_classes"], ), intensity_images=dict(argstr="--intensity-image %s...", mandatory=True,), + kmeans_init_centers=dict(), likelihood_model=dict(argstr="--likelihood-model %s",), mask_image=dict(argstr="--mask-image %s", extensions=None, mandatory=True,), maximum_number_of_icm_terations=dict(requires=["icm_use_synchronous_update"],), @@ -26,7 +27,7 @@ def test_Atropos_inputs(): ), output_posteriors_name_template=dict(usedefault=True,), posterior_formulation=dict(argstr="%s",), - prior_probability_images=dict(), + prior_image=dict(), prior_probability_threshold=dict(requires=["prior_weighting"],), prior_weighting=dict(), save_posteriors=dict(), diff --git a/nipype/interfaces/ants/tests/test_auto_ImageMath.py b/nipype/interfaces/ants/tests/test_auto_ImageMath.py new file mode 100644 index 0000000000..12aabf7a6e --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_ImageMath.py @@ -0,0 +1,37 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..utils import ImageMath + + +def test_ImageMath_inputs(): + input_map = dict( + args=dict(argstr="%s",), + copy_header=dict(usedefault=True,), + dimension=dict(argstr="%d", position=1, usedefault=True,), + environ=dict(nohash=True, usedefault=True,), + num_threads=dict(nohash=True, usedefault=True,), + op1=dict(argstr="%s", extensions=None, mandatory=True, position=-2,), + op2=dict(argstr="%s", position=-1,), + operation=dict(argstr="%s", mandatory=True, position=3,), + output_image=dict( + argstr="%s", + extensions=None, + keep_extension=True, + name_source=["op1"], + name_template="%s_maths", + position=2, + ), + ) + inputs = ImageMath.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value + + +def test_ImageMath_outputs(): + output_map = dict(output_image=dict(extensions=None,),) + outputs = ImageMath.output_spec() + + for key, metadata in list(output_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_auto_JointFusion.py b/nipype/interfaces/ants/tests/test_auto_JointFusion.py new file mode 100644 index 0000000000..57af10173d --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_JointFusion.py @@ -0,0 +1,59 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..segmentation import JointFusion + + +def test_JointFusion_inputs(): + input_map = dict( + alpha=dict(argstr="-a %s", usedefault=True,), + args=dict(argstr="%s",), + atlas_image=dict(argstr="-g %s...", mandatory=True,), + atlas_segmentation_image=dict(argstr="-l %s...", mandatory=True,), + beta=dict(argstr="-b %s", usedefault=True,), + constrain_nonnegative=dict(argstr="-c", usedefault=True,), + dimension=dict(argstr="-d %d",), + environ=dict(nohash=True, usedefault=True,), + exclusion_image=dict(), + exclusion_image_label=dict(argstr="-e %s", requires=["exclusion_image"],), + mask_image=dict(argstr="-x %s", extensions=None,), + num_threads=dict(nohash=True, usedefault=True,), + out_atlas_voting_weight_name_format=dict( + requires=[ + "out_label_fusion", + "out_intensity_fusion_name_format", + "out_label_post_prob_name_format", + ], + ), + out_intensity_fusion_name_format=dict(argstr="",), + out_label_fusion=dict(argstr="%s", extensions=None, hash_files=False,), + out_label_post_prob_name_format=dict( + requires=["out_label_fusion", "out_intensity_fusion_name_format"], + ), + patch_metric=dict(argstr="-m %s",), + patch_radius=dict(argstr="-p %s", maxlen=3, minlen=3,), + retain_atlas_voting_images=dict(argstr="-f", usedefault=True,), + retain_label_posterior_images=dict( + argstr="-r", requires=["atlas_segmentation_image"], usedefault=True, + ), + search_radius=dict(argstr="-s %s", usedefault=True,), + target_image=dict(argstr="-t %s", mandatory=True,), + verbose=dict(argstr="-v",), + ) + inputs = JointFusion.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value + + +def test_JointFusion_outputs(): + output_map = dict( + out_atlas_voting_weight=dict(), + out_intensity_fusion=dict(), + out_label_fusion=dict(extensions=None,), + out_label_post_prob=dict(), + ) + outputs = JointFusion.output_spec() + + for key, metadata in list(output_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py b/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py new file mode 100644 index 0000000000..a2834bf36c --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py @@ -0,0 +1,38 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..utils import ResampleImageBySpacing + + +def test_ResampleImageBySpacing_inputs(): + input_map = dict( + addvox=dict(argstr="%d", position=6, requires=["apply_smoothing"],), + apply_smoothing=dict(argstr="%d", position=5,), + args=dict(argstr="%s",), + dimension=dict(argstr="%d", position=1, usedefault=True,), + environ=dict(nohash=True, usedefault=True,), + input_image=dict(argstr="%s", extensions=None, mandatory=True, position=2,), + nn_interp=dict(argstr="%d", position=-1, requires=["addvox"],), + num_threads=dict(nohash=True, usedefault=True,), + out_spacing=dict(argstr="%s", mandatory=True, position=4,), + output_image=dict( + argstr="%s", + extensions=None, + keep_extension=True, + name_source=["input_image"], + name_template="%s_resampled", + position=3, + ), + ) + inputs = ResampleImageBySpacing.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value + + +def test_ResampleImageBySpacing_outputs(): + output_map = dict(output_image=dict(extensions=None,),) + outputs = ResampleImageBySpacing.output_spec() + + for key, metadata in list(output_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_auto_ThresholdImage.py b/nipype/interfaces/ants/tests/test_auto_ThresholdImage.py new file mode 100644 index 0000000000..c8e399f644 --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_ThresholdImage.py @@ -0,0 +1,47 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..utils import ThresholdImage + + +def test_ThresholdImage_inputs(): + input_map = dict( + args=dict(argstr="%s",), + copy_header=dict(mandatory=True, usedefault=True,), + dimension=dict(argstr="%d", position=1, usedefault=True,), + environ=dict(nohash=True, usedefault=True,), + input_image=dict(argstr="%s", extensions=None, mandatory=True, position=2,), + input_mask=dict(argstr="%s", extensions=None, requires=["num_thresholds"],), + inside_value=dict(argstr="%f", position=6, requires=["th_low"],), + mode=dict( + argstr="%s", + position=4, + requires=["num_thresholds"], + xor=["th_low", "th_high"], + ), + num_threads=dict(nohash=True, usedefault=True,), + num_thresholds=dict(argstr="%d", position=5,), + output_image=dict( + argstr="%s", + extensions=None, + keep_extension=True, + name_source=["input_image"], + name_template="%s_resampled", + position=3, + ), + outside_value=dict(argstr="%f", position=7, requires=["th_low"],), + th_high=dict(argstr="%f", position=5, xor=["mode"],), + th_low=dict(argstr="%f", position=4, xor=["mode"],), + ) + inputs = ThresholdImage.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value + + +def test_ThresholdImage_outputs(): + output_map = dict(output_image=dict(extensions=None,),) + outputs = ThresholdImage.output_spec() + + for key, metadata in list(output_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(outputs.traits()[key], metakey) == value From dfe85017cd1c1607e26f424376b0a27e668f8bcc Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 27 Feb 2020 19:58:08 -0800 Subject: [PATCH 07/18] tst: reduce some complexity and increase coverage --- nipype/interfaces/ants/segmentation.py | 90 +++++++++----------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index d2d75d900f..dc205ee4cf 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -2,6 +2,7 @@ import os from glob import glob from ...external.due import BibTeX +from ...utils.imagemanip import copy_header as _copy_header from ...utils.filemanip import split_filename, copyfile, which, fname_presuffix from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined from .base import ANTSCommand, ANTSCommandInputSpec @@ -235,7 +236,6 @@ def _gen_filename(self, name): _, name, ext = split_filename(self.inputs.intensity_images[0]) output = name + "_labeled" + ext return output - return None def _list_outputs(self): outputs = self._outputs().get() @@ -538,24 +538,15 @@ def _list_outputs(self): # Fix headers if self.inputs.copy_header: - self._copy_header(outputs["output_image"]) + _copy_header(self.inputs.input_image, outputs["output_image"], + keep_dtype=False) if self._out_bias_file: outputs["bias_image"] = os.path.abspath(self._out_bias_file) if self.inputs.copy_header: - self._copy_header(outputs["bias_image"]) + _copy_header(self.inputs.input_image, outputs["bias_image"]) return outputs - def _copy_header(self, fname): - """Copy header from input image to an output image.""" - import nibabel as nb - - in_img = nb.load(self.inputs.input_image) - out_img = nb.load(fname, mmap=False) - new_img = out_img.__class__(out_img.get_fdata(), in_img.affine, in_img.header) - new_img.set_data_dtype(out_img.get_data_dtype()) - new_img.to_filename(fname) - class CorticalThicknessInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( @@ -1500,55 +1491,44 @@ def _format_arg(self, opt, spec, val): 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, - ) + return " ".join(retval) + if opt == "patch_radius": + return "-p {0}".format(self._format_xarray(val)) + if opt == "search_radius": + return "-s {0}".format(self._format_xarray(val)) + if opt == "out_label_fusion": + args = [self.inputs.out_label_fusion] + for option in ( + self.inputs.out_intensity_fusion_name_format, + self.inputs.out_label_post_prob_name_format, + self.inputs.out_atlas_voting_weight_name_format + ): + if isdefined(option): + args.append(option) 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 = "" + break + if len(args) == 1: + return " ".join(("-o", args[0])) + return "-o [{}]".format(", ".join(args)) + if opt == "out_intensity_fusion_name_format": 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( + return "-o {0}".format(self.inputs.out_intensity_fusion_name_format) + return "" + if opt == "atlas_image": + return " ".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( + if opt == "target_image": + return " ".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 opt == "atlas_segmentation_image": if len(val) != len(self.inputs.atlas_image): raise ValueError( "Number of specified segmentations should be identical to the number " @@ -1557,14 +1537,10 @@ def _format_arg(self, opt, spec, val): ) ) - atlas_segmentation_image_cmd = " ".join( + return " ".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 + return super(AntsJointFusion, self)._format_arg(opt, spec, val) def _list_outputs(self): outputs = self._outputs().get() @@ -1805,8 +1781,6 @@ def _gen_filename(self, name): output = name + "_warped_white_matter" + ext return output - return None - def _format_arg(self, opt, spec, val): if opt == "segmentation_image": newval = "[{0},{1},{2}]".format( From dffcb7815b7e1fc7aa073746adb01bd1cb8efc3f Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 28 Feb 2020 14:54:12 -0800 Subject: [PATCH 08/18] fix: copy header for ResampleImageBySpacing Closes nipreps/niworkflows#459 --- .../ants/tests/test_auto_ResampleImageBySpacing.py | 1 + nipype/interfaces/ants/utils.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py b/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py index a2834bf36c..dce64b8930 100644 --- a/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py +++ b/nipype/interfaces/ants/tests/test_auto_ResampleImageBySpacing.py @@ -7,6 +7,7 @@ def test_ResampleImageBySpacing_inputs(): addvox=dict(argstr="%d", position=6, requires=["apply_smoothing"],), apply_smoothing=dict(argstr="%d", position=5,), args=dict(argstr="%s",), + copy_header=dict(mandatory=True, usedefault=True,), dimension=dict(argstr="%d", position=1, usedefault=True,), environ=dict(nohash=True, usedefault=True,), input_image=dict(argstr="%s", extensions=None, mandatory=True, position=2,), diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 28f4cbfd97..4322fadb7c 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -141,6 +141,12 @@ class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): nn_interp = traits.Bool( argstr="%d", desc="nn interpolation", position=-1, requires=["addvox"] ) + copy_header = traits.Bool( + True, + mandatory=True, + usedefault=True, + desc="copy headers of the original image into the output (corrected) file", + ) class _ResampleImageBySpacingOutputSpec(TraitedSpec): @@ -193,6 +199,14 @@ def _format_arg(self, name, trait_spec, value): return super(ResampleImageBySpacing, self)._format_arg(name, trait_spec, value) + def _list_outputs(self): + outputs = super(ResampleImageBySpacing, self)._list_outputs() + if self.inputs.copy_header: # Fix headers + _copy_header( + self.inputs.input_image, outputs["output_image"], keep_dtype=True + ) + return outputs + class _ThresholdImageInputSpec(ANTSCommandInputSpec): dimension = traits.Int( From cf48cbd60452bd49deb68f5fe6d122b9f3f36467 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 3 Mar 2020 11:09:01 -0800 Subject: [PATCH 09/18] Apply suggestions from code review Co-Authored-By: Chris Markiewicz --- nipype/interfaces/ants/segmentation.py | 2 +- nipype/interfaces/ants/utils.py | 28 ++++++++++---------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index dc205ee4cf..7b35942bab 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -35,7 +35,7 @@ class AtroposInputSpec(ANTSCommandInputSpec): prior_image = traits.Either( File(exists=True), traits.Str, - desc="either a string pattern (e.g., 'prior%%02d.nii') or an existing vector-image file.", + desc="either a string pattern (e.g., 'prior%02d.nii') or an existing vector-image file.", ) number_of_tissue_classes = traits.Int(mandatory=True) prior_weighting = traits.Float() diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 4322fadb7c..637c6260d9 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -1,7 +1,7 @@ """ANTs' utilities.""" import os from ...utils.imagemanip import copy_header as _copy_header -from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiPath +from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiObject from .base import ANTSCommandInputSpec, ANTSCommand @@ -400,11 +400,10 @@ class AI(ANTSCommand): 'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1] -o initialization.mat -p 0 -s [20,0.12] -t Affine[0.1] -v 0' - >>> AI( - ... fixed_image='structural.nii', - ... moving_image='epi.nii', - ... metric=('Mattes', 32, 'Regular', 1), - ... search_grid=(12, (1, 1, 1)), + >>> AI(fixed_image='structural.nii', + ... moving_image='epi.nii', + ... metric=('Mattes', 32, 'Regular', 1), + ... search_grid=(12, (1, 1, 1)), ... ).cmdline 'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1] -o initialization.mat -p 0 -s [20,0.12] -g [12.0,1x1x1] -t Affine[0.1] -v 0' @@ -418,15 +417,11 @@ class AI(ANTSCommand): 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) - ) - }, - ) + 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): @@ -439,8 +434,7 @@ def _format_arg(self, opt, spec, val): return spec.argstr % val if opt == "search_grid": - val1 = "x".join(["%g" % v for v in val[1]]) - fmtval = "[%s]" % ",".join([str(val[0]), val1]) + fmtval = "[%s,%s]" % (val[0], "x".join("%g" % v for v in val[1])) return spec.argstr % fmtval if opt == "fixed_image_mask": From 051c1e54f2c23c8c3279c60246c14593c987f1ee Mon Sep 17 00:00:00 2001 From: oesteban Date: Tue, 3 Mar 2020 18:11:38 -0800 Subject: [PATCH 10/18] fix(compat): ensure nibabel < 3 compatiblity of tests --- nipype/utils/tests/test_imagemanip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nipype/utils/tests/test_imagemanip.py b/nipype/utils/tests/test_imagemanip.py index 922d7681f1..af4c708b34 100644 --- a/nipype/utils/tests/test_imagemanip.py +++ b/nipype/utils/tests/test_imagemanip.py @@ -16,16 +16,16 @@ def test_copy_header(tmp_path, keep_dtype): np.zeros((10, 10, 10), dtype='uint8'), None, None) nii.set_qform(np.diag((1., 2., 3., 1.)), code=2) nii.set_sform(np.diag((1., 2., 3., 1.)), code=1) - nii.to_filename(fname1) + nii.to_filename(str(fname1)) nii.set_data_dtype('float32') nii.set_qform(np.eye(4), code=1) - nii.to_filename(fname2) + nii.to_filename(str(fname2)) copied = nb.load( copy_header(fname1, fname2, keep_dtype=keep_dtype) ) - ref = nb.load(fname1) + ref = nb.load(str(fname1)) assert np.all(copied.get_qform(coded=False) == ref.get_qform(coded=False)) assert np.all(copied.get_sform(coded=False) == ref.get_sform(coded=False)) assert copied.get_qform(coded=True)[1] == ref.get_qform(coded=True)[1] From 774f41d3bd8bb93ff83ccdff775661ceda509e14 Mon Sep 17 00:00:00 2001 From: oesteban Date: Tue, 3 Mar 2020 18:16:54 -0800 Subject: [PATCH 11/18] fix: use ``aggregate_outputs`` instead of ``_list_outputs`` --- nipype/interfaces/ants/utils.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 637c6260d9..0dfbf83937 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -98,10 +98,14 @@ class ImageMath(ANTSCommand): input_spec = _ImageMathInputSpec output_spec = _ImageMathOuputSpec - def _list_outputs(self): - outputs = super(ImageMath, self)._list_outputs() + def aggregate_outputs(self, runtime=None, needed_outputs=None): + """Overload the aggregation with header replacement, if required.""" + outputs = super(ImageMath, self).aggregate_outputs( + runtime, needed_outputs) if self.inputs.copy_header: # Fix headers - _copy_header(self.inputs.op1, outputs["output_image"], keep_dtype=True) + _copy_header( + self.inputs.op1, outputs["output_image"], keep_dtype=True + ) return outputs @@ -199,8 +203,10 @@ def _format_arg(self, name, trait_spec, value): return super(ResampleImageBySpacing, self)._format_arg(name, trait_spec, value) - def _list_outputs(self): - outputs = super(ResampleImageBySpacing, self)._list_outputs() + def aggregate_outputs(self, runtime=None, needed_outputs=None): + """Overload the aggregation with header replacement, if required.""" + outputs = super(ResampleImageBySpacing, self).aggregate_outputs( + runtime, needed_outputs) if self.inputs.copy_header: # Fix headers _copy_header( self.inputs.input_image, outputs["output_image"], keep_dtype=True @@ -293,8 +299,10 @@ class ThresholdImage(ANTSCommand): input_spec = _ThresholdImageInputSpec output_spec = _ThresholdImageOutputSpec - def _list_outputs(self): - outputs = super(ThresholdImage, self)._list_outputs() + def aggregate_outputs(self, runtime=None, needed_outputs=None): + """Overload the aggregation with header replacement, if required.""" + outputs = super(ThresholdImage, self).aggregate_outputs( + runtime, needed_outputs) if self.inputs.copy_header: # Fix headers _copy_header( self.inputs.input_image, outputs["output_image"], keep_dtype=True From 05441243b0c444a4a36e7cdeebac21da8c0ba9b1 Mon Sep 17 00:00:00 2001 From: oesteban Date: Tue, 3 Mar 2020 18:21:56 -0800 Subject: [PATCH 12/18] enh: remove ``aggregate_outputs`` duplications, fix InputMultiPath --- nipype/interfaces/ants/base.py | 15 +++++++ .../tests/test_auto_FixHeaderANTSCommand.py | 15 +++++++ nipype/interfaces/ants/utils.py | 45 +++---------------- 3 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py diff --git a/nipype/interfaces/ants/base.py b/nipype/interfaces/ants/base.py index 4b5e5ef8db..4d27fa619f 100644 --- a/nipype/interfaces/ants/base.py +++ b/nipype/interfaces/ants/base.py @@ -7,6 +7,7 @@ # Local imports from ... import logging, LooseVersion from ..base import CommandLine, CommandLineInputSpec, traits, isdefined, PackageInfo +from ...utils.imagemanip import copy_header as _copy_header iflogger = logging.getLogger("nipype.interface") @@ -121,3 +122,17 @@ def set_default_num_threads(cls, num_threads): @property def version(self): return Info.version() + + +class FixHeaderANTSCommand(ANTSCommand): + """Fix header if the copy_header input is on.""" + + def aggregate_outputs(self, runtime=None, needed_outputs=None): + """Overload the aggregation with header replacement, if required.""" + outputs = super(FixHeaderANTSCommand, self).aggregate_outputs( + runtime, needed_outputs) + if self.inputs.copy_header: # Fix headers + _copy_header( + self.inputs.op1, outputs["output_image"], keep_dtype=True + ) + return outputs diff --git a/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py b/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py new file mode 100644 index 0000000000..2f0d66dca9 --- /dev/null +++ b/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py @@ -0,0 +1,15 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..base import FixHeaderANTSCommand + + +def test_FixHeaderANTSCommand_inputs(): + input_map = dict( + args=dict(argstr="%s",), + environ=dict(nohash=True, usedefault=True,), + num_threads=dict(nohash=True, usedefault=True,), + ) + inputs = FixHeaderANTSCommand.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 0dfbf83937..da23bd8213 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -1,8 +1,7 @@ """ANTs' utilities.""" import os -from ...utils.imagemanip import copy_header as _copy_header from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiObject -from .base import ANTSCommandInputSpec, ANTSCommand +from .base import ANTSCommandInputSpec, ANTSCommand, FixHeaderANTSCommand class _ImageMathInputSpec(ANTSCommandInputSpec): @@ -68,7 +67,7 @@ class _ImageMathOuputSpec(TraitedSpec): output_image = File(exists=True, desc="output image file") -class ImageMath(ANTSCommand): +class ImageMath(FixHeaderANTSCommand): """ Operations over images. @@ -98,16 +97,6 @@ class ImageMath(ANTSCommand): input_spec = _ImageMathInputSpec output_spec = _ImageMathOuputSpec - def aggregate_outputs(self, runtime=None, needed_outputs=None): - """Overload the aggregation with header replacement, if required.""" - outputs = super(ImageMath, self).aggregate_outputs( - runtime, needed_outputs) - if self.inputs.copy_header: # Fix headers - _copy_header( - self.inputs.op1, outputs["output_image"], keep_dtype=True - ) - return outputs - class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): dimension = traits.Int( @@ -157,7 +146,7 @@ class _ResampleImageBySpacingOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") -class ResampleImageBySpacing(ANTSCommand): +class ResampleImageBySpacing(FixHeaderANTSCommand): """ Resample an image with a given spacing. @@ -203,16 +192,6 @@ def _format_arg(self, name, trait_spec, value): return super(ResampleImageBySpacing, self)._format_arg(name, trait_spec, value) - def aggregate_outputs(self, runtime=None, needed_outputs=None): - """Overload the aggregation with header replacement, if required.""" - outputs = super(ResampleImageBySpacing, self).aggregate_outputs( - runtime, needed_outputs) - if self.inputs.copy_header: # Fix headers - _copy_header( - self.inputs.input_image, outputs["output_image"], keep_dtype=True - ) - return outputs - class _ThresholdImageInputSpec(ANTSCommandInputSpec): dimension = traits.Int( @@ -269,7 +248,7 @@ class _ThresholdImageOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") -class ThresholdImage(ANTSCommand): +class ThresholdImage(FixHeaderANTSCommand): """ Apply thresholds on images. @@ -299,16 +278,6 @@ class ThresholdImage(ANTSCommand): input_spec = _ThresholdImageInputSpec output_spec = _ThresholdImageOutputSpec - def aggregate_outputs(self, runtime=None, needed_outputs=None): - """Overload the aggregation with header replacement, if required.""" - outputs = super(ThresholdImage, self).aggregate_outputs( - runtime, needed_outputs) - if self.inputs.copy_header: # Fix headers - _copy_header( - self.inputs.input_image, outputs["output_image"], keep_dtype=True - ) - return outputs - class _AIInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( @@ -465,7 +434,7 @@ class AverageAffineTransformInputSpec(ANTSCommandInputSpec): position=1, desc="Outputfname.txt: the name of the resulting transform.", ) - transforms = InputMultiPath( + transforms = InputMultiObject( File(exists=True), argstr="%s", mandatory=True, @@ -526,7 +495,7 @@ class AverageImagesInputSpec(ANTSCommandInputSpec): desc="Normalize: if true, the 2nd image is divided by its mean. " "This will select the largest image to average into.", ) - images = InputMultiPath( + images = InputMultiObject( File(exists=True), argstr="%s", mandatory=True, @@ -767,7 +736,7 @@ class ComposeMultiTransformInputSpec(ANTSCommandInputSpec): position=2, desc="Reference image (only necessary when output is warpfield)", ) - transforms = InputMultiPath( + transforms = InputMultiObject( File(exists=True), argstr="%s", mandatory=True, From f88add41dca713c67645752b56b533b407ca258d Mon Sep 17 00:00:00 2001 From: oesteban Date: Wed, 4 Mar 2020 10:19:43 -0800 Subject: [PATCH 13/18] fix: roll back under-i/o specs --- nipype/interfaces/ants/segmentation.py | 12 +++++----- nipype/interfaces/ants/utils.py | 32 +++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 7b35942bab..06b9350dbc 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -1248,7 +1248,7 @@ def _format_arg(self, name, trait_spec, value): return super(DenoiseImage, self)._format_arg(name, trait_spec, value) -class _JointFusionInputSpec(ANTSCommandInputSpec): +class JointFusionInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( 3, 2, @@ -1394,7 +1394,7 @@ class _JointFusionInputSpec(ANTSCommandInputSpec): verbose = traits.Bool(False, argstr="-v", desc=("Verbose output.")) -class _JointFusionOutputSpec(TraitedSpec): +class JointFusionOutputSpec(TraitedSpec): out_label_fusion = File(exists=True) out_intensity_fusion = OutputMultiPath(File(exists=True)) out_label_post_prob = OutputMultiPath(File(exists=True)) @@ -1477,8 +1477,8 @@ class JointFusion(ANTSCommand): """ - input_spec = _JointFusionInputSpec - output_spec = _JointFusionOutputSpec + input_spec = JointFusionInputSpec + output_spec = JointFusionOutputSpec _cmd = "antsJointFusion" def _format_arg(self, opt, spec, val): @@ -1569,8 +1569,8 @@ def _list_outputs(self): # For backwards compatibility AntsJointFusion = JointFusion -AntsJointFusionInputSpec = _JointFusionInputSpec -AntsJointFusionOutputSpec = _JointFusionOutputSpec +AntsJointFusionInputSpec = JointFusionInputSpec +AntsJointFusionOutputSpec = JointFusionOutputSpec class KellyKapowskiInputSpec(ANTSCommandInputSpec): diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index da23bd8213..58c91f629f 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -4,7 +4,7 @@ from .base import ANTSCommandInputSpec, ANTSCommand, FixHeaderANTSCommand -class _ImageMathInputSpec(ANTSCommandInputSpec): +class ImageMathInputSpec(ANTSCommandInputSpec): dimension = traits.Int( 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" ) @@ -63,7 +63,7 @@ class _ImageMathInputSpec(ANTSCommandInputSpec): ) -class _ImageMathOuputSpec(TraitedSpec): +class ImageMathOuputSpec(TraitedSpec): output_image = File(exists=True, desc="output image file") @@ -94,11 +94,11 @@ class ImageMath(FixHeaderANTSCommand): """ _cmd = "ImageMath" - input_spec = _ImageMathInputSpec - output_spec = _ImageMathOuputSpec + input_spec = ImageMathInputSpec + output_spec = ImageMathOuputSpec -class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): +class ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): dimension = traits.Int( 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" ) @@ -142,7 +142,7 @@ class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): ) -class _ResampleImageBySpacingOutputSpec(TraitedSpec): +class ResampleImageBySpacingOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") @@ -180,8 +180,8 @@ class ResampleImageBySpacing(FixHeaderANTSCommand): """ _cmd = "ResampleImageBySpacing" - input_spec = _ResampleImageBySpacingInputSpec - output_spec = _ResampleImageBySpacingOutputSpec + input_spec = ResampleImageBySpacingInputSpec + output_spec = ResampleImageBySpacingOutputSpec def _format_arg(self, name, trait_spec, value): if name == "out_spacing": @@ -193,7 +193,7 @@ def _format_arg(self, name, trait_spec, value): return super(ResampleImageBySpacing, self)._format_arg(name, trait_spec, value) -class _ThresholdImageInputSpec(ANTSCommandInputSpec): +class ThresholdImageInputSpec(ANTSCommandInputSpec): dimension = traits.Int( 3, usedefault=True, position=1, argstr="%d", desc="dimension of output image" ) @@ -244,7 +244,7 @@ class _ThresholdImageInputSpec(ANTSCommandInputSpec): ) -class _ThresholdImageOutputSpec(TraitedSpec): +class ThresholdImageOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") @@ -275,11 +275,11 @@ class ThresholdImage(FixHeaderANTSCommand): """ _cmd = "ThresholdImage" - input_spec = _ThresholdImageInputSpec - output_spec = _ThresholdImageOutputSpec + input_spec = ThresholdImageInputSpec + output_spec = ThresholdImageOutputSpec -class _AIInputSpec(ANTSCommandInputSpec): +class AIInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( 3, 2, usedefault=True, argstr="-d %d", desc="dimension of output image" ) @@ -359,7 +359,7 @@ class _AIInputSpec(ANTSCommandInputSpec): ) -class _AIOuputSpec(TraitedSpec): +class AIOuputSpec(TraitedSpec): output_transform = File(exists=True, desc="output file name") @@ -388,8 +388,8 @@ class AI(ANTSCommand): """ _cmd = "antsAI" - input_spec = _AIInputSpec - output_spec = _AIOuputSpec + 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) From 6979cbdcb041cb12b064513a57ff74b000bacdad Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 4 Mar 2020 16:30:11 -0500 Subject: [PATCH 14/18] RF: Move header copying to a mixin --- nipype/interfaces/ants/base.py | 14 --- nipype/interfaces/ants/segmentation.py | 24 ++--- nipype/interfaces/ants/utils.py | 12 ++- nipype/interfaces/mixins/__init__.py | 1 + nipype/interfaces/mixins/fixheader.py | 134 +++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 35 deletions(-) create mode 100644 nipype/interfaces/mixins/fixheader.py diff --git a/nipype/interfaces/ants/base.py b/nipype/interfaces/ants/base.py index 4d27fa619f..df45dd6c28 100644 --- a/nipype/interfaces/ants/base.py +++ b/nipype/interfaces/ants/base.py @@ -122,17 +122,3 @@ def set_default_num_threads(cls, num_threads): @property def version(self): return Info.version() - - -class FixHeaderANTSCommand(ANTSCommand): - """Fix header if the copy_header input is on.""" - - def aggregate_outputs(self, runtime=None, needed_outputs=None): - """Overload the aggregation with header replacement, if required.""" - outputs = super(FixHeaderANTSCommand, self).aggregate_outputs( - runtime, needed_outputs) - if self.inputs.copy_header: # Fix headers - _copy_header( - self.inputs.op1, outputs["output_image"], keep_dtype=True - ) - return outputs diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 06b9350dbc..d3319010d1 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -2,9 +2,9 @@ import os from glob import glob from ...external.due import BibTeX -from ...utils.imagemanip import copy_header as _copy_header from ...utils.filemanip import split_filename, copyfile, which, fname_presuffix from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined +from ..mixins import CopyHeaderInterface from .base import ANTSCommand, ANTSCommandInputSpec @@ -420,7 +420,7 @@ class N4BiasFieldCorrectionOutputSpec(TraitedSpec): bias_image = File(exists=True, desc="Estimated bias") -class N4BiasFieldCorrection(ANTSCommand): +class N4BiasFieldCorrection(ANTSCommand, CopyHeaderInterface): """ Bias field correction. @@ -491,6 +491,10 @@ class N4BiasFieldCorrection(ANTSCommand): _cmd = "N4BiasFieldCorrection" input_spec = N4BiasFieldCorrectionInputSpec output_spec = N4BiasFieldCorrectionOutputSpec + _copy_header_map = { + "output_image": ("input_image", False), + "bias_image": ("input_image", True), + } def __init__(self, *args, **kwargs): """Instantiate the N4BiasFieldCorrection interface.""" @@ -533,20 +537,6 @@ def _parse_inputs(self, skip=None): self._out_bias_file = bias_image return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) - def _list_outputs(self): - outputs = super(N4BiasFieldCorrection, self)._list_outputs() - - # Fix headers - if self.inputs.copy_header: - _copy_header(self.inputs.input_image, outputs["output_image"], - keep_dtype=False) - - if self._out_bias_file: - outputs["bias_image"] = os.path.abspath(self._out_bias_file) - if self.inputs.copy_header: - _copy_header(self.inputs.input_image, outputs["bias_image"]) - return outputs - class CorticalThicknessInputSpec(ANTSCommandInputSpec): dimension = traits.Enum( @@ -1501,7 +1491,7 @@ def _format_arg(self, opt, spec, val): for option in ( self.inputs.out_intensity_fusion_name_format, self.inputs.out_label_post_prob_name_format, - self.inputs.out_atlas_voting_weight_name_format + self.inputs.out_atlas_voting_weight_name_format, ): if isdefined(option): args.append(option) diff --git a/nipype/interfaces/ants/utils.py b/nipype/interfaces/ants/utils.py index 58c91f629f..6c3c05e9c4 100644 --- a/nipype/interfaces/ants/utils.py +++ b/nipype/interfaces/ants/utils.py @@ -1,7 +1,8 @@ """ANTs' utilities.""" import os from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiObject -from .base import ANTSCommandInputSpec, ANTSCommand, FixHeaderANTSCommand +from ..mixins import CopyHeaderInterface +from .base import ANTSCommandInputSpec, ANTSCommand class ImageMathInputSpec(ANTSCommandInputSpec): @@ -67,7 +68,7 @@ class ImageMathOuputSpec(TraitedSpec): output_image = File(exists=True, desc="output image file") -class ImageMath(FixHeaderANTSCommand): +class ImageMath(ANTSCommand, CopyHeaderInterface): """ Operations over images. @@ -96,6 +97,7 @@ class ImageMath(FixHeaderANTSCommand): _cmd = "ImageMath" input_spec = ImageMathInputSpec output_spec = ImageMathOuputSpec + _copy_header_map = {"output_image": "op1"} class ResampleImageBySpacingInputSpec(ANTSCommandInputSpec): @@ -146,7 +148,7 @@ class ResampleImageBySpacingOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") -class ResampleImageBySpacing(FixHeaderANTSCommand): +class ResampleImageBySpacing(ANTSCommand, CopyHeaderInterface): """ Resample an image with a given spacing. @@ -182,6 +184,7 @@ class ResampleImageBySpacing(FixHeaderANTSCommand): _cmd = "ResampleImageBySpacing" input_spec = ResampleImageBySpacingInputSpec output_spec = ResampleImageBySpacingOutputSpec + _copy_header_map = {"output_image": "input_image"} def _format_arg(self, name, trait_spec, value): if name == "out_spacing": @@ -248,7 +251,7 @@ class ThresholdImageOutputSpec(TraitedSpec): output_image = File(exists=True, desc="resampled file") -class ThresholdImage(FixHeaderANTSCommand): +class ThresholdImage(ANTSCommand, CopyHeaderInterface): """ Apply thresholds on images. @@ -277,6 +280,7 @@ class ThresholdImage(FixHeaderANTSCommand): _cmd = "ThresholdImage" input_spec = ThresholdImageInputSpec output_spec = ThresholdImageOutputSpec + _copy_header_map = {"output_image": "input_image"} class AIInputSpec(ANTSCommandInputSpec): diff --git a/nipype/interfaces/mixins/__init__.py b/nipype/interfaces/mixins/__init__.py index a64dc34ff2..e54986231f 100644 --- a/nipype/interfaces/mixins/__init__.py +++ b/nipype/interfaces/mixins/__init__.py @@ -3,3 +3,4 @@ ReportCapableInputSpec, ReportCapableOutputSpec, ) +from .fixheader import CopyHeaderInputSpec, CopyHeaderInterface diff --git a/nipype/interfaces/mixins/fixheader.py b/nipype/interfaces/mixins/fixheader.py new file mode 100644 index 0000000000..3eb15785a3 --- /dev/null +++ b/nipype/interfaces/mixins/fixheader.py @@ -0,0 +1,134 @@ +from ..base import BaseInterface, BaseInterfaceInputSpec, traits +from ...utils.imagemanip import copy_header as _copy_header + + +class CopyHeaderInputSpec(BaseInterfaceInputSpec): + copy_header = traits.Bool( + desc="Copy headers of the input image into the output image" + ) + + +class CopyHeaderInterface(BaseInterface): + """ Copy headers if the copy_header input is ``True`` + + This interface mixin adds a post-run hook that allows for copying + an input header to an output file. + The subclass should specify a ``_copy_header_map`` that maps the **output** + image to the **input** image whose header should be copied. + + This feature is intended for tools that are intended to adjust voxel data without + modifying the header, but for some reason do not reliably preserve the header. + + Here we show an example interface that takes advantage of the mixin by simply + setting the data block: + + >>> import os + >>> import numpy as np + >>> import nibabel as nb + >>> from nipype.interfaces.base import SimpleInterface, TraitedSpec, File + >>> from nipype.interfaces.mixins import CopyHeaderInputSpec, CopyHeaderInterface + + >>> class ZerofileInputSpec(CopyHeaderInputSpec): + ... in_file = File(mandatory=True, exists=True) + + >>> class ZerofileOutputSpec(TraitedSpec): + ... out_file = File() + + >>> class ZerofileInterface(SimpleInterface, CopyHeaderInterface): + ... input_spec = ZerofileInputSpec + ... output_spec = ZerofileOutputSpec + ... _copy_header_map = {'out_file': 'in_file'} + ... + ... def _run_interface(self, runtime): + ... img = nb.load(self.inputs.in_file) + ... # Just set the data. Let the CopyHeaderInterface mixin fix the affine and header. + ... nb.Nifti1Image(np.zeros(img.shape, dtype=np.uint8), None).to_filename('out.nii') + ... self._results = {'out_file': os.path.abspath('out.nii')} + ... return runtime + + Consider a file of all ones and a non-trivial affine: + + >>> in_file = 'test.nii' + >>> nb.Nifti1Image(np.ones((5,5,5), dtype=np.int16), + ... affine=np.diag((4, 3, 2, 1))).to_filename(in_file) + + The default behavior would produce a file with similar data: + + >>> res = ZerofileInterface(in_file=in_file).run() + >>> out_img = nb.load(res.outputs.out_file) + >>> out_img.shape + (5, 5, 5) + >>> np.all(out_img.get_fdata() == 0) + True + + An updated data type: + + >>> out_img.get_data_dtype() + dtype('uint8') + + But a different affine: + + >>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1))) + False + + With ``copy_header=True``, then the affine is also equal: + + >>> res = ZerofileInterface(in_file=in_file, copy_header=True).run() + >>> out_img = nb.load(res.outputs.out_file) + >>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1))) + True + + The data properties remain as expected: + + >>> out_img.shape + (5, 5, 5) + >>> out_img.get_data_dtype() + dtype('uint8') + >>> np.all(out_img.get_fdata() == 0) + True + + By default, the data type of the output file is permitted to vary from the + inputs. That is, the data type is preserved. + If the data type of the original file is preferred, the ``_copy_header_map`` + can indicate the output data type should **not** be preserved by providing a + tuple of the input and ``False``. + + >>> ZerofileInterface._copy_header_map['out_file'] = ('in_file', False) + + >>> res = ZerofileInterface(in_file=in_file, copy_header=True).run() + >>> out_img = nb.load(res.outputs.out_file) + >>> out_img.get_data_dtype() + dtype('>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1))) + True + >>> out_img.shape + (5, 5, 5) + >>> np.all(out_img.get_fdata() == 0) + True + + Providing a tuple where the second value is ``True`` is also permissible to + achieve the default behavior. + + """ + + _copy_header_map = None + + def _post_run_hook(self, runtime): + """Copy headers for outputs, if required.""" + runtime = super()._post_run_hook(runtime) + + if self._copy_header_map is None or not self.inputs.copy_header: + return runtime + + inputs = self.inputs.get_traitsfree() + outputs = self.aggregate_outputs(runtime=runtime).get_traitsfree() + for out, inp in self._copy_header_map.items(): + keep_dtype = True + if isinstance(inp, tuple): + inp, keep_dtype = inp + _copy_header(inputs[inp], outputs[out], keep_dtype=keep_dtype) + + return runtime From d50c1858564c0b3073fb23c54886a0454cb66afa Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 4 Mar 2020 16:30:39 -0500 Subject: [PATCH 15/18] STY: black --- nipype/utils/tests/test_imagemanip.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/nipype/utils/tests/test_imagemanip.py b/nipype/utils/tests/test_imagemanip.py index af4c708b34..a488633cab 100644 --- a/nipype/utils/tests/test_imagemanip.py +++ b/nipype/utils/tests/test_imagemanip.py @@ -6,25 +6,22 @@ from ..imagemanip import copy_header -@pytest.mark.parametrize('keep_dtype', (True, False)) +@pytest.mark.parametrize("keep_dtype", (True, False)) def test_copy_header(tmp_path, keep_dtype): """Cover copy_header.""" - fname1 = tmp_path / 'reference.nii.gz' - fname2 = tmp_path / 'target.nii.gz' + fname1 = tmp_path / "reference.nii.gz" + fname2 = tmp_path / "target.nii.gz" - nii = nb.Nifti1Image( - np.zeros((10, 10, 10), dtype='uint8'), None, None) - nii.set_qform(np.diag((1., 2., 3., 1.)), code=2) - nii.set_sform(np.diag((1., 2., 3., 1.)), code=1) + nii = nb.Nifti1Image(np.zeros((10, 10, 10), dtype="uint8"), None, None) + nii.set_qform(np.diag((1.0, 2.0, 3.0, 1.0)), code=2) + nii.set_sform(np.diag((1.0, 2.0, 3.0, 1.0)), code=1) nii.to_filename(str(fname1)) - nii.set_data_dtype('float32') + nii.set_data_dtype("float32") nii.set_qform(np.eye(4), code=1) nii.to_filename(str(fname2)) - copied = nb.load( - copy_header(fname1, fname2, keep_dtype=keep_dtype) - ) + copied = nb.load(copy_header(fname1, fname2, keep_dtype=keep_dtype)) ref = nb.load(str(fname1)) assert np.all(copied.get_qform(coded=False) == ref.get_qform(coded=False)) assert np.all(copied.get_sform(coded=False) == ref.get_sform(coded=False)) From 545a5fe1949d5f9d48af9ba001cbfbd639c313a0 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 4 Mar 2020 16:35:08 -0500 Subject: [PATCH 16/18] MNT: make specs --- .../ants/tests/test_auto_AntsJointFusion.py | 59 ------------------- .../tests/test_auto_FixHeaderANTSCommand.py | 15 ----- .../tests/test_auto_CopyHeaderInterface.py | 11 ++++ 3 files changed, 11 insertions(+), 74 deletions(-) delete mode 100644 nipype/interfaces/ants/tests/test_auto_AntsJointFusion.py delete mode 100644 nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py create mode 100644 nipype/interfaces/mixins/tests/test_auto_CopyHeaderInterface.py diff --git a/nipype/interfaces/ants/tests/test_auto_AntsJointFusion.py b/nipype/interfaces/ants/tests/test_auto_AntsJointFusion.py deleted file mode 100644 index d86f7f84cb..0000000000 --- a/nipype/interfaces/ants/tests/test_auto_AntsJointFusion.py +++ /dev/null @@ -1,59 +0,0 @@ -# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT -from ..segmentation import AntsJointFusion - - -def test_AntsJointFusion_inputs(): - input_map = dict( - alpha=dict(argstr="-a %s", usedefault=True,), - args=dict(argstr="%s",), - atlas_image=dict(argstr="-g %s...", mandatory=True,), - atlas_segmentation_image=dict(argstr="-l %s...", mandatory=True,), - beta=dict(argstr="-b %s", usedefault=True,), - constrain_nonnegative=dict(argstr="-c", usedefault=True,), - dimension=dict(argstr="-d %d",), - environ=dict(nohash=True, usedefault=True,), - exclusion_image=dict(), - exclusion_image_label=dict(argstr="-e %s", requires=["exclusion_image"],), - mask_image=dict(argstr="-x %s", extensions=None,), - num_threads=dict(nohash=True, usedefault=True,), - out_atlas_voting_weight_name_format=dict( - requires=[ - "out_label_fusion", - "out_intensity_fusion_name_format", - "out_label_post_prob_name_format", - ], - ), - out_intensity_fusion_name_format=dict(argstr="",), - out_label_fusion=dict(argstr="%s", extensions=None, hash_files=False,), - out_label_post_prob_name_format=dict( - requires=["out_label_fusion", "out_intensity_fusion_name_format"], - ), - patch_metric=dict(argstr="-m %s",), - patch_radius=dict(argstr="-p %s", maxlen=3, minlen=3,), - retain_atlas_voting_images=dict(argstr="-f", usedefault=True,), - retain_label_posterior_images=dict( - argstr="-r", requires=["atlas_segmentation_image"], usedefault=True, - ), - search_radius=dict(argstr="-s %s", usedefault=True,), - target_image=dict(argstr="-t %s", mandatory=True,), - verbose=dict(argstr="-v",), - ) - inputs = AntsJointFusion.input_spec() - - for key, metadata in list(input_map.items()): - for metakey, value in list(metadata.items()): - assert getattr(inputs.traits()[key], metakey) == value - - -def test_AntsJointFusion_outputs(): - output_map = dict( - out_atlas_voting_weight_name_format=dict(), - out_intensity_fusion_name_format=dict(), - out_label_fusion=dict(extensions=None,), - out_label_post_prob_name_format=dict(), - ) - outputs = AntsJointFusion.output_spec() - - for key, metadata in list(output_map.items()): - for metakey, value in list(metadata.items()): - assert getattr(outputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py b/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py deleted file mode 100644 index 2f0d66dca9..0000000000 --- a/nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py +++ /dev/null @@ -1,15 +0,0 @@ -# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT -from ..base import FixHeaderANTSCommand - - -def test_FixHeaderANTSCommand_inputs(): - input_map = dict( - args=dict(argstr="%s",), - environ=dict(nohash=True, usedefault=True,), - num_threads=dict(nohash=True, usedefault=True,), - ) - inputs = FixHeaderANTSCommand.input_spec() - - for key, metadata in list(input_map.items()): - for metakey, value in list(metadata.items()): - assert getattr(inputs.traits()[key], metakey) == value diff --git a/nipype/interfaces/mixins/tests/test_auto_CopyHeaderInterface.py b/nipype/interfaces/mixins/tests/test_auto_CopyHeaderInterface.py new file mode 100644 index 0000000000..58f9bc0864 --- /dev/null +++ b/nipype/interfaces/mixins/tests/test_auto_CopyHeaderInterface.py @@ -0,0 +1,11 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from ..fixheader import CopyHeaderInterface + + +def test_CopyHeaderInterface_inputs(): + input_map = dict() + inputs = CopyHeaderInterface.input_spec() + + for key, metadata in list(input_map.items()): + for metakey, value in list(metadata.items()): + assert getattr(inputs.traits()[key], metakey) == value From 135d900b52f8db60e8ec3b70a9d95828bfbef1b8 Mon Sep 17 00:00:00 2001 From: oesteban Date: Thu, 5 Mar 2020 00:03:16 -0800 Subject: [PATCH 17/18] sty: fix unused import --- nipype/interfaces/ants/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nipype/interfaces/ants/base.py b/nipype/interfaces/ants/base.py index df45dd6c28..4b5e5ef8db 100644 --- a/nipype/interfaces/ants/base.py +++ b/nipype/interfaces/ants/base.py @@ -7,7 +7,6 @@ # Local imports from ... import logging, LooseVersion from ..base import CommandLine, CommandLineInputSpec, traits, isdefined, PackageInfo -from ...utils.imagemanip import copy_header as _copy_header iflogger = logging.getLogger("nipype.interface") From 4e1d601e0c52187340bcc07bc8651376d3c31533 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sat, 7 Mar 2020 16:36:50 -0800 Subject: [PATCH 18/18] fix: consider that some outputs might not be defined --- nipype/interfaces/mixins/fixheader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/mixins/fixheader.py b/nipype/interfaces/mixins/fixheader.py index 3eb15785a3..ded1830582 100644 --- a/nipype/interfaces/mixins/fixheader.py +++ b/nipype/interfaces/mixins/fixheader.py @@ -125,7 +125,9 @@ def _post_run_hook(self, runtime): inputs = self.inputs.get_traitsfree() outputs = self.aggregate_outputs(runtime=runtime).get_traitsfree() - for out, inp in self._copy_header_map.items(): + defined_outputs = set(outputs.keys()).intersection(self._copy_header_map.keys()) + for out in defined_outputs: + inp = self._copy_header_map[out] keep_dtype = True if isinstance(inp, tuple): inp, keep_dtype = inp