mlarray 0.0.34__tar.gz → 0.0.36__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.
- {mlarray-0.0.34 → mlarray-0.0.36}/PKG-INFO +1 -1
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/optimization.md +2 -4
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/schema.md +25 -6
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/usage.md +37 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/examples/example_channel.py +3 -3
- mlarray-0.0.36/examples/example_non_spatial.py +42 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray/__init__.py +3 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray/meta.py +179 -40
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray/mlarray.py +57 -51
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/PKG-INFO +1 -1
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/SOURCES.txt +1 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/.github/workflows/workflow.yml +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/.gitignore +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/LICENSE +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/MANIFEST.in +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/README.md +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/assets/banner.png +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/assets/banner.png~ +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/api.md +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/cli.md +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/index.md +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/docs/why.md +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/examples/example_metadata_only.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/examples/example_open.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/examples/example_save_load.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mkdocs.yml +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray/cli.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray/utils.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/dependency_links.txt +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/entry_points.txt +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/requires.txt +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/mlarray.egg-info/top_level.txt +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/pyproject.toml +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/setup.cfg +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/tests/test_bboxes.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/tests/test_metadata.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/tests/test_optimization.py +0 -0
- {mlarray-0.0.34 → mlarray-0.0.36}/tests/test_usage.py +0 -0
|
@@ -25,7 +25,7 @@ Instead of requiring users to be storage experts, MLArray introduces a **patch s
|
|
|
25
25
|
* element size (bytes per pixel),
|
|
26
26
|
* CPU cache sizes (L1 / L3 per core),
|
|
27
27
|
* your patch size (2D or 3D),
|
|
28
|
-
*
|
|
28
|
+
* non-spatial axes layout (via `axis_labels`),
|
|
29
29
|
* and then chooses block/chunk sizes that aim to keep decompression and reads cache-friendly.
|
|
30
30
|
|
|
31
31
|
Practically: this means *reading a training patch should tend to require as few chunk/block touches as possible*, while keeping the decompressed working set aligned with CPU caches.
|
|
@@ -201,8 +201,6 @@ When to use:
|
|
|
201
201
|
|
|
202
202
|
## Notes and practical tips
|
|
203
203
|
|
|
204
|
-
* **Patch optimization is currently implemented for 2D and 3D images** (
|
|
204
|
+
* **Patch optimization is currently implemented for 2D and 3D images** (with at most one further non-spatial axis). If your data falls outside that, you can still set `chunk_size`/`block_size` manually or let Blosc2 decide.
|
|
205
205
|
* The best patch size to use is usually the **patch size your dataloader requests most often** (training patch, not necessarily inference tile size).
|
|
206
206
|
* If you’re unsure: start with the default (`patch_size='default'`) and only tune if profiling shows I/O bottlenecks.
|
|
207
|
-
|
|
208
|
-
If you want, I can also help you add a short “How to pick patch_size” subsection tailored to typical pipelines (nnU-Net, 2D slice training, multi-channel inputs).
|
|
@@ -58,15 +58,34 @@ Top-level metadata container.
|
|
|
58
58
|
* **Description:** Spatial metadata for the image.
|
|
59
59
|
* **Dataclass:** `MetaSpatial`.
|
|
60
60
|
|
|
61
|
-
This section stores the information needed to interpret the array in physical space (e.g., voxel spacing, coordinate origin, and orientation). It also optionally captures array shape and
|
|
61
|
+
This section stores the information needed to interpret the array in physical space (e.g., voxel spacing, coordinate origin, and orientation). It also optionally captures array shape and axes layout to make downstream consumers more robust.
|
|
62
62
|
|
|
63
63
|
| field | type | description |
|
|
64
64
|
| ------------ | --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
65
65
|
| spacing | Optional[List[float]] | Voxel spacing per spatial axis, length = `ndims`. |
|
|
66
66
|
| origin | Optional[List[float]] | Origin per spatial axis, length = `ndims`. |
|
|
67
67
|
| direction | Optional[List[List[float]]] | Direction matrix, shape `[ndims][ndims]`. |
|
|
68
|
-
| shape | Optional[List[int]] |
|
|
69
|
-
|
|
|
68
|
+
| shape | Optional[List[int]] | Full array shape, length = spatial + non-spatial axes. |
|
|
69
|
+
| axis_labels | Optional[List[str,AxisLabel]] | Per-axis labels or roles, length = full array `ndims`. |
|
|
70
|
+
| axis_units | Optional[List[str]] | Per-axis units, length = full array `ndims`. |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
#### AxisLabel
|
|
75
|
+
|
|
76
|
+
Axis labels describe the semantic role of each axis. They may be provided as strings or enum values.
|
|
77
|
+
|
|
78
|
+
| value | description |
|
|
79
|
+
| ------------- | -------------------------------------------------------- |
|
|
80
|
+
| spatial | Generic spatial axis (used when no axis-specific label). |
|
|
81
|
+
| spatial_x | Spatial axis representing X. |
|
|
82
|
+
| spatial_y | Spatial axis representing Y. |
|
|
83
|
+
| spatial_z | Spatial axis representing Z. |
|
|
84
|
+
| non_spatial | Generic non-spatial axis. |
|
|
85
|
+
| channel | Channel axis (e.g., color channels or feature maps). |
|
|
86
|
+
| temporal | Time axis. |
|
|
87
|
+
| continuous | Continuous-valued axis (non-spatial). |
|
|
88
|
+
| components | Component axis (e.g., vector components). |
|
|
70
89
|
|
|
71
90
|
---
|
|
72
91
|
|
|
@@ -131,9 +150,9 @@ This section records how the array was laid out on disk (chunking, blocking, pat
|
|
|
131
150
|
|
|
132
151
|
| field | type | description |
|
|
133
152
|
| ---------- | --------------------- | ---------------------------------------------------------------------- |
|
|
134
|
-
| chunk_size | Optional[List[float]] | Chunk size per axis, length = full array `ndims` (including
|
|
135
|
-
| block_size | Optional[List[float]] | Block size per axis, length = full array `ndims` (including
|
|
136
|
-
| patch_size | Optional[List[float]] | Patch size per spatial axis, length = `ndims` (
|
|
153
|
+
| chunk_size | Optional[List[float]] | Chunk size per axis, length = full array `ndims` (including non-spatial axes). |
|
|
154
|
+
| block_size | Optional[List[float]] | Block size per axis, length = full array `ndims` (including non-spatial axes). |
|
|
155
|
+
| patch_size | Optional[List[float]] | Patch size per spatial axis, length = `ndims` (non-spatial axes excluded). |
|
|
137
156
|
|
|
138
157
|
---
|
|
139
158
|
|
|
@@ -128,6 +128,43 @@ image.save("with-metadata.mla")
|
|
|
128
128
|
|
|
129
129
|
---
|
|
130
130
|
|
|
131
|
+
## Non-spatial data usage (Channels, temporal, ...)
|
|
132
|
+
|
|
133
|
+
Use `axis_labels` to mark which axes are spatial and which are non-spatial
|
|
134
|
+
(channels, temporal, components, etc.). Spatial metadata (`spacing`, `origin`,
|
|
135
|
+
`direction`) is specified only for the spatial axes, while the full array shape
|
|
136
|
+
includes both spatial and non-spatial axes.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import numpy as np
|
|
140
|
+
from mlarray import MLArray, MetaSpatial
|
|
141
|
+
|
|
142
|
+
# Example shape: (time, z, y, x, channels)
|
|
143
|
+
array = np.random.random((2, 6, 4, 4, 3, 2))
|
|
144
|
+
|
|
145
|
+
axis_labels = [
|
|
146
|
+
MetaSpatial.AxisLabel.temporal,
|
|
147
|
+
MetaSpatial.AxisLabel.spatial_z,
|
|
148
|
+
MetaSpatial.AxisLabel.spatial_y,
|
|
149
|
+
"spatial_x", # Possible to pass predefined labels as string as well
|
|
150
|
+
MetaSpatial.AxisLabel.channel,
|
|
151
|
+
"some-other-type" # Possible to pass arbitrary strings as well
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
image = MLArray(
|
|
155
|
+
array,
|
|
156
|
+
spacing=(2.5, 0.7, 0.7), # spatial axes only (z, y, x)
|
|
157
|
+
origin=(0.0, 0.0, 0.0),
|
|
158
|
+
axis_labels=axis_labels,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Optional per-axis units (length = full array ndims)
|
|
162
|
+
image.meta.spatial.axis_units = ["s", "mm", "mm", "mm", ""]
|
|
163
|
+
image.save("time-series.mla", patch_size=None)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
131
168
|
## Patch size variants
|
|
132
169
|
|
|
133
170
|
MLArray stores arrays in a chunked layout to enable efficient partial reads. You can control how data is chunked using `patch_size` (recommended in most cases), or manually specify chunk and block sizes when you need full control.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from mlarray import MLArray, Meta, MetaBbox
|
|
4
|
+
from mlarray import MLArray, Meta, MetaBbox, MetaSpatial
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
if __name__ == '__main__':
|
|
9
9
|
print("Creating array...")
|
|
10
10
|
array = np.random.random((32, 64, 64, 3))
|
|
11
|
-
|
|
11
|
+
axis_labels = (MetaSpatial.AxisLabel.spatial_z, MetaSpatial.AxisLabel.spatial_y, MetaSpatial.AxisLabel.spatial_x, MetaSpatial.AxisLabel.channel)
|
|
12
12
|
spacing = np.array((2, 2.5, 4))
|
|
13
13
|
origin = (1, 1, 1)
|
|
14
14
|
direction = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
|
@@ -20,7 +20,7 @@ if __name__ == '__main__':
|
|
|
20
20
|
os.remove(filepath)
|
|
21
21
|
|
|
22
22
|
print("Initializing image...")
|
|
23
|
-
image = MLArray(array, spacing=spacing, origin=origin, direction=direction,
|
|
23
|
+
image = MLArray(array, spacing=spacing, origin=origin, direction=direction, axis_labels=axis_labels, meta=Meta(source=source_meta, bbox=MetaBbox(bboxes)))
|
|
24
24
|
print("Saving image...")
|
|
25
25
|
image.save(filepath)
|
|
26
26
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from mlarray import MLArray, Meta, MetaBbox, MetaSpatial
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if __name__ == '__main__':
|
|
9
|
+
print("Creating array...")
|
|
10
|
+
array = np.random.random((2, 6, 4, 4, 3, 2))
|
|
11
|
+
axis_labels = [
|
|
12
|
+
MetaSpatial.AxisLabel.temporal,
|
|
13
|
+
MetaSpatial.AxisLabel.spatial_z,
|
|
14
|
+
MetaSpatial.AxisLabel.spatial_y,
|
|
15
|
+
"spatial_x", # Possible to pass predefined labels as string as well
|
|
16
|
+
MetaSpatial.AxisLabel.channel,
|
|
17
|
+
"some-other-type" # Possible to pass arbitrary strings as well
|
|
18
|
+
]
|
|
19
|
+
spacing = np.array((2, 2.5, 4))
|
|
20
|
+
origin = (1, 1, 1)
|
|
21
|
+
direction = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
|
22
|
+
source_meta = {"tmp1": "This is an image", "tmp2": 5, "tmp3": {"test1": 16.4587, "test2": [1, 2, 3, 4, 5, 6]}}
|
|
23
|
+
bboxes = [[[0, 1], [0, 1], [0, 1]]]
|
|
24
|
+
filepath = "tmp.mla"
|
|
25
|
+
|
|
26
|
+
if Path(filepath).is_file():
|
|
27
|
+
os.remove(filepath)
|
|
28
|
+
|
|
29
|
+
print("Initializing image...")
|
|
30
|
+
image = MLArray(array, spacing=spacing, origin=origin, direction=direction, axis_labels=axis_labels, meta=Meta(source=source_meta, bbox=MetaBbox(bboxes)))
|
|
31
|
+
image.meta.spatial.axis_units = ["s", "mm", "mm", "mm", ""]
|
|
32
|
+
print("Saving image...")
|
|
33
|
+
image.save(filepath, patch_size=None)
|
|
34
|
+
|
|
35
|
+
print("Loading image...")
|
|
36
|
+
image = MLArray(filepath)
|
|
37
|
+
print(json.dumps(image.meta.to_mapping(), indent=2, sort_keys=True))
|
|
38
|
+
print("Image mean value: ", np.mean(image.to_numpy()))
|
|
39
|
+
print("Some array data: \n", image[:2, :2, 0])
|
|
40
|
+
|
|
41
|
+
if Path(filepath).is_file():
|
|
42
|
+
os.remove(filepath)
|
|
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
17
17
|
MetaSpatial,
|
|
18
18
|
MetaStatistics,
|
|
19
19
|
MetaVersion,
|
|
20
|
+
AxisLabel
|
|
20
21
|
)
|
|
21
22
|
from mlarray.utils import is_serializable
|
|
22
23
|
from mlarray.cli import cli_print_header, cli_convert_to_mlarray
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"MetaSpatial",
|
|
37
38
|
"MetaStatistics",
|
|
38
39
|
"MetaVersion",
|
|
40
|
+
"AxisLabel",
|
|
39
41
|
"is_serializable",
|
|
40
42
|
"cli_print_header",
|
|
41
43
|
"cli_convert_to_mlarray",
|
|
@@ -61,6 +63,7 @@ _LAZY_ATTRS = {
|
|
|
61
63
|
"MetaSpatial": ("mlarray.meta", "MetaSpatial"),
|
|
62
64
|
"MetaStatistics": ("mlarray.meta", "MetaStatistics"),
|
|
63
65
|
"MetaVersion": ("mlarray.meta", "MetaVersion"),
|
|
66
|
+
"AxisLabel": ("mlarray.meta", "AxisLabel"),
|
|
64
67
|
"is_serializable": ("mlarray.utils", "is_serializable"),
|
|
65
68
|
"cli_print_header": ("mlarray.cli", "cli_print_header"),
|
|
66
69
|
"cli_convert_to_mlarray": ("mlarray.cli", "cli_convert_to_mlarray"),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import MISSING, dataclass, field, fields
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Mapping, Optional, Type, TypeVar, Union, TypeAlias, Iterable
|
|
5
|
+
from enum import Enum
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
from mlarray.utils import is_serializable
|
|
@@ -57,7 +58,7 @@ class BaseMeta:
|
|
|
57
58
|
"""Return a user-friendly string based on plain values."""
|
|
58
59
|
return str(self.to_plain())
|
|
59
60
|
|
|
60
|
-
def to_mapping(self, *, include_none: bool = True) ->
|
|
61
|
+
def to_mapping(self, *, include_none: bool = True) -> dict[str, Any]:
|
|
61
62
|
"""Serialize to a mapping, recursively expanding nested BaseMeta.
|
|
62
63
|
|
|
63
64
|
Args:
|
|
@@ -66,7 +67,7 @@ class BaseMeta:
|
|
|
66
67
|
Returns:
|
|
67
68
|
A dict of field names to serialized values.
|
|
68
69
|
"""
|
|
69
|
-
out:
|
|
70
|
+
out: dict[str, Any] = {}
|
|
70
71
|
for f in fields(self):
|
|
71
72
|
v = getattr(self, f.name)
|
|
72
73
|
if v is None and not include_none:
|
|
@@ -125,7 +126,7 @@ class BaseMeta:
|
|
|
125
126
|
A dict of field values, with nested BaseMeta expanded. SingleKeyBaseMeta
|
|
126
127
|
overrides this to return its wrapped value.
|
|
127
128
|
"""
|
|
128
|
-
out:
|
|
129
|
+
out: dict[str, Any] = {}
|
|
129
130
|
for f in fields(self):
|
|
130
131
|
v = getattr(self, f.name)
|
|
131
132
|
if v is None and not include_none:
|
|
@@ -251,7 +252,7 @@ class SingleKeyBaseMeta(BaseMeta):
|
|
|
251
252
|
"""Set the wrapped value."""
|
|
252
253
|
self.value = v
|
|
253
254
|
|
|
254
|
-
def to_mapping(self, *, include_none: bool = True) ->
|
|
255
|
+
def to_mapping(self, *, include_none: bool = True) -> dict[str, Any]:
|
|
255
256
|
"""Serialize to a mapping with the single key.
|
|
256
257
|
|
|
257
258
|
Args:
|
|
@@ -451,6 +452,109 @@ def _validate_float_int_matrix(value: Any, label: str, ndims: Optional[int] = No
|
|
|
451
452
|
for v in row:
|
|
452
453
|
if not isinstance(v, (float, int)):
|
|
453
454
|
raise TypeError(f"{label} must contain only floats or ints")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def validate_and_cast_axis_labels(
|
|
458
|
+
value: Any,
|
|
459
|
+
label: str,
|
|
460
|
+
ndims: Optional[int] = None,
|
|
461
|
+
) -> tuple[Optional[list[str]], int, int]:
|
|
462
|
+
"""
|
|
463
|
+
Validate axis labels/roles, normalize to list[str], and count spatial/non-spatial axes.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
value: None or list-like of axis labels/roles (enum members or strings).
|
|
467
|
+
label: Label used in error messages.
|
|
468
|
+
ndims: If provided, enforce list length == ndims.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
(labels, n_spatial, n_non_spatial)
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
TypeError: If value is not None and not list-like, or contains invalid items.
|
|
475
|
+
ValueError: If ndims is provided and length mismatch occurs.
|
|
476
|
+
"""
|
|
477
|
+
if value is None:
|
|
478
|
+
return None, 0, 0
|
|
479
|
+
|
|
480
|
+
# Cast list / tuple / ndarray -> list
|
|
481
|
+
v = _cast_to_list(value, label)
|
|
482
|
+
|
|
483
|
+
# Enforce 1D list
|
|
484
|
+
for i, item in enumerate(v):
|
|
485
|
+
if isinstance(item, list):
|
|
486
|
+
raise TypeError(
|
|
487
|
+
f"{label} must be a 1D list (got nested list at index {i})"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if ndims is not None and len(v) != ndims:
|
|
491
|
+
raise ValueError(f"{label} must have length {ndims}")
|
|
492
|
+
|
|
493
|
+
spatial_enum_roles = {
|
|
494
|
+
AxisLabelEnum.spatial,
|
|
495
|
+
AxisLabelEnum.spatial_x,
|
|
496
|
+
AxisLabelEnum.spatial_y,
|
|
497
|
+
AxisLabelEnum.spatial_z,
|
|
498
|
+
}
|
|
499
|
+
spatial_string_roles = {r.value for r in spatial_enum_roles}
|
|
500
|
+
|
|
501
|
+
out: list[str] = []
|
|
502
|
+
n_spatial = 0
|
|
503
|
+
n_non_spatial = 0
|
|
504
|
+
|
|
505
|
+
for i, x in enumerate(v):
|
|
506
|
+
if isinstance(x, AxisLabelEnum):
|
|
507
|
+
out.append(x.value)
|
|
508
|
+
if x in spatial_enum_roles:
|
|
509
|
+
n_spatial += 1
|
|
510
|
+
else:
|
|
511
|
+
n_non_spatial += 1
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
if isinstance(x, str):
|
|
515
|
+
out.append(x)
|
|
516
|
+
if x in spatial_string_roles:
|
|
517
|
+
n_spatial += 1
|
|
518
|
+
else:
|
|
519
|
+
n_non_spatial += 1
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
raise TypeError(
|
|
523
|
+
f"{label}[{i}] must be a str or AxisLabelEnum "
|
|
524
|
+
f"(got {type(x).__name__})"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
return out, n_spatial, n_non_spatial
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _is_spatial_axis(label: Union[str, AxisLabelEnum]) -> bool:
|
|
531
|
+
"""Return True if an axis label/role represents a spatial axis."""
|
|
532
|
+
if isinstance(label, AxisLabelEnum):
|
|
533
|
+
return label in {
|
|
534
|
+
AxisLabelEnum.spatial,
|
|
535
|
+
AxisLabelEnum.spatial_x,
|
|
536
|
+
AxisLabelEnum.spatial_y,
|
|
537
|
+
AxisLabelEnum.spatial_z,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if isinstance(label, str):
|
|
541
|
+
return label in {
|
|
542
|
+
AxisLabelEnum.spatial.value,
|
|
543
|
+
AxisLabelEnum.spatial_x.value,
|
|
544
|
+
AxisLabelEnum.spatial_y.value,
|
|
545
|
+
AxisLabelEnum.spatial_z.value,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return False
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _spatial_axis_mask(
|
|
552
|
+
labels: Iterable[Union[str, AxisLabelEnum]],
|
|
553
|
+
) -> list[bool]:
|
|
554
|
+
"""Return a boolean mask indicating which axes are spatial."""
|
|
555
|
+
if labels is None:
|
|
556
|
+
return None
|
|
557
|
+
return [_is_spatial_axis(label) for label in labels]
|
|
454
558
|
|
|
455
559
|
|
|
456
560
|
@dataclass(slots=True)
|
|
@@ -460,19 +564,18 @@ class MetaBlosc2(BaseMeta):
|
|
|
460
564
|
Attributes:
|
|
461
565
|
chunk_size: List of per-dimension chunk sizes. Length must match ndims.
|
|
462
566
|
block_size: List of per-dimension block sizes. Length must match ndims.
|
|
463
|
-
patch_size: List of per-dimension patch sizes. Length must match ndims
|
|
464
|
-
or (ndims - 1) when a channel axis is present.
|
|
567
|
+
patch_size: List of per-dimension patch sizes. Length must match spatial ndims.
|
|
465
568
|
"""
|
|
466
569
|
chunk_size: Optional[list] = None
|
|
467
570
|
block_size: Optional[list] = None
|
|
468
571
|
patch_size: Optional[list] = None
|
|
469
572
|
|
|
470
|
-
def _validate_and_cast(self, *, ndims: Optional[int] = None,
|
|
573
|
+
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
471
574
|
"""Validate and normalize tiling sizes.
|
|
472
575
|
|
|
473
576
|
Args:
|
|
474
|
-
ndims: Number of
|
|
475
|
-
|
|
577
|
+
ndims: Number of dimensions.
|
|
578
|
+
spatial_ndims: Number of spatial dimensions.
|
|
476
579
|
**_: Unused extra context.
|
|
477
580
|
"""
|
|
478
581
|
if self.chunk_size is not None:
|
|
@@ -484,9 +587,37 @@ class MetaBlosc2(BaseMeta):
|
|
|
484
587
|
_validate_float_int_list(self.block_size, "meta._blosc2.block_size", ndims)
|
|
485
588
|
|
|
486
589
|
if self.patch_size is not None:
|
|
487
|
-
|
|
590
|
+
spatial_ndims = ndims if spatial_ndims is None else spatial_ndims
|
|
488
591
|
self.patch_size = _cast_to_list(self.patch_size, "meta._blosc2.patch_size")
|
|
489
|
-
_validate_float_int_list(self.patch_size, "meta._blosc2.patch_size",
|
|
592
|
+
_validate_float_int_list(self.patch_size, "meta._blosc2.patch_size", spatial_ndims)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class AxisLabelEnum(str, Enum):
|
|
596
|
+
"""Axis label/role identifiers used for spatial metadata.
|
|
597
|
+
|
|
598
|
+
Attributes:
|
|
599
|
+
spatial: Generic spatial axis (used when no axis-specific label applies).
|
|
600
|
+
spatial_x: Spatial axis representing X.
|
|
601
|
+
spatial_y: Spatial axis representing Y.
|
|
602
|
+
spatial_z: Spatial axis representing Z.
|
|
603
|
+
non_spatial: Generic non-spatial axis.
|
|
604
|
+
channel: Channel axis (e.g., color channels or feature maps).
|
|
605
|
+
temporal: Time axis.
|
|
606
|
+
continuous: Continuous-valued axis (non-spatial).
|
|
607
|
+
components: Component axis (e.g., vector components).
|
|
608
|
+
"""
|
|
609
|
+
spatial = "spatial"
|
|
610
|
+
spatial_x = "spatial_x"
|
|
611
|
+
spatial_y = "spatial_y"
|
|
612
|
+
spatial_z = "spatial_z"
|
|
613
|
+
non_spatial = "non_spatial"
|
|
614
|
+
channel = "channel"
|
|
615
|
+
temporal = "temporal"
|
|
616
|
+
continuous = "continuous"
|
|
617
|
+
components = "components"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
AxisLabel: TypeAlias = Union[str, AxisLabelEnum]
|
|
490
621
|
|
|
491
622
|
|
|
492
623
|
@dataclass(slots=True)
|
|
@@ -497,42 +628,49 @@ class MetaSpatial(BaseMeta):
|
|
|
497
628
|
spacing: Per-dimension spacing values. Length must match ndims.
|
|
498
629
|
origin: Per-dimension origin values. Length must match ndims.
|
|
499
630
|
direction: Direction cosine matrix of shape [ndims, ndims].
|
|
500
|
-
shape: Array shape. Length must match
|
|
501
|
-
|
|
502
|
-
|
|
631
|
+
shape: Array shape. Length must match (spatial + non-spatial) ndims.
|
|
632
|
+
axis_labels: Per-axis labels or roles. Length must match ndims.
|
|
633
|
+
axis_units: Per-axis units. Length must match ndims.
|
|
634
|
+
_num_spatial_axes: Cached count of spatial axes derived from axis_labels.
|
|
635
|
+
_num_non_spatial_axes: Cached count of non-spatial axes derived from axis_labels.
|
|
503
636
|
"""
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
637
|
+
AxisLabel = AxisLabelEnum
|
|
638
|
+
spacing: Optional[list[Union[int,float]]] = None
|
|
639
|
+
origin: Optional[list[Union[int,float]]] = None
|
|
640
|
+
direction: Optional[list[list[Union[int,float]]]] = None
|
|
641
|
+
shape: Optional[list[int]] = None
|
|
642
|
+
axis_labels: Optional[list[Union[str,AxisLabel]]] = None
|
|
643
|
+
axis_units: Optional[list[str]] = None
|
|
644
|
+
_num_spatial_axes: Optional[int] = None
|
|
645
|
+
_num_non_spatial_axes: Optional[int] = None
|
|
646
|
+
|
|
647
|
+
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
511
648
|
"""Validate and normalize spatial fields.
|
|
512
649
|
|
|
513
650
|
Args:
|
|
514
|
-
ndims: Number of
|
|
651
|
+
ndims: Number of dimensions.
|
|
652
|
+
spatial_ndims: Number of spatial dimensions.
|
|
515
653
|
**_: Unused extra context.
|
|
516
654
|
"""
|
|
517
|
-
if self.
|
|
518
|
-
|
|
655
|
+
if self.axis_labels is not None:
|
|
656
|
+
self.axis_labels, self._num_spatial_axes, self._num_non_spatial_axes = validate_and_cast_axis_labels(self.axis_labels, "meta.spatial.axis_labels", ndims)
|
|
657
|
+
spatial_ndims = spatial_ndims if self._num_spatial_axes is None else self._num_spatial_axes
|
|
519
658
|
|
|
520
659
|
if self.spacing is not None:
|
|
521
660
|
self.spacing = _cast_to_list(self.spacing, "meta.spatial.spacing")
|
|
522
|
-
_validate_float_int_list(self.spacing, "meta.spatial.spacing",
|
|
661
|
+
_validate_float_int_list(self.spacing, "meta.spatial.spacing", spatial_ndims)
|
|
523
662
|
|
|
524
663
|
if self.origin is not None:
|
|
525
664
|
self.origin = _cast_to_list(self.origin, "meta.spatial.origin")
|
|
526
|
-
_validate_float_int_list(self.origin, "meta.spatial.origin",
|
|
665
|
+
_validate_float_int_list(self.origin, "meta.spatial.origin", spatial_ndims)
|
|
527
666
|
|
|
528
667
|
if self.direction is not None:
|
|
529
668
|
self.direction = _cast_to_list(self.direction, "meta.spatial.direction")
|
|
530
|
-
_validate_float_int_matrix(self.direction, "meta.spatial.direction",
|
|
669
|
+
_validate_float_int_matrix(self.direction, "meta.spatial.direction", spatial_ndims)
|
|
531
670
|
|
|
532
671
|
if self.shape is not None:
|
|
533
|
-
_ndims = ndims if (ndims is None or self.channel_axis is None) else ndims + 1
|
|
534
672
|
self.shape = _cast_to_list(self.shape, "meta.spatial.shape")
|
|
535
|
-
_validate_float_int_list(self.shape, "meta.spatial.shape",
|
|
673
|
+
_validate_float_int_list(self.shape, "meta.spatial.shape", ndims)
|
|
536
674
|
|
|
537
675
|
|
|
538
676
|
@dataclass(slots=True)
|
|
@@ -586,9 +724,9 @@ class MetaBbox(BaseMeta):
|
|
|
586
724
|
labels: Optional labels aligned with bboxes. Each label may be a string,
|
|
587
725
|
int, or float.
|
|
588
726
|
"""
|
|
589
|
-
bboxes: Optional[
|
|
590
|
-
scores: Optional[
|
|
591
|
-
labels: Optional[
|
|
727
|
+
bboxes: Optional[list[list[list[Union[int, float]]]]] = None
|
|
728
|
+
scores: Optional[list[Union[int, float]]] = None
|
|
729
|
+
labels: Optional[list[Union[str, int, float]]] = None
|
|
592
730
|
|
|
593
731
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
594
732
|
"""Validate bounding box structure and related fields."""
|
|
@@ -635,7 +773,7 @@ class MetaSource(SingleKeyBaseMeta):
|
|
|
635
773
|
Attributes:
|
|
636
774
|
data: Arbitrary JSON-serializable metadata.
|
|
637
775
|
"""
|
|
638
|
-
data:
|
|
776
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
639
777
|
|
|
640
778
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
641
779
|
"""Validate that data is a JSON-serializable dict."""
|
|
@@ -652,7 +790,7 @@ class MetaExtra(SingleKeyBaseMeta):
|
|
|
652
790
|
Attributes:
|
|
653
791
|
data: Arbitrary JSON-serializable metadata.
|
|
654
792
|
"""
|
|
655
|
-
data:
|
|
793
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
656
794
|
|
|
657
795
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
658
796
|
"""Validate that data is a JSON-serializable dict."""
|
|
@@ -749,11 +887,12 @@ class Meta(BaseMeta):
|
|
|
749
887
|
_image_meta_format: "MetaImageFormat" = field(default_factory=lambda: MetaImageFormat())
|
|
750
888
|
_mlarray_version: "MetaVersion" = field(default_factory=lambda: MetaVersion())
|
|
751
889
|
|
|
752
|
-
def _validate_and_cast(self, *, ndims: Optional[int] = None, **_: Any) -> None:
|
|
890
|
+
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
753
891
|
"""Coerce child metas and validate with optional context.
|
|
754
892
|
|
|
755
893
|
Args:
|
|
756
|
-
ndims: Number of
|
|
894
|
+
ndims: Number of dimensions for context-aware validation.
|
|
895
|
+
spatial_ndims: Number of spatial dimensions for context-aware validation.
|
|
757
896
|
**_: Unused extra context.
|
|
758
897
|
"""
|
|
759
898
|
self.source = MetaSource.ensure(self.source)
|
|
@@ -767,8 +906,8 @@ class Meta(BaseMeta):
|
|
|
767
906
|
self._image_meta_format = MetaImageFormat.ensure(self._image_meta_format)
|
|
768
907
|
self._mlarray_version = MetaVersion.ensure(self._mlarray_version)
|
|
769
908
|
|
|
770
|
-
self.spatial._validate_and_cast(ndims=ndims)
|
|
771
|
-
self._blosc2._validate_and_cast(ndims=ndims,
|
|
909
|
+
self.spatial._validate_and_cast(ndims=ndims, spatial_ndims=spatial_ndims)
|
|
910
|
+
self._blosc2._validate_and_cast(ndims=ndims, spatial_ndims=spatial_ndims)
|
|
772
911
|
|
|
773
912
|
def to_plain(self, *, include_none: bool = False) -> Any:
|
|
774
913
|
"""Convert to plain values, suppressing default sub-metas.
|
|
@@ -780,7 +919,7 @@ class Meta(BaseMeta):
|
|
|
780
919
|
A dict of field values where default child metas are represented
|
|
781
920
|
as None and optionally filtered out.
|
|
782
921
|
"""
|
|
783
|
-
out:
|
|
922
|
+
out: dict[str, Any] = {}
|
|
784
923
|
for f in fields(self):
|
|
785
924
|
v = getattr(self, f.name)
|
|
786
925
|
|
|
@@ -5,7 +5,7 @@ import math
|
|
|
5
5
|
from typing import Dict, Optional, Union, List, Tuple
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import os
|
|
8
|
-
from mlarray.meta import Meta, MetaBlosc2
|
|
8
|
+
from mlarray.meta import Meta, MetaBlosc2, AxisLabel, _spatial_axis_mask
|
|
9
9
|
from mlarray.utils import is_serializable
|
|
10
10
|
|
|
11
11
|
MLARRAY_SUFFIX = "mla"
|
|
@@ -21,7 +21,7 @@ class MLArray:
|
|
|
21
21
|
origin: Optional[Union[List, Tuple, np.ndarray]] = None,
|
|
22
22
|
direction: Optional[Union[List, Tuple, np.ndarray]] = None,
|
|
23
23
|
meta: Optional[Union[Dict, Meta]] = None,
|
|
24
|
-
|
|
24
|
+
axis_labels: Optional[List[Union[str, AxisLabel]]] = None,
|
|
25
25
|
num_threads: int = 1,
|
|
26
26
|
copy: Optional['MLArray'] = None) -> None:
|
|
27
27
|
"""Initializes a MLArray instance.
|
|
@@ -46,8 +46,7 @@ class MLArray:
|
|
|
46
46
|
meta (Optional[Dict | Meta]): Free-form metadata dictionary or Meta
|
|
47
47
|
instance. Must be JSON-serializable when saving.
|
|
48
48
|
If meta is passed as a Dict, it will internally be converted into a Meta object with the dict being interpreted as meta.image metadata.
|
|
49
|
-
|
|
50
|
-
in the array (e.g., 0 for CHW or -1 for HWC). If None, the array
|
|
49
|
+
axis_labels (Optional[List[Union[str, AxisLabel]]]): Per-axis labels or roles. Length must match ndims. If None, the array
|
|
51
50
|
is treated as purely spatial.
|
|
52
51
|
num_threads (int): Number of threads for Blosc2 operations.
|
|
53
52
|
copy (Optional[MLArray]): Another MLArray instance to copy metadata
|
|
@@ -58,13 +57,14 @@ class MLArray:
|
|
|
58
57
|
self.support_metadata = None
|
|
59
58
|
self.mmap = None
|
|
60
59
|
self.meta = None
|
|
61
|
-
if isinstance(array, (str, Path)) and (spacing is not None or origin is not None or direction is not None or meta is not None or
|
|
62
|
-
raise ("Spacing, origin, direction, meta,
|
|
60
|
+
if isinstance(array, (str, Path)) and (spacing is not None or origin is not None or direction is not None or meta is not None or axis_labels is not None or copy is not None):
|
|
61
|
+
raise ("Spacing, origin, direction, meta, axis_labels or copy cannot be set when array is a filepath.")
|
|
63
62
|
if isinstance(array, (str, Path)):
|
|
64
63
|
self._load(array, num_threads)
|
|
65
64
|
else:
|
|
66
65
|
self._store = array
|
|
67
|
-
|
|
66
|
+
has_array = array is not None
|
|
67
|
+
self._validate_and_add_meta(meta, spacing, origin, direction, axis_labels, has_array)
|
|
68
68
|
if copy is not None:
|
|
69
69
|
self.meta.copy_from(copy.meta)
|
|
70
70
|
|
|
@@ -74,7 +74,7 @@ class MLArray:
|
|
|
74
74
|
filepath: Union[str, Path],
|
|
75
75
|
shape: Optional[Union[List, Tuple, np.ndarray]] = None,
|
|
76
76
|
dtype: Optional[np.dtype] = None,
|
|
77
|
-
|
|
77
|
+
axis_labels: Optional[List[Union[str, AxisLabel]]] = None,
|
|
78
78
|
mmap: str = 'r',
|
|
79
79
|
patch_size: Optional[Union[int, List, Tuple]] = 'default', # 'default' means that the default of 192 is used. However, if set to 'default', the patch_size will be skipped if self.patch_size is set from a previously loaded MLArray image. In that case the self.patch_size is used.
|
|
80
80
|
chunk_size: Optional[Union[int, List, Tuple]]= None,
|
|
@@ -99,10 +99,10 @@ class MLArray:
|
|
|
99
99
|
".b2nd" or ".mla".
|
|
100
100
|
shape (Optional[Union[List, Tuple, np.ndarray]]): Shape of the array
|
|
101
101
|
to create. If provided, a new file is created. Length must match
|
|
102
|
-
the full array dimensionality (including
|
|
102
|
+
the full array dimensionality (including non-spatial axes if present).
|
|
103
103
|
dtype (Optional[np.dtype]): Numpy dtype for a newly created array.
|
|
104
|
-
|
|
105
|
-
Used for patch/chunk/block calculations.
|
|
104
|
+
axis_labels (Optional[List[Union[str, AxisLabel]]]): Per-axis labels or roles. Length must match ndims. If None, the array
|
|
105
|
+
is treated as purely spatial. Used for patch/chunk/block calculations.
|
|
106
106
|
mmap (str): Blosc2 mmap mode. One of "r", "r+", "w+", "c".
|
|
107
107
|
patch_size (Optional[Union[int, List, Tuple]]): Patch size hint for
|
|
108
108
|
chunk/block optimization. Provide an int for isotropic sizes or
|
|
@@ -126,7 +126,7 @@ class MLArray:
|
|
|
126
126
|
inconsistent, or if mmap mode is invalid for creation.
|
|
127
127
|
"""
|
|
128
128
|
class_instance = cls()
|
|
129
|
-
class_instance._open(filepath, shape, dtype,
|
|
129
|
+
class_instance._open(filepath, shape, dtype, axis_labels, mmap, patch_size, chunk_size, block_size, num_threads, cparams, dparams)
|
|
130
130
|
return class_instance
|
|
131
131
|
|
|
132
132
|
def _open(
|
|
@@ -134,7 +134,7 @@ class MLArray:
|
|
|
134
134
|
filepath: Union[str, Path],
|
|
135
135
|
shape: Optional[Union[List, Tuple, np.ndarray]] = None,
|
|
136
136
|
dtype: Optional[np.dtype] = None,
|
|
137
|
-
|
|
137
|
+
axis_labels: Optional[list[Union[str, AxisLabel]]] = None,
|
|
138
138
|
mmap: str = 'r',
|
|
139
139
|
patch_size: Optional[Union[int, List, Tuple]] = 'default', # 'default' means that the default of 192 is used. However, if set to 'default', the patch_size will be skipped if self.patch_size is set from a previously loaded MLArray image. In that case the self.patch_size is used.
|
|
140
140
|
chunk_size: Optional[Union[int, List, Tuple]]= None,
|
|
@@ -159,10 +159,10 @@ class MLArray:
|
|
|
159
159
|
".b2nd" or ".mla".
|
|
160
160
|
shape (Optional[Union[List, Tuple, np.ndarray]]): Shape of the array
|
|
161
161
|
to create. If provided, a new file is created. Length must match
|
|
162
|
-
the full array dimensionality (including
|
|
162
|
+
the full array dimensionality (including non-spatial axes if present).
|
|
163
163
|
dtype (Optional[np.dtype]): Numpy dtype for a newly created array.
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
axis_labels (Optional[List[Union[str, AxisLabel]]]): Per-axis labels or roles. Length must match ndims. If None, the array
|
|
165
|
+
is treated as purely spatial.
|
|
166
166
|
mmap (str): Blosc2 mmap mode. One of "r", "r+", "w+", "c".
|
|
167
167
|
patch_size (Optional[Union[int, List, Tuple]]): Patch size hint for
|
|
168
168
|
chunk/block optimization. Provide an int for isotropic sizes or
|
|
@@ -203,7 +203,8 @@ class MLArray:
|
|
|
203
203
|
create_array = mmap == 'w+'
|
|
204
204
|
|
|
205
205
|
if create_array:
|
|
206
|
-
|
|
206
|
+
spatial_axis_mask = [True] * len(shape) if axis_labels is None else _spatial_axis_mask(axis_labels)
|
|
207
|
+
self.meta._blosc2 = self._comp_and_validate_blosc2_meta(self.meta._blosc2, patch_size, chunk_size, block_size, shape, spatial_axis_mask)
|
|
207
208
|
self.meta._has_array.has_array = True
|
|
208
209
|
|
|
209
210
|
self.support_metadata = str(filepath).endswith(f".{MLARRAY_SUFFIX}")
|
|
@@ -332,7 +333,8 @@ class MLArray:
|
|
|
332
333
|
raise RuntimeError(f"MLArray requires '.b2nd' or '.{MLARRAY_SUFFIX}' as extension.")
|
|
333
334
|
|
|
334
335
|
if self._store is not None:
|
|
335
|
-
|
|
336
|
+
spatial_axis_mask = [True] * self.ndim if self.meta.spatial.axis_labels is None else _spatial_axis_mask(self.meta.spatial.axis_labels)
|
|
337
|
+
self.meta._blosc2 = self._comp_and_validate_blosc2_meta(self.meta._blosc2, patch_size, chunk_size, block_size, self._store.shape, spatial_axis_mask)
|
|
336
338
|
self.meta._has_array.has_array = True
|
|
337
339
|
|
|
338
340
|
self.support_metadata = str(filepath).endswith(f".{MLARRAY_SUFFIX}")
|
|
@@ -568,7 +570,7 @@ class MLArray:
|
|
|
568
570
|
|
|
569
571
|
@property
|
|
570
572
|
def ndim(self) -> int:
|
|
571
|
-
"""Returns the number of dimensions of the array.
|
|
573
|
+
"""Returns the number of spatial and non-spatial dimensions of the array.
|
|
572
574
|
|
|
573
575
|
Returns:
|
|
574
576
|
int: Number of dimensions, or None if no array is loaded.
|
|
@@ -581,23 +583,21 @@ class MLArray:
|
|
|
581
583
|
def _spatial_ndim(self) -> int:
|
|
582
584
|
"""Returns the number of spatial dimensions.
|
|
583
585
|
|
|
584
|
-
If ``channel_axis`` is set, the channel dimension is excluded.
|
|
585
|
-
|
|
586
586
|
Returns:
|
|
587
587
|
int: Number of spatial dimensions, or None if no array is loaded.
|
|
588
588
|
"""
|
|
589
589
|
if self._store is None or self.meta._has_array.has_array == False:
|
|
590
590
|
return None
|
|
591
591
|
ndim = len(self._store.shape)
|
|
592
|
-
if self.meta.spatial.
|
|
593
|
-
ndim
|
|
592
|
+
if self.meta.spatial._num_spatial_axes is not None:
|
|
593
|
+
ndim = self.meta.spatial._num_spatial_axes
|
|
594
594
|
return ndim
|
|
595
595
|
|
|
596
596
|
def comp_blosc2_params(
|
|
597
597
|
self,
|
|
598
598
|
image_size: Union[Tuple[int, int], Tuple[int, int, int], Tuple[int, int, int, int]],
|
|
599
599
|
patch_size: Union[Tuple[int, int], Tuple[int, int, int]],
|
|
600
|
-
|
|
600
|
+
spatial_axis_mask: Optional[list[bool]] = None,
|
|
601
601
|
bytes_per_pixel: int = 4, # 4 byte are float32
|
|
602
602
|
l1_cache_size_per_core_in_bytes: int = 32768, # 1 Kibibyte (KiB) = 2^10 Byte; 32 KiB = 32768 Byte
|
|
603
603
|
l3_cache_size_per_core_in_bytes: int = 1441792, # 1 Mibibyte (MiB) = 2^20 Byte = 1.048.576 Byte; 1.375MiB = 1441792 Byte
|
|
@@ -624,13 +624,11 @@ class MLArray:
|
|
|
624
624
|
Args:
|
|
625
625
|
image_size (Union[Tuple[int, int], Tuple[int, int, int], Tuple[int, int, int, int]]):
|
|
626
626
|
Image shape. Use a 2D, 3D, or 4D size; 2D/3D inputs are
|
|
627
|
-
internally expanded to 4D (with
|
|
627
|
+
internally expanded to 4D (with non-spatial axes first).
|
|
628
628
|
patch_size (Union[Tuple[int, int], Tuple[int, int, int]]): Patch
|
|
629
629
|
size for spatial dimensions. Use a 2-tuple (x, y) or 3-tuple
|
|
630
630
|
(x, y, z).
|
|
631
|
-
|
|
632
|
-
source array. If set, the size is moved to channels-first
|
|
633
|
-
for cache calculations.
|
|
631
|
+
spatial_axis_mask (Optional[list[bool]]): Mask indicating for every axis whether it is spatial or not.
|
|
634
632
|
bytes_per_pixel (int): Number of bytes per element. Defaults to 4
|
|
635
633
|
for float32.
|
|
636
634
|
l1_cache_size_per_core_in_bytes (int): L1 cache per core in bytes.
|
|
@@ -654,8 +652,14 @@ class MLArray:
|
|
|
654
652
|
image_size = (1, *image_size)
|
|
655
653
|
num_squeezes = 1
|
|
656
654
|
|
|
657
|
-
|
|
658
|
-
|
|
655
|
+
non_spatial_axis = None
|
|
656
|
+
if spatial_axis_mask is not None:
|
|
657
|
+
non_spatial_axis_mask = [not b for b in spatial_axis_mask]
|
|
658
|
+
if sum(non_spatial_axis_mask) > 1:
|
|
659
|
+
raise RuntimeError("Automatic blosc2 optimization currently only supports one non-spatial axis. Please set chunk and block size manually.")
|
|
660
|
+
non_spatial_axis = next((i for i, v in enumerate(non_spatial_axis_mask) if v), None)
|
|
661
|
+
if non_spatial_axis is not None:
|
|
662
|
+
image_size = _move_index_list(image_size, non_spatial_axis+num_squeezes, 0)
|
|
659
663
|
|
|
660
664
|
if len(image_size) != 4:
|
|
661
665
|
raise RuntimeError("Image size must be 4D.")
|
|
@@ -663,11 +667,11 @@ class MLArray:
|
|
|
663
667
|
if not (len(patch_size) == 2 or len(patch_size) == 3):
|
|
664
668
|
raise RuntimeError("Patch size must be 2D or 3D.")
|
|
665
669
|
|
|
666
|
-
|
|
670
|
+
non_spatial_size = image_size[0]
|
|
667
671
|
if len(patch_size) == 2:
|
|
668
672
|
patch_size = [1, *patch_size]
|
|
669
673
|
patch_size = np.array(patch_size)
|
|
670
|
-
block_size = np.array((
|
|
674
|
+
block_size = np.array((non_spatial_size, *[2 ** (max(0, math.ceil(math.log2(i)))) for i in patch_size]))
|
|
671
675
|
|
|
672
676
|
# shrink the block size until it fits in L1
|
|
673
677
|
estimated_nbytes_block = np.prod(block_size) * bytes_per_pixel
|
|
@@ -713,16 +717,16 @@ class MLArray:
|
|
|
713
717
|
# better safe than sorry
|
|
714
718
|
chunk_size = [min(i, j) for i, j in zip(image_size, chunk_size)]
|
|
715
719
|
|
|
716
|
-
if
|
|
717
|
-
block_size = _move_index_list(block_size, 0,
|
|
718
|
-
chunk_size = _move_index_list(chunk_size, 0,
|
|
720
|
+
if non_spatial_axis is not None:
|
|
721
|
+
block_size = _move_index_list(block_size, 0, non_spatial_axis+num_squeezes)
|
|
722
|
+
chunk_size = _move_index_list(chunk_size, 0, non_spatial_axis+num_squeezes)
|
|
719
723
|
|
|
720
724
|
block_size = block_size[num_squeezes:]
|
|
721
725
|
chunk_size = chunk_size[num_squeezes:]
|
|
722
726
|
|
|
723
727
|
return [int(value) for value in chunk_size], [int(value) for value in block_size]
|
|
724
728
|
|
|
725
|
-
def _comp_and_validate_blosc2_meta(self, meta_blosc2, patch_size, chunk_size, block_size, shape,
|
|
729
|
+
def _comp_and_validate_blosc2_meta(self, meta_blosc2, patch_size, chunk_size, block_size, shape, spatial_axis_mask):
|
|
726
730
|
"""Compute and validate Blosc2 chunk/block metadata.
|
|
727
731
|
|
|
728
732
|
Args:
|
|
@@ -732,32 +736,32 @@ class MLArray:
|
|
|
732
736
|
or "default". See ``open``/``save`` for expected shapes.
|
|
733
737
|
chunk_size (Optional[Union[int, List, Tuple]]): Explicit chunk size.
|
|
734
738
|
block_size (Optional[Union[int, List, Tuple]]): Explicit block size.
|
|
735
|
-
shape (Union[List, Tuple, np.ndarray]): Full array shape including
|
|
736
|
-
|
|
737
|
-
channel_axis (Optional[int]): Channel axis index, if any.
|
|
739
|
+
shape (Union[List, Tuple, np.ndarray]): Full array shape including non-spatial axes.
|
|
740
|
+
spatial_axis_mask (Optional[list[bool]]): Mask indicating for every axis whether it is spatial or not.
|
|
738
741
|
|
|
739
742
|
Returns:
|
|
740
743
|
MetaBlosc2: Validated Blosc2 metadata instance.
|
|
741
744
|
"""
|
|
742
|
-
|
|
743
|
-
|
|
745
|
+
num_spatial_axes = sum(spatial_axis_mask)
|
|
746
|
+
num_non_spatial_axes = sum([not b for b in spatial_axis_mask])
|
|
747
|
+
if patch_size is not None and patch_size != "default" and (num_spatial_axes == 1 or num_spatial_axes > 3 or num_non_spatial_axes > 1):
|
|
748
|
+
raise NotImplementedError("Chunk and block size optimization based on patch size is only implemented for 2D and 3D spatial images with at most one further non-spatial axis. Please set the chunk and block size manually or set to None for blosc2 to determine a chunk and block size.")
|
|
744
749
|
if patch_size is not None and patch_size != "default" and (chunk_size is not None or block_size is not None):
|
|
745
750
|
raise RuntimeError("patch_size and chunk_size / block_size cannot both be explicitly set.")
|
|
746
751
|
|
|
747
|
-
ndims = len(shape) if channel_axis is None else len(shape) - 1
|
|
748
752
|
if patch_size == "default":
|
|
749
753
|
if meta_blosc2 is not None and meta_blosc2.patch_size is not None: # Use previously loaded patch size, when patch size is not explicitly set and a patch size from a previously loaded image exists
|
|
750
754
|
patch_size = meta_blosc2.patch_size
|
|
751
755
|
else: # Use default patch size, when patch size is not explicitly set and no patch size from a previously loaded image exists
|
|
752
|
-
patch_size = [MLARRAY_DEFAULT_PATCH_SIZE] *
|
|
756
|
+
patch_size = [MLARRAY_DEFAULT_PATCH_SIZE] * num_spatial_axes
|
|
753
757
|
|
|
754
758
|
patch_size = [patch_size] * len(shape) if isinstance(patch_size, int) else patch_size
|
|
755
759
|
|
|
756
760
|
if patch_size is not None:
|
|
757
|
-
chunk_size, block_size = self.comp_blosc2_params(shape, patch_size,
|
|
761
|
+
chunk_size, block_size = self.comp_blosc2_params(shape, patch_size, spatial_axis_mask)
|
|
758
762
|
|
|
759
763
|
meta_blosc2 = MetaBlosc2(chunk_size, block_size, patch_size)
|
|
760
|
-
meta_blosc2._validate_and_cast(ndims=len(shape),
|
|
764
|
+
meta_blosc2._validate_and_cast(ndims=len(shape), spatial_ndims=num_spatial_axes)
|
|
761
765
|
return meta_blosc2
|
|
762
766
|
|
|
763
767
|
def _read_meta(self):
|
|
@@ -780,7 +784,7 @@ class MLArray:
|
|
|
780
784
|
raise RuntimeError("Metadata is not serializable.")
|
|
781
785
|
self._store.vlmeta["mlarray"] = metadata
|
|
782
786
|
|
|
783
|
-
def _validate_and_add_meta(self, meta, spacing=None, origin=None, direction=None,
|
|
787
|
+
def _validate_and_add_meta(self, meta, spacing=None, origin=None, direction=None, axis_labels=None, has_array=None):
|
|
784
788
|
"""Validate and attach metadata to the MLArray instance.
|
|
785
789
|
|
|
786
790
|
Args:
|
|
@@ -792,7 +796,7 @@ class MLArray:
|
|
|
792
796
|
spatial axis.
|
|
793
797
|
direction (Optional[Union[List, Tuple, np.ndarray]]): Direction
|
|
794
798
|
cosine matrix with shape (ndims, ndims).
|
|
795
|
-
|
|
799
|
+
axis_labels (Optional[List[Union[str, AxisLabel]]]): Per-axis labels or roles. Length must match ndims.
|
|
796
800
|
|
|
797
801
|
Raises:
|
|
798
802
|
ValueError: If ``meta`` is not None, dict, or Meta.
|
|
@@ -812,11 +816,13 @@ class MLArray:
|
|
|
812
816
|
self.meta.spatial.origin = origin
|
|
813
817
|
if direction is not None:
|
|
814
818
|
self.meta.spatial.direction = direction
|
|
815
|
-
if
|
|
816
|
-
self.meta.spatial.
|
|
817
|
-
if
|
|
819
|
+
if axis_labels is not None:
|
|
820
|
+
self.meta.spatial.axis_labels = axis_labels
|
|
821
|
+
if has_array == True:
|
|
822
|
+
self.meta._has_array.has_array = True
|
|
823
|
+
if self.meta._has_array.has_array:
|
|
818
824
|
self.meta.spatial.shape = self.shape
|
|
819
|
-
self.meta.spatial._validate_and_cast(ndims=self._spatial_ndim)
|
|
825
|
+
self.meta.spatial._validate_and_cast(ndims=self.ndim, spatial_ndims=self._spatial_ndim)
|
|
820
826
|
|
|
821
827
|
def _update_blosc2_meta(self):
|
|
822
828
|
"""Sync Blosc2 chunk and block sizes into metadata.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|