medh5 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- medh5-0.5.0/LICENSE +21 -0
- medh5-0.5.0/PKG-INFO +619 -0
- medh5-0.5.0/README.md +552 -0
- medh5-0.5.0/medh5/__init__.py +29 -0
- medh5-0.5.0/medh5/chunks.py +182 -0
- medh5-0.5.0/medh5/cli/__init__.py +81 -0
- medh5-0.5.0/medh5/cli/__main__.py +10 -0
- medh5-0.5.0/medh5/cli/_common.py +16 -0
- medh5-0.5.0/medh5/cli/convert.py +219 -0
- medh5-0.5.0/medh5/cli/dataset.py +132 -0
- medh5-0.5.0/medh5/cli/inspect.py +301 -0
- medh5-0.5.0/medh5/cli/review.py +130 -0
- medh5-0.5.0/medh5/core.py +1142 -0
- medh5-0.5.0/medh5/dataset/__init__.py +14 -0
- medh5-0.5.0/medh5/dataset/index.py +192 -0
- medh5-0.5.0/medh5/dataset/split.py +233 -0
- medh5-0.5.0/medh5/exceptions.py +19 -0
- medh5-0.5.0/medh5/integrity.py +118 -0
- medh5-0.5.0/medh5/io/__init__.py +45 -0
- medh5-0.5.0/medh5/io/dicom.py +366 -0
- medh5-0.5.0/medh5/io/nifti.py +473 -0
- medh5-0.5.0/medh5/io/nnunetv2.py +707 -0
- medh5-0.5.0/medh5/meta.py +295 -0
- medh5-0.5.0/medh5/py.typed +0 -0
- medh5-0.5.0/medh5/review.py +135 -0
- medh5-0.5.0/medh5/sampling.py +227 -0
- medh5-0.5.0/medh5/stats.py +316 -0
- medh5-0.5.0/medh5/torch.py +249 -0
- medh5-0.5.0/medh5/transforms.py +202 -0
- medh5-0.5.0/medh5.egg-info/PKG-INFO +619 -0
- medh5-0.5.0/medh5.egg-info/SOURCES.txt +49 -0
- medh5-0.5.0/medh5.egg-info/dependency_links.txt +1 -0
- medh5-0.5.0/medh5.egg-info/entry_points.txt +2 -0
- medh5-0.5.0/medh5.egg-info/requires.txt +23 -0
- medh5-0.5.0/medh5.egg-info/top_level.txt +1 -0
- medh5-0.5.0/pyproject.toml +113 -0
- medh5-0.5.0/setup.cfg +4 -0
- medh5-0.5.0/tests/test_chunks.py +76 -0
- medh5-0.5.0/tests/test_cli.py +436 -0
- medh5-0.5.0/tests/test_dataset.py +114 -0
- medh5-0.5.0/tests/test_integrity.py +103 -0
- medh5-0.5.0/tests/test_io.py +919 -0
- medh5-0.5.0/tests/test_partial.py +139 -0
- medh5-0.5.0/tests/test_review.py +74 -0
- medh5-0.5.0/tests/test_roundtrip.py +396 -0
- medh5-0.5.0/tests/test_sampling.py +156 -0
- medh5-0.5.0/tests/test_split.py +104 -0
- medh5-0.5.0/tests/test_stats.py +114 -0
- medh5-0.5.0/tests/test_torch.py +247 -0
- medh5-0.5.0/tests/test_transforms.py +172 -0
- medh5-0.5.0/tests/test_update.py +165 -0
medh5-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Puyang Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
medh5-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: medh5
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: HDF5 + Blosc2 single-file format for medical-imaging ML workloads (multi-modality images + segmentation + bboxes + label).
|
|
5
|
+
Author-email: Puyang Wang <pauliwang411@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Puyang Wang
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/XwK-P/medh5
|
|
29
|
+
Project-URL: Repository, https://github.com/XwK-P/medh5
|
|
30
|
+
Project-URL: Issues, https://github.com/XwK-P/medh5/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/XwK-P/medh5/blob/main/CHANGELOG.md
|
|
32
|
+
Keywords: medical imaging,hdf5,blosc2,machine learning,segmentation,nifti,dicom,pytorch
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering
|
|
43
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE
|
|
48
|
+
Requires-Dist: h5py>=3.10
|
|
49
|
+
Requires-Dist: hdf5plugin>=4.1
|
|
50
|
+
Requires-Dist: numpy>=1.24
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
54
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
55
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
56
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
57
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
58
|
+
Provides-Extra: torch
|
|
59
|
+
Requires-Dist: torch>=2.0; extra == "torch"
|
|
60
|
+
Provides-Extra: nifti
|
|
61
|
+
Requires-Dist: nibabel>=5; extra == "nifti"
|
|
62
|
+
Provides-Extra: dicom
|
|
63
|
+
Requires-Dist: pydicom>=2.4; extra == "dicom"
|
|
64
|
+
Provides-Extra: itk
|
|
65
|
+
Requires-Dist: SimpleITK>=2.3; extra == "itk"
|
|
66
|
+
Dynamic: license-file
|
|
67
|
+
|
|
68
|
+
# medh5
|
|
69
|
+
|
|
70
|
+
**HDF5 + Blosc2 multi-array format for ML workloads.**
|
|
71
|
+
|
|
72
|
+
> **Status:** Beta (0.5.0) — API may still change between minor versions.
|
|
73
|
+
> Backward compatibility is not guaranteed until 1.0.
|
|
74
|
+
|
|
75
|
+
Store multiple co-registered images (e.g. CT, MRI, PET) + segmentation
|
|
76
|
+
masks + bounding boxes + image-level label in a single `.medh5` file with
|
|
77
|
+
Blosc2 compression, chunk-size optimization for patch-based training, and
|
|
78
|
+
metadata as plain HDF5 attributes.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
Full docs live under [`docs/`](docs/index.md):
|
|
83
|
+
|
|
84
|
+
- [Getting started](docs/getting-started.md)
|
|
85
|
+
- [File format](docs/file-format.md)
|
|
86
|
+
- [Python API](docs/python-api.md)
|
|
87
|
+
- [CLI reference](docs/cli.md)
|
|
88
|
+
- [PyTorch integration](docs/pytorch.md)
|
|
89
|
+
- [Converters (NIfTI / DICOM / nnU-Net v2)](docs/converters.md)
|
|
90
|
+
- [Datasets and statistics](docs/dataset-and-stats.md)
|
|
91
|
+
- [Review / QA workflow](docs/review.md)
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install medh5
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With optional extras:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install medh5[torch] # PyTorch dataset support
|
|
103
|
+
pip install medh5[nifti] # NIfTI import/export (nibabel)
|
|
104
|
+
pip install medh5[dicom] # DICOM import (pydicom)
|
|
105
|
+
pip install medh5[itk] # Resampling via SimpleITK
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or install from source:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e ".[dev]"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Quick start
|
|
115
|
+
|
|
116
|
+
### Write a sample
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import numpy as np
|
|
120
|
+
from medh5 import MEDH5File
|
|
121
|
+
|
|
122
|
+
ct = np.random.random((128, 256, 256)).astype(np.float32)
|
|
123
|
+
pet = np.random.random((128, 256, 256)).astype(np.float32)
|
|
124
|
+
seg = {
|
|
125
|
+
"tumor": np.random.random(ct.shape) > 0.9,
|
|
126
|
+
"liver": np.random.random(ct.shape) > 0.5,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bboxes = np.array([
|
|
130
|
+
[[10, 30], [40, 80], [50, 90]], # box 1: [z_min,z_max], [y_min,y_max], [x_min,x_max]
|
|
131
|
+
[[60, 90], [20, 50], [100, 130]], # box 2
|
|
132
|
+
])
|
|
133
|
+
bbox_scores = np.array([0.97, 0.82])
|
|
134
|
+
bbox_labels = ["tumor", "cyst"]
|
|
135
|
+
|
|
136
|
+
MEDH5File.write(
|
|
137
|
+
"sample.medh5",
|
|
138
|
+
images={"CT": ct, "PET": pet},
|
|
139
|
+
seg=seg,
|
|
140
|
+
bboxes=bboxes,
|
|
141
|
+
bbox_scores=bbox_scores,
|
|
142
|
+
bbox_labels=bbox_labels,
|
|
143
|
+
label=1,
|
|
144
|
+
label_name="malignant",
|
|
145
|
+
spacing=[1.0, 0.5, 0.5],
|
|
146
|
+
origin=[0.0, 0.0, 0.0],
|
|
147
|
+
direction=[[1,0,0],[0,1,0],[0,0,1]],
|
|
148
|
+
coord_system="RAS",
|
|
149
|
+
patch_size=192,
|
|
150
|
+
extra={"patient_id": "P001"},
|
|
151
|
+
compression="balanced", # or "fast", "max"
|
|
152
|
+
checksum=True,
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Read a sample (eager)
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from medh5 import MEDH5File
|
|
160
|
+
|
|
161
|
+
sample = MEDH5File.read("sample.medh5")
|
|
162
|
+
|
|
163
|
+
print(sample.images.keys()) # dict_keys(['CT', 'PET'])
|
|
164
|
+
print(sample.images["CT"].shape) # (128, 256, 256)
|
|
165
|
+
print(list(sample.seg.keys())) # ['liver', 'tumor']
|
|
166
|
+
print(sample.bboxes.shape) # (2, 3, 2)
|
|
167
|
+
print(sample.meta.label) # 1
|
|
168
|
+
print(sample.meta.image_names) # ['CT', 'PET']
|
|
169
|
+
print(sample.meta.spatial.spacing) # [1.0, 0.5, 0.5]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Partial / patch read (lazy)
|
|
173
|
+
|
|
174
|
+
For large datasets where loading the whole volume is impractical:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from medh5 import MEDH5File
|
|
178
|
+
|
|
179
|
+
with MEDH5File.open("sample.medh5") as f:
|
|
180
|
+
patch = f["images/CT"][10:42, 50:114, 50:114]
|
|
181
|
+
tumor_patch = f["seg/tumor"][10:42, 50:114, 50:114]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Or use the context-manager for typed access:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from medh5 import MEDH5File
|
|
188
|
+
|
|
189
|
+
with MEDH5File("sample.medh5") as f:
|
|
190
|
+
meta = f.meta
|
|
191
|
+
patch = f.images["CT"][10:42, 50:114, 50:114]
|
|
192
|
+
if f.seg is not None:
|
|
193
|
+
seg_patch = f.seg["tumor"][10:42, 50:114, 50:114]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Metadata-only read
|
|
197
|
+
|
|
198
|
+
Inspect metadata without touching the array data:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from medh5 import MEDH5File
|
|
202
|
+
|
|
203
|
+
meta = MEDH5File.read_meta("sample.medh5")
|
|
204
|
+
print(meta.label) # 1
|
|
205
|
+
print(meta.image_names) # ['CT', 'PET']
|
|
206
|
+
print(meta.spatial.spacing) # [1.0, 0.5, 0.5]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### In-place updates
|
|
210
|
+
|
|
211
|
+
Update metadata or add segmentation masks without rewriting image data:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from medh5 import MEDH5File
|
|
215
|
+
|
|
216
|
+
# Simple convenience methods
|
|
217
|
+
MEDH5File.update_meta("sample.medh5", label=2, extra={"reviewed": True})
|
|
218
|
+
MEDH5File.add_seg("sample.medh5", "new_mask", mask_array)
|
|
219
|
+
|
|
220
|
+
# Unified update API — metadata, seg, and bbox in one call
|
|
221
|
+
MEDH5File.update(
|
|
222
|
+
"sample.medh5",
|
|
223
|
+
meta={"label": 3, "spacing": [1.0, 0.5, 0.5], "coord_system": "RAS"},
|
|
224
|
+
seg_ops={"add": {"organ": organ_mask}, "remove": ["old_mask"]},
|
|
225
|
+
bbox_ops={"bboxes": new_bboxes, "bbox_labels": ["tumor"]},
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Validate file structure
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from medh5 import MEDH5File
|
|
233
|
+
|
|
234
|
+
report = MEDH5File.validate("sample.medh5")
|
|
235
|
+
print(report.is_valid) # True if no errors
|
|
236
|
+
print(report.errors) # list of ValidationIssue(code=..., message=...)
|
|
237
|
+
print(report.warnings) # e.g. missing checksum
|
|
238
|
+
print(report.ok(strict=True)) # False if any errors OR warnings
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Verify file integrity
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from medh5 import MEDH5File
|
|
245
|
+
|
|
246
|
+
assert MEDH5File.verify("sample.medh5") # True if checksum matches
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### PyTorch integration
|
|
250
|
+
|
|
251
|
+
**Eager dataset** (full-volume read per sample):
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from medh5.torch import MEDH5TorchDataset
|
|
255
|
+
|
|
256
|
+
dataset = MEDH5TorchDataset(["s1.medh5", "s2.medh5", "s3.medh5"])
|
|
257
|
+
sample = dataset[0]
|
|
258
|
+
print(sample["images"]["CT"].shape) # torch.Size([128, 256, 256])
|
|
259
|
+
print(sample["label"]) # 1
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Patch-based dataset** (lazy chunk-aligned reads):
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from medh5.sampling import PatchSampler
|
|
266
|
+
from medh5.transforms import Compose, Clip, Normalize, RandomFlip
|
|
267
|
+
from medh5.torch import MEDH5PatchDataset
|
|
268
|
+
|
|
269
|
+
sampler = PatchSampler(
|
|
270
|
+
patch_size=(96, 96, 96),
|
|
271
|
+
strategy="foreground", # "uniform" | "foreground" | "balanced"
|
|
272
|
+
foreground_seg="tumor",
|
|
273
|
+
foreground_prob=0.7,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
transform = Compose([
|
|
277
|
+
Clip(min=-1000, max=1000),
|
|
278
|
+
Normalize(mean={"CT": -200.0}, std={"CT": 350.0}),
|
|
279
|
+
RandomFlip(axes=(1, 2), p=0.5),
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
dataset = MEDH5PatchDataset(
|
|
283
|
+
paths=["s1.medh5", "s2.medh5"],
|
|
284
|
+
sampler=sampler,
|
|
285
|
+
transform=transform,
|
|
286
|
+
samples_per_volume=4,
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Both dataset classes use a per-worker LRU file-handle cache, so repeated
|
|
291
|
+
reads against the same file reuse one `h5py.File` handle instead of
|
|
292
|
+
re-opening from scratch every call.
|
|
293
|
+
|
|
294
|
+
**`DataLoader` with `num_workers > 0`**
|
|
295
|
+
|
|
296
|
+
h5py is not fork-safe, and open `h5py.File` handles cannot be pickled for
|
|
297
|
+
`multiprocessing_context="spawn"` (the default on macOS / Windows /
|
|
298
|
+
Python 3.14+). medh5 handles this in two ways: the handle cache
|
|
299
|
+
detects PID changes on access and transparently resets itself, and
|
|
300
|
+
`medh5.torch.worker_init_fn` clears the cache at worker startup for
|
|
301
|
+
belt-and-braces safety. Always pass `worker_init_fn=medh5.torch.worker_init_fn`
|
|
302
|
+
when you use `num_workers > 0`:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from torch.utils.data import DataLoader
|
|
306
|
+
import medh5.torch as mt
|
|
307
|
+
|
|
308
|
+
dataset = mt.MEDH5PatchDataset(paths, sampler=sampler)
|
|
309
|
+
|
|
310
|
+
loader = DataLoader(
|
|
311
|
+
dataset,
|
|
312
|
+
batch_size=4,
|
|
313
|
+
num_workers=4,
|
|
314
|
+
worker_init_fn=mt.worker_init_fn, # <- required with num_workers > 0
|
|
315
|
+
# multiprocessing_context="spawn", # optional, default on macOS/Windows
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
for batch in loader:
|
|
319
|
+
...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Without `worker_init_fn`, a forked worker would observe stale h5py state
|
|
323
|
+
inherited from the parent process, which can deadlock or silently corrupt
|
|
324
|
+
reads. See [h5py's documentation](https://docs.h5py.org/en/stable/faq.html#multiprocessing)
|
|
325
|
+
for the underlying issue.
|
|
326
|
+
|
|
327
|
+
### NIfTI / DICOM conversion
|
|
328
|
+
|
|
329
|
+
Import NIfTI volumes into `.medh5`:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from medh5.io import from_nifti, to_nifti
|
|
333
|
+
|
|
334
|
+
from_nifti(
|
|
335
|
+
images={"CT": "ct.nii.gz", "PET": "pet.nii.gz"},
|
|
336
|
+
seg={"tumor": "tumor.nii.gz"},
|
|
337
|
+
out_path="sample.medh5",
|
|
338
|
+
label=1,
|
|
339
|
+
compression="balanced",
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Resample multi-resolution modalities onto a shared grid (requires `medh5[itk]`):
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from_nifti(
|
|
347
|
+
images={"CT": "ct_1mm.nii.gz", "PET": "pet_2mm.nii.gz"},
|
|
348
|
+
seg={"tumor": "tumor_2mm.nii.gz"},
|
|
349
|
+
out_path="sample.medh5",
|
|
350
|
+
resample_to="CT", # use CT grid as reference
|
|
351
|
+
interpolator="linear", # masks always use nearest-neighbor
|
|
352
|
+
)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Import a segmentation mask into an existing file:
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from medh5.io import import_seg_nifti
|
|
359
|
+
|
|
360
|
+
import_seg_nifti("sample.medh5", "edited_tumor.nii.gz", name="tumor", resample=True, replace=True)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Export back to NIfTI for editing in 3D Slicer / ITK-SNAP:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
to_nifti("sample.medh5", out_dir="export/")
|
|
367
|
+
# Writes export/image_CT.nii.gz, export/image_PET.nii.gz, export/seg_tumor.nii.gz
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Import a DICOM series:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
from medh5.io import from_dicom
|
|
374
|
+
|
|
375
|
+
from_dicom(
|
|
376
|
+
dicom_dir="path/to/series",
|
|
377
|
+
out_path="sample.medh5",
|
|
378
|
+
modality_name="CT",
|
|
379
|
+
series_uid="1.2.3.4.5", # optional: select specific series
|
|
380
|
+
apply_modality_lut=True, # apply RescaleSlope/Intercept (default)
|
|
381
|
+
extra_tags=["PatientID", "StudyDate"],
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### nnU-Net v2 dataset conversion
|
|
386
|
+
|
|
387
|
+
Convert a raw [nnU-Net v2](https://github.com/MIC-DKFZ/nnUNet) dataset folder
|
|
388
|
+
(`imagesTr/`, `labelsTr/`, optional `imagesTs/`, `dataset.json`) into a
|
|
389
|
+
directory of per-case `.medh5` files and back. Each case becomes one
|
|
390
|
+
`.medh5` bundling every channel plus one boolean mask per foreground class
|
|
391
|
+
declared in `dataset.json`. The parsed `dataset.json` payload is stashed in
|
|
392
|
+
`extra["nnunetv2"]` so export reconstructs the exact source layout —
|
|
393
|
+
including channel order and integer label values. Requires `medh5[nifti]`.
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from medh5.io import from_nnunetv2, to_nnunetv2
|
|
397
|
+
|
|
398
|
+
# Raw nnU-Net v2 → directory of .medh5 files
|
|
399
|
+
from_nnunetv2(
|
|
400
|
+
"Dataset042_BraTS/",
|
|
401
|
+
"medh5_out/",
|
|
402
|
+
include_test=True, # also convert imagesTs/ (seg=None)
|
|
403
|
+
compression="balanced",
|
|
404
|
+
checksum=True,
|
|
405
|
+
)
|
|
406
|
+
# Writes medh5_out/imagesTr/{case}.medh5 (+ medh5_out/imagesTs/{case}.medh5)
|
|
407
|
+
|
|
408
|
+
# Directory of .medh5 files → raw nnU-Net v2 layout
|
|
409
|
+
to_nnunetv2("medh5_out/", "Dataset042_BraTS_roundtrip/")
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The converters reject silent-data-loss conditions up front:
|
|
413
|
+
|
|
414
|
+
- **Import**: label volumes containing integer values that aren't declared
|
|
415
|
+
in `dataset.json`'s `labels` map raise `MEDH5ValidationError` instead of
|
|
416
|
+
silently dropping those voxels.
|
|
417
|
+
- **Export**: `.medh5` files whose seg-mask names or image channels do not
|
|
418
|
+
match the nnU-Net metadata stored in `extra["nnunetv2"]` are rejected
|
|
419
|
+
rather than silently omitted from the emitted dataset.
|
|
420
|
+
- Region-based labels (list-valued `labels`) are rejected with a clear
|
|
421
|
+
error — convert to integer labels first.
|
|
422
|
+
|
|
423
|
+
### Dataset operations
|
|
424
|
+
|
|
425
|
+
Build a manifest, filter, and split:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from medh5.dataset import Dataset, make_splits
|
|
429
|
+
|
|
430
|
+
ds = Dataset.from_directory("data/", recursive=True)
|
|
431
|
+
labeled = ds.filter(lambda r: r.label is not None)
|
|
432
|
+
ds.save("manifest.json")
|
|
433
|
+
|
|
434
|
+
splits = make_splits(
|
|
435
|
+
ds,
|
|
436
|
+
ratios={"train": 0.7, "val": 0.15, "test": 0.15},
|
|
437
|
+
stratify_by="label",
|
|
438
|
+
group_by="extra.patient_id",
|
|
439
|
+
seed=42,
|
|
440
|
+
)
|
|
441
|
+
splits["train"].save("train.json")
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Compute dataset-level statistics:
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
from medh5.stats import compute_stats
|
|
448
|
+
|
|
449
|
+
stats = compute_stats(ds, workers=4)
|
|
450
|
+
print(stats["CT"].mean, stats["CT"].std)
|
|
451
|
+
print(stats.label_counts)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Review / QA workflow
|
|
455
|
+
|
|
456
|
+
Track annotation review status without a GUI:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
from medh5 import MEDH5File
|
|
460
|
+
|
|
461
|
+
MEDH5File.set_review_status(
|
|
462
|
+
"sample.medh5",
|
|
463
|
+
status="reviewed", # pending | reviewed | flagged | rejected
|
|
464
|
+
annotator="puyang",
|
|
465
|
+
notes="ok",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
review = MEDH5File.get_review_status("sample.medh5")
|
|
469
|
+
print(review.status, review.annotator, review.timestamp)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### CLI
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
# Single-file operations
|
|
476
|
+
medh5 info sample.medh5 # print metadata summary
|
|
477
|
+
medh5 info sample.medh5 --json # machine-readable JSON output
|
|
478
|
+
medh5 validate sample.medh5 # check file structure
|
|
479
|
+
medh5 validate sample.medh5 --strict --json # warnings become errors
|
|
480
|
+
|
|
481
|
+
# Batch operations
|
|
482
|
+
medh5 validate-all data/ # validate every .medh5 under a directory
|
|
483
|
+
medh5 validate-all data/ --fail-fast --workers 4
|
|
484
|
+
medh5 audit data/ # verify SHA-256 checksums
|
|
485
|
+
medh5 recompress data/ --compression max # rewrite with different compression
|
|
486
|
+
|
|
487
|
+
# Dataset management
|
|
488
|
+
medh5 index data/ -o manifest.json
|
|
489
|
+
medh5 split manifest.json --ratios 0.7,0.15,0.15 --stratify label -o splits/
|
|
490
|
+
medh5 stats data/ -o stats.json --json
|
|
491
|
+
|
|
492
|
+
# NIfTI / DICOM / nnU-Net v2 conversion
|
|
493
|
+
medh5 import nifti --image CT ct.nii.gz -o sample.medh5
|
|
494
|
+
medh5 import nifti --image CT ct.nii.gz --image PET pet.nii.gz \
|
|
495
|
+
--resample-to CT --interpolator linear -o sample.medh5
|
|
496
|
+
medh5 import dicom /path/to/series -o sample.medh5
|
|
497
|
+
medh5 import dicom /path/to/series -o sample.medh5 \
|
|
498
|
+
--series-uid 1.2.3.4.5 --no-modality-lut
|
|
499
|
+
medh5 import nnunetv2 Dataset042_BraTS/ -o medh5_out/
|
|
500
|
+
medh5 import nnunetv2 Dataset042_BraTS/ -o medh5_out/ \
|
|
501
|
+
--no-test --compression max --checksum
|
|
502
|
+
medh5 export nifti sample.medh5 -o export/
|
|
503
|
+
medh5 export nnunetv2 medh5_out/ -o Dataset042_BraTS_roundtrip/
|
|
504
|
+
|
|
505
|
+
# Review workflow
|
|
506
|
+
medh5 review set sample.medh5 --status reviewed --annotator puyang
|
|
507
|
+
medh5 review get sample.medh5
|
|
508
|
+
medh5 review get sample.medh5 --json
|
|
509
|
+
medh5 review list data/ --status pending
|
|
510
|
+
medh5 review import-seg sample.medh5 --name tumor --from edited.nii.gz
|
|
511
|
+
medh5 review import-seg sample.medh5 --name tumor --from edited.nii.gz --resample --replace
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Inspect with standard HDF5 tools
|
|
515
|
+
|
|
516
|
+
Since `.medh5` is plain HDF5, you can use any HDF5 viewer:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
h5ls -v sample.medh5
|
|
520
|
+
h5dump -A sample.medh5 # show all attributes
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## On-disk layout
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
sample.medh5
|
|
527
|
+
├── images/ (group, required, >= 1 entry)
|
|
528
|
+
│ ├── CT (dataset, N-D, Blosc2-compressed, chunked)
|
|
529
|
+
│ ├── PET (dataset, N-D, Blosc2-compressed, chunked)
|
|
530
|
+
│ └── ...
|
|
531
|
+
├── seg/ (group, optional)
|
|
532
|
+
│ ├── tumor (dataset, N-D bool, Blosc2-compressed, chunked)
|
|
533
|
+
│ ├── liver (dataset, N-D bool, Blosc2-compressed, chunked)
|
|
534
|
+
│ └── ...
|
|
535
|
+
├── bboxes (dataset, [n, ndims, 2], optional)
|
|
536
|
+
├── bbox_scores (dataset, [n], optional)
|
|
537
|
+
├── bbox_labels (dataset, [n] variable-length string, optional)
|
|
538
|
+
└── (root attrs)
|
|
539
|
+
├── schema_version: "1"
|
|
540
|
+
├── image_names: JSON list of modality names
|
|
541
|
+
├── label: int or str
|
|
542
|
+
├── label_name: str
|
|
543
|
+
├── has_seg: bool
|
|
544
|
+
├── seg_names: JSON list of mask names
|
|
545
|
+
├── has_bbox: bool
|
|
546
|
+
├── extra: JSON string
|
|
547
|
+
└── checksum_sha256: str (optional)
|
|
548
|
+
|
|
549
|
+
images.attrs:
|
|
550
|
+
├── shape: int array
|
|
551
|
+
├── spacing: float array
|
|
552
|
+
├── origin: float array
|
|
553
|
+
├── direction: float array (flattened)
|
|
554
|
+
├── axis_labels: string array
|
|
555
|
+
├── coord_system: str
|
|
556
|
+
└── patch_size: int array
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Compression presets
|
|
560
|
+
|
|
561
|
+
| Preset | Compressor | Level | Use case |
|
|
562
|
+
|-------------|------------|-------|------------------------------|
|
|
563
|
+
| `"fast"` | lz4 | 3 | Fast write, moderate ratio |
|
|
564
|
+
| `"balanced"`| lz4hc | 8 | Default, good ratio + speed |
|
|
565
|
+
| `"max"` | zstd | 9 | Maximum compression ratio |
|
|
566
|
+
|
|
567
|
+
## Chunk optimization
|
|
568
|
+
|
|
569
|
+
medh5 includes a chunk-size optimizer (ported from
|
|
570
|
+
[mlarray](https://github.com/MIC-DKFZ/mlarray)) that sizes HDF5 chunks
|
|
571
|
+
to fit L3 cache for efficient patch-based training reads:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from medh5 import optimize_chunks
|
|
575
|
+
|
|
576
|
+
chunks = optimize_chunks(
|
|
577
|
+
image_shape=(128, 256, 256),
|
|
578
|
+
patch_size=192,
|
|
579
|
+
bytes_per_element=4, # float32
|
|
580
|
+
)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Design decisions
|
|
584
|
+
|
|
585
|
+
**Why HDF5?** HDF5 provides native chunking, compression filters, attribute
|
|
586
|
+
metadata, and partial I/O -- all critical for efficient patch-based ML
|
|
587
|
+
training on large volumes. It is widely supported (h5py, HDFView, MATLAB,
|
|
588
|
+
Julia) and inspectable without custom tooling.
|
|
589
|
+
|
|
590
|
+
**Why Blosc2?** Blosc2 is a high-performance meta-compressor optimized for
|
|
591
|
+
binary data. It supports multi-threaded compression, multiple codecs
|
|
592
|
+
(lz4, zstd, etc.), and integrates with HDF5 via hdf5plugin.
|
|
593
|
+
|
|
594
|
+
**Why a single-sample file?** Each `.medh5` file represents one sample
|
|
595
|
+
(patient/scan). This maps naturally to medical imaging workflows where
|
|
596
|
+
each scan is processed independently, and avoids the complexity of
|
|
597
|
+
multi-sample container formats.
|
|
598
|
+
|
|
599
|
+
**Multi-modality by default.** Medical imaging routinely involves multiple
|
|
600
|
+
co-registered modalities (CT + PET, multi-sequence MRI). All modalities
|
|
601
|
+
share the same spatial grid, so they share spatial metadata and chunk layout.
|
|
602
|
+
|
|
603
|
+
**Trust model.** The `extra` field stores arbitrary JSON. When reading
|
|
604
|
+
`.medh5` files from untrusted sources, be aware that very large or deeply
|
|
605
|
+
nested JSON could consume significant memory.
|
|
606
|
+
|
|
607
|
+
## Dependencies
|
|
608
|
+
|
|
609
|
+
- `h5py >= 3.8`
|
|
610
|
+
- `hdf5plugin >= 4.0`
|
|
611
|
+
- `numpy >= 1.24`
|
|
612
|
+
- `torch >= 2.0` (optional, for `medh5[torch]`)
|
|
613
|
+
- `nibabel >= 5` (optional, for `medh5[nifti]`)
|
|
614
|
+
- `pydicom >= 2.4` (optional, for `medh5[dicom]`)
|
|
615
|
+
- `SimpleITK >= 2.3` (optional, for `medh5[itk]`)
|
|
616
|
+
|
|
617
|
+
## License
|
|
618
|
+
|
|
619
|
+
MIT
|