Skip to content

Commit 292e2e8

Browse files
authored
Merge pull request #10 from effigies/enh/ants-utils-overhaul
RF: Move header copying to a mixin
2 parents f88add4 + 545a5fe commit 292e2e8

File tree

9 files changed

+169
-120
lines changed

9 files changed

+169
-120
lines changed

nipype/interfaces/ants/base.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,3 @@ def set_default_num_threads(cls, num_threads):
122122
@property
123123
def version(self):
124124
return Info.version()
125-
126-
127-
class FixHeaderANTSCommand(ANTSCommand):
128-
"""Fix header if the copy_header input is on."""
129-
130-
def aggregate_outputs(self, runtime=None, needed_outputs=None):
131-
"""Overload the aggregation with header replacement, if required."""
132-
outputs = super(FixHeaderANTSCommand, self).aggregate_outputs(
133-
runtime, needed_outputs)
134-
if self.inputs.copy_header: # Fix headers
135-
_copy_header(
136-
self.inputs.op1, outputs["output_image"], keep_dtype=True
137-
)
138-
return outputs

nipype/interfaces/ants/segmentation.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import os
33
from glob import glob
44
from ...external.due import BibTeX
5-
from ...utils.imagemanip import copy_header as _copy_header
65
from ...utils.filemanip import split_filename, copyfile, which, fname_presuffix
76
from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined
7+
from ..mixins import CopyHeaderInterface
88
from .base import ANTSCommand, ANTSCommandInputSpec
99

1010

@@ -420,7 +420,7 @@ class N4BiasFieldCorrectionOutputSpec(TraitedSpec):
420420
bias_image = File(exists=True, desc="Estimated bias")
421421

422422

423-
class N4BiasFieldCorrection(ANTSCommand):
423+
class N4BiasFieldCorrection(ANTSCommand, CopyHeaderInterface):
424424
"""
425425
Bias field correction.
426426
@@ -491,6 +491,10 @@ class N4BiasFieldCorrection(ANTSCommand):
491491
_cmd = "N4BiasFieldCorrection"
492492
input_spec = N4BiasFieldCorrectionInputSpec
493493
output_spec = N4BiasFieldCorrectionOutputSpec
494+
_copy_header_map = {
495+
"output_image": ("input_image", False),
496+
"bias_image": ("input_image", True),
497+
}
494498

495499
def __init__(self, *args, **kwargs):
496500
"""Instantiate the N4BiasFieldCorrection interface."""
@@ -533,20 +537,6 @@ def _parse_inputs(self, skip=None):
533537
self._out_bias_file = bias_image
534538
return super(N4BiasFieldCorrection, self)._parse_inputs(skip=skip)
535539

536-
def _list_outputs(self):
537-
outputs = super(N4BiasFieldCorrection, self)._list_outputs()
538-
539-
# Fix headers
540-
if self.inputs.copy_header:
541-
_copy_header(self.inputs.input_image, outputs["output_image"],
542-
keep_dtype=False)
543-
544-
if self._out_bias_file:
545-
outputs["bias_image"] = os.path.abspath(self._out_bias_file)
546-
if self.inputs.copy_header:
547-
_copy_header(self.inputs.input_image, outputs["bias_image"])
548-
return outputs
549-
550540

551541
class CorticalThicknessInputSpec(ANTSCommandInputSpec):
552542
dimension = traits.Enum(
@@ -1501,7 +1491,7 @@ def _format_arg(self, opt, spec, val):
15011491
for option in (
15021492
self.inputs.out_intensity_fusion_name_format,
15031493
self.inputs.out_label_post_prob_name_format,
1504-
self.inputs.out_atlas_voting_weight_name_format
1494+
self.inputs.out_atlas_voting_weight_name_format,
15051495
):
15061496
if isdefined(option):
15071497
args.append(option)

nipype/interfaces/ants/tests/test_auto_AntsJointFusion.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

nipype/interfaces/ants/tests/test_auto_FixHeaderANTSCommand.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

nipype/interfaces/ants/utils.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""ANTs' utilities."""
22
import os
33
from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiObject
4-
from .base import ANTSCommandInputSpec, ANTSCommand, FixHeaderANTSCommand
4+
from ..mixins import CopyHeaderInterface
5+
from .base import ANTSCommandInputSpec, ANTSCommand
56

67

78
class ImageMathInputSpec(ANTSCommandInputSpec):
@@ -67,7 +68,7 @@ class ImageMathOuputSpec(TraitedSpec):
6768
output_image = File(exists=True, desc="output image file")
6869

6970

70-
class ImageMath(FixHeaderANTSCommand):
71+
class ImageMath(ANTSCommand, CopyHeaderInterface):
7172
"""
7273
Operations over images.
7374
@@ -96,6 +97,7 @@ class ImageMath(FixHeaderANTSCommand):
9697
_cmd = "ImageMath"
9798
input_spec = ImageMathInputSpec
9899
output_spec = ImageMathOuputSpec
100+
_copy_header_map = {"output_image": "op1"}
99101

100102

101103
class ResampleImageBySpacingInputSpec(ANTSCommandInputSpec):
@@ -146,7 +148,7 @@ class ResampleImageBySpacingOutputSpec(TraitedSpec):
146148
output_image = File(exists=True, desc="resampled file")
147149

148150

149-
class ResampleImageBySpacing(FixHeaderANTSCommand):
151+
class ResampleImageBySpacing(ANTSCommand, CopyHeaderInterface):
150152
"""
151153
Resample an image with a given spacing.
152154
@@ -182,6 +184,7 @@ class ResampleImageBySpacing(FixHeaderANTSCommand):
182184
_cmd = "ResampleImageBySpacing"
183185
input_spec = ResampleImageBySpacingInputSpec
184186
output_spec = ResampleImageBySpacingOutputSpec
187+
_copy_header_map = {"output_image": "input_image"}
185188

186189
def _format_arg(self, name, trait_spec, value):
187190
if name == "out_spacing":
@@ -248,7 +251,7 @@ class ThresholdImageOutputSpec(TraitedSpec):
248251
output_image = File(exists=True, desc="resampled file")
249252

250253

251-
class ThresholdImage(FixHeaderANTSCommand):
254+
class ThresholdImage(ANTSCommand, CopyHeaderInterface):
252255
"""
253256
Apply thresholds on images.
254257
@@ -277,6 +280,7 @@ class ThresholdImage(FixHeaderANTSCommand):
277280
_cmd = "ThresholdImage"
278281
input_spec = ThresholdImageInputSpec
279282
output_spec = ThresholdImageOutputSpec
283+
_copy_header_map = {"output_image": "input_image"}
280284

281285

282286
class AIInputSpec(ANTSCommandInputSpec):

nipype/interfaces/mixins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
ReportCapableInputSpec,
44
ReportCapableOutputSpec,
55
)
6+
from .fixheader import CopyHeaderInputSpec, CopyHeaderInterface

nipype/interfaces/mixins/fixheader.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from ..base import BaseInterface, BaseInterfaceInputSpec, traits
2+
from ...utils.imagemanip import copy_header as _copy_header
3+
4+
5+
class CopyHeaderInputSpec(BaseInterfaceInputSpec):
6+
copy_header = traits.Bool(
7+
desc="Copy headers of the input image into the output image"
8+
)
9+
10+
11+
class CopyHeaderInterface(BaseInterface):
12+
""" Copy headers if the copy_header input is ``True``
13+
14+
This interface mixin adds a post-run hook that allows for copying
15+
an input header to an output file.
16+
The subclass should specify a ``_copy_header_map`` that maps the **output**
17+
image to the **input** image whose header should be copied.
18+
19+
This feature is intended for tools that are intended to adjust voxel data without
20+
modifying the header, but for some reason do not reliably preserve the header.
21+
22+
Here we show an example interface that takes advantage of the mixin by simply
23+
setting the data block:
24+
25+
>>> import os
26+
>>> import numpy as np
27+
>>> import nibabel as nb
28+
>>> from nipype.interfaces.base import SimpleInterface, TraitedSpec, File
29+
>>> from nipype.interfaces.mixins import CopyHeaderInputSpec, CopyHeaderInterface
30+
31+
>>> class ZerofileInputSpec(CopyHeaderInputSpec):
32+
... in_file = File(mandatory=True, exists=True)
33+
34+
>>> class ZerofileOutputSpec(TraitedSpec):
35+
... out_file = File()
36+
37+
>>> class ZerofileInterface(SimpleInterface, CopyHeaderInterface):
38+
... input_spec = ZerofileInputSpec
39+
... output_spec = ZerofileOutputSpec
40+
... _copy_header_map = {'out_file': 'in_file'}
41+
...
42+
... def _run_interface(self, runtime):
43+
... img = nb.load(self.inputs.in_file)
44+
... # Just set the data. Let the CopyHeaderInterface mixin fix the affine and header.
45+
... nb.Nifti1Image(np.zeros(img.shape, dtype=np.uint8), None).to_filename('out.nii')
46+
... self._results = {'out_file': os.path.abspath('out.nii')}
47+
... return runtime
48+
49+
Consider a file of all ones and a non-trivial affine:
50+
51+
>>> in_file = 'test.nii'
52+
>>> nb.Nifti1Image(np.ones((5,5,5), dtype=np.int16),
53+
... affine=np.diag((4, 3, 2, 1))).to_filename(in_file)
54+
55+
The default behavior would produce a file with similar data:
56+
57+
>>> res = ZerofileInterface(in_file=in_file).run()
58+
>>> out_img = nb.load(res.outputs.out_file)
59+
>>> out_img.shape
60+
(5, 5, 5)
61+
>>> np.all(out_img.get_fdata() == 0)
62+
True
63+
64+
An updated data type:
65+
66+
>>> out_img.get_data_dtype()
67+
dtype('uint8')
68+
69+
But a different affine:
70+
71+
>>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1)))
72+
False
73+
74+
With ``copy_header=True``, then the affine is also equal:
75+
76+
>>> res = ZerofileInterface(in_file=in_file, copy_header=True).run()
77+
>>> out_img = nb.load(res.outputs.out_file)
78+
>>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1)))
79+
True
80+
81+
The data properties remain as expected:
82+
83+
>>> out_img.shape
84+
(5, 5, 5)
85+
>>> out_img.get_data_dtype()
86+
dtype('uint8')
87+
>>> np.all(out_img.get_fdata() == 0)
88+
True
89+
90+
By default, the data type of the output file is permitted to vary from the
91+
inputs. That is, the data type is preserved.
92+
If the data type of the original file is preferred, the ``_copy_header_map``
93+
can indicate the output data type should **not** be preserved by providing a
94+
tuple of the input and ``False``.
95+
96+
>>> ZerofileInterface._copy_header_map['out_file'] = ('in_file', False)
97+
98+
>>> res = ZerofileInterface(in_file=in_file, copy_header=True).run()
99+
>>> out_img = nb.load(res.outputs.out_file)
100+
>>> out_img.get_data_dtype()
101+
dtype('<i2')
102+
103+
Again, the affine is updated.
104+
105+
>>> np.array_equal(out_img.affine, np.diag((4, 3, 2, 1)))
106+
True
107+
>>> out_img.shape
108+
(5, 5, 5)
109+
>>> np.all(out_img.get_fdata() == 0)
110+
True
111+
112+
Providing a tuple where the second value is ``True`` is also permissible to
113+
achieve the default behavior.
114+
115+
"""
116+
117+
_copy_header_map = None
118+
119+
def _post_run_hook(self, runtime):
120+
"""Copy headers for outputs, if required."""
121+
runtime = super()._post_run_hook(runtime)
122+
123+
if self._copy_header_map is None or not self.inputs.copy_header:
124+
return runtime
125+
126+
inputs = self.inputs.get_traitsfree()
127+
outputs = self.aggregate_outputs(runtime=runtime).get_traitsfree()
128+
for out, inp in self._copy_header_map.items():
129+
keep_dtype = True
130+
if isinstance(inp, tuple):
131+
inp, keep_dtype = inp
132+
_copy_header(inputs[inp], outputs[out], keep_dtype=keep_dtype)
133+
134+
return runtime
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from ..fixheader import CopyHeaderInterface
3+
4+
5+
def test_CopyHeaderInterface_inputs():
6+
input_map = dict()
7+
inputs = CopyHeaderInterface.input_spec()
8+
9+
for key, metadata in list(input_map.items()):
10+
for metakey, value in list(metadata.items()):
11+
assert getattr(inputs.traits()[key], metakey) == value

0 commit comments

Comments
 (0)