euler-loading 2.4.0__tar.gz → 2.6.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.
- {euler_loading-2.4.0 → euler_loading-2.6.0}/PKG-INFO +2 -2
- euler_loading-2.6.0/docs/loader-attributes.md +183 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/_resolution.py +27 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/_writing.py +28 -4
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/dataset.py +78 -5
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/contracts.py +6 -6
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/cpu/generic.py +112 -6
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/cpu/generic_dense_depth.py +14 -7
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/cpu/real_drive_sim.py +4 -4
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/cpu/vkitti2.py +8 -8
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/generate/loaders.json +102 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/gpu/generic.py +100 -6
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/gpu/generic_dense_depth.py +10 -5
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/gpu/real_drive_sim.py +7 -7
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/gpu/vkitti2.py +8 -8
- {euler_loading-2.4.0 → euler_loading-2.6.0}/package-lock.json +1 -1
- {euler_loading-2.4.0 → euler_loading-2.6.0}/pyproject.toml +3 -2
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_dataset.py +551 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_loaders.py +291 -2
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_writing.py +123 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/.github/workflows/workflow.yml +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/.gitignore +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/README.md +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/_dataset_contract.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/_ds_crawler_utils.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/_metadata.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/indexing.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/_annotations.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/_writer_utils.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/cpu/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/generate/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/generate/__main__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/generic.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/gpu/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/real_drive_sim.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/loaders/vkitti2.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/euler_loading/preprocessing.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/example.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/sample_rds.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/__init__.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/conftest.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/example_rds_calib.json +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_id_schema.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_indexing.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_preprocessing.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/tests/test_real_dataset.py +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/vkitti_cpu_example_output.json +0 -0
- {euler_loading-2.4.0 → euler_loading-2.6.0}/vkitti_gpu_example_output.json +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-loading
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Multi-modal PyTorch dataloader using ds-crawler indices
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
|
-
Requires-Dist: ds-crawler
|
|
6
|
+
Requires-Dist: ds-crawler>=2.3.0
|
|
7
7
|
Requires-Dist: euler-dataset-contract
|
|
8
8
|
Requires-Dist: numpy
|
|
9
9
|
Requires-Dist: pillow
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Design note: passing per-file `attributes` to loaders
|
|
2
|
+
|
|
3
|
+
Status: **implemented**
|
|
4
|
+
|
|
5
|
+
This is a follow-up to the per-file `attributes` field added in
|
|
6
|
+
ds-crawler (`file_entry["attributes"]`) and surfaced on samples in
|
|
7
|
+
euler-loading (`sample["attributes"][modality]` and
|
|
8
|
+
`sample["meta"][modality]["attributes"]`).
|
|
9
|
+
|
|
10
|
+
That change persists arbitrary per-file metadata through the index
|
|
11
|
+
schema and exposes it on the loaded sample. Loader callables can now opt
|
|
12
|
+
in to receiving the per-file dict by accepting an `attributes=` keyword.
|
|
13
|
+
Legacy loaders that accept only `(path, meta)` keep receiving the old
|
|
14
|
+
call shape.
|
|
15
|
+
|
|
16
|
+
## When this matters
|
|
17
|
+
|
|
18
|
+
Cases where the loader genuinely needs `attributes` rather than the
|
|
19
|
+
consumer reading it post-hoc on the sample:
|
|
20
|
+
|
|
21
|
+
- Per-file noise level / scale that must be applied during decode
|
|
22
|
+
(e.g. depth scale-to-meters that varies per scan).
|
|
23
|
+
- Per-file masks / clip ranges for normalization.
|
|
24
|
+
- Per-file lookup keys that select an external resource (e.g. a
|
|
25
|
+
per-file calibration tag pointing into a calibration database).
|
|
26
|
+
- Anything where the value can't be reconstructed once the file is
|
|
27
|
+
decoded into a tensor.
|
|
28
|
+
|
|
29
|
+
If `attributes` is purely descriptive (training weight, source tag,
|
|
30
|
+
log fields), keep it on `sample["attributes"][...]` and don't change
|
|
31
|
+
loader signatures.
|
|
32
|
+
|
|
33
|
+
## Loader signature
|
|
34
|
+
|
|
35
|
+
`euler_loading/loaders/contracts.py`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
def rgb(
|
|
39
|
+
self,
|
|
40
|
+
path: str | BinaryIO,
|
|
41
|
+
meta: dict[str, Any] | None = None,
|
|
42
|
+
*,
|
|
43
|
+
attributes: dict[str, Any] | None = None,
|
|
44
|
+
) -> torch.Tensor: ...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Implemented behavior:
|
|
48
|
+
|
|
49
|
+
- `dataset.py` probes each resolved loader once with `inspect.signature`
|
|
50
|
+
and caches whether it accepts `attributes` or `**kwargs`.
|
|
51
|
+
- Compatible loaders receive a copied per-file attributes dict (or
|
|
52
|
+
`None` when the file entry has no attributes).
|
|
53
|
+
- Legacy loaders that do not accept the keyword receive the original
|
|
54
|
+
`(path, meta)` call.
|
|
55
|
+
- Hierarchical-modality cache keys include the attributes payload for
|
|
56
|
+
opted-in loaders, so the same path with different attributes can be
|
|
57
|
+
decoded differently.
|
|
58
|
+
- Built-in CPU/GPU loaders accept `attributes=None`. Most ignore it;
|
|
59
|
+
`generic_dense_depth.depth` consumes
|
|
60
|
+
`attributes["scale_to_meters_override"]` when present.
|
|
61
|
+
|
|
62
|
+
### Rejected alternative: merge into `meta`
|
|
63
|
+
|
|
64
|
+
Extend `meta` from "modality-level meta" to "merged meta" — keep the
|
|
65
|
+
original keys plus a reserved `meta["__attributes__"]` (or
|
|
66
|
+
`meta["entry"]`) sub-dict. No signature change.
|
|
67
|
+
|
|
68
|
+
Pros: no signature change, no probe.
|
|
69
|
+
Cons: namespace collision risk; loaders parsing `meta` in surprising
|
|
70
|
+
ways may misinterpret the new key. Hides the source of each field.
|
|
71
|
+
Harder to type. Implicit contract.
|
|
72
|
+
|
|
73
|
+
### Rejected alternative: pass the full file entry
|
|
74
|
+
|
|
75
|
+
Replace `meta` with the file entry dict (carrying `attributes`,
|
|
76
|
+
`path_properties`, etc.). Strictly more information, but breaks every
|
|
77
|
+
existing loader.
|
|
78
|
+
|
|
79
|
+
Pros: maximum flexibility.
|
|
80
|
+
Cons: gratuitous churn, explicit migration of every loader.
|
|
81
|
+
|
|
82
|
+
## Implementation notes
|
|
83
|
+
|
|
84
|
+
### 1. Update contracts
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# euler_loading/loaders/contracts.py
|
|
88
|
+
class DenseDepthLoader(Protocol):
|
|
89
|
+
def rgb(
|
|
90
|
+
self,
|
|
91
|
+
path: Union[str, BinaryIO],
|
|
92
|
+
meta: dict[str, Any] | None = None,
|
|
93
|
+
*,
|
|
94
|
+
attributes: dict[str, Any] | None = None,
|
|
95
|
+
) -> torch.Tensor: ...
|
|
96
|
+
# ... and parallel updates to depth / sky_mask / read_intrinsics ...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. Loader feature probe
|
|
100
|
+
|
|
101
|
+
The utility that identifies whether each loader accepts the
|
|
102
|
+
`attributes` kwarg:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# euler_loading/_resolution.py
|
|
106
|
+
import inspect
|
|
107
|
+
|
|
108
|
+
def loader_accepts_attributes(loader: Callable[..., Any]) -> bool:
|
|
109
|
+
try:
|
|
110
|
+
sig = inspect.signature(loader)
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
return False
|
|
113
|
+
params = sig.parameters
|
|
114
|
+
if "attributes" in params:
|
|
115
|
+
return True
|
|
116
|
+
return any(
|
|
117
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Cache the result on the resolved-loaders dict at construction time so
|
|
122
|
+
the probe runs once per modality.
|
|
123
|
+
|
|
124
|
+
### 3. Update call sites
|
|
125
|
+
|
|
126
|
+
In `MultiModalDataset.__getitem__`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
loader = self._resolved_loaders[name]
|
|
130
|
+
file_attrs = _get_file_attributes(record.file_entry)
|
|
131
|
+
if self._loaders_accept_attributes[name]:
|
|
132
|
+
sample[name] = loader(file_or_path, modality_meta, attributes=file_attrs)
|
|
133
|
+
else:
|
|
134
|
+
sample[name] = loader(file_or_path, modality_meta)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The hierarchical-modality branch follows the same rule. For opted-in
|
|
138
|
+
loaders, the hierarchical cache key includes a serialized attributes
|
|
139
|
+
fragment so identical paths with different per-file attributes are
|
|
140
|
+
loaded independently.
|
|
141
|
+
|
|
142
|
+
### 4. Update built-in loaders
|
|
143
|
+
|
|
144
|
+
For each module under `euler_loading/loaders/{cpu,gpu}/`, every `read_*`
|
|
145
|
+
and modality function accepts `*, attributes: ... = None`. Most ignore
|
|
146
|
+
it. `generic_dense_depth.depth` consumes
|
|
147
|
+
`attributes.get("scale_to_meters_override")`.
|
|
148
|
+
|
|
149
|
+
### 5. Tests
|
|
150
|
+
|
|
151
|
+
Mirror the existing `tests/test_writing.py` and
|
|
152
|
+
`tests/test_dataset.py` structure. Minimum coverage:
|
|
153
|
+
|
|
154
|
+
- A loader that accepts `attributes=` receives the per-file dict.
|
|
155
|
+
- A loader that doesn't is called with the legacy 2-arg form (no
|
|
156
|
+
`TypeError`).
|
|
157
|
+
- A loader with `**kwargs` is treated as accepting attributes.
|
|
158
|
+
- Hierarchical-modality cache: same hierarchical file with two different
|
|
159
|
+
attributes loads twice.
|
|
160
|
+
- Round-trip: file entry attributes → loader receives them → if the
|
|
161
|
+
loader passes them through, they appear on `sample["attributes"]`
|
|
162
|
+
(already the case post the existing change, but pin it).
|
|
163
|
+
|
|
164
|
+
## Backwards compatibility
|
|
165
|
+
|
|
166
|
+
- Stability of the existing 2-arg signature is preserved by feature
|
|
167
|
+
probing. Loaders that never opt in keep working forever.
|
|
168
|
+
- The `attributes` argument is keyword-only — no position collision.
|
|
169
|
+
- `sample["attributes"]` and `sample["meta"][name]["attributes"]`
|
|
170
|
+
surfaces are unchanged by this work; they already exist.
|
|
171
|
+
|
|
172
|
+
## Open questions for the implementer
|
|
173
|
+
|
|
174
|
+
1. Should the feature probe also be done for *writers*? Today writers
|
|
175
|
+
take `(path, value, meta)`. If we want symmetry, writers gain
|
|
176
|
+
`attributes=` too. Same Option 1 treatment.
|
|
177
|
+
2. Should `loader_accepts_attributes` look at the function's
|
|
178
|
+
`__wrapped__` chain to handle `functools.wraps`-decorated loaders?
|
|
179
|
+
(Probably yes, but only if a real case appears.)
|
|
180
|
+
3. Worth adding a `FileContext` dataclass — `(meta, attributes,
|
|
181
|
+
file_entry)` — to consolidate future fields rather than growing kwargs
|
|
182
|
+
one at a time? Defer this until we have a second per-file thing to
|
|
183
|
+
add; one new field doesn't justify a wrapper type.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
|
+
import inspect
|
|
4
5
|
import logging
|
|
5
6
|
from collections.abc import Mapping
|
|
6
7
|
from types import ModuleType
|
|
@@ -68,6 +69,32 @@ def resolve_writer_module(name: str) -> ModuleType:
|
|
|
68
69
|
return resolve_loader_module(name)
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def loader_accepts_attributes(loader: Callable[..., Any]) -> bool:
|
|
73
|
+
"""Return whether *loader* can accept an ``attributes=`` keyword.
|
|
74
|
+
|
|
75
|
+
Loaders historically accepted ``(path, meta=None)``. Per-file
|
|
76
|
+
attributes are opt-in so legacy loaders keep receiving the old call
|
|
77
|
+
shape, while loaders declaring ``attributes`` or ``**kwargs`` receive
|
|
78
|
+
the new context.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
signature = inspect.signature(loader)
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
attributes_param = signature.parameters.get("attributes")
|
|
86
|
+
if (
|
|
87
|
+
attributes_param is not None
|
|
88
|
+
and attributes_param.kind is not inspect.Parameter.POSITIONAL_ONLY
|
|
89
|
+
):
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
return any(
|
|
93
|
+
param.kind is inspect.Parameter.VAR_KEYWORD
|
|
94
|
+
for param in signature.parameters.values()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
71
98
|
def _resolve_loader(
|
|
72
99
|
*,
|
|
73
100
|
modality_name: str,
|
|
@@ -244,15 +244,29 @@ def _write_value_to_destination(
|
|
|
244
244
|
full_id: str,
|
|
245
245
|
basename: str,
|
|
246
246
|
relative_path: str,
|
|
247
|
-
|
|
247
|
+
source_entry: Mapping[str, Any] | None,
|
|
248
|
+
entry_attributes: Mapping[str, Any] | None = None,
|
|
248
249
|
create_dirs: bool,
|
|
249
250
|
) -> str:
|
|
251
|
+
"""Write *value* to the destination and (when applicable) record a
|
|
252
|
+
ds-crawler file entry.
|
|
253
|
+
|
|
254
|
+
``meta`` is the modality-level metadata passed to the loader/writer
|
|
255
|
+
callable. ``entry_attributes`` is the per-file ``attributes`` dict
|
|
256
|
+
written onto the ds-crawler file entry; it is a no-op for plain
|
|
257
|
+
filesystem destinations (no index to record into).
|
|
258
|
+
"""
|
|
259
|
+
entry_attributes_dict = (
|
|
260
|
+
dict(entry_attributes) if entry_attributes is not None else None
|
|
261
|
+
)
|
|
262
|
+
|
|
250
263
|
if isinstance(destination, ZipDatasetWriter):
|
|
251
264
|
if supports_stream_target(writer):
|
|
252
265
|
with destination.open(
|
|
253
266
|
full_id,
|
|
254
267
|
basename,
|
|
255
|
-
|
|
268
|
+
source_entry=dict(source_entry or {}),
|
|
269
|
+
attributes=entry_attributes_dict,
|
|
256
270
|
) as stream:
|
|
257
271
|
_set_stream_name(stream, basename)
|
|
258
272
|
writer(stream, value, meta)
|
|
@@ -264,7 +278,8 @@ def _write_value_to_destination(
|
|
|
264
278
|
full_id,
|
|
265
279
|
basename,
|
|
266
280
|
temp_path.read_bytes(),
|
|
267
|
-
|
|
281
|
+
source_entry=dict(source_entry or {}),
|
|
282
|
+
attributes=entry_attributes_dict,
|
|
268
283
|
)
|
|
269
284
|
_register_destination_rel_path(destination, relative_path)
|
|
270
285
|
return _destination_location(destination, relative_path)
|
|
@@ -273,12 +288,21 @@ def _write_value_to_destination(
|
|
|
273
288
|
target_path = destination.get_path(
|
|
274
289
|
full_id,
|
|
275
290
|
basename,
|
|
276
|
-
|
|
291
|
+
source_entry=dict(source_entry or {}),
|
|
292
|
+
attributes=entry_attributes_dict,
|
|
277
293
|
)
|
|
278
294
|
writer(str(target_path), value, meta)
|
|
279
295
|
_register_destination_rel_path(destination, relative_path)
|
|
280
296
|
return str(target_path)
|
|
281
297
|
|
|
298
|
+
if entry_attributes_dict:
|
|
299
|
+
logger.debug(
|
|
300
|
+
"Filesystem destination %s has no index — entry attributes "
|
|
301
|
+
"for %s will not be persisted.",
|
|
302
|
+
destination,
|
|
303
|
+
relative_path,
|
|
304
|
+
)
|
|
305
|
+
|
|
282
306
|
target_path = Path(destination) / relative_path
|
|
283
307
|
if create_dirs:
|
|
284
308
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -29,6 +30,7 @@ from ._metadata import _build_runlog_entry, _get_ds_crawler_descriptor
|
|
|
29
30
|
from ._resolution import (
|
|
30
31
|
_resolve_loader,
|
|
31
32
|
_resolve_writer,
|
|
33
|
+
loader_accepts_attributes,
|
|
32
34
|
resolve_loader_module,
|
|
33
35
|
resolve_writer_module,
|
|
34
36
|
)
|
|
@@ -118,6 +120,33 @@ def _get_index_meta(index_output: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
|
118
120
|
return None
|
|
119
121
|
|
|
120
122
|
|
|
123
|
+
def _get_file_attributes(file_entry: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
124
|
+
attributes = file_entry.get("attributes")
|
|
125
|
+
if isinstance(attributes, Mapping):
|
|
126
|
+
return dict(attributes)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _attributes_cache_key(attributes: Mapping[str, Any] | None) -> str:
|
|
131
|
+
if attributes is None:
|
|
132
|
+
return "null"
|
|
133
|
+
return json.dumps(attributes, sort_keys=True, separators=(",", ":"), default=repr)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_with_optional_attributes(
|
|
137
|
+
loader: Callable[..., Any],
|
|
138
|
+
file_or_path: Any,
|
|
139
|
+
meta: dict[str, Any] | None,
|
|
140
|
+
attributes: dict[str, Any] | None,
|
|
141
|
+
*,
|
|
142
|
+
accepts_attributes: bool,
|
|
143
|
+
) -> Any:
|
|
144
|
+
if accepts_attributes:
|
|
145
|
+
loader_attributes = dict(attributes) if attributes is not None else None
|
|
146
|
+
return loader(file_or_path, meta, attributes=loader_attributes)
|
|
147
|
+
return loader(file_or_path, meta)
|
|
148
|
+
|
|
149
|
+
|
|
121
150
|
def _extract_id_schema(index_output: Mapping[str, Any]) -> dict[str, Any]:
|
|
122
151
|
"""Pull id/hierarchy separators from a ds-crawler output payload.
|
|
123
152
|
|
|
@@ -394,6 +423,7 @@ class MultiModalDataset(_BaseDataset):
|
|
|
394
423
|
# -- Index each modality and build ID → FileRecord lookups -----------
|
|
395
424
|
self._lookups: dict[str, dict[str, FileRecord]] = {}
|
|
396
425
|
self._resolved_loaders: dict[str, Callable[..., Any]] = {}
|
|
426
|
+
self._loaders_accept_attributes: dict[str, bool] = {}
|
|
397
427
|
self._resolved_writers: dict[str, Callable[..., Any] | None] = {}
|
|
398
428
|
|
|
399
429
|
for name, modality in modalities.items():
|
|
@@ -406,6 +436,9 @@ class MultiModalDataset(_BaseDataset):
|
|
|
406
436
|
self._resolved_loaders[name] = _resolve_loader(
|
|
407
437
|
modality_name=name, modality=modality, index=index,
|
|
408
438
|
)
|
|
439
|
+
self._loaders_accept_attributes[name] = loader_accepts_attributes(
|
|
440
|
+
self._resolved_loaders[name]
|
|
441
|
+
)
|
|
409
442
|
self._resolved_writers[name] = _resolve_writer(
|
|
410
443
|
modality_name=name, modality=modality, index=index,
|
|
411
444
|
)
|
|
@@ -438,6 +471,9 @@ class MultiModalDataset(_BaseDataset):
|
|
|
438
471
|
self._resolved_loaders[name] = _resolve_loader(
|
|
439
472
|
modality_name=name, modality=modality, index=index,
|
|
440
473
|
)
|
|
474
|
+
self._loaders_accept_attributes[name] = loader_accepts_attributes(
|
|
475
|
+
self._resolved_loaders[name]
|
|
476
|
+
)
|
|
441
477
|
self._resolved_writers[name] = _resolve_writer(
|
|
442
478
|
modality_name=name, modality=modality, index=index,
|
|
443
479
|
)
|
|
@@ -623,6 +659,7 @@ class MultiModalDataset(_BaseDataset):
|
|
|
623
659
|
*,
|
|
624
660
|
create_dirs: bool = True,
|
|
625
661
|
overwrite: bool = True,
|
|
662
|
+
attributes: Mapping[str, Mapping[str, Any]] | None = None,
|
|
626
663
|
) -> dict[str, str]:
|
|
627
664
|
"""Write model outputs for one sample to disk or a dataset writer.
|
|
628
665
|
|
|
@@ -640,6 +677,13 @@ class MultiModalDataset(_BaseDataset):
|
|
|
640
677
|
filesystem destinations.
|
|
641
678
|
overwrite: If false, raise when a target file already exists or a
|
|
642
679
|
duplicate writer entry would be created.
|
|
680
|
+
attributes: Optional per-modality ``{modality: {key: value}}``
|
|
681
|
+
mapping recorded on the destination's ds-crawler file
|
|
682
|
+
entry under the ``"attributes"`` field. Only applies when
|
|
683
|
+
the destination is a :class:`DatasetWriter` /
|
|
684
|
+
:class:`ZipDatasetWriter`; ignored for plain filesystem
|
|
685
|
+
paths. When omitted, any ``attributes`` carried on the
|
|
686
|
+
source file entry is inherited.
|
|
643
687
|
|
|
644
688
|
Returns:
|
|
645
689
|
Mapping ``{modality_name: written_location}``. For filesystem
|
|
@@ -677,6 +721,9 @@ class MultiModalDataset(_BaseDataset):
|
|
|
677
721
|
if _destination_rel_exists(destination, relative_path) and not overwrite:
|
|
678
722
|
raise FileExistsError(_destination_location(destination, relative_path))
|
|
679
723
|
|
|
724
|
+
per_modality_attrs = (
|
|
725
|
+
attributes.get(modality_name) if attributes is not None else None
|
|
726
|
+
)
|
|
680
727
|
written_paths[modality_name] = _write_value_to_destination(
|
|
681
728
|
destination=destination,
|
|
682
729
|
writer=writer,
|
|
@@ -685,7 +732,8 @@ class MultiModalDataset(_BaseDataset):
|
|
|
685
732
|
full_id=full_id,
|
|
686
733
|
basename=basename,
|
|
687
734
|
relative_path=relative_path,
|
|
688
|
-
|
|
735
|
+
source_entry=record.file_entry,
|
|
736
|
+
entry_attributes=per_modality_attrs,
|
|
689
737
|
create_dirs=create_dirs,
|
|
690
738
|
)
|
|
691
739
|
|
|
@@ -764,6 +812,7 @@ class MultiModalDataset(_BaseDataset):
|
|
|
764
812
|
sample: dict[str, Any] = {}
|
|
765
813
|
|
|
766
814
|
meta: dict[str, dict[str, Any]] = {}
|
|
815
|
+
attributes: dict[str, dict[str, Any]] = {}
|
|
767
816
|
file_id: str = ""
|
|
768
817
|
hierarchy_path: tuple[str, ...] = ()
|
|
769
818
|
|
|
@@ -778,8 +827,16 @@ class MultiModalDataset(_BaseDataset):
|
|
|
778
827
|
else:
|
|
779
828
|
file_or_path = f"{modality.path}/{record.file_entry['path']}"
|
|
780
829
|
|
|
781
|
-
|
|
830
|
+
entry_attributes = _get_file_attributes(record.file_entry)
|
|
831
|
+
sample[name] = _load_with_optional_attributes(
|
|
832
|
+
self._resolved_loaders[name],
|
|
833
|
+
file_or_path,
|
|
834
|
+
modality_meta,
|
|
835
|
+
entry_attributes,
|
|
836
|
+
accepts_attributes=self._loaders_accept_attributes[name],
|
|
837
|
+
)
|
|
782
838
|
meta[name] = record.file_entry
|
|
839
|
+
attributes[name] = entry_attributes or {}
|
|
783
840
|
|
|
784
841
|
# Hierarchy path from the first modality that has one.
|
|
785
842
|
if not hierarchy_path:
|
|
@@ -792,24 +849,40 @@ class MultiModalDataset(_BaseDataset):
|
|
|
792
849
|
matched = match_hierarchical_files(hierarchy_path, files_by_level)
|
|
793
850
|
modality_meta = _get_index_meta(self._hierarchical_index_outputs[name])
|
|
794
851
|
loaded: dict[str, Any] = {}
|
|
852
|
+
attrs_for_modality: dict[str, dict[str, Any]] = {}
|
|
795
853
|
for entry in matched:
|
|
854
|
+
entry_attributes = _get_file_attributes(entry)
|
|
796
855
|
cache_key = f"{modality.path}/{entry['path']}"
|
|
856
|
+
accepts_attributes = self._loaders_accept_attributes[name]
|
|
857
|
+
if accepts_attributes:
|
|
858
|
+
cache_key = (
|
|
859
|
+
f"{cache_key}::attributes="
|
|
860
|
+
f"{_attributes_cache_key(entry_attributes)}"
|
|
861
|
+
)
|
|
797
862
|
if cache_key not in self._hierarchical_cache:
|
|
798
863
|
if name in self._zip_modalities:
|
|
799
864
|
file_or_path = self._open_from_zip(
|
|
800
865
|
name, modality.path, entry["path"],
|
|
801
866
|
)
|
|
802
867
|
else:
|
|
803
|
-
file_or_path =
|
|
804
|
-
self._hierarchical_cache[cache_key] =
|
|
805
|
-
|
|
868
|
+
file_or_path = f"{modality.path}/{entry['path']}"
|
|
869
|
+
self._hierarchical_cache[cache_key] = _load_with_optional_attributes(
|
|
870
|
+
self._resolved_loaders[name],
|
|
871
|
+
file_or_path,
|
|
872
|
+
modality_meta,
|
|
873
|
+
entry_attributes,
|
|
874
|
+
accepts_attributes=accepts_attributes,
|
|
806
875
|
)
|
|
807
876
|
loaded[entry["id"]] = self._hierarchical_cache[cache_key]
|
|
877
|
+
if entry_attributes:
|
|
878
|
+
attrs_for_modality[entry["id"]] = entry_attributes
|
|
808
879
|
sample[name] = loaded
|
|
880
|
+
attributes[name] = attrs_for_modality
|
|
809
881
|
|
|
810
882
|
sample["id"] = file_id
|
|
811
883
|
sample["full_id"] = "/" + "/".join(hierarchy_path + (file_id,))
|
|
812
884
|
sample["meta"] = meta
|
|
885
|
+
sample["attributes"] = attributes
|
|
813
886
|
|
|
814
887
|
for transform in self._transforms:
|
|
815
888
|
sample = transform(sample)
|
|
@@ -23,8 +23,8 @@ class DenseDepthLoader(Protocol):
|
|
|
23
23
|
"""Contract for dense-depth dataset loaders.
|
|
24
24
|
|
|
25
25
|
A conforming module must expose the following callables, each accepting
|
|
26
|
-
a file path
|
|
27
|
-
``torch.Tensor``:
|
|
26
|
+
a file path, an optional modality-level ``meta`` dict, and optional
|
|
27
|
+
per-file ``attributes`` keyword, and returning a ``torch.Tensor``:
|
|
28
28
|
|
|
29
29
|
- **rgb** – ``(3, H, W)`` float32 in ``[0, 1]``
|
|
30
30
|
- **depth** – ``(1, H, W)`` float32 in metres
|
|
@@ -32,10 +32,10 @@ class DenseDepthLoader(Protocol):
|
|
|
32
32
|
- **read_intrinsics** – ``(3, 3)`` float32 camera matrix *K*
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
def rgb(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
36
|
-
def depth(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
37
|
-
def sky_mask(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
38
|
-
def read_intrinsics(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
35
|
+
def rgb(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
36
|
+
def depth(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
37
|
+
def sky_mask(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
38
|
+
def read_intrinsics(self, path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor: ...
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@runtime_checkable
|
|
@@ -10,6 +10,8 @@ Supported file extensions:
|
|
|
10
10
|
|
|
11
11
|
Return types
|
|
12
12
|
------------
|
|
13
|
+
- **map_2d** / **scattering_coefficient** -- ``np.ndarray`` of shape ``(H, W)`` float32.
|
|
14
|
+
- **map_3d** / **atmospheric_light** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
|
|
13
15
|
- **spherical_map** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
|
|
14
16
|
- **intrinsics** -- ``np.ndarray`` of shape ``(3, 3)`` float32.
|
|
15
17
|
- **sh_coeffs** -- ``np.ndarray`` of shape ``(N, 3)`` float32.
|
|
@@ -19,9 +21,11 @@ Usage::
|
|
|
19
21
|
from euler_loading.loaders.cpu import generic
|
|
20
22
|
from euler_loading import Modality
|
|
21
23
|
|
|
22
|
-
Modality("/data/my_dataset/
|
|
23
|
-
Modality("/data/my_dataset/
|
|
24
|
-
Modality("/data/my_dataset/
|
|
24
|
+
Modality("/data/my_dataset/scattering_coefficient", loader=generic.scattering_coefficient)
|
|
25
|
+
Modality("/data/my_dataset/atmospheric_light", loader=generic.atmospheric_light)
|
|
26
|
+
Modality("/data/my_dataset/spherical_map", loader=generic.spherical_map)
|
|
27
|
+
Modality("/data/my_dataset/intrinsics", loader=generic.intrinsics)
|
|
28
|
+
Modality("/data/my_dataset/sh_coeffs", loader=generic.sh_coeffs)
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
31
|
from __future__ import annotations
|
|
@@ -36,6 +40,7 @@ from euler_loading.loaders._writer_utils import (
|
|
|
36
40
|
ensure_parent,
|
|
37
41
|
get_target_name,
|
|
38
42
|
mark_stream_supported,
|
|
43
|
+
to_hw,
|
|
39
44
|
to_numpy,
|
|
40
45
|
)
|
|
41
46
|
|
|
@@ -85,13 +90,69 @@ def _write_numpy(path: Union[str, BinaryIO], value: Any) -> None:
|
|
|
85
90
|
# ---------------------------------------------------------------------------
|
|
86
91
|
|
|
87
92
|
|
|
93
|
+
@modality_meta(
|
|
94
|
+
modality_type="map_2d",
|
|
95
|
+
dtype="float32",
|
|
96
|
+
shape="HW",
|
|
97
|
+
file_formats=[".npy", ".npz"],
|
|
98
|
+
)
|
|
99
|
+
def map_2d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
100
|
+
"""Load an arbitrary 2D map as an ``(H, W)`` float32 array.
|
|
101
|
+
|
|
102
|
+
Suitable for any single-channel dense quantity (e.g. scattering
|
|
103
|
+
coefficient, attenuation, opacity) where no dataset-specific decoding
|
|
104
|
+
is required.
|
|
105
|
+
"""
|
|
106
|
+
return _load_numpy(path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@modality_meta(
|
|
110
|
+
modality_type="map_3d",
|
|
111
|
+
dtype="float32",
|
|
112
|
+
shape="HWC",
|
|
113
|
+
file_formats=[".npy", ".npz"],
|
|
114
|
+
)
|
|
115
|
+
def map_3d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
116
|
+
"""Load an arbitrary 3D map as an ``(H, W, C)`` float32 array.
|
|
117
|
+
|
|
118
|
+
The file is expected to be stored in ``(C, H, W)`` layout and is
|
|
119
|
+
transposed to ``(H, W, C)`` for CPU-oriented processing. Suitable
|
|
120
|
+
for any per-pixel vector quantity (e.g. atmospheric light, surface
|
|
121
|
+
normals, flow).
|
|
122
|
+
"""
|
|
123
|
+
arr = _load_numpy(path)
|
|
124
|
+
return np.transpose(arr, (1, 2, 0))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@modality_meta(
|
|
128
|
+
modality_type="scattering_coefficient",
|
|
129
|
+
dtype="float32",
|
|
130
|
+
shape="HW",
|
|
131
|
+
file_formats=[".npy", ".npz"],
|
|
132
|
+
)
|
|
133
|
+
def scattering_coefficient(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
134
|
+
"""Load a scattering-coefficient map as an ``(H, W)`` float32 array."""
|
|
135
|
+
return map_2d(path, meta)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@modality_meta(
|
|
139
|
+
modality_type="atmospheric_light",
|
|
140
|
+
dtype="float32",
|
|
141
|
+
shape="HWC",
|
|
142
|
+
file_formats=[".npy", ".npz"],
|
|
143
|
+
)
|
|
144
|
+
def atmospheric_light(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
145
|
+
"""Load an atmospheric-light map as an ``(H, W, C)`` float32 array."""
|
|
146
|
+
return map_3d(path, meta)
|
|
147
|
+
|
|
148
|
+
|
|
88
149
|
@modality_meta(
|
|
89
150
|
modality_type="spherical_map",
|
|
90
151
|
dtype="float32",
|
|
91
152
|
shape="HWC",
|
|
92
153
|
file_formats=[".npy", ".npz"],
|
|
93
154
|
)
|
|
94
|
-
def spherical_map(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> np.ndarray:
|
|
155
|
+
def spherical_map(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
95
156
|
"""Load a spherical map as an ``(H, W, C)`` float32 array.
|
|
96
157
|
|
|
97
158
|
The file is expected to be stored in ``(C, H, W)`` layout and is
|
|
@@ -112,7 +173,7 @@ def spherical_map(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None
|
|
|
112
173
|
shape="3x3",
|
|
113
174
|
file_formats=[".npy", ".npz"],
|
|
114
175
|
)
|
|
115
|
-
def intrinsics(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> np.ndarray:
|
|
176
|
+
def intrinsics(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
116
177
|
"""Load a camera intrinsics matrix as a ``(3, 3)`` float32 array.
|
|
117
178
|
|
|
118
179
|
The file is expected to contain a ``(3, 3)`` array::
|
|
@@ -130,7 +191,7 @@ def intrinsics(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -
|
|
|
130
191
|
shape="NC",
|
|
131
192
|
file_formats=[".npy", ".npz"],
|
|
132
193
|
)
|
|
133
|
-
def sh_coeffs(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) -> np.ndarray:
|
|
194
|
+
def sh_coeffs(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
|
|
134
195
|
"""Load spherical-harmonic coefficients as an ``(N, 3)`` float32 array.
|
|
135
196
|
|
|
136
197
|
*N* is the number of SH basis functions (e.g. 15 for degree-3 SH with
|
|
@@ -145,6 +206,51 @@ def sh_coeffs(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None) ->
|
|
|
145
206
|
# ---------------------------------------------------------------------------
|
|
146
207
|
|
|
147
208
|
|
|
209
|
+
@mark_stream_supported
|
|
210
|
+
def write_map_2d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
|
|
211
|
+
"""Write a 2D map to NumPy formats based on extension.
|
|
212
|
+
|
|
213
|
+
Accepts ``(H, W)``, ``(1, H, W)``, or ``(H, W, 1)`` input and stores
|
|
214
|
+
the array in ``(H, W)`` layout.
|
|
215
|
+
"""
|
|
216
|
+
arr = to_hw(value, name="map_2d")
|
|
217
|
+
_write_numpy(path, arr)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@mark_stream_supported
|
|
221
|
+
def write_map_3d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
|
|
222
|
+
"""Write a 3D map to NumPy formats based on extension.
|
|
223
|
+
|
|
224
|
+
Input is expected in ``(H, W, C)`` layout and is transposed to
|
|
225
|
+
``(C, H, W)`` for storage.
|
|
226
|
+
"""
|
|
227
|
+
ensure_parent(path)
|
|
228
|
+
ext = os.path.splitext(_get_name(path))[1].lower()
|
|
229
|
+
arr = to_numpy(value).astype(np.float32)
|
|
230
|
+
arr = np.transpose(arr, (2, 0, 1))
|
|
231
|
+
|
|
232
|
+
if ext == _NPY_EXTENSION:
|
|
233
|
+
np.save(path, arr)
|
|
234
|
+
return
|
|
235
|
+
if ext == _NPZ_EXTENSION:
|
|
236
|
+
np.savez_compressed(path, data=arr)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
raise ValueError(f"Unsupported map_3d output extension: {ext}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mark_stream_supported
|
|
243
|
+
def write_scattering_coefficient(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
|
|
244
|
+
"""Write a scattering-coefficient map (delegates to :func:`write_map_2d`)."""
|
|
245
|
+
write_map_2d(path, value, meta)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@mark_stream_supported
|
|
249
|
+
def write_atmospheric_light(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
|
|
250
|
+
"""Write an atmospheric-light map (delegates to :func:`write_map_3d`)."""
|
|
251
|
+
write_map_3d(path, value, meta)
|
|
252
|
+
|
|
253
|
+
|
|
148
254
|
@mark_stream_supported
|
|
149
255
|
def write_spherical_map(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
|
|
150
256
|
"""Write spherical-map data to NumPy formats based on extension.
|