Skip to content

Commit 31a2484

Browse files
authored
Merge pull request #174 from nipy/enh/read-composite-itk-affine-h5
ENH: Read ITK's composite transforms with only affines
2 parents 6a5142f + 321c0ee commit 31a2484

File tree

4 files changed

+139
-20
lines changed

4 files changed

+139
-20
lines changed

nitransforms/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
_data = None
1010
_brainmask = None
1111
_testdir = Path(os.getenv("TEST_DATA_HOME", "~/.nitransforms/testdata")).expanduser()
12+
_datadir = Path(__file__).parent / "tests" / "data"
1213

1314

1415
@pytest.fixture(autouse=True)
@@ -18,7 +19,7 @@ def doctest_autoimport(doctest_namespace):
1819
doctest_namespace["nb"] = nb
1920
doctest_namespace["os"] = os
2021
doctest_namespace["Path"] = Path
21-
doctest_namespace["regress_dir"] = Path(__file__).parent / "tests" / "data"
22+
doctest_namespace["regress_dir"] = _datadir
2223
doctest_namespace["test_dir"] = _testdir
2324

2425
tmpdir = tempfile.TemporaryDirectory()
@@ -35,7 +36,7 @@ def doctest_autoimport(doctest_namespace):
3536
@pytest.fixture
3637
def data_path():
3738
"""Return the test data folder."""
38-
return Path(__file__).parent / "tests" / "data"
39+
return _datadir
3940

4041

4142
@pytest.fixture

nitransforms/io/itk.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33
import numpy as np
44
from scipy.io import loadmat as _read_mat, savemat as _save_mat
5+
from h5py import File as H5File
56
from nibabel import Nifti1Header, Nifti1Image
67
from nibabel.affines import from_matvec
78
from .base import (
@@ -112,6 +113,9 @@ def from_filename(cls, filename):
112113
if str(filename).endswith(".mat"):
113114
with open(str(filename), "rb") as fileobj:
114115
return cls.from_binary(fileobj)
116+
elif str(filename).endswith(".h5"):
117+
with H5File(str(filename)) as f:
118+
return cls.from_h5obj(f)
115119

116120
with open(str(filename)) as fileobj:
117121
return cls.from_string(fileobj.read())
@@ -121,6 +125,10 @@ def from_fileobj(cls, fileobj, check=True):
121125
"""Read the struct from a file object."""
122126
if fileobj.name.endswith(".mat"):
123127
return cls.from_binary(fileobj)
128+
elif fileobj.name.endswith(".h5"):
129+
with H5File(fileobj) as f:
130+
return cls.from_h5obj(f)
131+
124132
return cls.from_string(fileobj.read())
125133

126134
@classmethod
@@ -145,6 +153,27 @@ def from_matlab_dict(cls, mdict, index=0):
145153
sa["offset"] = mdict["fixed"].flatten()
146154
return tf
147155

156+
@classmethod
157+
def from_h5obj(cls, fileobj, check=True):
158+
"""Read the struct from a file object."""
159+
160+
_xfm = ITKCompositeH5.from_h5obj(
161+
fileobj,
162+
check=check,
163+
only_linear=True,
164+
)
165+
166+
if not _xfm:
167+
raise TransformIOError(
168+
"Composite transform file does not contain at least one linear transform"
169+
)
170+
elif len(_xfm) > 1:
171+
raise TransformIOError(
172+
"Composite transform file contains more than one linear transform"
173+
)
174+
175+
return _xfm[0]
176+
148177
@classmethod
149178
def from_ras(cls, ras, index=0, moving=None, reference=None):
150179
"""Create an ITK affine from a nitransform's RAS+ matrix."""
@@ -225,6 +254,9 @@ def from_filename(cls, filename):
225254
if str(filename).endswith(".mat"):
226255
with open(str(filename), "rb") as f:
227256
return cls.from_binary(f)
257+
elif str(filename).endswith(".h5"):
258+
with H5File(str(filename)) as f:
259+
return cls.from_h5obj(f)
228260

229261
with open(str(filename)) as f:
230262
string = f.read()
@@ -235,6 +267,10 @@ def from_fileobj(cls, fileobj, check=True):
235267
"""Read the struct from a file object."""
236268
if fileobj.name.endswith(".mat"):
237269
return cls.from_binary(fileobj)
270+
271+
elif fileobj.name.endswith(".h5"):
272+
with H5File(fileobj) as f:
273+
return cls.from_h5obj(f)
238274
return cls.from_string(fileobj.read())
239275

240276
@classmethod
@@ -272,6 +308,18 @@ def from_string(cls, string):
272308
_self.xforms.append(cls._inner_type.from_string("#%s" % xfm))
273309
return _self
274310

311+
@classmethod
312+
def from_h5obj(cls, fileobj, check=True):
313+
"""Read the struct from a file object."""
314+
315+
_self = cls()
316+
_self.xforms = ITKCompositeH5.from_h5obj(
317+
fileobj,
318+
check=check,
319+
only_linear=True,
320+
)
321+
return _self
322+
275323

276324
class ITKDisplacementsField(DisplacementsField):
277325
"""A data structure representing displacements fields."""
@@ -302,18 +350,16 @@ class ITKCompositeH5:
302350
"""A data structure for ITK's HDF5 files."""
303351

304352
@classmethod
305-
def from_filename(cls, filename):
353+
def from_filename(cls, filename, only_linear=False):
306354
"""Read the struct from a file given its path."""
307-
from h5py import File as H5File
308-
309355
if not str(filename).endswith(".h5"):
310356
raise TransformFileError("Extension is not .h5")
311357

312358
with H5File(str(filename)) as f:
313-
return cls.from_h5obj(f)
359+
return cls.from_h5obj(f, only_linear=only_linear)
314360

315361
@classmethod
316-
def from_h5obj(cls, fileobj, check=True):
362+
def from_h5obj(cls, fileobj, check=True, only_linear=False):
317363
"""Read the struct from a file object."""
318364
xfm_list = []
319365
h5group = fileobj["TransformGroup"]
@@ -336,6 +382,8 @@ def from_h5obj(cls, fileobj, check=True):
336382
)
337383
continue
338384
if xfm["TransformType"][0].startswith(b"DisplacementFieldTransform"):
385+
if only_linear:
386+
continue
339387
_fixed = np.asanyarray(xfm[f"{typo_fallback}FixedParameters"])
340388
shape = _fixed[:3].astype("uint16").tolist()
341389
offset = _fixed[3:6].astype("float")
12.1 KB
Binary file not shown.

nitransforms/tests/test_io.py

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@
1515
from nibabel.affines import from_matvec
1616
from scipy.io import loadmat
1717
from nitransforms.linear import Affine
18-
from ..io import (
18+
from nitransforms.io import (
1919
afni,
2020
fsl,
2121
lta as fs,
2222
itk,
2323
)
24-
from ..io.lta import (
24+
from nitransforms.io.lta import (
2525
VolumeGeometry as VG,
2626
FSLinearTransform as LT,
2727
FSLinearTransformArray as LTA,
2828
)
29-
from ..io.base import LinearParameters, TransformIOError, TransformFileError
29+
from nitransforms.io.base import LinearParameters, TransformIOError, TransformFileError
30+
from nitransforms.conftest import _datadir, _testdir
3031

3132
LPS = np.diag([-1, -1, 1, 1])
3233
ITK_MAT = LPS.dot(np.ones((4, 4)).dot(LPS))
@@ -410,33 +411,35 @@ def test_afni_Displacements():
410411
afni.AFNIDisplacementsField.from_image(field)
411412

412413

413-
def test_itk_h5(tmpdir, testdata_path):
414+
@pytest.mark.parametrize("only_linear", [True, False])
415+
@pytest.mark.parametrize("h5_path,nxforms", [
416+
(_datadir / "affine-antsComposite.h5", 1),
417+
(_testdir / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5", 2),
418+
])
419+
def test_itk_h5(tmpdir, only_linear, h5_path, nxforms):
414420
"""Test displacements fields."""
415421
assert (
416422
len(
417423
list(
418424
itk.ITKCompositeH5.from_filename(
419-
testdata_path
420-
/ "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5"
425+
h5_path,
426+
only_linear=only_linear,
421427
)
422428
)
423429
)
424-
== 2
430+
== nxforms if not only_linear else 1
425431
)
426432

427433
with pytest.raises(TransformFileError):
428434
list(
429435
itk.ITKCompositeH5.from_filename(
430-
testdata_path
431-
/ "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.x5"
436+
h5_path.absolute().name.replace(".h5", ".x5"),
437+
only_linear=only_linear,
432438
)
433439
)
434440

435441
tmpdir.chdir()
436-
shutil.copy(
437-
testdata_path / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5",
438-
"test.h5",
439-
)
442+
shutil.copy(h5_path, "test.h5")
440443
os.chmod("test.h5", 0o666)
441444

442445
with H5File("test.h5", "r+") as h5file:
@@ -584,3 +587,70 @@ def _generate_reoriented(path, directions, swapaxes, parameters):
584587
hdr.set_qform(newaff, code=1)
585588
hdr.set_sform(newaff, code=1)
586589
return img.__class__(data, newaff, hdr), R
590+
591+
592+
def test_itk_linear_h5(tmpdir, data_path, testdata_path):
593+
"""Check different lower-level loading options."""
594+
595+
# File loadable with transform array
596+
h5xfm = itk.ITKLinearTransformArray.from_filename(
597+
data_path / "affine-antsComposite.h5"
598+
)
599+
assert len(h5xfm.xforms) == 1
600+
601+
with open(data_path / "affine-antsComposite.h5", "rb") as f:
602+
h5xfm = itk.ITKLinearTransformArray.from_fileobj(f)
603+
assert len(h5xfm.xforms) == 1
604+
605+
# File loadable with single affine object
606+
itk.ITKLinearTransform.from_filename(
607+
data_path / "affine-antsComposite.h5"
608+
)
609+
610+
with open(data_path / "affine-antsComposite.h5", "rb") as f:
611+
itk.ITKLinearTransform.from_fileobj(f)
612+
613+
# Exercise only_linear
614+
itk.ITKCompositeH5.from_filename(
615+
testdata_path / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5",
616+
only_linear=True,
617+
)
618+
619+
tmpdir.chdir()
620+
shutil.copy(data_path / "affine-antsComposite.h5", "test.h5")
621+
os.chmod("test.h5", 0o666)
622+
623+
with H5File("test.h5", "r+") as h5file:
624+
h5group = h5file["TransformGroup"]
625+
xfm = h5group.create_group("2")
626+
xfm["TransformType"] = (b"AffineTransform", b"")
627+
xfm["TransformParameters"] = np.zeros(12, dtype=float)
628+
xfm["TransformFixedParameters"] = np.zeros(3, dtype=float)
629+
630+
# File loadable with transform array
631+
h5xfm = itk.ITKLinearTransformArray.from_filename("test.h5")
632+
assert len(h5xfm.xforms) == 2
633+
634+
# File loadable with generalistic object (NOTE we directly access the list)
635+
h5xfm = itk.ITKCompositeH5.from_filename("test.h5")
636+
assert len(h5xfm) == 2
637+
638+
# Error raised if the we try to use the single affine loader
639+
with pytest.raises(TransformIOError):
640+
itk.ITKLinearTransform.from_filename("test.h5")
641+
642+
shutil.copy(data_path / "affine-antsComposite.h5", "test.h5")
643+
os.chmod("test.h5", 0o666)
644+
645+
# Generate an empty h5 file
646+
with H5File("test.h5", "r+") as h5file:
647+
h5group = h5file["TransformGroup"]
648+
del h5group["1"]
649+
650+
# File loadable with generalistic object
651+
h5xfm = itk.ITKCompositeH5.from_filename("test.h5")
652+
assert len(h5xfm) == 0
653+
654+
# Error raised if the we try to use the single affine loader
655+
with pytest.raises(TransformIOError):
656+
itk.ITKLinearTransform.from_filename("test.h5")

0 commit comments

Comments
 (0)