Skip to content

Commit 7a99841

Browse files
authored
Merge pull request #66 from oesteban/test/lta-itk-conversion
ENH: Add ITK-LTA conversion test
2 parents 4acfdbc + 772a791 commit 7a99841

File tree

8 files changed

+159
-64
lines changed

8 files changed

+159
-64
lines changed

nitransforms/io/lta.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,38 @@ def set_type(self, new_type):
160160
)
161161

162162
def to_ras(self, moving=None, reference=None):
163-
"""Return a nitransforms internal RAS+ matrix."""
163+
"""
164+
Return a nitransforms' internal RAS+ array.
165+
166+
Seemingly, the matrix of an LTA is defined such that it
167+
maps coordinates from the ``dest volume`` to the ``src volume``.
168+
Therefore, without inversion, the LTA matrix is appropiate
169+
to move the information from ``src volume`` into the
170+
``dest volume``'s grid.
171+
172+
.. important ::
173+
174+
The ``moving`` and ``reference`` parameters are dismissed
175+
because ``VOX2VOX`` LTAs are converted to ``RAS2RAS`` type
176+
before returning the RAS+ matrix, using the ``dest`` and
177+
``src`` contained in the LTA. Both arguments are kept for
178+
API compatibility.
179+
180+
Parameters
181+
----------
182+
moving : dismissed
183+
The spatial reference of moving images.
184+
reference : dismissed
185+
The spatial reference of moving images.
186+
187+
Returns
188+
-------
189+
matrix : :obj:`numpy.ndarray`
190+
The RAS+ affine matrix corresponding to the LTA.
191+
192+
"""
164193
self.set_type(1)
165-
return self.structarr['m_L']
194+
return np.linalg.inv(self.structarr['m_L'])
166195

167196
def to_string(self):
168197
"""Convert this transform to text."""

nitransforms/linear.py

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
class Affine(TransformBase):
2525
"""Represents linear transforms on image data."""
2626

27-
__slots__ = ('_matrix', )
27+
__slots__ = ("_matrix", )
2828

2929
def __init__(self, matrix=None, reference=None):
3030
"""
@@ -40,7 +40,7 @@ def __init__(self, matrix=None, reference=None):
4040
4141
Examples
4242
--------
43-
>>> xfm = Affine(reference=datadir / 'someones_anatomy.nii.gz')
43+
>>> xfm = Affine(reference=datadir / "someones_anatomy.nii.gz")
4444
>>> xfm.matrix # doctest: +NORMALIZE_WHITESPACE
4545
array([[1., 0., 0., 0.],
4646
[0., 1., 0., 0.],
@@ -61,13 +61,17 @@ def __init__(self, matrix=None, reference=None):
6161
if matrix is not None:
6262
matrix = np.array(matrix)
6363
if matrix.ndim != 2:
64-
raise TypeError('Affine should be 2D.')
64+
raise TypeError("Affine should be 2D.")
6565
elif matrix.shape[0] != matrix.shape[1]:
66-
raise TypeError('Matrix is not square.')
66+
raise TypeError("Matrix is not square.")
6767
self._matrix = matrix
6868

69-
if np.any(self._matrix[3, :] != (0, 0, 0, 1)):
70-
raise ValueError("Matrix does not represent a valid transform.")
69+
if not np.allclose(self._matrix[3, :], (0, 0, 0, 1)):
70+
raise ValueError("""The last row of a homogeneus matrix \
71+
should be (0, 0, 0, 1), got %s.""" % self._matrix[3, :])
72+
73+
# Normalize last row
74+
self._matrix[3, :] = (0, 0, 0, 1)
7175

7276
def __eq__(self, other):
7377
"""
@@ -83,7 +87,7 @@ def __eq__(self, other):
8387
"""
8488
_eq = np.allclose(self.matrix, other.matrix, rtol=EQUALITY_TOL)
8589
if _eq and self._reference != other._reference:
86-
warnings.warn('Affines are equal, but references do not match.')
90+
warnings.warn("Affines are equal, but references do not match.")
8791
return _eq
8892

8993
@property
@@ -125,16 +129,16 @@ def map(self, x, inverse=False):
125129

126130
def _to_hdf5(self, x5_root):
127131
"""Serialize this object into the x5 file format."""
128-
xform = x5_root.create_dataset('Transform', data=[self._matrix])
129-
xform.attrs['Type'] = 'affine'
130-
x5_root.create_dataset('Inverse', data=[np.linalg.inv(self._matrix)])
132+
xform = x5_root.create_dataset("Transform", data=[self._matrix])
133+
xform.attrs["Type"] = "affine"
134+
x5_root.create_dataset("Inverse", data=[np.linalg.inv(self._matrix)])
131135

132136
if self._reference:
133-
self.reference._to_hdf5(x5_root.create_group('Reference'))
137+
self.reference._to_hdf5(x5_root.create_group("Reference"))
134138

135-
def to_filename(self, filename, fmt='X5', moving=None):
139+
def to_filename(self, filename, fmt="X5", moving=None):
136140
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
137-
if fmt.lower() in ['itk', 'ants', 'elastix']:
141+
if fmt.lower() in ["itk", "ants", "elastix"]:
138142
itkobj = io.itk.ITKLinearTransform.from_ras(self.matrix)
139143
itkobj.to_filename(filename)
140144
return filename
@@ -145,45 +149,45 @@ def to_filename(self, filename, fmt='X5', moving=None):
145149
else:
146150
moving = self.reference
147151

148-
if fmt.lower() == 'afni':
152+
if fmt.lower() == "afni":
149153
afniobj = io.afni.AFNILinearTransform.from_ras(
150154
self.matrix, moving=moving, reference=self.reference)
151155
afniobj.to_filename(filename)
152156
return filename
153157

154-
if fmt.lower() == 'fsl':
158+
if fmt.lower() == "fsl":
155159
fslobj = io.fsl.FSLLinearTransform.from_ras(
156160
self.matrix, moving=moving, reference=self.reference
157161
)
158162
fslobj.to_filename(filename)
159163
return filename
160164

161-
if fmt.lower() == 'fs':
165+
if fmt.lower() == "fs":
162166
# xform info
163167
lt = io.LinearTransform()
164-
lt['sigma'] = 1.
165-
lt['m_L'] = self.matrix
168+
lt["sigma"] = 1.
169+
lt["m_L"] = self.matrix
166170
# Just for reference, nitransforms does not write VOX2VOX
167-
lt['src'] = io.VolumeGeometry.from_image(moving)
168-
lt['dst'] = io.VolumeGeometry.from_image(self.reference)
171+
lt["src"] = io.VolumeGeometry.from_image(moving)
172+
lt["dst"] = io.VolumeGeometry.from_image(self.reference)
169173
# to make LTA file format
170174
lta = io.LinearTransformArray()
171-
lta['type'] = 1 # RAS2RAS
172-
lta['xforms'].append(lt)
175+
lta["type"] = 1 # RAS2RAS
176+
lta["xforms"].append(lt)
173177

174-
with open(filename, 'w') as f:
178+
with open(filename, "w") as f:
175179
f.write(lta.to_string())
176180
return filename
177181

178182
raise NotImplementedError
179183

180184
@classmethod
181-
def from_filename(cls, filename, fmt='X5',
185+
def from_filename(cls, filename, fmt="X5",
182186
reference=None, moving=None):
183187
"""Create an affine from a transform file."""
184-
if fmt.lower() in ('itk', 'ants', 'elastix'):
188+
if fmt.lower() in ("itk", "ants", "elastix"):
185189
_factory = io.itk.ITKLinearTransformArray
186-
elif fmt.lower() in ('lta', 'fs'):
190+
elif fmt.lower() in ("lta", "fs"):
187191
_factory = io.LinearTransformArray
188192
else:
189193
raise NotImplementedError
@@ -193,7 +197,7 @@ def from_filename(cls, filename, fmt='X5',
193197
if cls == Affine:
194198
if np.shape(matrix)[0] != 1:
195199
raise TypeError(
196-
'Cannot load transform array "%s"' % filename)
200+
"Cannot load transform array '%s'" % filename)
197201
matrix = matrix[0]
198202
return cls(matrix, reference=reference)
199203

@@ -297,9 +301,9 @@ def map(self, x, inverse=False):
297301
affine = np.linalg.inv(affine)
298302
return np.swapaxes(affine.dot(coords), 1, 2)
299303

300-
def to_filename(self, filename, fmt='X5', moving=None):
304+
def to_filename(self, filename, fmt="X5", moving=None):
301305
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
302-
if fmt.lower() in ('itk', 'ants', 'elastix'):
306+
if fmt.lower() in ("itk", "ants", "elastix"):
303307
itkobj = io.itk.ITKLinearTransformArray.from_ras(self.matrix)
304308
itkobj.to_filename(filename)
305309
return filename
@@ -310,41 +314,41 @@ def to_filename(self, filename, fmt='X5', moving=None):
310314
else:
311315
moving = self.reference
312316

313-
if fmt.lower() == 'afni':
317+
if fmt.lower() == "afni":
314318
afniobj = io.afni.AFNILinearTransformArray.from_ras(
315319
self.matrix, moving=moving, reference=self.reference)
316320
afniobj.to_filename(filename)
317321
return filename
318322

319-
if fmt.lower() == 'fsl':
323+
if fmt.lower() == "fsl":
320324
fslobj = io.fsl.FSLLinearTransformArray.from_ras(
321325
self.matrix, moving=moving, reference=self.reference
322326
)
323327
fslobj.to_filename(filename)
324328
return filename
325329

326-
if fmt.lower() in ('fs', 'lta'):
330+
if fmt.lower() in ("fs", "lta"):
327331
# xform info
328332
# to make LTA file format
329333
lta = io.LinearTransformArray()
330-
lta['type'] = 1 # RAS2RAS
334+
lta["type"] = 1 # RAS2RAS
331335
for m in self.matrix:
332336
lt = io.LinearTransform()
333-
lt['sigma'] = 1.
334-
lt['m_L'] = m
337+
lt["sigma"] = 1.
338+
lt["m_L"] = m
335339
# Just for reference, nitransforms does not write VOX2VOX
336-
lt['src'] = io.VolumeGeometry.from_image(moving)
337-
lt['dst'] = io.VolumeGeometry.from_image(self.reference)
338-
lta['xforms'].append(lt)
340+
lt["src"] = io.VolumeGeometry.from_image(moving)
341+
lt["dst"] = io.VolumeGeometry.from_image(self.reference)
342+
lta["xforms"].append(lt)
339343

340-
with open(filename, 'w') as f:
344+
with open(filename, "w") as f:
341345
f.write(lta.to_string())
342346
return filename
343347

344348
raise NotImplementedError
345349

346350
def apply(self, spatialimage, reference=None,
347-
order=3, mode='constant', cval=0.0, prefilter=True, output_dtype=None):
351+
order=3, mode="constant", cval=0.0, prefilter=True, output_dtype=None):
348352
"""
349353
Apply a transformation to an image, resampling on the reference spatial object.
350354
@@ -359,11 +363,11 @@ def apply(self, spatialimage, reference=None,
359363
order : int, optional
360364
The order of the spline interpolation, default is 3.
361365
The order has to be in the range 0-5.
362-
mode : {'constant', 'reflect', 'nearest', 'mirror', 'wrap'}, optional
366+
mode : {"constant", "reflect", "nearest", "mirror", "wrap"}, optional
363367
Determines how the input image is extended when the resamplings overflows
364-
a border. Default is 'constant'.
368+
a border. Default is "constant".
365369
cval : float, optional
366-
Constant value for ``mode='constant'``. Default is 0.0.
370+
Constant value for ``mode="constant"``. Default is 0.0.
367371
prefilter: bool, optional
368372
Determines if the image's data array is prefiltered with
369373
a spline filter before interpolation. The default is ``True``,
@@ -436,17 +440,17 @@ def apply(self, spatialimage, reference=None,
436440
return resampled
437441

438442

439-
def load(filename, fmt='X5', reference=None, moving=None):
443+
def load(filename, fmt="X5", reference=None, moving=None):
440444
"""
441445
Load a linear transform file.
442446
443447
Examples
444448
--------
445-
>>> xfm = load(datadir / 'affine-LAS.itk.tfm', fmt='itk')
449+
>>> xfm = load(datadir / "affine-LAS.itk.tfm", fmt="itk")
446450
>>> isinstance(xfm, Affine)
447451
True
448452
449-
>>> xfm = load(datadir / 'itktflist.tfm', fmt='itk')
453+
>>> xfm = load(datadir / "itktflist.tfm", fmt="itk")
450454
>>> isinstance(xfm, LinearTransformsMapping)
451455
True
452456
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0.99970585 -0.00953967 -0.02228683 -15.19269657
2+
-0.00599674 0.79344094 -0.60861677 70.83446503
3+
0.02348929 0.60857153 0.79315072 15.39251423
4+
0.00000000 0.00000000 0.00000000 1.00000000
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type = 0 # LINEAR_VOX_TO_VOX
2+
nxforms = 1
3+
mean = 0.0000 0.0000 0.0000
4+
sigma = 1.0000
5+
1 4 4
6+
-3.124080896377563e+00 2.981145866215229e-02 8.914728462696075e-02 1.741926879882812e+02
7+
-1.405486371368170e-02 1.859627604484558e+00 -1.825850725173950e+00 5.312585067749023e+01
8+
5.505303665995598e-02 1.426339745521545e+00 2.379452466964722e+00 1.154438781738281e+01
9+
0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 9.999998807907104e-01
10+
src volume info
11+
valid = 1 # volume info valid
12+
filename = /home/oesteban/tmp/fmriprep-ds005/fprep-work/fmriprep_wf/single_subject_01_wf/func_preproc_task_mixedgamblestask_run_01_wf/bold_reg_wf/bbreg_wf/bbregister/uni_xform_masked.nii.gz
13+
volume = 64 64 34
14+
voxelsize = 3.125000000000000e+00 3.125000000000000e+00 4.000000000000000e+00
15+
xras = -1.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00
16+
yras = 0.000000000000000e+00 1.000000000000000e+00 0.000000000000000e+00
17+
zras = 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00
18+
cras = 1.000000000000000e+00 2.800000000000000e+01 -3.100000000000000e+01
19+
dst volume info
20+
valid = 1 # volume info valid
21+
filename = /oak/stanford/groups/russpold/data/openfmri/ds000005/sub-01/anat/sub-01_T1w.nii.gz
22+
volume = 160 192 192
23+
voxelsize = 1.000000000000000e+00 1.333333015441895e+00 1.333333015441895e+00
24+
xras = 1.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00
25+
yras = 0.000000000000000e+00 1.000000000000000e+00 0.000000000000000e+00
26+
zras = 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00
27+
cras = -1.000000000000000e+00 -5.000030517578125e+00 -1.000030517578125e+00
28+
subject sub-01
29+
fscale 0.100000
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: MatrixOffsetTransformBase_double_3_3
4+
Parameters: 0.999706355364118 0.0059967416827344956 0.023489316177446647 0.009539669218827523 0.7934419194347854 -0.6085721629337524 -0.02228683774477952 0.6086173680484374 0.7931513449655613 -5.5388974668589555 -45.57406723091313 -48.80406464758718
5+
FixedParameters: 0 0 0
Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
1-
# transform file /home/oesteban/tmp/fmriprep-ds005/fprep-work/fmriprep_wf/single_subject_01_wf/anat_preproc_wf/surface_recon_wf/fsnative2t1w_xfm/T1_robustreg.lta
2-
# created by oesteban on Sat Mar 14 19:28:37 2020
3-
41
type = 1 # LINEAR_RAS_TO_RAS
52
nxforms = 1
63
mean = 129.0000 157.0000 132.0000
74
sigma = 10000.0000
85
1 4 4
9-
9.999999403953552e-01 -1.698292035143822e-04 1.542967074783519e-04 -1.678466796875000e-04
10-
1.698438863968477e-04 9.999999403953552e-01 -9.513227996649221e-05 -1.318359375000000e-02
11-
-1.542805403005332e-04 9.515848068986088e-05 9.999999403953552e-01 -6.271362304687500e-03
12-
0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 9.999999403953552e-01
6+
1.000000000000000e+00 1.698439009487629e-04 -1.542805694043636e-04 1.691182987997308e-04
7+
-1.698292180662975e-04 1.000000000000000e+00 9.515849524177611e-05 1.318416278809309e-02
8+
1.542967220302671e-04 -9.513228724244982e-05 1.000000000000000e+00 6.270134821534157e-03
9+
0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00
1310
src volume info
1411
valid = 1 # volume info valid
15-
filename = /oak/stanford/groups/russpold/data/openfmri/derivatives/ds000005/freesurfer-6.0.1/sub-01/mri/T1.mgz
16-
volume = 256 256 256
17-
voxelsize = 1.000000000000000e+00 1.000000000000000e+00 1.000000000000000e+00
18-
xras = -9.999999403953552e-01 0.000000000000000e+00 0.000000000000000e+00
19-
yras = 0.000000000000000e+00 0.000000000000000e+00 -9.999999403953552e-01
20-
zras = 0.000000000000000e+00 9.999999403953552e-01 0.000000000000000e+00
21-
cras = -9.999847412109375e-01 -5.000015258789062e+00 -1.000038146972656e+00
22-
dst volume info
23-
valid = 1 # volume info valid
2412
filename = /oak/stanford/groups/russpold/data/openfmri/ds000005/sub-01/anat/sub-01_T1w.nii.gz
2513
volume = 160 192 192
2614
voxelsize = 1.000000000000000e+00 1.333333015441895e+00 1.333333015441895e+00
2715
xras = 1.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00
2816
yras = 0.000000000000000e+00 1.000000000000000e+00 0.000000000000000e+00
2917
zras = 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00
3018
cras = -1.000000000000000e+00 -5.000030517578125e+00 -1.000030517578125e+00
19+
dst volume info
20+
valid = 1 # volume info valid
21+
filename = /oak/stanford/groups/russpold/data/openfmri/derivatives/ds000005/freesurfer-6.0.1/sub-01/mri/T1.mgz
22+
volume = 256 256 256
23+
voxelsize = 1.000000000000000e+00 1.000000000000000e+00 1.000000000000000e+00
24+
xras = -9.999999403953552e-01 0.000000000000000e+00 0.000000000000000e+00
25+
yras = 0.000000000000000e+00 0.000000000000000e+00 -9.999999403953552e-01
26+
zras = 0.000000000000000e+00 9.999999403953552e-01 0.000000000000000e+00
27+
cras = -9.999847412109375e-01 -5.000015258789062e+00 -1.000038146972656e+00
28+
fscale 0.100000
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: AffineTransform_double_3_3
4+
Parameters: 1 -0.00016982921806629747 -0.00015429673658218235 0.00016984390094876289 1 9.5132294518407434e-05 0.00015428056940436363 -9.5158495241776109e-05 1 0.00016784669423941523 0.013183594681322575 -0.0062713632360100746
5+
FixedParameters: 0 0 0
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Conversions between formats."""
4+
import numpy as np
5+
from .. import linear as _l
6+
7+
8+
def test_conversions0(data_path):
9+
"""Check conversions between formats."""
10+
lta = _l.load(data_path / "regressions" / "robust_register.lta", fmt="lta")
11+
itk = _l.load(data_path / "regressions" / "robust_register.tfm", fmt="itk")
12+
13+
assert np.allclose(lta.matrix, itk.matrix)
14+
15+
16+
def test_conversions1(data_path):
17+
"""Check conversions between formats."""
18+
lta = _l.load(data_path / "regressions" / "bbregister.lta", fmt="lta")
19+
itk = _l.load(data_path / "regressions" / "bbregister.tfm", fmt="itk")
20+
21+
assert np.allclose(lta.matrix, itk.matrix)

0 commit comments

Comments
 (0)