Skip to content

Commit 95c5f37

Browse files
authored
Merge pull request #2572 from effigies/enh/reorient
ENH: Reorient interface
2 parents eb4d8b4 + ea5a5e2 commit 95c5f37

File tree

3 files changed

+239
-0
lines changed

3 files changed

+239
-0
lines changed

nipype/interfaces/image.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
5+
import numpy as np
6+
import nibabel as nb
7+
8+
from ..utils.filemanip import fname_presuffix
9+
from .base import (SimpleInterface, TraitedSpec, BaseInterfaceInputSpec,
10+
traits, File)
11+
12+
_axes = ('RL', 'AP', 'SI')
13+
_orientations = tuple(
14+
''.join((x[i], y[j], z[k]))
15+
for x in _axes for y in _axes for z in _axes
16+
if x != y != z != x
17+
for i in (0, 1) for j in (0, 1) for k in (0, 1))
18+
19+
20+
class ReorientInputSpec(BaseInterfaceInputSpec):
21+
in_file = File(exists=True, mandatory=True, desc='Input image')
22+
orientation = traits.Enum(_orientations, usedefault=True,
23+
desc='Target axis orientation')
24+
25+
26+
class ReorientOutputSpec(TraitedSpec):
27+
out_file = File(exists=True, desc='Reoriented image')
28+
transform = File(exists=True,
29+
desc='Affine transform from input orientation to output')
30+
31+
32+
class Reorient(SimpleInterface):
33+
"""Conform an image to a given orientation
34+
35+
Flips and reorder the image data array so that the axes match the
36+
directions indicated in ``orientation``.
37+
The default ``RAS`` orientation corresponds to the first axis being ordered
38+
from left to right, the second axis from posterior to anterior, and the
39+
third axis from inferior to superior.
40+
41+
For oblique images, the original orientation is considered to be the
42+
closest plumb orientation.
43+
44+
No resampling is performed, and thus the output image is not de-obliqued
45+
or registered to any other image or template.
46+
47+
The effective transform is calculated from the original affine matrix to
48+
the reoriented affine matrix.
49+
50+
Examples
51+
--------
52+
53+
If an image is not reoriented, the original file is not modified
54+
55+
>>> import numpy as np
56+
>>> from nipype.interfaces.image import Reorient
57+
>>> reorient = Reorient(orientation='LPS')
58+
>>> reorient.inputs.in_file = 'segmentation0.nii.gz'
59+
>>> res = reorient.run()
60+
>>> res.outputs.out_file
61+
'segmentation0.nii.gz'
62+
63+
>>> print(np.loadtxt(res.outputs.transform))
64+
[[1. 0. 0. 0.]
65+
[0. 1. 0. 0.]
66+
[0. 0. 1. 0.]
67+
[0. 0. 0. 1.]]
68+
69+
>>> reorient.inputs.orientation = 'RAS'
70+
>>> res = reorient.run()
71+
>>> res.outputs.out_file # doctest: +ELLIPSIS
72+
'.../segmentation0_ras.nii.gz'
73+
74+
>>> print(np.loadtxt(res.outputs.transform))
75+
[[-1. 0. 0. 60.]
76+
[ 0. -1. 0. 72.]
77+
[ 0. 0. 1. 0.]
78+
[ 0. 0. 0. 1.]]
79+
80+
"""
81+
input_spec = ReorientInputSpec
82+
output_spec = ReorientOutputSpec
83+
84+
def _run_interface(self, runtime):
85+
from nibabel.orientations import (
86+
axcodes2ornt, ornt_transform, inv_ornt_aff)
87+
88+
fname = self.inputs.in_file
89+
orig_img = nb.load(fname)
90+
91+
# Find transform from current (approximate) orientation to
92+
# target, in nibabel orientation matrix and affine forms
93+
orig_ornt = nb.io_orientation(orig_img.affine)
94+
targ_ornt = axcodes2ornt(self.inputs.orientation)
95+
transform = ornt_transform(orig_ornt, targ_ornt)
96+
affine_xfm = inv_ornt_aff(transform, orig_img.shape)
97+
98+
# Check can be eliminated when minimum nibabel version >= 2.2
99+
if hasattr(orig_img, 'as_reoriented'):
100+
reoriented = orig_img.as_reoriented(transform)
101+
else:
102+
reoriented = _as_reoriented_backport(orig_img, transform)
103+
104+
# Image may be reoriented
105+
if reoriented is not orig_img:
106+
suffix = '_' + self.inputs.orientation.lower()
107+
out_name = fname_presuffix(fname, suffix=suffix,
108+
newpath=runtime.cwd)
109+
reoriented.to_filename(out_name)
110+
else:
111+
out_name = fname
112+
113+
mat_name = fname_presuffix(fname, suffix='.mat',
114+
newpath=runtime.cwd, use_ext=False)
115+
np.savetxt(mat_name, affine_xfm, fmt='%.08f')
116+
117+
self._results['out_file'] = out_name
118+
self._results['transform'] = mat_name
119+
120+
return runtime
121+
122+
123+
def _as_reoriented_backport(img, ornt):
124+
"""Backport of img.as_reoriented as of nibabel 2.2.0"""
125+
from nibabel.orientations import inv_ornt_aff
126+
if np.array_equal(ornt, [[0, 1], [1, 1], [2, 1]]):
127+
return img
128+
129+
t_arr = nb.apply_orientation(img.get_data(), ornt)
130+
new_aff = img.affine.dot(inv_ornt_aff(ornt, img.shape))
131+
reoriented = img.__class__(t_arr, new_aff, img.header)
132+
133+
if isinstance(reoriented, nb.Nifti1Pair):
134+
# Also apply the transform to the dim_info fields
135+
new_dim = list(reoriented.header.get_dim_info())
136+
for idx, value in enumerate(new_dim):
137+
# For each value, leave as None if it was that way,
138+
# otherwise check where we have mapped it to
139+
if value is None:
140+
continue
141+
new_dim[idx] = np.where(ornt[:, 0] == idx)[0]
142+
143+
reoriented.header.set_dim_info(*new_dim)
144+
145+
return reoriented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from __future__ import unicode_literals
3+
from ..image import Reorient
4+
5+
6+
def test_Reorient_inputs():
7+
input_map = dict(
8+
ignore_exception=dict(
9+
deprecated='1.0.0',
10+
nohash=True,
11+
usedefault=True,
12+
),
13+
in_file=dict(mandatory=True, ),
14+
orientation=dict(usedefault=True, ),
15+
)
16+
inputs = Reorient.input_spec()
17+
18+
for key, metadata in list(input_map.items()):
19+
for metakey, value in list(metadata.items()):
20+
assert getattr(inputs.traits()[key], metakey) == value
21+
def test_Reorient_outputs():
22+
output_map = dict(
23+
out_file=dict(),
24+
transform=dict(),
25+
)
26+
outputs = Reorient.output_spec()
27+
28+
for key, metadata in list(output_map.items()):
29+
for metakey, value in list(metadata.items()):
30+
assert getattr(outputs.traits()[key], metakey) == value

nipype/interfaces/tests/test_image.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
import numpy as np
4+
import nibabel as nb
5+
import pytest
6+
7+
from nibabel.orientations import axcodes2ornt, ornt_transform
8+
9+
from ..image import _as_reoriented_backport, _orientations
10+
from ... import LooseVersion
11+
12+
nibabel22 = LooseVersion(nb.__version__) >= LooseVersion('2.2.0')
13+
14+
15+
@pytest.mark.skipif(not nibabel22,
16+
reason="Old nibabel - can't directly compare")
17+
def test_reorientation_backport():
18+
pixdims = ((1, 1, 1), (2, 2, 3))
19+
data = np.random.normal(size=(17, 18, 19, 2))
20+
21+
for pixdim in pixdims:
22+
# Generate a randomly rotated affine
23+
angles = np.random.uniform(-np.pi, np.pi, 3) * [1, 0.5, 1]
24+
rot = nb.eulerangles.euler2mat(*angles)
25+
scale = np.diag(pixdim)
26+
translation = np.array((17, 18, 19)) / 2
27+
affine = nb.affines.from_matvec(rot.dot(scale), translation)
28+
29+
# Create image
30+
img = nb.Nifti1Image(data, affine)
31+
dim_info = {'freq': 0, 'phase': 1, 'slice': 2}
32+
img.header.set_dim_info(**dim_info)
33+
34+
# Find a random, non-identity transform
35+
targ_ornt = orig_ornt = nb.io_orientation(affine)
36+
while np.array_equal(targ_ornt, orig_ornt):
37+
new_code = np.random.choice(_orientations)
38+
targ_ornt = axcodes2ornt(new_code)
39+
40+
identity = ornt_transform(orig_ornt, orig_ornt)
41+
transform = ornt_transform(orig_ornt, targ_ornt)
42+
43+
# Identity transform returns exact image
44+
assert img.as_reoriented(identity) is img
45+
assert _as_reoriented_backport(img, identity) is img
46+
47+
reoriented_a = img.as_reoriented(transform)
48+
reoriented_b = _as_reoriented_backport(img, transform)
49+
50+
flips_only = img.shape == reoriented_a.shape
51+
52+
# Reorientation changes affine and data array
53+
assert not np.allclose(img.affine, reoriented_a.affine)
54+
assert not (flips_only and
55+
np.allclose(img.get_data(), reoriented_a.get_data()))
56+
# Dimension info changes iff axes are reordered
57+
assert flips_only == np.array_equal(img.header.get_dim_info(),
58+
reoriented_a.header.get_dim_info())
59+
60+
# Both approaches produce equivalent images
61+
assert np.allclose(reoriented_a.affine, reoriented_b.affine)
62+
assert np.array_equal(reoriented_a.get_data(), reoriented_b.get_data())
63+
assert np.array_equal(reoriented_a.header.get_dim_info(),
64+
reoriented_b.header.get_dim_info())

0 commit comments

Comments
 (0)