From 7d8fdfe65129081d67a496d94ee5e6db2d57efab Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 29 Aug 2019 15:16:18 -0700 Subject: [PATCH 1/7] ENH: Add ``--rescale-intensities`` and name_source to N4BiasFieldCorrection --- nipype/interfaces/ants/segmentation.py | 80 +++++++++---------- .../tests/test_auto_N4BiasFieldCorrection.py | 8 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index adaf765527..277a716286 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -7,7 +7,7 @@ import os from ...external.due import BibTeX -from ...utils.filemanip import split_filename, copyfile, which +from ...utils.filemanip import split_filename, copyfile, which, fname_presuffix from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined from .base import ANTSCommand, ANTSCommandInputSpec @@ -274,18 +274,20 @@ class N4BiasFieldCorrectionInputSpec(ANTSCommandInputSpec): argstr='--input-image %s', mandatory=True, desc=('input for bias correction. Negative values or values close to ' - 'zero should be processed prior to correction')) + 'zero should be processed prior to correction')) mask_image = File( argstr='--mask-image %s', desc=('image to specify region to perform final bias correction in')) weight_image = File( argstr='--weight-image %s', desc=('image for relative weighting (e.g. probability map of the white ' - 'matter) of voxels during the B-spline fitting. ')) + 'matter) of voxels during the B-spline fitting. ')) output_image = traits.Str( argstr='--output %s', desc='output file name', - genfile=True, + name_source=['input_image'], + name_template='%s_corrected', + keep_extension=True, hash_files=False) bspline_fitting_distance = traits.Float(argstr="--bspline-fitting %s") bspline_order = traits.Int(requires=['bspline_fitting_distance']) @@ -306,6 +308,14 @@ class N4BiasFieldCorrectionInputSpec(ANTSCommandInputSpec): usedefault=True, desc='copy headers of the original image into the ' 'output (corrected) file') + rescale_intensities = traits.Bool( + False, usedefault=True, argstr='-r', + desc="""\ +At each iteration, a new intensity mapping is calculated and applied but there +is nothing which constrains the new intensity range to be within certain values. +The result is that the range can "drift" from the original at each iteration. +This option rescales to the [min,max] range of the original image intensities +within the user-specified mask.""") class N4BiasFieldCorrectionOutputSpec(TraitedSpec): @@ -314,7 +324,10 @@ class N4BiasFieldCorrectionOutputSpec(TraitedSpec): class N4BiasFieldCorrection(ANTSCommand): - """N4 is a variant of the popular N3 (nonparameteric nonuniform normalization) + """ + Bias field correction. + + N4 is a variant of the popular N3 (nonparameteric nonuniform normalization) retrospective bias correction algorithm. Based on the assumption that the corruption of the low frequency bias field can be modeled as a convolution of the intensity histogram by a Gaussian, the basic algorithmic protocol is to @@ -373,28 +386,9 @@ class N4BiasFieldCorrection(ANTSCommand): input_spec = N4BiasFieldCorrectionInputSpec output_spec = N4BiasFieldCorrectionOutputSpec - def _gen_filename(self, name): - if name == 'output_image': - output = self.inputs.output_image - if not isdefined(output): - _, name, ext = split_filename(self.inputs.input_image) - output = name + '_corrected' + ext - return output - - if name == 'bias_image': - output = self.inputs.bias_image - if not isdefined(output): - _, name, ext = split_filename(self.inputs.input_image) - output = name + '_bias' + ext - return output - return None - def _format_arg(self, name, trait_spec, value): - if ((name == 'output_image') and - (self.inputs.save_bias or isdefined(self.inputs.bias_image))): - bias_image = self._gen_filename('bias_image') - output = self._gen_filename('output_image') - newval = '[ %s, %s ]' % (output, bias_image) + if name == 'output_image' and getattr(self, '_out_bias_file', None): + newval = '[ %s, %s ]' % (value, getattr(self, '_out_bias_file')) return trait_spec.argstr % newval if name == 'bspline_fitting_distance': @@ -418,19 +412,25 @@ def _format_arg(self, name, trait_spec, value): name, trait_spec, value) def _parse_inputs(self, skip=None): - if skip is None: - skip = [] - skip += ['save_bias', 'bias_image'] + skip = (skip or []) + ['save_bias', 'bias_image'] + if self.inputs.save_bias or isdefined(self.inputs.bias_image): + bias_image = self.inputs.bias_image + if not isdefined(bias_image): + bias_image = fname_presuffix(os.path.basename(self.inputs.input_image), + suffix='_bias') + setattr(self, '_out_bias_file', bias_image) + else: + try: + delattr(self, '_out_bias_file') + except AttributeError: + pass return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) def _list_outputs(self): - outputs = self._outputs().get() - outputs['output_image'] = os.path.abspath( - self._gen_filename('output_image')) - - if self.inputs.save_bias or isdefined(self.inputs.bias_image): - outputs['bias_image'] = os.path.abspath( - self._gen_filename('bias_image')) + outputs = super(N4BiasFieldCorrection, self)._list_outputs() + bias_image = getattr(self, '_out_bias_file', None) + if bias_image: + outputs['bias_image'] = bias_image return outputs def _run_interface(self, runtime, correct_return_codes=(0, )): @@ -438,14 +438,14 @@ def _run_interface(self, runtime, correct_return_codes=(0, )): runtime, correct_return_codes) if self.inputs.copy_header and runtime.returncode in correct_return_codes: - self._copy_header(self._gen_filename('output_image')) - if self.inputs.save_bias or isdefined(self.inputs.bias_image): - self._copy_header(self._gen_filename('bias_image')) + self._copy_header(self.inputs.output_image) + if getattr(self, '_out_bias_file', None): + self._copy_header(getattr(self, '_out_bias_file')) return runtime def _copy_header(self, fname): - """Copy header from input image to an output image""" + """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) diff --git a/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py b/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py index c93d847d4b..b1fc710ea5 100644 --- a/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py +++ b/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py @@ -41,8 +41,14 @@ def test_N4BiasFieldCorrection_inputs(): ), output_image=dict( argstr='--output %s', - genfile=True, hash_files=False, + keep_extension=True, + name_source=['input_image'], + name_template='%s_corrected', + ), + rescale_intensities=dict( + argstr='-r', + usedefault=True, ), save_bias=dict( mandatory=True, From 355965dea991ce1ff091ced3a2d57b5ae12f7ffa Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 29 Aug 2019 21:13:03 -0700 Subject: [PATCH 2/7] fix: add _out_bias_file as a member, following @effigies' suggestion --- nipype/interfaces/ants/segmentation.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 277a716286..a62fdab8d9 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -386,9 +386,13 @@ class N4BiasFieldCorrection(ANTSCommand): input_spec = N4BiasFieldCorrectionInputSpec output_spec = N4BiasFieldCorrectionOutputSpec + def __init__(self, *args, **kwargs): + self._out_bias_file = None + super(N4BiasFieldCorrection, self).__init__(*args, **kwargs) + def _format_arg(self, name, trait_spec, value): - if name == 'output_image' and getattr(self, '_out_bias_file', None): - newval = '[ %s, %s ]' % (value, getattr(self, '_out_bias_file')) + if name == 'output_image' and self._out_bias_file: + newval = '[ %s, %s ]' % (value, self._out_bias_file) return trait_spec.argstr % newval if name == 'bspline_fitting_distance': @@ -413,24 +417,19 @@ def _format_arg(self, name, trait_spec, value): def _parse_inputs(self, skip=None): skip = (skip or []) + ['save_bias', 'bias_image'] + self._out_bias_file = None if self.inputs.save_bias or isdefined(self.inputs.bias_image): bias_image = self.inputs.bias_image if not isdefined(bias_image): bias_image = fname_presuffix(os.path.basename(self.inputs.input_image), suffix='_bias') - setattr(self, '_out_bias_file', bias_image) - else: - try: - delattr(self, '_out_bias_file') - except AttributeError: - pass + self._out_bias_file = bias_image return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) def _list_outputs(self): outputs = super(N4BiasFieldCorrection, self)._list_outputs() - bias_image = getattr(self, '_out_bias_file', None) - if bias_image: - outputs['bias_image'] = bias_image + if self._out_bias_file: + outputs['bias_image'] = self._out_bias_file return outputs def _run_interface(self, runtime, correct_return_codes=(0, )): @@ -439,8 +438,8 @@ def _run_interface(self, runtime, correct_return_codes=(0, )): if self.inputs.copy_header and runtime.returncode in correct_return_codes: self._copy_header(self.inputs.output_image) - if getattr(self, '_out_bias_file', None): - self._copy_header(getattr(self, '_out_bias_file')) + if self._out_bias_file: + self._copy_header(self._out_bias_file) return runtime From 0b695ae1bc0b95b60d20ac46cd3f09c2338e4f84 Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 30 Aug 2019 09:54:34 -0700 Subject: [PATCH 3/7] fix: make sure returned paths are absolute --- nipype/interfaces/ants/segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index a62fdab8d9..ab4c91fc94 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -422,7 +422,7 @@ def _parse_inputs(self, skip=None): bias_image = self.inputs.bias_image if not isdefined(bias_image): bias_image = fname_presuffix(os.path.basename(self.inputs.input_image), - suffix='_bias') + suffix='_bias', newpath=os.getcwd()) self._out_bias_file = bias_image return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) From 4af4c07fd0f4a9c70590b86074a53c4539dd9308 Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 30 Aug 2019 10:04:39 -0700 Subject: [PATCH 4/7] enh: note that new ``-r`` option works only with ants>=2.1.0 [skip ci] --- nipype/interfaces/ants/segmentation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index ab4c91fc94..7e361e7404 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -311,6 +311,7 @@ class N4BiasFieldCorrectionInputSpec(ANTSCommandInputSpec): rescale_intensities = traits.Bool( False, usedefault=True, argstr='-r', desc="""\ +[NOTE: Only ANTs>=2.1.0] At each iteration, a new intensity mapping is calculated and applied but there is nothing which constrains the new intensity range to be within certain values. The result is that the range can "drift" from the original at each iteration. From 2820411a861cc7dd805b183246ac2859d443943f Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 30 Aug 2019 13:17:50 -0700 Subject: [PATCH 5/7] fix: address @effigies' comments --- nipype/interfaces/ants/segmentation.py | 6 +++--- .../ants/tests/test_auto_N4BiasFieldCorrection.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 7e361e7404..fe5510af18 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -309,7 +309,7 @@ class N4BiasFieldCorrectionInputSpec(ANTSCommandInputSpec): desc='copy headers of the original image into the ' 'output (corrected) file') rescale_intensities = traits.Bool( - False, usedefault=True, argstr='-r', + False, usedefault=True, argstr='-r', min_ver='2.1.0', desc="""\ [NOTE: Only ANTs>=2.1.0] At each iteration, a new intensity mapping is calculated and applied but there @@ -423,8 +423,8 @@ def _parse_inputs(self, skip=None): bias_image = self.inputs.bias_image if not isdefined(bias_image): bias_image = fname_presuffix(os.path.basename(self.inputs.input_image), - suffix='_bias', newpath=os.getcwd()) - self._out_bias_file = bias_image + suffix='_bias') + self._out_bias_file = os.path.abspath(bias_image) return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) def _list_outputs(self): diff --git a/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py b/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py index b1fc710ea5..8d39968511 100644 --- a/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py +++ b/nipype/interfaces/ants/tests/test_auto_N4BiasFieldCorrection.py @@ -48,6 +48,7 @@ def test_N4BiasFieldCorrection_inputs(): ), rescale_intensities=dict( argstr='-r', + min_ver='2.1.0', usedefault=True, ), save_bias=dict( From 6818e979af91f45b0b9f66d2ed24fcc07da785ec Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 30 Aug 2019 14:43:27 -0700 Subject: [PATCH 6/7] fix: add ELLIPSIS to doctest --- nipype/interfaces/ants/segmentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index fe5510af18..98c09c2125 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -378,9 +378,9 @@ class N4BiasFieldCorrection(ANTSCommand): >>> n4_4.inputs.input_image = 'structural.nii' >>> n4_4.inputs.save_bias = True >>> n4_4.inputs.dimension = 3 - >>> n4_4.cmdline + >>> n4_4.cmdline # doctest: +ELLIPSIS 'N4BiasFieldCorrection -d 3 --input-image structural.nii \ ---output [ structural_corrected.nii, structural_bias.nii ]' +--output [ structural_corrected.nii, ...structural_bias.nii ]' """ _cmd = 'N4BiasFieldCorrection' From f37b65eeeaaf4b7e04fb43feeff16c61cf0ec178 Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 30 Aug 2019 14:55:44 -0700 Subject: [PATCH 7/7] fix: roll back ELLIPSIS, calculate abspath in _list_outputs() --- nipype/interfaces/ants/segmentation.py | 29 ++++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/nipype/interfaces/ants/segmentation.py b/nipype/interfaces/ants/segmentation.py index 98c09c2125..4118fd53bb 100644 --- a/nipype/interfaces/ants/segmentation.py +++ b/nipype/interfaces/ants/segmentation.py @@ -378,9 +378,9 @@ class N4BiasFieldCorrection(ANTSCommand): >>> n4_4.inputs.input_image = 'structural.nii' >>> n4_4.inputs.save_bias = True >>> n4_4.inputs.dimension = 3 - >>> n4_4.cmdline # doctest: +ELLIPSIS + >>> n4_4.cmdline 'N4BiasFieldCorrection -d 3 --input-image structural.nii \ ---output [ structural_corrected.nii, ...structural_bias.nii ]' +--output [ structural_corrected.nii, structural_bias.nii ]' """ _cmd = 'N4BiasFieldCorrection' @@ -388,6 +388,7 @@ class N4BiasFieldCorrection(ANTSCommand): output_spec = N4BiasFieldCorrectionOutputSpec def __init__(self, *args, **kwargs): + """Instantiate the N4BiasFieldCorrection interface.""" self._out_bias_file = None super(N4BiasFieldCorrection, self).__init__(*args, **kwargs) @@ -424,32 +425,28 @@ def _parse_inputs(self, skip=None): if not isdefined(bias_image): bias_image = fname_presuffix(os.path.basename(self.inputs.input_image), suffix='_bias') - self._out_bias_file = os.path.abspath(bias_image) + self._out_bias_file = bias_image return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip) def _list_outputs(self): outputs = super(N4BiasFieldCorrection, self)._list_outputs() - if self._out_bias_file: - outputs['bias_image'] = self._out_bias_file - return outputs - - def _run_interface(self, runtime, correct_return_codes=(0, )): - runtime = super(N4BiasFieldCorrection, self)._run_interface( - runtime, correct_return_codes) - if self.inputs.copy_header and runtime.returncode in correct_return_codes: - self._copy_header(self.inputs.output_image) - if self._out_bias_file: - self._copy_header(self._out_bias_file) + # Fix headers + if self.inputs.copy_header: + self._copy_header(outputs['output_image']) - return runtime + 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']) + 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_data(), in_img.affine, + 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)