Skip to content

Commit b25045e

Browse files
committed
enh: added test file / reduce code duplicity
1 parent a47bf1c commit b25045e

File tree

4 files changed

+115
-37
lines changed

4 files changed

+115
-37
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: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,23 @@ def from_matlab_dict(cls, mdict, index=0):
156156
@classmethod
157157
def from_h5obj(cls, fileobj, check=True):
158158
"""Read the struct from a file object."""
159-
raise NotImplementedError
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]
160176

161177
@classmethod
162178
def from_ras(cls, ras, index=0, moving=None, reference=None):
@@ -295,26 +311,13 @@ def from_string(cls, string):
295311
@classmethod
296312
def from_h5obj(cls, fileobj, check=True):
297313
"""Read the struct from a file object."""
298-
h5group = fileobj["TransformGroup"]
299-
typo_fallback = "Transform"
300-
try:
301-
h5group["1"][f"{typo_fallback}Parameters"]
302-
except KeyError:
303-
typo_fallback = "Tranform"
304314

305315
_self = cls()
306-
_self.xforms = []
307-
for xfm in list(h5group.values())[1:]:
308-
if xfm["TransformType"][0].startswith(b"AffineTransform"):
309-
_params = np.asanyarray(xfm[f"{typo_fallback}Parameters"])
310-
_self.xforms.append(
311-
ITKLinearTransform(
312-
parameters=from_matvec(
313-
_params[:-3].reshape(3, 3), _params[-3:]
314-
),
315-
offset=np.asanyarray(xfm[f"{typo_fallback}FixedParameters"]),
316-
)
317-
)
316+
_self.xforms = ITKCompositeH5.from_h5obj(
317+
fileobj,
318+
check=check,
319+
only_linear=True,
320+
)
318321
return _self
319322

320323

@@ -347,16 +350,16 @@ class ITKCompositeH5:
347350
"""A data structure for ITK's HDF5 files."""
348351

349352
@classmethod
350-
def from_filename(cls, filename):
353+
def from_filename(cls, filename, only_linear=False):
351354
"""Read the struct from a file given its path."""
352355
if not str(filename).endswith(".h5"):
353356
raise TransformFileError("Extension is not .h5")
354357

355358
with H5File(str(filename)) as f:
356-
return cls.from_h5obj(f)
359+
return cls.from_h5obj(f, only_linear=only_linear)
357360

358361
@classmethod
359-
def from_h5obj(cls, fileobj, check=True):
362+
def from_h5obj(cls, fileobj, check=True, only_linear=False):
360363
"""Read the struct from a file object."""
361364
xfm_list = []
362365
h5group = fileobj["TransformGroup"]
@@ -379,6 +382,8 @@ def from_h5obj(cls, fileobj, check=True):
379382
)
380383
continue
381384
if xfm["TransformType"][0].startswith(b"DisplacementFieldTransform"):
385+
if only_linear:
386+
continue
382387
_fixed = np.asanyarray(xfm[f"{typo_fallback}FixedParameters"])
383388
shape = _fixed[:3].astype("uint16").tolist()
384389
offset = _fixed[3:6].astype("float")
12.1 KB
Binary file not shown.

nitransforms/tests/test_io.py

Lines changed: 85 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,72 @@ 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+
h5xfm = itk.ITKLinearTransformArray.from_fileobj(
602+
(data_path / "affine-antsComposite.h5").open()
603+
)
604+
assert len(h5xfm.xforms) == 1
605+
606+
# File loadable with single affine object
607+
itk.ITKLinearTransform.from_filename(
608+
data_path / "affine-antsComposite.h5"
609+
)
610+
611+
itk.ITKLinearTransform.from_fileobj(
612+
(data_path / "affine-antsComposite.h5").open()
613+
)
614+
615+
# Exercise only_linear
616+
itk.ITKCompositeH5.from_filename(
617+
testdata_path / "ds-005_sub-01_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5",
618+
only_linear=True,
619+
)
620+
621+
tmpdir.chdir()
622+
shutil.copy(data_path / "affine-antsComposite.h5", "test.h5")
623+
os.chmod("test.h5", 0o666)
624+
625+
with H5File("test.h5", "r+") as h5file:
626+
h5group = h5file["TransformGroup"]
627+
xfm = h5group.create_group("2")
628+
xfm["TransformType"] = (b"AffineTransform", b"")
629+
xfm["TransformParameters"] = np.zeros(12, dtype=float)
630+
xfm["TransformFixedParameters"] = np.zeros(3, dtype=float)
631+
632+
# File loadable with transform array
633+
h5xfm = itk.ITKLinearTransformArray.from_filename("test.h5")
634+
assert len(h5xfm.xforms) == 2
635+
636+
# File loadable with generalistic object (NOTE we directly access the list)
637+
h5xfm = itk.ITKCompositeH5.from_filename("test.h5")
638+
assert len(h5xfm) == 2
639+
640+
# Error raised if the we try to use the single affine loader
641+
with pytest.raises(TransformIOError):
642+
itk.ITKLinearTransform.from_filename("test.h5")
643+
644+
shutil.copy(data_path / "affine-antsComposite.h5", "test.h5")
645+
os.chmod("test.h5", 0o666)
646+
647+
# Generate an empty h5 file
648+
with H5File("test.h5", "r+") as h5file:
649+
h5group = h5file["TransformGroup"]
650+
del h5group["1"]
651+
652+
# File loadable with generalistic object
653+
h5xfm = itk.ITKCompositeH5.from_filename("test.h5")
654+
assert len(h5xfm) == 0
655+
656+
# Error raised if the we try to use the single affine loader
657+
with pytest.raises(TransformIOError):
658+
itk.ITKLinearTransform.from_filename("test.h5")

0 commit comments

Comments
 (0)