Skip to content

Commit 4ed6878

Browse files
committed
enh(afni): add warpdrive tests
1 parent fa47e5a commit 4ed6878

File tree

3 files changed

+108
-13
lines changed

3 files changed

+108
-13
lines changed

nitransforms/io/afni.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ def from_image(cls, imgobj):
147147

148148

149149
def _is_oblique(affine, thres=OBLIQUITY_THRESHOLD_DEG):
150+
"""
151+
Determine whether the dataset is oblique.
152+
153+
Examples
154+
--------
155+
>>> _is_oblique(np.eye(4))
156+
False
157+
158+
>>> _is_oblique(nb.affines.from_matvec(
159+
... nb.eulerangles.euler2mat(x=0.9, y=0.001, z=0.001),
160+
... [4.0, 2.0, -1.0],
161+
... ))
162+
True
163+
164+
"""
150165
return (obliquity(affine).min() * 180 / pi) > thres
151166

152167

@@ -161,7 +176,8 @@ def _afni_warpdrive(oblique, shape, forward=True, ras=False):
161176
plumb : 4x4 numpy.array
162177
corresponding affine that is aligned to the cardinal axes.
163178
forward : :obj:`bool`
164-
Transforms the affine of oblique into an AFNI's plumb (if ``True``)
179+
Returns the forward transformation if True, i.e.,
180+
the matrix to convert an oblique affine into an AFNI's plumb (if ``True``)
165181
or viceversa plumb -> oblique (if ``false``).
166182
167183
Returns
@@ -177,21 +193,19 @@ def _afni_warpdrive(oblique, shape, forward=True, ras=False):
177193
plumb_r *= voxel_sizes(oblique)
178194
plumb = np.eye(4)
179195
plumb[:3, :3] = plumb_r
180-
obliq_o = apply_affine(oblique, 0.5 * (shape - 1))
181-
plumb_c = apply_affine(plumb, 0.5 * (shape - 1))
182-
plumb[:3, 3] = -plumb_c + obliq_o
183-
print(obliq_o, apply_affine(plumb, 0.5 * (shape - 1)))
184-
196+
obliq_c = oblique @ np.hstack((0.5 * shape, 1.0))
197+
plumb_c = plumb @ np.hstack((0.5 * shape, 1.0))
198+
plumb[:3, 3] = -plumb_c[:3] + obliq_c[:3]
185199
R = plumb @ np.linalg.inv(oblique)
200+
186201
if not ras:
187202
# Change sign to match AFNI's warpdrive_matvec signs
188-
B = np.ones((2, 2))
189-
R *= np.block([[B, -1.0 * B], [-1.0 * B, B]])
203+
R = LPS @ R @ LPS
190204

191205
return R if forward else np.linalg.inv(R)
192206

193207

194-
def _afni_header(nii, field="WARPDRIVE_MATVEC_FOR_000000"):
208+
def _afni_header(nii, field="WARPDRIVE_MATVEC_FOR_000000", to_ras=False):
195209
from lxml import etree
196210
root = etree.fromstring(nii.header.extensions[0].get_content().decode())
197211
retval = np.fromstring(
@@ -200,6 +214,8 @@ def _afni_header(nii, field="WARPDRIVE_MATVEC_FOR_000000"):
200214
dtype="float32"
201215
)
202216
if retval.size == 12:
203-
return np.vstack((retval.reshape((3, 4)), (0, 0, 0, 1)))
217+
retval = np.vstack((retval.reshape((3, 4)), (0, 0, 0, 1)))
218+
if to_ras:
219+
retval = LPS @ retval @ LPS
204220

205221
return retval

nitransforms/tests/test_io.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""I/O test cases."""
4+
from subprocess import check_call
5+
from io import StringIO
6+
import filecmp
7+
import shutil
48
import numpy as np
59
import pytest
6-
from io import StringIO
710

8-
import filecmp
911
import nibabel as nb
1012
from nibabel.eulerangles import euler2mat
1113
from nibabel.affines import from_matvec
@@ -433,3 +435,80 @@ def test_itk_h5(testdata_path):
433435
)
434436
def test_regressions(file_type, test_file, data_path):
435437
file_type.from_filename(data_path / "regressions" / test_file)
438+
439+
440+
@pytest.mark.parametrize("parameters", [
441+
{"x": 0.1, "y": 0.03, "z": 0.002},
442+
{"x": 0.001, "y": 0.3, "z": 0.002},
443+
{"x": 0.01, "y": 0.03, "z": 0.2},
444+
])
445+
@pytest.mark.parametrize("dir_x", (-1, 1))
446+
@pytest.mark.parametrize("dir_y", (-1, 1))
447+
@pytest.mark.parametrize("dir_z", (1, -1))
448+
@pytest.mark.parametrize("swapaxes", [
449+
None, (0, 1), (1, 2), (0, 2),
450+
])
451+
def test_afni_oblique(tmpdir, parameters, swapaxes, testdata_path, dir_x, dir_y, dir_z):
452+
tmpdir.chdir()
453+
img = nb.load(testdata_path / "someones_anatomy.nii.gz")
454+
shape = np.array(img.shape[:3])
455+
hdr = img.header.copy()
456+
aff = img.affine.copy()
457+
data = np.asanyarray(img.dataobj, dtype="uint8")
458+
459+
directions = (dir_x, dir_y, dir_z)
460+
if directions != (1, 1, 1):
461+
last_ijk = np.hstack((shape - 1, 1.0))
462+
last_xyz = aff @ last_ijk
463+
aff = np.diag((dir_x, dir_y, dir_z, 1)) @ aff
464+
465+
for ax in range(3):
466+
if (directions[ax] == -1):
467+
aff[ax, 3] = last_xyz[ax]
468+
data = np.flip(data, ax)
469+
470+
hdr.set_qform(aff, code=1)
471+
hdr.set_sform(aff, code=1)
472+
img.__class__(data, aff, hdr).to_filename("flips.nii.gz")
473+
474+
if swapaxes is not None:
475+
data = np.swapaxes(data, swapaxes[0], swapaxes[1])
476+
aff[reversed(swapaxes), :] = aff[(swapaxes), :]
477+
478+
hdr.set_qform(aff, code=1)
479+
hdr.set_sform(aff, code=1)
480+
img.__class__(data, aff, hdr).to_filename("swaps.nii.gz")
481+
482+
R = from_matvec(euler2mat(**parameters), [0.0, 0.0, 0.0])
483+
484+
img_center = aff @ np.hstack((shape * 0.5, 1.0))
485+
R[:3, 3] += (img_center - (R @ aff @ np.hstack((shape * 0.5, 1.0))))[:3]
486+
newaff = R @ aff
487+
hdr.set_qform(newaff, code=1)
488+
hdr.set_sform(newaff, code=1)
489+
img = img.__class__(data, newaff, hdr)
490+
img.to_filename("oblique.nii.gz")
491+
492+
# Run AFNI's 3dDeoblique
493+
if not shutil.which("3dWarp"):
494+
pytest.skip("Command 3dWarp not found on host")
495+
496+
cmd = f"3dWarp -verb -deoblique -prefix {tmpdir}/deob.nii.gz {tmpdir}/oblique.nii.gz"
497+
498+
# resample mask
499+
assert check_call([cmd], shell=True) == 0
500+
afni_warpdrive_inv = afni._afni_header(
501+
nb.load("deob.nii.gz"),
502+
field="WARPDRIVE_MATVEC_INV_000000",
503+
to_ras=True,
504+
)
505+
506+
deobnii = nb.load("deob.nii.gz")
507+
508+
# Confirm AFNI's rotation of axis is consistent with the one we introduced
509+
assert np.allclose(afni_warpdrive_inv[:3, :3], R[:3, :3])
510+
511+
# Check nitransforms' estimation of warpdrive with header
512+
nt_warpdrive_inv = afni._afni_warpdrive(newaff, img.shape, forward=False)
513+
import pdb;pdb.set_trace()
514+
assert np.allclose(afni_warpdrive_inv[:3, :3], nt_warpdrive_inv[:3, :3])

nitransforms/tests/test_linear.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def test_linear_save(tmpdir, data_path, get_testdata, image_orientation, sw_tool
165165
assert_affines_by_filename(xfm_fname1, xfm_fname2)
166166

167167

168-
@pytest.mark.parametrize("image_orientation", ["RAS", "LAS", "LPS", ]) # 'oblique',
168+
@pytest.mark.parametrize("image_orientation", ["RAS", "LAS", "LPS", 'oblique', ])
169169
@pytest.mark.parametrize("sw_tool", ["itk", "fsl", "afni", "fs"])
170170
def test_apply_linear_transform(tmpdir, get_testdata, get_testmask, image_orientation, sw_tool):
171171
"""Check implementation of exporting affines to formats."""

0 commit comments

Comments
 (0)