euler-loading 2.6.0__tar.gz → 2.8.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.6.0 → euler_loading-2.8.0}/PKG-INFO +1 -1
- {euler_loading-2.6.0 → euler_loading-2.8.0}/README.md +52 -4
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/_writing.py +4 -3
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/dataset.py +274 -15
- {euler_loading-2.6.0 → euler_loading-2.8.0}/package-lock.json +1 -1
- {euler_loading-2.6.0 → euler_loading-2.8.0}/pyproject.toml +1 -1
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_dataset.py +145 -4
- {euler_loading-2.6.0 → euler_loading-2.8.0}/.github/workflows/workflow.yml +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/.gitignore +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/docs/loader-attributes.md +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/_dataset_contract.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/_ds_crawler_utils.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/_metadata.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/_resolution.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/indexing.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/_annotations.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/_writer_utils.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/contracts.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/generic.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/generate/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/generate/__main__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/generate/loaders.json +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/generic.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/generic.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/real_drive_sim.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/vkitti2.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/preprocessing.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/example.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/sample_rds.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/__init__.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/conftest.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/example_rds_calib.json +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_id_schema.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_indexing.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_loaders.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_preprocessing.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_real_dataset.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/tests/test_writing.py +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/vkitti_cpu_example_output.json +0 -0
- {euler_loading-2.6.0 → euler_loading-2.8.0}/vkitti_gpu_example_output.json +0 -0
|
@@ -4,7 +4,7 @@ Multi-modal PyTorch `Dataset` that synchronises files across arbitrary dataset m
|
|
|
4
4
|
|
|
5
5
|
Each modality points at a directory (or `.zip` archive) that carries its own `ds-crawler.config` (or cached `output.json`).
|
|
6
6
|
ds-crawler indexes the directory tree, discovers files, and exposes hierarchical metadata (path properties, calibration files, …).
|
|
7
|
-
euler-loading then **intersects file IDs** across all modalities so that every sample contains exactly one file per modality. Additional hierarchical data (e.g. per-scene calibration files) can be loaded via `hierarchical_modalities`.
|
|
7
|
+
euler-loading then **intersects file IDs** across all modalities so that every sample contains exactly one file per modality. Additional hierarchical data (e.g. per-scene calibration files) can be loaded via `hierarchical_modalities`. For augmentation-style datasets — many augmented files per ground-truth sample — `euler_layout` metadata lets `MultiModalDataset.from_layout(...)` load shared targets as collapsed hierarchical modalities.
|
|
8
8
|
How a file is actually **loaded** (image, depth map, point cloud, …) is configurable per modality — either supply a `Callable` or let euler-loading resolve a built-in loader automatically from the ds-crawler config.
|
|
9
9
|
Writer functions can be resolved the same way, so inference outputs can be written back in dataset-native formats.
|
|
10
10
|
|
|
@@ -65,6 +65,8 @@ Frozen dataclass describing one data modality.
|
|
|
65
65
|
| `hierarchy_scope` | `str \| None` | Optional scope label for hierarchical modalities (e.g. `scene_camera`). |
|
|
66
66
|
| `applies_to` | `list[str] \| None` | Optional list of regular modality names a hierarchical modality applies to. |
|
|
67
67
|
| `split` | `str \| None` | Optional inline split name. Loads `.ds_crawler/split_<name>.json` from the modality root (directory or zip) and overlays it on the normal ds-crawler metadata. |
|
|
68
|
+
| `cache` | `bool \| None` | Opt-in/out for in-memory caching of decoded values (only meaningful for hierarchical modalities; regular modalities are never cached). `True` keeps every distinct loaded file in a process-lifetime dict. `False` re-reads on every access. `None` (default): hierarchical → `True` because small shared calibration files benefit. |
|
|
69
|
+
| `collapse_single` | `bool` | For hierarchical modalities only. When `True`, a sample that matches exactly one hierarchical file returns that loaded value directly instead of `{file_id: value}`. This is useful for shared GT tensors used by multiple augmentations of the same source sample. |
|
|
68
70
|
| `metadata` | `dict[str, Any]` | Optional arbitrary metadata. Keys under `metadata["euler_loading"]` are treated as euler-loading defaults. |
|
|
69
71
|
|
|
70
72
|
The loader is the **only** place where domain-specific I/O happens.
|
|
@@ -168,14 +170,22 @@ PyTorch `Dataset`. On construction it:
|
|
|
168
170
|
...
|
|
169
171
|
"id": str, # file ID (leaf only, shared across modalities)
|
|
170
172
|
"full_id": str, # full hierarchical path including file ID (e.g. "/scene/camera/frame")
|
|
171
|
-
"meta": { # per-modality ds-crawler file entries
|
|
172
|
-
"<modality_name>": {"id": ..., "path": ..., "path_properties": ..., "basename_properties": ...},
|
|
173
|
+
"meta": { # per-modality ds-crawler file entries
|
|
174
|
+
"<modality_name>": {"id": ..., "path": ..., "path_properties": ..., "basename_properties": ..., "attributes": ...},
|
|
175
|
+
...
|
|
176
|
+
},
|
|
177
|
+
"attributes": { # per-modality top-level surface for file_entry["attributes"]
|
|
178
|
+
"<modality_name>": {...}, # for regular modalities (single dict)
|
|
179
|
+
"<hierarchical_modality_name>": { # for hierarchical modalities (one dict per matched file)
|
|
180
|
+
"<file_id>": {...},
|
|
181
|
+
...
|
|
182
|
+
},
|
|
173
183
|
...
|
|
174
184
|
},
|
|
175
185
|
}
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
Hierarchical modality results are cached
|
|
188
|
+
Hierarchical modality results are cached by default. This can be flipped via `Modality(..., cache=True|False)` — see the [`cache` row in the `Modality` table](#modalitypath--loadernone-metadatanone).
|
|
179
189
|
|
|
180
190
|
### `FileRecord`
|
|
181
191
|
|
|
@@ -315,6 +325,44 @@ Files across modalities are matched by these IDs, so **the directory structure m
|
|
|
315
325
|
|
|
316
326
|
Calibration files or other per-scene/per-sequence metadata can be loaded via `hierarchical_modalities`. These files are matched to samples based on their position in the hierarchy — all files at or above a sample's hierarchy level are included and cached for efficiency.
|
|
317
327
|
|
|
328
|
+
## Layout-aware augmentation loading
|
|
329
|
+
|
|
330
|
+
Augmentation datasets often contain many files for the same source sample, while a target modality such as GT depth should be reused for every augmentation of that source. The recommended layout is to index the source sample as a hierarchy level and the augmentation variant as the file ID:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
augmented_rgb/
|
|
334
|
+
scene_000000/CS_FRONT/file_id:000000000000000025/mor_10m.png
|
|
335
|
+
scene_000000/CS_FRONT/file_id:000000000000000025/mor_20m.png
|
|
336
|
+
scene_000000/CS_FRONT/file_id:000000000000000026/mor_10m.png
|
|
337
|
+
...
|
|
338
|
+
gt_depth/
|
|
339
|
+
scene_000000/CS_FRONT/file_id:000000000000000025/depth.png
|
|
340
|
+
scene_000000/CS_FRONT/file_id:000000000000000026/depth.png
|
|
341
|
+
...
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Both modality indexes should include an `euler_layout` addon. The augmented RGB layout declares a `sample_axis` such as `file_id` at `location="hierarchy"` and a `variant_axis` at `location="file_id"`. The GT depth layout declares the same `sample_axis` and no variant axis.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from euler_loading import Modality, MultiModalDataset
|
|
348
|
+
|
|
349
|
+
dataset = MultiModalDataset.from_layout(
|
|
350
|
+
{
|
|
351
|
+
"rgb_aug": Modality("/data/augmented_rgb", loader=load_rgb),
|
|
352
|
+
"depth": Modality("/data/gt_depth", loader=load_depth),
|
|
353
|
+
},
|
|
354
|
+
primary="rgb_aug",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
sample = dataset[0]
|
|
358
|
+
# sample["rgb_aug"] – the per-augmentation RGB
|
|
359
|
+
# sample["depth"] – the GT depth for this file_id, shared across variants
|
|
360
|
+
# sample["full_id"] – e.g. "/scene_000000/CS_FRONT/file_id:000…025/mor_10m"
|
|
361
|
+
# sample["id"] – the augmentation's leaf id, e.g. "mor_10m"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`from_layout(...)` plans the GT modality as `hierarchical_modalities={"depth": Modality(..., collapse_single=True)}`. The hierarchical match uses the regular sample's hierarchy path, so source samples remain disambiguated by their parent hierarchy as well as by `file_id`. If a shared modality declares the same `sample_axis` but its index does not expose that axis as hierarchy, construction raises and the dataset should be re-indexed with the source sample axis in hierarchy.
|
|
365
|
+
|
|
318
366
|
## DenseDepthLoader protocol
|
|
319
367
|
|
|
320
368
|
`euler_loading.DenseDepthLoader` is a `runtime_checkable` Protocol defining the loader contract for dense-depth datasets. A conforming module must expose:
|
|
@@ -55,10 +55,11 @@ def _wrap_writer_save_index(
|
|
|
55
55
|
result["meta"] = dict(meta)
|
|
56
56
|
|
|
57
57
|
if isinstance(addons, Mapping):
|
|
58
|
-
for addon_name in (
|
|
59
|
-
|
|
58
|
+
for addon_name, addon in addons.items():
|
|
59
|
+
if not str(addon_name).startswith("euler_"):
|
|
60
|
+
continue
|
|
60
61
|
if isinstance(addon, Mapping):
|
|
61
|
-
result[addon_name] = dict(addon)
|
|
62
|
+
result[str(addon_name)] = dict(addon)
|
|
62
63
|
|
|
63
64
|
index_tree = output.get("index")
|
|
64
65
|
if isinstance(index_tree, Mapping):
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import zipfile
|
|
9
9
|
from collections.abc import Mapping
|
|
10
|
-
from dataclasses import dataclass, field
|
|
10
|
+
from dataclasses import dataclass, field, replace
|
|
11
11
|
from typing import Any, Callable
|
|
12
12
|
|
|
13
13
|
try:
|
|
@@ -25,6 +25,11 @@ from ds_crawler import (
|
|
|
25
25
|
)
|
|
26
26
|
from ds_crawler.zip_utils import get_zip_root_prefix, is_zip_path
|
|
27
27
|
|
|
28
|
+
try:
|
|
29
|
+
from ds_crawler import get_layout_addon as _ds_crawler_get_layout_addon
|
|
30
|
+
except ImportError: # pragma: no cover - compatibility with older ds-crawler
|
|
31
|
+
_ds_crawler_get_layout_addon = None
|
|
32
|
+
|
|
28
33
|
from ._ds_crawler_utils import load_index_output, parse_path_with_split
|
|
29
34
|
from ._metadata import _build_runlog_entry, _get_ds_crawler_descriptor
|
|
30
35
|
from ._resolution import (
|
|
@@ -120,6 +125,112 @@ def _get_index_meta(index_output: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
|
120
125
|
return None
|
|
121
126
|
|
|
122
127
|
|
|
128
|
+
def _get_index_layout(index_output: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
129
|
+
"""Return the optional ``euler_layout`` addon from an index payload."""
|
|
130
|
+
if _ds_crawler_get_layout_addon is not None:
|
|
131
|
+
try:
|
|
132
|
+
return _ds_crawler_get_layout_addon(dict(index_output))
|
|
133
|
+
except Exception:
|
|
134
|
+
logger.debug("Failed to parse euler_layout addon.", exc_info=True)
|
|
135
|
+
|
|
136
|
+
head = index_output.get("head")
|
|
137
|
+
if isinstance(head, Mapping):
|
|
138
|
+
addons = head.get("addons")
|
|
139
|
+
if isinstance(addons, Mapping):
|
|
140
|
+
layout = addons.get("euler_layout")
|
|
141
|
+
if isinstance(layout, Mapping):
|
|
142
|
+
return dict(layout)
|
|
143
|
+
|
|
144
|
+
layout = index_output.get("euler_layout")
|
|
145
|
+
if isinstance(layout, Mapping):
|
|
146
|
+
return dict(layout)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _layout_axis(layout: Mapping[str, Any] | None, axis: str) -> dict[str, Any] | None:
|
|
151
|
+
if not isinstance(layout, Mapping):
|
|
152
|
+
return None
|
|
153
|
+
value = layout.get(axis)
|
|
154
|
+
return dict(value) if isinstance(value, Mapping) else None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _layout_axis_name(layout: Mapping[str, Any] | None, axis: str) -> str | None:
|
|
158
|
+
value = _layout_axis(layout, axis)
|
|
159
|
+
name = value.get("name") if value else None
|
|
160
|
+
return name if isinstance(name, str) and name else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _layout_has_variant_axis(layout: Mapping[str, Any] | None) -> bool:
|
|
164
|
+
return _layout_axis(layout, "variant_axis") is not None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _layout_family(layout: Mapping[str, Any] | None) -> str | None:
|
|
168
|
+
if not isinstance(layout, Mapping):
|
|
169
|
+
return None
|
|
170
|
+
family = layout.get("family")
|
|
171
|
+
return family if isinstance(family, str) and family else None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _layout_families_compatible(
|
|
175
|
+
left: Mapping[str, Any] | None,
|
|
176
|
+
right: Mapping[str, Any] | None,
|
|
177
|
+
) -> bool:
|
|
178
|
+
left_family = _layout_family(left)
|
|
179
|
+
right_family = _layout_family(right)
|
|
180
|
+
return (
|
|
181
|
+
left_family is None
|
|
182
|
+
or right_family is None
|
|
183
|
+
or left_family == right_family
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _index_hierarchy_separator(index_output: Mapping[str, Any]) -> str | None:
|
|
188
|
+
indexing = index_output.get("indexing")
|
|
189
|
+
hierarchy = indexing.get("hierarchy") if isinstance(indexing, Mapping) else None
|
|
190
|
+
if isinstance(hierarchy, Mapping):
|
|
191
|
+
separator = hierarchy.get("separator")
|
|
192
|
+
if isinstance(separator, str) and separator:
|
|
193
|
+
return separator
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _tree_has_hierarchy_axis(
|
|
198
|
+
node: Mapping[str, Any],
|
|
199
|
+
*,
|
|
200
|
+
axis_name: str,
|
|
201
|
+
separator: str,
|
|
202
|
+
) -> bool:
|
|
203
|
+
prefix = f"{axis_name}{separator}"
|
|
204
|
+
children = node.get("children")
|
|
205
|
+
if not isinstance(children, Mapping):
|
|
206
|
+
return False
|
|
207
|
+
for key, child in children.items():
|
|
208
|
+
if isinstance(key, str) and key.startswith(prefix):
|
|
209
|
+
return True
|
|
210
|
+
if isinstance(child, Mapping) and _tree_has_hierarchy_axis(
|
|
211
|
+
child, axis_name=axis_name, separator=separator,
|
|
212
|
+
):
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _index_has_hierarchy_axis(
|
|
218
|
+
index_output: Mapping[str, Any],
|
|
219
|
+
*,
|
|
220
|
+
axis_name: str,
|
|
221
|
+
) -> bool:
|
|
222
|
+
separator = _index_hierarchy_separator(index_output)
|
|
223
|
+
if not separator:
|
|
224
|
+
return False
|
|
225
|
+
try:
|
|
226
|
+
tree = _get_index_tree(index_output)
|
|
227
|
+
except KeyError:
|
|
228
|
+
return False
|
|
229
|
+
return _tree_has_hierarchy_axis(
|
|
230
|
+
tree, axis_name=axis_name, separator=separator,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
123
234
|
def _get_file_attributes(file_entry: Mapping[str, Any]) -> dict[str, Any] | None:
|
|
124
235
|
attributes = file_entry.get("attributes")
|
|
125
236
|
if isinstance(attributes, Mapping):
|
|
@@ -217,6 +328,22 @@ class Modality:
|
|
|
217
328
|
split: Optional inline split name. When set, euler-loading reads the
|
|
218
329
|
full ds-crawler metadata from the modality root and overlays the
|
|
219
330
|
dataset payload from ``.ds_crawler/split_<name>.json``.
|
|
331
|
+
cache: Optional opt-in/out for in-memory caching of loaded values.
|
|
332
|
+
Only meaningful for hierarchical modalities; regular
|
|
333
|
+
modalities load once per sample and never cache.
|
|
334
|
+
|
|
335
|
+
- ``True``: every distinct file is decoded at most once
|
|
336
|
+
per dataset lifetime, then served from memory.
|
|
337
|
+
- ``False``: every access re-reads and re-decodes the
|
|
338
|
+
file. Use this for large per-sample tensors where the
|
|
339
|
+
cache would balloon.
|
|
340
|
+
- ``None`` (default): hierarchical modalities default to
|
|
341
|
+
``True`` (small calibration files benefit).
|
|
342
|
+
collapse_single: When ``True`` for a hierarchical modality, return the
|
|
343
|
+
single matched hierarchical value directly instead of a
|
|
344
|
+
``{file_id: value}`` dict. This is useful for shared GT
|
|
345
|
+
tensors loaded once per source sample across augmentations.
|
|
346
|
+
A sample with more than one matched file raises.
|
|
220
347
|
metadata: Optional arbitrary metadata. Keys under
|
|
221
348
|
``metadata["euler_loading"]`` are treated as
|
|
222
349
|
euler-loading-specific defaults.
|
|
@@ -232,6 +359,8 @@ class Modality:
|
|
|
232
359
|
hierarchy_scope: str | None = None
|
|
233
360
|
applies_to: list[str] | None = None
|
|
234
361
|
split: str | None = None
|
|
362
|
+
cache: bool | None = None
|
|
363
|
+
collapse_single: bool = False
|
|
235
364
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
236
365
|
|
|
237
366
|
def __post_init__(self) -> None:
|
|
@@ -403,6 +532,100 @@ class MultiModalDataset(_BaseDataset):
|
|
|
403
532
|
"hierarchical_modalities": hierarchical_modalities,
|
|
404
533
|
}
|
|
405
534
|
|
|
535
|
+
@classmethod
|
|
536
|
+
def from_layout(
|
|
537
|
+
cls,
|
|
538
|
+
modalities: dict[str, Modality],
|
|
539
|
+
*,
|
|
540
|
+
primary: str | None = None,
|
|
541
|
+
transforms: list[Callable[[dict[str, Any]], dict[str, Any]]] | None = None,
|
|
542
|
+
) -> "MultiModalDataset":
|
|
543
|
+
"""Create a dataset by planning modality roles from ``euler_layout``.
|
|
544
|
+
|
|
545
|
+
When the primary modality has a variant axis (for example fog
|
|
546
|
+
augmentations grouped under ``file_id:<source>``) and an auxiliary
|
|
547
|
+
modality has the same sample axis but no variant axis, the auxiliary is
|
|
548
|
+
loaded as shared per-source data. If the auxiliary index already
|
|
549
|
+
exposes that sample axis as a hierarchy key, it is wired as a collapsed
|
|
550
|
+
hierarchical modality. Otherwise construction fails because the shared
|
|
551
|
+
sample key cannot be addressed unambiguously.
|
|
552
|
+
|
|
553
|
+
Existing constructor semantics remain unchanged; this helper is the
|
|
554
|
+
opt-in path for layout-aware loading.
|
|
555
|
+
"""
|
|
556
|
+
if not modalities:
|
|
557
|
+
raise ValueError("At least one modality must be provided.")
|
|
558
|
+
|
|
559
|
+
indexed: dict[str, dict[str, Any]] = {}
|
|
560
|
+
layouts: dict[str, dict[str, Any] | None] = {}
|
|
561
|
+
for name, modality in modalities.items():
|
|
562
|
+
index = load_index_output(
|
|
563
|
+
modality.path,
|
|
564
|
+
split=modality.split,
|
|
565
|
+
index_dataset_from_path_fn=index_dataset_from_path,
|
|
566
|
+
)
|
|
567
|
+
indexed[name] = index
|
|
568
|
+
layouts[name] = _get_index_layout(index)
|
|
569
|
+
|
|
570
|
+
if primary is None:
|
|
571
|
+
primary = next(
|
|
572
|
+
(
|
|
573
|
+
name for name, layout in layouts.items()
|
|
574
|
+
if _layout_has_variant_axis(layout)
|
|
575
|
+
),
|
|
576
|
+
next(iter(modalities)),
|
|
577
|
+
)
|
|
578
|
+
if primary not in modalities:
|
|
579
|
+
raise ValueError(
|
|
580
|
+
f"primary modality {primary!r} is not in modalities; "
|
|
581
|
+
f"known: {sorted(modalities)}"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
primary_layout = layouts[primary]
|
|
585
|
+
primary_sample_axis = _layout_axis_name(primary_layout, "sample_axis")
|
|
586
|
+
primary_has_variants = _layout_has_variant_axis(primary_layout)
|
|
587
|
+
|
|
588
|
+
regular_modalities: dict[str, Modality] = {primary: modalities[primary]}
|
|
589
|
+
hierarchical_modalities: dict[str, Modality] = {}
|
|
590
|
+
|
|
591
|
+
for name, modality in modalities.items():
|
|
592
|
+
if name == primary:
|
|
593
|
+
continue
|
|
594
|
+
|
|
595
|
+
layout = layouts[name]
|
|
596
|
+
sample_axis = _layout_axis_name(layout, "sample_axis")
|
|
597
|
+
shared_with_primary = (
|
|
598
|
+
primary_has_variants
|
|
599
|
+
and primary_sample_axis is not None
|
|
600
|
+
and sample_axis == primary_sample_axis
|
|
601
|
+
and not _layout_has_variant_axis(layout)
|
|
602
|
+
and _layout_families_compatible(primary_layout, layout)
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if not shared_with_primary:
|
|
606
|
+
regular_modalities[name] = modality
|
|
607
|
+
continue
|
|
608
|
+
|
|
609
|
+
if _index_has_hierarchy_axis(indexed[name], axis_name=sample_axis):
|
|
610
|
+
hierarchical_modalities[name] = replace(
|
|
611
|
+
modality, collapse_single=True,
|
|
612
|
+
)
|
|
613
|
+
continue
|
|
614
|
+
|
|
615
|
+
raise ValueError(
|
|
616
|
+
f"Modality {name!r} shares sample_axis={sample_axis!r} with "
|
|
617
|
+
f"variant primary {primary!r}, but its ds-crawler index has "
|
|
618
|
+
"no matching hierarchy axis. Re-index it with that axis as "
|
|
619
|
+
"hierarchy so it can be loaded as a collapsed hierarchical "
|
|
620
|
+
"modality."
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
return cls(
|
|
624
|
+
modalities=regular_modalities,
|
|
625
|
+
hierarchical_modalities=hierarchical_modalities or None,
|
|
626
|
+
transforms=transforms,
|
|
627
|
+
)
|
|
628
|
+
|
|
406
629
|
def __init__(
|
|
407
630
|
self,
|
|
408
631
|
modalities: dict[str, Modality],
|
|
@@ -498,10 +721,11 @@ class MultiModalDataset(_BaseDataset):
|
|
|
498
721
|
modality_counts = {
|
|
499
722
|
name: len(lookup) for name, lookup in self._lookups.items()
|
|
500
723
|
}
|
|
501
|
-
|
|
724
|
+
message = (
|
|
502
725
|
f"No common IDs found across modalities. "
|
|
503
726
|
f"Per-modality file counts: {modality_counts}"
|
|
504
727
|
)
|
|
728
|
+
raise ValueError(message)
|
|
505
729
|
|
|
506
730
|
for name, lookup in self._lookups.items():
|
|
507
731
|
total = len(lookup)
|
|
@@ -521,7 +745,10 @@ class MultiModalDataset(_BaseDataset):
|
|
|
521
745
|
self._zip_prefixes: dict[str, str] = {}
|
|
522
746
|
self._zip_handles: dict[tuple[str, int], zipfile.ZipFile] = {}
|
|
523
747
|
|
|
524
|
-
for name, modality in
|
|
748
|
+
for name, modality in (
|
|
749
|
+
list(modalities.items())
|
|
750
|
+
+ list(self._hierarchical_modalities.items())
|
|
751
|
+
):
|
|
525
752
|
if is_zip_path(modality.path):
|
|
526
753
|
self._zip_modalities.add(name)
|
|
527
754
|
self._zip_prefixes[name] = get_zip_root_prefix(
|
|
@@ -529,7 +756,13 @@ class MultiModalDataset(_BaseDataset):
|
|
|
529
756
|
)
|
|
530
757
|
|
|
531
758
|
# -- Caches ----------------------------------------------------------
|
|
532
|
-
|
|
759
|
+
# Per-modality caches; ``None`` disables caching for that modality.
|
|
760
|
+
# Hierarchical default: enabled (calibration-style small files).
|
|
761
|
+
self._caches: dict[str, dict[str, Any] | None] = {}
|
|
762
|
+
for name, modality in self._hierarchical_modalities.items():
|
|
763
|
+
self._caches[name] = {} if (
|
|
764
|
+
modality.cache if modality.cache is not None else True
|
|
765
|
+
) else None
|
|
533
766
|
|
|
534
767
|
# -- Optional transform binding -------------------------------------
|
|
535
768
|
for transform in self._transforms:
|
|
@@ -699,18 +932,21 @@ class MultiModalDataset(_BaseDataset):
|
|
|
699
932
|
written_paths: dict[str, str] = {}
|
|
700
933
|
|
|
701
934
|
for modality_name, value in outputs.items():
|
|
702
|
-
if modality_name
|
|
935
|
+
if modality_name in self._lookups:
|
|
936
|
+
record = self._lookups[modality_name][sample_id]
|
|
937
|
+
modality_meta = _get_index_meta(
|
|
938
|
+
self._index_outputs[modality_name]
|
|
939
|
+
)
|
|
940
|
+
else:
|
|
703
941
|
raise KeyError(
|
|
704
|
-
f"Modality {modality_name!r} is not a regular
|
|
705
|
-
"be addressed by sample index."
|
|
942
|
+
f"Modality {modality_name!r} is not a regular "
|
|
943
|
+
"modality and cannot be addressed by sample index."
|
|
706
944
|
)
|
|
707
945
|
|
|
708
946
|
writer = self.get_writer(modality_name)
|
|
709
947
|
destination = _resolve_output_destination(
|
|
710
948
|
output_root=output_root, modality_name=modality_name
|
|
711
949
|
)
|
|
712
|
-
record = self._lookups[modality_name][sample_id]
|
|
713
|
-
modality_meta = _get_index_meta(self._index_outputs[modality_name])
|
|
714
950
|
basename = Path(record.file_entry["path"]).name
|
|
715
951
|
relative_path = record.file_entry["path"]
|
|
716
952
|
full_id = _build_writer_full_id(
|
|
@@ -848,36 +1084,59 @@ class MultiModalDataset(_BaseDataset):
|
|
|
848
1084
|
files_by_level = self._hierarchical_lookups[name]
|
|
849
1085
|
matched = match_hierarchical_files(hierarchy_path, files_by_level)
|
|
850
1086
|
modality_meta = _get_index_meta(self._hierarchical_index_outputs[name])
|
|
1087
|
+
cache = self._caches[name]
|
|
851
1088
|
loaded: dict[str, Any] = {}
|
|
852
1089
|
attrs_for_modality: dict[str, dict[str, Any]] = {}
|
|
1090
|
+
entries_by_id: dict[str, dict[str, Any]] = {}
|
|
853
1091
|
for entry in matched:
|
|
854
1092
|
entry_attributes = _get_file_attributes(entry)
|
|
855
|
-
cache_key = f"{modality.path}/{entry['path']}"
|
|
856
1093
|
accepts_attributes = self._loaders_accept_attributes[name]
|
|
1094
|
+
cache_key = f"{modality.path}/{entry['path']}"
|
|
857
1095
|
if accepts_attributes:
|
|
858
1096
|
cache_key = (
|
|
859
1097
|
f"{cache_key}::attributes="
|
|
860
1098
|
f"{_attributes_cache_key(entry_attributes)}"
|
|
861
1099
|
)
|
|
862
|
-
if
|
|
1100
|
+
if cache is not None and cache_key in cache:
|
|
1101
|
+
value = cache[cache_key]
|
|
1102
|
+
else:
|
|
863
1103
|
if name in self._zip_modalities:
|
|
864
1104
|
file_or_path = self._open_from_zip(
|
|
865
1105
|
name, modality.path, entry["path"],
|
|
866
1106
|
)
|
|
867
1107
|
else:
|
|
868
1108
|
file_or_path = f"{modality.path}/{entry['path']}"
|
|
869
|
-
|
|
1109
|
+
value = _load_with_optional_attributes(
|
|
870
1110
|
self._resolved_loaders[name],
|
|
871
1111
|
file_or_path,
|
|
872
1112
|
modality_meta,
|
|
873
1113
|
entry_attributes,
|
|
874
1114
|
accepts_attributes=accepts_attributes,
|
|
875
1115
|
)
|
|
876
|
-
|
|
1116
|
+
if cache is not None:
|
|
1117
|
+
cache[cache_key] = value
|
|
1118
|
+
loaded[entry["id"]] = value
|
|
1119
|
+
entries_by_id[entry["id"]] = entry
|
|
877
1120
|
if entry_attributes:
|
|
878
1121
|
attrs_for_modality[entry["id"]] = entry_attributes
|
|
879
|
-
|
|
880
|
-
|
|
1122
|
+
if modality.collapse_single:
|
|
1123
|
+
if len(loaded) > 1:
|
|
1124
|
+
raise ValueError(
|
|
1125
|
+
f"Hierarchical modality {name!r} was configured with "
|
|
1126
|
+
f"collapse_single=True, but sample {sample_id!r} "
|
|
1127
|
+
f"matched {len(loaded)} files."
|
|
1128
|
+
)
|
|
1129
|
+
if loaded:
|
|
1130
|
+
only_id, only_value = next(iter(loaded.items()))
|
|
1131
|
+
sample[name] = only_value
|
|
1132
|
+
meta[name] = entries_by_id[only_id]
|
|
1133
|
+
attributes[name] = attrs_for_modality.get(only_id, {})
|
|
1134
|
+
else:
|
|
1135
|
+
sample[name] = None
|
|
1136
|
+
attributes[name] = {}
|
|
1137
|
+
else:
|
|
1138
|
+
sample[name] = loaded
|
|
1139
|
+
attributes[name] = attrs_for_modality
|
|
881
1140
|
|
|
882
1141
|
sample["id"] = file_id
|
|
883
1142
|
sample["full_id"] = "/" + "/".join(hierarchy_path + (file_id,))
|
|
@@ -586,7 +586,7 @@ class TestHierarchicalModalities:
|
|
|
586
586
|
assert s0 == s1
|
|
587
587
|
|
|
588
588
|
def test_hierarchical_cached(self):
|
|
589
|
-
"""Shared hierarchical files are loaded only once."""
|
|
589
|
+
"""Shared hierarchical files are loaded only once (default behavior)."""
|
|
590
590
|
loader = MagicMock(return_value="parsed")
|
|
591
591
|
rgb_index = _deep_regular_index(["f001", "f002", "f003"])
|
|
592
592
|
hier_index = _hierarchical_intrinsics_index()
|
|
@@ -614,6 +614,36 @@ class TestHierarchicalModalities:
|
|
|
614
614
|
# The intrinsics file should have been loaded exactly once.
|
|
615
615
|
loader.assert_called_once()
|
|
616
616
|
|
|
617
|
+
def test_hierarchical_cache_false_reloads_every_access(self):
|
|
618
|
+
"""Opt-out via cache=False reloads even shared hierarchical files."""
|
|
619
|
+
loader = MagicMock(return_value="parsed")
|
|
620
|
+
rgb_index = _deep_regular_index(["f001", "f002", "f003"])
|
|
621
|
+
hier_index = _hierarchical_intrinsics_index()
|
|
622
|
+
|
|
623
|
+
def mock_index(path, **kw):
|
|
624
|
+
return rgb_index if "rgb" in path else hier_index
|
|
625
|
+
|
|
626
|
+
with patch(
|
|
627
|
+
"euler_loading.dataset.index_dataset_from_path",
|
|
628
|
+
side_effect=mock_index,
|
|
629
|
+
):
|
|
630
|
+
ds = MultiModalDataset(
|
|
631
|
+
modalities={
|
|
632
|
+
"rgb": Modality("/data/rgb", loader=dummy_loader),
|
|
633
|
+
},
|
|
634
|
+
hierarchical_modalities={
|
|
635
|
+
"cam_intrinsics": Modality(
|
|
636
|
+
"/data/intrinsics", loader=loader, cache=False,
|
|
637
|
+
),
|
|
638
|
+
},
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
for i in range(3):
|
|
642
|
+
_ = ds[i]
|
|
643
|
+
|
|
644
|
+
# 3 accesses with caching disabled → 3 loads of the same file.
|
|
645
|
+
assert loader.call_count == 3
|
|
646
|
+
|
|
617
647
|
def test_no_hierarchy_overlap_returns_empty_dict(self):
|
|
618
648
|
"""When hierarchical modality has no matching ancestors, result is {}."""
|
|
619
649
|
rgb_index = _deep_regular_index(["f001"])
|
|
@@ -801,7 +831,7 @@ class TestAugmentedRgbWithHierarchicalDepth:
|
|
|
801
831
|
# 2 file-ids x 2 augs per file-id
|
|
802
832
|
assert len(ds) == 4
|
|
803
833
|
|
|
804
|
-
def
|
|
834
|
+
def test_each_sample_has_depth_grouped_by_file_id(self):
|
|
805
835
|
ds = self._make()
|
|
806
836
|
for i in range(len(ds)):
|
|
807
837
|
sample = ds[i]
|
|
@@ -888,6 +918,117 @@ class TestAugmentedRgbWithHierarchicalDepth:
|
|
|
888
918
|
assert loader.call_count == 2
|
|
889
919
|
|
|
890
920
|
|
|
921
|
+
# ---------------------------------------------------------------------------
|
|
922
|
+
# Layout-aware augmentation planning
|
|
923
|
+
# ---------------------------------------------------------------------------
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _augmented_variant_index(*, separator: str = ":") -> dict[str, Any]:
|
|
927
|
+
"""Augmented index with ``children[file_id:<id>]`` source-sample level."""
|
|
928
|
+
return {
|
|
929
|
+
"indexing": {
|
|
930
|
+
"hierarchy": {"separator": separator},
|
|
931
|
+
"id": {"join_char": "+"},
|
|
932
|
+
},
|
|
933
|
+
"dataset": {
|
|
934
|
+
"children": {
|
|
935
|
+
f"file_id{separator}abc": {
|
|
936
|
+
"files": [
|
|
937
|
+
_make_file("aug-aug_1", "abc/aug_1.png"),
|
|
938
|
+
_make_file("aug-aug_2", "abc/aug_2.png"),
|
|
939
|
+
]
|
|
940
|
+
},
|
|
941
|
+
f"file_id{separator}xyz": {
|
|
942
|
+
"files": [
|
|
943
|
+
_make_file("aug-aug_1", "xyz/aug_1.png"),
|
|
944
|
+
_make_file("aug-aug_2", "xyz/aug_2.png"),
|
|
945
|
+
]
|
|
946
|
+
},
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _with_layout(
|
|
953
|
+
index: dict[str, Any],
|
|
954
|
+
*,
|
|
955
|
+
variant: bool,
|
|
956
|
+
family: str = "demo",
|
|
957
|
+
) -> dict[str, Any]:
|
|
958
|
+
result = json.loads(json.dumps(index))
|
|
959
|
+
layout: dict[str, Any] = {
|
|
960
|
+
"version": "1.0",
|
|
961
|
+
"family": family,
|
|
962
|
+
"sample_axis": {"name": "file_id", "location": "hierarchy"},
|
|
963
|
+
}
|
|
964
|
+
if variant:
|
|
965
|
+
layout["variant_axis"] = {
|
|
966
|
+
"name": "fog_augmentation",
|
|
967
|
+
"location": "file_id",
|
|
968
|
+
}
|
|
969
|
+
result["head"] = {
|
|
970
|
+
"contract": {"kind": "dataset_head", "version": "1.0"},
|
|
971
|
+
"dataset": {"id": "demo", "name": "Demo"},
|
|
972
|
+
"modality": {"key": "rgb" if variant else "depth", "meta": {}},
|
|
973
|
+
"addons": {"euler_layout": layout},
|
|
974
|
+
}
|
|
975
|
+
result.setdefault(
|
|
976
|
+
"indexing",
|
|
977
|
+
{"hierarchy": {"separator": ":"}, "id": {"join_char": "+"}},
|
|
978
|
+
)
|
|
979
|
+
return result
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
class TestLayoutAwareDatasetPlanning:
|
|
983
|
+
def test_from_layout_collapses_shared_hierarchical_modality(self):
|
|
984
|
+
rgb_index = _with_layout(_augmented_variant_index(), variant=True)
|
|
985
|
+
depth_index = _with_layout(
|
|
986
|
+
_per_id_depth_hierarchical_index(), variant=False,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
def mock_index(path, **kw):
|
|
990
|
+
return rgb_index if "rgb" in path else depth_index
|
|
991
|
+
|
|
992
|
+
with patch(
|
|
993
|
+
"euler_loading.dataset.index_dataset_from_path",
|
|
994
|
+
side_effect=mock_index,
|
|
995
|
+
):
|
|
996
|
+
ds = MultiModalDataset.from_layout(
|
|
997
|
+
{
|
|
998
|
+
"rgb_aug": Modality("/data/rgb_aug", loader=dummy_loader),
|
|
999
|
+
"depth": Modality("/data/depth", loader=dummy_loader),
|
|
1000
|
+
},
|
|
1001
|
+
primary="rgb_aug",
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
sample = ds[0]
|
|
1005
|
+
|
|
1006
|
+
assert len(ds) == 4
|
|
1007
|
+
assert isinstance(sample["depth"], str)
|
|
1008
|
+
assert sample["depth"].startswith("loaded:/data/depth/")
|
|
1009
|
+
assert isinstance(sample["meta"]["depth"], dict)
|
|
1010
|
+
|
|
1011
|
+
def test_from_layout_rejects_flat_shared_modality(self):
|
|
1012
|
+
rgb_index = _with_layout(_augmented_variant_index(), variant=True)
|
|
1013
|
+
depth_index = _with_layout(_flat_index("depth", ["abc", "xyz"]), variant=False)
|
|
1014
|
+
|
|
1015
|
+
def mock_index(path, **kw):
|
|
1016
|
+
return rgb_index if "rgb" in path else depth_index
|
|
1017
|
+
|
|
1018
|
+
with patch(
|
|
1019
|
+
"euler_loading.dataset.index_dataset_from_path",
|
|
1020
|
+
side_effect=mock_index,
|
|
1021
|
+
):
|
|
1022
|
+
with pytest.raises(ValueError, match="no matching hierarchy axis"):
|
|
1023
|
+
MultiModalDataset.from_layout(
|
|
1024
|
+
{
|
|
1025
|
+
"rgb_aug": Modality("/data/rgb_aug", loader=dummy_loader),
|
|
1026
|
+
"depth": Modality("/data/depth", loader=dummy_loader),
|
|
1027
|
+
},
|
|
1028
|
+
primary="rgb_aug",
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
|
|
891
1032
|
# ---------------------------------------------------------------------------
|
|
892
1033
|
# Per-file ``attributes`` field exposed on samples
|
|
893
1034
|
# ---------------------------------------------------------------------------
|
|
@@ -939,7 +1080,7 @@ class TestSampleAttributes:
|
|
|
939
1080
|
assert "attributes" in sample
|
|
940
1081
|
assert isinstance(sample["attributes"], dict)
|
|
941
1082
|
|
|
942
|
-
def
|
|
1083
|
+
def test_top_level_attributes_grouped_by_modality(self):
|
|
943
1084
|
ds = self._make()
|
|
944
1085
|
sample = ds[0]
|
|
945
1086
|
assert sample["attributes"]["rgb"] == {"weight": 0.42, "src": "blender"}
|
|
@@ -1159,7 +1300,7 @@ class TestHierarchicalModalityAttributes:
|
|
|
1159
1300
|
},
|
|
1160
1301
|
)
|
|
1161
1302
|
|
|
1162
|
-
def
|
|
1303
|
+
def test_hierarchical_attributes_grouped_by_file_id(self):
|
|
1163
1304
|
ds = self._make()
|
|
1164
1305
|
sample = ds[0]
|
|
1165
1306
|
assert sample["attributes"]["cam_intrinsics"] == {
|
|
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
|
{euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/cpu/generic_dense_depth.py
RENAMED
|
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
|
{euler_loading-2.6.0 → euler_loading-2.8.0}/euler_loading/loaders/gpu/generic_dense_depth.py
RENAMED
|
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
|