Skip to content

Commit 8d559a0

Browse files
authored
Merge pull request #24 from oesteban/enh/22
ENH: Uber-refactor of code style, method names, etc.
2 parents 94445d9 + d31c55e commit 8d559a0

File tree

7 files changed

+176
-182
lines changed

7 files changed

+176
-182
lines changed

nitransforms/base.py

+79-28
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
EQUALITY_TOL = 1e-5
1616

1717

18-
class ImageSpace(object):
18+
class ImageGrid(object):
1919
"""Class to represent spaces of gridded data (images)."""
2020

2121
__slots__ = ['_affine', '_shape', '_ndim', '_ndindex', '_coords', '_nvox',
2222
'_inverse']
2323

2424
def __init__(self, image):
25+
"""Create a gridded sampling reference."""
2526
self._affine = image.affine
2627
self._shape = image.shape
2728
self._ndim = len(image.shape)
@@ -34,26 +35,32 @@ def __init__(self, image):
3435

3536
@property
3637
def affine(self):
38+
"""Access the indexes-to-RAS affine."""
3739
return self._affine
3840

3941
@property
4042
def inverse(self):
43+
"""Access the RAS-to-indexes affine."""
4144
return self._inverse
4245

4346
@property
4447
def shape(self):
48+
"""Access the space's size of each dimension."""
4549
return self._shape
4650

4751
@property
4852
def ndim(self):
53+
"""Access the number of dimensions."""
4954
return self._ndim
5055

5156
@property
5257
def nvox(self):
58+
"""Access the total number of voxels."""
5359
return self._nvox
5460

5561
@property
5662
def ndindex(self):
63+
"""List the indexes corresponding to the space grid."""
5764
if self._ndindex is None:
5865
indexes = tuple([np.arange(s) for s in self._shape])
5966
self._ndindex = np.array(np.meshgrid(
@@ -62,6 +69,7 @@ def ndindex(self):
6269

6370
@property
6471
def ndcoords(self):
72+
"""List the physical coordinates of this gridded space samples."""
6573
if self._coords is None:
6674
self._coords = np.tensordot(
6775
self._affine,
@@ -70,14 +78,15 @@ def ndcoords(self):
7078
)[:3, ...]
7179
return self._coords
7280

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
7785

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
8190

8291
def _to_hdf5(self, group):
8392
group.attrs['Type'] = 'image'
@@ -86,14 +95,9 @@ def _to_hdf5(self, group):
8695
group.create_dataset('shape', data=self.shape)
8796

8897
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)
97101

98102

99103
class TransformBase(object):
@@ -102,6 +106,7 @@ class TransformBase(object):
102106
__slots__ = ['_reference']
103107

104108
def __init__(self):
109+
"""Instantiate a transform."""
105110
self._reference = None
106111

107112
def __eq__(self, other):
@@ -110,25 +115,31 @@ def __eq__(self, other):
110115
return False
111116
return np.allclose(self.matrix, other.matrix, rtol=EQUALITY_TOL)
112117

118+
def __call__(self, x, inverse=False, index=0):
119+
"""Apply y = f(x)."""
120+
return self.map(x, inverse=inverse, index=index)
121+
113122
@property
114123
def reference(self):
115-
'''A reference space where data will be resampled onto'''
124+
"""Access a reference space where data will be resampled onto."""
116125
if self._reference is None:
117126
raise ValueError('Reference space not set')
118127
return self._reference
119128

120129
@reference.setter
121130
def reference(self, image):
122-
self._reference = ImageSpace(image)
131+
self._reference = ImageGrid(image)
123132

124133
@property
125134
def ndim(self):
135+
"""Access the dimensions of the reference space."""
126136
return self.reference.ndim
127137

128138
def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
129139
output_dtype=None):
130140
"""
131141
Resample the moving image in reference space.
142+
132143
Parameters
133144
----------
134145
moving : `spatialimage`
@@ -150,43 +161,58 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
150161
slightly blurred if *order > 1*, unless the input is prefiltered,
151162
i.e. it is the result of calling the spline filter on the original
152163
input.
164+
153165
Returns
154166
-------
155167
moved_image : `spatialimage`
156168
The moving imaged after resampling to reference space.
169+
157170
"""
158171
moving_data = np.asanyarray(moving.dataobj)
159172
if output_dtype is None:
160173
output_dtype = moving_data.dtype
161174

162175
moved = ndi.geometric_transform(
163176
moving_data,
164-
mapping=self.map_voxel,
177+
mapping=self._map_index,
165178
output_shape=self.reference.shape,
166179
output=output_dtype,
167180
order=order,
168181
mode=mode,
169182
cval=cval,
170183
prefilter=prefilter,
171-
extra_keywords={'moving': moving},
184+
extra_keywords={'moving': ImageGrid(moving)},
172185
)
173186

174187
moved_image = moving.__class__(moved, self.reference.affine, moving.header)
175188
moved_image.header.set_data_dtype(output_dtype)
176189
return moved_image
177190

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)`.
181194
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
185203
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+
"""
188210
raise NotImplementedError
189211

212+
def _map_index(self, ijk, moving):
213+
x = self.reference.ras(_as_homogeneous(ijk))
214+
return moving.index(self.map(x))
215+
190216
def to_filename(self, filename, fmt='X5'):
191217
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
192218
with h5py.File(filename, 'w') as out_file:
@@ -196,3 +222,28 @@ def to_filename(self, filename, fmt='X5'):
196222
self._to_hdf5(root)
197223

198224
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)))

nitransforms/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def data_path():
3939

4040

4141
@pytest.fixture
42-
def get_data():
42+
def get_testdata():
4343
"""Generate data in the requested orientation."""
4444
global _data
4545

nitransforms/linear.py

+19-16
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
143143
nvols = moving.shape[3]
144144

145145
movaff = moving.affine
146-
movingdata = moving.get_data()
146+
movingdata = np.asanyarray(moving.dataobj)
147147
if nvols == 1:
148148
movingdata = movingdata[..., np.newaxis]
149149

@@ -178,42 +178,45 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
178178

179179
return moved_image
180180

181-
def map_point(self, coords, index=0, forward=True):
182-
"""
183-
Apply y = f(x), where x is the argument `coords`.
181+
def map(self, x, inverse=False, index=0):
182+
r"""
183+
Apply :math:`y = f(x)`.
184184
185185
Parameters
186186
----------
187-
coords : array_like
188-
RAS coordinates to map
187+
x : N x D numpy.ndarray
188+
Input RAS+ coordinates (i.e., physical coordinates).
189+
inverse : bool
190+
If ``True``, apply the inverse transform :math:`x = f^{-1}(y)`.
189191
index : int, optional
190192
Transformation index
191-
forward: bool, optional
192-
Direction of mapping. Default is set to ``True``. If ``False``,
193-
the inverse transformation is applied.
194193
195194
Returns
196195
-------
197-
out: ndarray
198-
Transformed coordinates
196+
y : N x D numpy.ndarray
197+
Transformed (mapped) RAS+ coordinates (i.e., physical coordinates).
199198
200199
Examples
201200
--------
202201
>>> xfm = Affine([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]])
203-
>>> xfm.map_point((0,0,0))
202+
>>> xfm.map((0,0,0))
204203
array([1, 2, 3])
205204
206-
>>> xfm.map_point((0,0,0), forward=False)
205+
>>> xfm.map((0,0,0), inverse=True)
207206
array([-1., -2., -3.])
208207
209208
"""
210-
coords = np.array(coords)
209+
coords = np.array(x)
211210
if coords.shape[0] == self._matrix[index].shape[0] - 1:
212211
coords = np.append(coords, [1])
213-
affine = self._matrix[index] if forward else np.linalg.inv(self._matrix[index])
212+
affine = self._matrix[index]
213+
214+
if inverse is True:
215+
affine = np.linalg.inv(self._matrix[index])
216+
214217
return affine.dot(coords)[:-1]
215218

216-
def map_voxel(self, index, nindex=0, moving=None):
219+
def _map_voxel(self, index, nindex=0, moving=None):
217220
"""Apply ijk' = f_ijk((i, j, k)), equivalent to the above with indexes."""
218221
try:
219222
reference = self.reference

0 commit comments

Comments
 (0)