rpx_benchmark.metrics.depth_paper
Canonical RPX D1-F paper metrics and fixed camera calibration.
The RPX paper declares one RGB-camera calibration for the released 640x480 benchmark. The release does not include per-device calibration files, so this module deliberately fails on any other resolution rather than inventing a rescaling rule.
1"""Canonical RPX D1-F paper metrics and fixed camera calibration. 2 3The RPX paper declares one RGB-camera calibration for the released 640x480 4benchmark. The release does not include per-device calibration files, so this 5module deliberately fails on any other resolution rather than inventing a 6rescaling rule. 7""" 8 9from __future__ import annotations 10 11from dataclasses import dataclass 12 13import numpy as np 14 15from ..api import DepthGroundTruth, DepthPrediction, TaskType 16from ..exceptions import MetricError 17from .registry import MetricSuite 18 19DEPTH_MIN_M = 0.3 20DEPTH_MAX_M = 5.0 21FSCORE_THRESHOLD_M = 0.05 22EXPECTED_HW = (480, 640) 23 24 25@dataclass(frozen=True) 26class D1Calibration: 27 fx: float = 615.0 28 fy: float = 615.0 29 cx: float = 320.0 30 cy: float = 240.0 31 width: int = 640 32 height: int = 480 33 version: str = "rpx-paper-d1-rgb-640x480-v1" 34 provenance: str = "RPX-4.pdf Appendix I.1; fixed paper-declared RGB intrinsics" 35 36 def to_dict(self) -> dict[str, object]: 37 return { 38 "fx": self.fx, 39 "fy": self.fy, 40 "cx": self.cx, 41 "cy": self.cy, 42 "width": self.width, 43 "height": self.height, 44 "version": self.version, 45 "provenance": self.provenance, 46 "intrinsics_policy": "fixed paper-declared calibration; never rescaled", 47 "coordinate_system": "RGB camera", 48 "pixel_origin": "top-left pixel centre", 49 "released_depth_alignment": "depth aligned to RGB color stream", 50 "per_device_calibration": "not present in released HF manifests", 51 "extrinsics_used": False, 52 } 53 54 55D1_CALIBRATION = D1Calibration() 56PAPER_METRIC_KEYS = ( 57 "absrel", 58 "rmse", 59 "silog", 60 "delta1", 61 "irmse", 62 "fscore_5cm", 63) 64FAST_PAPER_METRIC_KEYS = PAPER_METRIC_KEYS[:-1] 65PREDICTION_EVALUATION_POLICY = { 66 "version": "rpx-d1f-prediction-clip-v1", 67 "operation": "clip", 68 "minimum_m": DEPTH_MIN_M, 69 "maximum_m": DEPTH_MAX_M, 70 "applies_to": "evaluation copy before every D1-F metric; raw NPZ prediction is preserved", 71 "reason": "enforce the declared finite positive D1-F evaluation domain uniformly", 72 "trigger_observation": { 73 "sample_id": "scene026__0__00155", 74 "coordinates_yx": [[5, 323], [6, 323]], 75 "raw_prediction_m": [-0.044158936, -0.061798096], 76 "ground_truth_m": [1.134, 1.138], 77 "paper_valid": [True, True], 78 }, 79} 80 81 82def valid_depth_mask(gt_m: np.ndarray) -> np.ndarray: 83 """Paper validity mask: finite GT strictly inside the D435 range.""" 84 return np.isfinite(gt_m) & (gt_m > DEPTH_MIN_M) & (gt_m < DEPTH_MAX_M) 85 86 87def clip_prediction_for_evaluation(prediction: np.ndarray) -> np.ndarray: 88 """Return the uniformly range-clipped D1-F evaluation copy.""" 89 pred = np.asarray(prediction, dtype=np.float32) 90 if not np.isfinite(pred).all(): 91 raise MetricError("Depth prediction contains non-finite values.") 92 return np.clip(pred, DEPTH_MIN_M, DEPTH_MAX_M) 93 94 95def _point_cloud(depth: np.ndarray, valid: np.ndarray) -> np.ndarray: 96 y, x = np.nonzero(valid) 97 z = depth[valid].astype(np.float64, copy=False) 98 return np.column_stack( 99 ( 100 (x.astype(np.float64) - D1_CALIBRATION.cx) * z / D1_CALIBRATION.fx, 101 (y.astype(np.float64) - D1_CALIBRATION.cy) * z / D1_CALIBRATION.fy, 102 z, 103 ) 104 ) 105 106 107def _matched_fraction(tree, points: np.ndarray, *, chunk_size: int = 65_536) -> float: 108 matched = 0 109 for start in range(0, len(points), chunk_size): 110 distances, _ = tree.query( 111 points[start : start + chunk_size], 112 k=1, 113 workers=1, 114 ) 115 matched += int(np.count_nonzero(distances < FSCORE_THRESHOLD_M)) 116 return float(matched / len(points)) 117 118 119def point_cloud_fscore_5cm( 120 pred_m: np.ndarray, 121 gt_m: np.ndarray, 122 valid: np.ndarray, 123) -> float: 124 """Bidirectional nearest-neighbour F-score at a strict 5 cm threshold.""" 125 try: 126 from scipy.spatial import cKDTree 127 except ImportError as exc: # pragma: no cover - Docker and test env include scipy 128 raise MetricError( 129 "F-Score@5cm requires scipy.spatial.cKDTree.", 130 hint="Install scipy>=1.10 or use the RPX depth-paper Docker image.", 131 ) from exc 132 133 gt_points = _point_cloud(gt_m, valid) 134 pred_points = _point_cloud(pred_m, valid) 135 if len(gt_points) == 0 or len(pred_points) == 0: 136 raise MetricError("F-Score@5cm received an empty valid point cloud.") 137 138 precision = _matched_fraction(cKDTree(gt_points), pred_points) 139 recall = _matched_fraction(cKDTree(pred_points), gt_points) 140 denom = precision + recall 141 return 0.0 if denom == 0.0 else float(2.0 * precision * recall / denom) 142 143 144def compute_d1_paper_metrics( 145 prediction: np.ndarray, 146 ground_truth: np.ndarray, 147 *, 148 include_fscore: bool = True, 149) -> dict[str, float]: 150 """Compute the D1-F headline metrics for one 640x480 frame. 151 152 ``include_fscore=False`` retains the exact paper mask and formulas for 153 the five image-space metrics while deferring the expensive point-cloud 154 calculation. This is used by the staged production workflow; the saved 155 predictions can be evaluated for F-Score later without another model 156 forward. 157 """ 158 pred_raw = np.asarray(prediction, dtype=np.float32) 159 gt = np.asarray(ground_truth, dtype=np.float32) 160 if pred_raw.shape != gt.shape: 161 raise MetricError( 162 f"Depth prediction shape {pred_raw.shape} does not match ground-truth shape {gt.shape}." 163 ) 164 if gt.shape != EXPECTED_HW: 165 raise MetricError( 166 f"RPX D1-F paper calibration requires depth shape {EXPECTED_HW}; got {gt.shape}.", 167 hint="Do not rescale the fixed paper intrinsics; use the released 640x480 frames.", 168 ) 169 170 pred = clip_prediction_for_evaluation(pred_raw) 171 valid = valid_depth_mask(gt) 172 if not valid.any(): 173 raise MetricError("Depth frame contains no finite GT pixels in the 0.3-5.0 m range.") 174 pred_valid = pred[valid] 175 gt_valid = gt[valid] 176 diff = pred_valid - gt_valid 177 log_error = np.log(pred_valid) - np.log(gt_valid) 178 ratio = np.maximum(pred_valid / gt_valid, gt_valid / pred_valid) 179 silog_variance = max( 180 float(np.mean(log_error**2) - np.mean(log_error) ** 2), 181 0.0, 182 ) 183 metrics = { 184 "absrel": float(np.mean(np.abs(diff) / gt_valid)), 185 "rmse": float(np.sqrt(np.mean(diff**2))), 186 "silog": float(100.0 * np.sqrt(silog_variance)), 187 "delta1": float(np.mean(ratio < 1.25)), 188 "irmse": float(np.sqrt(np.mean((1.0 / pred_valid - 1.0 / gt_valid) ** 2))), 189 } 190 if include_fscore: 191 metrics["fscore_5cm"] = point_cloud_fscore_5cm(pred, gt, valid) 192 if not all(np.isfinite(value) for value in metrics.values()): 193 raise MetricError("D1-F paper metric computation produced a non-finite value.") 194 return metrics 195 196 197class PaperDepthMetricSuite(MetricSuite): 198 """Metric suite dedicated to the strict RPX D1-F paper protocol.""" 199 200 def __init__(self) -> None: 201 super().__init__(TaskType.MONOCULAR_DEPTH) 202 203 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 204 if not isinstance(prediction, DepthPrediction): 205 raise MetricError( 206 f"PaperDepthMetricSuite expected DepthPrediction, got {type(prediction).__name__}." 207 ) 208 if not isinstance(ground_truth, DepthGroundTruth): 209 raise MetricError( 210 "PaperDepthMetricSuite expected DepthGroundTruth, " 211 f"got {type(ground_truth).__name__}." 212 ) 213 return compute_d1_paper_metrics(prediction.depth_map, ground_truth.depth_map) 214 215 216class FastPaperDepthMetricSuite(PaperDepthMetricSuite): 217 """Strict D1-F suite with only F-Score deferred to post-processing.""" 218 219 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 220 if not isinstance(prediction, DepthPrediction): 221 raise MetricError( 222 f"FastPaperDepthMetricSuite expected DepthPrediction, got " 223 f"{type(prediction).__name__}." 224 ) 225 if not isinstance(ground_truth, DepthGroundTruth): 226 raise MetricError( 227 "FastPaperDepthMetricSuite expected DepthGroundTruth, " 228 f"got {type(ground_truth).__name__}." 229 ) 230 return compute_d1_paper_metrics( 231 prediction.depth_map, 232 ground_truth.depth_map, 233 include_fscore=False, 234 )
26@dataclass(frozen=True) 27class D1Calibration: 28 fx: float = 615.0 29 fy: float = 615.0 30 cx: float = 320.0 31 cy: float = 240.0 32 width: int = 640 33 height: int = 480 34 version: str = "rpx-paper-d1-rgb-640x480-v1" 35 provenance: str = "RPX-4.pdf Appendix I.1; fixed paper-declared RGB intrinsics" 36 37 def to_dict(self) -> dict[str, object]: 38 return { 39 "fx": self.fx, 40 "fy": self.fy, 41 "cx": self.cx, 42 "cy": self.cy, 43 "width": self.width, 44 "height": self.height, 45 "version": self.version, 46 "provenance": self.provenance, 47 "intrinsics_policy": "fixed paper-declared calibration; never rescaled", 48 "coordinate_system": "RGB camera", 49 "pixel_origin": "top-left pixel centre", 50 "released_depth_alignment": "depth aligned to RGB color stream", 51 "per_device_calibration": "not present in released HF manifests", 52 "extrinsics_used": False, 53 }
37 def to_dict(self) -> dict[str, object]: 38 return { 39 "fx": self.fx, 40 "fy": self.fy, 41 "cx": self.cx, 42 "cy": self.cy, 43 "width": self.width, 44 "height": self.height, 45 "version": self.version, 46 "provenance": self.provenance, 47 "intrinsics_policy": "fixed paper-declared calibration; never rescaled", 48 "coordinate_system": "RGB camera", 49 "pixel_origin": "top-left pixel centre", 50 "released_depth_alignment": "depth aligned to RGB color stream", 51 "per_device_calibration": "not present in released HF manifests", 52 "extrinsics_used": False, 53 }
83def valid_depth_mask(gt_m: np.ndarray) -> np.ndarray: 84 """Paper validity mask: finite GT strictly inside the D435 range.""" 85 return np.isfinite(gt_m) & (gt_m > DEPTH_MIN_M) & (gt_m < DEPTH_MAX_M)
Paper validity mask: finite GT strictly inside the D435 range.
88def clip_prediction_for_evaluation(prediction: np.ndarray) -> np.ndarray: 89 """Return the uniformly range-clipped D1-F evaluation copy.""" 90 pred = np.asarray(prediction, dtype=np.float32) 91 if not np.isfinite(pred).all(): 92 raise MetricError("Depth prediction contains non-finite values.") 93 return np.clip(pred, DEPTH_MIN_M, DEPTH_MAX_M)
Return the uniformly range-clipped D1-F evaluation copy.
120def point_cloud_fscore_5cm( 121 pred_m: np.ndarray, 122 gt_m: np.ndarray, 123 valid: np.ndarray, 124) -> float: 125 """Bidirectional nearest-neighbour F-score at a strict 5 cm threshold.""" 126 try: 127 from scipy.spatial import cKDTree 128 except ImportError as exc: # pragma: no cover - Docker and test env include scipy 129 raise MetricError( 130 "F-Score@5cm requires scipy.spatial.cKDTree.", 131 hint="Install scipy>=1.10 or use the RPX depth-paper Docker image.", 132 ) from exc 133 134 gt_points = _point_cloud(gt_m, valid) 135 pred_points = _point_cloud(pred_m, valid) 136 if len(gt_points) == 0 or len(pred_points) == 0: 137 raise MetricError("F-Score@5cm received an empty valid point cloud.") 138 139 precision = _matched_fraction(cKDTree(gt_points), pred_points) 140 recall = _matched_fraction(cKDTree(pred_points), gt_points) 141 denom = precision + recall 142 return 0.0 if denom == 0.0 else float(2.0 * precision * recall / denom)
Bidirectional nearest-neighbour F-score at a strict 5 cm threshold.
145def compute_d1_paper_metrics( 146 prediction: np.ndarray, 147 ground_truth: np.ndarray, 148 *, 149 include_fscore: bool = True, 150) -> dict[str, float]: 151 """Compute the D1-F headline metrics for one 640x480 frame. 152 153 ``include_fscore=False`` retains the exact paper mask and formulas for 154 the five image-space metrics while deferring the expensive point-cloud 155 calculation. This is used by the staged production workflow; the saved 156 predictions can be evaluated for F-Score later without another model 157 forward. 158 """ 159 pred_raw = np.asarray(prediction, dtype=np.float32) 160 gt = np.asarray(ground_truth, dtype=np.float32) 161 if pred_raw.shape != gt.shape: 162 raise MetricError( 163 f"Depth prediction shape {pred_raw.shape} does not match ground-truth shape {gt.shape}." 164 ) 165 if gt.shape != EXPECTED_HW: 166 raise MetricError( 167 f"RPX D1-F paper calibration requires depth shape {EXPECTED_HW}; got {gt.shape}.", 168 hint="Do not rescale the fixed paper intrinsics; use the released 640x480 frames.", 169 ) 170 171 pred = clip_prediction_for_evaluation(pred_raw) 172 valid = valid_depth_mask(gt) 173 if not valid.any(): 174 raise MetricError("Depth frame contains no finite GT pixels in the 0.3-5.0 m range.") 175 pred_valid = pred[valid] 176 gt_valid = gt[valid] 177 diff = pred_valid - gt_valid 178 log_error = np.log(pred_valid) - np.log(gt_valid) 179 ratio = np.maximum(pred_valid / gt_valid, gt_valid / pred_valid) 180 silog_variance = max( 181 float(np.mean(log_error**2) - np.mean(log_error) ** 2), 182 0.0, 183 ) 184 metrics = { 185 "absrel": float(np.mean(np.abs(diff) / gt_valid)), 186 "rmse": float(np.sqrt(np.mean(diff**2))), 187 "silog": float(100.0 * np.sqrt(silog_variance)), 188 "delta1": float(np.mean(ratio < 1.25)), 189 "irmse": float(np.sqrt(np.mean((1.0 / pred_valid - 1.0 / gt_valid) ** 2))), 190 } 191 if include_fscore: 192 metrics["fscore_5cm"] = point_cloud_fscore_5cm(pred, gt, valid) 193 if not all(np.isfinite(value) for value in metrics.values()): 194 raise MetricError("D1-F paper metric computation produced a non-finite value.") 195 return metrics
Compute the D1-F headline metrics for one 640x480 frame.
include_fscore=False retains the exact paper mask and formulas for
the five image-space metrics while deferring the expensive point-cloud
calculation. This is used by the staged production workflow; the saved
predictions can be evaluated for F-Score later without another model
forward.
198class PaperDepthMetricSuite(MetricSuite): 199 """Metric suite dedicated to the strict RPX D1-F paper protocol.""" 200 201 def __init__(self) -> None: 202 super().__init__(TaskType.MONOCULAR_DEPTH) 203 204 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 205 if not isinstance(prediction, DepthPrediction): 206 raise MetricError( 207 f"PaperDepthMetricSuite expected DepthPrediction, got {type(prediction).__name__}." 208 ) 209 if not isinstance(ground_truth, DepthGroundTruth): 210 raise MetricError( 211 "PaperDepthMetricSuite expected DepthGroundTruth, " 212 f"got {type(ground_truth).__name__}." 213 ) 214 return compute_d1_paper_metrics(prediction.depth_map, ground_truth.depth_map)
Metric suite dedicated to the strict RPX D1-F paper protocol.
204 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 205 if not isinstance(prediction, DepthPrediction): 206 raise MetricError( 207 f"PaperDepthMetricSuite expected DepthPrediction, got {type(prediction).__name__}." 208 ) 209 if not isinstance(ground_truth, DepthGroundTruth): 210 raise MetricError( 211 "PaperDepthMetricSuite expected DepthGroundTruth, " 212 f"got {type(ground_truth).__name__}." 213 ) 214 return compute_d1_paper_metrics(prediction.depth_map, ground_truth.depth_map)
Run every registered calculator and return merged results.
Raises
MetricError Propagated from individual calculators when inputs are shape-mismatched or wrong-typed.
Inherited Members
217class FastPaperDepthMetricSuite(PaperDepthMetricSuite): 218 """Strict D1-F suite with only F-Score deferred to post-processing.""" 219 220 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 221 if not isinstance(prediction, DepthPrediction): 222 raise MetricError( 223 f"FastPaperDepthMetricSuite expected DepthPrediction, got " 224 f"{type(prediction).__name__}." 225 ) 226 if not isinstance(ground_truth, DepthGroundTruth): 227 raise MetricError( 228 "FastPaperDepthMetricSuite expected DepthGroundTruth, " 229 f"got {type(ground_truth).__name__}." 230 ) 231 return compute_d1_paper_metrics( 232 prediction.depth_map, 233 ground_truth.depth_map, 234 include_fscore=False, 235 )
Strict D1-F suite with only F-Score deferred to post-processing.
220 def evaluate(self, prediction: object, ground_truth: object) -> dict[str, float]: 221 if not isinstance(prediction, DepthPrediction): 222 raise MetricError( 223 f"FastPaperDepthMetricSuite expected DepthPrediction, got " 224 f"{type(prediction).__name__}." 225 ) 226 if not isinstance(ground_truth, DepthGroundTruth): 227 raise MetricError( 228 "FastPaperDepthMetricSuite expected DepthGroundTruth, " 229 f"got {type(ground_truth).__name__}." 230 ) 231 return compute_d1_paper_metrics( 232 prediction.depth_map, 233 ground_truth.depth_map, 234 include_fscore=False, 235 )
Run every registered calculator and return merged results.
Raises
MetricError Propagated from individual calculators when inputs are shape-mismatched or wrong-typed.