Skip to content

Commit 355a07c

Browse files
oestebanfeilong
andcommitted
fix: proper ordering of displacements fields when reading and writing
Transposes data as suggested by @feilong when reading and writing displacements fields. Tests have been added to ensure round-trip consistency. Related-to: #171. Co-Authored-By: Feilong Ma <[email protected]>
1 parent 60168b2 commit 355a07c

File tree

4 files changed

+35
-30
lines changed

4 files changed

+35
-30
lines changed

env.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ dependencies:
2424
- nitime=0.10
2525
- scikit-image=0.22
2626
- scikit-learn=1.4
27+
# SimpleITK, so build doesn't complain about building scikit from sources
28+
- simpleitk=2.4
2729
# Utilities
2830
- graphviz=9.0
2931
- pandoc=3.1

nitransforms/io/itk.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,10 @@ def from_image(cls, imgobj):
347347
warnings.warn("Incorrect intent identified.")
348348
hdr.set_intent("vector")
349349

350-
field = np.squeeze(np.asanyarray(imgobj.dataobj))
350+
field = np.squeeze(np.asanyarray(imgobj.dataobj)).transpose(2, 1, 0, 3)
351351
field[..., (0, 1)] *= -1.0
352352

353-
return imgobj.__class__(field, imgobj.affine, hdr)
353+
return imgobj.__class__(field, LPS @ imgobj.affine, hdr)
354354

355355
@classmethod
356356
def to_image(cls, imgobj):
@@ -359,10 +359,9 @@ def to_image(cls, imgobj):
359359
hdr = imgobj.header.copy()
360360
hdr.set_intent("vector")
361361

362-
warp_data = imgobj.get_fdata().reshape(imgobj.shape[:3] + (1, imgobj.shape[-1]))
363-
warp_data[..., (0, 1)] *= -1
364-
365-
return imgobj.__class__(warp_data, imgobj.affine, hdr)
362+
field = imgobj.get_fdata().transpose(2, 1, 0, 3)[..., None, :]
363+
field[..., (0, 1)] *= -1.0
364+
return imgobj.__class__(field, LPS @ imgobj.affine, hdr)
366365

367366

368367
class ITKCompositeH5:
@@ -410,21 +409,16 @@ def from_h5obj(cls, fileobj, check=True, only_linear=False):
410409
directions = np.reshape(_fixed[9:], (3, 3))
411410
affine = from_matvec(directions * zooms, offset)
412411
# ITK uses Fortran ordering, like NIfTI, but with the vector dimension first
413-
field = np.moveaxis(
414-
np.reshape(
415-
xfm[f"{typo_fallback}Parameters"],
416-
(3, *shape.astype(int)),
417-
order="F",
418-
),
419-
0,
420-
-1,
421-
)
422-
field[..., (0, 1)] *= -1.0
412+
# In practice, this seems to work (see issue #171)
413+
field = np.reshape(
414+
xfm[f"{typo_fallback}Parameters"], (*shape.astype(int), 3)
415+
).transpose(2, 1, 0, 3)
416+
423417
hdr = Nifti1Header()
424418
hdr.set_intent("vector")
425419
hdr.set_data_dtype("float")
426420

427-
xfm_list.append(Nifti1Image(field.astype("float"), LPS @ affine, hdr))
421+
xfm_list.append(Nifti1Image(field.astype("float"), affine, hdr))
428422
continue
429423

430424
raise TransformIOError(

nitransforms/tests/test_resampling.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -175,19 +175,20 @@ def test_displacements_field1(
175175
msk.to_filename("mask.nii.gz")
176176

177177
fieldmap = np.zeros(
178-
(*nii.shape[:3], 1, 3) if sw_tool != "fsl" else (*nii.shape[:3], 3),
178+
(*nii.shape[:3], 1, 3) if sw_tool == "afni" else (*nii.shape[:3], 3),
179179
dtype="float32",
180180
)
181181
fieldmap[..., axis] = -10.0
182-
183182
_hdr = nii.header.copy()
184-
if sw_tool in ("itk",):
185-
_hdr.set_intent("vector")
186183
_hdr.set_data_dtype("float32")
187184

188-
xfm_fname = "warp.nii.gz"
189185
field = nb.Nifti1Image(fieldmap, nii.affine, _hdr)
190-
field.to_filename(xfm_fname)
186+
187+
xfm_fname = "warp.nii.gz"
188+
if sw_tool == "itk":
189+
io.itk.ITKDisplacementsField.to_filename(field, xfm_fname)
190+
else:
191+
field.to_filename(xfm_fname)
191192

192193
xfm = nitnl.load(xfm_fname, fmt=sw_tool)
193194

@@ -197,7 +198,8 @@ def test_displacements_field1(
197198
reference=tmp_path / "mask.nii.gz",
198199
moving=tmp_path / "mask.nii.gz",
199200
output=tmp_path / "resampled_brainmask.nii.gz",
200-
extra="--output-data-type uchar" if sw_tool == "itk" else "",
201+
extra="",
202+
# extra="--output-data-type uchar" if sw_tool == "itk" else "",
201203
)
202204

203205
# skip test if command is not available on host
@@ -207,14 +209,18 @@ def test_displacements_field1(
207209

208210
# resample mask
209211
exit_code = check_call([cmd], shell=True)
210-
assert exit_code == 0
211212
sw_moved_mask = nb.load("resampled_brainmask.nii.gz")
212213
nt_moved_mask = apply(xfm, msk, order=0)
213-
nt_moved_mask.set_data_dtype(msk.get_data_dtype())
214-
diff = np.asanyarray(sw_moved_mask.dataobj) - np.asanyarray(nt_moved_mask.dataobj)
215214

216-
assert np.sqrt((diff**2).mean()) < RMSE_TOL_LINEAR
215+
# Calculate xor between both:
216+
sw_mask = np.asanyarray(sw_moved_mask.dataobj, dtype=bool)
217217
brainmask = np.asanyarray(nt_moved_mask.dataobj, dtype=bool)
218+
percent_diff = (sw_mask != brainmask)[5:-5, 5:-5, 5:-5].sum() / brainmask.size
219+
220+
assert exit_code == 0
221+
assert percent_diff < 1e-8, (
222+
f"Resampled masks differed by {percent_diff * 100:0.2f}%."
223+
)
218224

219225
# Then apply the transform and cross-check with software
220226
cmd = APPLY_NONLINEAR_CMD[sw_tool](
@@ -236,8 +242,9 @@ def test_displacements_field1(
236242
diff = np.asanyarray(
237243
sw_moved.dataobj, dtype=sw_moved.get_data_dtype()
238244
) - np.asanyarray(nt_moved.dataobj, dtype=nt_moved.get_data_dtype())
239-
# A certain tolerance is necessary because of resampling at borders
240-
assert np.sqrt((diff[brainmask] ** 2).mean()) < RMSE_TOL_LINEAR
245+
246+
# Drop samples close to the border of the image
247+
assert np.sqrt((diff[5:-5, 5:-5, 5:-5] ** 2).mean()) < 1e-6
241248

242249

243250
@pytest.mark.xfail(

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ test = [
4141
"pytest-xdist >= 2.5",
4242
"coverage[toml] >= 5.2.1",
4343
"nitransforms[niftiext]",
44+
"SimpleITK ~= 2.4",
45+
"scikit-build",
4446
]
4547
# Aliases
4648
niftiexts = ["nitransforms[niftiext]"]

0 commit comments

Comments
 (0)