euler-loading 2.28.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 (81) hide show
  1. {euler_loading-2.28.0 → euler_loading-2.29.0}/CHANGELOG.md +42 -0
  2. {euler_loading-2.28.0 → euler_loading-2.29.0}/PKG-INFO +6 -3
  3. {euler_loading-2.28.0 → euler_loading-2.29.0}/README.md +5 -2
  4. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/loaders.md +58 -7
  5. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/_resolution.py +32 -6
  6. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/cli.py +3 -0
  7. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/dataset.py +21 -2
  8. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/dry_run.py +186 -58
  9. euler_loading-2.29.0/euler_loading/loaders/_sky.py +168 -0
  10. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/generic.py +23 -0
  11. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/generic_dense_depth.py +27 -19
  12. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/muses.py +41 -42
  13. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/real_drive_sim.py +38 -11
  14. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/synscapes.py +51 -25
  15. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/vkitti2.py +26 -15
  16. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/loaders.json +45 -9
  17. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/generic.py +16 -0
  18. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/generic_dense_depth.py +7 -41
  19. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/muses.py +5 -59
  20. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/real_drive_sim.py +9 -15
  21. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/synscapes.py +5 -4
  22. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/vkitti2.py +8 -23
  23. {euler_loading-2.28.0 → euler_loading-2.29.0}/pyproject.toml +1 -1
  24. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_loaders.py +4 -2
  25. euler_loading-2.29.0/tests/test_sky_class.py +388 -0
  26. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_synscapes.py +18 -10
  27. {euler_loading-2.28.0 → euler_loading-2.29.0}/.gitignore +0 -0
  28. {euler_loading-2.28.0 → euler_loading-2.29.0}/LICENSE +0 -0
  29. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/README.md +0 -0
  30. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/dataset.md +0 -0
  31. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/materialization.md +0 -0
  32. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/preprocessing.md +0 -0
  33. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/transform-descriptors.md +0 -0
  34. {euler_loading-2.28.0 → euler_loading-2.29.0}/docs/writing.md +0 -0
  35. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/__init__.py +0 -0
  36. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/__main__.py +0 -0
  37. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/_dataset_contract.py +0 -0
  38. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/_ds_crawler_utils.py +0 -0
  39. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/_metadata.py +0 -0
  40. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/_writing.py +0 -0
  41. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/geometry.py +0 -0
  42. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/indexing.py +0 -0
  43. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/__init__.py +0 -0
  44. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/_annotations.py +0 -0
  45. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/_princeton_dense.py +0 -0
  46. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/_writer_utils.py +0 -0
  47. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/contracts.py +0 -0
  48. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  49. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/cpu/princeton_dense.py +0 -0
  50. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/__init__.py +0 -0
  51. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/generate/__main__.py +0 -0
  52. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/generic.py +0 -0
  53. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  54. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/gpu/princeton_dense.py +0 -0
  55. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/materialized.py +0 -0
  56. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/muses.py +0 -0
  57. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/princeton_dense.py +0 -0
  58. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  59. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/synscapes.py +0 -0
  60. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/loaders/vkitti2.py +0 -0
  61. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/materialization.py +0 -0
  62. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/output_encoding.py +0 -0
  63. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/preprocessing.py +0 -0
  64. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/receipts.py +0 -0
  65. {euler_loading-2.28.0 → euler_loading-2.29.0}/euler_loading/transform_descriptors.py +0 -0
  66. {euler_loading-2.28.0 → euler_loading-2.29.0}/examples/README.md +0 -0
  67. {euler_loading-2.28.0 → euler_loading-2.29.0}/examples/real_drive_sim_preview.py +0 -0
  68. {euler_loading-2.28.0 → euler_loading-2.29.0}/examples/vkitti2_sample.py +0 -0
  69. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/__init__.py +0 -0
  70. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/conftest.py +0 -0
  71. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/example_rds_calib.json +0 -0
  72. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_dataset.py +0 -0
  73. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_dry_run.py +0 -0
  74. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_id_schema.py +0 -0
  75. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_indexing.py +0 -0
  76. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_materialization.py +0 -0
  77. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_preprocessing.py +0 -0
  78. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_python_compat.py +0 -0
  79. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_real_dataset.py +0 -0
  80. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_transform_descriptors.py +0 -0
  81. {euler_loading-2.28.0 → euler_loading-2.29.0}/tests/test_writing.py +0 -0
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
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
+
3
45
  ## 2.27.0 (unreleased)
4
46
 
5
47
  - Add a dry-run command: `euler-loading <path>` (also `python -m
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: euler-loading
3
- Version: 2.28.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,8 +169,11 @@ 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
 
@@ -130,8 +130,11 @@ 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
 
@@ -120,6 +120,14 @@ An explicit `Modality.loader` always takes precedence. Automatic resolution
120
120
  uses the GPU variant; import and pass a CPU loader explicitly when NumPy output
121
121
  is required.
122
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
+
123
131
  For compatibility, euler-loading can still read the legacy top-level
124
132
  `euler_loading` mapping from older indexes. New datasets should declare the
125
133
  addon in `dataset-head.json` as shown above.
@@ -210,6 +218,8 @@ that addresses it.
210
218
  | `decoded 0/2` with per-file `FAILED` | the files are indexed and present, but the declared loader cannot decode them |
211
219
  | `(hierarchical)` after the loader | the declared function reads per-scene data; pass that modality as `hierarchical_modalities`, not `modalities` |
212
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 |
213
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` |
214
224
 
215
225
  The same report is available as an API, which is what the command is a thin
@@ -226,6 +236,46 @@ if not report.ok:
226
236
  report.to_dict() # the same structure --json prints
227
237
  ```
228
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
+
229
279
  ## Loader protocols
230
280
 
231
281
  `DenseDepthLoader` is a `runtime_checkable` Protocol defining the loader
@@ -276,7 +326,7 @@ annotations on the loader functions themselves.
276
326
  | `class_segmentation` | CHW / HWC | int64 | RGB-encoded class mask |
277
327
  | `instance_segmentation` | CHW / HWC | int64 | RGB-encoded instance mask |
278
328
  | `scene_flow` | CHW / HWC | float32 | Optical/scene flow, normalised to `[0, 1]` |
279
- | `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]` |
280
330
  | `read_intrinsics` | 3×3 | float32 | From a `.txt` file · *hierarchical* |
281
331
  | `read_extrinsics` | N×1 | float32 | From a `.txt` file · *hierarchical* |
282
332
 
@@ -290,14 +340,14 @@ Writers exist for every modality above.
290
340
  | `depth` | 1HW / HW | float32 | `img/depth/<id>.exr`, `Z` channel, planar depth in metres |
291
341
  | `class_segmentation` | 1HW / HW | int64 | `img/class/<id>.png`, original Cityscapes label IDs |
292
342
  | `instance_segmentation` | 1HW / HW | int64 | `img/instance/<id>.png`, decoded as `R + 256 * G + 65536 * B` |
293
- | `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 |
294
344
  | `read_intrinsics` | 3×3 | float32 | `camera.intrinsic` from `meta/<id>.json`: `fx`, `fy`, `u0`, `v0` |
295
345
  | `read_extrinsics` | 4×4 | float32 | `camera.extrinsic` from `meta/<id>.json`: `x`, `y`, `z`, `pitch`, `roll`, `yaw` |
296
346
 
297
347
  Writers exist for every modality above. `write_depth` needs a filesystem path
298
348
  because OpenEXR cannot write to a stream; dataset writers hand it a temporary
299
349
  file automatically, so zip outputs work unchanged. `write_sky_mask` emits a
300
- class image labelling sky with `meta['sky_class_id']`, default 23, and
350
+ class image labelling sky with the label `sky_mask` reads, and
301
351
  `write_intrinsics` and `write_extrinsics` merge into one `meta/<id>.json`
302
352
  rather than overwriting each other or the file's `scene` and `instance`
303
353
  blocks.
@@ -384,7 +434,7 @@ same resolution. Loaders preserve the stored resolution.
384
434
  | `reference_rgb` | CHW / HWC | float32 | Clear-weather reference frame, `[0, 1]` |
385
435
  | `semantic_segmentation` | 1HW / HW | int64 | Cityscapes `labelIds` or `labelTrainIds` |
386
436
  | `semantic_segmentation_color` | CHW / HWC | uint8 | Cityscapes `labelColor` PNG |
387
- | `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` |
388
438
  | `panoptic_segmentation` | 1HW / HW | int64 | COCO-style RGB panoptic PNG → segment IDs |
389
439
  | `lidar_point_cloud` | (N, 6) | float64 | `.bin`: `x, y, z, intensity, ring, timestamp` |
390
440
  | `sparse_depth` | (N, 6) | float64 | Same points, for sparse-depth workflows |
@@ -400,7 +450,7 @@ same resolution. Loaders preserve the stored resolution.
400
450
  | `rgb` | CHW / HWC | float32 | PNG, `[0, 1]` |
401
451
  | `depth` | 1HW / HW | float32 | `.npz`, metres |
402
452
  | `class_segmentation` | 1HW / HW | int64 | Class IDs from the red channel of an RGBA PNG |
403
- | `sky_mask` | 1HW / HW | bool | Class ID `29` |
453
+ | `sky_mask` | 1HW / HW | bool | Head `skyclass` colour, default `[29, 0, 0]` (class ID 29) |
404
454
  | `calibration` | dict | — | `{sensor: {"K": (3,3), "T": (4,4), "distortion": (8,)}}` · *hierarchical* |
405
455
  | `all_intrinsics` | dict | — | `{sensor: (3, 3)}` · *hierarchical* |
406
456
  | `read_intrinsics` | 3×3 | float32 | Defaults to `CS_FRONT` · *hierarchical* |
@@ -427,7 +477,7 @@ Useful for datasets without a dedicated module.
427
477
  |---|---|---|---|
428
478
  | `rgb` | CHW / HWC | float32 | Image (`.png`, `.jpg`, `.bmp`, `.tif`) or NumPy (`.npy`, `.npz`), `[0, 1]` |
429
479
  | `depth` | 1HW / HW | float32 | Image or NumPy, returned as-is; honours `attributes["scale_to_meters_override"]` |
430
- | `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 |
431
481
  | `read_intrinsics` | 3×3 | float32 | Returns `meta["intrinsics"]`, ignores the path — requires `meta` · *hierarchical* |
432
482
 
433
483
  ### Generic NumPy modalities — `generic`
@@ -444,8 +494,9 @@ assumptions beyond shape and dtype.
444
494
  | `atmospheric_light` | CHW / HWC | float32 |
445
495
  | `scattering_coefficient` | HW | float32 |
446
496
  | `semantic_segmentation` | HW | uint8 |
497
+ | `sky_mask` | HW | bool (head `skyclass` label, required) |
447
498
  | `instance_segmentation` | HW | uint16 |
448
499
  | `intrinsics` | 3×3 | float32 |
449
500
  | `sh_coeffs` | (N, C) | float32 |
450
501
 
451
- Writers exist for every function in this module.
502
+ Writers exist for every function in this module except `sky_mask`.
@@ -142,8 +142,10 @@ def _resolve_loader(
142
142
  ) -> Callable[..., Any]:
143
143
  """Return the effective loader for a modality.
144
144
 
145
- An explicit ``Modality.loader`` always wins; *variant* only selects
146
- between the built-in GPU and CPU modules during automatic resolution.
145
+ An explicit ``Modality.loader`` always wins; ``Modality.function`` replaces
146
+ the dataset head's function within its loader module. *variant* only
147
+ selects between the built-in GPU and CPU modules during automatic
148
+ resolution.
147
149
  """
148
150
  if modality.loader is not None:
149
151
  return modality.loader
@@ -156,7 +158,8 @@ def _resolve_loader(
156
158
  f"'addons.euler_loading' entry."
157
159
  )
158
160
 
159
- for key in ("loader", "function"):
161
+ required = ("loader",) if modality.function is not None else ("loader", "function")
162
+ for key in required:
160
163
  if key not in euler_loading_meta:
161
164
  raise ValueError(
162
165
  f"Modality {modality_name!r}: no explicit loader provided and "
@@ -165,7 +168,7 @@ def _resolve_loader(
165
168
  )
166
169
 
167
170
  module_name: str = euler_loading_meta["loader"]
168
- func_name: str = euler_loading_meta["function"]
171
+ func_name: str = modality.function or euler_loading_meta["function"]
169
172
 
170
173
  module = resolve_loader_module(module_name, variant=variant)
171
174
 
@@ -184,6 +187,24 @@ def _resolve_loader(
184
187
  return func
185
188
 
186
189
 
190
+ def describe_loader(
191
+ loader: Callable[..., Any],
192
+ *,
193
+ modality: Modality,
194
+ index: Mapping[str, Any],
195
+ ) -> str:
196
+ """Name the effective loader for logs, e.g. ``real_drive_sim.sky_mask``."""
197
+ module = getattr(loader, "__module__", None) or ""
198
+ name = getattr(loader, "__qualname__", None) or repr(loader)
199
+ label = f"{module.rsplit('.', 1)[-1]}.{name}" if module else name
200
+ if modality.loader is not None:
201
+ return f"{label} (explicit loader)"
202
+ declared = as_non_empty_str((_get_euler_loading_meta(index) or {}).get("function"))
203
+ if modality.function is not None and declared not in (None, modality.function):
204
+ return f"{label} (the index declares {declared!r})"
205
+ return label
206
+
207
+
187
208
  def _writer_name_candidates(read_function_name: str, explicit: Any) -> list[str]:
188
209
  names: list[str] = []
189
210
  explicit_name = as_non_empty_str(explicit)
@@ -220,7 +241,9 @@ def _resolve_writer(
220
241
  return None
221
242
 
222
243
  module_name = as_non_empty_str(euler_loading_meta.get("loader"))
223
- read_function_name = as_non_empty_str(euler_loading_meta.get("function"))
244
+ read_function_name = as_non_empty_str(
245
+ modality.function or euler_loading_meta.get("function")
246
+ )
224
247
  if module_name is None or read_function_name is None:
225
248
  return None
226
249
 
@@ -234,9 +257,12 @@ def _resolve_writer(
234
257
  )
235
258
  return None
236
259
 
260
+ # The head's writer_function pairs with its own function, not an override.
237
261
  writer_names = _writer_name_candidates(
238
262
  read_function_name,
239
- euler_loading_meta.get("writer_function"),
263
+ None
264
+ if modality.function is not None
265
+ else euler_loading_meta.get("writer_function"),
240
266
  )
241
267
  for writer_name in writer_names:
242
268
  writer = getattr(module, writer_name, None)
@@ -148,6 +148,9 @@ def _render_modality(check: ModalityCheck) -> list[str]:
148
148
  detail = f"FAILED {sample.error}"
149
149
  lines.append(f"{_INDENT * 2}{name} -> {detail}")
150
150
 
151
+ if check.sky:
152
+ lines.append(_field("sky", check.sky))
153
+
151
154
  for warning in check.warnings:
152
155
  lines.append(_field("warning", warning))
153
156
  for error in check.errors:
@@ -43,6 +43,7 @@ from ._metadata import _build_runlog_entry, _get_ds_crawler_descriptor
43
43
  from ._resolution import (
44
44
  _resolve_loader,
45
45
  _resolve_writer,
46
+ describe_loader,
46
47
  loader_accepts_attributes,
47
48
  resolve_loader_module,
48
49
  resolve_writer_module,
@@ -369,6 +370,12 @@ class Modality:
369
370
  ``metadata["euler_loading"]`` are treated as defaults for
370
371
  roles and run logging. They do not select a loader; automatic
371
372
  loader resolution uses the dataset-head addon described above.
373
+ function: Optional name of the loader function to use from the module
374
+ that the dataset head's ``addons.euler_loading.loader``
375
+ names, instead of its ``function``. For example
376
+ ``"sky_mask"`` reads a ``class_segmentation`` index as
377
+ boolean sky masks. The writer follows the same function.
378
+ Mutually exclusive with *loader*.
372
379
  """
373
380
 
374
381
  path: str
@@ -385,8 +392,14 @@ class Modality:
385
392
  cache: bool | None = None
386
393
  collapse_single: bool = False
387
394
  metadata: dict[str, Any] = field(default_factory=dict)
395
+ function: str | None = None
388
396
 
389
397
  def __post_init__(self) -> None:
398
+ if self.loader is not None and self.function is not None:
399
+ raise ValueError(
400
+ f"Modality {self.path!r} sets both loader and function "
401
+ f"({self.function!r}); pass one of them."
402
+ )
390
403
  parsed_path, path_split, path_metadata_scope = parse_modality_path(self.path)
391
404
  if path_split is not None:
392
405
  if self.split is not None:
@@ -791,11 +804,14 @@ class MultiModalDataset(_BaseDataset):
791
804
  }
792
805
  self._lookups[name] = lookup
793
806
  logger.info(
794
- "Modality '%s': indexed %d files from %s%s",
807
+ "Modality '%s': indexed %d files from %s%s, loaded with %s",
795
808
  name,
796
809
  len(lookup),
797
810
  modality.path,
798
811
  _modality_selector_suffix(modality),
812
+ describe_loader(
813
+ self._resolved_loaders[name], modality=modality, index=index
814
+ ),
799
815
  )
800
816
 
801
817
  # -- Index hierarchical modalities -----------------------------------
@@ -825,12 +841,15 @@ class MultiModalDataset(_BaseDataset):
825
841
  total_files = sum(len(f) for f in files_by_level.values())
826
842
  logger.info(
827
843
  "Hierarchical modality '%s': indexed %d files across %d "
828
- "hierarchy levels from %s%s",
844
+ "hierarchy levels from %s%s, loaded with %s",
829
845
  name,
830
846
  total_files,
831
847
  len(files_by_level),
832
848
  modality.path,
833
849
  _modality_selector_suffix(modality),
850
+ describe_loader(
851
+ self._resolved_loaders[name], modality=modality, index=index
852
+ ),
834
853
  )
835
854
 
836
855
  # -- Compute common IDs (intersection across regular modalities) -----