Skip to content

Commit 7ee6339

Browse files
committed
[format] initial port of jnifti and hdf5 format parsers from jnifty and jsnirfy
1 parent a07f2e4 commit 7ee6339

4 files changed

Lines changed: 1660 additions & 2 deletions

File tree

jdata/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@
5151
)
5252
from .jdata import encode, decode, jdtype, jsonfilter
5353
from .jpath import jsonpath
54+
from .jnifti import (
55+
nii2jnii,
56+
jnii2nii,
57+
loadnifti,
58+
loadjnifti,
59+
savenifti,
60+
savejnifti,
61+
nifticreate,
62+
jnifticreate,
63+
memmapstream,
64+
niiheader2jnii,
65+
niicodemap,
66+
niiformat,
67+
)
68+
from .h5 import loadh5, saveh5
5469

5570
__version__ = "0.6.0"
5671
__all__ = [
@@ -73,6 +88,20 @@
7388
"jsonfilter",
7489
"jext",
7590
"jsonpath",
91+
"nii2jnii",
92+
"jnii2nii",
93+
"loadnifti",
94+
"loadjnifti",
95+
"savenifti",
96+
"savejnifti",
97+
"nifticreate",
98+
"jnifticreate",
99+
"memmapstream",
100+
"niiheader2jnii",
101+
"niicodemap",
102+
"niiformat",
103+
"loadh5",
104+
"saveh5",
76105
]
77106
__license__ = """Apache license 2.0, Copyright (c) 2019-2024 Qianqian Fang"""
78107

jdata/h5.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""@package docstring
2+
File IO to load/decode HDF5 or SNIRF/JSNIRF files
3+
4+
Copyright (c) 2019-2025 Qianqian Fang <q.fang at neu.edu>
5+
"""
6+
7+
__all__ = [
8+
"loadh5",
9+
"saveh5",
10+
]
11+
12+
##====================================================================================
13+
## dependent libraries
14+
##====================================================================================
15+
16+
import numpy as np
17+
18+
19+
def loadh5(filename, *args, **kwargs):
20+
"""
21+
Load data in an HDF5 file into a Python dictionary.
22+
23+
Parameters:
24+
filename : str
25+
HDF5 file name
26+
args : optional
27+
May contain root path or option dictionary
28+
kwargs : optional
29+
Options such as:
30+
- Order ('creation' | 'alphabet')
31+
- Regroup (0 | 1)
32+
- PackHex (0 | 1)
33+
- ComplexFormat: [realKey, imagKey]
34+
- Transpose (0 | 1)
35+
36+
Returns:
37+
data : dict
38+
Dictionary containing datasets
39+
meta : dict
40+
Dictionary containing attributes
41+
"""
42+
import h5py
43+
44+
# Parse args
45+
path = ""
46+
opt = {
47+
"Transpose": 1,
48+
"StringArray": 0,
49+
"PackHex": 1,
50+
"ComplexFormat": ["Real", "Imag"],
51+
"Regroup": 0,
52+
"jdata": 0,
53+
}
54+
55+
if len(args) == 1 and isinstance(args[0], str):
56+
path = args[0]
57+
elif len(args) >= 2:
58+
path = args[0]
59+
opt.update(args[1])
60+
elif len(args) % 2 == 0:
61+
for k, v in zip(args[::2], args[1::2]):
62+
opt[k] = v
63+
64+
opt.update(kwargs)
65+
66+
def read_attrs(obj):
67+
return {k: obj.attrs[k] for k in obj.attrs}
68+
69+
def fix_data(data, attrs):
70+
if isinstance(data, np.ndarray):
71+
if opt["Transpose"] and data.ndim > 1:
72+
data = data.transpose()
73+
if isinstance(data, bytes):
74+
data = data.decode("utf-8")
75+
if isinstance(data, np.ndarray) and data.dtype == np.object:
76+
try:
77+
data = np.array(
78+
[d.decode("utf-8") if isinstance(d, bytes) else d for d in data]
79+
)
80+
except Exception:
81+
pass
82+
if isinstance(data, dict):
83+
fields = data.keys()
84+
ck = opt["ComplexFormat"]
85+
if ck[0] in fields and ck[1] in fields:
86+
data = np.array(data[ck[0]]) + 1j * np.array(data[ck[1]])
87+
return data
88+
89+
def visit_group(g, prefix=""):
90+
d = {}
91+
m = {}
92+
for k in g:
93+
item = g[k]
94+
name = k
95+
if isinstance(item, h5py.Group):
96+
sub_d, sub_m = visit_group(item, prefix + "/" + k)
97+
d[name] = sub_d
98+
m[name] = sub_m
99+
elif isinstance(item, h5py.Dataset):
100+
try:
101+
raw = item[()]
102+
attr = read_attrs(item)
103+
raw = fix_data(raw, attr)
104+
d[name] = raw
105+
m[name] = attr
106+
except Exception as e:
107+
d[name] = None
108+
m[name] = {"error": str(e)}
109+
return d, m
110+
111+
with h5py.File(filename, "r") as f:
112+
if path and path in f:
113+
root = f[path]
114+
else:
115+
root = f
116+
data, meta = visit_group(root)
117+
118+
return data, meta
119+
120+
121+
def saveh5(data, fname, **kwargs):
122+
"""
123+
Save a Python dictionary or object into an HDF5 file.
124+
125+
Parameters:
126+
data : dict, list, or array-like
127+
Data to be saved.
128+
fname : str
129+
Output HDF5 filename.
130+
kwargs : optional arguments for customization
131+
Supported keys:
132+
- rootname (str)
133+
- compression ('gzip' or None)
134+
- compresslevel (int)
135+
- transpose (bool)
136+
- complex_format (tuple of str)
137+
"""
138+
import h5py
139+
140+
rootname = kwargs.get("rootname", "data")
141+
compression = kwargs.get("compression", None)
142+
compresslevel = kwargs.get("compresslevel", 4)
143+
transpose = kwargs.get("transpose", True)
144+
complex_format = kwargs.get("complex_format", ("Real", "Imag"))
145+
146+
def write_data(h5file, path, value):
147+
if isinstance(value, dict):
148+
grp = h5file.require_group(path)
149+
for k, v in value.items():
150+
write_data(h5file, f"{path}/{k}", v)
151+
elif isinstance(value, (list, tuple)) and all(
152+
isinstance(i, dict) for i in value
153+
):
154+
for i, v in enumerate(value):
155+
write_data(h5file, f"{path}/{i}", v)
156+
elif isinstance(value, complex):
157+
grp = h5file.require_group(path)
158+
grp.create_dataset(complex_format[0], data=np.real(value))
159+
grp.create_dataset(complex_format[1], data=np.imag(value))
160+
elif isinstance(value, np.ndarray) and np.iscomplexobj(value):
161+
grp = h5file.require_group(path)
162+
grp.create_dataset(
163+
complex_format[0],
164+
data=np.real(value),
165+
compression=compression,
166+
compression_opts=compresslevel,
167+
)
168+
grp.create_dataset(
169+
complex_format[1],
170+
data=np.imag(value),
171+
compression=compression,
172+
compression_opts=compresslevel,
173+
)
174+
elif isinstance(value, (np.ndarray, list, tuple)):
175+
arr = np.array(value)
176+
if transpose and arr.ndim > 1:
177+
arr = arr.T
178+
h5file.create_dataset(
179+
path, data=arr, compression=compression, compression_opts=compresslevel
180+
)
181+
else:
182+
h5file.create_dataset(path, data=value)
183+
184+
with h5py.File(fname, "w") as f:
185+
write_data(f, rootname, data)

0 commit comments

Comments
 (0)