mlarray 0.0.48__tar.gz → 0.0.50__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.48 → mlarray-0.0.50}/PKG-INFO +2 -2
- {mlarray-0.0.48 → mlarray-0.0.50}/README.md +1 -1
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/cli.md +1 -1
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray/cli.py +5 -3
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray/meta.py +401 -31
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray/mlarray.py +150 -103
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/PKG-INFO +2 -2
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/SOURCES.txt +1 -0
- mlarray-0.0.50/tests/test_meta_safety.py +141 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_usage.py +68 -13
- {mlarray-0.0.48 → mlarray-0.0.50}/.github/workflows/workflow.yml +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/.gitignore +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/LICENSE +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/MANIFEST.in +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/assets/banner.png +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/assets/banner.png~ +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/api.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/index.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/optimization.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/schema.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/usage.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/docs/why.md +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_asarray.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_bboxes_only.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_channel.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_compressed_vs_uncompressed.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_in_memory_constructors.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_metadata_only.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_non_spatial.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_open.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/examples/example_save_load.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mkdocs.yml +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray/__init__.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray/utils.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/dependency_links.txt +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/entry_points.txt +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/requires.txt +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/mlarray.egg-info/top_level.txt +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/pyproject.toml +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/setup.cfg +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_asarray.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_bboxes.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_constructors.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_create.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_metadata.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_open.py +0 -0
- {mlarray-0.0.48 → mlarray-0.0.50}/tests/test_optimization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlarray
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.50
|
|
4
4
|
Summary: Array format specialized for Machine Learning with Blosc2 backend and standardized metadata.
|
|
5
5
|
Author-email: Karol Gotkowski <karol.gotkowski@dkfz.de>
|
|
6
6
|
License: MIT
|
|
@@ -228,7 +228,7 @@ image.save("blosc2-auto.mla")
|
|
|
228
228
|
|
|
229
229
|
### mlarray_header
|
|
230
230
|
|
|
231
|
-
Print the metadata header from a `.mla`
|
|
231
|
+
Print the metadata header from a `.mla` file.
|
|
232
232
|
|
|
233
233
|
```bash
|
|
234
234
|
mlarray_header sample.mla
|
|
@@ -8,7 +8,7 @@ The CLI currently focuses on core workflows (header inspection and conversion).
|
|
|
8
8
|
|
|
9
9
|
## `mlarray_header`
|
|
10
10
|
|
|
11
|
-
Print the metadata header from a `.mla`
|
|
11
|
+
Print the metadata header from a `.mla` file.
|
|
12
12
|
|
|
13
13
|
This command is useful for quickly checking spatial metadata, stored schemas, and other file-level information without loading the full array into memory.
|
|
14
14
|
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
from typing import Union
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from mlarray import MLArray
|
|
6
|
+
from mlarray.meta import _meta_internal_write
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
9
|
from medvol import MedVol
|
|
@@ -14,7 +15,7 @@ def print_header(filepath: Union[str, Path]) -> None:
|
|
|
14
15
|
"""Print the MLArray metadata header for a file.
|
|
15
16
|
|
|
16
17
|
Args:
|
|
17
|
-
filepath: Path to a ".mla"
|
|
18
|
+
filepath: Path to a ".mla" file.
|
|
18
19
|
"""
|
|
19
20
|
meta = MLArray(filepath).meta
|
|
20
21
|
if meta is None:
|
|
@@ -33,7 +34,8 @@ def convert_to_mlarray(load_filepath: Union[str, Path], save_filepath: Union[str
|
|
|
33
34
|
image_meta_format = "nrrd"
|
|
34
35
|
image_medvol = MedVol(load_filepath)
|
|
35
36
|
image_mlarray = MLArray(image_medvol.array, spacing=image_medvol.spacing, origin=image_medvol.origin, direction=image_medvol.direction, meta=image_medvol.header)
|
|
36
|
-
|
|
37
|
+
with _meta_internal_write():
|
|
38
|
+
image_mlarray.meta._image_meta_format = image_meta_format
|
|
37
39
|
image_mlarray.save(save_filepath)
|
|
38
40
|
|
|
39
41
|
|
|
@@ -42,7 +44,7 @@ def cli_print_header() -> None:
|
|
|
42
44
|
prog="mlarray_header",
|
|
43
45
|
description="Print the MLArray metadata header for a file.",
|
|
44
46
|
)
|
|
45
|
-
parser.add_argument("filepath", help="Path to a .mla
|
|
47
|
+
parser.add_argument("filepath", help="Path to a .mla file.")
|
|
46
48
|
args = parser.parse_args()
|
|
47
49
|
print_header(args.filepath)
|
|
48
50
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from contextvars import ContextVar
|
|
3
5
|
from dataclasses import MISSING, dataclass, field, fields
|
|
4
6
|
from typing import Any, Mapping, Optional, Type, TypeVar, Union, TypeAlias, Iterable
|
|
5
7
|
from enum import Enum
|
|
@@ -11,6 +13,110 @@ T = TypeVar("T", bound="BaseMeta")
|
|
|
11
13
|
SK = TypeVar("SK", bound="SingleKeyBaseMeta")
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
_INTERNAL_META_WRITE: ContextVar[bool] = ContextVar(
|
|
17
|
+
"_INTERNAL_META_WRITE",
|
|
18
|
+
default=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def _meta_internal_write():
|
|
24
|
+
"""Allow internal metadata writes within a bounded context."""
|
|
25
|
+
token = _INTERNAL_META_WRITE.set(True)
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
finally:
|
|
29
|
+
_INTERNAL_META_WRITE.reset(token)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_meta_internal_write() -> bool:
|
|
33
|
+
return _INTERNAL_META_WRITE.get()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _has_initialized_attr(obj: Any, name: str) -> bool:
|
|
37
|
+
try:
|
|
38
|
+
object.__getattribute__(obj, name)
|
|
39
|
+
return True
|
|
40
|
+
except AttributeError:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _raise_internal_only(label: str) -> None:
|
|
45
|
+
raise AttributeError(f"{label} is managed by MLArray and is read-only.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _public_dataclass_fields(obj_or_cls: Any):
|
|
49
|
+
return [
|
|
50
|
+
f for f in fields(obj_or_cls)
|
|
51
|
+
if not f.metadata.get("mlarray_internal_state", False)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _FrozenList(list):
|
|
56
|
+
"""A list that disallows in-place mutation."""
|
|
57
|
+
|
|
58
|
+
def _readonly(self, *_: Any, **__: Any) -> None:
|
|
59
|
+
raise AttributeError("This metadata field is read-only.")
|
|
60
|
+
|
|
61
|
+
__setitem__ = _readonly
|
|
62
|
+
__delitem__ = _readonly
|
|
63
|
+
__iadd__ = _readonly
|
|
64
|
+
__imul__ = _readonly
|
|
65
|
+
append = _readonly
|
|
66
|
+
clear = _readonly
|
|
67
|
+
extend = _readonly
|
|
68
|
+
insert = _readonly
|
|
69
|
+
pop = _readonly
|
|
70
|
+
remove = _readonly
|
|
71
|
+
reverse = _readonly
|
|
72
|
+
sort = _readonly
|
|
73
|
+
|
|
74
|
+
def __reduce_ex__(self, protocol: int):
|
|
75
|
+
return (_FrozenList, (list(self),))
|
|
76
|
+
|
|
77
|
+
def __copy__(self):
|
|
78
|
+
return _FrozenList(self)
|
|
79
|
+
|
|
80
|
+
def __deepcopy__(self, memo):
|
|
81
|
+
copied = _FrozenList(self)
|
|
82
|
+
memo[id(self)] = copied
|
|
83
|
+
return copied
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _FrozenDict(dict):
|
|
87
|
+
"""A dict that disallows in-place mutation."""
|
|
88
|
+
|
|
89
|
+
def _readonly(self, *_: Any, **__: Any) -> None:
|
|
90
|
+
raise AttributeError("This metadata field is read-only.")
|
|
91
|
+
|
|
92
|
+
__setitem__ = _readonly
|
|
93
|
+
__delitem__ = _readonly
|
|
94
|
+
clear = _readonly
|
|
95
|
+
pop = _readonly
|
|
96
|
+
popitem = _readonly
|
|
97
|
+
setdefault = _readonly
|
|
98
|
+
update = _readonly
|
|
99
|
+
|
|
100
|
+
def __reduce_ex__(self, protocol: int):
|
|
101
|
+
return (_FrozenDict, (dict(self),))
|
|
102
|
+
|
|
103
|
+
def __copy__(self):
|
|
104
|
+
return _FrozenDict(self)
|
|
105
|
+
|
|
106
|
+
def __deepcopy__(self, memo):
|
|
107
|
+
copied = _FrozenDict(self)
|
|
108
|
+
memo[id(self)] = copied
|
|
109
|
+
return copied
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _freeze_jsonable(value: Any) -> Any:
|
|
113
|
+
if isinstance(value, Mapping):
|
|
114
|
+
return _FrozenDict({k: _freeze_jsonable(v) for k, v in value.items()})
|
|
115
|
+
if isinstance(value, list):
|
|
116
|
+
return _FrozenList([_freeze_jsonable(v) for v in value])
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
14
120
|
def _is_unset_value(v: Any) -> bool:
|
|
15
121
|
"""Return True when a value should be treated as "unset".
|
|
16
122
|
|
|
@@ -37,10 +143,67 @@ class BaseMeta:
|
|
|
37
143
|
Subclasses should implement _validate_and_cast to coerce and validate
|
|
38
144
|
fields after initialization or mutation.
|
|
39
145
|
"""
|
|
146
|
+
_validate_ndims: Optional[int] = field(
|
|
147
|
+
default=None,
|
|
148
|
+
init=False,
|
|
149
|
+
repr=False,
|
|
150
|
+
compare=False,
|
|
151
|
+
metadata={"mlarray_internal_state": True},
|
|
152
|
+
)
|
|
153
|
+
_validate_spatial_ndims: Optional[int] = field(
|
|
154
|
+
default=None,
|
|
155
|
+
init=False,
|
|
156
|
+
repr=False,
|
|
157
|
+
compare=False,
|
|
158
|
+
metadata={"mlarray_internal_state": True},
|
|
159
|
+
)
|
|
160
|
+
_PROTECTED_FIELDS = frozenset()
|
|
161
|
+
_PROTECTED_FIELD_PREFIX = ""
|
|
162
|
+
|
|
163
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
164
|
+
if name in {"_validate_ndims", "_validate_spatial_ndims"}:
|
|
165
|
+
object.__setattr__(self, name, value)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
if _is_meta_internal_write() or not _has_initialized_attr(self, name):
|
|
169
|
+
object.__setattr__(self, name, value)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if name in self._PROTECTED_FIELDS:
|
|
173
|
+
prefix = self._PROTECTED_FIELD_PREFIX
|
|
174
|
+
label = f"{prefix}{name}" if prefix else name
|
|
175
|
+
_raise_internal_only(label)
|
|
176
|
+
|
|
177
|
+
current = getattr(self, name)
|
|
178
|
+
if isinstance(current, BaseMeta) and not isinstance(value, current.__class__):
|
|
179
|
+
value = current.__class__.ensure(value)
|
|
180
|
+
|
|
181
|
+
object.__setattr__(self, name, value)
|
|
182
|
+
try:
|
|
183
|
+
with _meta_internal_write():
|
|
184
|
+
self._validate_and_cast(
|
|
185
|
+
ndims=self._validate_ndims,
|
|
186
|
+
spatial_ndims=self._validate_spatial_ndims,
|
|
187
|
+
)
|
|
188
|
+
except Exception:
|
|
189
|
+
object.__setattr__(self, name, current)
|
|
190
|
+
raise
|
|
40
191
|
|
|
41
192
|
def __post_init__(self) -> None:
|
|
42
193
|
"""Validate and normalize fields after dataclass initialization."""
|
|
43
|
-
|
|
194
|
+
with _meta_internal_write():
|
|
195
|
+
self._validate_and_cast()
|
|
196
|
+
|
|
197
|
+
def _remember_validation_context(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
ndims: Optional[int] = None,
|
|
201
|
+
spatial_ndims: Optional[int] = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
if ndims is not None:
|
|
204
|
+
object.__setattr__(self, "_validate_ndims", ndims)
|
|
205
|
+
if spatial_ndims is not None:
|
|
206
|
+
object.__setattr__(self, "_validate_spatial_ndims", spatial_ndims)
|
|
44
207
|
|
|
45
208
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
46
209
|
"""Validate and normalize fields in subclasses.
|
|
@@ -68,7 +231,7 @@ class BaseMeta:
|
|
|
68
231
|
A dict of field names to serialized values.
|
|
69
232
|
"""
|
|
70
233
|
out: dict[str, Any] = {}
|
|
71
|
-
for f in
|
|
234
|
+
for f in _public_dataclass_fields(self):
|
|
72
235
|
v = getattr(self, f.name)
|
|
73
236
|
if v is None and not include_none:
|
|
74
237
|
continue
|
|
@@ -98,14 +261,14 @@ class BaseMeta:
|
|
|
98
261
|
)
|
|
99
262
|
|
|
100
263
|
dd = dict(d)
|
|
101
|
-
known = {f.name for f in
|
|
264
|
+
known = {f.name for f in _public_dataclass_fields(cls)}
|
|
102
265
|
unknown = set(dd) - known
|
|
103
266
|
if unknown:
|
|
104
267
|
raise KeyError(
|
|
105
268
|
f"Unknown {cls.__name__} keys in from_mapping: {sorted(unknown)}"
|
|
106
269
|
)
|
|
107
270
|
|
|
108
|
-
for f in
|
|
271
|
+
for f in _public_dataclass_fields(cls):
|
|
109
272
|
if f.name not in dd:
|
|
110
273
|
continue
|
|
111
274
|
v = dd[f.name]
|
|
@@ -127,7 +290,7 @@ class BaseMeta:
|
|
|
127
290
|
overrides this to return its wrapped value.
|
|
128
291
|
"""
|
|
129
292
|
out: dict[str, Any] = {}
|
|
130
|
-
for f in
|
|
293
|
+
for f in _public_dataclass_fields(self):
|
|
131
294
|
v = getattr(self, f.name)
|
|
132
295
|
if v is None and not include_none:
|
|
133
296
|
continue
|
|
@@ -141,7 +304,7 @@ class BaseMeta:
|
|
|
141
304
|
"""Return True if this equals a default-constructed instance."""
|
|
142
305
|
default = self.__class__() # type: ignore[call-arg]
|
|
143
306
|
|
|
144
|
-
for f in
|
|
307
|
+
for f in _public_dataclass_fields(self):
|
|
145
308
|
a = getattr(self, f.name)
|
|
146
309
|
b = getattr(default, f.name)
|
|
147
310
|
|
|
@@ -155,7 +318,7 @@ class BaseMeta:
|
|
|
155
318
|
|
|
156
319
|
def reset(self) -> None:
|
|
157
320
|
"""Reset all fields to their default or None."""
|
|
158
|
-
for f in
|
|
321
|
+
for f in _public_dataclass_fields(self):
|
|
159
322
|
if f.default_factory is not MISSING: # type: ignore[attr-defined]
|
|
160
323
|
setattr(self, f.name, f.default_factory()) # type: ignore[misc]
|
|
161
324
|
elif f.default is not MISSING:
|
|
@@ -179,7 +342,7 @@ class BaseMeta:
|
|
|
179
342
|
if other.__class__ is not self.__class__:
|
|
180
343
|
raise TypeError(f"copy_from expects {self.__class__.__name__}")
|
|
181
344
|
|
|
182
|
-
for f in
|
|
345
|
+
for f in _public_dataclass_fields(self):
|
|
183
346
|
src = getattr(other, f.name)
|
|
184
347
|
dst = getattr(self, f.name)
|
|
185
348
|
|
|
@@ -230,7 +393,7 @@ class SingleKeyBaseMeta(BaseMeta):
|
|
|
230
393
|
Raises:
|
|
231
394
|
TypeError: If the subclass does not define exactly one field.
|
|
232
395
|
"""
|
|
233
|
-
flds =
|
|
396
|
+
flds = _public_dataclass_fields(cls)
|
|
234
397
|
if len(flds) != 1:
|
|
235
398
|
raise TypeError(
|
|
236
399
|
f"{cls.__name__} must define exactly one dataclass field (found {len(flds)})"
|
|
@@ -611,6 +774,10 @@ class MetaBlosc2(BaseMeta):
|
|
|
611
774
|
patch_size: Optional[list] = None
|
|
612
775
|
cparams: Optional[dict[str, Any]] = None
|
|
613
776
|
dparams: Optional[dict[str, Any]] = None
|
|
777
|
+
_PROTECTED_FIELDS = frozenset(
|
|
778
|
+
{"chunk_size", "block_size", "patch_size", "cparams", "dparams"}
|
|
779
|
+
)
|
|
780
|
+
_PROTECTED_FIELD_PREFIX = "meta.blosc2."
|
|
614
781
|
|
|
615
782
|
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
616
783
|
"""Validate and normalize tiling sizes.
|
|
@@ -620,24 +787,37 @@ class MetaBlosc2(BaseMeta):
|
|
|
620
787
|
spatial_ndims: Number of spatial dimensions.
|
|
621
788
|
**_: Unused extra context.
|
|
622
789
|
"""
|
|
790
|
+
self._remember_validation_context(
|
|
791
|
+
ndims=ndims,
|
|
792
|
+
spatial_ndims=spatial_ndims,
|
|
793
|
+
)
|
|
623
794
|
if self.chunk_size is not None:
|
|
624
|
-
|
|
625
|
-
_validate_float_int_list(
|
|
795
|
+
chunk_size = _cast_to_list(self.chunk_size, "meta.blosc2.chunk_size")
|
|
796
|
+
_validate_float_int_list(chunk_size, "meta.blosc2.chunk_size", ndims)
|
|
797
|
+
self.chunk_size = _FrozenList(chunk_size)
|
|
626
798
|
|
|
627
799
|
if self.block_size is not None:
|
|
628
|
-
|
|
629
|
-
_validate_float_int_list(
|
|
800
|
+
block_size = _cast_to_list(self.block_size, "meta.blosc2.block_size")
|
|
801
|
+
_validate_float_int_list(block_size, "meta.blosc2.block_size", ndims)
|
|
802
|
+
self.block_size = _FrozenList(block_size)
|
|
630
803
|
|
|
631
804
|
if self.patch_size is not None:
|
|
632
805
|
spatial_ndims = ndims if spatial_ndims is None else spatial_ndims
|
|
633
|
-
|
|
634
|
-
_validate_float_int_list(
|
|
806
|
+
patch_size = _cast_to_list(self.patch_size, "meta.blosc2.patch_size")
|
|
807
|
+
_validate_float_int_list(
|
|
808
|
+
patch_size,
|
|
809
|
+
"meta.blosc2.patch_size",
|
|
810
|
+
spatial_ndims,
|
|
811
|
+
)
|
|
812
|
+
self.patch_size = _FrozenList(patch_size)
|
|
635
813
|
|
|
636
814
|
if self.cparams is not None:
|
|
637
|
-
|
|
815
|
+
cparams = _cast_to_jsonable_mapping(self.cparams, "meta.blosc2.cparams")
|
|
816
|
+
self.cparams = _freeze_jsonable(cparams)
|
|
638
817
|
|
|
639
818
|
if self.dparams is not None:
|
|
640
|
-
|
|
819
|
+
dparams = _cast_to_jsonable_mapping(self.dparams, "meta.blosc2.dparams")
|
|
820
|
+
self.dparams = _freeze_jsonable(dparams)
|
|
641
821
|
|
|
642
822
|
|
|
643
823
|
class AxisLabelEnum(str, Enum):
|
|
@@ -676,6 +856,7 @@ class MetaSpatial(BaseMeta):
|
|
|
676
856
|
spacing: Per-dimension spacing values. Length must match ndims.
|
|
677
857
|
origin: Per-dimension origin values. Length must match ndims.
|
|
678
858
|
direction: Direction cosine matrix of shape [ndims, ndims].
|
|
859
|
+
affine: Homogeneous affine matrix of shape [ndims + 1, ndims + 1].
|
|
679
860
|
shape: Array shape. Length must match (spatial + non-spatial) ndims.
|
|
680
861
|
axis_labels: Per-axis labels or roles. Length must match ndims.
|
|
681
862
|
axis_units: Per-axis units. Length must match ndims.
|
|
@@ -686,11 +867,14 @@ class MetaSpatial(BaseMeta):
|
|
|
686
867
|
spacing: Optional[list[Union[int,float]]] = None
|
|
687
868
|
origin: Optional[list[Union[int,float]]] = None
|
|
688
869
|
direction: Optional[list[list[Union[int,float]]]] = None
|
|
870
|
+
affine: Optional[list[list[Union[int,float]]]] = None
|
|
689
871
|
shape: Optional[list[int]] = None
|
|
690
872
|
axis_labels: Optional[list[Union[str,AxisLabel]]] = None
|
|
691
873
|
axis_units: Optional[list[str]] = None
|
|
692
874
|
_num_spatial_axes: Optional[int] = None
|
|
693
875
|
_num_non_spatial_axes: Optional[int] = None
|
|
876
|
+
_PROTECTED_FIELDS = frozenset({"shape"})
|
|
877
|
+
_PROTECTED_FIELD_PREFIX = "meta.spatial."
|
|
694
878
|
|
|
695
879
|
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
696
880
|
"""Validate and normalize spatial fields.
|
|
@@ -700,6 +884,20 @@ class MetaSpatial(BaseMeta):
|
|
|
700
884
|
spatial_ndims: Number of spatial dimensions.
|
|
701
885
|
**_: Unused extra context.
|
|
702
886
|
"""
|
|
887
|
+
self._remember_validation_context(
|
|
888
|
+
ndims=ndims,
|
|
889
|
+
spatial_ndims=spatial_ndims,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
if self.affine is not None and (
|
|
893
|
+
self.spacing is not None
|
|
894
|
+
or self.origin is not None
|
|
895
|
+
or self.direction is not None
|
|
896
|
+
):
|
|
897
|
+
raise ValueError(
|
|
898
|
+
"meta.spatial.affine cannot be set together with spacing, origin, or direction."
|
|
899
|
+
)
|
|
900
|
+
|
|
703
901
|
if self.axis_labels is not None:
|
|
704
902
|
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)
|
|
705
903
|
spatial_ndims = spatial_ndims if self._num_spatial_axes is None else self._num_spatial_axes
|
|
@@ -716,9 +914,50 @@ class MetaSpatial(BaseMeta):
|
|
|
716
914
|
self.direction = _cast_to_list(self.direction, "meta.spatial.direction")
|
|
717
915
|
_validate_float_int_matrix(self.direction, "meta.spatial.direction", spatial_ndims)
|
|
718
916
|
|
|
917
|
+
if self.affine is not None:
|
|
918
|
+
self.affine = _cast_to_list(self.affine, "meta.spatial.affine")
|
|
919
|
+
if spatial_ndims is not None:
|
|
920
|
+
_validate_float_int_matrix(
|
|
921
|
+
self.affine,
|
|
922
|
+
"meta.spatial.affine",
|
|
923
|
+
spatial_ndims + 1,
|
|
924
|
+
)
|
|
925
|
+
else:
|
|
926
|
+
_validate_float_int_matrix(self.affine, "meta.spatial.affine")
|
|
927
|
+
n_rows = len(self.affine)
|
|
928
|
+
for row in self.affine:
|
|
929
|
+
if len(row) != n_rows:
|
|
930
|
+
raise ValueError("meta.spatial.affine must be a square matrix")
|
|
931
|
+
|
|
719
932
|
if self.shape is not None:
|
|
720
|
-
|
|
721
|
-
_validate_float_int_list(
|
|
933
|
+
shape = _cast_to_list(self.shape, "meta.spatial.shape")
|
|
934
|
+
_validate_float_int_list(shape, "meta.spatial.shape", ndims)
|
|
935
|
+
self.shape = _FrozenList(shape)
|
|
936
|
+
|
|
937
|
+
def copy_from(self, other: "MetaSpatial", *, overwrite: bool = False) -> None:
|
|
938
|
+
if other.__class__ is not self.__class__:
|
|
939
|
+
raise TypeError(f"copy_from expects {self.__class__.__name__}")
|
|
940
|
+
|
|
941
|
+
for f in _public_dataclass_fields(self):
|
|
942
|
+
if f.name == "shape" and not _is_meta_internal_write():
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
src = getattr(other, f.name)
|
|
946
|
+
dst = getattr(self, f.name)
|
|
947
|
+
|
|
948
|
+
if overwrite:
|
|
949
|
+
setattr(self, f.name, src)
|
|
950
|
+
continue
|
|
951
|
+
|
|
952
|
+
if isinstance(dst, BaseMeta) and isinstance(src, BaseMeta):
|
|
953
|
+
if dst.is_default():
|
|
954
|
+
setattr(self, f.name, src)
|
|
955
|
+
else:
|
|
956
|
+
dst.copy_from(src, overwrite=False)
|
|
957
|
+
continue
|
|
958
|
+
|
|
959
|
+
if _is_unset_value(dst):
|
|
960
|
+
setattr(self, f.name, src)
|
|
722
961
|
|
|
723
962
|
|
|
724
963
|
@dataclass(slots=True)
|
|
@@ -871,6 +1110,8 @@ class MetaHasArray(SingleKeyBaseMeta):
|
|
|
871
1110
|
has_array: True when array data is present.
|
|
872
1111
|
"""
|
|
873
1112
|
has_array: bool = False
|
|
1113
|
+
_PROTECTED_FIELDS = frozenset({"has_array"})
|
|
1114
|
+
_PROTECTED_FIELD_PREFIX = "meta._has_array."
|
|
874
1115
|
|
|
875
1116
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
876
1117
|
"""Validate has_array as bool."""
|
|
@@ -886,6 +1127,8 @@ class MetaImageFormat(SingleKeyBaseMeta):
|
|
|
886
1127
|
image_meta_format: Format identifier, or None.
|
|
887
1128
|
"""
|
|
888
1129
|
image_meta_format: Optional[str] = None
|
|
1130
|
+
_PROTECTED_FIELDS = frozenset({"image_meta_format"})
|
|
1131
|
+
_PROTECTED_FIELD_PREFIX = "meta._image_meta_format."
|
|
889
1132
|
|
|
890
1133
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
891
1134
|
"""Validate image_meta_format as str or None."""
|
|
@@ -901,6 +1144,8 @@ class MetaVersion(SingleKeyBaseMeta):
|
|
|
901
1144
|
mlarray_version: Version string, or None.
|
|
902
1145
|
"""
|
|
903
1146
|
mlarray_version: Optional[str] = None
|
|
1147
|
+
_PROTECTED_FIELDS = frozenset({"mlarray_version"})
|
|
1148
|
+
_PROTECTED_FIELD_PREFIX = "meta._mlarray_version."
|
|
904
1149
|
|
|
905
1150
|
def _validate_and_cast(self, **_: Any) -> None:
|
|
906
1151
|
"""Validate mlarray_version as str or None."""
|
|
@@ -915,7 +1160,7 @@ class Meta(BaseMeta):
|
|
|
915
1160
|
Attributes:
|
|
916
1161
|
source: Source metadata from the original image source (JSON-serializable dict).
|
|
917
1162
|
extra: Additional metadata (JSON-serializable dict).
|
|
918
|
-
spatial: Spatial metadata (spacing, origin, direction, shape).
|
|
1163
|
+
spatial: Spatial metadata (spacing, origin, direction, affine, shape).
|
|
919
1164
|
stats: Summary statistics.
|
|
920
1165
|
bbox: Bounding boxes.
|
|
921
1166
|
is_seg: Segmentation flag.
|
|
@@ -934,6 +1179,10 @@ class Meta(BaseMeta):
|
|
|
934
1179
|
_has_array: "MetaHasArray" = field(default_factory=lambda: MetaHasArray())
|
|
935
1180
|
_image_meta_format: "MetaImageFormat" = field(default_factory=lambda: MetaImageFormat())
|
|
936
1181
|
_mlarray_version: "MetaVersion" = field(default_factory=lambda: MetaVersion())
|
|
1182
|
+
_USER_FIELDS = ("source", "extra", "spatial", "stats", "bbox", "is_seg")
|
|
1183
|
+
_INTERNAL_FIELDS = ("blosc2", "_has_array", "_image_meta_format", "_mlarray_version")
|
|
1184
|
+
_PROTECTED_FIELDS = frozenset(_INTERNAL_FIELDS)
|
|
1185
|
+
_PROTECTED_FIELD_PREFIX = "meta."
|
|
937
1186
|
|
|
938
1187
|
def _validate_and_cast(self, *, ndims: Optional[int] = None, spatial_ndims: Optional[int] = None, **_: Any) -> None:
|
|
939
1188
|
"""Coerce child metas and validate with optional context.
|
|
@@ -943,20 +1192,141 @@ class Meta(BaseMeta):
|
|
|
943
1192
|
spatial_ndims: Number of spatial dimensions for context-aware validation.
|
|
944
1193
|
**_: Unused extra context.
|
|
945
1194
|
"""
|
|
946
|
-
self.
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
self
|
|
951
|
-
self
|
|
952
|
-
self
|
|
953
|
-
self
|
|
954
|
-
self
|
|
955
|
-
self
|
|
1195
|
+
self._remember_validation_context(
|
|
1196
|
+
ndims=ndims,
|
|
1197
|
+
spatial_ndims=spatial_ndims,
|
|
1198
|
+
)
|
|
1199
|
+
object.__setattr__(self, "source", MetaSource.ensure(self.source))
|
|
1200
|
+
object.__setattr__(self, "extra", MetaExtra.ensure(self.extra))
|
|
1201
|
+
object.__setattr__(self, "spatial", MetaSpatial.ensure(self.spatial))
|
|
1202
|
+
object.__setattr__(self, "stats", MetaStatistics.ensure(self.stats))
|
|
1203
|
+
object.__setattr__(self, "bbox", MetaBbox.ensure(self.bbox))
|
|
1204
|
+
object.__setattr__(self, "is_seg", MetaIsSeg.ensure(self.is_seg))
|
|
1205
|
+
object.__setattr__(self, "blosc2", MetaBlosc2.ensure(self.blosc2))
|
|
1206
|
+
object.__setattr__(self, "_has_array", MetaHasArray.ensure(self._has_array))
|
|
1207
|
+
object.__setattr__(
|
|
1208
|
+
self,
|
|
1209
|
+
"_image_meta_format",
|
|
1210
|
+
MetaImageFormat.ensure(self._image_meta_format),
|
|
1211
|
+
)
|
|
1212
|
+
object.__setattr__(self, "_mlarray_version", MetaVersion.ensure(self._mlarray_version))
|
|
956
1213
|
|
|
957
1214
|
self.spatial._validate_and_cast(ndims=ndims, spatial_ndims=spatial_ndims)
|
|
958
1215
|
self.blosc2._validate_and_cast(ndims=ndims, spatial_ndims=spatial_ndims)
|
|
959
1216
|
|
|
1217
|
+
def copy_from(self, other: "Meta", *, overwrite: bool = False) -> None:
|
|
1218
|
+
if other.__class__ is not self.__class__:
|
|
1219
|
+
raise TypeError(f"copy_from expects {self.__class__.__name__}")
|
|
1220
|
+
|
|
1221
|
+
field_names = list(self._USER_FIELDS)
|
|
1222
|
+
if _is_meta_internal_write():
|
|
1223
|
+
field_names.extend(self._INTERNAL_FIELDS)
|
|
1224
|
+
|
|
1225
|
+
for name in field_names:
|
|
1226
|
+
src = getattr(other, name)
|
|
1227
|
+
dst = getattr(self, name)
|
|
1228
|
+
|
|
1229
|
+
if overwrite:
|
|
1230
|
+
object.__setattr__(self, name, src)
|
|
1231
|
+
continue
|
|
1232
|
+
|
|
1233
|
+
if isinstance(dst, BaseMeta) and isinstance(src, BaseMeta):
|
|
1234
|
+
if dst.is_default():
|
|
1235
|
+
object.__setattr__(self, name, src)
|
|
1236
|
+
else:
|
|
1237
|
+
dst.copy_from(src, overwrite=False)
|
|
1238
|
+
continue
|
|
1239
|
+
|
|
1240
|
+
if _is_unset_value(dst):
|
|
1241
|
+
object.__setattr__(self, name, src)
|
|
1242
|
+
|
|
1243
|
+
def set_source(self, value: Union["MetaSource", Mapping[str, Any]]) -> "Meta":
|
|
1244
|
+
object.__setattr__(self, "source", MetaSource.ensure(value))
|
|
1245
|
+
return self
|
|
1246
|
+
|
|
1247
|
+
def set_extra(self, value: Union["MetaExtra", Mapping[str, Any]]) -> "Meta":
|
|
1248
|
+
object.__setattr__(self, "extra", MetaExtra.ensure(value))
|
|
1249
|
+
return self
|
|
1250
|
+
|
|
1251
|
+
def set_spatial(self, value: Union["MetaSpatial", Mapping[str, Any]]) -> "Meta":
|
|
1252
|
+
spatial = MetaSpatial.ensure(value)
|
|
1253
|
+
if spatial.shape is not None and not _is_meta_internal_write():
|
|
1254
|
+
_raise_internal_only("meta.spatial.shape")
|
|
1255
|
+
object.__setattr__(self, "spatial", spatial)
|
|
1256
|
+
return self
|
|
1257
|
+
|
|
1258
|
+
def set_stats(self, value: Union["MetaStatistics", Mapping[str, Any]]) -> "Meta":
|
|
1259
|
+
object.__setattr__(self, "stats", MetaStatistics.ensure(value))
|
|
1260
|
+
return self
|
|
1261
|
+
|
|
1262
|
+
def set_bbox(
|
|
1263
|
+
self,
|
|
1264
|
+
value: Union["MetaBbox", Mapping[str, Any], list[list[list[Union[int, float]]]]],
|
|
1265
|
+
) -> "Meta":
|
|
1266
|
+
bbox = MetaBbox.ensure(value) if isinstance(value, (MetaBbox, Mapping)) else MetaBbox(bboxes=value)
|
|
1267
|
+
object.__setattr__(self, "bbox", bbox)
|
|
1268
|
+
return self
|
|
1269
|
+
|
|
1270
|
+
def set_is_seg(self, value: Union["MetaIsSeg", Optional[bool]]) -> "Meta":
|
|
1271
|
+
is_seg = (
|
|
1272
|
+
MetaIsSeg.ensure(value)
|
|
1273
|
+
if isinstance(value, (MetaIsSeg, Mapping))
|
|
1274
|
+
else MetaIsSeg(is_seg=value)
|
|
1275
|
+
)
|
|
1276
|
+
object.__setattr__(self, "is_seg", is_seg)
|
|
1277
|
+
return self
|
|
1278
|
+
|
|
1279
|
+
def update_source(self, value: Mapping[str, Any]) -> "Meta":
|
|
1280
|
+
if not isinstance(value, Mapping):
|
|
1281
|
+
raise TypeError("source update value must be a mapping")
|
|
1282
|
+
self.source.data.update(dict(value))
|
|
1283
|
+
self.source._validate_and_cast()
|
|
1284
|
+
return self
|
|
1285
|
+
|
|
1286
|
+
def update_extra(self, value: Mapping[str, Any]) -> "Meta":
|
|
1287
|
+
if not isinstance(value, Mapping):
|
|
1288
|
+
raise TypeError("extra update value must be a mapping")
|
|
1289
|
+
self.extra.data.update(dict(value))
|
|
1290
|
+
self.extra._validate_and_cast()
|
|
1291
|
+
return self
|
|
1292
|
+
|
|
1293
|
+
def add_bbox(
|
|
1294
|
+
self,
|
|
1295
|
+
bbox: list[list[Union[int, float]]],
|
|
1296
|
+
score: Optional[Union[int, float]] = None,
|
|
1297
|
+
label: Optional[Union[str, int, float]] = None,
|
|
1298
|
+
) -> "Meta":
|
|
1299
|
+
prev_n = 0 if self.bbox.bboxes is None else len(self.bbox.bboxes)
|
|
1300
|
+
if prev_n > 0 and self.bbox.scores is None and score is not None:
|
|
1301
|
+
raise ValueError(
|
|
1302
|
+
"Cannot add a scored bbox when existing bboxes have no scores."
|
|
1303
|
+
)
|
|
1304
|
+
if prev_n > 0 and self.bbox.labels is None and label is not None:
|
|
1305
|
+
raise ValueError(
|
|
1306
|
+
"Cannot add a labeled bbox when existing bboxes have no labels."
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
if self.bbox.bboxes is None:
|
|
1310
|
+
self.bbox.bboxes = []
|
|
1311
|
+
self.bbox.bboxes.append(_cast_to_list(bbox, "meta.bbox.bbox"))
|
|
1312
|
+
|
|
1313
|
+
if score is not None:
|
|
1314
|
+
if self.bbox.scores is None:
|
|
1315
|
+
self.bbox.scores = []
|
|
1316
|
+
self.bbox.scores.append(score)
|
|
1317
|
+
elif self.bbox.scores is not None:
|
|
1318
|
+
raise ValueError("score must be provided because meta.bbox.scores already exists")
|
|
1319
|
+
|
|
1320
|
+
if label is not None:
|
|
1321
|
+
if self.bbox.labels is None:
|
|
1322
|
+
self.bbox.labels = []
|
|
1323
|
+
self.bbox.labels.append(label)
|
|
1324
|
+
elif self.bbox.labels is not None:
|
|
1325
|
+
raise ValueError("label must be provided because meta.bbox.labels already exists")
|
|
1326
|
+
|
|
1327
|
+
self.bbox._validate_and_cast()
|
|
1328
|
+
return self
|
|
1329
|
+
|
|
960
1330
|
def to_plain(self, *, include_none: bool = False) -> Any:
|
|
961
1331
|
"""Convert to plain values, suppressing default sub-metas.
|
|
962
1332
|
|
|
@@ -968,7 +1338,7 @@ class Meta(BaseMeta):
|
|
|
968
1338
|
as None and optionally filtered out.
|
|
969
1339
|
"""
|
|
970
1340
|
out: dict[str, Any] = {}
|
|
971
|
-
for f in
|
|
1341
|
+
for f in _public_dataclass_fields(self):
|
|
972
1342
|
v = getattr(self, f.name)
|
|
973
1343
|
|
|
974
1344
|
if isinstance(v, BaseMeta):
|