evaluators 2.1.4__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {evaluators-2.1.4 → evaluators-3.0.0}/PKG-INFO +5 -4
  2. {evaluators-2.1.4 → evaluators-3.0.0}/README.md +3 -3
  3. {evaluators-2.1.4 → evaluators-3.0.0}/pyproject.toml +10 -1
  4. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/_engine.py +1 -1
  5. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/_streams.py +1 -3
  6. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/benchmarks/sys_info.py +11 -7
  7. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/cli/memory.py +5 -3
  8. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/integrations/lightning.py +111 -6
  9. evaluators-3.0.0/sources/evaluators/io/__init__.py +7 -0
  10. evaluators-3.0.0/sources/evaluators/io/mot_challenge.py +136 -0
  11. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/_registry.py +3 -0
  12. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/gds/writer.py +1 -1
  13. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_buffers.py +2 -2
  14. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_writer.py +2 -3
  15. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_blosc.py +1 -1
  16. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/depth.py +2 -2
  17. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/dvps.py +97 -15
  18. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/__init__.py +4 -0
  19. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/_utils.py +1 -0
  20. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/panoptic.py +402 -62
  21. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/segmentation/semantic.py +15 -16
  22. evaluators-3.0.0/sources/evaluators/metrics/domain/segmentation/vendor/__init__.py +7 -0
  23. evaluators-3.0.0/sources/evaluators/metrics/domain/segmentation/vendor/panopticapi.py +252 -0
  24. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/domain/step.py +5 -5
  25. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/__init__.py +16 -0
  26. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_base.py +803 -0
  27. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_classcomb.py +73 -0
  28. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_clear.py +132 -0
  29. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_combine.py +38 -0
  30. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_hota.py +125 -0
  31. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_identity.py +66 -0
  32. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_preprocess.py +263 -0
  33. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_seqdata.py +119 -0
  34. evaluators-3.0.0/sources/evaluators/metrics/domain/tracking/_similarity.py +208 -0
  35. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/stats.py +1 -1
  36. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/presets/__init__.py +3 -2
  37. evaluators-3.0.0/sources/evaluators/presets/mot.py +228 -0
  38. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/coco.py +3 -4
  39. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/utils/tensor_ops.py +2 -4
  40. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/visuals/tools/_visualizer.py +12 -9
  41. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/visuals/tools/transform.py +1 -1
  42. evaluators-2.1.4/sources/evaluators/metrics/domain/tracking.py +0 -0
  43. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/__init__.py +0 -0
  44. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/benchmarks/__init__.py +0 -0
  45. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/benchmarks/coordinator.py +0 -0
  46. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/benchmarks/scenarios.py +0 -0
  47. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/benchmarks/worker.py +0 -0
  48. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/cli/__main__.py +0 -0
  49. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/cli/compute.py +0 -0
  50. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/cli/third_party.py +0 -0
  51. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/constants.py +0 -0
  52. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/__init__.py +0 -0
  53. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/__init__.py +0 -0
  54. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/__init__.py +0 -0
  55. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/_reader.py +0 -0
  56. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/filesystem/_writer.py +0 -0
  57. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/gds/__init__.py +0 -0
  58. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/gds/reader.py +0 -0
  59. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/gds/utils.py +0 -0
  60. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/__init__.py +0 -0
  61. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/_reader.py +0 -0
  62. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/lmdb/_writer.py +0 -0
  63. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/__init__.py +0 -0
  64. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/backends/memmap/_reader.py +0 -0
  65. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/codecs/__init__.py +0 -0
  66. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_png.py +0 -0
  67. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_raw.py +0 -0
  68. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/codecs/_tiff.py +0 -0
  69. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/config.py +0 -0
  70. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/interfaces.py +0 -0
  71. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/memory/schema.py +0 -0
  72. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/__init__.py +0 -0
  73. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/adapters/coco.py +0 -0
  74. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/metrics/adapters/trackeval.py +0 -0
  75. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/presets/cityscapes.py +0 -0
  76. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/presets/kitti.py +0 -0
  77. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/presets/semkitti.py +0 -0
  78. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/scheduling/__init__.py +0 -0
  79. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/scheduling/_catalog.py +0 -0
  80. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/scheduling/_partitioner.py +0 -0
  81. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/__init__.py +0 -0
  82. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/__main__.py +0 -0
  83. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/__init__.py +0 -0
  84. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/dvpq.py +0 -0
  85. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/third_party/adapters/pq.py +0 -0
  86. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/types.py +0 -0
  87. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/utils/comms.py +0 -0
  88. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/utils/configs.py +0 -0
  89. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/visuals/__init__.py +0 -0
  90. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/visuals/metric.py +0 -0
  91. {evaluators-2.1.4 → evaluators-3.0.0}/sources/evaluators/visuals/tools/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: evaluators
3
- Version: 2.1.4
3
+ Version: 3.0.0
4
4
  Summary: Various scene understanding and perception evaluation metrics.
5
5
  Author: Kurt Stolle
6
6
  Author-email: Kurt Stolle <kurt@computer.org>
@@ -27,6 +27,7 @@ Requires-Dist: pydantic
27
27
  Requires-Dist: opencv-python>=4.13.0.92
28
28
  Requires-Dist: omegaconf>=2.3.0
29
29
  Requires-Dist: hydra-zen>=0.16.0
30
+ Requires-Dist: scipy>=1.14.0
30
31
  Requires-Dist: blosc2>=4.1.2 ; extra == 'blosc2'
31
32
  Requires-Dist: lightning>=2.6.1 ; extra == 'lightning'
32
33
  Requires-Dist: lmdb>=2.2.0 ; extra == 'lmdb'
@@ -103,7 +104,7 @@ from evaluators.metrics.domain.segmentation import SemanticMetric
103
104
  dataset = CityscapesDataset(...)
104
105
  metric = SemanticMetric(
105
106
  num_classes=19,
106
- target_source=dataset,
107
+ target_source=dataset,
107
108
  )
108
109
 
109
110
  ### Define the explicit memory schema
@@ -144,12 +145,12 @@ for batch in dataloader: # Model forward pass
144
145
  "frame_index": batch["frame_index"]
145
146
  }
146
147
  )
147
-
148
+
148
149
  # Finalize
149
150
  writer.close()
150
151
  ```
151
152
 
152
- > Note: `sequence_id` and `frame_index` must be `torch.int64`.
153
+ > Note: `sequence_id` and `frame_index` must be `torch.int64`.
153
154
 
154
155
  ##### 4. Finalize and compute
155
156
 
@@ -62,7 +62,7 @@ from evaluators.metrics.domain.segmentation import SemanticMetric
62
62
  dataset = CityscapesDataset(...)
63
63
  metric = SemanticMetric(
64
64
  num_classes=19,
65
- target_source=dataset,
65
+ target_source=dataset,
66
66
  )
67
67
 
68
68
  ### Define the explicit memory schema
@@ -103,12 +103,12 @@ for batch in dataloader: # Model forward pass
103
103
  "frame_index": batch["frame_index"]
104
104
  }
105
105
  )
106
-
106
+
107
107
  # Finalize
108
108
  writer.close()
109
109
  ```
110
110
 
111
- > Note: `sequence_id` and `frame_index` must be `torch.int64`.
111
+ > Note: `sequence_id` and `frame_index` must be `torch.int64`.
112
112
 
113
113
  ##### 4. Finalize and compute
114
114
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "evaluators"
7
- version = "2.1.4"
7
+ version = "3.0.0"
8
8
  description = "Various scene understanding and perception evaluation metrics."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13, <3.14"
@@ -35,6 +35,7 @@ dependencies = [
35
35
  "opencv-python>=4.13.0.92",
36
36
  "omegaconf>=2.3.0",
37
37
  "hydra-zen>=0.16.0",
38
+ "scipy>=1.14.0",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
@@ -117,6 +118,10 @@ ignore = [
117
118
  "SIM118", # usage of 'x in dict.keys()' -- conflicts with `tensordict`.
118
119
  "PLR2004", # magic numbers
119
120
  "PLW2901", # for loop variable overwritten
121
+ # Lazy/deferred imports are a deliberate, pervasive pattern (optional deps:
122
+ # matplotlib, codecs, heavy adapters) used to keep import and CLI startup
123
+ # cheap; PLC0415 (import-should-be-at-top) fights that design codebase-wide.
124
+ "PLC0415",
120
125
  "D212",
121
126
  "D203",
122
127
  "D107", # docstring on __init__
@@ -209,6 +214,10 @@ include = ["sources", "tests", "notebooks", "scripts"]
209
214
 
210
215
  [tool.pytest.ini_options]
211
216
  timeout = 10
217
+ # Vendored reference implementations under third_party/ ship their own
218
+ # environment-dependent test suites (e.g. TrackEval); never collect them as
219
+ # part of this project's tests. Mirrors the ruff `exclude = ["third_party"]`.
220
+ addopts = "--ignore=third_party"
212
221
  markers = [
213
222
  "integration: integration tests that exercise real pretrained model inference end-to-end (may require network for first-time weight download).",
214
223
  "slow: tests with high wall-clock cost.",
@@ -272,7 +272,7 @@ def _save_plots(
272
272
  force: bool,
273
273
  ) -> None:
274
274
  """Call ``plot()`` on each metric and save the figure to *out_path*."""
275
- import matplotlib.pyplot as plt
275
+ import matplotlib.pyplot as plt # lazy: matplotlib is optional
276
276
 
277
277
  for metric in metrics:
278
278
  name = metric.__class__.__name__
@@ -216,9 +216,7 @@ class MetricStream(nn.Module):
216
216
 
217
217
  # 2. Plumbing / Renaming
218
218
  if self.input_map:
219
- # We invert the map to {model_key: metric_key} for the rename operation
220
- # rename_map = {v: k for k, v in self.input_map.items()}
221
- # filtered_batch = filtered_batch.rename_keys_(rename_map, strict=False)
219
+ # We map model keys to metric keys as defined in the input map.
222
220
  for metric_key, model_key in self.input_map.items():
223
221
  if model_key in filtered_batch.keys():
224
222
  filtered_batch.rename_key_(model_key, metric_key)
@@ -139,7 +139,7 @@ def get_machine_info() -> MachineInfo:
139
139
  # CPU Info
140
140
  try:
141
141
  cpu_info = subprocess.check_output(
142
- "lscpu", shell=True, stderr=subprocess.DEVNULL
142
+ ["lscpu"], stderr=subprocess.DEVNULL
143
143
  ).decode()
144
144
  for line in cpu_info.split("\n"):
145
145
  if "Model name:" in line:
@@ -166,8 +166,14 @@ def get_machine_info() -> MachineInfo:
166
166
  try:
167
167
  mnt_out = (
168
168
  subprocess.check_output(
169
- f"findmnt -n -o SOURCE,FSTYPE --target {current_dir}",
170
- shell=True,
169
+ [
170
+ "findmnt",
171
+ "-n",
172
+ "-o",
173
+ "SOURCE,FSTYPE",
174
+ "--target",
175
+ current_dir,
176
+ ],
171
177
  stderr=subprocess.DEVNULL,
172
178
  )
173
179
  .decode()
@@ -184,8 +190,7 @@ def get_machine_info() -> MachineInfo:
184
190
  try:
185
191
  parent = (
186
192
  subprocess.check_output(
187
- f"lsblk -no PKNAME {target_dev}",
188
- shell=True,
193
+ ["lsblk", "-no", "PKNAME", target_dev],
189
194
  stderr=subprocess.DEVNULL,
190
195
  )
191
196
  .decode()
@@ -204,8 +209,7 @@ def get_machine_info() -> MachineInfo:
204
209
  try:
205
210
  model_lines = (
206
211
  subprocess.check_output(
207
- f"lsblk -no MODEL {target_dev}",
208
- shell=True,
212
+ ["lsblk", "-no", "MODEL", target_dev],
209
213
  stderr=subprocess.DEVNULL,
210
214
  )
211
215
  .decode()
@@ -289,9 +289,11 @@ def handle_validate(args):
289
289
  status.update(f"[bold green]Validating {vid_id}...")
290
290
  if args.quick:
291
291
  # Just check if fragment files exist
292
- for frag in entry["fragments"]:
293
- if not (path / frag["chunk_id"]).exists():
294
- errors.append((vid_id, f"Chunk {frag['chunk_id']} missing"))
292
+ errors.extend(
293
+ (vid_id, f"Chunk {frag['chunk_id']} missing")
294
+ for frag in entry["fragments"]
295
+ if not (path / frag["chunk_id"]).exists()
296
+ )
295
297
  continue
296
298
 
297
299
  try:
@@ -10,6 +10,7 @@ Exports
10
10
  - ``DistributedInferenceWriter``: For Predict mode (Save only).
11
11
  """
12
12
 
13
+ import contextlib
13
14
  import gc
14
15
  import logging
15
16
  import shutil
@@ -24,6 +25,7 @@ from evaluators.memory import (
24
25
  TemporalWriterProtocol,
25
26
  save_evaluation_config,
26
27
  )
28
+ from evaluators.types import MetricResult
27
29
  from evaluators.utils import comms
28
30
  from lightning import Callback, LightningModule, Trainer
29
31
 
@@ -55,15 +57,23 @@ def _default_evaluate_streams(
55
57
  pl_module: LightningModule | None = None,
56
58
  ) -> dict[str, dict[str, dict[str, float]]]:
57
59
  """Default post-predict evaluator using offline evaluation."""
58
- del trainer, pl_module
60
+ del trainer
59
61
  artifact_root = Path(artifact_root)
60
62
  all_results: dict[str, dict[str, dict[str, float]]] = {}
61
63
 
64
+ # Pick the trainer's compute device for metric ``.update`` calls.
65
+ # ``run_offline_evaluation`` defaults to CPU if ``device`` is
66
+ # ``None``; the GPU dispatch shaves ~12 min off SemKITTI-DVPS
67
+ # finalize. See the ``DistributedEvaluationCallback._finalize_epoch``
68
+ # callsite for the longer rationale.
69
+ device = pl_module.device if pl_module is not None else None
70
+
62
71
  for stream in streams:
63
72
  stream_results = run_offline_evaluation(
64
73
  metrics=stream.metrics,
65
74
  artifact_dir=artifact_root / stream.name,
66
75
  show_progress=False,
76
+ device=device,
67
77
  )
68
78
  all_results[stream.name] = stream_results
69
79
 
@@ -212,16 +222,18 @@ class DistributedEvaluationCallback(_DistributedWriterBase):
212
222
  def on_validation_epoch_end(
213
223
  self, trainer: Trainer, pl_module: LightningModule
214
224
  ) -> None:
215
- self._finalize_epoch(pl_module, "val")
225
+ self._finalize_epoch(trainer, pl_module, "val")
216
226
 
217
227
  def on_test_epoch_start(self, trainer: Trainer, pl_module: LightningModule) -> None:
218
228
  self._setup_streams(pl_module)
219
229
 
220
230
  def on_test_epoch_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
221
- self._finalize_epoch(pl_module, "test")
231
+ self._finalize_epoch(trainer, pl_module, "test")
222
232
 
223
- def _finalize_epoch(self, pl_module: LightningModule, split: str) -> None:
224
- """Closes writers, runs metrics, and logs results to Lightning.
233
+ def _finalize_epoch(
234
+ self, trainer: Trainer, pl_module: LightningModule, split: str
235
+ ) -> None:
236
+ """Close writers, run metrics, log scalars and plots to Lightning.
225
237
 
226
238
  Writer-side memory maps are released before ``run_offline_evaluation``
227
239
  opens reader-side maps over the same files; otherwise the two sets
@@ -238,6 +250,17 @@ class DistributedEvaluationCallback(_DistributedWriterBase):
238
250
  metrics=stream.metrics,
239
251
  artifact_dir=stream_dir,
240
252
  show_progress=False,
253
+ # Route per-frame metric ``.update`` to the same
254
+ # device the trainer is using. Without this,
255
+ # ``run_offline_evaluation`` defaults to ``None`` →
256
+ # CPU, which made full-validation metric finalize
257
+ # take ~12 min on the SemKITTI-DVPS val split
258
+ # (255 frames, PQ + DVPQ {k=1,5,10,20} +
259
+ # SemanticMetric + DepthMetric). Panopticapi PQ
260
+ # matching is still CPU-only (pure Python), but
261
+ # the tensor reductions for the other three
262
+ # metrics + the DVPQ window stacks move to GPU.
263
+ device=pl_module.device,
241
264
  )
242
265
 
243
266
  if comms.get_rank() != 0:
@@ -249,7 +272,13 @@ class DistributedEvaluationCallback(_DistributedWriterBase):
249
272
  log_key = f"{split}/{stream_name}/{metric_cls}/{key}"
250
273
  pl_module.log(log_key, val, sync_dist=True)
251
274
 
252
- # 2. Print Summary Tables (Interactive/Human-Friendly)
275
+ # 2. Per-metric plots (when the metric implements ``plot()``
276
+ # and at least one Lightning logger supports image upload).
277
+ self._log_metric_plots(
278
+ trainer, pl_module, split, stream_name, stream, stream_results
279
+ )
280
+
281
+ # 3. Print Summary Tables (Interactive/Human-Friendly)
253
282
  if sys.stdout.isatty():
254
283
  self._print_stream_summary(stream_name, stream)
255
284
 
@@ -263,6 +292,82 @@ class DistributedEvaluationCallback(_DistributedWriterBase):
263
292
  self._streams = {}
264
293
  gc.collect()
265
294
 
295
+ @staticmethod
296
+ def _log_metric_plots(
297
+ trainer: Trainer,
298
+ pl_module: LightningModule,
299
+ split: str,
300
+ stream_name: str,
301
+ stream: MetricStream,
302
+ stream_results: dict[str, dict[str, float]],
303
+ ) -> None:
304
+ """Render each metric's ``plot()`` and push the figure to every
305
+ Lightning logger that exposes ``log_image`` (e.g. ``WandbLogger``).
306
+
307
+ Iterates ``trainer.loggers`` so multi-logger setups (such as
308
+ ``[CSVLogger, WandbLogger]``) are handled correctly. Quietly skips
309
+ metrics that have no ``plot`` method, return None, or raise during
310
+ rendering; observability failures are not load-bearing for training
311
+ and must not crash the validation epoch.
312
+
313
+ Calls ``plot(val=...)`` with a precomputed :class:`MetricResult` so
314
+ the metric does not re-invoke ``compute()`` on state that
315
+ ``run_offline_evaluation`` has already consumed.
316
+ """
317
+ image_loggers = [
318
+ logger_obj
319
+ for logger_obj in getattr(trainer, "loggers", []) or []
320
+ if callable(getattr(logger_obj, "log_image", None))
321
+ ]
322
+ if not image_loggers:
323
+ return
324
+
325
+ for metric in stream.metrics:
326
+ plot_fn = getattr(metric, "plot", None)
327
+ if plot_fn is None or not callable(plot_fn):
328
+ continue
329
+
330
+ metric_name = metric.__class__.__name__
331
+ scores = stream_results.get(metric_name)
332
+ if scores is None:
333
+ continue
334
+
335
+ val = MetricResult.from_dict(scores)
336
+ try:
337
+ result = plot_fn(val=val)
338
+ except Exception:
339
+ logger.exception(
340
+ "metric %s.plot() raised; skipping plot upload", metric_name
341
+ )
342
+ continue
343
+ if result is None:
344
+ continue
345
+
346
+ fig = result[0] if isinstance(result, tuple) else result
347
+ try:
348
+ for image_logger in image_loggers:
349
+ try:
350
+ image_logger.log_image(
351
+ key=f"{split}/{stream_name}/{metric_name}/plot",
352
+ images=[fig],
353
+ step=getattr(pl_module, "global_step", None),
354
+ )
355
+ except Exception:
356
+ logger.exception(
357
+ "log_image failed for %s; skipping plot upload",
358
+ metric_name,
359
+ )
360
+ finally:
361
+ close_fn = getattr(fig, "close", None)
362
+ if close_fn is not None and callable(close_fn):
363
+ with contextlib.suppress(Exception):
364
+ close_fn()
365
+ else:
366
+ with contextlib.suppress(Exception):
367
+ import matplotlib.pyplot as plt
368
+
369
+ plt.close(fig)
370
+
266
371
  @staticmethod
267
372
  def _print_stream_summary(stream_name: str, stream: MetricStream) -> None:
268
373
  from rich.console import Console
@@ -0,0 +1,7 @@
1
+ """I/O readers for tracking-evaluation file formats (MOTChallenge, ...)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .mot_challenge import read_mot_sequence
6
+
7
+ __all__ = ["read_mot_sequence"]
@@ -0,0 +1,136 @@
1
+ """MOTChallenge ground-truth / tracker text-file reader.
2
+
3
+ Parses MOTChallenge CSV rows
4
+ ``frame, id, bb_left, bb_top, bb_width, bb_height, conf, class, visibility``
5
+ for both the gt file and a tracker results file, and returns a list of
6
+ :class:`~evaluators.metrics.domain.tracking._preprocess.RawFrame` (one per
7
+ timestep) ready for the MOTChallenge preprocessor.
8
+
9
+ This module is I/O only; it does no scoring. In MOT gt files the 7th column
10
+ (``conf``) is the 0/1 "consider" flag, exposed here as ``gt_zero_marked``; in
11
+ tracker files the same column is the detection confidence.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import configparser
17
+ import csv
18
+ from pathlib import Path
19
+
20
+ import numpy as np
21
+
22
+ from evaluators.metrics.domain.tracking._preprocess import RawFrame
23
+
24
+ __all__ = ["read_mot_sequence", "read_seq_length"]
25
+
26
+ # Column layout (0-based) of a MOTChallenge CSV row.
27
+ _COL_FRAME = 0
28
+ _COL_ID = 1
29
+ _COL_BOX = slice(2, 6) # bb_left, bb_top, bb_width, bb_height (xywh)
30
+ _COL_CONF = 6 # tracker confidence OR gt zero-marked flag
31
+ _COL_CLASS = 7
32
+ _MIN_COLS = 7 # frame..conf are always required; class defaults to 1 if absent.
33
+
34
+
35
+ def _read_rows(path: Path) -> dict[int, list[list[float]]]:
36
+ """Parse a MOTChallenge file into ``{frame -> list of float rows}``."""
37
+ by_frame: dict[int, list[list[float]]] = {}
38
+ text = Path(path).read_text()
39
+ reader = csv.reader(text.splitlines())
40
+ for raw in reader:
41
+ row = [c for c in raw if c.strip() != ""]
42
+ if len(row) < _MIN_COLS:
43
+ continue
44
+ values = [float(c) for c in row]
45
+ frame = int(values[_COL_FRAME])
46
+ by_frame.setdefault(frame, []).append(values)
47
+ return by_frame
48
+
49
+
50
+ def _frame_arrays(
51
+ rows: list[list[float]],
52
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
53
+ """Convert raw float rows for one frame into ``(boxes, ids, classes, conf)``."""
54
+ if not rows:
55
+ return (
56
+ np.zeros((0, 4), dtype=np.float64),
57
+ np.zeros((0,), dtype=np.int64),
58
+ np.zeros((0,), dtype=np.int64),
59
+ np.zeros((0,), dtype=np.float64),
60
+ )
61
+ data = np.asarray(rows, dtype=np.float64)
62
+ boxes = data[:, _COL_BOX].astype(np.float64)
63
+ ids = data[:, _COL_ID].astype(np.int64)
64
+ conf = data[:, _COL_CONF].astype(np.float64)
65
+ if data.shape[1] > _COL_CLASS:
66
+ classes = data[:, _COL_CLASS].astype(np.int64)
67
+ else:
68
+ classes = np.ones_like(ids)
69
+ return boxes, ids, classes, conf
70
+
71
+
72
+ def read_seq_length(seqinfo: str | Path) -> int:
73
+ """Read ``seqLength`` from a MOTChallenge ``seqinfo.ini``."""
74
+ parser = configparser.ConfigParser()
75
+ parser.read(seqinfo)
76
+ return int(parser["Sequence"]["seqLength"])
77
+
78
+
79
+ def read_mot_sequence(
80
+ gt_path: str | Path,
81
+ ts_path: str | Path,
82
+ *,
83
+ seqinfo: str | Path | None = None,
84
+ ) -> list[RawFrame]:
85
+ """Read a MOTChallenge gt + tracker sequence into per-frame raw data.
86
+
87
+ Parameters
88
+ ----------
89
+ gt_path
90
+ Path to the ground-truth file (``gt.txt``).
91
+ ts_path
92
+ Path to the tracker results file (``det.txt`` / results).
93
+ seqinfo
94
+ Optional path to ``seqinfo.ini``. If given, frames are padded to
95
+ ``seqLength`` (trailing empty frames become empty arrays). If absent, the
96
+ number of frames is the maximum frame index present across either file.
97
+
98
+ Returns
99
+ -------
100
+ list of RawFrame
101
+ One :class:`RawFrame` per timestep, in frame order (index ``t`` is file
102
+ frame ``t + 1``). ``gt_zero_marked`` is taken from the gt ``conf`` column;
103
+ tracker ``conf`` becomes ``tracker_confidences``.
104
+ """
105
+ gt_by_frame = _read_rows(Path(gt_path))
106
+ ts_by_frame = _read_rows(Path(ts_path))
107
+
108
+ if seqinfo is not None:
109
+ num_frames = read_seq_length(seqinfo)
110
+ else:
111
+ max_gt = max(gt_by_frame, default=0)
112
+ max_ts = max(ts_by_frame, default=0)
113
+ num_frames = max(max_gt, max_ts)
114
+
115
+ frames: list[RawFrame] = []
116
+ for t in range(num_frames):
117
+ file_frame = t + 1
118
+ gt_boxes, gt_ids, gt_classes, gt_conf = _frame_arrays(
119
+ gt_by_frame.get(file_frame, [])
120
+ )
121
+ ts_boxes, ts_ids, ts_classes, ts_conf = _frame_arrays(
122
+ ts_by_frame.get(file_frame, [])
123
+ )
124
+ frames.append(
125
+ RawFrame(
126
+ gt_boxes=gt_boxes,
127
+ gt_ids=gt_ids,
128
+ gt_classes=gt_classes,
129
+ gt_zero_marked=gt_conf.astype(np.int64),
130
+ tracker_boxes=ts_boxes,
131
+ tracker_ids=ts_ids,
132
+ tracker_classes=ts_classes,
133
+ tracker_confidences=ts_conf,
134
+ )
135
+ )
136
+ return frames
@@ -113,6 +113,9 @@ def _validate_non_lmdb_chunks(artifact_dir: Path, chunk_ids: set[str]) -> None:
113
113
  raise FileNotFoundError(msg)
114
114
 
115
115
 
116
+ # Backend reader imports are deferred to per-backend dispatchers so importing
117
+ # the registry does not pull every backend's optional dependencies (lmdb,
118
+ # torch.cuda.gds, etc.) at module load.
116
119
  def _open_lmdb_sequence(fragments: Sequence[interfaces.Fragment], artifact_dir: Path):
117
120
  from .lmdb._reader import LMDBVirtualSequence
118
121
 
@@ -40,7 +40,7 @@ class GDSTemporalWriter:
40
40
  ) -> None:
41
41
  # Check for torch.cuda.gds support
42
42
  try:
43
- import torch.cuda.gds
43
+ import torch.cuda.gds # optional CUDA submodule probe
44
44
 
45
45
  if not torch.cuda.gds.is_available():
46
46
  msg = "NVIDIA GPUDirect Storage (GDS) is not available."
@@ -72,8 +72,8 @@ class RaggedBuffer:
72
72
  filename=directory / f"{filename_prefix}_vals.memmap",
73
73
  )
74
74
 
75
- # 2. Offsets Buffer (The index)
76
- # Stores [start_0, start_1, ..., start_N, total_end]
75
+ # 2. Offsets Buffer (The index).
76
+ # Stores the start and end pointers for each row.
77
77
  # We need capacity_rows + 1 to store the final end pointer.
78
78
  self.offsets = MemoryMappedTensor.empty(
79
79
  (capacity_rows + 1,),
@@ -6,6 +6,7 @@ both fixed-size dense tensors and codec-encoded variable-length payloads under
6
6
  one implementation.
7
7
  """
8
8
 
9
+ import contextlib
9
10
  import gc
10
11
  import json
11
12
  from pathlib import Path
@@ -335,10 +336,8 @@ class MemmapTemporalWriter:
335
336
 
336
337
  def __del__(self) -> None:
337
338
  # Best-effort cleanup if close() was skipped due to an exception path.
338
- try:
339
+ with contextlib.suppress(Exception):
339
340
  self._release_buffers()
340
- except Exception:
341
- pass
342
341
 
343
342
  def _validate_special_key(self, key: str, tensor: object) -> None:
344
343
  if not torch.is_tensor(tensor):
@@ -99,7 +99,7 @@ class BloscCodec:
99
99
  )
100
100
  if isinstance(packed, int):
101
101
  msg = "Blosc returned file-size integer, expected in-memory bytes cframe"
102
- raise ValueError(msg)
102
+ raise TypeError(msg)
103
103
  return packed
104
104
 
105
105
  @classmethod
@@ -485,7 +485,7 @@ class DepthMetric(BaseMetric):
485
485
  The matplotlib figure and axes.
486
486
 
487
487
  """
488
- import matplotlib.pyplot as plt
488
+ import matplotlib.pyplot as plt # lazy: matplotlib optional
489
489
 
490
490
  result = val if val is not None else self.compute()
491
491
  d = result.to_dict() if hasattr(result, "to_dict") else dict(result)
@@ -579,7 +579,7 @@ class DepthMetric(BaseMetric):
579
579
  Depth metrics table.
580
580
 
581
581
  """
582
- import pandas as pd
582
+ import pandas as pd # lazy: pandas optional
583
583
 
584
584
  result = val if val is not None else self.compute()
585
585
  d = result.to_dict() if hasattr(result, "to_dict") else dict(result)