euler-preprocess 3.13.0__tar.gz → 3.16.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_preprocess-3.16.0/CHANGELOG.md +5 -0
- euler_preprocess-3.16.0/MANIFEST.in +5 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/PKG-INFO +23 -11
- euler_preprocess-3.13.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.16.0/README.md +19 -27
- euler_preprocess-3.16.0/docs/spatial.md +61 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/cli.py +22 -0
- euler_preprocess-3.16.0/euler_preprocess/common/dataset.py +212 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/intrinsics.py +3 -2
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/output.py +72 -24
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/transform.py +9 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/transform.py +3 -2
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/radial/transform.py +3 -3
- euler_preprocess-3.16.0/euler_preprocess/spatial.py +110 -0
- euler_preprocess-3.13.0/README.md → euler_preprocess-3.16.0/euler_preprocess.egg-info/PKG-INFO +39 -9
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess.egg-info/SOURCES.txt +6 -0
- euler_preprocess-3.16.0/euler_preprocess.egg-info/requires.txt +11 -0
- euler_preprocess-3.16.0/examples/spatial-five-field.json +262 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/pyproject.toml +4 -2
- euler_preprocess-3.16.0/tests/test_modality_roles.py +192 -0
- euler_preprocess-3.16.0/tests/test_spatial.py +130 -0
- euler_preprocess-3.13.0/MANIFEST.in +0 -1
- euler_preprocess-3.13.0/euler_preprocess/common/dataset.py +0 -56
- euler_preprocess-3.13.0/euler_preprocess.egg-info/requires.txt +0 -9
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/LICENSE +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/inference.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/models.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/fog/pipeline.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/setup.cfg +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_fog_aux_outputs.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_fog_integration.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.13.0 → euler_preprocess-3.16.0}/tests/test_zip_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.16.0
|
|
4
4
|
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
5
|
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,9 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading
|
|
12
|
+
Requires-Dist: euler-loading>=2.24.0
|
|
13
|
+
Requires-Dist: ds-crawler>=2.11.0
|
|
14
|
+
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
13
15
|
Provides-Extra: gpu
|
|
14
16
|
Requires-Dist: torch; extra == "gpu"
|
|
15
17
|
Provides-Extra: progress
|
|
@@ -92,9 +94,7 @@ loader.
|
|
|
92
94
|
"modalities": {
|
|
93
95
|
"rgb": {"path": "/path/to/rgb", "split": "train"},
|
|
94
96
|
"depth": "/path/to/depth",
|
|
95
|
-
"semantic_segmentation": "/path/to/classSegmentation"
|
|
96
|
-
},
|
|
97
|
-
"hierarchical_modalities": {
|
|
97
|
+
"semantic_segmentation": "/path/to/classSegmentation",
|
|
98
98
|
"intrinsics": {"path": "/path/to/intrinsics"}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -107,17 +107,27 @@ loader.
|
|
|
107
107
|
| `output_slot` | Optional slot selector when `pipeline.output_targets` has several entries. Defaults to `rgb` for `fog` and `depth` for `sky-depth` / `radial`. |
|
|
108
108
|
| `sample` | Optional 0-based dataset index. Only `dataset[sample]` is transformed — useful for small benchmark slices of large datasets. |
|
|
109
109
|
| `samples` | Optional multi-sample selector: a list of indices (`[0, 10, 20]`) or a slice object (`{"start": 0, "stop": 1000, "step": 2, "count": 100}`). `stop` is exclusive, `count` caps the result. Mutually exclusive with `sample`. |
|
|
110
|
-
| `modalities` |
|
|
111
|
-
| `hierarchical_modalities` |
|
|
110
|
+
| `modalities` | All input modalities. Each value is a path string or an object with `path` and optional `split`. Whether a modality is per-sample or hierarchical is inferred (see below). |
|
|
111
|
+
| `hierarchical_modalities` | Legacy, optional. Same format; entries are pooled with `modalities` and resolved the same way. |
|
|
112
112
|
| `pipeline` | Optional runtime routing block compatible with `euler-inference`. |
|
|
113
113
|
|
|
114
114
|
**Required modalities per transform:**
|
|
115
115
|
|
|
116
|
-
| Transform |
|
|
116
|
+
| Transform | Required | Optional |
|
|
117
117
|
|---|---|---|
|
|
118
|
-
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics
|
|
118
|
+
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
|
|
119
119
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
|
-
| `radial` | `depth
|
|
120
|
+
| `radial` | `depth`, `intrinsics` | — |
|
|
121
|
+
|
|
122
|
+
**Per-sample vs. hierarchical modalities.** You don't declare a modality's role:
|
|
123
|
+
it is inferred from the ds-crawler indexes. A modality whose qualified IDs
|
|
124
|
+
(hierarchy path plus file ID) match the transform's per-sample inputs is joined per
|
|
125
|
+
sample, e.g. Synscapes' per-image `meta/<id>.json` intrinsics. A modality whose
|
|
126
|
+
files share no sample IDs but sit at or above the samples' hierarchy level is
|
|
127
|
+
matched hierarchically and shared by every sample beneath it, e.g. one intrinsics
|
|
128
|
+
file per scene or camera. A modality that is neither raises an error, and the
|
|
129
|
+
inferred roles are logged. Listing a per-sample modality under
|
|
130
|
+
`hierarchical_modalities` logs a warning and loads it per sample.
|
|
121
131
|
|
|
122
132
|
When a modality directory contains ds-crawler split files
|
|
123
133
|
(`.ds_crawler/split_<name>.json`), set `split` on that modality to select a subset.
|
|
@@ -675,7 +685,7 @@ d_radial(u, v) = d_planar(u, v) * sqrt(((u - cx)/fx)^2 + ((v - cy)/fy)^2 + 1)
|
|
|
675
685
|
```
|
|
676
686
|
|
|
677
687
|
The transform config takes no parameters (`{}`); intrinsics are read from the
|
|
678
|
-
`intrinsics`
|
|
688
|
+
`intrinsics` modality, stored either per sample or per scene/camera.
|
|
679
689
|
|
|
680
690
|
CLI runs write a source-backed depth dataset mirroring the input depth modality's layout
|
|
681
691
|
and writer metadata, with `meta.radial_depth` set to `true` in the emitted `index.json`.
|
|
@@ -686,3 +696,5 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
|
|
|
686
696
|
## License
|
|
687
697
|
|
|
688
698
|
[MIT](LICENSE) © Daniel Rothenpieler
|
|
699
|
+
|
|
700
|
+
See [Phase 2 spatial](docs/spatial.md) for the opt-in captured spatial workflow (3.14.0, unreleased).
|
euler_preprocess-3.13.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.16.0/README.md
RENAMED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: euler-preprocess
|
|
3
|
-
Version: 3.13.0
|
|
4
|
-
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
|
-
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
11
|
-
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading
|
|
13
|
-
Provides-Extra: gpu
|
|
14
|
-
Requires-Dist: torch; extra == "gpu"
|
|
15
|
-
Provides-Extra: progress
|
|
16
|
-
Requires-Dist: tqdm; extra == "progress"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
<!-- euler header — shared across the euler packages.
|
|
20
2
|
Per package, change only: the <h1>, the tagline, and the badge URLs. -->
|
|
21
3
|
<p align="center">
|
|
@@ -92,9 +74,7 @@ loader.
|
|
|
92
74
|
"modalities": {
|
|
93
75
|
"rgb": {"path": "/path/to/rgb", "split": "train"},
|
|
94
76
|
"depth": "/path/to/depth",
|
|
95
|
-
"semantic_segmentation": "/path/to/classSegmentation"
|
|
96
|
-
},
|
|
97
|
-
"hierarchical_modalities": {
|
|
77
|
+
"semantic_segmentation": "/path/to/classSegmentation",
|
|
98
78
|
"intrinsics": {"path": "/path/to/intrinsics"}
|
|
99
79
|
}
|
|
100
80
|
}
|
|
@@ -107,17 +87,27 @@ loader.
|
|
|
107
87
|
| `output_slot` | Optional slot selector when `pipeline.output_targets` has several entries. Defaults to `rgb` for `fog` and `depth` for `sky-depth` / `radial`. |
|
|
108
88
|
| `sample` | Optional 0-based dataset index. Only `dataset[sample]` is transformed — useful for small benchmark slices of large datasets. |
|
|
109
89
|
| `samples` | Optional multi-sample selector: a list of indices (`[0, 10, 20]`) or a slice object (`{"start": 0, "stop": 1000, "step": 2, "count": 100}`). `stop` is exclusive, `count` caps the result. Mutually exclusive with `sample`. |
|
|
110
|
-
| `modalities` |
|
|
111
|
-
| `hierarchical_modalities` |
|
|
90
|
+
| `modalities` | All input modalities. Each value is a path string or an object with `path` and optional `split`. Whether a modality is per-sample or hierarchical is inferred (see below). |
|
|
91
|
+
| `hierarchical_modalities` | Legacy, optional. Same format; entries are pooled with `modalities` and resolved the same way. |
|
|
112
92
|
| `pipeline` | Optional runtime routing block compatible with `euler-inference`. |
|
|
113
93
|
|
|
114
94
|
**Required modalities per transform:**
|
|
115
95
|
|
|
116
|
-
| Transform |
|
|
96
|
+
| Transform | Required | Optional |
|
|
117
97
|
|---|---|---|
|
|
118
|
-
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics
|
|
98
|
+
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
|
|
119
99
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
|
-
| `radial` | `depth
|
|
100
|
+
| `radial` | `depth`, `intrinsics` | — |
|
|
101
|
+
|
|
102
|
+
**Per-sample vs. hierarchical modalities.** You don't declare a modality's role:
|
|
103
|
+
it is inferred from the ds-crawler indexes. A modality whose qualified IDs
|
|
104
|
+
(hierarchy path plus file ID) match the transform's per-sample inputs is joined per
|
|
105
|
+
sample, e.g. Synscapes' per-image `meta/<id>.json` intrinsics. A modality whose
|
|
106
|
+
files share no sample IDs but sit at or above the samples' hierarchy level is
|
|
107
|
+
matched hierarchically and shared by every sample beneath it, e.g. one intrinsics
|
|
108
|
+
file per scene or camera. A modality that is neither raises an error, and the
|
|
109
|
+
inferred roles are logged. Listing a per-sample modality under
|
|
110
|
+
`hierarchical_modalities` logs a warning and loads it per sample.
|
|
121
111
|
|
|
122
112
|
When a modality directory contains ds-crawler split files
|
|
123
113
|
(`.ds_crawler/split_<name>.json`), set `split` on that modality to select a subset.
|
|
@@ -675,7 +665,7 @@ d_radial(u, v) = d_planar(u, v) * sqrt(((u - cx)/fx)^2 + ((v - cy)/fy)^2 + 1)
|
|
|
675
665
|
```
|
|
676
666
|
|
|
677
667
|
The transform config takes no parameters (`{}`); intrinsics are read from the
|
|
678
|
-
`intrinsics`
|
|
668
|
+
`intrinsics` modality, stored either per sample or per scene/camera.
|
|
679
669
|
|
|
680
670
|
CLI runs write a source-backed depth dataset mirroring the input depth modality's layout
|
|
681
671
|
and writer metadata, with `meta.radial_depth` set to `true` in the emitted `index.json`.
|
|
@@ -686,3 +676,5 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
|
|
|
686
676
|
## License
|
|
687
677
|
|
|
688
678
|
[MIT](LICENSE) © Daniel Rothenpieler
|
|
679
|
+
|
|
680
|
+
See [Phase 2 spatial](docs/spatial.md) for the opt-in captured spatial workflow (3.14.0, unreleased).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Deterministic spatial exports (3.14.0, unreleased)
|
|
2
|
+
|
|
3
|
+
`euler-preprocess spatial --config examples/spatial-five-field.json` is the
|
|
4
|
+
opt-in crop/resize producer. It uses loading 2.24.0, crawler 2.11.0 and contract
|
|
5
|
+
0.5.0. These source versions must be installed together. No benchmark evaluation
|
|
6
|
+
is enabled. Fog/model/value effects are not labelled replayable spatial exports.
|
|
7
|
+
|
|
8
|
+
The example config expects five source modality datasets. Change their paths to
|
|
9
|
+
your sources, and supply decoded profiles, image plane, qualified calibration
|
|
10
|
+
selection and source revision assertions that describe those files. The producer
|
|
11
|
+
hashes the actual source content itself; it does not accept supplied hashes as
|
|
12
|
+
independently checked. Paths in CLI configs are resolved against the config file.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from euler_preprocess.spatial import export_spatial
|
|
16
|
+
paths = export_spatial(config)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`preprocessing` uses the shared `SamplePreprocessor` config. `bindings` has
|
|
20
|
+
`fields`, `image_planes`, `revisions` (source alias to revision digest) and an
|
|
21
|
+
optional explicit CPU `backend` (`torch_cpu` by default). `outputs` selects
|
|
22
|
+
fields; each needs `path`, a distinct derived `dataset_id`, and `revision`.
|
|
23
|
+
Optional output options are `metadata_scope`, `resume` (directories only), and
|
|
24
|
+
`encoding: {"id":"npy"}` or a supported PNG codec, e.g.
|
|
25
|
+
`{"id":"png_depth16","scale":0.001}`. NPY preserves K/rays exactly. PNG RGB
|
|
26
|
+
expects normalized float32 values, masks require bool, and depth requires meters.
|
|
27
|
+
Clipping is rejected. Codec parameters, profiles and real dimensions are stored
|
|
28
|
+
alongside the bytes; source metadata cannot override them.
|
|
29
|
+
|
|
30
|
+
`spatial_variants` replaces top-level preprocessing/bindings with explicit named
|
|
31
|
+
`{preprocessing, bindings}` entries. Every variant is frozen in the output plan
|
|
32
|
+
before writing. Variants can select different qualified calibrations or concrete
|
|
33
|
+
geometries. A `samples` list selects source indices. Optional
|
|
34
|
+
`mapping: {variant: {source_full_id: output_full_id}}` supplies exact output IDs.
|
|
35
|
+
Otherwise a single variant preserves source IDs; multiple variants append the
|
|
36
|
+
explicit variant ID as a hierarchy component. Conflicting mappings are rejected.
|
|
37
|
+
The producer computes the complete expected output set before starting writes.
|
|
38
|
+
Use explicit mappings/subsets, rather than inferring relationships from filenames.
|
|
39
|
+
|
|
40
|
+
Each sample carries the actual producer receipt into `SourceBackedOutputBackend`.
|
|
41
|
+
`prepare_output_backend` and `prepare_output_backends` also accept explicitly
|
|
42
|
+
configured `materialization_outputs` for Python integrations; each output option
|
|
43
|
+
includes a frozen `derivation` and optional `expected_full_ids`. The dataset must
|
|
44
|
+
already have source-backed transform capture enabled. An arbitrary transform's
|
|
45
|
+
changed values will fail the receipt checks. Ordinary attributes are preserved,
|
|
46
|
+
while the new receipt and output encoding metadata are authoritative.
|
|
47
|
+
|
|
48
|
+
`receipt_sidecars: true` moves receipts to relative content-addressed records.
|
|
49
|
+
Failed encoding cannot register a receipt. Finalization verifies source-backed
|
|
50
|
+
replay, output bytes, expected IDs and references before publishing each logical
|
|
51
|
+
output dataset. There is no all-or-nothing transaction across several output
|
|
52
|
+
roots: each head is independently complete for its one selected modality.
|
|
53
|
+
Directory resume is idempotent with the same source/content/recipe/revision;
|
|
54
|
+
ZIP supports one owner/one finalization. Interrupted outputs do not advertise
|
|
55
|
+
complete metadata.
|
|
56
|
+
|
|
57
|
+
The common dataset builder now consumes `preprocessing` for ordinary pipelines
|
|
58
|
+
as well. Existing callable pipelines retain their APIs. Strict receipts are
|
|
59
|
+
created only by the explicit spatial/materialization path. Reload through
|
|
60
|
+
`MultiModalDataset` loads stored bytes once; explicit loading replay and typed
|
|
61
|
+
`CalibrationView` resolve the original geometry without evaluator heuristics.
|
|
@@ -244,6 +244,23 @@ def _cmd_radial(args: argparse.Namespace) -> int:
|
|
|
244
244
|
return _run_transform(args, RadialTransform)
|
|
245
245
|
|
|
246
246
|
|
|
247
|
+
def _cmd_spatial(args: argparse.Namespace) -> int:
|
|
248
|
+
from euler_preprocess.spatial import export_spatial
|
|
249
|
+
|
|
250
|
+
config_path = Path(args.config).resolve()
|
|
251
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
252
|
+
for group in ("modalities", "hierarchical_modalities"):
|
|
253
|
+
for key, entry in config.get(group, {}).items():
|
|
254
|
+
if isinstance(entry, str):
|
|
255
|
+
config[group][key] = str(_resolve(entry, config_path.parent))
|
|
256
|
+
else:
|
|
257
|
+
entry["path"] = str(_resolve(entry["path"], config_path.parent))
|
|
258
|
+
for entry in config["outputs"].values():
|
|
259
|
+
entry["path"] = str(_resolve(entry["path"], config_path.parent))
|
|
260
|
+
export_spatial(config)
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
247
264
|
def parse_args() -> argparse.Namespace:
|
|
248
265
|
parser = argparse.ArgumentParser(
|
|
249
266
|
description="Preprocessing transforms for RGB+depth datasets.",
|
|
@@ -281,6 +298,11 @@ def parse_args() -> argparse.Namespace:
|
|
|
281
298
|
)
|
|
282
299
|
radial_parser.set_defaults(func=_cmd_radial)
|
|
283
300
|
|
|
301
|
+
spatial_parser = subparsers.add_parser(
|
|
302
|
+
"spatial", parents=[common], help="Export deterministic crop/resize with verified execution receipts.",
|
|
303
|
+
)
|
|
304
|
+
spatial_parser.set_defaults(func=_cmd_spatial)
|
|
305
|
+
|
|
284
306
|
args = parser.parse_args()
|
|
285
307
|
if not hasattr(args, "func"):
|
|
286
308
|
parser.print_help()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
from euler_preprocess.common.logging import get_logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _make_modality(entry: str | dict):
|
|
9
|
+
from euler_loading import Modality
|
|
10
|
+
|
|
11
|
+
if isinstance(entry, str):
|
|
12
|
+
return Modality(entry)
|
|
13
|
+
return Modality(
|
|
14
|
+
entry["path"],
|
|
15
|
+
**{
|
|
16
|
+
key: entry[key]
|
|
17
|
+
for key in (
|
|
18
|
+
"split",
|
|
19
|
+
"metadata_scope",
|
|
20
|
+
"collapse_single",
|
|
21
|
+
"cache",
|
|
22
|
+
"applies_to",
|
|
23
|
+
"hierarchy_scope",
|
|
24
|
+
)
|
|
25
|
+
if key in entry
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_index_tree(name: str, modality) -> dict:
|
|
31
|
+
"""Load a modality's ds-crawler tree the same way ``MultiModalDataset`` does."""
|
|
32
|
+
from ds_crawler import index_dataset_from_path
|
|
33
|
+
from euler_loading._ds_crawler_utils import load_index_output
|
|
34
|
+
from euler_loading.dataset import _get_index_tree, _resolve_metadata_scope
|
|
35
|
+
|
|
36
|
+
modality = _resolve_metadata_scope(name, modality)
|
|
37
|
+
index = load_index_output(
|
|
38
|
+
modality.path,
|
|
39
|
+
split=modality.split,
|
|
40
|
+
metadata_scope=modality.metadata_scope,
|
|
41
|
+
index_dataset_from_path_fn=index_dataset_from_path,
|
|
42
|
+
)
|
|
43
|
+
return _get_index_tree(index)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _check_attachment(
|
|
47
|
+
name: str, seed: str, tree: dict, samples_per_level: Counter
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Check that hierarchical modality *name* applies to the samples by tree position."""
|
|
50
|
+
from euler_loading.indexing import collect_hierarchical_files, match_hierarchical_files
|
|
51
|
+
|
|
52
|
+
files_by_level = collect_hierarchical_files(tree)
|
|
53
|
+
attached = {
|
|
54
|
+
level: len(match_hierarchical_files(level, files_by_level))
|
|
55
|
+
for level in samples_per_level
|
|
56
|
+
}
|
|
57
|
+
covered = sum(count for level, count in samples_per_level.items() if attached[level])
|
|
58
|
+
if not covered:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Modality '{name}' shares no sample IDs with '{seed}' and none of its "
|
|
61
|
+
"files sit at or above the hierarchy level of any sample, so it can be "
|
|
62
|
+
"neither joined per sample nor matched hierarchically. Check that its "
|
|
63
|
+
f"ds-crawler IDs and hierarchy follow the same conventions as '{seed}'."
|
|
64
|
+
)
|
|
65
|
+
logger = get_logger()
|
|
66
|
+
total = sum(samples_per_level.values())
|
|
67
|
+
log = logger.info if covered == total else logger.warning
|
|
68
|
+
log(
|
|
69
|
+
"Modality '%s' is hierarchical (no sample IDs shared with '%s'); it applies "
|
|
70
|
+
"to %d/%d samples by hierarchy position.",
|
|
71
|
+
name,
|
|
72
|
+
seed,
|
|
73
|
+
covered,
|
|
74
|
+
total,
|
|
75
|
+
)
|
|
76
|
+
most = max(attached.values())
|
|
77
|
+
if most > 1:
|
|
78
|
+
logger.warning(
|
|
79
|
+
"Hierarchical modality '%s' attaches up to %d files to one sample. If it "
|
|
80
|
+
"holds per-sample data, its ds-crawler IDs do not match '%s'.",
|
|
81
|
+
name,
|
|
82
|
+
most,
|
|
83
|
+
seed,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve_roles(
|
|
88
|
+
modalities: dict, per_sample: set[str]
|
|
89
|
+
) -> tuple[list[str], list[str]]:
|
|
90
|
+
"""Split modality names into regular and hierarchical ones.
|
|
91
|
+
|
|
92
|
+
The role follows from the ds-crawler indexes. A modality is regular
|
|
93
|
+
(joined per sample) when its qualified file IDs -- hierarchy path plus
|
|
94
|
+
file ID -- overlap those of the seed modality. Otherwise it is
|
|
95
|
+
hierarchical (matched by tree position, like per-scene intrinsics) and
|
|
96
|
+
must sit at or above the hierarchy level of at least one sample.
|
|
97
|
+
|
|
98
|
+
*per_sample* modalities are regular by contract and the first one is the
|
|
99
|
+
seed. Without one, the seed is the modality aligned with the most others,
|
|
100
|
+
then the one with the most files, then the first listed.
|
|
101
|
+
"""
|
|
102
|
+
from euler_loading.indexing import collect_files
|
|
103
|
+
|
|
104
|
+
names = list(modalities)
|
|
105
|
+
if len(names) < 2 or per_sample.issuperset(names):
|
|
106
|
+
return names, []
|
|
107
|
+
|
|
108
|
+
trees = {name: _load_index_tree(name, modalities[name]) for name in names}
|
|
109
|
+
# Qualified sample ID -> hierarchy path, keyed like euler-loading's lookups.
|
|
110
|
+
sample_ids = {}
|
|
111
|
+
for name, tree in trees.items():
|
|
112
|
+
sample_ids[name] = {
|
|
113
|
+
"/".join(r.hierarchy_path + (r.file_entry["id"],)): r.hierarchy_path
|
|
114
|
+
for r in collect_files(tree)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def aligned(name: str, other: str) -> bool:
|
|
118
|
+
return not sample_ids[name].keys().isdisjoint(sample_ids[other].keys())
|
|
119
|
+
|
|
120
|
+
anchors = [name for name in names if name in per_sample]
|
|
121
|
+
seed = anchors[0] if anchors else max(
|
|
122
|
+
names,
|
|
123
|
+
key=lambda name: (
|
|
124
|
+
sum(aligned(other, name) for other in names),
|
|
125
|
+
len(sample_ids[name]),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
regular = [
|
|
129
|
+
name
|
|
130
|
+
for name in names
|
|
131
|
+
if name in per_sample or name == seed or aligned(name, seed)
|
|
132
|
+
]
|
|
133
|
+
hierarchical = [name for name in names if name not in regular]
|
|
134
|
+
get_logger().info(
|
|
135
|
+
"Modality roles inferred from ds-crawler indexes: per-sample %s; "
|
|
136
|
+
"hierarchical %s",
|
|
137
|
+
", ".join(regular),
|
|
138
|
+
", ".join(hierarchical) or "none",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Without common samples MultiModalDataset raises its own, clearer error.
|
|
142
|
+
common = set(sample_ids[seed]).intersection(*(sample_ids[name] for name in regular))
|
|
143
|
+
if common:
|
|
144
|
+
samples_per_level = Counter(sample_ids[seed][sample_id] for sample_id in common)
|
|
145
|
+
for name in hierarchical:
|
|
146
|
+
_check_attachment(name, seed, trees[name], samples_per_level)
|
|
147
|
+
return regular, hierarchical
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_dataset(
|
|
151
|
+
config: dict,
|
|
152
|
+
required_modalities: set[str],
|
|
153
|
+
required_hierarchical: set[str] | None = None,
|
|
154
|
+
):
|
|
155
|
+
"""Build a ``MultiModalDataset`` from a config dict.
|
|
156
|
+
|
|
157
|
+
Each modality entry is either a plain path string or a dict with
|
|
158
|
+
``path`` and optional ``split``. Loader resolution (which function to
|
|
159
|
+
call, which module to use) is handled by euler-loading via the
|
|
160
|
+
ds-crawler index at each path — point the config at a path whose
|
|
161
|
+
index declares the function you want (e.g. a ``sky_mask`` index for
|
|
162
|
+
boolean sky masks vs. a ``class_segmentation`` index for raw class
|
|
163
|
+
id maps).
|
|
164
|
+
|
|
165
|
+
Every modality can be listed under ``modalities``: whether it is joined
|
|
166
|
+
per sample or matched hierarchically (e.g. per-scene intrinsics) is
|
|
167
|
+
inferred from the ds-crawler indexes. Entries under the legacy
|
|
168
|
+
``hierarchical_modalities`` key are pooled with them and resolved the
|
|
169
|
+
same way. *required_modalities* are per-sample inputs and anchor that
|
|
170
|
+
inference; *required_hierarchical* names further required modalities,
|
|
171
|
+
whose role is inferred like any other.
|
|
172
|
+
"""
|
|
173
|
+
from euler_loading import MultiModalDataset, SamplePreprocessor
|
|
174
|
+
|
|
175
|
+
raw_modalities = config.get("modalities", {})
|
|
176
|
+
raw_hierarchical = config.get("hierarchical_modalities", {})
|
|
177
|
+
|
|
178
|
+
duplicated = raw_modalities.keys() & raw_hierarchical.keys()
|
|
179
|
+
if duplicated:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"Modalities listed under both 'modalities' and "
|
|
182
|
+
f"'hierarchical_modalities': {', '.join(sorted(duplicated))}"
|
|
183
|
+
)
|
|
184
|
+
configured = {**raw_modalities, **raw_hierarchical}
|
|
185
|
+
|
|
186
|
+
required = set(required_modalities) | set(required_hierarchical or ())
|
|
187
|
+
missing = required - configured.keys()
|
|
188
|
+
if missing:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Missing required modalities in config: {', '.join(sorted(missing))}. "
|
|
191
|
+
f"'modalities' must contain at least: {', '.join(sorted(required))}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
modalities = {name: _make_modality(entry) for name, entry in configured.items()}
|
|
195
|
+
regular, hierarchical = _resolve_roles(modalities, set(required_modalities))
|
|
196
|
+
logger = get_logger()
|
|
197
|
+
for name in regular:
|
|
198
|
+
if name in raw_hierarchical:
|
|
199
|
+
logger.warning(
|
|
200
|
+
"Modality '%s' is listed under 'hierarchical_modalities' but holds "
|
|
201
|
+
"per-sample data; loading it per sample.",
|
|
202
|
+
name,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return MultiModalDataset(
|
|
206
|
+
modalities={name: modalities[name] for name in regular},
|
|
207
|
+
hierarchical_modalities={name: modalities[name] for name in hierarchical}
|
|
208
|
+
or None,
|
|
209
|
+
transforms=[SamplePreprocessor.from_config(config["preprocessing"])]
|
|
210
|
+
if config.get("preprocessing")
|
|
211
|
+
else None,
|
|
212
|
+
)
|
|
@@ -13,8 +13,9 @@ except ImportError:
|
|
|
13
13
|
def extract_intrinsics(sample: dict) -> np.ndarray | None:
|
|
14
14
|
"""Extract the 3x3 intrinsics matrix from a sample dict.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
Per-sample intrinsics are stored directly as ``sample["intrinsics"]``.
|
|
17
|
+
Hierarchical modalities are stored as ``sample[name][file_id]``; for
|
|
18
|
+
intrinsics the convention is ``sample["intrinsics"]["intrinsics"]``.
|
|
18
19
|
"""
|
|
19
20
|
intr_dict = sample.get("intrinsics")
|
|
20
21
|
if intr_dict is None:
|
|
@@ -8,19 +8,20 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from ds_crawler import DatasetWriter, ZipDatasetWriter
|
|
11
|
+
from ds_crawler import DatasetWriter, ValidatedDatasetWriter, ZipDatasetWriter
|
|
12
12
|
from ds_crawler.zip_utils import (
|
|
13
13
|
METADATA_DIR,
|
|
14
|
-
OUTPUT_FILENAME as DS_CRAWLER_INDEX_FILENAME,
|
|
15
14
|
get_metadata_entry_name,
|
|
16
15
|
)
|
|
16
|
+
from ds_crawler.zip_utils import (
|
|
17
|
+
OUTPUT_FILENAME as DS_CRAWLER_INDEX_FILENAME,
|
|
18
|
+
)
|
|
17
19
|
from euler_loading import MultiModalDataset
|
|
18
20
|
from euler_loading.dataset import create_dataset_writer_from_index
|
|
19
21
|
from euler_loading.loaders._writer_utils import supports_stream_target
|
|
20
22
|
|
|
21
23
|
from euler_preprocess.common.io import OutputWriter
|
|
22
24
|
|
|
23
|
-
|
|
24
25
|
PIPELINE_OUTPUTS_MANIFEST_VERSION = 1
|
|
25
26
|
_PIPELINE_OUTPUT_STORAGE_KINDS = {"directory", "zip", "file"}
|
|
26
27
|
_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}
|
|
@@ -420,9 +421,32 @@ class SourceBackedOutputBackend:
|
|
|
420
421
|
output_full_id: str | None = None,
|
|
421
422
|
output_basename: str | None = None,
|
|
422
423
|
attributes: dict[str, Any] | None = None,
|
|
424
|
+
variant_id: str | None = None,
|
|
425
|
+
sidecar: bool = False,
|
|
423
426
|
) -> Path:
|
|
424
427
|
del default_path
|
|
425
428
|
|
|
429
|
+
if isinstance(self.dataset_writer, ValidatedDatasetWriter):
|
|
430
|
+
from euler_loading import write_materialized
|
|
431
|
+
|
|
432
|
+
provenance = sample.get("provenance")
|
|
433
|
+
if variant_id is not None and (
|
|
434
|
+
not provenance or provenance["variant_id"] != variant_id
|
|
435
|
+
):
|
|
436
|
+
raise ValueError("output variant disagrees with captured execution")
|
|
437
|
+
source_attrs = dict(
|
|
438
|
+
sample.get("attributes", {}).get(self.source_modality, {})
|
|
439
|
+
)
|
|
440
|
+
source_attrs.pop("euler_transforms", None)
|
|
441
|
+
source_attrs.pop("output_encoding", None)
|
|
442
|
+
source_attrs.update(attributes or {})
|
|
443
|
+
return Path(write_materialized(
|
|
444
|
+
self.dataset_writer, value, provenance,
|
|
445
|
+
output_full_id=output_full_id or sample["full_id"],
|
|
446
|
+
output_basename=output_basename,
|
|
447
|
+
attributes=source_attrs, sidecar=sidecar,
|
|
448
|
+
))
|
|
449
|
+
|
|
426
450
|
sample_id = sample.get("id", "?")
|
|
427
451
|
full_id = str(output_full_id or sample.get("full_id") or f"/{sample_id}")
|
|
428
452
|
meta = sample.get("meta")
|
|
@@ -505,15 +529,11 @@ class SourceBackedOutputBackend:
|
|
|
505
529
|
|
|
506
530
|
def set_hierarchy_separator(self, separator: str) -> None:
|
|
507
531
|
"""Set the writer hierarchy separator used for future entries."""
|
|
508
|
-
|
|
532
|
+
self.dataset_writer.set_hierarchy_separator(separator)
|
|
509
533
|
|
|
510
534
|
def add_head_addon(self, name: str, payload: dict[str, Any]) -> None:
|
|
511
535
|
"""Add a dataset-head addon before the writer saves its artifacts."""
|
|
512
|
-
|
|
513
|
-
addons = getattr(head, "addons", None)
|
|
514
|
-
if not isinstance(addons, dict):
|
|
515
|
-
raise RuntimeError("Unsupported dataset writer head object")
|
|
516
|
-
addons[name] = dict(payload)
|
|
536
|
+
self.dataset_writer.set_head_addon(name, payload)
|
|
517
537
|
self.index_overrides[name] = dict(payload)
|
|
518
538
|
|
|
519
539
|
def write_json(self, path: Path, data: dict[str, Any]) -> None:
|
|
@@ -522,17 +542,13 @@ class SourceBackedOutputBackend:
|
|
|
522
542
|
)
|
|
523
543
|
|
|
524
544
|
def finalize(self) -> None:
|
|
525
|
-
self.dataset_writer
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
self.
|
|
529
|
-
filename=DS_CRAWLER_INDEX_FILENAME
|
|
530
|
-
)
|
|
545
|
+
if isinstance(self.dataset_writer, ValidatedDatasetWriter):
|
|
546
|
+
self.dataset_writer.finalize()
|
|
547
|
+
else:
|
|
548
|
+
self.dataset_writer.save_index(filename=DS_CRAWLER_INDEX_FILENAME)
|
|
549
|
+
_patch_output_index(self.dataset_writer, self.index_overrides, filename=DS_CRAWLER_INDEX_FILENAME)
|
|
531
550
|
if self.pipeline_manifest_path and self.pipeline_manifest_targets:
|
|
532
|
-
_write_pipeline_outputs_manifest(
|
|
533
|
-
self.pipeline_manifest_path,
|
|
534
|
-
self.pipeline_manifest_targets,
|
|
535
|
-
)
|
|
551
|
+
_write_pipeline_outputs_manifest(self.pipeline_manifest_path, self.pipeline_manifest_targets)
|
|
536
552
|
|
|
537
553
|
|
|
538
554
|
def _resolve_output_slot(
|
|
@@ -644,6 +660,10 @@ def prepare_output_backend(
|
|
|
644
660
|
f"{transform_class.__name__} does not declare SOURCE_MODALITY"
|
|
645
661
|
)
|
|
646
662
|
|
|
663
|
+
materialized = config.get("materialization_outputs", {}).get(source_modality)
|
|
664
|
+
if materialized is not None:
|
|
665
|
+
return prepare_materialized_backend(dataset, source_modality, materialized)
|
|
666
|
+
|
|
647
667
|
pipeline = parse_pipeline_config(config)
|
|
648
668
|
pipeline_target = _select_pipeline_target(config, pipeline, transform_class)
|
|
649
669
|
root = _resolve_output_root(config, pipeline, pipeline_target)
|
|
@@ -834,6 +854,12 @@ def prepare_output_backends(
|
|
|
834
854
|
``OUTPUT_SLOTS`` order.
|
|
835
855
|
"""
|
|
836
856
|
|
|
857
|
+
if config.get("materialization_outputs"):
|
|
858
|
+
return {
|
|
859
|
+
field: prepare_materialized_backend(dataset, field, options)
|
|
860
|
+
for field, options in config["materialization_outputs"].items()
|
|
861
|
+
}
|
|
862
|
+
|
|
837
863
|
primary_slot = _resolve_primary_slot(transform_class)
|
|
838
864
|
primary_backend = prepare_output_backend(config, dataset, transform_class)
|
|
839
865
|
backends: dict[str, SourceBackedOutputBackend] = {primary_slot: primary_backend}
|
|
@@ -859,11 +885,7 @@ def prepare_output_backends(
|
|
|
859
885
|
|
|
860
886
|
# Aggregate every slot we actually wrote into the manifest the primary
|
|
861
887
|
# backend will emit, so a single manifest documents the full set.
|
|
862
|
-
if (
|
|
863
|
-
pipeline is not None
|
|
864
|
-
and pipeline.outputs_manifest_path
|
|
865
|
-
and len(backends) > 1
|
|
866
|
-
):
|
|
888
|
+
if pipeline is not None and pipeline.outputs_manifest_path and len(backends) > 1:
|
|
867
889
|
manifest_targets: list[PipelineOutputTargetConfig] = list(
|
|
868
890
|
primary_backend.pipeline_manifest_targets
|
|
869
891
|
)
|
|
@@ -876,3 +898,29 @@ def prepare_output_backends(
|
|
|
876
898
|
primary_backend.pipeline_manifest_targets = manifest_targets
|
|
877
899
|
|
|
878
900
|
return backends
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def prepare_materialized_backend(dataset, field, options):
|
|
904
|
+
"""Explicit bound-output slot. Arbitrary producer changes fail receipt checks."""
|
|
905
|
+
from euler_loading import output_encoding
|
|
906
|
+
|
|
907
|
+
codec_config = options.get("encoding", {"id": "npy"})
|
|
908
|
+
codec = output_encoding(codec_config["id"], scale=codec_config.get("scale"))
|
|
909
|
+
writer = dataset.create_output_writer(
|
|
910
|
+
field,
|
|
911
|
+
options["path"],
|
|
912
|
+
derivation=options["derivation"],
|
|
913
|
+
dataset_id=options["dataset_id"],
|
|
914
|
+
revision=options["revision"],
|
|
915
|
+
encoding=codec,
|
|
916
|
+
expected_full_ids=options.get("expected_full_ids"),
|
|
917
|
+
metadata_scope=options.get("metadata_scope"),
|
|
918
|
+
resume=options.get("resume", False),
|
|
919
|
+
)
|
|
920
|
+
return SourceBackedOutputBackend(
|
|
921
|
+
source_modality=field,
|
|
922
|
+
root=Path(options["path"]),
|
|
923
|
+
dataset_writer=writer,
|
|
924
|
+
modality_writer=None,
|
|
925
|
+
modality_meta=None,
|
|
926
|
+
)
|