@@ -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
313346def _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