rpx_benchmark.adapters.base
Core types for the RPX adapter framework.
::
Sample ─► InputAdapter.prepare ─► PreparedInput(payload, context)
│
▼
model(payload)
│
▼
Sample, context, model_output ─► OutputAdapter.finalize ─► Prediction
Users extending RPX for their own model only need to supply the model and pick a matching pair of adapters. The adapters ship with the library for common families (HuggingFace transformers, UniDepth, Metric3D, raw numpy callables).
1"""Core types for the RPX adapter framework. 2 3:: 4 5 Sample ─► InputAdapter.prepare ─► PreparedInput(payload, context) 6 │ 7 ▼ 8 model(payload) 9 │ 10 ▼ 11 Sample, context, model_output ─► OutputAdapter.finalize ─► Prediction 12 13Users extending RPX for their own model only need to supply the *model* 14and pick a matching pair of adapters. The adapters ship with the 15library for common families (HuggingFace transformers, UniDepth, 16Metric3D, raw numpy callables). 17""" 18 19from __future__ import annotations 20 21from dataclasses import dataclass, field 22from typing import Any, Callable, Dict, Optional, Protocol, Sequence, runtime_checkable 23 24import numpy as np 25 26from ..api import ( 27 BenchmarkModel, 28 DepthPrediction, 29 DetectionPrediction, 30 KeypointCorrespondencePrediction, 31 RelativePosePrediction, 32 Sample, 33 SegmentationPrediction, 34 SparseDepthPrediction, 35 TaskType, 36 Tracklet, 37 TrackletPrediction, 38 VideoDepthPrediction, 39 VideoSample, 40 VisualGroundingPrediction, 41) 42from ..exceptions import AdapterError 43 44# --------------------------------------------------------------------------- # 45# Payload container 46# --------------------------------------------------------------------------- # 47 48 49@dataclass 50class PreparedInput: 51 """Everything a model needs for one sample, plus context for post-processing. 52 53 ``payload`` is whatever the model's forward call accepts. If it is a 54 ``dict``, the default invoker calls ``model(**payload)``; otherwise 55 ``model(payload)``. 56 57 ``context`` is a free-form dict the output adapter receives back. Use 58 it to stash things like target image size, original intrinsics, or 59 any preprocessing metadata the postprocessing step needs. 60 """ 61 62 payload: Any 63 context: Dict[str, Any] = field(default_factory=dict) 64 65 66# --------------------------------------------------------------------------- # 67# Protocol types 68# --------------------------------------------------------------------------- # 69 70 71@runtime_checkable 72class InputAdapter(Protocol): 73 """Sample → model-ready payload.""" 74 75 def setup(self) -> None: # pragma: no cover - optional hook 76 """Optional one-time setup (e.g., build a processor on first use).""" 77 78 def prepare(self, sample: Sample) -> PreparedInput: ... 79 80 81@runtime_checkable 82class OutputAdapter(Protocol): 83 """Model output → RPX prediction object.""" 84 85 def setup(self) -> None: # pragma: no cover - optional hook 86 """Optional one-time setup.""" 87 88 def finalize( 89 self, 90 model_output: Any, 91 context: Dict[str, Any], 92 sample: Sample, 93 ) -> Any: ... 94 95 96ModelInvoker = Callable[[Any, Any], Any] 97"""Given (model, payload) return the raw model output. 98 99The default implementation handles the two common cases: dict payloads 100become ``model(**payload)``, anything else becomes ``model(payload)``. 101Override when the model needs non-standard invocation (e.g., a method 102other than ``__call__``). 103""" 104 105 106def default_invoker(model: Any, payload: Any) -> Any: 107 """Call ``model`` with ``payload``. Uses torch no_grad if available.""" 108 try: 109 import torch # noqa: F401 110 except ImportError: 111 return _dispatch(model, payload) 112 113 import torch 114 115 with torch.no_grad(): 116 return _dispatch(model, payload) 117 118 119def _dispatch(model: Any, payload: Any) -> Any: 120 if isinstance(payload, dict): 121 return model(**payload) 122 return model(payload) 123 124 125# --------------------------------------------------------------------------- # 126# BenchmarkableModel 127# --------------------------------------------------------------------------- # 128 129 130@dataclass 131class BenchmarkableModel(BenchmarkModel): 132 """Compose an input adapter, a model, and an output adapter. 133 134 This is the canonical way to plug a model into the RPX benchmark 135 harness. The ``BenchmarkRunner`` only ever sees the 136 :class:`BenchmarkModel` contract (``setup``, ``predict``); all the 137 model-family-specific logic lives in the adapters so the harness 138 stays task-agnostic. 139 140 Example — wrap a plain numpy depth callable:: 141 142 import numpy as np 143 import rpx_benchmark as rpx 144 145 def my_depth(rgb: np.ndarray) -> np.ndarray: 146 return ... # H x W float32, metres 147 148 bm = rpx.make_numpy_depth_model(my_depth, name="my_model") 149 # Hand `bm` to a task runner or the BenchmarkRunner directly. 150 151 Example — wrap a plain numpy callable:: 152 153 def my_depth(rgb: np.ndarray) -> np.ndarray: 154 return some_depth_in_metres 155 156 bm = make_numpy_depth_model(my_depth) 157 """ 158 159 task: TaskType 160 input_adapter: InputAdapter 161 model: Any 162 output_adapter: OutputAdapter 163 invoker: ModelInvoker = field(default=default_invoker) 164 name: str = "benchmarkable_model" 165 setup_hook: Optional[Callable[[], None]] = None 166 167 def setup(self) -> None: 168 if hasattr(self.input_adapter, "setup"): 169 self.input_adapter.setup() 170 if hasattr(self.output_adapter, "setup"): 171 self.output_adapter.setup() 172 if self.setup_hook is not None: 173 self.setup_hook() 174 175 def predict(self, batch: Sequence[Sample]) -> Sequence[Any]: 176 preds = [] 177 for sample in batch: 178 prepared = self.input_adapter.prepare(sample) 179 model_out = self.invoker(self.model, prepared.payload) 180 pred = self.output_adapter.finalize(model_out, prepared.context, sample) 181 preds.append(pred) 182 return preds 183 184 185# --------------------------------------------------------------------------- # 186# Convenience factory: numpy-in / numpy-out depth model 187# --------------------------------------------------------------------------- # 188 189 190class _NumpyDepthInput: 191 """Hand the model the raw ``(H, W, 3) uint8`` array untouched.""" 192 193 def prepare(self, sample: Sample) -> PreparedInput: 194 rgb = np.asarray(sample.rgb, dtype=np.uint8) 195 return PreparedInput(payload=rgb, context={"target_hw": rgb.shape[:2]}) 196 197 198class _NumpyDepthOutput: 199 """Wrap a raw ``(H_pred, W_pred) float`` array into ``DepthPrediction``. 200 201 If the model returns a shape different from ``target_hw``, the result 202 is bilinearly resized (via PIL) so ``BenchmarkRunner`` can compare 203 pixel-for-pixel against the GT. 204 """ 205 206 def finalize( 207 self, 208 model_output: Any, 209 context: Dict[str, Any], 210 sample: Sample, 211 ) -> DepthPrediction: 212 depth = np.asarray(model_output, dtype=np.float32) 213 if depth.ndim != 2: 214 raise AdapterError( 215 f"numpy depth model must return a 2-D array; got shape {depth.shape}", 216 hint="Your depth callable must return a (H, W) float array. " 217 "A common mistake is returning (1, H, W) or (H, W, 1).", 218 ) 219 target_hw = context.get("target_hw", depth.shape) 220 if depth.shape != target_hw: 221 from PIL import Image 222 223 pil = Image.fromarray(depth, mode="F") 224 pil = pil.resize((target_hw[1], target_hw[0]), Image.BILINEAR) 225 depth = np.asarray(pil, dtype=np.float32) 226 return DepthPrediction(depth_map=depth) 227 228 229# --------------------------------------------------------------------------- # 230# Convenience factory: numpy-in / numpy-out video-depth model 231# --------------------------------------------------------------------------- # 232 233 234class _NumpyVideoDepthInput: 235 """Hand the model the raw ``(T, H, W, 3) uint8`` clip untouched.""" 236 237 def prepare(self, sample: VideoSample) -> PreparedInput: 238 rgb_seq = np.asarray(sample.rgb_seq, dtype=np.uint8) 239 return PreparedInput( 240 payload=rgb_seq, 241 context={"target_thw": rgb_seq.shape[:3]}, 242 ) 243 244 245class _NumpyVideoDepthOutput: 246 """Wrap a raw ``(T, H_pred, W_pred) float`` array into 247 :class:`VideoDepthPrediction`. 248 249 If the model returns a shape different from ``target_thw``, the 250 per-frame depth maps are bilinearly resized (via PIL) so the 251 runner can compare pixel-for-pixel against the GT clip. T must 252 match — a model that drops frames is a contract violation, not 253 something this output adapter is allowed to paper over. 254 """ 255 256 def finalize( 257 self, 258 model_output: Any, 259 context: Dict[str, Any], 260 sample: VideoSample, 261 ) -> VideoDepthPrediction: 262 depth_seq = np.asarray(model_output, dtype=np.float32) 263 if depth_seq.ndim == 4 and depth_seq.shape[1] == 1: 264 # (T, 1, H, W) → (T, H, W); a common model output shape. 265 depth_seq = depth_seq.squeeze(1) 266 if depth_seq.ndim != 3: 267 raise AdapterError( 268 f"numpy video-depth model must return a 3-D (T, H, W) " 269 f"array; got shape {depth_seq.shape}", 270 hint="Your video-depth callable must return (T, H, W) " 271 "float32. A common mistake is returning (T, 1, H, W) " 272 "without squeezing the channel dim.", 273 ) 274 target_thw = context.get("target_thw", depth_seq.shape) 275 target_t, target_h, target_w = target_thw 276 if depth_seq.shape[0] != target_t: 277 raise AdapterError( 278 f"numpy video-depth model returned {depth_seq.shape[0]} " 279 f"frames; clip has {target_t}. The model must emit one " 280 "depth map per input frame — runner / metric calculators " 281 "assume aligned T.", 282 ) 283 if depth_seq.shape[1:] != (target_h, target_w): 284 from PIL import Image 285 286 resized = np.empty( 287 (target_t, target_h, target_w), dtype=np.float32 288 ) 289 for t in range(target_t): 290 pil = Image.fromarray(depth_seq[t], mode="F") 291 pil = pil.resize((target_w, target_h), Image.BILINEAR) 292 resized[t] = np.asarray(pil, dtype=np.float32) 293 depth_seq = resized 294 return VideoDepthPrediction(depth_map_seq=depth_seq) 295 296 297def make_numpy_video_depth_model( 298 fn: Callable[[np.ndarray], np.ndarray], 299 *, 300 name: str = "numpy_video_depth_model", 301 depth_output_kind: str = "metric", 302) -> BenchmarkableModel: 303 """Wrap a plain numpy video-depth callable as a 304 :class:`BenchmarkableModel`. 305 306 The callable must accept a ``(T, H, W, 3) uint8`` RGB clip and 307 return a ``(T, H', W') float`` per-frame depth sequence (metres 308 if metric; up-to-scale if relative — the runner applies per-clip 309 ``(s, t)`` alignment for relative models). If ``(H', W') != 310 (H, W)``, the output is bilinearly resized per frame to match. 311 ``T`` must match the input clip length. 312 313 Parameters 314 ---------- 315 fn : callable 316 Signature: ``fn(rgb_seq_uint8) -> depth_seq_float``. 317 name : str 318 Display name used in logs and reports. 319 depth_output_kind : str 320 ``"metric"`` (default) — model emits metres; runner skips 321 alignment. ``"relative"`` — runner applies per-clip 322 ``(s, t)`` scale-and-shift alignment before metrics. 323 324 Examples 325 -------- 326 >>> import numpy as np 327 >>> import rpx_benchmark as rpx 328 >>> def my_clip_depth(rgb_seq): 329 ... T, H, W, _ = rgb_seq.shape 330 ... return np.full((T, H, W), 2.0, dtype=np.float32) 331 >>> bm = rpx.make_numpy_video_depth_model(my_clip_depth, name="mine") 332 >>> bm.task is rpx.TaskType.VIDEO_DEPTH 333 True 334 """ 335 model = BenchmarkableModel( 336 task=TaskType.VIDEO_DEPTH, 337 input_adapter=_NumpyVideoDepthInput(), 338 model=fn, 339 output_adapter=_NumpyVideoDepthOutput(), 340 invoker=lambda model, payload: model(payload), 341 name=name, 342 ) 343 # Propagate depth_output_kind to the runner's alignment dispatch. 344 model.depth_output_kind = depth_output_kind 345 return model 346 347 348def make_numpy_depth_model( 349 fn: Callable[[np.ndarray], np.ndarray], 350 *, 351 name: str = "numpy_depth_model", 352 depth_output_kind: str = "metric", 353 native_alignment: str | None = None, 354) -> BenchmarkableModel: 355 """Wrap a plain numpy depth callable as a :class:`BenchmarkableModel`. 356 357 The callable must accept a ``(H, W, 3) uint8`` RGB image and return a 358 ``(H', W') float`` depth map. If ``depth_output_kind="metric"`` the 359 values are metres. Relative outputs remain raw until the runner fits 360 one pooled alignment per ``(scene, phase)`` cell. If ``(H', W') != 361 (H, W)``, the output is bilinearly resized to match the ground truth. 362 363 Parameters 364 ---------- 365 fn : callable 366 The depth function. Signature: ``fn(rgb_uint8) -> depth_float``. 367 name : str 368 Display name used in logs and reports. 369 depth_output_kind : str 370 ``"metric"`` (default) or ``"relative"``. 371 native_alignment : str, optional 372 Alignment used for relative output. Defaults to ``"ls_affine"`` 373 for relative models and ``"none"`` for metric models. 374 375 Examples 376 -------- 377 >>> import numpy as np 378 >>> import rpx_benchmark as rpx 379 >>> def my_depth(rgb): 380 ... return np.full(rgb.shape[:2], 2.0, dtype=np.float32) 381 >>> bm = rpx.make_numpy_depth_model(my_depth, name="mine") 382 >>> bm.task is rpx.TaskType.MONOCULAR_DEPTH 383 True 384 """ 385 if depth_output_kind not in {"metric", "relative"}: 386 raise AdapterError( 387 f"depth_output_kind must be 'metric' or 'relative', got {depth_output_kind!r}", 388 ) 389 model = BenchmarkableModel( 390 task=TaskType.MONOCULAR_DEPTH, 391 input_adapter=_NumpyDepthInput(), 392 model=fn, 393 output_adapter=_NumpyDepthOutput(), 394 invoker=lambda model, payload: model(payload), 395 name=name, 396 ) 397 model.depth_output_kind = depth_output_kind 398 model.native_alignment = native_alignment or ( 399 "ls_affine" if depth_output_kind == "relative" else "none" 400 ) 401 return model 402 403 404# --------------------------------------------------------------------------- # 405# Convenience factory: numpy-in / int-mask-out segmentation model 406# --------------------------------------------------------------------------- # 407 408 409class _NumpyMaskInput: 410 """Hand the model the raw ``(H, W, 3) uint8`` array untouched.""" 411 412 def prepare(self, sample: Sample) -> PreparedInput: 413 rgb = np.asarray(sample.rgb, dtype=np.uint8) 414 return PreparedInput(payload=rgb, context={"target_hw": rgb.shape[:2]}) 415 416 417class _NumpyMaskOutput: 418 """Wrap a raw ``(H_pred, W_pred) int`` array into ``SegmentationPrediction``. 419 420 If the model returns a shape different from ``target_hw``, the result 421 is nearest-neighbour resized (masks must keep integer IDs — bilinear 422 would introduce invalid fractional values). 423 """ 424 425 def finalize( 426 self, 427 model_output: Any, 428 context: Dict[str, Any], 429 sample: Sample, 430 ) -> SegmentationPrediction: 431 mask = np.asarray(model_output) 432 if mask.ndim != 2: 433 raise AdapterError( 434 f"numpy mask model must return a 2-D array; got shape {mask.shape}", 435 hint="Your mask callable must return a (H, W) int array. " 436 "A common mistake is returning (H, W, C) per-class logits.", 437 ) 438 mask = mask.astype(np.int32, copy=False) 439 target_hw = context.get("target_hw", mask.shape) 440 if mask.shape != target_hw: 441 from PIL import Image 442 443 pil = Image.fromarray(mask.astype(np.int32), mode="I") 444 pil = pil.resize((target_hw[1], target_hw[0]), Image.NEAREST) 445 mask = np.asarray(pil, dtype=np.int32) 446 return SegmentationPrediction(mask=mask) 447 448 449def make_numpy_mask_model( 450 fn: Callable[[np.ndarray], np.ndarray], 451 *, 452 name: str = "numpy_mask_model", 453) -> BenchmarkableModel: 454 """Wrap a plain numpy instance-mask callable as a :class:`BenchmarkableModel`. 455 456 The callable must accept a ``(H, W, 3) uint8`` RGB image and return 457 a ``(H', W') int`` instance mask where pixel values are instance 458 IDs (``0`` is conventionally background). If ``(H', W') != (H, W)`` 459 the output is nearest-neighbour resized to match the GT mask so 460 integer IDs are preserved. 461 462 Parameters 463 ---------- 464 fn : callable 465 ``fn(rgb_uint8) -> mask_int``. 466 name : str 467 468 Examples 469 -------- 470 >>> import numpy as np 471 >>> import rpx_benchmark as rpx 472 >>> def my_seg(rgb): 473 ... mask = np.zeros(rgb.shape[:2], dtype=np.int32) 474 ... mask[rgb.sum(-1) > 384] = 1 # trivial brightness threshold 475 ... return mask 476 >>> bm = rpx.make_numpy_mask_model(my_seg, name="mine") 477 >>> bm.task is rpx.TaskType.OBJECT_SEGMENTATION 478 True 479 """ 480 return BenchmarkableModel( 481 task=TaskType.OBJECT_SEGMENTATION, 482 input_adapter=_NumpyMaskInput(), 483 model=fn, 484 output_adapter=_NumpyMaskOutput(), 485 invoker=lambda model, payload: model(payload), 486 name=name, 487 ) 488 489 490# --------------------------------------------------------------------------- # 491# Shared helpers for numpy fast paths 492# --------------------------------------------------------------------------- # 493 494 495def _passthrough_invoker(model: Any, payload: Any) -> Any: 496 """Invoker for numpy fast paths — just call the function on its payload.""" 497 return model(payload) 498 499 500class _NumpyRgbInput: 501 """Every numpy task's input adapter: just hand the RGB ndarray through. 502 503 Extra tokens (second RGB, target pose, ...) travel in 504 ``sample.metadata`` and are picked out by the per-task output 505 adapter, so a single input adapter serves every task. 506 """ 507 508 def prepare(self, sample: Sample) -> PreparedInput: 509 rgb = np.asarray(sample.rgb, dtype=np.uint8) 510 context = {"target_hw": rgb.shape[:2], "sample": sample} 511 return PreparedInput(payload=rgb, context=context) 512 513 514def _coerce_dict_result( 515 result: Any, 516 *, 517 required_keys: Sequence[str], 518 task_name: str, 519) -> Dict[str, Any]: 520 """Normalise a function's return value into a dict with the expected keys. 521 522 Accepts either a dict or an ``(a, b, ...)`` tuple aligned with 523 ``required_keys``. Raises :class:`AdapterError` when neither 524 shape matches. 525 """ 526 if isinstance(result, dict): 527 missing = [k for k in required_keys if k not in result] 528 if missing: 529 raise AdapterError( 530 f"{task_name} model result is missing required keys: {missing}", 531 hint=f"Return a dict with keys {list(required_keys)}.", 532 ) 533 return {k: result[k] for k in required_keys} 534 if isinstance(result, tuple) and len(result) == len(required_keys): 535 return dict(zip(required_keys, result, strict=False)) 536 raise AdapterError( 537 f"{task_name} model must return a dict with keys " 538 f"{list(required_keys)} or a {len(required_keys)}-tuple in that order; " 539 f"got {type(result).__name__}", 540 ) 541 542 543# --------------------------------------------------------------------------- # 544# Object detection — fn(rgb) -> {boxes, scores, labels} 545# --------------------------------------------------------------------------- # 546 547 548class _NumpyDetectionOutput: 549 """Wrap a detection callable's output into a :class:`DetectionPrediction`.""" 550 551 def finalize( 552 self, 553 model_output: Any, 554 context: Dict[str, Any], 555 sample: Sample, 556 ) -> DetectionPrediction: 557 fields = _coerce_dict_result( 558 model_output, 559 required_keys=("boxes", "scores", "labels"), 560 task_name="detection", 561 ) 562 boxes = np.asarray(fields["boxes"], dtype=np.float32).reshape(-1, 4) 563 scores = np.asarray(fields["scores"], dtype=np.float32).reshape(-1) 564 labels = list(fields["labels"]) 565 if not (len(boxes) == len(scores) == len(labels)): 566 raise AdapterError( 567 f"detection model returned mismatched lengths: " 568 f"boxes={len(boxes)} scores={len(scores)} labels={len(labels)}", 569 ) 570 return DetectionPrediction(boxes=boxes, scores=scores, labels=labels) 571 572 573def make_numpy_detection_model( 574 fn: Callable[[np.ndarray], Any], 575 *, 576 name: str = "numpy_detection_model", 577 task: TaskType = TaskType.OBJECT_DETECTION, 578) -> BenchmarkableModel: 579 """Wrap a plain numpy detection callable as a :class:`BenchmarkableModel`. 580 581 The callable must accept a ``(H, W, 3) uint8`` RGB image and 582 return either a dict with keys ``"boxes"`` / ``"scores"`` / 583 ``"labels"`` or a ``(boxes, scores, labels)`` tuple in that 584 order. Boxes are pixel coordinates in ``(x1, y1, x2, y2)`` 585 format, scores are floats in ``[0, 1]``, labels are strings. 586 587 Parameters 588 ---------- 589 fn : callable 590 ``fn(rgb_uint8) -> dict | tuple``. 591 name : str 592 Display name for reports. 593 task : TaskType 594 Use :attr:`TaskType.OBJECT_DETECTION` for closed-vocabulary 595 detection or :attr:`TaskType.OPEN_VOCAB_DETECTION` for 596 open-vocab (the Prediction contract is the same). 597 598 Examples 599 -------- 600 >>> import numpy as np 601 >>> import rpx_benchmark as rpx 602 >>> def my_det(rgb): 603 ... return { 604 ... "boxes": np.array([[10, 10, 30, 30]], dtype=np.float32), 605 ... "scores": np.array([0.9], dtype=np.float32), 606 ... "labels": ["cup"], 607 ... } 608 >>> bm = rpx.make_numpy_detection_model(my_det) 609 >>> bm.task is rpx.TaskType.OBJECT_DETECTION 610 True 611 """ 612 return BenchmarkableModel( 613 task=task, 614 input_adapter=_NumpyRgbInput(), 615 model=fn, 616 output_adapter=_NumpyDetectionOutput(), 617 invoker=_passthrough_invoker, 618 name=name, 619 ) 620 621 622# --------------------------------------------------------------------------- # 623# Visual grounding — fn(rgb, text) -> {boxes, scores} 624# --------------------------------------------------------------------------- # 625 626 627class _NumpyGroundingInput: 628 """Grounding input: raw RGB + the referring expression from the GT.""" 629 630 def prepare(self, sample: Sample) -> PreparedInput: 631 rgb = np.asarray(sample.rgb, dtype=np.uint8) 632 text = getattr(sample.ground_truth, "text", "") 633 return PreparedInput( 634 payload={"rgb": rgb, "text": text}, 635 context={"target_hw": rgb.shape[:2]}, 636 ) 637 638 639class _NumpyGroundingOutput: 640 def finalize( 641 self, 642 model_output: Any, 643 context: Dict[str, Any], 644 sample: Sample, 645 ) -> VisualGroundingPrediction: 646 fields = _coerce_dict_result( 647 model_output, 648 required_keys=("boxes", "scores"), 649 task_name="grounding", 650 ) 651 boxes = np.asarray(fields["boxes"], dtype=np.float32).reshape(-1, 4) 652 scores = np.asarray(fields["scores"], dtype=np.float32).reshape(-1) 653 labels = fields.get("labels") if isinstance(fields, dict) else None 654 return VisualGroundingPrediction( 655 boxes=boxes, 656 scores=scores, 657 labels=labels, 658 ) 659 660 661def _grounding_invoker(model: Any, payload: Dict[str, Any]) -> Any: 662 """Grounding callable signature: ``fn(rgb, text)``.""" 663 return model(payload["rgb"], payload["text"]) 664 665 666def make_numpy_grounding_model( 667 fn: Callable[[np.ndarray, str], Any], 668 *, 669 name: str = "numpy_grounding_model", 670) -> BenchmarkableModel: 671 """Wrap a visual-grounding callable as a :class:`BenchmarkableModel`. 672 673 The callable takes ``(rgb_uint8, text)`` and returns either a 674 dict with keys ``"boxes"`` / ``"scores"`` or a tuple 675 ``(boxes, scores)``. Boxes are ``(x1, y1, x2, y2)`` pixel 676 coordinates; scores are floats. The referring expression 677 ``text`` is plucked from ``sample.ground_truth.text`` by the 678 adapter so the callable never sees the GT boxes. 679 680 Examples 681 -------- 682 >>> import numpy as np 683 >>> import rpx_benchmark as rpx 684 >>> def my_ground(rgb, text): 685 ... return ( 686 ... np.array([[10, 10, 30, 30]], dtype=np.float32), 687 ... np.array([0.8], dtype=np.float32), 688 ... ) 689 >>> bm = rpx.make_numpy_grounding_model(my_ground) 690 """ 691 return BenchmarkableModel( 692 task=TaskType.VISUAL_GROUNDING, 693 input_adapter=_NumpyGroundingInput(), 694 model=fn, 695 output_adapter=_NumpyGroundingOutput(), 696 invoker=_grounding_invoker, 697 name=name, 698 ) 699 700 701# --------------------------------------------------------------------------- # 702# Relative camera pose — fn(rgb_a, rgb_b) -> {rotation, translation} 703# --------------------------------------------------------------------------- # 704 705 706class _NumpyPoseInput: 707 """Pose input: extract rgb_a + rgb_b from the sample metadata side-channel.""" 708 709 def prepare(self, sample: Sample) -> PreparedInput: 710 rgb_a = np.asarray(sample.rgb, dtype=np.uint8) 711 rgb_b = None 712 if sample.metadata is not None: 713 rgb_b = sample.metadata.get("rgb_b") 714 if rgb_b is None: 715 raise AdapterError( 716 "Relative-pose numpy adapter requires a second RGB frame " 717 "(`rgb_b`) loaded by the dataset.", 718 hint="Check that your manifest entries include `rgb_b` " 719 "alongside `rgb` so the loader stashes it in sample.metadata.", 720 ) 721 return PreparedInput( 722 payload={"rgb_a": rgb_a, "rgb_b": np.asarray(rgb_b, dtype=np.uint8)}, 723 context={"target_hw": rgb_a.shape[:2]}, 724 ) 725 726 727class _NumpyPoseOutput: 728 def finalize( 729 self, 730 model_output: Any, 731 context: Dict[str, Any], 732 sample: Sample, 733 ) -> RelativePosePrediction: 734 fields = _coerce_dict_result( 735 model_output, 736 required_keys=("rotation", "translation"), 737 task_name="relative_pose", 738 ) 739 return RelativePosePrediction( 740 rotation=np.asarray(fields["rotation"], dtype=np.float64), 741 translation=np.asarray(fields["translation"], dtype=np.float64), 742 ) 743 744 745def _pose_invoker(model: Any, payload: Dict[str, Any]) -> Any: 746 return model(payload["rgb_a"], payload["rgb_b"]) 747 748 749def make_numpy_pose_model( 750 fn: Callable[[np.ndarray, np.ndarray], Any], 751 *, 752 name: str = "numpy_pose_model", 753) -> BenchmarkableModel: 754 """Wrap a relative-camera-pose callable as a :class:`BenchmarkableModel`. 755 756 The callable takes ``(rgb_a, rgb_b)`` and returns either a dict 757 with keys ``"rotation"`` (3×3 rotation matrix or 4-element 758 quaternion) and ``"translation"`` (3-vector, metres) or a 759 ``(rotation, translation)`` tuple. 760 761 Examples 762 -------- 763 >>> import numpy as np 764 >>> import rpx_benchmark as rpx 765 >>> def my_pose(rgb_a, rgb_b): 766 ... return {"rotation": np.eye(3), "translation": np.zeros(3)} 767 >>> bm = rpx.make_numpy_pose_model(my_pose) 768 """ 769 return BenchmarkableModel( 770 task=TaskType.RELATIVE_CAMERA_POSE, 771 input_adapter=_NumpyPoseInput(), 772 model=fn, 773 output_adapter=_NumpyPoseOutput(), 774 invoker=_pose_invoker, 775 name=name, 776 ) 777 778 779# --------------------------------------------------------------------------- # 780# Sparse depth — fn(rgb, coords) -> depths 781# --------------------------------------------------------------------------- # 782 783 784class _NumpySparseDepthInput: 785 """Sparse-depth input: rgb + the GT sparse pixel coordinates.""" 786 787 def prepare(self, sample: Sample) -> PreparedInput: 788 rgb = np.asarray(sample.rgb, dtype=np.uint8) 789 gt = sample.ground_truth 790 coords = getattr(gt, "coordinates", None) 791 if coords is None: 792 raise AdapterError( 793 "Sparse-depth ground truth missing `coordinates`.", 794 ) 795 return PreparedInput( 796 payload={"rgb": rgb, "coordinates": np.asarray(coords, dtype=np.float32)}, 797 context={"target_hw": rgb.shape[:2]}, 798 ) 799 800 801class _NumpySparseDepthOutput: 802 def finalize( 803 self, 804 model_output: Any, 805 context: Dict[str, Any], 806 sample: Sample, 807 ) -> SparseDepthPrediction: 808 # The callable is responsible for returning depths at the 809 # sparse coords it was given; we pass its coords back as-is. 810 coords = np.asarray(sample.ground_truth.coordinates, dtype=np.float32) 811 depths = np.asarray(model_output, dtype=np.float32).reshape(-1) 812 if len(depths) != len(coords): 813 raise AdapterError( 814 f"sparse-depth model returned {len(depths)} depths for " 815 f"{len(coords)} queried coordinates.", 816 ) 817 return SparseDepthPrediction(coordinates=coords, depths=depths) 818 819 820def _sparse_depth_invoker(model: Any, payload: Dict[str, Any]) -> Any: 821 return model(payload["rgb"], payload["coordinates"]) 822 823 824def make_numpy_sparse_depth_model( 825 fn: Callable[[np.ndarray, np.ndarray], np.ndarray], 826 *, 827 name: str = "numpy_sparse_depth_model", 828) -> BenchmarkableModel: 829 """Wrap a sparse-depth callable as a :class:`BenchmarkableModel`. 830 831 The callable takes ``(rgb_uint8, coords)`` where ``coords`` is a 832 ``(N, 2)`` float32 array of pixel coordinates and returns an 833 ``(N,)`` float32 array of depths in metres at those exact 834 coordinates. 835 836 Examples 837 -------- 838 >>> import numpy as np 839 >>> import rpx_benchmark as rpx 840 >>> def my_sd(rgb, coords): 841 ... return np.full(len(coords), 2.0, dtype=np.float32) 842 >>> bm = rpx.make_numpy_sparse_depth_model(my_sd) 843 """ 844 return BenchmarkableModel( 845 task=TaskType.SPARSE_DEPTH, 846 input_adapter=_NumpySparseDepthInput(), 847 model=fn, 848 output_adapter=_NumpySparseDepthOutput(), 849 invoker=_sparse_depth_invoker, 850 name=name, 851 ) 852 853 854# --------------------------------------------------------------------------- # 855# --------------------------------------------------------------------------- # 856 857 858 859 860 861 862 863 864 865 866# --------------------------------------------------------------------------- # 867# Keypoint matching — fn(rgb_a, rgb_b) -> {points0, points1} 868# --------------------------------------------------------------------------- # 869 870 871class _NumpyKeypointInput: 872 """Keypoint matching input: rgb_a + rgb_b from sample metadata.""" 873 874 def prepare(self, sample: Sample) -> PreparedInput: 875 rgb_a = np.asarray(sample.rgb, dtype=np.uint8) 876 rgb_b = None 877 if sample.metadata is not None: 878 rgb_b = sample.metadata.get("rgb_b") 879 if rgb_b is None: 880 raise AdapterError( 881 "Keypoint matching numpy adapter requires a second RGB " 882 "frame (`rgb_b`) loaded by the dataset.", 883 hint="Check that your manifest entries include `rgb_b` alongside `rgb`.", 884 ) 885 return PreparedInput( 886 payload={"rgb_a": rgb_a, "rgb_b": np.asarray(rgb_b, dtype=np.uint8)}, 887 context={"target_hw": rgb_a.shape[:2]}, 888 ) 889 890 891class _NumpyKeypointOutput: 892 def finalize( 893 self, 894 model_output: Any, 895 context: Dict[str, Any], 896 sample: Sample, 897 ) -> KeypointCorrespondencePrediction: 898 if isinstance(model_output, dict): 899 p0 = np.asarray(model_output.get("points0"), dtype=np.float32) 900 p1 = np.asarray(model_output.get("points1"), dtype=np.float32) 901 scores = model_output.get("scores") 902 elif isinstance(model_output, tuple) and len(model_output) in (2, 3): 903 p0 = np.asarray(model_output[0], dtype=np.float32) 904 p1 = np.asarray(model_output[1], dtype=np.float32) 905 scores = model_output[2] if len(model_output) == 3 else None 906 else: 907 raise AdapterError( 908 "keypoint matching model must return a dict with keys " 909 "{points0, points1[, scores]} or a 2/3-tuple.", 910 ) 911 scores_arr = ( 912 np.asarray(scores, dtype=np.float32).reshape(-1) if scores is not None else None 913 ) 914 return KeypointCorrespondencePrediction( 915 points0=p0.reshape(-1, 2), 916 points1=p1.reshape(-1, 2), 917 scores=scores_arr, 918 ) 919 920 921def _keypoint_invoker(model: Any, payload: Dict[str, Any]) -> Any: 922 return model(payload["rgb_a"], payload["rgb_b"]) 923 924 925def make_numpy_keypoint_model( 926 fn: Callable[[np.ndarray, np.ndarray], Any], 927 *, 928 name: str = "numpy_keypoint_model", 929) -> BenchmarkableModel: 930 """Wrap a keypoint-matching callable as a :class:`BenchmarkableModel`. 931 932 The callable takes ``(rgb_a, rgb_b)`` and returns either a dict 933 with keys ``"points0"``, ``"points1"`` and optional ``"scores"`` 934 or a 2/3-tuple in the same order. Points are ``(N, 2)`` pixel 935 coordinates. 936 937 Examples 938 -------- 939 >>> import numpy as np 940 >>> import rpx_benchmark as rpx 941 >>> def my_matcher(rgb_a, rgb_b): 942 ... pts = np.array([[10, 10], [20, 20]], dtype=np.float32) 943 ... return pts, pts 944 >>> bm = rpx.make_numpy_keypoint_model(my_matcher) 945 """ 946 return BenchmarkableModel( 947 task=TaskType.KEYPOINT_MATCHING, 948 input_adapter=_NumpyKeypointInput(), 949 model=fn, 950 output_adapter=_NumpyKeypointOutput(), 951 invoker=_keypoint_invoker, 952 name=name, 953 ) 954 955 956# --------------------------------------------------------------------------- # 957# Object tracking — fn(rgb) -> [{track_id, boxes, scores?}, ...] 958# --------------------------------------------------------------------------- # 959 960 961class _NumpyTrackingInput: 962 """Tracking input: a single RGB frame per step. 963 964 Per-frame tracking protocol: the model is handed one frame and 965 must return the *currently active* tracks with a persistent 966 ``track_id`` per track. The surrounding benchmark pipeline is 967 responsible for accumulating tracks into a full :class:`Tracklet` 968 set at scene boundaries. 969 """ 970 971 def prepare(self, sample: Sample) -> PreparedInput: 972 rgb = np.asarray(sample.rgb, dtype=np.uint8) 973 return PreparedInput( 974 payload={"rgb": rgb}, 975 context={"target_hw": rgb.shape[:2], "sample_id": sample.id}, 976 ) 977 978 979class _NumpyTrackingOutput: 980 def finalize( 981 self, 982 model_output: Any, 983 context: Dict[str, Any], 984 sample: Sample, 985 ) -> TrackletPrediction: 986 tracks = _coerce_tracking_output(model_output) 987 return TrackletPrediction(tracks=tracks) 988 989 990def _coerce_tracking_output(model_output: Any) -> list[Tracklet]: 991 """Accept a variety of tracking return shapes; normalise to Tracklets. 992 993 Supported shapes: 994 * ``TrackletPrediction(tracks=[...])`` — returned verbatim. 995 * ``[Tracklet, ...]`` — returned verbatim. 996 * ``[{"track_id": str, "boxes": (T, 4), "scores"?: (T,)}, ...]`` 997 * ``{"tracks": [as above]}`` — unwrapped. 998 """ 999 if isinstance(model_output, TrackletPrediction): 1000 return list(model_output.tracks) 1001 if isinstance(model_output, dict) and "tracks" in model_output: 1002 model_output = model_output["tracks"] 1003 if not isinstance(model_output, Sequence): 1004 raise AdapterError( 1005 "Tracking model must return a sequence of tracks or a TrackletPrediction.", 1006 hint="Return either [Tracklet, ...] or " 1007 "[{'track_id': str, 'boxes': (T,4) array, ...}, ...].", 1008 ) 1009 tracks: list[Tracklet] = [] 1010 for item in model_output: 1011 if isinstance(item, Tracklet): 1012 tracks.append(item) 1013 continue 1014 if not isinstance(item, dict): 1015 raise AdapterError( 1016 f"Tracking output entries must be Tracklet or dict, got {type(item).__name__}.", 1017 ) 1018 try: 1019 boxes = np.asarray(item["boxes"], dtype=np.float32).reshape(-1, 4) 1020 except (KeyError, ValueError) as e: 1021 raise AdapterError( 1022 f"Tracklet dict is missing / malformed 'boxes': {e}", 1023 hint="Boxes must be a (T, 4) array of [x1,y1,x2,y2] floats.", 1024 ) from e 1025 scores_raw = item.get("scores") 1026 scores = ( 1027 np.asarray(scores_raw, dtype=np.float32).reshape(-1) if scores_raw is not None else None 1028 ) 1029 tracks.append( 1030 Tracklet( 1031 track_id=str(item.get("track_id", f"t{len(tracks)}")), 1032 boxes=boxes, 1033 scores=scores, 1034 ) 1035 ) 1036 return tracks 1037 1038 1039def _tracking_invoker(model: Any, payload: Dict[str, Any]) -> Any: 1040 return model(payload["rgb"]) 1041 1042 1043def make_numpy_tracking_model( 1044 fn: Callable[[np.ndarray], Any], 1045 *, 1046 name: str = "numpy_tracking_model", 1047) -> BenchmarkableModel: 1048 """Wrap a per-frame tracking callable as a :class:`BenchmarkableModel`. 1049 1050 The callable takes a single ``rgb`` ``H×W×3 uint8`` frame and 1051 returns the active tracks. Accepted return shapes (see 1052 :func:`_coerce_tracking_output`): 1053 1054 - ``TrackletPrediction`` 1055 - list of :class:`Tracklet` 1056 - list of ``{"track_id": str, "boxes": (T, 4), "scores"?: (T,)}`` 1057 - ``{"tracks": [as above]}`` 1058 1059 Examples 1060 -------- 1061 >>> import numpy as np 1062 >>> import rpx_benchmark as rpx 1063 >>> def my_tracker(rgb): 1064 ... return [{ 1065 ... "track_id": "obj_0", 1066 ... "boxes": np.array([[10, 10, 50, 50]], dtype=np.float32), 1067 ... }] 1068 >>> bm = rpx.make_numpy_tracking_model(my_tracker) 1069 """ 1070 return BenchmarkableModel( 1071 task=TaskType.OBJECT_TRACKING, 1072 input_adapter=_NumpyTrackingInput(), 1073 model=fn, 1074 output_adapter=_NumpyTrackingOutput(), 1075 invoker=_tracking_invoker, 1076 name=name, 1077 )
50@dataclass 51class PreparedInput: 52 """Everything a model needs for one sample, plus context for post-processing. 53 54 ``payload`` is whatever the model's forward call accepts. If it is a 55 ``dict``, the default invoker calls ``model(**payload)``; otherwise 56 ``model(payload)``. 57 58 ``context`` is a free-form dict the output adapter receives back. Use 59 it to stash things like target image size, original intrinsics, or 60 any preprocessing metadata the postprocessing step needs. 61 """ 62 63 payload: Any 64 context: Dict[str, Any] = field(default_factory=dict)
Everything a model needs for one sample, plus context for post-processing.
payload is whatever the model's forward call accepts. If it is a
dict, the default invoker calls model(**payload); otherwise
model(payload).
context is a free-form dict the output adapter receives back. Use
it to stash things like target image size, original intrinsics, or
any preprocessing metadata the postprocessing step needs.
72@runtime_checkable 73class InputAdapter(Protocol): 74 """Sample → model-ready payload.""" 75 76 def setup(self) -> None: # pragma: no cover - optional hook 77 """Optional one-time setup (e.g., build a processor on first use).""" 78 79 def prepare(self, sample: Sample) -> PreparedInput: ...
Sample → model-ready payload.
1771def _no_init_or_replace_init(self, *args, **kwargs): 1772 cls = type(self) 1773 1774 if cls._is_protocol: 1775 raise TypeError('Protocols cannot be instantiated') 1776 1777 # Already using a custom `__init__`. No need to calculate correct 1778 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1779 if cls.__init__ is not _no_init_or_replace_init: 1780 return 1781 1782 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1783 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1784 # searches for a proper new `__init__` in the MRO. The new `__init__` 1785 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1786 # instantiation of the protocol subclass will thus use the new 1787 # `__init__` and no longer call `_no_init_or_replace_init`. 1788 for base in cls.__mro__: 1789 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1790 if init is not _no_init_or_replace_init: 1791 cls.__init__ = init 1792 break 1793 else: 1794 # should not happen 1795 cls.__init__ = object.__init__ 1796 1797 cls.__init__(self, *args, **kwargs)
76 def setup(self) -> None: # pragma: no cover - optional hook 77 """Optional one-time setup (e.g., build a processor on first use)."""
Optional one-time setup (e.g., build a processor on first use).
79 def prepare(self, sample: Sample) -> PreparedInput: ...
82@runtime_checkable 83class OutputAdapter(Protocol): 84 """Model output → RPX prediction object.""" 85 86 def setup(self) -> None: # pragma: no cover - optional hook 87 """Optional one-time setup.""" 88 89 def finalize( 90 self, 91 model_output: Any, 92 context: Dict[str, Any], 93 sample: Sample, 94 ) -> Any: ...
Model output → RPX prediction object.
1771def _no_init_or_replace_init(self, *args, **kwargs): 1772 cls = type(self) 1773 1774 if cls._is_protocol: 1775 raise TypeError('Protocols cannot be instantiated') 1776 1777 # Already using a custom `__init__`. No need to calculate correct 1778 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1779 if cls.__init__ is not _no_init_or_replace_init: 1780 return 1781 1782 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1783 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1784 # searches for a proper new `__init__` in the MRO. The new `__init__` 1785 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1786 # instantiation of the protocol subclass will thus use the new 1787 # `__init__` and no longer call `_no_init_or_replace_init`. 1788 for base in cls.__mro__: 1789 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1790 if init is not _no_init_or_replace_init: 1791 cls.__init__ = init 1792 break 1793 else: 1794 # should not happen 1795 cls.__init__ = object.__init__ 1796 1797 cls.__init__(self, *args, **kwargs)
Given (model, payload) return the raw model output.
The default implementation handles the two common cases: dict payloads
become model(**payload), anything else becomes model(payload).
Override when the model needs non-standard invocation (e.g., a method
other than __call__).
107def default_invoker(model: Any, payload: Any) -> Any: 108 """Call ``model`` with ``payload``. Uses torch no_grad if available.""" 109 try: 110 import torch # noqa: F401 111 except ImportError: 112 return _dispatch(model, payload) 113 114 import torch 115 116 with torch.no_grad(): 117 return _dispatch(model, payload)
Call model with payload. Uses torch no_grad if available.
131@dataclass 132class BenchmarkableModel(BenchmarkModel): 133 """Compose an input adapter, a model, and an output adapter. 134 135 This is the canonical way to plug a model into the RPX benchmark 136 harness. The ``BenchmarkRunner`` only ever sees the 137 :class:`BenchmarkModel` contract (``setup``, ``predict``); all the 138 model-family-specific logic lives in the adapters so the harness 139 stays task-agnostic. 140 141 Example — wrap a plain numpy depth callable:: 142 143 import numpy as np 144 import rpx_benchmark as rpx 145 146 def my_depth(rgb: np.ndarray) -> np.ndarray: 147 return ... # H x W float32, metres 148 149 bm = rpx.make_numpy_depth_model(my_depth, name="my_model") 150 # Hand `bm` to a task runner or the BenchmarkRunner directly. 151 152 Example — wrap a plain numpy callable:: 153 154 def my_depth(rgb: np.ndarray) -> np.ndarray: 155 return some_depth_in_metres 156 157 bm = make_numpy_depth_model(my_depth) 158 """ 159 160 task: TaskType 161 input_adapter: InputAdapter 162 model: Any 163 output_adapter: OutputAdapter 164 invoker: ModelInvoker = field(default=default_invoker) 165 name: str = "benchmarkable_model" 166 setup_hook: Optional[Callable[[], None]] = None 167 168 def setup(self) -> None: 169 if hasattr(self.input_adapter, "setup"): 170 self.input_adapter.setup() 171 if hasattr(self.output_adapter, "setup"): 172 self.output_adapter.setup() 173 if self.setup_hook is not None: 174 self.setup_hook() 175 176 def predict(self, batch: Sequence[Sample]) -> Sequence[Any]: 177 preds = [] 178 for sample in batch: 179 prepared = self.input_adapter.prepare(sample) 180 model_out = self.invoker(self.model, prepared.payload) 181 pred = self.output_adapter.finalize(model_out, prepared.context, sample) 182 preds.append(pred) 183 return preds
Compose an input adapter, a model, and an output adapter.
This is the canonical way to plug a model into the RPX benchmark
harness. The BenchmarkRunner only ever sees the
BenchmarkModel contract (setup, predict); all the
model-family-specific logic lives in the adapters so the harness
stays task-agnostic.
Example — wrap a plain numpy depth callable::
import numpy as np
import rpx_benchmark as rpx
def my_depth(rgb: np.ndarray) -> np.ndarray:
return ... # H x W float32, metres
bm = rpx.make_numpy_depth_model(my_depth, name="my_model")
# Hand `bm` to a task runner or the BenchmarkRunner directly.
Example — wrap a plain numpy callable::
def my_depth(rgb: np.ndarray) -> np.ndarray:
return some_depth_in_metres
bm = make_numpy_depth_model(my_depth)
107def default_invoker(model: Any, payload: Any) -> Any: 108 """Call ``model`` with ``payload``. Uses torch no_grad if available.""" 109 try: 110 import torch # noqa: F401 111 except ImportError: 112 return _dispatch(model, payload) 113 114 import torch 115 116 with torch.no_grad(): 117 return _dispatch(model, payload)
Call model with payload. Uses torch no_grad if available.
168 def setup(self) -> None: 169 if hasattr(self.input_adapter, "setup"): 170 self.input_adapter.setup() 171 if hasattr(self.output_adapter, "setup"): 172 self.output_adapter.setup() 173 if self.setup_hook is not None: 174 self.setup_hook()
Load checkpoints, warm CUDA, and do any other one-time init.
The runner calls this exactly once before iterating the
dataset, unless BenchmarkRunner(call_setup=False) was
passed — in which case the caller is responsible.
176 def predict(self, batch: Sequence[Sample]) -> Sequence[Any]: 177 preds = [] 178 for sample in batch: 179 prepared = self.input_adapter.prepare(sample) 180 model_out = self.invoker(self.model, prepared.payload) 181 pred = self.output_adapter.finalize(model_out, prepared.context, sample) 182 preds.append(pred) 183 return preds
Run inference on a batch of samples.
Parameters
batch : sequence of Sample
One or more samples. Length equals dataset.batch_size
except possibly for the final tail batch.
Returns
sequence
One task-specific Prediction dataclass per input sample,
in the same order. The prediction dataclass must match
what MetricSuite expects for this task.
Raises
ModelError (By convention) when a sample cannot be processed. The runner surfaces it as a clean error rather than a stack trace.
Inherited Members
298def make_numpy_video_depth_model( 299 fn: Callable[[np.ndarray], np.ndarray], 300 *, 301 name: str = "numpy_video_depth_model", 302 depth_output_kind: str = "metric", 303) -> BenchmarkableModel: 304 """Wrap a plain numpy video-depth callable as a 305 :class:`BenchmarkableModel`. 306 307 The callable must accept a ``(T, H, W, 3) uint8`` RGB clip and 308 return a ``(T, H', W') float`` per-frame depth sequence (metres 309 if metric; up-to-scale if relative — the runner applies per-clip 310 ``(s, t)`` alignment for relative models). If ``(H', W') != 311 (H, W)``, the output is bilinearly resized per frame to match. 312 ``T`` must match the input clip length. 313 314 Parameters 315 ---------- 316 fn : callable 317 Signature: ``fn(rgb_seq_uint8) -> depth_seq_float``. 318 name : str 319 Display name used in logs and reports. 320 depth_output_kind : str 321 ``"metric"`` (default) — model emits metres; runner skips 322 alignment. ``"relative"`` — runner applies per-clip 323 ``(s, t)`` scale-and-shift alignment before metrics. 324 325 Examples 326 -------- 327 >>> import numpy as np 328 >>> import rpx_benchmark as rpx 329 >>> def my_clip_depth(rgb_seq): 330 ... T, H, W, _ = rgb_seq.shape 331 ... return np.full((T, H, W), 2.0, dtype=np.float32) 332 >>> bm = rpx.make_numpy_video_depth_model(my_clip_depth, name="mine") 333 >>> bm.task is rpx.TaskType.VIDEO_DEPTH 334 True 335 """ 336 model = BenchmarkableModel( 337 task=TaskType.VIDEO_DEPTH, 338 input_adapter=_NumpyVideoDepthInput(), 339 model=fn, 340 output_adapter=_NumpyVideoDepthOutput(), 341 invoker=lambda model, payload: model(payload), 342 name=name, 343 ) 344 # Propagate depth_output_kind to the runner's alignment dispatch. 345 model.depth_output_kind = depth_output_kind 346 return model
Wrap a plain numpy video-depth callable as a
BenchmarkableModel.
The callable must accept a (T, H, W, 3) uint8 RGB clip and
return a (T, H', W') float per-frame depth sequence (metres
if metric; up-to-scale if relative — the runner applies per-clip
(s, t) alignment for relative models). If (H', W') !=
(H, W), the output is bilinearly resized per frame to match.
T must match the input clip length.
Parameters
fn : callable
Signature: fn(rgb_seq_uint8) -> depth_seq_float.
name : str
Display name used in logs and reports.
depth_output_kind : str
"metric" (default) — model emits metres; runner skips
alignment. "relative" — runner applies per-clip
(s, t) scale-and-shift alignment before metrics.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_clip_depth(rgb_seq):
... T, H, W, _ = rgb_seq.shape
... return np.full((T, H, W), 2.0, dtype=np.float32)
>>> bm = rpx.make_numpy_video_depth_model(my_clip_depth, name="mine")
>>> bm.task is rpx.TaskType.VIDEO_DEPTH
True
349def make_numpy_depth_model( 350 fn: Callable[[np.ndarray], np.ndarray], 351 *, 352 name: str = "numpy_depth_model", 353 depth_output_kind: str = "metric", 354 native_alignment: str | None = None, 355) -> BenchmarkableModel: 356 """Wrap a plain numpy depth callable as a :class:`BenchmarkableModel`. 357 358 The callable must accept a ``(H, W, 3) uint8`` RGB image and return a 359 ``(H', W') float`` depth map. If ``depth_output_kind="metric"`` the 360 values are metres. Relative outputs remain raw until the runner fits 361 one pooled alignment per ``(scene, phase)`` cell. If ``(H', W') != 362 (H, W)``, the output is bilinearly resized to match the ground truth. 363 364 Parameters 365 ---------- 366 fn : callable 367 The depth function. Signature: ``fn(rgb_uint8) -> depth_float``. 368 name : str 369 Display name used in logs and reports. 370 depth_output_kind : str 371 ``"metric"`` (default) or ``"relative"``. 372 native_alignment : str, optional 373 Alignment used for relative output. Defaults to ``"ls_affine"`` 374 for relative models and ``"none"`` for metric models. 375 376 Examples 377 -------- 378 >>> import numpy as np 379 >>> import rpx_benchmark as rpx 380 >>> def my_depth(rgb): 381 ... return np.full(rgb.shape[:2], 2.0, dtype=np.float32) 382 >>> bm = rpx.make_numpy_depth_model(my_depth, name="mine") 383 >>> bm.task is rpx.TaskType.MONOCULAR_DEPTH 384 True 385 """ 386 if depth_output_kind not in {"metric", "relative"}: 387 raise AdapterError( 388 f"depth_output_kind must be 'metric' or 'relative', got {depth_output_kind!r}", 389 ) 390 model = BenchmarkableModel( 391 task=TaskType.MONOCULAR_DEPTH, 392 input_adapter=_NumpyDepthInput(), 393 model=fn, 394 output_adapter=_NumpyDepthOutput(), 395 invoker=lambda model, payload: model(payload), 396 name=name, 397 ) 398 model.depth_output_kind = depth_output_kind 399 model.native_alignment = native_alignment or ( 400 "ls_affine" if depth_output_kind == "relative" else "none" 401 ) 402 return model
Wrap a plain numpy depth callable as a BenchmarkableModel.
The callable must accept a (H, W, 3) uint8 RGB image and return a
(H', W') float depth map. If depth_output_kind="metric" the
values are metres. Relative outputs remain raw until the runner fits
one pooled alignment per (scene, phase) cell. If (H', W') !=
(H, W), the output is bilinearly resized to match the ground truth.
Parameters
fn : callable
The depth function. Signature: fn(rgb_uint8) -> depth_float.
name : str
Display name used in logs and reports.
depth_output_kind : str
"metric" (default) or "relative".
native_alignment : str, optional
Alignment used for relative output. Defaults to "ls_affine"
for relative models and "none" for metric models.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_depth(rgb):
... return np.full(rgb.shape[:2], 2.0, dtype=np.float32)
>>> bm = rpx.make_numpy_depth_model(my_depth, name="mine")
>>> bm.task is rpx.TaskType.MONOCULAR_DEPTH
True
450def make_numpy_mask_model( 451 fn: Callable[[np.ndarray], np.ndarray], 452 *, 453 name: str = "numpy_mask_model", 454) -> BenchmarkableModel: 455 """Wrap a plain numpy instance-mask callable as a :class:`BenchmarkableModel`. 456 457 The callable must accept a ``(H, W, 3) uint8`` RGB image and return 458 a ``(H', W') int`` instance mask where pixel values are instance 459 IDs (``0`` is conventionally background). If ``(H', W') != (H, W)`` 460 the output is nearest-neighbour resized to match the GT mask so 461 integer IDs are preserved. 462 463 Parameters 464 ---------- 465 fn : callable 466 ``fn(rgb_uint8) -> mask_int``. 467 name : str 468 469 Examples 470 -------- 471 >>> import numpy as np 472 >>> import rpx_benchmark as rpx 473 >>> def my_seg(rgb): 474 ... mask = np.zeros(rgb.shape[:2], dtype=np.int32) 475 ... mask[rgb.sum(-1) > 384] = 1 # trivial brightness threshold 476 ... return mask 477 >>> bm = rpx.make_numpy_mask_model(my_seg, name="mine") 478 >>> bm.task is rpx.TaskType.OBJECT_SEGMENTATION 479 True 480 """ 481 return BenchmarkableModel( 482 task=TaskType.OBJECT_SEGMENTATION, 483 input_adapter=_NumpyMaskInput(), 484 model=fn, 485 output_adapter=_NumpyMaskOutput(), 486 invoker=lambda model, payload: model(payload), 487 name=name, 488 )
Wrap a plain numpy instance-mask callable as a BenchmarkableModel.
The callable must accept a (H, W, 3) uint8 RGB image and return
a (H', W') int instance mask where pixel values are instance
IDs (0 is conventionally background). If (H', W') != (H, W)
the output is nearest-neighbour resized to match the GT mask so
integer IDs are preserved.
Parameters
fn : callable
fn(rgb_uint8) -> mask_int.
name : str
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_seg(rgb):
... mask = np.zeros(rgb.shape[:2], dtype=np.int32)
... mask[rgb.sum(-1) > 384] = 1 # trivial brightness threshold
... return mask
>>> bm = rpx.make_numpy_mask_model(my_seg, name="mine")
>>> bm.task is rpx.TaskType.OBJECT_SEGMENTATION
True
574def make_numpy_detection_model( 575 fn: Callable[[np.ndarray], Any], 576 *, 577 name: str = "numpy_detection_model", 578 task: TaskType = TaskType.OBJECT_DETECTION, 579) -> BenchmarkableModel: 580 """Wrap a plain numpy detection callable as a :class:`BenchmarkableModel`. 581 582 The callable must accept a ``(H, W, 3) uint8`` RGB image and 583 return either a dict with keys ``"boxes"`` / ``"scores"`` / 584 ``"labels"`` or a ``(boxes, scores, labels)`` tuple in that 585 order. Boxes are pixel coordinates in ``(x1, y1, x2, y2)`` 586 format, scores are floats in ``[0, 1]``, labels are strings. 587 588 Parameters 589 ---------- 590 fn : callable 591 ``fn(rgb_uint8) -> dict | tuple``. 592 name : str 593 Display name for reports. 594 task : TaskType 595 Use :attr:`TaskType.OBJECT_DETECTION` for closed-vocabulary 596 detection or :attr:`TaskType.OPEN_VOCAB_DETECTION` for 597 open-vocab (the Prediction contract is the same). 598 599 Examples 600 -------- 601 >>> import numpy as np 602 >>> import rpx_benchmark as rpx 603 >>> def my_det(rgb): 604 ... return { 605 ... "boxes": np.array([[10, 10, 30, 30]], dtype=np.float32), 606 ... "scores": np.array([0.9], dtype=np.float32), 607 ... "labels": ["cup"], 608 ... } 609 >>> bm = rpx.make_numpy_detection_model(my_det) 610 >>> bm.task is rpx.TaskType.OBJECT_DETECTION 611 True 612 """ 613 return BenchmarkableModel( 614 task=task, 615 input_adapter=_NumpyRgbInput(), 616 model=fn, 617 output_adapter=_NumpyDetectionOutput(), 618 invoker=_passthrough_invoker, 619 name=name, 620 )
Wrap a plain numpy detection callable as a BenchmarkableModel.
The callable must accept a (H, W, 3) uint8 RGB image and
return either a dict with keys "boxes" / "scores" /
"labels" or a (boxes, scores, labels) tuple in that
order. Boxes are pixel coordinates in (x1, y1, x2, y2)
format, scores are floats in [0, 1], labels are strings.
Parameters
fn : callable
fn(rgb_uint8) -> dict | tuple.
name : str
Display name for reports.
task : TaskType
Use TaskType.OBJECT_DETECTION for closed-vocabulary
detection or TaskType.OPEN_VOCAB_DETECTION for
open-vocab (the Prediction contract is the same).
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_det(rgb):
... return {
... "boxes": np.array([[10, 10, 30, 30]], dtype=np.float32),
... "scores": np.array([0.9], dtype=np.float32),
... "labels": ["cup"],
... }
>>> bm = rpx.make_numpy_detection_model(my_det)
>>> bm.task is rpx.TaskType.OBJECT_DETECTION
True
667def make_numpy_grounding_model( 668 fn: Callable[[np.ndarray, str], Any], 669 *, 670 name: str = "numpy_grounding_model", 671) -> BenchmarkableModel: 672 """Wrap a visual-grounding callable as a :class:`BenchmarkableModel`. 673 674 The callable takes ``(rgb_uint8, text)`` and returns either a 675 dict with keys ``"boxes"`` / ``"scores"`` or a tuple 676 ``(boxes, scores)``. Boxes are ``(x1, y1, x2, y2)`` pixel 677 coordinates; scores are floats. The referring expression 678 ``text`` is plucked from ``sample.ground_truth.text`` by the 679 adapter so the callable never sees the GT boxes. 680 681 Examples 682 -------- 683 >>> import numpy as np 684 >>> import rpx_benchmark as rpx 685 >>> def my_ground(rgb, text): 686 ... return ( 687 ... np.array([[10, 10, 30, 30]], dtype=np.float32), 688 ... np.array([0.8], dtype=np.float32), 689 ... ) 690 >>> bm = rpx.make_numpy_grounding_model(my_ground) 691 """ 692 return BenchmarkableModel( 693 task=TaskType.VISUAL_GROUNDING, 694 input_adapter=_NumpyGroundingInput(), 695 model=fn, 696 output_adapter=_NumpyGroundingOutput(), 697 invoker=_grounding_invoker, 698 name=name, 699 )
Wrap a visual-grounding callable as a BenchmarkableModel.
The callable takes (rgb_uint8, text) and returns either a
dict with keys "boxes" / "scores" or a tuple
(boxes, scores). Boxes are (x1, y1, x2, y2) pixel
coordinates; scores are floats. The referring expression
text is plucked from sample.ground_truth.text by the
adapter so the callable never sees the GT boxes.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_ground(rgb, text):
... return (
... np.array([[10, 10, 30, 30]], dtype=np.float32),
... np.array([0.8], dtype=np.float32),
... )
>>> bm = rpx.make_numpy_grounding_model(my_ground)
750def make_numpy_pose_model( 751 fn: Callable[[np.ndarray, np.ndarray], Any], 752 *, 753 name: str = "numpy_pose_model", 754) -> BenchmarkableModel: 755 """Wrap a relative-camera-pose callable as a :class:`BenchmarkableModel`. 756 757 The callable takes ``(rgb_a, rgb_b)`` and returns either a dict 758 with keys ``"rotation"`` (3×3 rotation matrix or 4-element 759 quaternion) and ``"translation"`` (3-vector, metres) or a 760 ``(rotation, translation)`` tuple. 761 762 Examples 763 -------- 764 >>> import numpy as np 765 >>> import rpx_benchmark as rpx 766 >>> def my_pose(rgb_a, rgb_b): 767 ... return {"rotation": np.eye(3), "translation": np.zeros(3)} 768 >>> bm = rpx.make_numpy_pose_model(my_pose) 769 """ 770 return BenchmarkableModel( 771 task=TaskType.RELATIVE_CAMERA_POSE, 772 input_adapter=_NumpyPoseInput(), 773 model=fn, 774 output_adapter=_NumpyPoseOutput(), 775 invoker=_pose_invoker, 776 name=name, 777 )
Wrap a relative-camera-pose callable as a BenchmarkableModel.
The callable takes (rgb_a, rgb_b) and returns either a dict
with keys "rotation" (3×3 rotation matrix or 4-element
quaternion) and "translation" (3-vector, metres) or a
(rotation, translation) tuple.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_pose(rgb_a, rgb_b):
... return {"rotation": np.eye(3), "translation": np.zeros(3)}
>>> bm = rpx.make_numpy_pose_model(my_pose)
825def make_numpy_sparse_depth_model( 826 fn: Callable[[np.ndarray, np.ndarray], np.ndarray], 827 *, 828 name: str = "numpy_sparse_depth_model", 829) -> BenchmarkableModel: 830 """Wrap a sparse-depth callable as a :class:`BenchmarkableModel`. 831 832 The callable takes ``(rgb_uint8, coords)`` where ``coords`` is a 833 ``(N, 2)`` float32 array of pixel coordinates and returns an 834 ``(N,)`` float32 array of depths in metres at those exact 835 coordinates. 836 837 Examples 838 -------- 839 >>> import numpy as np 840 >>> import rpx_benchmark as rpx 841 >>> def my_sd(rgb, coords): 842 ... return np.full(len(coords), 2.0, dtype=np.float32) 843 >>> bm = rpx.make_numpy_sparse_depth_model(my_sd) 844 """ 845 return BenchmarkableModel( 846 task=TaskType.SPARSE_DEPTH, 847 input_adapter=_NumpySparseDepthInput(), 848 model=fn, 849 output_adapter=_NumpySparseDepthOutput(), 850 invoker=_sparse_depth_invoker, 851 name=name, 852 )
Wrap a sparse-depth callable as a BenchmarkableModel.
The callable takes (rgb_uint8, coords) where coords is a
(N, 2) float32 array of pixel coordinates and returns an
(N,) float32 array of depths in metres at those exact
coordinates.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_sd(rgb, coords):
... return np.full(len(coords), 2.0, dtype=np.float32)
>>> bm = rpx.make_numpy_sparse_depth_model(my_sd)
926def make_numpy_keypoint_model( 927 fn: Callable[[np.ndarray, np.ndarray], Any], 928 *, 929 name: str = "numpy_keypoint_model", 930) -> BenchmarkableModel: 931 """Wrap a keypoint-matching callable as a :class:`BenchmarkableModel`. 932 933 The callable takes ``(rgb_a, rgb_b)`` and returns either a dict 934 with keys ``"points0"``, ``"points1"`` and optional ``"scores"`` 935 or a 2/3-tuple in the same order. Points are ``(N, 2)`` pixel 936 coordinates. 937 938 Examples 939 -------- 940 >>> import numpy as np 941 >>> import rpx_benchmark as rpx 942 >>> def my_matcher(rgb_a, rgb_b): 943 ... pts = np.array([[10, 10], [20, 20]], dtype=np.float32) 944 ... return pts, pts 945 >>> bm = rpx.make_numpy_keypoint_model(my_matcher) 946 """ 947 return BenchmarkableModel( 948 task=TaskType.KEYPOINT_MATCHING, 949 input_adapter=_NumpyKeypointInput(), 950 model=fn, 951 output_adapter=_NumpyKeypointOutput(), 952 invoker=_keypoint_invoker, 953 name=name, 954 )
Wrap a keypoint-matching callable as a BenchmarkableModel.
The callable takes (rgb_a, rgb_b) and returns either a dict
with keys "points0", "points1" and optional "scores"
or a 2/3-tuple in the same order. Points are (N, 2) pixel
coordinates.
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_matcher(rgb_a, rgb_b):
... pts = np.array([[10, 10], [20, 20]], dtype=np.float32)
... return pts, pts
>>> bm = rpx.make_numpy_keypoint_model(my_matcher)
1044def make_numpy_tracking_model( 1045 fn: Callable[[np.ndarray], Any], 1046 *, 1047 name: str = "numpy_tracking_model", 1048) -> BenchmarkableModel: 1049 """Wrap a per-frame tracking callable as a :class:`BenchmarkableModel`. 1050 1051 The callable takes a single ``rgb`` ``H×W×3 uint8`` frame and 1052 returns the active tracks. Accepted return shapes (see 1053 :func:`_coerce_tracking_output`): 1054 1055 - ``TrackletPrediction`` 1056 - list of :class:`Tracklet` 1057 - list of ``{"track_id": str, "boxes": (T, 4), "scores"?: (T,)}`` 1058 - ``{"tracks": [as above]}`` 1059 1060 Examples 1061 -------- 1062 >>> import numpy as np 1063 >>> import rpx_benchmark as rpx 1064 >>> def my_tracker(rgb): 1065 ... return [{ 1066 ... "track_id": "obj_0", 1067 ... "boxes": np.array([[10, 10, 50, 50]], dtype=np.float32), 1068 ... }] 1069 >>> bm = rpx.make_numpy_tracking_model(my_tracker) 1070 """ 1071 return BenchmarkableModel( 1072 task=TaskType.OBJECT_TRACKING, 1073 input_adapter=_NumpyTrackingInput(), 1074 model=fn, 1075 output_adapter=_NumpyTrackingOutput(), 1076 invoker=_tracking_invoker, 1077 name=name, 1078 )
Wrap a per-frame tracking callable as a BenchmarkableModel.
The callable takes a single rgb H×W×3 uint8 frame and
returns the active tracks. Accepted return shapes (see
_coerce_tracking_output()):
TrackletPrediction- list of
Tracklet - list of
{"track_id": str, "boxes": (T, 4), "scores"?: (T,)} {"tracks": [as above]}
Examples
>>> import numpy as np
>>> import rpx_benchmark as rpx
>>> def my_tracker(rgb):
... return [{
... "track_id": "obj_0",
... "boxes": np.array([[10, 10, 50, 50]], dtype=np.float32),
... }]
>>> bm = rpx.make_numpy_tracking_model(my_tracker)