euler-loading 2.26.0__tar.gz → 2.29.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.
Files changed (82) hide show
  1. {euler_loading-2.26.0 → euler_loading-2.29.0}/.gitignore +5 -0
  2. euler_loading-2.29.0/CHANGELOG.md +100 -0
  3. {euler_loading-2.26.0 → euler_loading-2.29.0}/PKG-INFO +37 -3
  4. {euler_loading-2.26.0 → euler_loading-2.29.0}/README.md +36 -2
  5. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/README.md +1 -1
  6. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/loaders.md +155 -7
  7. euler_loading-2.29.0/euler_loading/__main__.py +10 -0
  8. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/_ds_crawler_utils.py +18 -0
  9. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/_resolution.py +63 -12
  10. euler_loading-2.29.0/euler_loading/cli.py +323 -0
  11. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/dataset.py +27 -8
  12. euler_loading-2.29.0/euler_loading/dry_run.py +851 -0
  13. euler_loading-2.29.0/euler_loading/loaders/_sky.py +168 -0
  14. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/generic.py +23 -0
  15. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/generic_dense_depth.py +27 -19
  16. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/muses.py +41 -42
  17. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/real_drive_sim.py +38 -11
  18. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/synscapes.py +51 -25
  19. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/vkitti2.py +26 -15
  20. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/loaders.json +45 -9
  21. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/generic.py +16 -0
  22. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/generic_dense_depth.py +7 -41
  23. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/muses.py +5 -59
  24. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/real_drive_sim.py +9 -15
  25. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/synscapes.py +5 -4
  26. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/vkitti2.py +8 -23
  27. {euler_loading-2.26.0 → euler_loading-2.29.0}/pyproject.toml +4 -1
  28. euler_loading-2.29.0/tests/test_dry_run.py +592 -0
  29. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_loaders.py +4 -2
  30. euler_loading-2.29.0/tests/test_sky_class.py +388 -0
  31. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_synscapes.py +18 -10
  32. euler_loading-2.26.0/CHANGELOG.md +0 -43
  33. {euler_loading-2.26.0 → euler_loading-2.29.0}/LICENSE +0 -0
  34. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/dataset.md +0 -0
  35. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/materialization.md +0 -0
  36. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/preprocessing.md +0 -0
  37. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/transform-descriptors.md +0 -0
  38. {euler_loading-2.26.0 → euler_loading-2.29.0}/docs/writing.md +0 -0
  39. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/__init__.py +0 -0
  40. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/_dataset_contract.py +0 -0
  41. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/_metadata.py +0 -0
  42. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/_writing.py +0 -0
  43. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/geometry.py +0 -0
  44. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/indexing.py +0 -0
  45. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/__init__.py +0 -0
  46. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/_annotations.py +0 -0
  47. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/_princeton_dense.py +0 -0
  48. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/_writer_utils.py +0 -0
  49. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/contracts.py +0 -0
  50. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  51. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/princeton_dense.py +0 -0
  52. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/__init__.py +0 -0
  53. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/__main__.py +0 -0
  54. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/generic.py +0 -0
  55. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  56. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/princeton_dense.py +0 -0
  57. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/materialized.py +0 -0
  58. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/muses.py +0 -0
  59. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/princeton_dense.py +0 -0
  60. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  61. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/synscapes.py +0 -0
  62. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/loaders/vkitti2.py +0 -0
  63. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/materialization.py +0 -0
  64. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/output_encoding.py +0 -0
  65. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/preprocessing.py +0 -0
  66. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/receipts.py +0 -0
  67. {euler_loading-2.26.0 → euler_loading-2.29.0}/euler_loading/transform_descriptors.py +0 -0
  68. {euler_loading-2.26.0 → euler_loading-2.29.0}/examples/README.md +0 -0
  69. {euler_loading-2.26.0 → euler_loading-2.29.0}/examples/real_drive_sim_preview.py +0 -0
  70. {euler_loading-2.26.0 → euler_loading-2.29.0}/examples/vkitti2_sample.py +0 -0
  71. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/__init__.py +0 -0
  72. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/conftest.py +0 -0
  73. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/example_rds_calib.json +0 -0
  74. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_dataset.py +0 -0
  75. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_id_schema.py +0 -0
  76. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_indexing.py +0 -0
  77. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_materialization.py +0 -0
  78. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_preprocessing.py +0 -0
  79. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_python_compat.py +0 -0
  80. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_real_dataset.py +0 -0
  81. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_transform_descriptors.py +0 -0
  82. {euler_loading-2.26.0 → euler_loading-2.29.0}/tests/test_writing.py +0 -0
@@ -42,3 +42,8 @@ TASK.md
42
42
 
43
43
  # uv
44
44
  uv.lock
45
+
46
+ # Stray npm lockfile. Nothing in this package is built with npm, and worktree
47
+ # tooling rewrites its "name" field to the branch directory on every branch,
48
+ # so tracking it produced a merge conflict on every quick-merge.
49
+ package-lock.json
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ - `Modality.function` picks a callable from the loader module the dataset head
6
+ names, instead of its `function`; the writer follows it.
7
+ `Modality(path, function="sky_mask")` reads a `class_segmentation` index as
8
+ boolean sky masks. It is exclusive with `Modality.loader`.
9
+ - The `sky_mask` loaders and writers of `real_drive_sim`, `vkitti2`,
10
+ `synscapes`, `muses` and `generic_dense_depth` read the sky definition from
11
+ the contract field `skyclass` (the sky label in the file's RGB view;
12
+ `[v, v, v]` for a single-channel label): first from the file's attributes,
13
+ then from the dataset head's meta. Their old hard-coded values are now
14
+ defaults, and they log the definition they use once.
15
+ - A `null` `skyclass`, the default from euler-dataset-contract's next release,
16
+ declares no sky: the loaders fall back to a deprecated key or their default,
17
+ and `generic` / `generic_dense_depth` raise. `[0, 0, 0]`, the contract's old
18
+ default, is still taken literally, but a head that declares it logs a warning
19
+ naming it the old default, in place of the warning about differing from the
20
+ loader's default. Loaders without a default warn too.
21
+ - Deprecate the per-loader sky keys `sky_class_id`, `sky_color`, `sky_class`
22
+ and `sky_mask` in favour of `skyclass`. They are still read when no
23
+ `skyclass` is given and raise a `DeprecationWarning`; when `skyclass` is
24
+ also given it wins, and the ignored key is logged as a warning.
25
+ - Warn when the resolved sky differs from a loader's built-in default.
26
+ - Reject values a loader cannot match instead of returning empty masks: a
27
+ non-grey `skyclass` for MUSES label PNGs (as for Synscapes and `generic`),
28
+ and anything but `[id, 0, 0]` for Real Drive Sim.
29
+ - The dry run decodes segmentation heads with their loader module's
30
+ `sky_mask` (at least `SKY_SAMPLES` files), reports the sky fraction, and
31
+ warns when no sampled file has sky or one is more than 90% sky.
32
+ - `real_drive_sim.sky_mask` compares the PNG's RGB value with `skyclass`
33
+ instead of only the red channel; for its own data, where sky is
34
+ `(29, 0, 0)`, the masks are unchanged.
35
+ - Add `generic.sky_mask` for `.npy`/`.npz` class-id maps, which requires
36
+ `skyclass`.
37
+ - The torch `sky_mask` loaders and writers of `real_drive_sim`, `vkitti2`,
38
+ `muses` and `generic_dense_depth` wrap the NumPy ones instead of
39
+ duplicating them.
40
+ - `muses.sky_mask`'s declared default meta is `skyclass: [23, 23, 23]`; the
41
+ old `sky_class: [0, 0, 23]` matched no pixel of a `labelIds` PNG.
42
+ - `MultiModalDataset` logs each modality's effective loader, noting when
43
+ `Modality.function` overrides the index.
44
+
45
+ ## 2.27.0 (unreleased)
46
+
47
+ - Add a dry-run command: `euler-loading <path>` (also `python -m
48
+ euler_loading <path>`) walks the ds-crawler artifact sets at or below one
49
+ folder, resolves the loader each dataset head declares in
50
+ `addons.euler_loading`, decodes a sample of the indexed files with it, and
51
+ reports shapes, dtypes and value ranges. Directories and `.zip` archives
52
+ are treated alike, nothing is written, and the exit status is non-zero when
53
+ a modality fails to resolve or decode. `euler_loading.dry_run.dry_run()`
54
+ exposes the same report to Python.
55
+ - `resolve_loader_module` and `resolve_writer_module` take a
56
+ `variant="gpu"|"cpu"` keyword, so the CPU modules can be resolved through
57
+ the same contract pathway as the torch ones. This is what the dry run's
58
+ `--cpu` uses to check archives on a torch-free install.
59
+
60
+ ## 2.26.0
61
+
62
+ - Add `read_extrinsics` to the Synscapes loaders, building a 4x4 rigid
63
+ transform from the six `camera.extrinsic` scalars. `transform_direction`
64
+ and `camera_axes` select the pose, its inverse, and vehicle or optical
65
+ camera axes; the assumed `Rz(yaw) @ Ry(pitch) @ Rx(roll)` order is recorded
66
+ in the modality metadata because the dataset does not document it.
67
+ - Add Synscapes writers for every modality, making the loader a
68
+ `DenseDepthCodec`. Depth writes a float32 EXR `Z` channel and needs a
69
+ filesystem path; the intrinsics and extrinsics writers merge into a single
70
+ `meta/<id>.json`, replacing it in one step and keeping its permissions,
71
+ symlink and unmodelled fields rather than truncating it in place.
72
+ - `sky_mask` now honours `meta['sky_class_id']`, or the same key in per-file
73
+ attributes, instead of hardcoding Cityscapes ID 23, so a dataset that
74
+ relabels sky round-trips through `write_sky_mask`.
75
+ - Accept the plural `camera.intrinsics` / `camera.extrinsics` spellings and
76
+ report missing camera fields by name instead of raising `KeyError`.
77
+
78
+ ## 2.24.0 (unreleased)
79
+
80
+ Add source-backed capture, strict per-output writers, explicit NPY/PNG encoding, typed calibration views and replay. Consolidate pinhole geometry and correct legacy skew scaling.
81
+
82
+
83
+ ## 2.23.0 (source changes; not published)
84
+
85
+ - Reject wrapped decoders during dataset export by checking actual built-in
86
+ function identity. Ordinary callable loading remains supported.
87
+ - Apply boolean thresholds before Pillow dtype conversion, and permit exact
88
+ int32/int64 crop and identity-resize plans while refusing float32 resampling.
89
+ - Add `SamplePreprocessor.export_descriptor`, `MultiModalDataset.export_transform_plan`,
90
+ `SerializableTransform`, and `resolve_transform_descriptor`, paired with contract 0.4.0.
91
+ - Freeze profiles, qualified source/calibration bindings, field policies, operation
92
+ order, and installed CPU backend versions. Infer output profiles and virtual
93
+ calibration geometry without materialization claims.
94
+ - Add explicit Torch/Pillow execution, corrected pinhole skew in the opt-in path,
95
+ validity-aware depth policy, source immutability, and worker validator registration.
96
+ - Cover five-field round trips, projected geometry, ambiguous bindings, unsupported
97
+ semantics, backend choices, schemas, and spawned workers in synthetic tests.
98
+
99
+ Legacy preprocessing and writer behavior remain unchanged. Execution receipts,
100
+ materialized metadata propagation, producer wiring, and GT replay are later work.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: euler-loading
3
- Version: 2.26.0
3
+ Version: 2.29.0
4
4
  Summary: Multi-modal PyTorch dataloader using ds-crawler indices
5
5
  Project-URL: Homepage, https://github.com/d-rothen/euler-loading
6
6
  Project-URL: Repository, https://github.com/d-rothen/euler-loading
@@ -169,11 +169,44 @@ RGB contract looks like this:
169
169
  ```
170
170
 
171
171
  `loader` selects a built-in loader module and `function` selects the callable
172
- inside it. Automatic resolution uses the GPU variant; pass a loader explicitly
173
- when you want the CPU variant or a custom callable. See
172
+ inside it. `Modality(path, function="sky_mask")` picks another callable from
173
+ the same module, e.g. boolean sky masks from a `class_segmentation` index,
174
+ with the sky taken from the head's `skyclass`. Automatic resolution uses the
175
+ GPU variant; pass a loader explicitly when you want the CPU variant or a
176
+ custom callable. See
174
177
  [Automatic loader resolution](docs/loaders.md#automatic-loader-resolution) for
175
178
  the full contract and writer rules.
176
179
 
180
+ ## Checking a dataset
181
+
182
+ Before any of that runs, point the bundled command at a folder to see what
183
+ euler-loading would make of it:
184
+
185
+ ```bash
186
+ euler-loading /data/vkitti2
187
+ ```
188
+
189
+ For every ds-crawler artifact set below that path it resolves the loader the
190
+ dataset declares, decodes a sample of the indexed files with it, and prints
191
+ the shape, dtype and value range that came back:
192
+
193
+ ```text
194
+ vkitti_2.0.3_rgb [ok]
195
+ path /data/vkitti2/vkitti_2.0.3_rgb
196
+ contract vkitti2_rgb, "Virtual KITTI 2 RGB", modality key: rgb
197
+ loader vkitti2.rgb -> euler_loading.loaders.gpu.vkitti2.rgb
198
+ index 21260 files; available splits: train, val
199
+ decoded 1/1
200
+ Scene01/clone/frames/rgb/Camera_0/rgb_00000.jpg -> Tensor (3, 375, 1242) float32 in [0, 1] (11.4 ms)
201
+ ```
202
+
203
+ Nothing is written, directories and `.zip` archives are treated alike, and the
204
+ exit status is non-zero if any modality failed to resolve or decode — so it
205
+ also works as a CI step. `--all` decodes every file instead of a sample,
206
+ `--cpu` checks the NumPy loaders on a torch-free install, and `--json` prints
207
+ the same report for a machine. See
208
+ [Dry-running loaders](docs/loaders.md#dry-running-loaders).
209
+
177
210
  ## What you get
178
211
 
179
212
  | | |
@@ -186,6 +219,7 @@ the full contract and writer rules.
186
219
  | **Loader resolution** | Loaders and writers resolve from the `dataset-head.json` `addons.euler_loading` contract, so datasets describe how to read themselves. |
187
220
  | **Writing back** | Resolved writers put inference outputs back in dataset-native formats, re-indexable with matching IDs. |
188
221
  | **Spatial preprocessing** | `SamplePreprocessor` resizes and crops consistently across images, depth, masks, ray maps *and* intrinsics. |
222
+ | **Dry runs** | `euler-loading <folder>` resolves and exercises every loader a dataset declares, without writing anything. |
189
223
 
190
224
  ## Built-in loaders
191
225
 
@@ -130,11 +130,44 @@ RGB contract looks like this:
130
130
  ```
131
131
 
132
132
  `loader` selects a built-in loader module and `function` selects the callable
133
- inside it. Automatic resolution uses the GPU variant; pass a loader explicitly
134
- when you want the CPU variant or a custom callable. See
133
+ inside it. `Modality(path, function="sky_mask")` picks another callable from
134
+ the same module, e.g. boolean sky masks from a `class_segmentation` index,
135
+ with the sky taken from the head's `skyclass`. Automatic resolution uses the
136
+ GPU variant; pass a loader explicitly when you want the CPU variant or a
137
+ custom callable. See
135
138
  [Automatic loader resolution](docs/loaders.md#automatic-loader-resolution) for
136
139
  the full contract and writer rules.
137
140
 
141
+ ## Checking a dataset
142
+
143
+ Before any of that runs, point the bundled command at a folder to see what
144
+ euler-loading would make of it:
145
+
146
+ ```bash
147
+ euler-loading /data/vkitti2
148
+ ```
149
+
150
+ For every ds-crawler artifact set below that path it resolves the loader the
151
+ dataset declares, decodes a sample of the indexed files with it, and prints
152
+ the shape, dtype and value range that came back:
153
+
154
+ ```text
155
+ vkitti_2.0.3_rgb [ok]
156
+ path /data/vkitti2/vkitti_2.0.3_rgb
157
+ contract vkitti2_rgb, "Virtual KITTI 2 RGB", modality key: rgb
158
+ loader vkitti2.rgb -> euler_loading.loaders.gpu.vkitti2.rgb
159
+ index 21260 files; available splits: train, val
160
+ decoded 1/1
161
+ Scene01/clone/frames/rgb/Camera_0/rgb_00000.jpg -> Tensor (3, 375, 1242) float32 in [0, 1] (11.4 ms)
162
+ ```
163
+
164
+ Nothing is written, directories and `.zip` archives are treated alike, and the
165
+ exit status is non-zero if any modality failed to resolve or decode — so it
166
+ also works as a CI step. `--all` decodes every file instead of a sample,
167
+ `--cpu` checks the NumPy loaders on a torch-free install, and `--json` prints
168
+ the same report for a machine. See
169
+ [Dry-running loaders](docs/loaders.md#dry-running-loaders).
170
+
138
171
  ## What you get
139
172
 
140
173
  | | |
@@ -147,6 +180,7 @@ the full contract and writer rules.
147
180
  | **Loader resolution** | Loaders and writers resolve from the `dataset-head.json` `addons.euler_loading` contract, so datasets describe how to read themselves. |
148
181
  | **Writing back** | Resolved writers put inference outputs back in dataset-native formats, re-indexable with matching IDs. |
149
182
  | **Spatial preprocessing** | `SamplePreprocessor` resizes and crops consistently across images, depth, masks, ray maps *and* intrinsics. |
183
+ | **Dry runs** | `euler-loading <folder>` resolves and exercises every loader a dataset declares, without writing anything. |
150
184
 
151
185
  ## Built-in loaders
152
186
 
@@ -3,7 +3,7 @@
3
3
  | Guide | Covers |
4
4
  |---|---|
5
5
  | [Dataset & modalities](dataset.md) | `Modality` and `MultiModalDataset` reference, the sample dict, hierarchical modalities, splits, scoped metadata, zip archives, layout-aware loading |
6
- | [Loaders & writers](loaders.md) | The loader contract, per-file attributes, automatic resolution, protocols, and the full built-in loader inventory |
6
+ | [Loaders & writers](loaders.md) | The loader contract, per-file attributes, automatic resolution, the `euler-loading` dry run, protocols, and the full built-in loader inventory |
7
7
  | [Preprocessing & transforms](preprocessing.md) | Cross-modal transforms, `SamplePreprocessor`, field kinds, calibration-aware resize and crop |
8
8
  | [Writing outputs](writing.md) | Writing predictions back in dataset-native formats and re-indexing them |
9
9
 
@@ -7,6 +7,7 @@ that file means.
7
7
  - [The contract](#the-contract)
8
8
  - [Per-file attributes](#per-file-attributes)
9
9
  - [Automatic loader resolution](#automatic-loader-resolution)
10
+ - [Dry-running loaders](#dry-running-loaders)
10
11
  - [Loader protocols](#loader-protocols)
11
12
  - [Built-in loaders](#built-in-loaders)
12
13
 
@@ -119,6 +120,14 @@ An explicit `Modality.loader` always takes precedence. Automatic resolution
119
120
  uses the GPU variant; import and pass a CPU loader explicitly when NumPy output
120
121
  is required.
121
122
 
123
+ `Modality.function` keeps the head's `loader` module but picks another callable
124
+ in it, and the writer follows it. `Modality(path, function="sky_mask")` reads a
125
+ `class_segmentation` index as boolean sky masks, taking the sky definition from
126
+ the head's `skyclass` (see [Sky masks](#sky-masks)). It cannot be combined with
127
+ `Modality.loader`. Each modality's effective loader is logged when the dataset
128
+ is built, e.g. `loaded with real_drive_sim.sky_mask (the index declares
129
+ 'class_segmentation')`.
130
+
122
131
  For compatibility, euler-loading can still read the legacy top-level
123
132
  `euler_loading` mapping from older indexes. New datasets should declare the
124
133
  addon in `dataset-head.json` as shown above.
@@ -129,6 +138,144 @@ Writers resolve from the same `addons.euler_loading` entry, in this order:
129
138
  2. for `function: "read_<suffix>"`, `write_<suffix>`,
130
139
  3. `write_<function>`.
131
140
 
141
+ ## Dry-running loaders
142
+
143
+ Resolution only pays off if the loader a dataset names can really read its
144
+ files. Point the bundled command at a path to find out, before a training run
145
+ depends on it:
146
+
147
+ ```bash
148
+ euler-loading /data/vkitti2
149
+ python -m euler_loading /data/vkitti2 # same thing, no console script needed
150
+ ```
151
+
152
+ It takes one path — a modality root, a `.zip` archive, or a folder holding
153
+ several of them — and for every ds-crawler artifact set below it:
154
+
155
+ 1. loads the index, or a named split,
156
+ 2. resolves the loader its `addons.euler_loading` entry declares,
157
+ 3. decodes a sample of the indexed files with that loader,
158
+ 4. reports the shape, dtype and value range of what came back.
159
+
160
+ Nothing is written and no dataset is constructed. A modality that cannot be
161
+ read is reported, not raised, so one broken archive does not hide the rest.
162
+
163
+ ```text
164
+ euler-loading dry-run: /data/vkitti2
165
+ loader variant: gpu (torch tensors)
166
+
167
+ vkitti_2.0.3_rgb [ok]
168
+ path /data/vkitti2/vkitti_2.0.3_rgb
169
+ contract vkitti2_rgb, "Virtual KITTI 2 RGB", modality key: rgb
170
+ loader vkitti2.rgb -> euler_loading.loaders.gpu.vkitti2.rgb
171
+ writer write_rgb
172
+ index 21260 files; available splits: train, val
173
+ decoded 1/1
174
+ Scene01/clone/frames/rgb/Camera_0/rgb_00000.jpg -> Tensor (3, 375, 1242) float32 in [0, 1] (11.4 ms)
175
+
176
+ vkitti_2.0.3_textgt [ok]
177
+ path /data/vkitti2/vkitti_2.0.3_textgt
178
+ contract vkitti2_intrinsics, "Virtual KITTI 2 intrinsics", modality key: camera_intrinsics
179
+ loader vkitti2.read_intrinsics -> euler_loading.loaders.gpu.vkitti2.read_intrinsics (hierarchical)
180
+ writer write_intrinsics
181
+ index 50 files
182
+ decoded 1/1
183
+ Scene01/clone/intrinsic.txt -> Tensor (3, 3) float32 in [0, 725] (0.6 ms)
184
+
185
+ 2 modalities checked: 2 ok, 0 failed; 2/2 files decoded
186
+ ```
187
+
188
+ The exit status is 0 when every modality resolved a loader and decoded every
189
+ file it was asked to, and 1 otherwise, so the command works as a CI step or a
190
+ job prologue.
191
+
192
+ | Flag | Effect |
193
+ |---|---|
194
+ | `-n, --samples N` | Files to decode per modality, spread evenly across the index (default 1). `0` resolves loaders without decoding anything. |
195
+ | `--all` | Decode every indexed file. Slow, but checks the whole archive. |
196
+ | `--split NAME` | Dry-run a named ds-crawler split instead of the canonical index. |
197
+ | `--scope SCOPE` | Read `.ds_crawler/SCOPE/` only, instead of every artifact set a root carries. |
198
+ | `--cpu` | Resolve the NumPy loaders instead of the torch ones. Works on an install without PyTorch. |
199
+ | `--max-depth N` | Directory levels below the path to search for dataset roots (default 3). |
200
+ | `--json` | Print the report as JSON instead of text. |
201
+ | `-v, --verbose` | Log debug output, including a traceback for every failed load. |
202
+
203
+ The path takes the same inline selectors as `Modality`, so
204
+ `euler-loading /data/muses.zip:train#scope=rgb` checks one split of one scope.
205
+
206
+ Discovery stops at the first dataset root down each branch: a directory
207
+ carrying its own `.ds_crawler/` is checked, never descended into, so a
208
+ dataset's own scene directories are never walked. A root with several
209
+ metadata scopes contributes one entry per scope, labelled with the selector
210
+ that addresses it.
211
+
212
+ ### Reading the report
213
+
214
+ | Line | What it is telling you |
215
+ |---|---|
216
+ | `error loader: ... no 'addons.euler_loading' entry` | the dataset head declares no loader, so only an explicit `Modality(..., loader=...)` can read it |
217
+ | `loader vkitti2.rgb (unresolved)` plus an `error` | the contract names a loader that is unknown, misspelled, or needs a dependency this install lacks — `--cpu` resolves the torch-free variant |
218
+ | `decoded 0/2` with per-file `FAILED` | the files are indexed and present, but the declared loader cannot decode them |
219
+ | `(hierarchical)` after the loader | the declared function reads per-scene data; pass that modality as `hierarchical_modalities`, not `modalities` |
220
+ | `error index: ...` | ds-crawler could not produce an index — a missing split, an unreadable archive, or a dataset head the contract rejects |
221
+ | `sky skyclass [29, 0, 0]: sky in 8/8 files, ...` | a segmentation head's files, decoded with its loader module's `sky_mask` (at least `SKY_SAMPLES` = 8 files) |
222
+ | a warning that `sky_mask` finds no sky, or marks a file as more than 90% sky | the head's `skyclass` probably names the wrong label; an `error sky_mask: ...` means the loader rejects it outright |
223
+ | a warning about shared file IDs | the modalities below the path have no file ID in common, so they cannot be intersected into one `MultiModalDataset` |
224
+
225
+ The same report is available as an API, which is what the command is a thin
226
+ shell around:
227
+
228
+ ```python
229
+ from euler_loading.dry_run import dry_run
230
+
231
+ report = dry_run("/data/vkitti2", samples=3)
232
+ if not report.ok:
233
+ for modality in report.modalities:
234
+ print(modality.label, modality.errors)
235
+
236
+ report.to_dict() # the same structure --json prints
237
+ ```
238
+
239
+ ## Sky masks
240
+
241
+ The dataset contract requires every `semantic_segmentation` head to declare
242
+ `modality.meta.skyclass`: the sky label as it appears in the segmentation file
243
+ viewed as RGB. That is the sky colour of a colour-coded map, and `[v, v, v]`
244
+ for a single-channel label `v`:
245
+
246
+ | Dataset | Encoding | `skyclass` |
247
+ |---|---|---|
248
+ | Virtual KITTI 2 | RGB class colours | `[90, 200, 255]` |
249
+ | Real Drive Sim | class ID in the red channel of an RGBA PNG | `[29, 0, 0]` |
250
+ | Synscapes | single-channel Cityscapes label IDs | `[23, 23, 23]` |
251
+ | MUSES `labelIds` | single-channel Cityscapes label IDs | `[23, 23, 23]` |
252
+ | Cityscapes train IDs, e.g. `generic` `.npy` maps | single-channel | `[10, 10, 10]` |
253
+
254
+ `skyclass: null`, the contract's default, declares no sky. `[0, 0, 0]` is taken
255
+ literally, as label 0 or black; contract releases before `null` used it as the
256
+ default, so heads written then can carry it without anyone having declared it.
257
+ A head that declares `[0, 0, 0]` is therefore logged as a warning.
258
+
259
+ Every `sky_mask` loader (and the matching `write_sky_mask`) resolves the sky in
260
+ this order, skipping a `null` `skyclass`, and logs the definition it uses once:
261
+
262
+ 1. `skyclass` in the file's ds-crawler attributes,
263
+ 2. `skyclass` in the dataset head's meta,
264
+ 3. a deprecated per-loader key — `sky_class_id` (Synscapes, Real Drive Sim,
265
+ MUSES), `sky_color` (VKITTI2), `sky_class` / `sky_mask` (MUSES) or
266
+ `sky_mask` (generic dense depth) — which raises a `DeprecationWarning`,
267
+ 4. the loader's built-in default; `generic.sky_mask` and
268
+ `generic_dense_depth.sky_mask` have none and require `skyclass`.
269
+
270
+ A deprecated key that `skyclass` overrides is logged as a warning, saying
271
+ whether it named the same sky. So is a resolved sky that differs from the
272
+ loader's default. Values a loader cannot match raise instead of silently
273
+ finding no sky: a non-grey `skyclass` for single-channel maps (Synscapes,
274
+ MUSES label PNGs, `generic`), and anything but `[id, 0, 0]` for Real Drive Sim,
275
+ which stores class IDs in the red channel. The [dry run](#dry-running-loaders)
276
+ decodes segmentation heads with `sky_mask` and warns about suspicious sky
277
+ fractions.
278
+
132
279
  ## Loader protocols
133
280
 
134
281
  `DenseDepthLoader` is a `runtime_checkable` Protocol defining the loader
@@ -179,7 +326,7 @@ annotations on the loader functions themselves.
179
326
  | `class_segmentation` | CHW / HWC | int64 | RGB-encoded class mask |
180
327
  | `instance_segmentation` | CHW / HWC | int64 | RGB-encoded instance mask |
181
328
  | `scene_flow` | CHW / HWC | float32 | Optical/scene flow, normalised to `[0, 1]` |
182
- | `sky_mask` | 1HW / HW | bool | Sky colour `[90, 200, 255]` in the class mask |
329
+ | `sky_mask` | 1HW / HW | bool | Head `skyclass` colour in the class mask, default `[90, 200, 255]` |
183
330
  | `read_intrinsics` | 3×3 | float32 | From a `.txt` file · *hierarchical* |
184
331
  | `read_extrinsics` | N×1 | float32 | From a `.txt` file · *hierarchical* |
185
332
 
@@ -193,14 +340,14 @@ Writers exist for every modality above.
193
340
  | `depth` | 1HW / HW | float32 | `img/depth/<id>.exr`, `Z` channel, planar depth in metres |
194
341
  | `class_segmentation` | 1HW / HW | int64 | `img/class/<id>.png`, original Cityscapes label IDs |
195
342
  | `instance_segmentation` | 1HW / HW | int64 | `img/instance/<id>.png`, decoded as `R + 256 * G + 65536 * B` |
196
- | `sky_mask` | 1HW / HW | bool | Sky label in `img/class/<id>.png`; ID `23` by default, or `meta['sky_class_id']` |
343
+ | `sky_mask` | 1HW / HW | bool | Sky label in `img/class/<id>.png`: the grey `skyclass`, ID `23` by default |
197
344
  | `read_intrinsics` | 3×3 | float32 | `camera.intrinsic` from `meta/<id>.json`: `fx`, `fy`, `u0`, `v0` |
198
345
  | `read_extrinsics` | 4×4 | float32 | `camera.extrinsic` from `meta/<id>.json`: `x`, `y`, `z`, `pitch`, `roll`, `yaw` |
199
346
 
200
347
  Writers exist for every modality above. `write_depth` needs a filesystem path
201
348
  because OpenEXR cannot write to a stream; dataset writers hand it a temporary
202
349
  file automatically, so zip outputs work unchanged. `write_sky_mask` emits a
203
- class image labelling sky with `meta['sky_class_id']`, default 23, and
350
+ class image labelling sky with the label `sky_mask` reads, and
204
351
  `write_intrinsics` and `write_extrinsics` merge into one `meta/<id>.json`
205
352
  rather than overwriting each other or the file's `scene` and `instance`
206
353
  blocks.
@@ -287,7 +434,7 @@ same resolution. Loaders preserve the stored resolution.
287
434
  | `reference_rgb` | CHW / HWC | float32 | Clear-weather reference frame, `[0, 1]` |
288
435
  | `semantic_segmentation` | 1HW / HW | int64 | Cityscapes `labelIds` or `labelTrainIds` |
289
436
  | `semantic_segmentation_color` | CHW / HWC | uint8 | Cityscapes `labelColor` PNG |
290
- | `sky_mask` | 1HW / HW | bool | `trainId=10`, `labelId=23`, or RGB `meta["sky_class"]` |
437
+ | `sky_mask` | 1HW / HW | bool | `skyclass` (grey for label PNGs, a colour for RGB class images); default `trainId=10` / `labelId=23` |
291
438
  | `panoptic_segmentation` | 1HW / HW | int64 | COCO-style RGB panoptic PNG → segment IDs |
292
439
  | `lidar_point_cloud` | (N, 6) | float64 | `.bin`: `x, y, z, intensity, ring, timestamp` |
293
440
  | `sparse_depth` | (N, 6) | float64 | Same points, for sparse-depth workflows |
@@ -303,7 +450,7 @@ same resolution. Loaders preserve the stored resolution.
303
450
  | `rgb` | CHW / HWC | float32 | PNG, `[0, 1]` |
304
451
  | `depth` | 1HW / HW | float32 | `.npz`, metres |
305
452
  | `class_segmentation` | 1HW / HW | int64 | Class IDs from the red channel of an RGBA PNG |
306
- | `sky_mask` | 1HW / HW | bool | Class ID `29` |
453
+ | `sky_mask` | 1HW / HW | bool | Head `skyclass` colour, default `[29, 0, 0]` (class ID 29) |
307
454
  | `calibration` | dict | — | `{sensor: {"K": (3,3), "T": (4,4), "distortion": (8,)}}` · *hierarchical* |
308
455
  | `all_intrinsics` | dict | — | `{sensor: (3, 3)}` · *hierarchical* |
309
456
  | `read_intrinsics` | 3×3 | float32 | Defaults to `CS_FRONT` · *hierarchical* |
@@ -330,7 +477,7 @@ Useful for datasets without a dedicated module.
330
477
  |---|---|---|---|
331
478
  | `rgb` | CHW / HWC | float32 | Image (`.png`, `.jpg`, `.bmp`, `.tif`) or NumPy (`.npy`, `.npz`), `[0, 1]` |
332
479
  | `depth` | 1HW / HW | float32 | Image or NumPy, returned as-is; honours `attributes["scale_to_meters_override"]` |
333
- | `sky_mask` | 1HW / HW | bool | Compares pixels against `meta["sky_mask"]` (`[R, G, B]`) — requires `meta` |
480
+ | `sky_mask` | 1HW / HW | bool | Compares pixels against the `skyclass` colour — required |
334
481
  | `read_intrinsics` | 3×3 | float32 | Returns `meta["intrinsics"]`, ignores the path — requires `meta` · *hierarchical* |
335
482
 
336
483
  ### Generic NumPy modalities — `generic`
@@ -347,8 +494,9 @@ assumptions beyond shape and dtype.
347
494
  | `atmospheric_light` | CHW / HWC | float32 |
348
495
  | `scattering_coefficient` | HW | float32 |
349
496
  | `semantic_segmentation` | HW | uint8 |
497
+ | `sky_mask` | HW | bool (head `skyclass` label, required) |
350
498
  | `instance_segmentation` | HW | uint16 |
351
499
  | `intrinsics` | 3×3 | float32 |
352
500
  | `sh_coeffs` | (N, C) | float32 |
353
501
 
354
- Writers exist for every function in this module.
502
+ Writers exist for every function in this module except `sky_mask`.
@@ -0,0 +1,10 @@
1
+ """``python -m euler_loading`` — dry-run the loaders a dataset declares."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import io
4
+ import zipfile
3
5
  from collections.abc import Mapping
4
6
  from pathlib import Path
5
7
  from typing import Any
@@ -22,6 +24,22 @@ from ds_crawler.zip_utils import (
22
24
  )
23
25
 
24
26
 
27
+ def read_zip_member(
28
+ archive: zipfile.ZipFile,
29
+ prefix: str,
30
+ relative_path: str,
31
+ ) -> io.BytesIO:
32
+ """Read one member of a zip-backed modality into an in-memory buffer.
33
+
34
+ The buffer carries the modality-relative path as its ``name`` so loaders
35
+ that branch on the file extension behave the same as for a filesystem
36
+ path.
37
+ """
38
+ buffer = io.BytesIO(archive.read(prefix + relative_path))
39
+ buffer.name = relative_path
40
+ return buffer
41
+
42
+
25
43
  def as_non_empty_str(value: Any) -> str | None:
26
44
  if value is None:
27
45
  return None