Skip to content

Commit 1a611a8

Browse files
committed
enh: full implementation of the resample method
1 parent ffdbc30 commit 1a611a8

File tree

8 files changed

+160
-299
lines changed

8 files changed

+160
-299
lines changed

nitransforms/base.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
"""Common interface for transforms."""
10+
from pathlib import Path
1011
import numpy as np
1112
import h5py
1213
from nibabel.loadsave import load
@@ -24,15 +25,16 @@ class ImageGrid(object):
2425

2526
def __init__(self, image):
2627
"""Create a gridded sampling reference."""
28+
if isinstance(image, (str, Path)):
29+
image = load(str(image))
30+
2731
self._affine = image.affine
2832
self._shape = image.shape
2933
self._ndim = len(image.shape)
3034
self._nvox = np.prod(image.shape) # Do not access data array
3135
self._ndindex = None
3236
self._coords = None
3337
self._inverse = np.linalg.inv(image.affine)
34-
if self._ndim not in [2, 3]:
35-
raise ValueError('Invalid image space (%d-D)' % self._ndim)
3638

3739
@property
3840
def affine(self):
@@ -81,13 +83,11 @@ def ndcoords(self):
8183

8284
def ras(self, ijk):
8385
"""Get RAS+ coordinates from input indexes."""
84-
ras = self._affine.dot(_as_homogeneous(ijk).T)[:3, ...]
85-
return ras.T
86+
return _apply_affine(ijk, self._affine, self._ndim)
8687

8788
def index(self, x):
8889
"""Get the image array's indexes corresponding to coordinates."""
89-
ijk = self._inverse.dot(_as_homogeneous(x).T)[:3, ...]
90-
return ijk.T
90+
return _apply_affine(x, self._inverse, self._ndim)
9191

9292
def _to_hdf5(self, group):
9393
group.attrs['Type'] = 'image'
@@ -173,22 +173,23 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
173173
moving = load(moving)
174174

175175
moving_data = np.asanyarray(moving.dataobj)
176-
if output_dtype is None:
177-
output_dtype = moving_data.dtype
176+
output_dtype = output_dtype or moving_data.dtype
177+
targets = ImageGrid(moving).index(
178+
_as_homogeneous(self.map(self.reference.ndcoords.T),
179+
dim=self.reference.ndim))
178180

179-
moved = ndi.geometric_transform(
181+
moved = ndi.map_coordinates(
180182
moving_data,
181-
mapping=self._map_index,
182-
output_shape=self.reference.shape,
183+
targets.T,
183184
output=output_dtype,
184185
order=order,
185186
mode=mode,
186187
cval=cval,
187188
prefilter=prefilter,
188-
extra_keywords={'moving': ImageGrid(moving)},
189189
)
190190

191-
moved_image = moving.__class__(moved, self.reference.affine, moving.header)
191+
moved_image = moving.__class__(moved.reshape(self.reference.shape),
192+
self.reference.affine, moving.header)
192193
moved_image.header.set_data_dtype(output_dtype)
193194
return moved_image
194195

@@ -213,10 +214,6 @@ def map(self, x, inverse=False, index=0):
213214
"""
214215
raise NotImplementedError
215216

216-
def _map_index(self, ijk, moving):
217-
x = self.reference.ras(_as_homogeneous(ijk))
218-
return moving.index(self.map(x))
219-
220217
def to_filename(self, filename, fmt='X5'):
221218
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
222219
with h5py.File(filename, 'w') as out_file:
@@ -232,22 +229,33 @@ def _to_hdf5(self, x5_root):
232229
raise NotImplementedError
233230

234231

235-
def _as_homogeneous(xyz, dtype='float32'):
232+
def _as_homogeneous(xyz, dtype='float32', dim=3):
236233
"""
237234
Convert 2D and 3D coordinates into homogeneous coordinates.
238235
239236
Examples
240237
--------
241-
>>> _as_homogeneous((4, 5), dtype='int8').tolist()
238+
>>> _as_homogeneous((4, 5), dtype='int8', dim=2).tolist()
242239
[[4, 5, 1]]
243240
244241
>>> _as_homogeneous((4, 5, 6),dtype='int8').tolist()
245242
[[4, 5, 6, 1]]
246243
244+
>>> _as_homogeneous((4, 5, 6, 1),dtype='int8').tolist()
245+
[[4, 5, 6, 1]]
246+
247247
>>> _as_homogeneous([(1, 2, 3), (4, 5, 6)]).tolist()
248248
[[1.0, 2.0, 3.0, 1.0], [4.0, 5.0, 6.0, 1.0]]
249249
250250
251251
"""
252252
xyz = np.atleast_2d(np.array(xyz, dtype=dtype))
253+
if np.shape(xyz)[-1] == dim + 1:
254+
return xyz
255+
253256
return np.hstack((xyz, np.ones((xyz.shape[0], 1), dtype=dtype)))
257+
258+
259+
def _apply_affine(x, affine, dim):
260+
"""Get the image array's indexes corresponding to coordinates."""
261+
return affine.dot(_as_homogeneous(x, dim=dim).T)[:dim, ...].T

nitransforms/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def doctest_autoimport(doctest_namespace):
3535
@pytest.fixture
3636
def data_path():
3737
"""Return the test data folder."""
38-
return os.path.join(os.path.dirname(__file__), 'tests/data')
38+
return Path(__file__).parent / 'tests' / 'data'
3939

4040

4141
@pytest.fixture

nitransforms/linear.py

Lines changed: 5 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99
"""Linear transforms."""
1010
import sys
1111
import numpy as np
12-
from scipy import ndimage as ndi
1312
from pathlib import Path
14-
import warnings
1513

1614
from nibabel.loadsave import load as loadimg
1715
from nibabel.affines import from_matvec, voxel_sizes, obliquity
18-
from .base import TransformBase
16+
from .base import TransformBase, _as_homogeneous
1917
from .patched import shape_zoom_affine
2018
from . import io
2119

@@ -74,110 +72,6 @@ def matrix(self):
7472
"""Access the internal representation of this affine."""
7573
return self._matrix
7674

77-
def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
78-
output_dtype=None):
79-
"""
80-
Resample the moving image in reference space.
81-
82-
Parameters
83-
----------
84-
moving : `spatialimage`
85-
The image object containing the data to be resampled in reference
86-
space
87-
order : int, optional
88-
The order of the spline interpolation, default is 3.
89-
The order has to be in the range 0-5.
90-
mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
91-
Determines how the input image is extended when the resamplings overflows
92-
a border. Default is 'constant'.
93-
cval : float, optional
94-
Constant value for ``mode='constant'``. Default is 0.0.
95-
prefilter: bool, optional
96-
Determines if the moving image's data array is prefiltered with
97-
a spline filter before interpolation. The default is ``True``,
98-
which will create a temporary *float64* array of filtered values
99-
if *order > 1*. If setting this to ``False``, the output will be
100-
slightly blurred if *order > 1*, unless the input is prefiltered,
101-
i.e. it is the result of calling the spline filter on the original
102-
input.
103-
104-
Returns
105-
-------
106-
moved_image : `spatialimage`
107-
The moving imaged after resampling to reference space.
108-
109-
Examples
110-
--------
111-
>>> a = Affine()
112-
>>> a.matrix
113-
array([[[1., 0., 0., 0.],
114-
[0., 1., 0., 0.],
115-
[0., 0., 1., 0.],
116-
[0., 0., 0., 1.]]])
117-
118-
>>> xfm = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
119-
>>> ref = nb.load(testfile)
120-
>>> xfm.reference = ref
121-
>>> refdata = ref.get_fdata()
122-
>>> np.allclose(refdata, 0)
123-
True
124-
125-
>>> refdata[5, 5, 5] = 1 # Set a one in the middle voxel
126-
>>> moving = nb.Nifti1Image(refdata, ref.affine, ref.header)
127-
>>> resampled = xfm.resample(moving, order=0).get_fdata()
128-
>>> resampled[1, 5, 5]
129-
1.0
130-
131-
"""
132-
if output_dtype is None:
133-
output_dtype = moving.header.get_data_dtype()
134-
135-
try:
136-
reference = self.reference
137-
except ValueError:
138-
warnings.warn('No reference space defined, using moving as reference')
139-
reference = moving
140-
141-
nvols = 1
142-
if len(moving.shape) > 3:
143-
nvols = moving.shape[3]
144-
145-
movaff = moving.affine
146-
movingdata = np.asanyarray(moving.dataobj)
147-
if nvols == 1:
148-
movingdata = movingdata[..., np.newaxis]
149-
150-
nmats = self._matrix.shape[0]
151-
if nvols != nmats and nmats > 1:
152-
raise ValueError("""\
153-
The moving image contains {0} volumes, while the transform is defined for \
154-
{1} volumes""".format(nvols, nmats))
155-
156-
singlemat = None
157-
if nmats == 1:
158-
singlemat = np.linalg.inv(movaff).dot(self._matrix[0].dot(reference.affine))
159-
160-
if singlemat is not None and nvols > nmats:
161-
warnings.warn('Resampling a 4D volume with a single affine matrix')
162-
163-
# Compose an index to index affine matrix
164-
moved = []
165-
for i in range(nvols):
166-
i2imat = singlemat if singlemat is not None else np.linalg.inv(
167-
movaff).dot(self._matrix[i].dot(reference.affine))
168-
mdata = movingdata[..., i]
169-
moved += [ndi.affine_transform(
170-
mdata, matrix=i2imat[:-1, :-1],
171-
offset=i2imat[:-1, -1],
172-
output_shape=reference.shape, order=order, mode=mode,
173-
cval=cval, prefilter=prefilter)]
174-
175-
moved_image = moving.__class__(
176-
np.squeeze(np.stack(moved, -1)), reference.affine, moving.header)
177-
moved_image.header.set_data_dtype(output_dtype)
178-
179-
return moved_image
180-
18175
def map(self, x, inverse=False, index=0):
18276
r"""
18377
Apply :math:`y = f(x)`.
@@ -200,45 +94,17 @@ def map(self, x, inverse=False, index=0):
20094
--------
20195
>>> xfm = Affine([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]])
20296
>>> xfm.map((0,0,0))
203-
array([1, 2, 3])
97+
array([[1., 2., 3.]])
20498
20599
>>> xfm.map((0,0,0), inverse=True)
206-
array([-1., -2., -3.])
100+
array([[-1., -2., -3.]])
207101
208102
"""
209-
coords = np.array(x)
210-
if coords.shape[0] == self._matrix[index].shape[0] - 1:
211-
coords = np.append(coords, [1])
103+
coords = _as_homogeneous(x, dim=self._matrix[0].shape[0] - 1).T
212104
affine = self._matrix[index]
213-
214105
if inverse is True:
215106
affine = np.linalg.inv(self._matrix[index])
216-
217-
return affine.dot(coords)[:-1]
218-
219-
def _map_voxel(self, index, nindex=0, moving=None):
220-
"""Apply ijk' = f_ijk((i, j, k)), equivalent to the above with indexes."""
221-
try:
222-
reference = self.reference
223-
except ValueError:
224-
print('Warning: no reference space defined, using moving as reference',
225-
file=sys.stderr)
226-
reference = moving
227-
else:
228-
if moving is None:
229-
moving = reference
230-
finally:
231-
if reference is None:
232-
raise ValueError('Reference and moving spaces are both undefined')
233-
234-
index = np.array(index)
235-
if index.shape[0] == self._matrix[nindex].shape[0] - 1:
236-
index = np.append(index, [1])
237-
238-
matrix = reference.affine.dot(
239-
self._matrix[nindex].dot(np.linalg.inv(moving.affine))
240-
)
241-
return tuple(matrix.dot(index)[:-1])
107+
return affine.dot(coords).T[..., :-1]
242108

243109
def _to_hdf5(self, x5_root):
244110
"""Serialize this object into the x5 file format."""

0 commit comments

Comments
 (0)