euler-preprocess 1.9.0__tar.gz → 2.1.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-1.9.0 → euler_preprocess-2.1.0}/PKG-INFO +1 -1
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/cli.py +17 -5
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/output.py +250 -6
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/transform.py +11 -1
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/models.py +53 -7
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/transform.py +228 -22
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/PKG-INFO +1 -1
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/SOURCES.txt +1 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/pyproject.toml +1 -1
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_dcp_heuristic_airlight.py +2 -2
- euler_preprocess-2.1.0/tests/test_fog_aux_outputs.py +465 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/README.md +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/dataset.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/foggify.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/foggify_logging.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/requires.txt +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/setup.cfg +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_foggify_integration.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_radial.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-1.9.0 → euler_preprocess-2.1.0}/tests/test_zip_output.py +0 -0
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
from euler_preprocess.common.dataset import build_dataset
|
|
14
14
|
from euler_preprocess.common.logging import get_logger, log_dataset_info
|
|
15
|
-
from euler_preprocess.common.output import
|
|
15
|
+
from euler_preprocess.common.output import prepare_output_backends
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
@@ -55,7 +55,8 @@ def _run_transform(args: argparse.Namespace, transform_class: type) -> int:
|
|
|
55
55
|
required_modalities = transform_class.REQUIRED_MODALITIES
|
|
56
56
|
required_hierarchical = transform_class.REQUIRED_HIERARCHICAL_MODALITIES or None
|
|
57
57
|
dataset = build_dataset(config, required_modalities, required_hierarchical)
|
|
58
|
-
|
|
58
|
+
output_backends = prepare_output_backends(config, dataset, transform_class)
|
|
59
|
+
primary_backend = next(iter(output_backends.values()))
|
|
59
60
|
dataset_name = config.get("dataset", "dataset")
|
|
60
61
|
|
|
61
62
|
raw_modalities = {
|
|
@@ -69,14 +70,25 @@ def _run_transform(args: argparse.Namespace, transform_class: type) -> int:
|
|
|
69
70
|
else:
|
|
70
71
|
modality_info[name] = entry
|
|
71
72
|
log_dataset_info(logger, dataset_name, len(dataset), modality_info, use_gpu)
|
|
72
|
-
|
|
73
|
+
for slot, backend in output_backends.items():
|
|
74
|
+
logger.info("Output path [%s]: %s", slot, backend.root)
|
|
73
75
|
|
|
74
76
|
transform_kwargs: dict = {
|
|
75
77
|
"config_path": str(transform_config_path),
|
|
76
|
-
"out_path": str(
|
|
77
|
-
"output_backend": output_backend,
|
|
78
|
+
"out_path": str(primary_backend.root),
|
|
78
79
|
}
|
|
79
80
|
init_params = inspect.signature(transform_class.__init__).parameters
|
|
81
|
+
if "output_backends" in init_params:
|
|
82
|
+
transform_kwargs["output_backends"] = output_backends
|
|
83
|
+
else:
|
|
84
|
+
transform_kwargs["output_backend"] = primary_backend
|
|
85
|
+
if len(output_backends) > 1:
|
|
86
|
+
extra = [s for s in output_backends if s != next(iter(output_backends))]
|
|
87
|
+
logger.warning(
|
|
88
|
+
"%s does not accept output_backends; ignoring auxiliary slots: %s",
|
|
89
|
+
transform_class.__name__,
|
|
90
|
+
extra,
|
|
91
|
+
)
|
|
80
92
|
if "strict" in init_params:
|
|
81
93
|
transform_kwargs["strict"] = bool(getattr(args, "strict", False))
|
|
82
94
|
elif getattr(args, "strict", False):
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import tempfile
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
@@ -19,6 +20,39 @@ _PIPELINE_OUTPUT_STORAGE_KINDS = {"directory", "zip", "file"}
|
|
|
19
20
|
_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class OutputSlotSpec:
|
|
25
|
+
"""Auxiliary-slot spec for transforms producing more than one modality.
|
|
26
|
+
|
|
27
|
+
Auxiliary slots reuse the source modality's hierarchy/indexing so the
|
|
28
|
+
written files line up with the input dataset, but supply their own writer
|
|
29
|
+
and ds-crawler metadata so the resulting on-disk dataset advertises the
|
|
30
|
+
correct modality type and loader.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
source_modality: Name of the input modality whose hierarchy and
|
|
34
|
+
per-sample basenames are mirrored when writing auxiliary outputs.
|
|
35
|
+
writer: Writer callable invoked as ``writer(target, value, meta)``.
|
|
36
|
+
``target`` is either a filesystem path (``str``/``PathLike``) or a
|
|
37
|
+
binary stream (when the writer is marked stream-supported and the
|
|
38
|
+
output is a zip).
|
|
39
|
+
index_overlay: Mapping merged on top of the source modality's
|
|
40
|
+
``index_output`` to produce the ds-crawler head metadata for this
|
|
41
|
+
slot. Use this to override ``name``/``type``/``euler_train``/
|
|
42
|
+
``euler_loading``/``meta`` while inheriting indexing/hierarchy.
|
|
43
|
+
output_extension: When set (e.g. ``".npy"``), source basenames are
|
|
44
|
+
rewritten with this extension before writing.
|
|
45
|
+
meta: Optional ``modality_meta`` passed to the writer. Defaults to
|
|
46
|
+
the ``meta`` field from the merged ``index_overlay`` when set there.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
source_modality: str
|
|
50
|
+
writer: Callable[..., None]
|
|
51
|
+
index_overlay: dict[str, Any]
|
|
52
|
+
output_extension: str | None = None
|
|
53
|
+
meta: dict[str, Any] | None = None
|
|
54
|
+
|
|
55
|
+
|
|
22
56
|
@dataclass(frozen=True)
|
|
23
57
|
class PipelineOutputTargetConfig:
|
|
24
58
|
"""Runtime-resolved pipeline output target for a single transform output."""
|
|
@@ -218,6 +252,7 @@ class SourceBackedOutputBackend:
|
|
|
218
252
|
dataset_writer: DatasetWriter | ZipDatasetWriter,
|
|
219
253
|
modality_writer: Any,
|
|
220
254
|
modality_meta: dict[str, Any] | None,
|
|
255
|
+
output_extension: str | None = None,
|
|
221
256
|
pipeline_manifest_path: Path | None = None,
|
|
222
257
|
pipeline_manifest_targets: list[PipelineOutputTargetConfig] | None = None,
|
|
223
258
|
) -> None:
|
|
@@ -226,6 +261,7 @@ class SourceBackedOutputBackend:
|
|
|
226
261
|
self.dataset_writer = dataset_writer
|
|
227
262
|
self.modality_writer = modality_writer
|
|
228
263
|
self.modality_meta = modality_meta
|
|
264
|
+
self.output_extension = output_extension
|
|
229
265
|
self.pipeline_manifest_path = pipeline_manifest_path
|
|
230
266
|
self.pipeline_manifest_targets = pipeline_manifest_targets or []
|
|
231
267
|
|
|
@@ -254,8 +290,13 @@ class SourceBackedOutputBackend:
|
|
|
254
290
|
"requires sample['meta'][source_modality]['path']."
|
|
255
291
|
)
|
|
256
292
|
|
|
257
|
-
|
|
258
|
-
|
|
293
|
+
source_path = Path(str(source_meta["path"]))
|
|
294
|
+
if self.output_extension is not None:
|
|
295
|
+
basename = source_path.stem + self.output_extension
|
|
296
|
+
relative_path = str(source_path.with_suffix(self.output_extension))
|
|
297
|
+
else:
|
|
298
|
+
basename = source_path.name
|
|
299
|
+
relative_path = str(source_path)
|
|
259
300
|
source_meta_copy = dict(source_meta)
|
|
260
301
|
|
|
261
302
|
if isinstance(self.dataset_writer, ZipDatasetWriter):
|
|
@@ -340,11 +381,29 @@ def _select_pipeline_target(
|
|
|
340
381
|
f"pipeline.output_targets does not contain slot '{slot}'"
|
|
341
382
|
)
|
|
342
383
|
|
|
343
|
-
|
|
344
|
-
|
|
384
|
+
# Auxiliary slot names declared by the transform are reserved for
|
|
385
|
+
# OUTPUT_SLOT_SPECS and routed by prepare_output_backends; ignore them
|
|
386
|
+
# when picking the *primary* target. This lets pipeline configs use
|
|
387
|
+
# arbitrary slot aliases (e.g. ``"fog"`` for the primary RGB output)
|
|
388
|
+
# alongside named auxiliary outputs.
|
|
389
|
+
aux_slots = set((getattr(transform_class, "OUTPUT_SLOT_SPECS", None) or {}).keys())
|
|
390
|
+
primary_candidates = [
|
|
391
|
+
t for t in pipeline.output_targets if t.slot not in aux_slots
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
if len(primary_candidates) == 1:
|
|
395
|
+
return primary_candidates[0]
|
|
396
|
+
|
|
397
|
+
if not primary_candidates:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
f"pipeline.output_targets has no primary target for "
|
|
400
|
+
f"{transform_class.__name__}; only auxiliary slots present "
|
|
401
|
+
f"({sorted(aux_slots)}). Add a target for the primary output."
|
|
402
|
+
)
|
|
345
403
|
|
|
346
404
|
raise ValueError(
|
|
347
|
-
"pipeline.output_targets contains multiple entries
|
|
405
|
+
"pipeline.output_targets contains multiple primary entries "
|
|
406
|
+
f"({[t.slot for t in primary_candidates]}); set top-level "
|
|
348
407
|
"'output_slot' to select the target for this transform."
|
|
349
408
|
)
|
|
350
409
|
|
|
@@ -380,7 +439,11 @@ def prepare_output_backend(
|
|
|
380
439
|
dataset: MultiModalDataset,
|
|
381
440
|
transform_class: type,
|
|
382
441
|
) -> SourceBackedOutputBackend:
|
|
383
|
-
"""Create a source-backed output backend for a transform run.
|
|
442
|
+
"""Create a source-backed output backend for a transform run.
|
|
443
|
+
|
|
444
|
+
Used for the *primary* output slot. Transforms with auxiliary outputs
|
|
445
|
+
should use :func:`prepare_output_backends` (plural).
|
|
446
|
+
"""
|
|
384
447
|
|
|
385
448
|
source_modality = getattr(transform_class, "SOURCE_MODALITY", None)
|
|
386
449
|
if not isinstance(source_modality, str) or not source_modality:
|
|
@@ -436,3 +499,184 @@ def prepare_output_backend(
|
|
|
436
499
|
pipeline_manifest_path=pipeline_manifest_path,
|
|
437
500
|
pipeline_manifest_targets=[pipeline_target] if pipeline_target else [],
|
|
438
501
|
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _build_auxiliary_backend(
|
|
505
|
+
*,
|
|
506
|
+
spec: OutputSlotSpec,
|
|
507
|
+
pipeline_target: PipelineOutputTargetConfig,
|
|
508
|
+
dataset: MultiModalDataset,
|
|
509
|
+
) -> SourceBackedOutputBackend:
|
|
510
|
+
"""Create a backend for an auxiliary slot using its OutputSlotSpec.
|
|
511
|
+
|
|
512
|
+
The auxiliary backend does not own the pipeline manifest — that is
|
|
513
|
+
aggregated and written by the primary backend.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
if pipeline_target.storage == "file":
|
|
517
|
+
raise ValueError(
|
|
518
|
+
f"Pipeline output target '{pipeline_target.slot}' uses "
|
|
519
|
+
"unsupported storage='file'"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
root = Path(pipeline_target.path)
|
|
523
|
+
zip_mode = pipeline_target.storage == "zip"
|
|
524
|
+
|
|
525
|
+
base_index = dataset.get_modality_index(spec.source_modality)
|
|
526
|
+
index_output = _build_auxiliary_index(base_index, spec)
|
|
527
|
+
|
|
528
|
+
dataset_writer = create_dataset_writer_from_index(
|
|
529
|
+
index_output=index_output,
|
|
530
|
+
root=root,
|
|
531
|
+
zip=zip_mode,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
modality_meta = spec.meta
|
|
535
|
+
if modality_meta is None:
|
|
536
|
+
head = index_output.get("head") or {}
|
|
537
|
+
modality_meta = (head.get("modality") or {}).get("meta")
|
|
538
|
+
if modality_meta is None:
|
|
539
|
+
modality_meta = index_output.get("meta")
|
|
540
|
+
|
|
541
|
+
return SourceBackedOutputBackend(
|
|
542
|
+
source_modality=spec.source_modality,
|
|
543
|
+
root=root,
|
|
544
|
+
dataset_writer=dataset_writer,
|
|
545
|
+
modality_writer=spec.writer,
|
|
546
|
+
modality_meta=modality_meta,
|
|
547
|
+
output_extension=spec.output_extension,
|
|
548
|
+
pipeline_manifest_path=None,
|
|
549
|
+
pipeline_manifest_targets=[],
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _build_auxiliary_index(
|
|
554
|
+
base_index: dict[str, Any], spec: OutputSlotSpec
|
|
555
|
+
) -> dict[str, Any]:
|
|
556
|
+
"""Apply ``spec.index_overlay`` to a copy of ``base_index``.
|
|
557
|
+
|
|
558
|
+
The overlay's recognised keys map to fields used by ds-crawler's writer
|
|
559
|
+
construction. ``name`` / ``type`` rewrite the dataset id+name and
|
|
560
|
+
modality key on both the contract head and the legacy top-level fields;
|
|
561
|
+
``meta`` overrides the modality's meta dict; ``euler_train`` /
|
|
562
|
+
``euler_loading`` replace those addon entries. Any other overlay keys
|
|
563
|
+
are passed through as top-level fields for the legacy writer path.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
overlay = dict(spec.index_overlay)
|
|
567
|
+
index_output: dict[str, Any] = {**base_index}
|
|
568
|
+
|
|
569
|
+
head = base_index.get("head")
|
|
570
|
+
if isinstance(head, dict):
|
|
571
|
+
new_head = json.loads(json.dumps(head)) # deep copy via JSON
|
|
572
|
+
new_head.setdefault("dataset", {})
|
|
573
|
+
new_head.setdefault("modality", {})
|
|
574
|
+
new_head.setdefault("addons", {})
|
|
575
|
+
|
|
576
|
+
if "name" in overlay:
|
|
577
|
+
name = overlay["name"]
|
|
578
|
+
new_head["dataset"]["id"] = name
|
|
579
|
+
new_head["dataset"]["name"] = name
|
|
580
|
+
if "type" in overlay:
|
|
581
|
+
new_head["modality"]["key"] = overlay["type"]
|
|
582
|
+
if "meta" in overlay:
|
|
583
|
+
new_head["modality"]["meta"] = dict(overlay["meta"])
|
|
584
|
+
if "euler_train" in overlay:
|
|
585
|
+
new_head["addons"]["euler_train"] = dict(overlay["euler_train"])
|
|
586
|
+
if "euler_loading" in overlay:
|
|
587
|
+
new_head["addons"]["euler_loading"] = dict(overlay["euler_loading"])
|
|
588
|
+
|
|
589
|
+
index_output["head"] = new_head
|
|
590
|
+
|
|
591
|
+
# Legacy top-level fields used by the non-contract writer construction
|
|
592
|
+
# path. Preserved alongside the head for compatibility.
|
|
593
|
+
for key, value in overlay.items():
|
|
594
|
+
if isinstance(value, dict):
|
|
595
|
+
index_output[key] = dict(value)
|
|
596
|
+
else:
|
|
597
|
+
index_output[key] = value
|
|
598
|
+
|
|
599
|
+
return index_output
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _resolve_primary_slot(transform_class: type) -> str:
|
|
603
|
+
"""Return the *primary* slot name declared by *transform_class*."""
|
|
604
|
+
|
|
605
|
+
output_slots = getattr(transform_class, "OUTPUT_SLOTS", None)
|
|
606
|
+
if output_slots:
|
|
607
|
+
return output_slots[0]
|
|
608
|
+
|
|
609
|
+
output_slot = getattr(transform_class, "OUTPUT_SLOT", None)
|
|
610
|
+
if isinstance(output_slot, str) and output_slot:
|
|
611
|
+
return output_slot
|
|
612
|
+
|
|
613
|
+
source_modality = getattr(transform_class, "SOURCE_MODALITY", None)
|
|
614
|
+
if isinstance(source_modality, str) and source_modality:
|
|
615
|
+
return source_modality
|
|
616
|
+
|
|
617
|
+
raise ValueError(
|
|
618
|
+
f"{transform_class.__name__} declares no output slot "
|
|
619
|
+
"(set OUTPUT_SLOT, OUTPUT_SLOTS, or SOURCE_MODALITY)"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def prepare_output_backends(
|
|
624
|
+
config: dict[str, Any],
|
|
625
|
+
dataset: MultiModalDataset,
|
|
626
|
+
transform_class: type,
|
|
627
|
+
) -> dict[str, SourceBackedOutputBackend]:
|
|
628
|
+
"""Create per-slot output backends for *transform_class*.
|
|
629
|
+
|
|
630
|
+
Returns ``{slot_name: backend}``. The primary slot (the first entry of
|
|
631
|
+
``OUTPUT_SLOTS``, falling back to ``OUTPUT_SLOT`` / ``SOURCE_MODALITY``) is
|
|
632
|
+
always present. Auxiliary slots declared in
|
|
633
|
+
:attr:`Transform.OUTPUT_SLOT_SPECS` are included only when the dataset
|
|
634
|
+
config's ``pipeline.output_targets`` contains a matching entry; otherwise
|
|
635
|
+
the slot is silently omitted (auxiliary outputs are opt-in).
|
|
636
|
+
|
|
637
|
+
The returned dict's iteration order matches the declared
|
|
638
|
+
``OUTPUT_SLOTS`` order.
|
|
639
|
+
"""
|
|
640
|
+
|
|
641
|
+
primary_slot = _resolve_primary_slot(transform_class)
|
|
642
|
+
primary_backend = prepare_output_backend(config, dataset, transform_class)
|
|
643
|
+
backends: dict[str, SourceBackedOutputBackend] = {primary_slot: primary_backend}
|
|
644
|
+
|
|
645
|
+
slot_specs = getattr(transform_class, "OUTPUT_SLOT_SPECS", None) or {}
|
|
646
|
+
pipeline = parse_pipeline_config(config)
|
|
647
|
+
|
|
648
|
+
declared_slots = getattr(transform_class, "OUTPUT_SLOTS", ()) or ()
|
|
649
|
+
aux_slots = [s for s in declared_slots if s != primary_slot]
|
|
650
|
+
if pipeline is not None and slot_specs:
|
|
651
|
+
for slot in aux_slots:
|
|
652
|
+
spec = slot_specs.get(slot)
|
|
653
|
+
if spec is None:
|
|
654
|
+
continue
|
|
655
|
+
target = pipeline.get_output_target(slot)
|
|
656
|
+
if target is None:
|
|
657
|
+
continue
|
|
658
|
+
backends[slot] = _build_auxiliary_backend(
|
|
659
|
+
spec=spec,
|
|
660
|
+
pipeline_target=target,
|
|
661
|
+
dataset=dataset,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# Aggregate every slot we actually wrote into the manifest the primary
|
|
665
|
+
# backend will emit, so a single manifest documents the full set.
|
|
666
|
+
if (
|
|
667
|
+
pipeline is not None
|
|
668
|
+
and pipeline.outputs_manifest_path
|
|
669
|
+
and len(backends) > 1
|
|
670
|
+
):
|
|
671
|
+
manifest_targets: list[PipelineOutputTargetConfig] = list(
|
|
672
|
+
primary_backend.pipeline_manifest_targets
|
|
673
|
+
)
|
|
674
|
+
for slot in aux_slots:
|
|
675
|
+
if slot not in backends:
|
|
676
|
+
continue
|
|
677
|
+
target = pipeline.get_output_target(slot)
|
|
678
|
+
if target is not None:
|
|
679
|
+
manifest_targets.append(target)
|
|
680
|
+
primary_backend.pipeline_manifest_targets = manifest_targets
|
|
681
|
+
|
|
682
|
+
return backends
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import ClassVar
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Transform(ABC):
|
|
@@ -11,12 +11,22 @@ class Transform(ABC):
|
|
|
11
11
|
|
|
12
12
|
Subclasses declare the modalities they need via class variables and
|
|
13
13
|
implement :meth:`run` to process samples.
|
|
14
|
+
|
|
15
|
+
Output slots:
|
|
16
|
+
Most transforms produce a single output (the *primary* slot, declared
|
|
17
|
+
via :attr:`OUTPUT_SLOT`). Transforms that produce additional auxiliary
|
|
18
|
+
outputs (e.g. fog β / L_s maps) declare them in :attr:`OUTPUT_SLOTS`
|
|
19
|
+
together with per-slot specs in :attr:`OUTPUT_SLOT_SPECS`. Auxiliary
|
|
20
|
+
slots are opt-in: they are only written when the dataset config's
|
|
21
|
+
``pipeline.output_targets`` includes a matching entry.
|
|
14
22
|
"""
|
|
15
23
|
|
|
16
24
|
REQUIRED_MODALITIES: ClassVar[set[str]] = set()
|
|
17
25
|
REQUIRED_HIERARCHICAL_MODALITIES: ClassVar[set[str]] = set()
|
|
18
26
|
SOURCE_MODALITY: ClassVar[str | None] = None
|
|
19
27
|
OUTPUT_SLOT: ClassVar[str | None] = None
|
|
28
|
+
OUTPUT_SLOTS: ClassVar[tuple[str, ...]] = ()
|
|
29
|
+
OUTPUT_SLOT_SPECS: ClassVar[dict[str, Any]] = {}
|
|
20
30
|
OUTPUT_INDEX_META_OVERRIDES: ClassVar[dict[str, object]] = {}
|
|
21
31
|
|
|
22
32
|
@abstractmethod
|
|
@@ -243,6 +243,33 @@ def uses_estimated_airlight(al_spec) -> bool:
|
|
|
243
243
|
return al_spec is None or al_spec in AIRLIGHT_METHODS
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
def broadcast_k_field(k_field: Any, height: int, width: int) -> np.ndarray:
|
|
247
|
+
"""Return ``k_field`` as a ``(H, W)`` float32 map (broadcasting if scalar)."""
|
|
248
|
+
arr = np.asarray(k_field, dtype=np.float32)
|
|
249
|
+
if arr.ndim == 0:
|
|
250
|
+
return np.broadcast_to(arr, (height, width)).astype(np.float32, copy=True)
|
|
251
|
+
if arr.shape == (height, width):
|
|
252
|
+
return arr.astype(np.float32, copy=False)
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"k_field must be scalar or shape ({height}, {width}); got {arr.shape}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def broadcast_ls_field(ls_field: Any, height: int, width: int) -> np.ndarray:
|
|
259
|
+
"""Return ``ls_field`` as a ``(H, W, 3)`` float32 map (broadcasting if needed)."""
|
|
260
|
+
arr = np.asarray(ls_field, dtype=np.float32)
|
|
261
|
+
if arr.shape == (3,):
|
|
262
|
+
return np.broadcast_to(arr, (height, width, 3)).astype(np.float32, copy=True)
|
|
263
|
+
if arr.shape == (1, 1, 3):
|
|
264
|
+
return np.broadcast_to(arr, (height, width, 3)).astype(np.float32, copy=True)
|
|
265
|
+
if arr.shape == (height, width, 3):
|
|
266
|
+
return arr.astype(np.float32, copy=False)
|
|
267
|
+
raise ValueError(
|
|
268
|
+
f"ls_field must have shape (3,), (1, 1, 3), or "
|
|
269
|
+
f"({height}, {width}, 3); got {arr.shape}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
246
273
|
def apply_model(
|
|
247
274
|
rgb: np.ndarray,
|
|
248
275
|
depth_m: np.ndarray,
|
|
@@ -251,7 +278,18 @@ def apply_model(
|
|
|
251
278
|
rng: np.random.Generator,
|
|
252
279
|
contrast_threshold_default: float,
|
|
253
280
|
estimated_airlight: np.ndarray,
|
|
254
|
-
) -> tuple[np.ndarray, float, np.ndarray]:
|
|
281
|
+
) -> tuple[np.ndarray, float, np.ndarray, np.ndarray, np.ndarray]:
|
|
282
|
+
"""Apply a fog model to ``rgb``.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Tuple ``(foggy, k_mean, ls_base, k_map, ls_map)``:
|
|
286
|
+
|
|
287
|
+
* ``foggy``: ``(H, W, 3)`` foggy RGB image.
|
|
288
|
+
* ``k_mean``: scalar mean scattering coefficient (for filenames/logs).
|
|
289
|
+
* ``ls_base``: ``(3,)`` base atmospheric light (for filenames/logs).
|
|
290
|
+
* ``k_map``: ``(H, W)`` β-field actually used (broadcast for uniform).
|
|
291
|
+
* ``ls_map``: ``(H, W, 3)`` L_s-field actually used (broadcast for uniform).
|
|
292
|
+
"""
|
|
255
293
|
if model_name not in DEFAULT_MODEL_CONFIGS:
|
|
256
294
|
raise ValueError(f"Unsupported fog model: {model_name}")
|
|
257
295
|
visibility = float(sample_value(model_cfg.get("visibility_m"), rng))
|
|
@@ -269,14 +307,19 @@ def apply_model(
|
|
|
269
307
|
sampled_al = sample_value(al_spec, rng)
|
|
270
308
|
ls_base = normalize_atmospheric_light(np.asarray(sampled_al))
|
|
271
309
|
|
|
310
|
+
height, width = depth_m.shape
|
|
311
|
+
|
|
272
312
|
if model_name == "uniform":
|
|
273
313
|
ls_field = ls_base.reshape(1, 1, 3)
|
|
274
|
-
|
|
314
|
+
foggy = apply_fog(rgb, depth_m, k_mean, ls_field)
|
|
315
|
+
k_map = broadcast_k_field(k_mean, height, width)
|
|
316
|
+
ls_map = broadcast_ls_field(ls_base, height, width)
|
|
317
|
+
return foggy, k_mean, ls_base, k_map, ls_map
|
|
275
318
|
|
|
276
319
|
if model_name in ("heterogeneous_k", "heterogeneous_k_ls"):
|
|
277
320
|
k_cfg = model_cfg.get("k_hetero", {})
|
|
278
|
-
k_scales = resolve_scales(k_cfg,
|
|
279
|
-
k_noise = perlin_fbm(
|
|
321
|
+
k_scales = resolve_scales(k_cfg, height, width, rng)
|
|
322
|
+
k_noise = perlin_fbm(height, width, k_scales, rng)
|
|
280
323
|
min_factor = float(sample_value(k_cfg.get("min_factor", 1.0), rng))
|
|
281
324
|
max_factor = float(sample_value(k_cfg.get("max_factor", 1.0), rng))
|
|
282
325
|
k_field = modulate_with_noise(
|
|
@@ -291,8 +334,8 @@ def apply_model(
|
|
|
291
334
|
|
|
292
335
|
if model_name in ("heterogeneous_ls", "heterogeneous_k_ls"):
|
|
293
336
|
ls_cfg = model_cfg.get("ls_hetero", {})
|
|
294
|
-
ls_scales = resolve_scales(ls_cfg,
|
|
295
|
-
ls_noise = perlin_fbm(
|
|
337
|
+
ls_scales = resolve_scales(ls_cfg, height, width, rng)
|
|
338
|
+
ls_noise = perlin_fbm(height, width, ls_scales, rng)
|
|
296
339
|
min_factor = float(sample_value(ls_cfg.get("min_factor", 1.0), rng))
|
|
297
340
|
max_factor = float(sample_value(ls_cfg.get("max_factor", 1.0), rng))
|
|
298
341
|
ls_field = modulate_with_noise(
|
|
@@ -306,4 +349,7 @@ def apply_model(
|
|
|
306
349
|
else:
|
|
307
350
|
ls_field = ls_base.reshape(1, 1, 3)
|
|
308
351
|
|
|
309
|
-
|
|
352
|
+
foggy = apply_fog(rgb, depth_m, k_field, ls_field)
|
|
353
|
+
k_map = broadcast_k_field(k_field, height, width)
|
|
354
|
+
ls_map = broadcast_ls_field(ls_field, height, width)
|
|
355
|
+
return foggy, k_mean, ls_base, k_map, ls_map
|