whassan's picture
Deploy Sensorflow Studio
729628a verified
Raw History Blame Contribute Delete
8.08 kB
"""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