Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fe74efb

Browse files
committedSep 27, 2019
fix: add a first battery of tests
Added some initial tests to check the writing of transforms to ITK and FSL formats.
1 parent 9902f50 commit fe74efb

12 files changed

+145
-4
lines changed
 

‎nibabel/tests/data/affine-LAS-itk.tfm

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
affine-RAS-itk.tfm

‎nibabel/tests/data/affine-LAS.fsl

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
affine-RAS.fsl

‎nibabel/tests/data/affine-LPS-itk.tfm

+5
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.999999 -0.000999999 -0.001 0.00140494 0.621609 0.783327 -0.000161717 -0.783327 0.62161 -4 -2 -1
5+
FixedParameters: 0 0 0

‎nibabel/tests/data/affine-LPS.fsl

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0.999999 -0.00140494 0.000161717 -3.89014
2+
0.000999999 0.621609 -0.783327 105.905
3+
0.001 0.783327 0.62161 -34.3513
4+
0 0 0 1

‎nibabel/tests/data/affine-RAS-itk.tfm

+5
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.999999 -0.000999999 -0.001 0.00140494 0.621609 0.783327 -0.000161717 -0.783327 0.62161 -4 -2 -1
5+
FixedParameters: 0 0 0

‎nibabel/tests/data/affine-RAS.fsl

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0.999999 -0.00140494 -0.000161717 4.14529
2+
0.000999999 0.621609 0.783327 -37.3811
3+
-0.001 -0.783327 0.62161 107.976
4+
0 0 0 1
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.999999 -0.000999999 -0.001 0.00140494 0.621609 0.783327 -0.000161717 -0.783327 0.62161 -4 -2 -1
5+
FixedParameters: 0 0 0

‎nibabel/tests/data/affine-oblique.fsl

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0.999998 -0.00181872 -0.0011965 4.26083
2+
0.00206779 0.621609 0.783325 -25.3129
3+
-0.000680894 -0.783326 0.621611 101.967
4+
0 0 0 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../doc/source/downloads/someones_anatomy.nii.gz

‎nibabel/tests/test_transform.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Tests of the transform module."""
4+
import os
5+
import numpy as np
6+
from numpy.testing import assert_array_equal, assert_almost_equal, \
7+
assert_array_almost_equal
8+
import pytest
9+
10+
from ..loadsave import load as loadimg
11+
from ..nifti1 import Nifti1Image
12+
from ..eulerangles import euler2mat
13+
from ..affines import from_matvec
14+
from ..volumeutils import shape_zoom_affine
15+
from ..transform import linear as nbl
16+
from ..testing import (assert_equal, assert_not_equal, assert_true,
17+
assert_false, assert_raises, data_path,
18+
suppress_warnings, assert_dt_equal)
19+
from ..tmpdirs import InTemporaryDirectory
20+
21+
22+
SOMEONES_ANATOMY = os.path.join(data_path, 'someones_anatomy.nii.gz')
23+
# SOMEONES_ANATOMY = os.path.join(data_path, 'someones_anatomy.nii.gz')
24+
25+
26+
@pytest.mark.parametrize('image_orientation', ['RAS', 'LAS', 'LPS', 'oblique'])
27+
def test_affines_save(image_orientation):
28+
"""Check implementation of exporting affines to formats."""
29+
# Generate test transform
30+
img = loadimg(SOMEONES_ANATOMY)
31+
imgaff = img.affine
32+
33+
if image_orientation == 'LAS':
34+
newaff = imgaff.copy()
35+
newaff[0, 0] *= -1.0
36+
newaff[0, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[0]
37+
img = Nifti1Image(np.flip(img.get_fdata(), 0), newaff, img.header)
38+
elif image_orientation == 'LPS':
39+
newaff = imgaff.copy()
40+
newaff[0, 0] *= -1.0
41+
newaff[1, 1] *= -1.0
42+
newaff[:2, 3] = imgaff.dot(np.hstack((np.array(img.shape[:3]) - 1, 1.0)))[:2]
43+
img = Nifti1Image(np.flip(np.flip(img.get_fdata(), 0), 1), newaff, img.header)
44+
elif image_orientation == 'oblique':
45+
A = shape_zoom_affine(img.shape, img.header.get_zooms(), x_flip=False)
46+
R = from_matvec(euler2mat(x=0.09, y=0.001, z=0.001))
47+
newaff = R.dot(A)
48+
img = Nifti1Image(img.get_fdata(), newaff, img.header)
49+
img.header.set_qform(newaff, 1)
50+
img.header.set_sform(newaff, 1)
51+
52+
T = from_matvec(euler2mat(x=0.9, y=0.001, z=0.001), [4.0, 2.0, -1.0])
53+
54+
xfm = nbl.Affine(T)
55+
xfm.reference = img
56+
57+
itk = nbl.load(os.path.join(data_path, 'affine-%s-itk.tfm' % image_orientation),
58+
fmt='itk')
59+
fsl = np.loadtxt(os.path.join(data_path, 'affine-%s.fsl' % image_orientation))
60+
61+
with InTemporaryDirectory():
62+
xfm.to_filename('M.tfm', fmt='itk')
63+
xfm.to_filename('M.fsl', fmt='fsl')
64+
65+
nb_itk = nbl.load('M.tfm', fmt='itk')
66+
nb_fsl = np.loadtxt('M.fsl')
67+
68+
assert_equal(itk, nb_itk)
69+
assert_almost_equal(fsl, nb_fsl)
70+
71+
# Create version not aligned to canonical

‎nibabel/transform/base.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from scipy import ndimage as ndi
1414

15+
EQUALITY_TOL = 1e-5
16+
1517

1618
class ImageSpace(object):
1719
"""Class to represent spaces of gridded data (images)."""
@@ -85,7 +87,8 @@ def _to_hdf5(self, group):
8587

8688
def __eq__(self, other):
8789
try:
88-
return np.allclose(self.affine, other.affine) and self.shape == other.shape
90+
return np.allclose(
91+
self.affine, other.affine, rtol=EQUALITY_TOL) and self.shape == other.shape
8992
except AttributeError:
9093
pass
9194
return False
@@ -99,6 +102,12 @@ class TransformBase(object):
99102
def __init__(self):
100103
self._reference = None
101104

105+
def __eq__(self, other):
106+
"""Overload equals operator."""
107+
if not self._reference == other._reference:
108+
return False
109+
return np.allclose(self.matrix, other.matrix, rtol=EQUALITY_TOL)
110+
102111
@property
103112
def reference(self):
104113
'''A reference space where data will be resampled onto'''

‎nibabel/transform/linear.py

+34-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import sys
1111
import numpy as np
1212
from scipy import ndimage as ndi
13+
from pathlib import Path
1314

1415
from ..loadsave import load as loadimg
15-
from ..affines import from_matvec, voxel_sizes
16+
from ..affines import from_matvec, voxel_sizes, obliquity
17+
from ..volumeutils import shape_zoom_affine
1618
from .base import TransformBase
1719

1820

1921
LPS = np.diag([-1, -1, 1, 1])
22+
OBLIQUITY_THRESHOLD_DEG = 0.01
2023

2124

2225
class Affine(TransformBase):
@@ -64,6 +67,7 @@ def __init__(self, matrix=None, reference=None):
6467

6568
@property
6669
def matrix(self):
70+
"""Access the internal representation of this affine."""
6771
return self._matrix
6872

6973
def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
@@ -217,8 +221,35 @@ def to_filename(self, filename, fmt='X5', moving=None):
217221
return filename
218222

219223
if fmt.lower() == 'afni':
220-
parameters = np.swapaxes(LPS.dot(self.matrix.dot(LPS)), 0, 1)
221-
parameters = parameters[:, :3, :].reshape((self.matrix.shape[0], -1))
224+
from math import pi
225+
226+
if moving and isinstance(moving, (str, bytes, Path)):
227+
moving = loadimg(str(moving))
228+
229+
T = self.matrix.copy()
230+
pre = LPS
231+
post = LPS
232+
if any(obliquity(self.reference.affine) * 180 / pi > OBLIQUITY_THRESHOLD_DEG):
233+
print('Reference affine axes are oblique.')
234+
M = self.reference.affine
235+
A = shape_zoom_affine(self.reference.shape,
236+
voxel_sizes(M), x_flip=True)
237+
pre = M.dot(np.linalg.inv(A))
238+
239+
if not moving:
240+
moving = self.reference
241+
242+
if moving and any(obliquity(moving.affine) * 180 / pi > OBLIQUITY_THRESHOLD_DEG):
243+
print('Moving affine axes are oblique.')
244+
M = moving.affine
245+
A = shape_zoom_affine(moving.shape,
246+
voxel_sizes(M), x_flip=True)
247+
post = M.dot(np.linalg.inv(A))
248+
249+
# swapaxes is necessary, as axis 0 encodes series of transforms
250+
T = np.swapaxes(post.dot(self.matrix.copy().dot(pre)), 0, 1)
251+
parameters = np.swapaxes(post.dot(T.dot(pre)), 0, 1)
252+
parameters = parameters[:, :3, :].reshape((T.shape[0], -1))
222253
np.savetxt(filename, parameters, delimiter='\t', header="""\
223254
3dvolreg matrices (DICOM-to-DICOM, row-by-row):""", fmt='%g')
224255
return filename

0 commit comments

Comments
 (0)
Please sign in to comment.