Skip to content

Commit bea9c27

Browse files
oestebaneffigies
andcommitted
Apply suggestions from code review
Co-authored-by: Chris Markiewicz <[email protected]>
1 parent 4d4e3d9 commit bea9c27

File tree

1 file changed

+41
-9
lines changed

1 file changed

+41
-9
lines changed

nitransforms/io/afni.py

+41-9
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,32 @@ def to_string(self, banner=True):
3737

3838
@classmethod
3939
def from_ras(cls, ras, moving=None, reference=None):
40-
"""Create an AFNI affine from a nitransform's RAS+ matrix."""
40+
"""Create an AFNI affine from a nitransform's RAS+ matrix.
41+
42+
AFNI implicitly de-obliques image affine matrices before applying transforms, so
43+
for consistency we update the transform to account for the obliquity of the images.
44+
45+
.. testsetup:
46+
>>> import pytest
47+
>>> pytest.skip()
48+
49+
>>> moving.affine == ras @ reference.affine
50+
51+
We can decompose the affines into oblique and de-obliqued components:
52+
53+
>>> moving.affine == m_obl @ m_deobl
54+
>>> reference.affine == r_obl @ r_deobl
55+
56+
To generate an equivalent AFNI transform, we need an effective transform (``e_ras``):
57+
58+
>>> m_obl @ m_deobl == ras @ r_obl @ r_deobl
59+
>>> m_deobl == inv(m_obl) @ ras @ r_obl @ r_deobl
60+
61+
Hence,
62+
63+
>>> m_deobl == e_ras @ r_deobl
64+
>>> e_ras == inv(m_obl) @ ras @ r_obl
65+
"""
4166
# swapaxes is necessary, as axis 0 encodes series of transforms
4267

4368
reference = _ensure_image(reference)
@@ -48,6 +73,7 @@ def from_ras(cls, ras, moving=None, reference=None):
4873
if moving is not None and _is_oblique(moving.affine):
4974
ras = _cardinal_rotation(moving.affine, True) @ ras
5075

76+
# AFNI represents affine transformations as LPS-to-LPS
5177
parameters = np.swapaxes(LPS @ ras @ LPS, 0, 1)
5278

5379
tf = cls()
@@ -213,9 +239,17 @@ def _afni_deobliqued_grid(oblique, shape):
213239
vs = voxel_sizes(oblique)
214240

215241
# Calculate new shape of deobliqued grid
216-
corners_ijk = np.array([
217-
(i, j, k) for k in (0, shape[2]) for j in (0, shape[1]) for i in (0, shape[0])
218-
]) - 0.5
242+
corners_ijk = (
243+
np.array(
244+
[
245+
(i, j, k)
246+
for k in (0, shape[2])
247+
for j in (0, shape[1])
248+
for i in (0, shape[0])
249+
]
250+
)
251+
- 0.5
252+
)
219253
corners_xyz = oblique @ np.hstack((corners_ijk, np.ones((len(corners_ijk), 1)))).T
220254
extent = corners_xyz.min(1)[:3], corners_xyz.max(1)[:3]
221255
nshape = ((extent[1] - extent[0]) / vs + 0.999).astype(int)
@@ -280,8 +314,7 @@ def _cardinal_rotation(oblique, real_to_card=True):
280314
"""
281315
card = _dicom_real_to_card(oblique)
282316
return (
283-
card @ np.linalg.inv(oblique) if real_to_card else
284-
oblique @ np.linalg.inv(card)
317+
card @ np.linalg.inv(oblique) if real_to_card else oblique @ np.linalg.inv(card)
285318
)
286319

287320

@@ -312,11 +345,10 @@ def _afni_warpdrive(oblique, forward=True):
312345

313346
def _afni_header(nii, field="WARPDRIVE_MATVEC_FOR_000000", to_ras=False):
314347
from lxml import etree
348+
315349
root = etree.fromstring(nii.header.extensions[0].get_content().decode())
316350
retval = np.fromstring(
317-
root.find(f".//*[@atr_name='{field}']").text,
318-
sep="\n",
319-
dtype="float32"
351+
root.find(f".//*[@atr_name='{field}']").text, sep="\n", dtype="float32"
320352
)
321353
if retval.size == 12:
322354
retval = np.vstack((retval.reshape((3, 4)), (0, 0, 0, 1)))

0 commit comments

Comments
 (0)