Spaces:
Paused
Paused
File size: 8,082 Bytes
729628a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """Strict Quality Validation Engine: deterministic geometric plausibility checks.
Compares LiDAR point cloud vs bounding-box proposal (spec §8) and computes
per-3D-box metrics with configurable tolerances (spec §9): 3D IoU, position
error, orientation error, dimension error, point density, point-in-box ratio,
ground contact error, plus sensor-consistency checks.
"""
from __future__ import annotations
import math
from typing import Dict, List, Optional
import numpy as np
from sensorflow.evaluation.records import (
Annotation,
CheckLine,
EvalStore,
Frame,
ValidationResult,
)
from sensorflow.evaluation.synthetic import (
CLASS_DIMS,
frame_points,
points_in_box,
project_bbox_2d,
)
from sensorflow.metrics.perception_3d import bev_iou
DEFAULT_TOLERANCES: Dict[str, float] = {
"min_points_in_box": 12,
"min_point_density": 0.4, # points per m^3
"min_box_occupancy": 0.05, # fraction of nearby points captured
"max_dim_ratio": 1.6, # vs class prior
"min_dim_ratio": 0.55,
"max_centroid_offset_m": 1.2, # points centroid vs box center (BEV)
"max_ground_contact_error_m": 0.5,
"max_orientation_track_dev_deg": 45.0,
"min_iou_3d": 0.5, # plausibility floor (triage gate is stricter)
"max_position_error_m": 1.0,
"max_orientation_error_deg": 30.0,
"max_dimension_error": 0.5,
}
def iou_3d(box_a: List[float], box_b: List[float]) -> float:
"""BEV IoU extended with vertical overlap."""
biou = bev_iou(box_a, box_b)
za0, za1 = box_a[2] - box_a[5] / 2, box_a[2] + box_a[5] / 2
zb0, zb1 = box_b[2] - box_b[5] / 2, box_b[2] + box_b[5] / 2
inter_h = max(0.0, min(za1, zb1) - max(za0, zb0))
union_h = max(za1, zb1) - min(za0, zb0)
if union_h <= 0:
return 0.0
return biou * (inter_h / union_h) * (union_h / max(box_a[5], box_b[5])) # approx volumetric
def validate_annotation(
store: EvalStore,
ann: Annotation,
frame: Frame,
points: Optional[np.ndarray] = None,
tolerances: Optional[Dict[str, float]] = None,
) -> ValidationResult:
tol = dict(DEFAULT_TOLERANCES)
if tolerances:
tol.update(tolerances)
checks: List[CheckLine] = []
result = ValidationResult(annotation_id=ann.annotation_id)
if not ann.bbox_3d:
result.passed = False
result.checks = [CheckLine(gate="has_bbox_3d", actual=False, threshold=True, passed=False)]
store.put("validations", result)
return result
if points is None:
points = frame_points(store, frame)
x, y, z, l, w, h, yaw = ann.bbox_3d
volume = max(l * w * h, 1e-3)
# --- point support
mask = points_in_box(points, ann.bbox_3d)
n_in = int(mask.sum())
density = n_in / volume
result.point_density = round(density, 3)
checks.append(CheckLine(gate="points_in_box", actual=n_in,
threshold=tol["min_points_in_box"],
passed=n_in >= tol["min_points_in_box"]))
checks.append(CheckLine(gate="point_density_per_m3", actual=round(density, 2),
threshold=tol["min_point_density"],
passed=density >= tol["min_point_density"]))
# --- box occupancy: fraction of points near the box that fall inside it
near = points[(np.abs(points[:, 0] - x) < max(l, 4)) & (np.abs(points[:, 1] - y) < max(w, 4))]
above_ground = near[near[:, 2] > 0.15] if len(near) else near
occupancy = n_in / max(len(above_ground), 1)
result.point_in_box_ratio = round(min(occupancy, 1.0), 4)
checks.append(CheckLine(gate="box_occupancy", actual=round(occupancy, 3),
threshold=tol["min_box_occupancy"],
passed=occupancy >= tol["min_box_occupancy"]))
# --- dimension plausibility vs class prior
exp = CLASS_DIMS.get(ann.class_name)
if exp:
ratio = float(np.mean([l / exp[0], w / exp[1], h / exp[2]]))
ok = tol["min_dim_ratio"] <= ratio <= tol["max_dim_ratio"]
checks.append(CheckLine(gate="box_dimensions_vs_class_prior", actual=round(ratio, 3),
threshold=f"[{tol['min_dim_ratio']}, {tol['max_dim_ratio']}]", passed=ok))
else:
checks.append(CheckLine(gate="box_dimensions_vs_class_prior", actual="unknown_class",
threshold="known class", passed=True, applicable=False))
# --- centroid consistency: LiDAR centroid vs box center
if n_in >= 3:
centroid = points[mask].mean(axis=0)
offset = math.hypot(centroid[0] - x, centroid[1] - y)
checks.append(CheckLine(gate="centroid_consistency_m", actual=round(offset, 3),
threshold=tol["max_centroid_offset_m"],
passed=offset <= tol["max_centroid_offset_m"]))
else:
checks.append(CheckLine(gate="centroid_consistency_m", actual="insufficient points",
threshold=tol["max_centroid_offset_m"], passed=False, applicable=n_in > 0))
# --- object-to-ground relationship
ground_err = abs((z - h / 2) - 0.0)
result.ground_contact_error = round(ground_err, 3)
checks.append(CheckLine(gate="ground_contact_error_m", actual=round(ground_err, 3),
threshold=tol["max_ground_contact_error_m"],
passed=ground_err <= tol["max_ground_contact_error_m"]))
# --- sensor consistency: LiDAR box should have a camera projection when
# visible; a projectable box missing a 2D detection is a disagreement.
expected_2d = project_bbox_2d(ann.bbox_3d)
sensor_ok = not (expected_2d is not None and ann.bbox_2d is None)
result.sensor_consistent = sensor_ok
checks.append(CheckLine(gate="sensor_consistency_cam_lidar",
actual="2d_present" if ann.bbox_2d else "2d_missing",
threshold="camera+lidar agree", passed=sensor_ok,
applicable=expected_2d is not None))
# --- reference comparison (only when a reference ground truth exists)
gt = next((g for g in frame.gt_boxes if g.gt_id == ann.matched_gt_id), None)
if gt is not None:
iou = iou_3d(ann.bbox_3d, gt.bbox_3d)
pos_err = math.hypot(x - gt.bbox_3d[0], y - gt.bbox_3d[1])
yaw_err = abs(yaw - gt.bbox_3d[6]) % (2 * math.pi)
yaw_err = min(yaw_err, 2 * math.pi - yaw_err)
yaw_err_deg = math.degrees(yaw_err)
dim_err = float(np.mean([abs(ann.bbox_3d[i] - gt.bbox_3d[i]) / max(gt.bbox_3d[i], 0.1) for i in (3, 4, 5)]))
result.iou_3d = round(iou, 4)
result.position_error = round(pos_err, 3)
result.orientation_error_deg = round(yaw_err_deg, 2)
result.dimension_error = round(dim_err, 4)
checks.append(CheckLine(gate="iou_3d_vs_reference", actual=round(iou, 3),
threshold=tol["min_iou_3d"], passed=iou >= tol["min_iou_3d"]))
checks.append(CheckLine(gate="position_error_m", actual=round(pos_err, 3),
threshold=tol["max_position_error_m"],
passed=pos_err <= tol["max_position_error_m"]))
checks.append(CheckLine(gate="orientation_error_deg", actual=round(yaw_err_deg, 1),
threshold=tol["max_orientation_error_deg"],
passed=yaw_err_deg <= tol["max_orientation_error_deg"]))
checks.append(CheckLine(gate="dimension_error", actual=round(dim_err, 3),
threshold=tol["max_dimension_error"],
passed=dim_err <= tol["max_dimension_error"]))
else:
checks.append(CheckLine(gate="iou_3d_vs_reference", actual="no reference GT",
threshold=tol["min_iou_3d"], passed=True, applicable=False))
result.checks = checks
result.passed = all(c.passed for c in checks if c.applicable)
store.put("validations", result)
return result
|