@@ -37,7 +37,32 @@ def to_string(self, banner=True):
37
37
38
38
@classmethod
39
39
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
+ """
41
66
# swapaxes is necessary, as axis 0 encodes series of transforms
42
67
43
68
reference = _ensure_image (reference )
@@ -48,6 +73,7 @@ def from_ras(cls, ras, moving=None, reference=None):
48
73
if moving is not None and _is_oblique (moving .affine ):
49
74
ras = _cardinal_rotation (moving .affine , True ) @ ras
50
75
76
+ # AFNI represents affine transformations as LPS-to-LPS
51
77
parameters = np .swapaxes (LPS @ ras @ LPS , 0 , 1 )
52
78
53
79
tf = cls ()
@@ -213,9 +239,17 @@ def _afni_deobliqued_grid(oblique, shape):
213
239
vs = voxel_sizes (oblique )
214
240
215
241
# 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
+ )
219
253
corners_xyz = oblique @ np .hstack ((corners_ijk , np .ones ((len (corners_ijk ), 1 )))).T
220
254
extent = corners_xyz .min (1 )[:3 ], corners_xyz .max (1 )[:3 ]
221
255
nshape = ((extent [1 ] - extent [0 ]) / vs + 0.999 ).astype (int )
@@ -280,8 +314,7 @@ def _cardinal_rotation(oblique, real_to_card=True):
280
314
"""
281
315
card = _dicom_real_to_card (oblique )
282
316
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 )
285
318
)
286
319
287
320
@@ -312,11 +345,10 @@ def _afni_warpdrive(oblique, forward=True):
312
345
313
346
def _afni_header (nii , field = "WARPDRIVE_MATVEC_FOR_000000" , to_ras = False ):
314
347
from lxml import etree
348
+
315
349
root = etree .fromstring (nii .header .extensions [0 ].get_content ().decode ())
316
350
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"
320
352
)
321
353
if retval .size == 12 :
322
354
retval = np .vstack ((retval .reshape ((3 , 4 )), (0 , 0 , 0 , 1 )))
0 commit comments