Skip to content

Commit 51672ed

Browse files
committed
enh: ITK's coordinate system deciphered
1 parent 6778457 commit 51672ed

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
@@ -172,7 +172,7 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
172172
)
173173
if not warpfile.exists():
174174
pytest.skip("Composite transform test data not available")
175-
175+
176176
nii = ITKDisplacementsField.from_filename(warpfile)
177177

178178
# Get sampling indices
@@ -201,13 +201,14 @@ def test_densefield_map_vs_ants(testdata_path, tmp_path, ongrid):
201201
ants_mapped_xyz = ants_pts.reshape(*shape, 3)
202202
nit_mapped_xyz = mapped.reshape(*shape, 3)
203203

204+
nb.load(warpfile).to_filename(tmp_path / "original_ants_deltas.nii.gz")
205+
204206
nb.Nifti1Image(coords_map, ref_affine, None).to_filename(
205-
tmp_path / "baseline_field.nii.gz"
207+
tmp_path / "baseline_positions.nii.gz"
206208
)
207209

208-
nb.Nifti1Image(ants_mapped_xyz, ref_affine, None).to_filename(
209-
tmp_path / "ants_deformation_xyz.nii.gz"
210-
)
210+
nii.to_filename(tmp_path / "original_interpreted_deltas.nii.gz")
211+
211212
nb.Nifti1Image(nit_mapped_xyz, ref_affine, None).to_filename(
212213
tmp_path / "nit_deformation_xyz.nii.gz"
213214
)
@@ -236,7 +237,6 @@ def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, ongri
236237
)
237238

238239
coords_map = grid_xyz.reshape(*shape, 3)
239-
gold_mapped_xyz = coords_map + deltas
240240

241241
deltas = np.hstack(
242242
(
@@ -245,6 +245,7 @@ def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, ongri
245245
np.linspace(-50, 50, num=np.prod(shape)),
246246
)
247247
).reshape(shape + (3,))
248+
gold_mapped_xyz = coords_map + deltas
248249

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

0 commit comments

Comments
 (0)