@@ -196,7 +196,7 @@ def from_string(cls, string):
196196 lines = lines [1 :] # Drop banner with version
197197
198198 parameters = np .eye (4 , dtype = "f4" )
199- sa ["index" ] = int (lines [0 ][lines [0 ].index ("T" ):].split ()[1 ])
199+ sa ["index" ] = int (lines [0 ][lines [0 ].index ("T" ) :].split ()[1 ])
200200 sa ["offset" ] = np .genfromtxt (
201201 [lines [3 ].split (":" )[- 1 ].encode ()], dtype = cls .dtype ["offset" ]
202202 )
@@ -329,7 +329,15 @@ def from_h5obj(cls, fileobj, check=True):
329329
330330
331331class ITKDisplacementsField (DisplacementsField ):
332- """A data structure representing displacements fields."""
332+ """
333+ A data structure representing displacements fields.
334+
335+ Note that ITK's world coordinates are LPS:
336+ http://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/.
337+ This translates into the fact that `antsApplyTransformsToPoints` expects LPS coordinates,
338+ and therefore, points must correct for the x and y directions before feeding into the tool.
339+
340+ """
333341
334342 @classmethod
335343 def from_image (cls , imgobj ):
@@ -347,24 +355,47 @@ def from_image(cls, imgobj):
347355 warnings .warn ("Incorrect intent identified." )
348356 hdr .set_intent ("vector" )
349357
350- field = np .squeeze (np .asanyarray (imgobj .dataobj ))
351- affine = imgobj .affine
352- midindex = (np .array (field .shape [:3 ]) - 1 ) * 0.5
353- offset = (LPS @ affine - affine ) @ (* midindex , 1 )
354- affine [:3 , 3 ] += offset [:3 ]
355- return imgobj .__class__ (np .flip (field , axis = (0 , 1 )), imgobj .affine , hdr )
358+ affine , qcode = hdr .get_qform (coded = True )
359+
360+ if qcode != 1 :
361+ warnings .warn (
362+ "Displacements field has qform code={qcode}, which is not ITK-like. "
363+ "Setting it to 1 (aligned with the image)."
364+ )
365+ affine = imgobj .affine
366+ hdr .set_qform (imgobj .affine , code = 1 )
367+ hdr .set_sform (imgobj .affine , code = 0 )
368+
369+ # ITK uses LPS coordinates, so first we patch the affine's offset
370+ # This patch was developed under gh-266, by adapting
371+ # nitransforms/tests/test_nonlinear.py::test_densefield_map_vs_ants[True]
372+ # until the same delta maps were obtained with `antsApplyTransformsToPoints`
373+ # and our NiTransforms transform's `.map()`.
374+ mid_ijk = 0.5 * (np .array (imgobj .shape [:3 ]) - 1 )
375+ offset = (affine - LPS @ affine ) @ (* mid_ijk , 1.0 )
376+ affine [:3 , 3 ] -= offset [:3 ]
377+
378+ # Create a NIfTI image with the patched affine
379+ data = np .squeeze (imgobj .get_fdata (dtype = "float32" ))
380+ refmap = imgobj .__class__ (np .flip (data , axis = (0 , 1 )), affine , hdr )
381+
382+ return refmap
356383
357384 @classmethod
358385 def to_image (cls , imgobj ):
359386 """Export a displacements field from a nibabel object."""
360387
361388 hdr = imgobj .header .copy ()
362389 hdr .set_intent ("vector" )
363-
364- field = imgobj .get_fdata ()
365- field = field .transpose (2 , 1 , 0 , 3 )[..., None , :]
366- field [..., (0 , 1 )] *= 1.0
367- return imgobj .__class__ (field , LPS @ imgobj .affine , hdr )
390+ hdr .set_data_dtype ("float32" )
391+ affine = LPS @ imgobj .affine @ LPS
392+ hdr .set_qform (affine , code = 1 )
393+ hdr .set_sform (affine , code = 0 )
394+ hdr .set_xyzt_units ("mm" , None )
395+
396+ return imgobj .__class__ (
397+ imgobj .get_fdata (dtype = "float32" )[..., None , :], affine , hdr
398+ )
368399
369400
370401class ITKCompositeH5 :
0 commit comments