Skip to content

Commit 4c2ebda

Browse files
committed
enh: first draft of ITK's io module
1 parent 7650dbe commit 4c2ebda

File tree

3 files changed

+126
-28
lines changed

3 files changed

+126
-28
lines changed

nitransforms/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""
1919
from .linear import Affine
2020
from .nonlinear import DisplacementsFieldTransform
21+
from .io import itk
2122

2223

23-
__all__ = ['Affine', 'DisplacementsFieldTransform']
24+
__all__ = ['itk', 'Affine', 'DisplacementsFieldTransform']

nitransforms/io/itk.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Read/write ITK transforms."""
2+
import numpy as np
3+
from .base import StringBasedStruct
4+
5+
6+
class ITKLinearTransform(StringBasedStruct):
7+
template_dtype = np.dtype([
8+
('type', 'i4'),
9+
('id', 'i4'),
10+
('parameters', 'f4', (4, 4)),
11+
('offset', 'f4', 3), # Center of rotation
12+
])
13+
dtype = template_dtype
14+
15+
def __init__(self):
16+
super().__init__()
17+
self.structarr['offset'] = [0, 0, 0]
18+
self.structarr['id'] = 1
19+
20+
def to_string(self, banner=True):
21+
sa = self.structarr
22+
lines = [
23+
'#Transform {:d}'.format(sa['id']),
24+
'Transform: MatrixOffsetTransformBase_double_3_3',
25+
'Parameters: {}'.format(' '.join(
26+
['%g' % p
27+
for p in sa['parameters'][:3, :3].reshape(-1).tolist() +
28+
sa['parameters'][:3, 3].tolist()])),
29+
'FixedParameters: {:g} {:g} {:g}'.format(*sa['offset']),
30+
'',
31+
]
32+
if banner:
33+
lines.insert(0, '#Insight Transform File V1.0')
34+
return '\n'.join(lines)
35+
36+
@classmethod
37+
def from_string(klass, string):
38+
tf = klass()
39+
sa = tf.structarr
40+
lines = [l for l in string.splitlines()
41+
if l.strip()]
42+
assert lines[0][0] == '#'
43+
if lines[1][0] == '#':
44+
lines = lines[1:] # Drop banner with version
45+
46+
parameters = np.eye(4, dtype='f4')
47+
sa['id'] = int(lines[0][lines[0].index('T'):].split()[1])
48+
sa['offset'] = np.genfromtxt([lines[3].split(':')[-1].encode()],
49+
dtype=klass.dtype['offset'])
50+
vals = np.genfromtxt([lines[2].split(':')[-1].encode()],
51+
dtype='f4')
52+
parameters[:3, :3] = vals[:-3].reshape((3, 3))
53+
parameters[:3, 3] = vals[-3:]
54+
sa['parameters'] = parameters
55+
return tf
56+
57+
@classmethod
58+
def from_fileobj(klass, fileobj, check=True):
59+
return klass.from_string(fileobj.read())
60+
61+
62+
class ITKLinearTransformArray(StringBasedStruct):
63+
template_dtype = np.dtype([('nxforms', 'i4')])
64+
dtype = template_dtype
65+
_xforms = None
66+
67+
def __init__(self,
68+
xforms=None,
69+
binaryblock=None,
70+
endianness=None,
71+
check=True):
72+
super().__init__(binaryblock, endianness, check)
73+
self._xforms = []
74+
for mat in xforms or []:
75+
xfm = ITKLinearTransform()
76+
xfm['parameters'] = mat
77+
self._xforms.append(xfm)
78+
79+
def __getitem__(self, idx):
80+
if idx == 'xforms':
81+
return self._xforms
82+
if idx == 'nxforms':
83+
return len(self._xforms)
84+
return super().__getitem__(idx)
85+
86+
def to_string(self):
87+
strings = []
88+
for i, xfm in enumerate(self._xforms):
89+
xfm.structarr['id'] = i + 1
90+
strings.append(xfm.to_string(banner=False))
91+
strings.insert(0, '#Insight Transform File V1.0')
92+
return '\n'.join(strings)
93+
94+
@classmethod
95+
def from_string(klass, string):
96+
_self = klass()
97+
sa = _self.structarr
98+
99+
lines = [l.strip() for l in string.splitlines()
100+
if l.strip()]
101+
102+
if lines[0][0] != '#' or 'Insight Transform File V1.0' not in lines[0]:
103+
raise ValueError('Unknown Insight Transform File format.')
104+
105+
string = '\n'.join(lines[1:])
106+
for xfm in string.split('#')[1:]:
107+
_self._xforms.append(ITKLinearTransform.from_string(
108+
'#%s' % xfm))
109+
return _self
110+
111+
@classmethod
112+
def from_fileobj(klass, fileobj, check=True):
113+
return klass.from_string(fileobj.read())

nitransforms/linear.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,10 @@ def _to_hdf5(self, x5_root):
133133
def to_filename(self, filename, fmt='X5', moving=None):
134134
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
135135
if fmt.lower() in ['itk', 'ants', 'elastix']:
136+
itkobj = io.itk.ITKLinearTransformArray(
137+
xforms=[LPS.dot(m.dot(LPS)) for m in self.matrix])
136138
with open(filename, 'w') as f:
137-
f.write('#Insight Transform File V1.0\n')
138-
139-
for i in range(self.matrix.shape[0]):
140-
parameters = LPS.dot(self.matrix[i].dot(LPS))
141-
parameters = parameters[:3, :3].reshape(-1).tolist() + \
142-
parameters[:3, 3].tolist()
143-
itkfmt = """\
144-
#Transform {0}
145-
Transform: MatrixOffsetTransformBase_double_3_3
146-
Parameters: {1}
147-
FixedParameters: 0 0 0\n""".format
148-
f.write(itkfmt(i, ' '.join(['%g' % p for p in parameters])))
139+
f.write(itkobj.to_string())
149140
return filename
150141

151142
if fmt.lower() == 'afni':
@@ -239,23 +230,16 @@ def load(filename, fmt='X5', reference=None):
239230
"""Load a linear transform."""
240231
if fmt.lower() in ['itk', 'ants', 'elastix', 'nifty']:
241232
with open(filename) as itkfile:
242-
itkxfm = itkfile.read().splitlines()
243-
244-
if 'Insight Transform File' not in itkxfm[0]:
245-
raise ValueError(
246-
"File %s does not look like an ITK transform text file." % filename)
233+
itkxfm = io.itk.ITKLinearTransformArray.from_fileobj(
234+
itkfile)
247235

248236
matlist = []
249-
for nxfm in range(len(itkxfm[1:]) // 4):
250-
parameters = np.fromstring(itkxfm[nxfm * 4 + 3].split(':')[-1].strip(),
251-
dtype=float, sep=' ')
252-
offset = np.fromstring(itkxfm[nxfm * 4 + 4].split(':')[-1].strip(),
253-
dtype=float, sep=' ')
254-
if len(parameters) == 12:
255-
matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:])
256-
c_neg = from_matvec(np.eye(3), offset * -1.0)
257-
c_pos = from_matvec(np.eye(3), offset)
258-
matlist.append(LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS)))))
237+
for xfm in itkxfm['xforms']:
238+
matrix = xfm['parameters']
239+
offset = xfm['offset']
240+
c_neg = from_matvec(np.eye(3), offset * -1.0)
241+
c_pos = from_matvec(np.eye(3), offset)
242+
matlist.append(LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS)))))
259243
matrix = np.stack(matlist)
260244
# elif fmt.lower() == 'afni':
261245
# parameters = LPS.dot(self.matrix.dot(LPS))

0 commit comments

Comments
 (0)