1
1
"""Read/write ITK transforms."""
2
2
import numpy as np
3
- from .base import StringBasedStruct
3
+ from scipy .io import savemat as _save_mat
4
+ from nibabel .affines import from_matvec
5
+ from .base import StringBasedStruct , _read_mat
6
+
7
+ LPS = np .diag ([- 1 , - 1 , 1 , 1 ])
4
8
5
9
6
10
class ITKLinearTransform (StringBasedStruct ):
@@ -13,20 +17,24 @@ class ITKLinearTransform(StringBasedStruct):
13
17
('offset' , 'f4' , 3 ), # Center of rotation
14
18
])
15
19
dtype = template_dtype
20
+ # files_types = (('string', '.tfm'), ('binary', '.mat'))
21
+ # valid_exts = ('.tfm', '.mat')
16
22
17
- def __init__ (self ):
23
+ def __init__ (self , parameters = None , offset = None ):
18
24
"""Initialize with default offset and index."""
19
25
super ().__init__ ()
20
- self .structarr ['offset' ] = [0 , 0 , 0 ]
21
26
self .structarr ['index' ] = 1
27
+ self .structarr ['offset' ] = offset or [0 , 0 , 0 ]
22
28
self .structarr ['parameters' ] = np .eye (4 )
29
+ if parameters is not None :
30
+ self .structarr ['parameters' ] = parameters
23
31
24
32
def __str__ (self ):
25
33
"""Generate a string representation."""
26
34
sa = self .structarr
27
35
lines = [
28
36
'#Transform {:d}' .format (sa ['index' ]),
29
- 'Transform: MatrixOffsetTransformBase_double_3_3 ' ,
37
+ 'Transform: AffineTransform_float_3_3 ' ,
30
38
'Parameters: {}' .format (' ' .join (
31
39
['%g' % p
32
40
for p in sa ['parameters' ][:3 , :3 ].reshape (- 1 ).tolist () +
@@ -36,6 +44,33 @@ def __str__(self):
36
44
]
37
45
return '\n ' .join (lines )
38
46
47
+ def to_filename (self , filename ):
48
+ """Store this transform to a file with the appropriate format."""
49
+ if str (filename ).endswith ('.mat' ):
50
+ sa = self .structarr
51
+ affine = np .array (np .hstack ((
52
+ sa ['parameters' ][:3 , :3 ].reshape (- 1 ),
53
+ sa ['parameters' ][:3 , 3 ]))[..., np .newaxis ], dtype = 'f4' )
54
+ fixed = np .array (sa ['offset' ][..., np .newaxis ], dtype = 'f4' )
55
+ mdict = {
56
+ 'AffineTransform_float_3_3' : affine ,
57
+ 'fixed' : fixed ,
58
+ }
59
+ _save_mat (str (filename ), mdict , format = '4' )
60
+ return
61
+
62
+ with open (str (filename ), 'w' ) as f :
63
+ f .write (self .to_string ())
64
+
65
+ def to_ras (self ):
66
+ """Return a nitransforms' internal RAS matrix."""
67
+ sa = self .structarr
68
+ matrix = sa ['parameters' ]
69
+ offset = sa ['offset' ]
70
+ c_neg = from_matvec (np .eye (3 ), offset * - 1.0 )
71
+ c_pos = from_matvec (np .eye (3 ), offset )
72
+ return LPS .dot (c_pos .dot (matrix .dot (c_neg .dot (LPS ))))
73
+
39
74
def to_string (self , banner = None ):
40
75
"""Convert to a string directly writeable to file."""
41
76
string = '%s'
@@ -48,9 +83,47 @@ def to_string(self, banner=None):
48
83
return string % self
49
84
50
85
@classmethod
51
- def from_string (klass , string ):
86
+ def from_binary (cls , byte_stream , index = None ):
87
+ """Read the struct from a matlab binary file."""
88
+ mdict = _read_mat (byte_stream )
89
+ return cls .from_matlab_dict (mdict , index = index )
90
+
91
+ @classmethod
92
+ def from_fileobj (cls , fileobj , check = True ):
93
+ """Read the struct from a file object."""
94
+ if fileobj .name .endswith ('.mat' ):
95
+ return cls .from_binary (fileobj )
96
+ return cls .from_string (fileobj .read ())
97
+
98
+ @classmethod
99
+ def from_matlab_dict (cls , mdict , index = None ):
100
+ """Read the struct from a matlab dictionary."""
101
+ tf = cls ()
102
+ sa = tf .structarr
103
+ if index is not None :
104
+ raise NotImplementedError
105
+
106
+ sa ['index' ] = 1
107
+ parameters = np .eye (4 , dtype = 'f4' )
108
+ parameters [:3 , :3 ] = mdict ['AffineTransform_float_3_3' ][:- 3 ].reshape ((3 , 3 ))
109
+ parameters [:3 , 3 ] = mdict ['AffineTransform_float_3_3' ][- 3 :].flatten ()
110
+ sa ['parameters' ] = parameters
111
+ sa ['offset' ] = mdict ['fixed' ].flatten ()
112
+ return tf
113
+
114
+ @classmethod
115
+ def from_ras (cls , ras , index = 0 ):
116
+ """Create an ITK affine from a nitransform's RAS+ matrix."""
117
+ tf = cls ()
118
+ sa = tf .structarr
119
+ sa ['index' ] = index + 1
120
+ sa ['parameters' ] = LPS .dot (ras .dot (LPS ))
121
+ return tf
122
+
123
+ @classmethod
124
+ def from_string (cls , string ):
52
125
"""Read the struct from string."""
53
- tf = klass ()
126
+ tf = cls ()
54
127
sa = tf .structarr
55
128
lines = [l for l in string .splitlines ()
56
129
if l .strip ()]
@@ -61,19 +134,14 @@ def from_string(klass, string):
61
134
parameters = np .eye (4 , dtype = 'f4' )
62
135
sa ['index' ] = int (lines [0 ][lines [0 ].index ('T' ):].split ()[1 ])
63
136
sa ['offset' ] = np .genfromtxt ([lines [3 ].split (':' )[- 1 ].encode ()],
64
- dtype = klass .dtype ['offset' ])
137
+ dtype = cls .dtype ['offset' ])
65
138
vals = np .genfromtxt ([lines [2 ].split (':' )[- 1 ].encode ()],
66
139
dtype = 'f4' )
67
140
parameters [:3 , :3 ] = vals [:- 3 ].reshape ((3 , 3 ))
68
141
parameters [:3 , 3 ] = vals [- 3 :]
69
142
sa ['parameters' ] = parameters
70
143
return tf
71
144
72
- @classmethod
73
- def from_fileobj (klass , fileobj , check = True ):
74
- """Read the struct from a file object."""
75
- return klass .from_string (fileobj .read ())
76
-
77
145
78
146
class ITKLinearTransformArray (StringBasedStruct ):
79
147
"""A string-based structure for series of ITK linear transforms."""
@@ -89,33 +157,80 @@ def __init__(self,
89
157
check = True ):
90
158
"""Initialize with (optionally) a list of transforms."""
91
159
super ().__init__ (binaryblock , endianness , check )
92
- self ._xforms = []
93
- for i , mat in enumerate (xforms or []):
94
- xfm = ITKLinearTransform ()
95
- xfm ['parameters' ] = mat
96
- xfm ['index' ] = i + 1
97
- self ._xforms .append (xfm )
160
+ self .xforms = [ITKLinearTransform (parameters = mat )
161
+ for mat in xforms or []]
162
+
163
+ @property
164
+ def xforms (self ):
165
+ """Get the list of internal ITKLinearTransforms."""
166
+ return self ._xforms
167
+
168
+ @xforms .setter
169
+ def xforms (self , value ):
170
+ self ._xforms = value
171
+
172
+ # Update indexes
173
+ for i , val in enumerate (self ._xforms ):
174
+ val ['index' ] = i + 1
98
175
99
176
def __getitem__ (self , idx ):
100
177
"""Allow dictionary access to the transforms."""
101
178
if idx == 'xforms' :
102
179
return self ._xforms
103
180
if idx == 'nxforms' :
104
181
return len (self ._xforms )
105
- return super ().__getitem__ (idx )
182
+ raise KeyError (idx )
183
+
184
+ def to_filename (self , filename ):
185
+ """Store this transform to a file with the appropriate format."""
186
+ if str (filename ).endswith ('.mat' ):
187
+ raise NotImplementedError
188
+
189
+ with open (str (filename ), 'w' ) as f :
190
+ f .write (self .to_string ())
191
+
192
+ def to_ras (self ):
193
+ """Return a nitransforms' internal RAS matrix."""
194
+ return np .stack ([xfm .to_ras () for xfm in self ._xforms ])
106
195
107
196
def to_string (self ):
108
197
"""Convert to a string directly writeable to file."""
109
198
strings = []
110
- for i , xfm in enumerate (self ._xforms ):
199
+ for i , xfm in enumerate (self .xforms ):
111
200
xfm .structarr ['index' ] = i + 1
112
201
strings .append (xfm .to_string ())
113
202
return '\n ' .join (strings )
114
203
115
204
@classmethod
116
- def from_string (klass , string ):
205
+ def from_binary (cls , byte_stream ):
206
+ """Read the struct from a matlab binary file."""
207
+ mdict = _read_mat (byte_stream )
208
+ nxforms = mdict ['fixed' ].shape [0 ]
209
+
210
+ _self = cls ()
211
+ _self .xforms = [ITKLinearTransform .from_matlab_dict (mdict , i )
212
+ for i in range (nxforms )]
213
+ return _self
214
+
215
+ @classmethod
216
+ def from_fileobj (cls , fileobj , check = True ):
217
+ """Read the struct from a file object."""
218
+ if fileobj .name .endswith ('.mat' ):
219
+ return cls .from_binary (fileobj )
220
+ return cls .from_string (fileobj .read ())
221
+
222
+ @classmethod
223
+ def from_ras (cls , ras ):
224
+ """Create an ITK affine from a nitransform's RAS+ matrix."""
225
+ _self = cls ()
226
+ _self .xforms = [ITKLinearTransform .from_ras (ras [i , ...], i )
227
+ for i in range (ras .shape [0 ])]
228
+ return _self
229
+
230
+ @classmethod
231
+ def from_string (cls , string ):
117
232
"""Read the struct from string."""
118
- _self = klass ()
233
+ _self = cls ()
119
234
lines = [l .strip () for l in string .splitlines ()
120
235
if l .strip ()]
121
236
@@ -124,11 +239,6 @@ def from_string(klass, string):
124
239
125
240
string = '\n ' .join (lines [1 :])
126
241
for xfm in string .split ('#' )[1 :]:
127
- _self ._xforms .append (ITKLinearTransform .from_string (
242
+ _self .xforms .append (ITKLinearTransform .from_string (
128
243
'#%s' % xfm ))
129
244
return _self
130
-
131
- @classmethod
132
- def from_fileobj (klass , fileobj , check = True ):
133
- """Read the struct from a file object."""
134
- return klass .from_string (fileobj .read ())
0 commit comments