Skip to content

Commit e465332

Browse files
Julien Marabottooesteban
authored andcommitted
enh: x5 implementation
1 parent a102f1e commit e465332

File tree

6 files changed

+253
-52
lines changed

6 files changed

+253
-52
lines changed

nitransforms/base.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -248,17 +248,48 @@ def __ne__(self, other):
248248
class TransformBase:
249249
"""Abstract image class to represent transforms."""
250250

251-
__slots__ = (
252-
"_reference",
253-
"_ndim",
254-
)
255-
256-
def __init__(self, reference=None):
251+
__slots__ = ("_reference", "_ndim", "_affine", "_shape", "_header",
252+
"_grid", "_mapping", "_hdf5_dct", "_x5_dct")
253+
254+
x5_struct = {
255+
'TransformGroup/0': {
256+
'Type': None,
257+
'Transform': None,
258+
'Metadata': None,
259+
'Inverse': None
260+
},
261+
'TransformGroup/0/Domain': {
262+
'Grid': None,
263+
'Size': None,
264+
'Mapping': None
265+
},
266+
'TransformGroup/1': {},
267+
'TransformChain': {}
268+
}
269+
270+
def __init__(self, x5=None, hdf5=None, nifti=None, shape=None, affine=None,
271+
header=None, reference=None):
257272
"""Instantiate a transform."""
273+
258274
self._reference = None
259275
if reference:
260276
self.reference = reference
261277

278+
if nifti is not None:
279+
self._x5_dct = self.init_x5_structure(nifti)
280+
elif hdf5:
281+
self.update_x5_structure(hdf5)
282+
elif x5:
283+
self.update_x5_structure(x5)
284+
285+
self._shape = shape
286+
self._affine = affine
287+
self._header = header
288+
289+
# TO-DO
290+
self._grid = None
291+
self._mapping = None
292+
262293
def __call__(self, x, inverse=False):
263294
"""Apply y = f(x)."""
264295
return self.map(x, inverse=inverse)
@@ -295,6 +326,12 @@ def ndim(self):
295326
"""Access the dimensions of the reference space."""
296327
raise TypeError("TransformBase has no dimensions")
297328

329+
def init_x5_structure(self, xfm_data=None):
330+
self.x5_struct['TransformGroup/0/Transform'] = xfm_data
331+
332+
def update_x5_structure(self, hdf5_struct=None):
333+
self.x5_struct.update(hdf5_struct)
334+
298335
def map(self, x, inverse=False):
299336
r"""
300337
Apply :math:`y = f(x)`.
@@ -316,20 +353,6 @@ def map(self, x, inverse=False):
316353
"""
317354
return x
318355

319-
def to_filename(self, filename, fmt="X5"):
320-
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
321-
with h5py.File(filename, "w") as out_file:
322-
out_file.attrs["Format"] = "X5"
323-
out_file.attrs["Version"] = np.uint16(1)
324-
root = out_file.create_group("/0")
325-
self._to_hdf5(root)
326-
327-
return filename
328-
329-
def _to_hdf5(self, x5_root):
330-
"""Serialize this object into the x5 file format."""
331-
raise NotImplementedError
332-
333356
def apply(self, *args, **kwargs):
334357
"""Apply the transform to a dataset.
335358

nitransforms/cli.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import os
33
from textwrap import dedent
44

5+
from nitransforms.base import TransformBase
6+
from nitransforms.io.base import xfm_loader
7+
from nitransforms.linear import load as linload
8+
from nitransforms.nonlinear import load as nlinload
9+
from nitransforms.resampling import apply
510

6-
from .linear import load as linload
7-
from .nonlinear import load as nlinload
8-
from .resampling import apply
9-
11+
import pprint
1012

1113
def cli_apply(pargs):
1214
"""
@@ -47,8 +49,26 @@ def cli_apply(pargs):
4749
cval=pargs.cval,
4850
prefilter=pargs.prefilter,
4951
)
50-
moved.to_filename(pargs.out or f"nt_{os.path.basename(pargs.moving)}")
52+
#moved.to_filename(pargs.out or f"nt_{os.path.basename(pargs.moving)}")
53+
54+
55+
def cli_xfm_util(pargs):
56+
"""
57+
"""
58+
59+
xfm_data = xfm_loader(pargs.transform)
60+
xfm_x5 = TransformBase(**xfm_data)
61+
62+
if pargs.info:
63+
pprint.pprint(xfm_x5.x5_struct)
64+
print(f"Shape:\n{xfm_x5._shape}")
65+
print(f"Affine:\n{xfm_x5._affine}")
5166

67+
if pargs.x5:
68+
filename = f"{os.path.basename(pargs.transform).split('.')[0]}.x5"
69+
xfm_x5.to_filename(filename)
70+
print(f"Writing out {filename}")
71+
5272

5373
def get_parser():
5474
desc = dedent(
@@ -58,6 +78,7 @@ def get_parser():
5878
Commands:
5979
6080
apply Apply a transformation to an image
81+
xfm_util Assorted transform utilities
6182
6283
For command specific information, use 'nt <command> -h'.
6384
"""
@@ -122,6 +143,17 @@ def _add_subparser(name, description):
122143
help="Determines if the image's data array is prefiltered with a spline filter before "
123144
"interpolation (default: True)",
124145
)
146+
147+
xfm_util = _add_subparser("xfm_util", cli_xfm_util.__doc__)
148+
xfm_util.set_defaults(func=cli_xfm_util)
149+
xfm_util.add_argument("transform", help="The transform file")
150+
xfm_util.add_argument("--info",
151+
action="store_true",
152+
help="Get information about the transform")
153+
xfm_util.add_argument("--x5",
154+
action="store_true",
155+
help="Convert transform to .x5 file format.")
156+
125157
return parser, subparsers
126158

127159

@@ -135,3 +167,7 @@ def main(pargs=None):
135167
subparser = subparsers.choices[pargs.command]
136168
subparser.print_help()
137169
raise (e)
170+
171+
172+
if __name__ == "__main__":
173+
main()

nitransforms/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"fs": (lta, "FSLinearTransform"),
2323
"fsl": (fsl, "FSLLinearTransform"),
2424
"afni": (afni, "AFNILinearTransform"),
25-
"x5": (x5, "X5LinearTransform")
25+
"x5": (x5, "X5Transform")
2626
}
2727

2828

nitransforms/io/base.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,137 @@
11
"""Read/write linear transforms."""
22
from pathlib import Path
33
import numpy as np
4+
import nibabel as nb
45
from nibabel import load as loadimg
56

7+
import h5py
8+
69
from ..patched import LabeledWrapStruct
710

811

12+
def get_xfm_filetype(xfm_file):
13+
path = Path(xfm_file)
14+
ext = path.suffix
15+
if ext == '.gz' and path.name.endswith('.nii.gz'):
16+
return 'nifti'
17+
18+
file_types = {
19+
'.nii': 'nifti',
20+
'.h5': 'hdf5',
21+
'.x5': 'x5',
22+
'.txt': 'txt',
23+
'.mat': 'txt'
24+
}
25+
return file_types.get(ext, 'unknown')
26+
27+
def gather_fields(x5=None, hdf5=None, nifti=None, shape=None, affine=None, header=None):
28+
xfm_fields = {
29+
"x5": x5,
30+
"hdf5": hdf5,
31+
"nifti": nifti,
32+
"header": header,
33+
"shape": shape,
34+
"affine": affine
35+
}
36+
return xfm_fields
37+
38+
def load_nifti(nifti_file):
39+
nifti_xfm = nb.load(nifti_file)
40+
xfm_data = nifti_xfm.get_fdata()
41+
shape = nifti_xfm.shape
42+
affine = nifti_xfm.affine
43+
header = getattr(nifti_xfm, "header", None)
44+
return gather_fields(nifti=xfm_data, shape=shape, affine=affine, header=header)
45+
46+
def load_hdf5(hdf5_file):
47+
storage = {}
48+
49+
def get_hdf5_items(name, x5_root):
50+
if isinstance(x5_root, h5py.Dataset):
51+
storage[name] = {
52+
'type': 'dataset',
53+
'attrs': dict(x5_root.attrs),
54+
'shape': x5_root.shape,
55+
'data': x5_root[()]
56+
}
57+
elif isinstance(x5_root, h5py.Group):
58+
storage[name] = {
59+
'type': 'group',
60+
'attrs': dict(x5_root.attrs),
61+
'members': {}
62+
}
63+
64+
with h5py.File(hdf5_file, 'r') as f:
65+
f.visititems(get_hdf5_items)
66+
if storage:
67+
hdf5_storage = {'hdf5': storage}
68+
return hdf5_storage
69+
70+
def load_x5(x5_file):
71+
load_hdf5(x5_file)
72+
73+
def load_mat(mat_file):
74+
affine_matrix = np.loadtxt(mat_file)
75+
affine = nb.affines.from_matvec(affine_matrix[:,:3], affine_matrix[:,3])
76+
return gather_fields(affine=affine)
77+
78+
def xfm_loader(xfm_file):
79+
loaders = {
80+
'nifti': load_nifti,
81+
'hdf5': load_hdf5,
82+
'x5': load_x5,
83+
'txt': load_mat,
84+
'mat': load_mat
85+
}
86+
xfm_filetype = get_xfm_filetype(xfm_file)
87+
loader = loaders.get(xfm_filetype)
88+
if loader is None:
89+
raise ValueError(f"Unsupported file type: {xfm_filetype}")
90+
return loader(xfm_file)
91+
92+
def to_filename(self, filename, fmt="X5"):
93+
"""Store the transform in BIDS-Transforms HDF5 file format (.x5)."""
94+
with h5py.File(filename, "w") as out_file:
95+
out_file.attrs["Format"] = "X5"
96+
out_file.attrs["Version"] = np.uint16(1)
97+
root = out_file.create_group("/0")
98+
self._to_hdf5(root)
99+
100+
return filename
101+
102+
def _to_hdf5(self, x5_root):
103+
"""Serialize this object into the x5 file format."""
104+
transform_group = x5_root.create_group("TransformGroup")
105+
106+
"""Group '0' containing Affine transform"""
107+
transform_0 = transform_group.create_group("0")
108+
109+
transform_0.attrs["Type"] = "Affine"
110+
transform_0.create_dataset("Transform", data=self._affine)
111+
transform_0.create_dataset("Inverse", data=np.linalg.inv(self._affine))
112+
113+
metadata = {"key": "value"}
114+
transform_0.attrs["Metadata"] = str(metadata)
115+
116+
"""sub-group 'Domain' contained within group '0' """
117+
domain_group = transform_0.create_group("Domain")
118+
#domain_group.attrs["Grid"] = self._grid
119+
#domain_group.create_dataset("Size", data=_as_homogeneous(self._reference.shape))
120+
#domain_group.create_dataset("Mapping", data=self.mapping)
121+
122+
def _from_x5(self, x5_root):
123+
variables = {}
124+
125+
x5_root.visititems(lambda name, x5_root: loader(name, x5_root, variables))
126+
127+
_transform = variables["TransformGroup/0/Transform"]
128+
_inverse = variables["TransformGroup/0/Inverse"]
129+
_size = variables["TransformGroup/0/Domain/Size"]
130+
_mapping = variables["TransformGroup/0/Domain/Mapping"]
131+
132+
return _transform, _inverse, _size, _map
133+
134+
9135
class TransformIOError(IOError):
10136
"""General I/O exception while reading/writing transforms."""
11137

nitransforms/io/x5.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,46 +13,43 @@
1313
TransformFileError,
1414
)
1515

16-
LPS = np.diag([-1, -1, 1, 1])
17-
18-
class X5LinearTransform(LinearParameters):
16+
class X5Transform:
1917
"""A string-based structure for X5 linear transforms."""
2018

21-
template_dtype = np.dtype(
22-
[
23-
("type", "i4"),
24-
("index", "i4"),
25-
("parameters", "f8", (4, 4)),
26-
("offset", "f4", 3), # Center of rotation
27-
]
28-
)
29-
dtype = template_dtype
19+
_transform = None
3020

3121
def __init__(self, parameters=None, offset=None):
3222
return
3323

3424
def __str__(self):
3525
return
3626

37-
def to_filename(self, filename):
38-
'''store this transform to a file with the X5 format'''
39-
sa = self.structarr
40-
affine = '''some affine that will return a 4x4 array'''
41-
return
42-
4327
@classmethod
4428
def from_filename(cls, filename):
4529
"""Read the struct from a X5 file given its path."""
4630
if str(filename).endswith(".h5"):
47-
with H5File(str(filename)) as f:
48-
return cls.from_h5obj(f)
31+
with H5File(str(filename), 'r') as hdf:
32+
return cls.from_h5obj(hdf)
33+
34+
@classmethod
35+
def from_h5obj(cls, h5obj):
36+
"""Read the transformations in an X5 file."""
37+
xfm_list = list(h5obj.keys())
38+
39+
xfm = xfm_list["Transform"]
40+
inv = xfm_list["Inverse"]
41+
coords = xfm_list["Size"]
42+
map = xfm_list["Mapping"]
43+
44+
return xfm, inv, coords, map
45+
4946

5047
class X5LinearTransformArray(BaseLinearTransformList):
5148
"""A string-based structure for series of X5 linear transforms."""
5249

53-
_inner_type = X5LinearTransform
50+
_inner_type = X5Transform
5451

5552
@property
5653
def xforms(self):
57-
"""Get the list of internal ITKLinearTransforms."""
54+
"""Get the list of internal X5LinearTransforms."""
5855
return self._xforms

0 commit comments

Comments
 (0)