| """Postprocess cache exporter (test-time). |
| |
| This evaluator is used with vis4d's EvaluatorCallback to export per-image caches |
| needed for depth-based 3D box post-processing, without changing the normal |
| evaluation flow. |
| |
| Cache layout: |
| {cache_root}/{dataset_name}/{image_id}.npz |
| |
| We intentionally store the full metric depth map (aligned to original_hw) to |
| avoid coordinate-system bugs from cropping. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| from typing import Any |
|
|
| import numpy as np |
| import torch |
| from vis4d.common.array import array_to_numpy |
| from vis4d.common.typing import GenericFunc, MetricLogs, NDArrayNumber |
| from vis4d.eval.base import Evaluator |
|
|
|
|
| class PostprocessCacheExporter(Evaluator): |
| """Exports model outputs needed for post-processing into .npz cache files.""" |
|
|
| def __init__( |
| self, |
| cache_root: str, |
| compress: bool = True, |
| overwrite: bool = False, |
| depth_dtype: str = "float32", |
| ) -> None: |
| super().__init__() |
| self.cache_root = cache_root |
| self.compress = compress |
| self.overwrite = overwrite |
| if depth_dtype not in {"float16", "float32"}: |
| raise ValueError(f"Unsupported depth_dtype: {depth_dtype}") |
| self.depth_dtype = depth_dtype |
|
|
| self._num_written = 0 |
| self._num_skipped = 0 |
|
|
| @property |
| def metrics(self) -> list[str]: |
| |
| return [] |
|
|
| def reset(self) -> None: |
| self._num_written = 0 |
| self._num_skipped = 0 |
|
|
| def gather(self, gather_func: GenericFunc) -> None: |
| |
| return |
|
|
| def process_batch( |
| self, |
| coco_image_id: list[int], |
| dataset_names: list[str], |
| pred_boxes: list[NDArrayNumber], |
| pred_scores: list[NDArrayNumber], |
| pred_classes: list[NDArrayNumber], |
| pred_boxes3d: list[NDArrayNumber] | None = None, |
| pred_categories: list[list[str]] | None = None, |
| depth_maps: list[torch.Tensor] | None = None, |
| intrinsics: list[NDArrayNumber] | NDArrayNumber | None = None, |
| original_hw: list[tuple[int, int]] | None = None, |
| ) -> None: |
| """Write one .npz per image.""" |
| if pred_boxes3d is None: |
| |
| print("[PostprocessCacheExporter] Skipping: pred_boxes3d is None") |
| return |
| if depth_maps is None: |
| |
| print("[PostprocessCacheExporter] Skipping: depth_maps is None") |
| return |
| if intrinsics is None: |
| print("[PostprocessCacheExporter] Skipping: intrinsics is None") |
| return |
| if original_hw is None: |
| print("[PostprocessCacheExporter] Skipping: original_hw is None") |
| return |
|
|
| print(f"[PostprocessCacheExporter] Processing batch: {len(coco_image_id)} images") |
|
|
| |
| if torch.is_tensor(intrinsics): |
| |
| intrinsics_np = intrinsics.detach().cpu().numpy() |
| intrinsics_list = [intrinsics_np[j] for j in range(intrinsics_np.shape[0])] |
| elif isinstance(intrinsics, np.ndarray): |
| |
| if intrinsics.ndim == 2: |
| intrinsics_list = [intrinsics for _ in range(len(coco_image_id))] |
| else: |
| intrinsics_list = [intrinsics[j] for j in range(intrinsics.shape[0])] |
| else: |
| |
| intrinsics_list = list(intrinsics) |
|
|
| for i, image_id in enumerate(coco_image_id): |
| dataset_name = dataset_names[i] |
| out_dir = os.path.join(self.cache_root, str(dataset_name)) |
| os.makedirs(out_dir, exist_ok=True) |
|
|
| out_path = os.path.join(out_dir, f"{int(image_id)}.npz") |
| if (not self.overwrite) and os.path.exists(out_path): |
| self._num_skipped += 1 |
| continue |
|
|
| boxes2d = array_to_numpy( |
| pred_boxes[i].to(torch.float32) if hasattr(pred_boxes[i], "to") else pred_boxes[i], |
| n_dims=None, |
| dtype=np.float32, |
| ) |
| scores = array_to_numpy( |
| pred_scores[i].to(torch.float32) if hasattr(pred_scores[i], "to") else pred_scores[i], |
| n_dims=None, |
| dtype=np.float32, |
| ) |
| class_ids = array_to_numpy( |
| pred_classes[i].to(torch.int64) if hasattr(pred_classes[i], "to") else pred_classes[i], |
| n_dims=None, |
| dtype=np.int64, |
| ) |
| boxes3d = array_to_numpy( |
| pred_boxes3d[i].to(torch.float32) if hasattr(pred_boxes3d[i], "to") else pred_boxes3d[i], |
| n_dims=None, |
| dtype=np.float32, |
| ) |
|
|
| |
| depth = depth_maps[i] |
| if depth.ndim == 3 and depth.shape[0] == 1: |
| depth = depth[0] |
| depth_np = depth.detach().cpu().numpy() |
| depth_np = depth_np.astype(np.float16 if self.depth_dtype == "float16" else np.float32) |
|
|
| Ki = intrinsics_list[i] |
| if torch.is_tensor(Ki): |
| K = Ki.detach().cpu().numpy().astype(np.float32) |
| else: |
| K = np.asarray(Ki, dtype=np.float32) |
| hw = original_hw[i] |
|
|
| meta: dict[str, Any] = { |
| "dataset_name": str(dataset_name), |
| "image_id": int(image_id), |
| "original_hw": np.asarray(hw, dtype=np.int32), |
| } |
|
|
| |
| if pred_categories is not None and i < len(pred_categories) and pred_categories[i] is not None: |
| cats = np.asarray(pred_categories[i], dtype=object) |
| else: |
| cats = np.asarray([], dtype=object) |
|
|
| save_fn = np.savez_compressed if self.compress else np.savez |
| save_fn( |
| out_path, |
| boxes2d=boxes2d, |
| scores=scores, |
| class_ids=class_ids, |
| boxes3d_raw=boxes3d, |
| categories=cats, |
| depth_map=depth_np, |
| intrinsics=K, |
| meta=np.asarray(meta, dtype=object), |
| ) |
| self._num_written += 1 |
|
|
| def evaluate(self, metric: str) -> tuple[MetricLogs, str]: |
| |
| return {}, f"PostprocessCacheExporter: wrote={self._num_written}, skipped={self._num_skipped}" |
|
|
| def save(self, metric: str, output_dir: str, prefix: str | None = None) -> None: |
| |
| return |
|
|
|
|
|
|