Skip to content

Commit b49e9f2

Browse files
committed
ENH: Add ITK-LTA conversion test
Also, finally fixes #64.
1 parent 2180e54 commit b49e9f2

File tree

4 files changed

+82
-62
lines changed

4 files changed

+82
-62
lines changed

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: 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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_conversions(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)

0 commit comments

Comments
 (0)