euler-loading 2.5.0__tar.gz → 2.7.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.5.0 → euler_loading-2.7.0}/PKG-INFO +2 -2
- {euler_loading-2.5.0 → euler_loading-2.7.0}/README.md +118 -7
- euler_loading-2.7.0/docs/loader-attributes.md +183 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/_resolution.py +27 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/_writing.py +28 -4
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/dataset.py +566 -15
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/contracts.py +6 -6
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/cpu/generic.py +46 -12
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/cpu/generic_dense_depth.py +14 -7
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/cpu/real_drive_sim.py +4 -4
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/cpu/vkitti2.py +8 -8
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/generate/loaders.json +34 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/gpu/generic.py +46 -12
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/gpu/generic_dense_depth.py +10 -5
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/gpu/real_drive_sim.py +7 -7
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/gpu/vkitti2.py +8 -8
- {euler_loading-2.5.0 → euler_loading-2.7.0}/package-lock.json +1 -1
- {euler_loading-2.5.0 → euler_loading-2.7.0}/pyproject.toml +3 -2
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_dataset.py +1113 -1
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_loaders.py +107 -2
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_writing.py +123 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/.github/workflows/workflow.yml +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/.gitignore +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/_dataset_contract.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/_ds_crawler_utils.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/_metadata.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/indexing.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/_annotations.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/_writer_utils.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/cpu/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/generate/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/generate/__main__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/generic.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/gpu/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/real_drive_sim.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/loaders/vkitti2.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/euler_loading/preprocessing.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/example.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/sample_rds.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/__init__.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/conftest.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/example_rds_calib.json +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_id_schema.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_indexing.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_preprocessing.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/tests/test_real_dataset.py +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.0}/vkitti_cpu_example_output.json +0 -0
- {euler_loading-2.5.0 → euler_loading-2.7.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.7.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
|
|
@@ -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 — `keyed_modalities` joins each augmented sample to its single GT file via the value encoded in the augmented sample's deepest hierarchy key.
|
|
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
|
+
| `keyed_by` | `Mapping[str, str] \| None` | Optional join configuration used only when this modality is passed under `MultiModalDataset(keyed_modalities=...)`. Recognised keys: `key_name` (named-group prefix at the regular modality's deepest hierarchy key, e.g. `"file_id"`; auto-detected from the anchor's data when omitted) and `modality` (anchor regular-modality name; auto-inferred when there's exactly one regular). Both keys are optional — the kwarg can be left unset entirely. |
|
|
69
|
+
| `cache` | `bool \| None` | Opt-in/out for in-memory caching of decoded values (only meaningful for hierarchical and keyed 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` (small calibration files benefit), keyed → `False` (safe for the augmentation case where the keyed modality holds full per-sample tensors and unbounded caching would OOM). |
|
|
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.
|
|
@@ -135,23 +137,30 @@ Returns a dict mapping each regular modality name to `{"path": ..., "origin_path
|
|
|
135
137
|
|
|
136
138
|
Returns a dict mapping each hierarchical modality name to `{"path": ..., "origin_path": ...}` and includes `split` when configured.
|
|
137
139
|
|
|
140
|
+
### `MultiModalDataset.keyed_modality_paths()`
|
|
141
|
+
|
|
142
|
+
Returns a dict mapping each keyed modality name to `{"path": ..., "origin_path": ...}` plus `keyed_by_modality` (the resolved anchor) and `keyed_by_key_name` (the resolved or auto-detected key name). Includes `split` when configured. Useful for verifying which prefix the auto-detection picked.
|
|
143
|
+
|
|
138
144
|
### `MultiModalDataset.get_modality_metadata(modality_name)`
|
|
139
145
|
|
|
140
146
|
Returns the ds-crawler metadata dict for the given modality.
|
|
141
147
|
|
|
142
|
-
### `MultiModalDataset(modalities, hierarchical_modalities=None, transforms=None)`
|
|
148
|
+
### `MultiModalDataset(modalities, hierarchical_modalities=None, transforms=None, keyed_modalities=None, strict_keyed=False)`
|
|
143
149
|
|
|
144
150
|
PyTorch `Dataset`. On construction it:
|
|
145
151
|
|
|
146
|
-
1. Runs `ds_crawler.index_dataset_from_path()` for every modality (regular and
|
|
152
|
+
1. Runs `ds_crawler.index_dataset_from_path()` for every modality (regular, hierarchical, and keyed).
|
|
147
153
|
2. Computes the **sorted intersection** of file IDs across all regular modalities.
|
|
148
|
-
3.
|
|
154
|
+
3. Validates keyed-modality joins for every common id; drops samples whose join misses (or raises when `strict_keyed=True`).
|
|
155
|
+
4. Logs warnings for unmatched files; raises `ValueError` when the intersection is empty or all samples are dropped by keyed-join validation.
|
|
149
156
|
|
|
150
157
|
| Parameter | Type | Description |
|
|
151
158
|
|-----------|------|-------------|
|
|
152
159
|
| `modalities` | `dict[str, Modality]` | At least one entry required. Keys become the sample dict keys. These modalities participate in ID intersection. |
|
|
153
160
|
| `hierarchical_modalities` | `dict[str, Modality] \| None` | Optional modalities whose files live at intermediate hierarchy levels (e.g. per-scene intrinsics). These do **not** participate in ID intersection. Each sample will contain a dict `{file_id: loaded_result}` with all files at or above the sample's hierarchy level. Results are cached so shared files are parsed only once. |
|
|
161
|
+
| `keyed_modalities` | `dict[str, Modality] \| None` | Optional modalities joined to a regular sample by the value of the regular sample's deepest hierarchy key. See [Keyed modalities](#keyed-modalities) below. Contributes a single loaded value per sample (not a dict). Caching is **off by default** for keyed modalities (per-sample tensors can be large); set `Modality(..., cache=True)` to opt in when the keyed dataset is small enough to fit in memory. |
|
|
154
162
|
| `transforms` | `list[Callable[[dict], dict]] \| None` | Applied in order after loading. Each receives and returns the full sample dict. |
|
|
163
|
+
| `strict_keyed` | `bool` | When `True`, missing or mis-decoded keyed-modality joins raise immediately at construction instead of warning + dropping the affected samples. Default `False`. |
|
|
155
164
|
|
|
156
165
|
#### Sample dict
|
|
157
166
|
|
|
@@ -166,16 +175,26 @@ PyTorch `Dataset`. On construction it:
|
|
|
166
175
|
...
|
|
167
176
|
},
|
|
168
177
|
...
|
|
178
|
+
"<keyed_modality_name>": <loader result>, # one entry per keyed modality (single value)
|
|
179
|
+
...
|
|
169
180
|
"id": str, # file ID (leaf only, shared across modalities)
|
|
170
181
|
"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": ...},
|
|
182
|
+
"meta": { # per-modality ds-crawler file entries
|
|
183
|
+
"<modality_name>": {"id": ..., "path": ..., "path_properties": ..., "basename_properties": ..., "attributes": ...},
|
|
184
|
+
...
|
|
185
|
+
},
|
|
186
|
+
"attributes": { # per-modality top-level surface for file_entry["attributes"]
|
|
187
|
+
"<modality_name>": {...}, # for regular and keyed modalities (single dict)
|
|
188
|
+
"<hierarchical_modality_name>": { # for hierarchical modalities (one dict per matched file)
|
|
189
|
+
"<file_id>": {...},
|
|
190
|
+
...
|
|
191
|
+
},
|
|
173
192
|
...
|
|
174
193
|
},
|
|
175
194
|
}
|
|
176
195
|
```
|
|
177
196
|
|
|
178
|
-
Hierarchical modality results are cached
|
|
197
|
+
Hierarchical modality results are cached by default; keyed modality results are not. Both can be flipped via `Modality(..., cache=True|False)` — see the [`cache` row in the `Modality` table](#modalitypath--loadernone-metadatanone) and [Keyed modalities](#keyed-modalities) for details on when each default is the right choice.
|
|
179
198
|
|
|
180
199
|
### `FileRecord`
|
|
181
200
|
|
|
@@ -315,6 +334,98 @@ Files across modalities are matched by these IDs, so **the directory structure m
|
|
|
315
334
|
|
|
316
335
|
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
336
|
|
|
337
|
+
## Keyed modalities
|
|
338
|
+
|
|
339
|
+
A **keyed modality** joins each regular sample to a single file in another dataset by reading the *value* of the regular sample's deepest hierarchy key. This is the right tool when one modality augments samples of another at a different hierarchy depth — typically: many augmented files per ground-truth sample.
|
|
340
|
+
|
|
341
|
+
### When to use it
|
|
342
|
+
|
|
343
|
+
Layout where the augmented modality nests files under an extra `file_id:<id>` level, while the GT keeps the file id as the filename stem:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
augmented_rgb/
|
|
347
|
+
scene_000000/CS_FRONT/file_id:000000000000000025/mor_10m.png
|
|
348
|
+
scene_000000/CS_FRONT/file_id:000000000000000025/mor_20m.png
|
|
349
|
+
scene_000000/CS_FRONT/file_id:000000000000000026/mor_10m.png
|
|
350
|
+
...
|
|
351
|
+
gt_depth/
|
|
352
|
+
scene_000000/CS_FRONT/000000000000000025.png
|
|
353
|
+
scene_000000/CS_FRONT/000000000000000026.png
|
|
354
|
+
...
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
ds-crawler indexes both layouts unchanged; the augmented modality's `indexing.hierarchy.separator` must be `":"` (or any single separator) so the deepest key `"file_id:000…025"` decodes into `("file_id", "000…025")`.
|
|
358
|
+
|
|
359
|
+
### Wiring
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from euler_loading import Modality, MultiModalDataset
|
|
363
|
+
|
|
364
|
+
dataset = MultiModalDataset(
|
|
365
|
+
modalities={
|
|
366
|
+
"rgb_aug": Modality("/data/augmented_rgb", loader=load_rgb),
|
|
367
|
+
},
|
|
368
|
+
keyed_modalities={
|
|
369
|
+
"depth": Modality("/data/gt_depth", loader=load_depth),
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
sample = dataset[0]
|
|
374
|
+
# sample["rgb_aug"] – the per-aug RGB
|
|
375
|
+
# sample["depth"] – the GT depth for this file_id (single value, shared across augs)
|
|
376
|
+
# sample["full_id"] – e.g. "/scene_000000/CS_FRONT/file_id:000…025/mor_10m"
|
|
377
|
+
# sample["id"] – the augmentation's leaf id (e.g. "mor_10m")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`keyed_by` is optional — both `key_name` (the named-group prefix at the regular modality's deepest hierarchy key) and `modality` (the anchor regular modality) are auto-detected when unambiguous. Set them explicitly when:
|
|
381
|
+
|
|
382
|
+
- there are multiple regular modalities (anchor must be picked: `keyed_by={"modality": "rgb_aug", "key_name": "file_id"}`); or
|
|
383
|
+
- the anchor's deepest hierarchy keys mix multiple prefixes (e.g. some `file_id:…` and some `frame:…`).
|
|
384
|
+
|
|
385
|
+
### Sample shape
|
|
386
|
+
|
|
387
|
+
A keyed modality contributes a **single loaded value** per sample, unlike hierarchical modalities which return `{file_id: loader_result}`. The join returns exactly one record by construction (one GT per augmented sample's file id at the parent hierarchy prefix), so a dict would be misleading.
|
|
388
|
+
|
|
389
|
+
### How the join works
|
|
390
|
+
|
|
391
|
+
For each common id in the regular modalities, euler-loading:
|
|
392
|
+
|
|
393
|
+
1. Looks up the anchor record's `hierarchy_path`, e.g. `(scene_000000, CS_FRONT, file_id:000…025)`.
|
|
394
|
+
2. Splits the deepest key on the regular modality's `indexing.hierarchy.separator` → `("file_id", "000…025")`.
|
|
395
|
+
3. Verifies the key name matches the configured (or auto-detected) `key_name`.
|
|
396
|
+
4. Looks up the keyed modality's record at hierarchy `(scene_000000, CS_FRONT)` with `id == "000…025"`.
|
|
397
|
+
|
|
398
|
+
### Caching
|
|
399
|
+
|
|
400
|
+
Caching for keyed modalities is **off by default**, which differs from hierarchical modalities (default on). The reason is workload shape: hierarchical files are usually tiny calibration / metadata blobs that you'd happily keep in memory forever, whereas a keyed modality typically holds the same kind of full per-sample tensor as a regular modality. Caching every decoded GT depth tensor in a 500 GB dataset would OOM the process.
|
|
401
|
+
|
|
402
|
+
Override per modality:
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
keyed_modalities={
|
|
406
|
+
# Small GT (a few hundred MB total): worth caching to avoid re-reads
|
|
407
|
+
# across augmentations of the same file_id.
|
|
408
|
+
"depth": Modality("/data/gt_depth", loader=load_depth, cache=True),
|
|
409
|
+
# Large GT (multi-hundred-GB total): keep it off, re-read each access.
|
|
410
|
+
"lidar": Modality("/data/gt_lidar", loader=load_lidar), # cache=False default
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
When `cache=True`, the GT for `file_id:000…025` is decoded once and reused across every augmentation that points at it. When `cache=False` (the default), every sample access re-reads and re-decodes the file.
|
|
415
|
+
|
|
416
|
+
### Validation and missing joins
|
|
417
|
+
|
|
418
|
+
Construction-time validation runs the decoding for every common id. Samples whose decode or lookup fails are dropped from the dataset with a per-modality warning summarising the count. Pass `strict_keyed=True` to raise instead — useful in pipelines where dropped samples should be a hard error. Common error cases (with precise messages):
|
|
419
|
+
|
|
420
|
+
- key-name mismatch (deepest key starts with a different prefix);
|
|
421
|
+
- regular modality with no `indexing.hierarchy.separator`;
|
|
422
|
+
- ambiguous auto-detection (multiple distinct deepest-key prefixes in the anchor);
|
|
423
|
+
- regular modality with no hierarchy at all.
|
|
424
|
+
|
|
425
|
+
### Writing keyed-modality outputs
|
|
426
|
+
|
|
427
|
+
`MultiModalDataset.write_sample(...)` accepts keyed modality names alongside regular ones. The destination path is derived from the keyed record (the GT's relative path), so prediction outputs land in the GT-shape layout — *not* under a synthetic `file_id:` subdirectory.
|
|
428
|
+
|
|
318
429
|
## DenseDepthLoader protocol
|
|
319
430
|
|
|
320
431
|
`euler_loading.DenseDepthLoader` is a `runtime_checkable` Protocol defining the loader contract for dense-depth datasets. A conforming module must expose:
|
|
@@ -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)
|