@@ -196,7 +196,7 @@ def from_string(cls, string):
196
196
lines = lines [1 :] # Drop banner with version
197
197
198
198
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 ])
200
200
sa ["offset" ] = np .genfromtxt (
201
201
[lines [3 ].split (":" )[- 1 ].encode ()], dtype = cls .dtype ["offset" ]
202
202
)
@@ -329,7 +329,15 @@ def from_h5obj(cls, fileobj, check=True):
329
329
330
330
331
331
class 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
+ """
333
341
334
342
@classmethod
335
343
def from_image (cls , imgobj ):
@@ -347,24 +355,47 @@ def from_image(cls, imgobj):
347
355
warnings .warn ("Incorrect intent identified." )
348
356
hdr .set_intent ("vector" )
349
357
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
356
383
357
384
@classmethod
358
385
def to_image (cls , imgobj ):
359
386
"""Export a displacements field from a nibabel object."""
360
387
361
388
hdr = imgobj .header .copy ()
362
389
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
+ )
368
399
369
400
370
401
class ITKCompositeH5 :
0 commit comments