|
| 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 |
0 commit comments