Skip to content

Commit 04ce632

Browse files
authored
Merge pull request #39 from oesteban/enh/io-fsl-afni
ENH: Refactor of AFNI and FSL i/o with StringStructs
2 parents 71277ae + 71d8a57 commit 04ce632

17 files changed

+736
-305
lines changed

nitransforms/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""
1919
from .linear import Affine
2020
from .nonlinear import DisplacementsFieldTransform
21-
from .io import itk
21+
from .io import afni, fsl, itk
2222

2323

24-
__all__ = ['itk', 'Affine', 'DisplacementsFieldTransform']
24+
__all__ = ['afni', 'fsl', 'itk', 'Affine', 'DisplacementsFieldTransform']

nitransforms/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,13 @@ def apply(self, spatialimage, reference=None,
225225
226226
"""
227227
if reference is not None and isinstance(reference, (str, Path)):
228-
reference = load(reference)
228+
reference = load(str(reference))
229229

230230
_ref = self.reference if reference is None \
231231
else SpatialReference.factory(reference)
232232

233233
if isinstance(spatialimage, (str, Path)):
234-
spatialimage = load(spatialimage)
234+
spatialimage = load(str(spatialimage))
235235

236236
data = np.asanyarray(spatialimage.dataobj)
237237
output_dtype = output_dtype or data.dtype

nitransforms/io/afni.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Read/write AFNI's transforms."""
2+
from math import pi
3+
import numpy as np
4+
from nibabel.affines import obliquity, voxel_sizes
5+
6+
from ..patched import shape_zoom_affine
7+
from .base import BaseLinearTransformList, LinearParameters, TransformFileError
8+
9+
LPS = np.diag([-1, -1, 1, 1])
10+
OBLIQUITY_THRESHOLD_DEG = 0.01
11+
12+
13+
class AFNILinearTransform(LinearParameters):
14+
"""A string-based structure for AFNI linear transforms."""
15+
16+
def __str__(self):
17+
"""Generate a string representation."""
18+
param = self.structarr['parameters']
19+
return '\t'.join(['%g' % p for p in param[:3, :].reshape(-1)])
20+
21+
def to_string(self, banner=True):
22+
"""Convert to a string directly writeable to file."""
23+
string = '%s\n' % self
24+
if banner:
25+
string = '\n'.join(("# 3dvolreg matrices (DICOM-to-DICOM, row-by-row):",
26+
string))
27+
return string % self
28+
29+
@classmethod
30+
def from_ras(cls, ras, moving=None, reference=None):
31+
"""Create an ITK affine from a nitransform's RAS+ matrix."""
32+
ras = ras.copy()
33+
pre = LPS.copy()
34+
post = LPS.copy()
35+
if _is_oblique(reference.affine):
36+
print('Reference affine axes are oblique.')
37+
M = reference.affine
38+
A = shape_zoom_affine(reference.shape,
39+
voxel_sizes(M), x_flip=False, y_flip=False)
40+
pre = M.dot(np.linalg.inv(A)).dot(LPS)
41+
42+
if _is_oblique(moving.affine):
43+
print('Moving affine axes are oblique.')
44+
M2 = moving.affine
45+
A2 = shape_zoom_affine(moving.shape,
46+
voxel_sizes(M2), x_flip=True, y_flip=True)
47+
post = A2.dot(np.linalg.inv(M2))
48+
49+
# swapaxes is necessary, as axis 0 encodes series of transforms
50+
parameters = np.swapaxes(post.dot(ras.dot(pre)), 0, 1)
51+
52+
tf = cls()
53+
tf.structarr['parameters'] = parameters.T
54+
return tf
55+
56+
@classmethod
57+
def from_string(cls, string):
58+
"""Read the struct from string."""
59+
tf = cls()
60+
sa = tf.structarr
61+
lines = [
62+
l for l in string.splitlines()
63+
if l.strip() and not (l.startswith('#') or '3dvolreg matrices' in l)
64+
]
65+
66+
if not lines:
67+
raise TransformFileError
68+
69+
parameters = np.vstack((
70+
np.genfromtxt([lines[0].encode()],
71+
dtype='f8').reshape((3, 4)),
72+
(0., 0., 0., 1.)))
73+
sa['parameters'] = parameters
74+
return tf
75+
76+
77+
class AFNILinearTransformArray(BaseLinearTransformList):
78+
"""A string-based structure for series of AFNI linear transforms."""
79+
80+
_inner_type = AFNILinearTransform
81+
82+
def to_ras(self, moving=None, reference=None):
83+
"""Return a nitransforms' internal RAS matrix."""
84+
return np.stack([xfm.to_ras(moving=moving, reference=reference)
85+
for xfm in self.xforms])
86+
87+
def to_string(self):
88+
"""Convert to a string directly writeable to file."""
89+
strings = []
90+
for i, xfm in enumerate(self.xforms):
91+
lines = [
92+
l.strip()
93+
for l in xfm.to_string(banner=(i == 0)).splitlines()
94+
if l.strip()]
95+
strings += lines
96+
return '\n'.join(strings)
97+
98+
@classmethod
99+
def from_ras(cls, ras, moving=None, reference=None):
100+
"""Create an ITK affine from a nitransform's RAS+ matrix."""
101+
_self = cls()
102+
_self.xforms = [cls._inner_type.from_ras(
103+
ras[i, ...], moving=moving, reference=reference)
104+
for i in range(ras.shape[0])]
105+
return _self
106+
107+
@classmethod
108+
def from_string(cls, string):
109+
"""Read the struct from string."""
110+
_self = cls()
111+
112+
lines = [l.strip() for l in string.splitlines()
113+
if l.strip() and not l.startswith('#')]
114+
if not lines:
115+
raise TransformFileError('Input string is empty.')
116+
117+
_self.xforms = [cls._inner_type.from_string(l)
118+
for l in lines]
119+
return _self
120+
121+
122+
def _is_oblique(affine, thres=OBLIQUITY_THRESHOLD_DEG):
123+
return (obliquity(affine).min() * 180 / pi) > thres

nitransforms/io/base.py

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Read/write linear transforms."""
2+
import numpy as np
23
from scipy.io.matlab.miobase import get_matfile_version
34
from scipy.io.matlab.mio4 import MatFile4Reader
45
from scipy.io.matlab.mio5 import MatFile5Reader
@@ -29,12 +30,141 @@ def __array__(self):
2930
return self._structarr
3031

3132

33+
class LinearParameters(StringBasedStruct):
34+
"""
35+
A string-based structure for linear transforms.
36+
37+
Examples
38+
--------
39+
>>> lp = LinearParameters()
40+
>>> np.all(lp.structarr['parameters'] == np.eye(4))
41+
True
42+
43+
>>> p = np.diag([2., 2., 2., 1.])
44+
>>> lp = LinearParameters(p)
45+
>>> np.all(lp.structarr['parameters'] == p)
46+
True
47+
48+
"""
49+
50+
template_dtype = np.dtype([
51+
('parameters', 'f8', (4, 4)),
52+
])
53+
dtype = template_dtype
54+
55+
def __init__(self, parameters=None):
56+
"""Initialize with default parameters."""
57+
super().__init__()
58+
self.structarr['parameters'] = np.eye(4)
59+
if parameters is not None:
60+
self.structarr['parameters'] = parameters
61+
62+
def to_filename(self, filename):
63+
"""Store this transform to a file with the appropriate format."""
64+
with open(str(filename), 'w') as f:
65+
f.write(self.to_string())
66+
67+
def to_ras(self, moving=None, reference=None):
68+
"""Return a nitransforms internal RAS+ matrix."""
69+
raise NotImplementedError
70+
71+
@classmethod
72+
def from_filename(cls, filename):
73+
"""Read the struct from a file given its path."""
74+
with open(str(filename)) as f:
75+
string = f.read()
76+
return cls.from_string(string)
77+
78+
@classmethod
79+
def from_fileobj(cls, fileobj, check=True):
80+
"""Read the struct from a file object."""
81+
return cls.from_string(fileobj.read())
82+
83+
@classmethod
84+
def from_string(cls, string):
85+
"""Read the struct from string."""
86+
raise NotImplementedError
87+
88+
89+
class BaseLinearTransformList(StringBasedStruct):
90+
"""A string-based structure for series of linear transforms."""
91+
92+
template_dtype = np.dtype([('nxforms', 'i4')])
93+
dtype = template_dtype
94+
_xforms = None
95+
_inner_type = LinearParameters
96+
97+
def __init__(self,
98+
xforms=None,
99+
binaryblock=None,
100+
endianness=None,
101+
check=True):
102+
"""Initialize with (optionally) a list of transforms."""
103+
super().__init__(binaryblock, endianness, check)
104+
self.xforms = [self._inner_type(parameters=mat)
105+
for mat in xforms or []]
106+
107+
@property
108+
def xforms(self):
109+
"""Get the list of internal transforms."""
110+
return self._xforms
111+
112+
@xforms.setter
113+
def xforms(self, value):
114+
self._xforms = list(value)
115+
116+
def __getitem__(self, idx):
117+
"""Allow dictionary access to the transforms."""
118+
if idx == 'xforms':
119+
return self._xforms
120+
if idx == 'nxforms':
121+
return len(self._xforms)
122+
raise KeyError(idx)
123+
124+
def to_filename(self, filename):
125+
"""Store this transform to a file with the appropriate format."""
126+
with open(str(filename), 'w') as f:
127+
f.write(self.to_string())
128+
129+
def to_ras(self, moving=None, reference=None):
130+
"""Return a nitransforms' internal RAS matrix."""
131+
raise NotImplementedError
132+
133+
def to_string(self):
134+
"""Convert to a string directly writeable to file."""
135+
raise NotImplementedError
136+
137+
@classmethod
138+
def from_filename(cls, filename):
139+
"""Read the struct from a file given its path."""
140+
with open(str(filename)) as f:
141+
string = f.read()
142+
return cls.from_string(string)
143+
144+
@classmethod
145+
def from_fileobj(cls, fileobj, check=True):
146+
"""Read the struct from a file object."""
147+
return cls.from_string(fileobj.read())
148+
149+
@classmethod
150+
def from_ras(cls, ras, moving=None, reference=None):
151+
"""Create an ITK affine from a nitransform's RAS+ matrix."""
152+
raise NotImplementedError
153+
154+
@classmethod
155+
def from_string(cls, string):
156+
"""Read the struct from string."""
157+
raise NotImplementedError
158+
159+
32160
def _read_mat(byte_stream):
33161
mjv, _ = get_matfile_version(byte_stream)
34162
if mjv == 0:
35163
reader = MatFile4Reader(byte_stream)
36164
elif mjv == 1:
37165
reader = MatFile5Reader(byte_stream)
38166
elif mjv == 2:
39-
raise TransformFileError('Please use HDF reader for matlab v7.3 files')
167+
raise TransformFileError('Please use HDF reader for Matlab v7.3 files')
168+
else:
169+
raise TransformFileError('Not a Matlab file.')
40170
return reader.get_variables()

0 commit comments

Comments
 (0)