15
15
EQUALITY_TOL = 1e-5
16
16
17
17
18
- class ImageSpace (object ):
18
+ class ImageGrid (object ):
19
19
"""Class to represent spaces of gridded data (images)."""
20
20
21
21
__slots__ = ['_affine' , '_shape' , '_ndim' , '_ndindex' , '_coords' , '_nvox' ,
22
22
'_inverse' ]
23
23
24
24
def __init__ (self , image ):
25
+ """Create a gridded sampling reference."""
25
26
self ._affine = image .affine
26
27
self ._shape = image .shape
27
28
self ._ndim = len (image .shape )
@@ -34,26 +35,32 @@ def __init__(self, image):
34
35
35
36
@property
36
37
def affine (self ):
38
+ """Access the indexes-to-RAS affine."""
37
39
return self ._affine
38
40
39
41
@property
40
42
def inverse (self ):
43
+ """Access the RAS-to-indexes affine."""
41
44
return self ._inverse
42
45
43
46
@property
44
47
def shape (self ):
48
+ """Access the space's size of each dimension."""
45
49
return self ._shape
46
50
47
51
@property
48
52
def ndim (self ):
53
+ """Access the number of dimensions."""
49
54
return self ._ndim
50
55
51
56
@property
52
57
def nvox (self ):
58
+ """Access the total number of voxels."""
53
59
return self ._nvox
54
60
55
61
@property
56
62
def ndindex (self ):
63
+ """List the indexes corresponding to the space grid."""
57
64
if self ._ndindex is None :
58
65
indexes = tuple ([np .arange (s ) for s in self ._shape ])
59
66
self ._ndindex = np .array (np .meshgrid (
@@ -62,6 +69,7 @@ def ndindex(self):
62
69
63
70
@property
64
71
def ndcoords (self ):
72
+ """List the physical coordinates of this gridded space samples."""
65
73
if self ._coords is None :
66
74
self ._coords = np .tensordot (
67
75
self ._affine ,
@@ -70,14 +78,15 @@ def ndcoords(self):
70
78
)[:3 , ...]
71
79
return self ._coords
72
80
73
- def map_voxels (self , coordinates ):
74
- coordinates = np . array ( coordinates )
75
- ncoords = coordinates . shape [ - 1 ]
76
- coordinates = np . vstack (( coordinates , np . ones (( 1 , ncoords ))))
81
+ def ras (self , ijk ):
82
+ """Get RAS+ coordinates from input indexes."""
83
+ ras = self . _affine . dot ( _as_homogeneous ( ijk ). T )[: 3 , ... ]
84
+ return ras . T
77
85
78
- # Back to grid coordinates
79
- return np .tensordot (np .linalg .inv (self ._affine ),
80
- coordinates , axes = 1 )[:3 , ...]
86
+ def index (self , x ):
87
+ """Get the image array's indexes corresponding to coordinates."""
88
+ ijk = self ._inverse .dot (_as_homogeneous (x ).T )[:3 , ...]
89
+ return ijk .T
81
90
82
91
def _to_hdf5 (self , group ):
83
92
group .attrs ['Type' ] = 'image'
@@ -86,14 +95,9 @@ def _to_hdf5(self, group):
86
95
group .create_dataset ('shape' , data = self .shape )
87
96
88
97
def __eq__ (self , other ):
89
- try :
90
- return (
91
- np .allclose (self .affine , other .affine , rtol = EQUALITY_TOL )
92
- and self .shape == other .shape
93
- )
94
- except AttributeError :
95
- pass
96
- return False
98
+ """Overload equals operator."""
99
+ return (np .allclose (self .affine , other .affine , rtol = EQUALITY_TOL ) and
100
+ self .shape == other .shape )
97
101
98
102
99
103
class TransformBase (object ):
@@ -102,6 +106,7 @@ class TransformBase(object):
102
106
__slots__ = ['_reference' ]
103
107
104
108
def __init__ (self ):
109
+ """Instantiate a transform."""
105
110
self ._reference = None
106
111
107
112
def __eq__ (self , other ):
@@ -110,25 +115,31 @@ def __eq__(self, other):
110
115
return False
111
116
return np .allclose (self .matrix , other .matrix , rtol = EQUALITY_TOL )
112
117
118
+ def __call__ (self , x , inverse = False , index = 0 ):
119
+ """Apply y = f(x)."""
120
+ return self .map (x , inverse = inverse , index = index )
121
+
113
122
@property
114
123
def reference (self ):
115
- '''A reference space where data will be resampled onto'''
124
+ """Access a reference space where data will be resampled onto."""
116
125
if self ._reference is None :
117
126
raise ValueError ('Reference space not set' )
118
127
return self ._reference
119
128
120
129
@reference .setter
121
130
def reference (self , image ):
122
- self ._reference = ImageSpace (image )
131
+ self ._reference = ImageGrid (image )
123
132
124
133
@property
125
134
def ndim (self ):
135
+ """Access the dimensions of the reference space."""
126
136
return self .reference .ndim
127
137
128
138
def resample (self , moving , order = 3 , mode = 'constant' , cval = 0.0 , prefilter = True ,
129
139
output_dtype = None ):
130
140
"""
131
141
Resample the moving image in reference space.
142
+
132
143
Parameters
133
144
----------
134
145
moving : `spatialimage`
@@ -150,43 +161,58 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
150
161
slightly blurred if *order > 1*, unless the input is prefiltered,
151
162
i.e. it is the result of calling the spline filter on the original
152
163
input.
164
+
153
165
Returns
154
166
-------
155
167
moved_image : `spatialimage`
156
168
The moving imaged after resampling to reference space.
169
+
157
170
"""
158
171
moving_data = np .asanyarray (moving .dataobj )
159
172
if output_dtype is None :
160
173
output_dtype = moving_data .dtype
161
174
162
175
moved = ndi .geometric_transform (
163
176
moving_data ,
164
- mapping = self .map_voxel ,
177
+ mapping = self ._map_index ,
165
178
output_shape = self .reference .shape ,
166
179
output = output_dtype ,
167
180
order = order ,
168
181
mode = mode ,
169
182
cval = cval ,
170
183
prefilter = prefilter ,
171
- extra_keywords = {'moving' : moving },
184
+ extra_keywords = {'moving' : ImageGrid ( moving ) },
172
185
)
173
186
174
187
moved_image = moving .__class__ (moved , self .reference .affine , moving .header )
175
188
moved_image .header .set_data_dtype (output_dtype )
176
189
return moved_image
177
190
178
- def map_point (self , coords ):
179
- """Apply y = f(x), where x is the argument `coords`. """
180
- raise NotImplementedError
191
+ def map (self , x , inverse = False , index = 0 ):
192
+ r """
193
+ Apply :math:`y = f(x)`.
181
194
182
- def map_voxel (self , index , moving = None ):
183
- """Apply ijk' = f_ijk((i, j, k)), equivalent to the above with indexes."""
184
- raise NotImplementedError
195
+ Parameters
196
+ ----------
197
+ x : N x D numpy.ndarray
198
+ Input RAS+ coordinates (i.e., physical coordinates).
199
+ inverse : bool
200
+ If ``True``, apply the inverse transform :math:`x = f^{-1}(y)`.
201
+ index : int, optional
202
+ Transformation index
185
203
186
- def _to_hdf5 (self , x5_root ):
187
- """Serialize this object into the x5 file format."""
204
+ Returns
205
+ -------
206
+ y : N x D numpy.ndarray
207
+ Transformed (mapped) RAS+ coordinates (i.e., physical coordinates).
208
+
209
+ """
188
210
raise NotImplementedError
189
211
212
+ def _map_index (self , ijk , moving ):
213
+ x = self .reference .ras (_as_homogeneous (ijk ))
214
+ return moving .index (self .map (x ))
215
+
190
216
def to_filename (self , filename , fmt = 'X5' ):
191
217
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
192
218
with h5py .File (filename , 'w' ) as out_file :
@@ -196,3 +222,28 @@ def to_filename(self, filename, fmt='X5'):
196
222
self ._to_hdf5 (root )
197
223
198
224
return filename
225
+
226
+ def _to_hdf5 (self , x5_root ):
227
+ """Serialize this object into the x5 file format."""
228
+ raise NotImplementedError
229
+
230
+
231
+ def _as_homogeneous (xyz , dtype = 'float32' ):
232
+ """
233
+ Convert 2D and 3D coordinates into homogeneous coordinates.
234
+
235
+ Examples
236
+ --------
237
+ >>> _as_homogeneous((4, 5), dtype='int8').tolist()
238
+ [[4, 5, 1]]
239
+
240
+ >>> _as_homogeneous((4, 5, 6),dtype='int8').tolist()
241
+ [[4, 5, 6, 1]]
242
+
243
+ >>> _as_homogeneous([(1, 2, 3), (4, 5, 6)]).tolist()
244
+ [[1.0, 2.0, 3.0, 1.0], [4.0, 5.0, 6.0, 1.0]]
245
+
246
+
247
+ """
248
+ xyz = np .atleast_2d (np .array (xyz , dtype = dtype ))
249
+ return np .hstack ((xyz , np .ones ((xyz .shape [0 ], 1 ), dtype = dtype )))
0 commit comments