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 , TransformFileError
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
- self .structarr ['index ' ] = 1
26
+ self .structarr ['index ' ] = 0
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,21 +44,84 @@ 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'
42
77
43
78
if banner is None :
44
- banner = self .structarr ['index' ] == 1
79
+ banner = self .structarr ['index' ] == 0
45
80
46
81
if banner :
47
82
string = '#Insight Transform File V1.0\n %s'
48
83
return string % self
49
84
50
85
@classmethod
51
- def from_string (klass , string ):
86
+ def from_binary (cls , byte_stream , index = 0 ):
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 = 0 ):
100
+ """Read the struct from a matlab dictionary."""
101
+ tf = cls ()
102
+ sa = tf .structarr
103
+
104
+ sa ['index' ] = index
105
+ parameters = np .eye (4 , dtype = 'f4' )
106
+ parameters [:3 , :3 ] = mdict ['AffineTransform_float_3_3' ][:- 3 ].reshape ((3 , 3 ))
107
+ parameters [:3 , 3 ] = mdict ['AffineTransform_float_3_3' ][- 3 :].flatten ()
108
+ sa ['parameters' ] = parameters
109
+ sa ['offset' ] = mdict ['fixed' ].flatten ()
110
+ return tf
111
+
112
+ @classmethod
113
+ def from_ras (cls , ras , index = 0 ):
114
+ """Create an ITK affine from a nitransform's RAS+ matrix."""
115
+ tf = cls ()
116
+ sa = tf .structarr
117
+ sa ['index' ] = index
118
+ sa ['parameters' ] = LPS .dot (ras .dot (LPS ))
119
+ return tf
120
+
121
+ @classmethod
122
+ def from_string (cls , string ):
52
123
"""Read the struct from string."""
53
- tf = klass ()
124
+ tf = cls ()
54
125
sa = tf .structarr
55
126
lines = [l for l in string .splitlines ()
56
127
if l .strip ()]
@@ -61,19 +132,14 @@ def from_string(klass, string):
61
132
parameters = np .eye (4 , dtype = 'f4' )
62
133
sa ['index' ] = int (lines [0 ][lines [0 ].index ('T' ):].split ()[1 ])
63
134
sa ['offset' ] = np .genfromtxt ([lines [3 ].split (':' )[- 1 ].encode ()],
64
- dtype = klass .dtype ['offset' ])
135
+ dtype = cls .dtype ['offset' ])
65
136
vals = np .genfromtxt ([lines [2 ].split (':' )[- 1 ].encode ()],
66
137
dtype = 'f4' )
67
138
parameters [:3 , :3 ] = vals [:- 3 ].reshape ((3 , 3 ))
68
139
parameters [:3 , 3 ] = vals [- 3 :]
69
140
sa ['parameters' ] = parameters
70
141
return tf
71
142
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
143
78
144
class ITKLinearTransformArray (StringBasedStruct ):
79
145
"""A string-based structure for series of ITK linear transforms."""
@@ -89,33 +155,74 @@ def __init__(self,
89
155
check = True ):
90
156
"""Initialize with (optionally) a list of transforms."""
91
157
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 )
158
+ self .xforms = [ITKLinearTransform (parameters = mat )
159
+ for mat in xforms or []]
160
+
161
+ @property
162
+ def xforms (self ):
163
+ """Get the list of internal ITKLinearTransforms."""
164
+ return self ._xforms
165
+
166
+ @xforms .setter
167
+ def xforms (self , value ):
168
+ self ._xforms = list (value )
169
+
170
+ # Update indexes
171
+ for i , val in enumerate (self .xforms ):
172
+ val ['index' ] = i
98
173
99
174
def __getitem__ (self , idx ):
100
175
"""Allow dictionary access to the transforms."""
101
176
if idx == 'xforms' :
102
177
return self ._xforms
103
178
if idx == 'nxforms' :
104
179
return len (self ._xforms )
105
- return super ().__getitem__ (idx )
180
+ raise KeyError (idx )
181
+
182
+ def to_filename (self , filename ):
183
+ """Store this transform to a file with the appropriate format."""
184
+ if str (filename ).endswith ('.mat' ):
185
+ raise TransformFileError ("Please use the ITK's new .h5 format." )
186
+
187
+ with open (str (filename ), 'w' ) as f :
188
+ f .write (self .to_string ())
189
+
190
+ def to_ras (self ):
191
+ """Return a nitransforms' internal RAS matrix."""
192
+ return np .stack ([xfm .to_ras () for xfm in self .xforms ])
106
193
107
194
def to_string (self ):
108
195
"""Convert to a string directly writeable to file."""
109
196
strings = []
110
- for i , xfm in enumerate (self ._xforms ):
111
- xfm .structarr ['index' ] = i + 1
197
+ for i , xfm in enumerate (self .xforms ):
198
+ xfm .structarr ['index' ] = i
112
199
strings .append (xfm .to_string ())
113
200
return '\n ' .join (strings )
114
201
115
202
@classmethod
116
- def from_string (klass , string ):
203
+ def from_binary (cls , byte_stream ):
204
+ """Read the struct from a matlab binary file."""
205
+ raise TransformFileError ("Please use the ITK's new .h5 format." )
206
+
207
+ @classmethod
208
+ def from_fileobj (cls , fileobj , check = True ):
209
+ """Read the struct from a file object."""
210
+ if fileobj .name .endswith ('.mat' ):
211
+ return cls .from_binary (fileobj )
212
+ return cls .from_string (fileobj .read ())
213
+
214
+ @classmethod
215
+ def from_ras (cls , ras ):
216
+ """Create an ITK affine from a nitransform's RAS+ matrix."""
217
+ _self = cls ()
218
+ _self .xforms = [ITKLinearTransform .from_ras (ras [i , ...], i )
219
+ for i in range (ras .shape [0 ])]
220
+ return _self
221
+
222
+ @classmethod
223
+ def from_string (cls , string ):
117
224
"""Read the struct from string."""
118
- _self = klass ()
225
+ _self = cls ()
119
226
lines = [l .strip () for l in string .splitlines ()
120
227
if l .strip ()]
121
228
@@ -124,11 +231,6 @@ def from_string(klass, string):
124
231
125
232
string = '\n ' .join (lines [1 :])
126
233
for xfm in string .split ('#' )[1 :]:
127
- _self ._xforms .append (ITKLinearTransform .from_string (
234
+ _self .xforms .append (ITKLinearTransform .from_string (
128
235
'#%s' % xfm ))
129
236
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