Skip to content

Commit 044fe02

Browse files
committed
enh: ITK's coordinate system deciphered
1 parent 4ca2f68 commit 044fe02

File tree

2 files changed

+50
-18
lines changed

2 files changed

+50
-18
lines changed

nitransforms/io/itk.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,15 @@ def from_h5obj(cls, fileobj, check=True):
329329

330330

331331
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+
"""
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

370401
class ITKCompositeH5:

nitransforms/tests/test_nonlinear.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
272272
)
273273
if not warpfile.exists():
274274
pytest.skip("Composite transform test data not available")
275-
275+
276276
nii = ITKDisplacementsField.from_filename(warpfile)
277277

278278
# Get sampling indices
@@ -301,13 +301,14 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
301301
ants_mapped_xyz = ants_pts.reshape(*shape, 3)
302302
nit_mapped_xyz = mapped.reshape(*shape, 3)
303303

304+
nb.load(warpfile).to_filename(tmp_path / "original_ants_deltas.nii.gz")
305+
304306
nb.Nifti1Image(coords_map, ref_affine, None).to_filename(
305-
tmp_path / "baseline_field.nii.gz"
307+
tmp_path / "baseline_positions.nii.gz"
306308
)
307309

308-
nb.Nifti1Image(ants_mapped_xyz, ref_affine, None).to_filename(
309-
tmp_path / "ants_deformation_xyz.nii.gz"
310-
)
310+
nii.to_filename(tmp_path / "original_interpreted_deltas.nii.gz")
311+
311312
nb.Nifti1Image(nit_mapped_xyz, ref_affine, None).to_filename(
312313
tmp_path / "nit_deformation_xyz.nii.gz"
313314
)
@@ -336,7 +337,6 @@ def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, ongri
336337
)
337338

338339
coords_map = grid_xyz.reshape(*shape, 3)
339-
gold_mapped_xyz = coords_map + deltas
340340

341341
deltas = np.hstack(
342342
(
@@ -345,6 +345,7 @@ def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, ongri
345345
np.linspace(-50, 50, num=np.prod(shape)),
346346
)
347347
).reshape(shape + (3,))
348+
gold_mapped_xyz = coords_map + deltas
348349

349350
fieldnii = nb.Nifti1Image(deltas, ref_affine, None)
350351
warpfile = tmp_path / "itk_transform.nii.gz"

0 commit comments

Comments
 (0)