evaluators 2.1.1__tar.gz → 2.1.3__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 (77) hide show
  1. {evaluators-2.1.1 → evaluators-2.1.3}/PKG-INFO +12 -6
  2. {evaluators-2.1.1 → evaluators-2.1.3}/README.md +2 -2
  3. {evaluators-2.1.1 → evaluators-2.1.3}/pyproject.toml +15 -5
  4. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/__init__.py +11 -5
  5. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/_engine.py +3 -3
  6. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/_streams.py +1 -0
  7. evaluators-2.1.3/sources/evaluators/cli/compute.py +167 -0
  8. evaluators-2.1.3/sources/evaluators/cli/third_party.py +177 -0
  9. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/integrations/lightning.py +72 -3
  10. evaluators-2.1.3/sources/evaluators/memory/__init__.py +146 -0
  11. evaluators-2.1.3/sources/evaluators/memory/backends/__init__.py +11 -0
  12. evaluators-2.1.3/sources/evaluators/memory/backends/_registry.py +192 -0
  13. evaluators-2.1.3/sources/evaluators/memory/backends/filesystem/__init__.py +8 -0
  14. evaluators-2.1.3/sources/evaluators/memory/backends/filesystem/_reader.py +157 -0
  15. evaluators-2.1.3/sources/evaluators/memory/backends/filesystem/_writer.py +184 -0
  16. {evaluators-2.1.1/sources/evaluators/memory → evaluators-2.1.3/sources/evaluators/memory/backends}/lmdb/_reader.py +42 -57
  17. evaluators-2.1.3/sources/evaluators/memory/backends/lmdb/_writer.py +202 -0
  18. evaluators-2.1.3/sources/evaluators/memory/backends/memmap/_reader.py +242 -0
  19. evaluators-2.1.3/sources/evaluators/memory/backends/memmap/_writer.py +302 -0
  20. evaluators-2.1.3/sources/evaluators/memory/codecs/__init__.py +42 -0
  21. evaluators-2.1.3/sources/evaluators/memory/codecs/_blosc.py +138 -0
  22. evaluators-2.1.3/sources/evaluators/memory/codecs/_png.py +60 -0
  23. evaluators-2.1.3/sources/evaluators/memory/codecs/_raw.py +35 -0
  24. evaluators-2.1.3/sources/evaluators/memory/codecs/_tiff.py +193 -0
  25. evaluators-2.1.3/sources/evaluators/memory/config.py +73 -0
  26. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/memory/interfaces.py +33 -3
  27. evaluators-2.1.3/sources/evaluators/memory/schema.py +96 -0
  28. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/depth.py +1 -1
  29. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/dvps.py +1 -1
  30. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/segmentation/panoptic.py +4 -4
  31. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/segmentation/semantic.py +2 -2
  32. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/step.py +2 -0
  33. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/scheduling/_catalog.py +2 -0
  34. evaluators-2.1.3/sources/evaluators/third_party/__init__.py +7 -0
  35. evaluators-2.1.3/sources/evaluators/third_party/__main__.py +6 -0
  36. evaluators-2.1.3/sources/evaluators/third_party/adapters/__init__.py +4 -0
  37. evaluators-2.1.3/sources/evaluators/third_party/adapters/coco.py +312 -0
  38. evaluators-2.1.3/sources/evaluators/third_party/adapters/dvpq.py +262 -0
  39. evaluators-2.1.3/sources/evaluators/third_party/adapters/pq.py +26 -0
  40. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/types.py +56 -1
  41. evaluators-2.1.3/sources/evaluators/utils/configs.py +62 -0
  42. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/visuals/metric.py +6 -2
  43. evaluators-2.1.3/sources/evaluators/visuals/tools/__init__.py +11 -0
  44. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/visuals/tools/_visualizer.py +14 -42
  45. evaluators-2.1.1/sources/evaluators/memory/__init__.py +0 -43
  46. evaluators-2.1.1/sources/evaluators/memory/encoded_memmap/__init__.py +0 -13
  47. evaluators-2.1.1/sources/evaluators/memory/encoded_memmap/_reader.py +0 -176
  48. evaluators-2.1.1/sources/evaluators/memory/encoded_memmap/_writer.py +0 -196
  49. evaluators-2.1.1/sources/evaluators/memory/lmdb/_writer.py +0 -180
  50. evaluators-2.1.1/sources/evaluators/memory/memmap/_reader.py +0 -218
  51. evaluators-2.1.1/sources/evaluators/memory/memmap/_writer.py +0 -272
  52. evaluators-2.1.1/sources/evaluators/memory/schema.py +0 -60
  53. evaluators-2.1.1/sources/evaluators/memory/utils/encoding.py +0 -148
  54. evaluators-2.1.1/sources/evaluators/visuals/tools/__init__.py +0 -9
  55. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/cli/__main__.py +0 -0
  56. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/cli/memory.py +0 -0
  57. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/constants.py +0 -0
  58. {evaluators-2.1.1/sources/evaluators/memory → evaluators-2.1.3/sources/evaluators/memory/backends}/lmdb/__init__.py +0 -0
  59. {evaluators-2.1.1/sources/evaluators/memory → evaluators-2.1.3/sources/evaluators/memory/backends}/memmap/__init__.py +0 -0
  60. {evaluators-2.1.1/sources/evaluators/memory → evaluators-2.1.3/sources/evaluators/memory/backends}/memmap/_buffers.py +0 -0
  61. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/__init__.py +0 -0
  62. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/adapters/coco.py +0 -0
  63. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/adapters/trackeval.py +0 -0
  64. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/segmentation/__init__.py +0 -0
  65. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/segmentation/_utils.py +2 -2
  66. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/domain/tracking.py +0 -0
  67. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/metrics/stats.py +0 -0
  68. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/presets/__init__.py +0 -0
  69. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/presets/cityscapes.py +0 -0
  70. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/presets/kitti.py +0 -0
  71. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/presets/semkitti.py +0 -0
  72. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/scheduling/__init__.py +0 -0
  73. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/scheduling/_partitioner.py +0 -0
  74. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/utils/comms.py +0 -0
  75. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/utils/tensor_ops.py +0 -0
  76. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/visuals/__init__.py +0 -0
  77. {evaluators-2.1.1 → evaluators-2.1.3}/sources/evaluators/visuals/tools/transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: evaluators
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Various scene understanding and perception evaluation metrics.
5
5
  Author: Kurt Stolle
6
6
  Author-email: Kurt Stolle <kurt@computer.org>
@@ -23,14 +23,20 @@ Requires-Dist: tensordict>=0.11.0
23
23
  Requires-Dist: numpy>=2.0.0
24
24
  Requires-Dist: matplotlib>=3.9.0
25
25
  Requires-Dist: rich>=13.9.4
26
- Requires-Dist: lmdb
27
- Requires-Dist: tifffile
28
26
  Requires-Dist: pydantic
29
27
  Requires-Dist: opencv-python>=4.13.0.92
30
- Requires-Dist: imagecodecs>=2026.3.6
28
+ Requires-Dist: omegaconf>=2.3.0
29
+ Requires-Dist: hydra-zen>=0.16.0
30
+ Requires-Dist: blosc2>=4.1.2 ; extra == 'blosc2'
31
31
  Requires-Dist: lightning>=2.6.1 ; extra == 'lightning'
32
+ Requires-Dist: lmdb>=2.2.0 ; extra == 'lmdb'
33
+ Requires-Dist: imagecodecs>=2026.3.6 ; extra == 'tiff'
34
+ Requires-Dist: tifffile>=2026.3.3 ; extra == 'tiff'
32
35
  Requires-Python: >=3.13, <3.14
36
+ Provides-Extra: blosc2
33
37
  Provides-Extra: lightning
38
+ Provides-Extra: lmdb
39
+ Provides-Extra: tiff
34
40
  Description-Content-Type: text/markdown
35
41
 
36
42
  # Scalable Distributed Evaluation for Computer Vision
@@ -219,10 +225,10 @@ The evaluation pipeline consists of three stages.
219
225
  Each GPU writes predictions to locally sharded files (e.g. `.memmap` or `.lmdb`). No communication occurs.
220
226
  2. **Schedule.**
221
227
  A synchronization barrier is reached. The main process aggregates metadata manifests from all shards to build a Global Catalog. It partitions the workload (videos) among workers using a greedy strategy to balance duration.
222
- 3. **Compute.**
228
+ 3. **Compute.**
223
229
  Workers iterate through their assigned Virtual Sequences. Data is streamed from disk, processed by torchmetrics, and reduced globally.
224
230
 
225
- See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed diagrams and design principles.
231
+ See [OFFLINE_EVALUATION.md](OFFLINE_EVALUATION.md) for detailed usage and design principles.
226
232
 
227
233
  ---
228
234
 
@@ -184,10 +184,10 @@ The evaluation pipeline consists of three stages.
184
184
  Each GPU writes predictions to locally sharded files (e.g. `.memmap` or `.lmdb`). No communication occurs.
185
185
  2. **Schedule.**
186
186
  A synchronization barrier is reached. The main process aggregates metadata manifests from all shards to build a Global Catalog. It partitions the workload (videos) among workers using a greedy strategy to balance duration.
187
- 3. **Compute.**
187
+ 3. **Compute.**
188
188
  Workers iterate through their assigned Virtual Sequences. Data is streamed from disk, processed by torchmetrics, and reduced globally.
189
189
 
190
- See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed diagrams and design principles.
190
+ See [OFFLINE_EVALUATION.md](OFFLINE_EVALUATION.md) for detailed usage and design principles.
191
191
 
192
192
  ---
193
193
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "evaluators"
7
- version = "2.1.1"
7
+ version = "2.1.3"
8
8
  description = "Various scene understanding and perception evaluation metrics."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13, <3.14"
@@ -31,15 +31,17 @@ dependencies = [
31
31
  "numpy>=2.0.0",
32
32
  "matplotlib>=3.9.0",
33
33
  "rich>=13.9.4",
34
- "lmdb",
35
- "tifffile",
36
34
  "pydantic",
37
35
  "opencv-python>=4.13.0.92",
38
- "imagecodecs>=2026.3.6",
36
+ "omegaconf>=2.3.0",
37
+ "hydra-zen>=0.16.0",
39
38
  ]
40
39
 
41
40
  [project.optional-dependencies]
42
41
  lightning = ["lightning>=2.6.1"]
42
+ lmdb = ["lmdb>=2.2.0"]
43
+ blosc2 = ["blosc2>=4.1.2"]
44
+ tiff = ["imagecodecs>=2026.3.6", "tifffile>=2026.3.3"]
43
45
 
44
46
  [dependency-groups]
45
47
  dev = [
@@ -98,15 +100,23 @@ select = [
98
100
  "UP", # pyupgrade
99
101
  ]
100
102
  ignore = [
103
+ "SIM118", # usage of 'x in dict.keys()' -- conflicts with `tensordict`.
101
104
  "PLR2004", # magic numbers
102
105
  "PLW2901", # for loop variable overwritten
103
106
  "D212",
104
107
  "D203",
105
108
  "D107", # docstring on __init__
109
+ "PT011", # pytest.raises too broad (restrictive)
110
+
111
+
106
112
  ]
107
113
 
108
114
  [tool.ruff.lint.per-file-ignores]
109
- "tests/*" = ["D", "T"]
115
+ "tests/*" = [
116
+ "D",
117
+ "T",
118
+ "E402", # Module level import not at top of file
119
+ ]
110
120
 
111
121
  [tool.ruff.format]
112
122
  docstring-code-format = true
@@ -22,33 +22,39 @@ from typing import TYPE_CHECKING
22
22
  from ._engine import run_offline_evaluation
23
23
  from ._streams import BackendInterface, MetricStream, UnboundStreamWarning
24
24
  from .memory import (
25
- EncodedMemmapTemporalWriter,
26
25
  LMDBTemporalWriter,
27
- MemorySchema,
28
26
  MemmapTemporalWriter,
27
+ MemorySchema,
29
28
  TensorField,
29
+ deserialize_metrics,
30
+ load_evaluation_config,
31
+ save_evaluation_config,
32
+ serialize_metrics,
30
33
  )
31
34
 
32
35
  if TYPE_CHECKING:
33
36
  __version__: str
34
37
  __all__ = [
35
38
  "BackendInterface",
36
- "EncodedMemmapTemporalWriter",
37
39
  "LMDBTemporalWriter",
38
- "MemorySchema",
39
40
  "MemmapTemporalWriter",
41
+ "MemorySchema",
40
42
  "MetricStream",
41
43
  "TensorField",
42
44
  "UnboundStreamWarning",
43
45
  "__version__",
46
+ "deserialize_metrics",
47
+ "load_evaluation_config",
44
48
  "run_offline_evaluation",
49
+ "save_evaluation_config",
50
+ "serialize_metrics",
45
51
  ]
46
52
 
47
53
 
48
54
  # 2. Runtime Lazy Loader
49
55
  def __getattr__(name: str) -> str:
50
56
  if name == "__version__":
51
- from importlib.metadata import PackageNotFoundError, version # noqa: PLC0415
57
+ from importlib.metadata import PackageNotFoundError, version
52
58
 
53
59
  try:
54
60
  return version("evaluators")
@@ -18,7 +18,7 @@ from pathlib import Path
18
18
  import rich.progress
19
19
  import torch
20
20
 
21
- from evaluators.memory import MemmapVirtualSequence
21
+ from evaluators.memory.backends._registry import open_virtual_sequence
22
22
  from evaluators.scheduling import CatalogBuilder, WorkPartitioner
23
23
  from evaluators.types import BaseMetric, MetricResult
24
24
  from evaluators.utils import comms
@@ -192,7 +192,7 @@ def _evaluate_metrics(
192
192
 
193
193
  for i, item in enumerate(work_items):
194
194
  fragments = item.entry["fragments"]
195
- sequence = MemmapVirtualSequence(fragments, artifact_path)
195
+ sequence = open_virtual_sequence(artifact_path, item.sequence_id, fragments)
196
196
 
197
197
  if show_progress:
198
198
  with rich.progress.Progress() as progress:
@@ -265,7 +265,7 @@ def _save_plots(
265
265
  force: bool,
266
266
  ) -> None:
267
267
  """Call ``plot()`` on each metric and save the figure to *out_path*."""
268
- import matplotlib.pyplot as plt # noqa: PLC0415
268
+ import matplotlib.pyplot as plt
269
269
 
270
270
  for metric in metrics:
271
271
  name = metric.__class__.__name__
@@ -30,6 +30,7 @@ from tensordict import TensorDict
30
30
  from torch import nn
31
31
 
32
32
  from evaluators.types import BaseMetric
33
+
33
34
  from .memory.schema import MemorySchema
34
35
 
35
36
  __all__ = ["BackendInterface", "MetricStream", "UnboundStreamWarning"]
@@ -0,0 +1,167 @@
1
+ """
2
+ CLI Module: Evaluate.
3
+
4
+ Runs offline evaluation on stored inference results using the config saved in
5
+ the artifact directory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import logging
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, cast
15
+
16
+ import rich.console
17
+ import rich.pretty
18
+ import torch
19
+ from evaluators import run_offline_evaluation
20
+ from evaluators.memory import deserialize_metrics, load_evaluation_config
21
+ from omegaconf import DictConfig, ListConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def setup_parser() -> argparse.ArgumentParser:
27
+ """Define arguments for the compute subcommand."""
28
+ parser = argparse.ArgumentParser(
29
+ prog="evaluators compute",
30
+ description="Run offline evaluation on stored inference results.",
31
+ )
32
+
33
+ parser.add_argument(
34
+ "artifact_dir",
35
+ type=str,
36
+ help="Path to directory containing stored inference results.",
37
+ )
38
+
39
+ parser.add_argument(
40
+ "--output-dir",
41
+ "-o",
42
+ type=str,
43
+ default=None,
44
+ help="Directory to save metric results and plots.",
45
+ )
46
+
47
+ parser.add_argument(
48
+ "--plot",
49
+ action="store_true",
50
+ help="Generate and save visualization plots.",
51
+ )
52
+
53
+ parser.add_argument(
54
+ "--force",
55
+ action="store_true",
56
+ help="Force recomputation of cached metrics.",
57
+ )
58
+
59
+ parser.add_argument(
60
+ "--device",
61
+ type=str,
62
+ default="cuda" if torch.cuda.is_available() else "cpu",
63
+ help="Device for computation (default: cuda if available).",
64
+ )
65
+
66
+ parser.add_argument(
67
+ "--quiet",
68
+ "-q",
69
+ action="store_true",
70
+ help="Suppress progress bars.",
71
+ )
72
+
73
+ return parser
74
+
75
+
76
+ def _load_from_config_file(config_path: str | Path) -> list[Any]:
77
+ """Load metrics from serialized YAML metric definitions in config file."""
78
+ config = load_evaluation_config(config_path)
79
+
80
+ raw_specs = config.get("metrics", [])
81
+ if not raw_specs:
82
+ raise ValueError(
83
+ "Saved config has no metric definitions under 'metrics'. "
84
+ "Offline evaluation requires serialized metrics written at inference time."
85
+ )
86
+
87
+ if not isinstance(raw_specs, (list, ListConfig)):
88
+ raise ValueError("Saved config field 'metrics' must be a list.")
89
+
90
+ raw_spec_items = cast(list[Any], raw_specs)
91
+ metric_specs: list[DictConfig] = []
92
+ for spec in raw_spec_items:
93
+ if not isinstance(spec, DictConfig):
94
+ raise ValueError("Saved metric definitions must be DictConfig mappings.")
95
+ metric_specs.append(spec)
96
+ return deserialize_metrics(metric_specs)
97
+
98
+
99
+ def _find_saved_config(artifact_dir: Path) -> str | None:
100
+ """Check if artifact directory has a saved evaluation config."""
101
+ for name in ["evaluation_config.json", "evaluation_config.yaml"]:
102
+ if (artifact_dir / name).exists():
103
+ return str(artifact_dir / name)
104
+ return None
105
+
106
+
107
+ def _load_evaluation_metrics(artifact_path: Path) -> list[Any]:
108
+ """Load evaluation metrics from config saved in artifact directory."""
109
+ saved_config = _find_saved_config(artifact_path)
110
+ if not saved_config:
111
+ raise FileNotFoundError(
112
+ "No saved evaluation config found in artifact directory. "
113
+ "Expected evaluation_config.json or evaluation_config.yaml."
114
+ )
115
+ return _load_from_config_file(saved_config)
116
+
117
+
118
+ def handle_evaluate(args: Any) -> None:
119
+ """Execute the evaluate subcommand."""
120
+ console = rich.console.Console()
121
+ artifact_path = Path(args.artifact_dir)
122
+
123
+ if not artifact_path.exists():
124
+ console.print(f"[red]Error: {artifact_path} not found[/red]")
125
+ sys.exit(1)
126
+
127
+ # Load metrics
128
+ metrics = _load_evaluation_metrics(artifact_path)
129
+ if not metrics:
130
+ console.print("[yellow]No metrics found in saved evaluation config.[/yellow]")
131
+ sys.exit(1)
132
+
133
+ console.print(f"[bold]Loaded {len(metrics)} metric(s):[/bold]")
134
+ for metric in metrics:
135
+ metric_str = "\t" + "\n\t".join(str(metric).splitlines())
136
+ console.print(metric_str)
137
+
138
+ # Run evaluation
139
+ device = torch.device(args.device)
140
+ console.print(f"[bold]Starting evaluation on {device!s}...[/bold]")
141
+
142
+ results = run_offline_evaluation(
143
+ metrics,
144
+ artifact_path,
145
+ output_dir=args.output_dir,
146
+ plot=args.plot,
147
+ force=args.force,
148
+ device=device,
149
+ show_progress=not args.quiet,
150
+ )
151
+
152
+ if results:
153
+ console.print("\n[bold green]Evaluation Complete[/bold green]")
154
+ for metric_name, values in results.items():
155
+ console.print(f"\n{metric_name}:")
156
+ for key, val in values.items():
157
+ console.print(f" {key:30s}: {val:.4f}")
158
+
159
+ if args.output_dir:
160
+ console.print(f"\n[bold]Results: {args.output_dir}[/bold]")
161
+
162
+
163
+ def main(args: list[str]) -> None:
164
+ """Entry point for evaluate command."""
165
+ parser = setup_parser()
166
+ parsed_args = parser.parse_args(args)
167
+ handle_evaluate(parsed_args)
@@ -0,0 +1,177 @@
1
+ """
2
+ CLI module for third-party benchmark export.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from evaluators.third_party.adapters.dvpq import VipDeeplabKeyMap, export_vip_deeplab
11
+ from evaluators.third_party.adapters.pq import export_pq
12
+
13
+ BENCHMARK_CHOICES: tuple[str, ...] = (
14
+ "pq",
15
+ "cityscapes_dvpq",
16
+ "semkitti_dvpq",
17
+ "semkitti_dstq",
18
+ )
19
+ VIP_DEEPLAB_BENCHMARKS: tuple[str, ...] = (
20
+ "cityscapes_dvpq",
21
+ "semkitti_dvpq",
22
+ "semkitti_dstq",
23
+ )
24
+
25
+
26
+ def setup_parser(prog: str = "evaluators third_party") -> argparse.ArgumentParser:
27
+ """Build argument parser for third-party benchmark export CLI."""
28
+ parser = argparse.ArgumentParser(
29
+ prog=prog,
30
+ description=(
31
+ "Export offline memory artifacts for third-party benchmark scripts."
32
+ ),
33
+ )
34
+ subparsers = parser.add_subparsers(dest="subcommand", required=True)
35
+
36
+ export_parser = subparsers.add_parser(
37
+ "export",
38
+ help="Export artifacts for a third-party benchmark script.",
39
+ )
40
+ export_parser.add_argument(
41
+ "--memory",
42
+ type=str,
43
+ required=True,
44
+ help="Path to the offline memory artifact directory.",
45
+ )
46
+ export_parser.add_argument(
47
+ "--benchmark",
48
+ type=str,
49
+ required=True,
50
+ choices=BENCHMARK_CHOICES,
51
+ help="Benchmark adapter target.",
52
+ )
53
+ export_parser.add_argument(
54
+ "--output",
55
+ type=str,
56
+ required=True,
57
+ help="Output directory for exported files.",
58
+ )
59
+ export_parser.add_argument(
60
+ "--pred-panoptic-key",
61
+ type=str,
62
+ required=True,
63
+ help="Key name for predicted panoptic tensor in memory.",
64
+ )
65
+ export_parser.add_argument(
66
+ "--frame-index-key",
67
+ type=str,
68
+ required=True,
69
+ help="Key name for frame index tensor in memory.",
70
+ )
71
+ export_parser.add_argument(
72
+ "--target-panoptic-key",
73
+ type=str,
74
+ help="Key name for target panoptic tensor in memory (required for pq).",
75
+ )
76
+ export_parser.add_argument(
77
+ "--pred-depth-key",
78
+ type=str,
79
+ help="Key name for predicted depth tensor in memory (required for DVPQ/DSTQ).",
80
+ )
81
+ export_parser.add_argument(
82
+ "--panoptic-divisor",
83
+ type=int,
84
+ required=True,
85
+ help="Divisor used to encode panoptic IDs as category*divisor + instance.",
86
+ )
87
+ export_parser.add_argument(
88
+ "--thing-category-ids",
89
+ type=str,
90
+ required=True,
91
+ help="Comma-separated thing category IDs used to set COCO isthing flags.",
92
+ )
93
+ return parser
94
+
95
+
96
+ def _require_arg(value: str | None, flag: str, benchmark: str) -> str:
97
+ if value:
98
+ return value
99
+ raise ValueError(f"{flag} is required for benchmark '{benchmark}'.")
100
+
101
+
102
+ def _parse_thing_category_ids(value: str) -> set[int]:
103
+ raw_parts = [part.strip() for part in value.split(",")]
104
+ if any(part == "" for part in raw_parts):
105
+ raise ValueError("--thing-category-ids must be a comma-separated list of ints.")
106
+ ids: set[int] = set()
107
+ for part in raw_parts:
108
+ try:
109
+ category_id = int(part)
110
+ except ValueError as exc:
111
+ raise ValueError(
112
+ f"Invalid thing category id '{part}'. Expected integer values."
113
+ ) from exc
114
+ if category_id < 0:
115
+ raise ValueError("Thing category ids must be non-negative integers.")
116
+ ids.add(category_id)
117
+ return ids
118
+
119
+
120
+ def handle_export(args: argparse.Namespace) -> None:
121
+ memory = Path(args.memory)
122
+ output = Path(args.output)
123
+ thing_category_ids = _parse_thing_category_ids(args.thing_category_ids)
124
+
125
+ if args.benchmark == "pq":
126
+ key_map = {
127
+ "pred_panoptic": args.pred_panoptic_key,
128
+ "target_panoptic": _require_arg(
129
+ args.target_panoptic_key, "--target-panoptic-key", args.benchmark
130
+ ),
131
+ "frame_index": args.frame_index_key,
132
+ }
133
+ summary = export_pq(
134
+ memory=memory,
135
+ output=output,
136
+ key_map=key_map,
137
+ panoptic_divisor=args.panoptic_divisor,
138
+ thing_category_ids=thing_category_ids,
139
+ )
140
+ print(
141
+ f"Export complete for '{args.benchmark}'. "
142
+ f"images={summary['images']}, "
143
+ f"predictions={summary['predictions']}, "
144
+ f"targets={summary['targets']}."
145
+ )
146
+ return
147
+
148
+ vip_key_map: VipDeeplabKeyMap = {
149
+ "pred_panoptic": args.pred_panoptic_key,
150
+ "frame_index": args.frame_index_key,
151
+ "pred_depth": _require_arg(
152
+ args.pred_depth_key, "--pred-depth-key", args.benchmark
153
+ ),
154
+ }
155
+ if args.benchmark not in VIP_DEEPLAB_BENCHMARKS:
156
+ raise ValueError(f"Unsupported ViP-DeepLab benchmark: {args.benchmark}")
157
+ summary = export_vip_deeplab(
158
+ memory=memory,
159
+ output=output,
160
+ key_map=vip_key_map,
161
+ format_name=args.benchmark,
162
+ panoptic_divisor=args.panoptic_divisor,
163
+ )
164
+ print(
165
+ f"Export complete for '{args.benchmark}'. "
166
+ f"images={summary['images']}, "
167
+ f"panoptic_files={summary['panoptic_files']}, "
168
+ f"depth_files={summary['depth_files']}."
169
+ )
170
+
171
+
172
+ def main(args: list[str] | None = None) -> None:
173
+ """Entry point for third_party command."""
174
+ parser = setup_parser()
175
+ parsed_args = parser.parse_args(args)
176
+ if parsed_args.subcommand == "export":
177
+ handle_export(parsed_args)
@@ -12,12 +12,15 @@ Exports
12
12
 
13
13
  import logging
14
14
  import shutil
15
+ from collections.abc import Sequence
15
16
  from pathlib import Path
17
+ from typing import Protocol
16
18
 
17
19
  from evaluators import MetricStream, run_offline_evaluation
18
20
  from evaluators.memory import (
19
21
  MemmapTemporalWriter,
20
22
  TemporalWriterProtocol,
23
+ save_evaluation_config,
21
24
  )
22
25
  from evaluators.utils import comms
23
26
  from lightning import Callback, LightningModule, Trainer
@@ -27,6 +30,44 @@ __all__ = ["DistributedEvaluationCallback", "DistributedInferenceWriter"]
27
30
  logger = logging.getLogger(__name__)
28
31
 
29
32
 
33
+ class PredictEvaluationCallable(Protocol):
34
+ """Signature for post-prediction evaluation callables."""
35
+
36
+ def __call__(
37
+ self,
38
+ streams: Sequence[MetricStream],
39
+ artifact_root: str | Path,
40
+ *,
41
+ trainer: Trainer | None = None,
42
+ pl_module: LightningModule | None = None,
43
+ ) -> dict[str, dict[str, dict[str, float]]] | None:
44
+ """Run evaluation for persisted artifacts."""
45
+ ...
46
+
47
+
48
+ def _default_evaluate_streams(
49
+ streams: Sequence[MetricStream],
50
+ artifact_root: str | Path,
51
+ *,
52
+ trainer: Trainer | None = None,
53
+ pl_module: LightningModule | None = None,
54
+ ) -> dict[str, dict[str, dict[str, float]]]:
55
+ """Default post-predict evaluator using offline evaluation."""
56
+ del trainer, pl_module
57
+ artifact_root = Path(artifact_root)
58
+ all_results: dict[str, dict[str, dict[str, float]]] = {}
59
+
60
+ for stream in streams:
61
+ stream_results = run_offline_evaluation(
62
+ metrics=stream.metrics,
63
+ artifact_dir=artifact_root / stream.name,
64
+ show_progress=False,
65
+ )
66
+ all_results[stream.name] = stream_results
67
+
68
+ return all_results
69
+
70
+
30
71
  class _DistributedWriterBase(Callback):
31
72
  """
32
73
  Base class for callbacks that need to bind MetricStreams to Disk Writers.
@@ -83,6 +124,11 @@ class _DistributedWriterBase(Callback):
83
124
  **self.backend_kwargs,
84
125
  )
85
126
  stream.bind(writer)
127
+ save_evaluation_config(
128
+ output_dir=stream_dir,
129
+ metrics=stream.metrics,
130
+ metadata={"stream_name": stream.name},
131
+ )
86
132
  self._active_streams[stream.name] = (stream, writer)
87
133
 
88
134
  def _close_writers(self) -> None:
@@ -186,11 +232,34 @@ class DistributedInferenceWriter(_DistributedWriterBase):
186
232
  Does NOT run any metrics. Useful for generating offline artifacts.
187
233
  """
188
234
 
235
+ def __init__(
236
+ self,
237
+ output_dir: str | Path,
238
+ *,
239
+ backend: type[TemporalWriterProtocol] = MemmapTemporalWriter,
240
+ evaluate: PredictEvaluationCallable | None = _default_evaluate_streams,
241
+ **backend_kwargs,
242
+ ) -> None:
243
+ super().__init__(
244
+ output_dir=output_dir,
245
+ backend=backend,
246
+ **backend_kwargs,
247
+ )
248
+ self.evaluate = evaluate
249
+
189
250
  def on_predict_start(self, trainer: Trainer, pl_module: LightningModule) -> None:
190
251
  self._setup_streams(pl_module)
191
252
 
192
- def on_predict_epoch_end(
193
- self, trainer: Trainer, pl_module: LightningModule
194
- ) -> None:
253
+ def on_predict_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
195
254
  self._close_writers()
255
+
256
+ if self.evaluate is not None:
257
+ streams = [stream for stream, _ in self._active_streams.values()]
258
+ self.evaluate(
259
+ streams,
260
+ artifact_root=self.output_dir,
261
+ trainer=trainer,
262
+ pl_module=pl_module,
263
+ )
264
+
196
265
  self._active_streams = {}