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.
Files changed (51) hide show
  1. medh5-0.5.0/LICENSE +21 -0
  2. medh5-0.5.0/PKG-INFO +619 -0
  3. medh5-0.5.0/README.md +552 -0
  4. medh5-0.5.0/medh5/__init__.py +29 -0
  5. medh5-0.5.0/medh5/chunks.py +182 -0
  6. medh5-0.5.0/medh5/cli/__init__.py +81 -0
  7. medh5-0.5.0/medh5/cli/__main__.py +10 -0
  8. medh5-0.5.0/medh5/cli/_common.py +16 -0
  9. medh5-0.5.0/medh5/cli/convert.py +219 -0
  10. medh5-0.5.0/medh5/cli/dataset.py +132 -0
  11. medh5-0.5.0/medh5/cli/inspect.py +301 -0
  12. medh5-0.5.0/medh5/cli/review.py +130 -0
  13. medh5-0.5.0/medh5/core.py +1142 -0
  14. medh5-0.5.0/medh5/dataset/__init__.py +14 -0
  15. medh5-0.5.0/medh5/dataset/index.py +192 -0
  16. medh5-0.5.0/medh5/dataset/split.py +233 -0
  17. medh5-0.5.0/medh5/exceptions.py +19 -0
  18. medh5-0.5.0/medh5/integrity.py +118 -0
  19. medh5-0.5.0/medh5/io/__init__.py +45 -0
  20. medh5-0.5.0/medh5/io/dicom.py +366 -0
  21. medh5-0.5.0/medh5/io/nifti.py +473 -0
  22. medh5-0.5.0/medh5/io/nnunetv2.py +707 -0
  23. medh5-0.5.0/medh5/meta.py +295 -0
  24. medh5-0.5.0/medh5/py.typed +0 -0
  25. medh5-0.5.0/medh5/review.py +135 -0
  26. medh5-0.5.0/medh5/sampling.py +227 -0
  27. medh5-0.5.0/medh5/stats.py +316 -0
  28. medh5-0.5.0/medh5/torch.py +249 -0
  29. medh5-0.5.0/medh5/transforms.py +202 -0
  30. medh5-0.5.0/medh5.egg-info/PKG-INFO +619 -0
  31. medh5-0.5.0/medh5.egg-info/SOURCES.txt +49 -0
  32. medh5-0.5.0/medh5.egg-info/dependency_links.txt +1 -0
  33. medh5-0.5.0/medh5.egg-info/entry_points.txt +2 -0
  34. medh5-0.5.0/medh5.egg-info/requires.txt +23 -0
  35. medh5-0.5.0/medh5.egg-info/top_level.txt +1 -0
  36. medh5-0.5.0/pyproject.toml +113 -0
  37. medh5-0.5.0/setup.cfg +4 -0
  38. medh5-0.5.0/tests/test_chunks.py +76 -0
  39. medh5-0.5.0/tests/test_cli.py +436 -0
  40. medh5-0.5.0/tests/test_dataset.py +114 -0
  41. medh5-0.5.0/tests/test_integrity.py +103 -0
  42. medh5-0.5.0/tests/test_io.py +919 -0
  43. medh5-0.5.0/tests/test_partial.py +139 -0
  44. medh5-0.5.0/tests/test_review.py +74 -0
  45. medh5-0.5.0/tests/test_roundtrip.py +396 -0
  46. medh5-0.5.0/tests/test_sampling.py +156 -0
  47. medh5-0.5.0/tests/test_split.py +104 -0
  48. medh5-0.5.0/tests/test_stats.py +114 -0
  49. medh5-0.5.0/tests/test_torch.py +247 -0
  50. medh5-0.5.0/tests/test_transforms.py +172 -0
  51. 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