Spaces:
Paused
Paused
File size: 7,539 Bytes
729628a 6342806 729628a 6342806 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 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """3D perception metrics: mAP, mAR, IoU, orientation/position error."""
from __future__ import annotations
import math
from typing import Dict, List, Tuple
import numpy as np
def _rotate_corners(cx: float, cy: float, l: float, w: float, yaw: float) -> np.ndarray:
"""Return 4 BEV corners of rotated rectangle."""
cos_y, sin_y = math.cos(yaw), math.sin(yaw)
corners = np.array([
[-l / 2, -w / 2], [l / 2, -w / 2], [l / 2, w / 2], [-l / 2, w / 2],
])
rot = np.array([[cos_y, -sin_y], [sin_y, cos_y]])
return corners @ rot.T + np.array([cx, cy])
def _polygon_area(poly: np.ndarray) -> float:
x, y = poly[:, 0], poly[:, 1]
return 0.5 * abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
def _clip_polygon(subject: np.ndarray, clip: np.ndarray) -> np.ndarray:
"""Sutherland-Hodgman polygon clipping (convex)."""
def inside(p, edge_start, edge_end):
return (edge_end[0] - edge_start[0]) * (p[1] - edge_start[1]) - \
(edge_end[1] - edge_start[1]) * (p[0] - edge_start[0]) >= 0
def intersection(s, e, cp1, cp2):
dc = cp1 - cp2
dp = s - e
n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0]
n2 = s[0] * e[1] - s[1] * e[0]
denom = dc[0] * dp[1] - dc[1] * dp[0]
if abs(denom) < 1e-10:
return s
return np.array([(n1 * dp[0] - n2 * dc[0]) / denom, (n1 * dp[1] - n2 * dc[1]) / denom])
output = subject.copy()
for i in range(len(clip)):
input_list = output
output = []
cp1, cp2 = clip[i], clip[(i + 1) % len(clip)]
if len(input_list) == 0:
break
s = input_list[-1]
for e in input_list:
if inside(e, cp1, cp2):
if not inside(s, cp1, cp2):
output.append(intersection(s, e, cp1, cp2))
output.append(e)
elif inside(s, cp1, cp2):
output.append(intersection(s, e, cp1, cp2))
s = e
output = np.array(output) if output else np.array([]).reshape(0, 2)
return output
def bev_iou(box_a: List[float], box_b: List[float]) -> float:
"""BEV rotated rectangle IoU for bbox_3d [x,y,z,l,w,h,yaw]."""
xa, ya, la, wa, yaw_a = box_a[0], box_a[1], box_a[3], box_a[4], box_a[6]
xb, yb, lb, wb, yaw_b = box_b[0], box_b[1], box_b[3], box_b[4], box_b[6]
poly_a = _rotate_corners(xa, ya, la, wa, yaw_a)
poly_b = _rotate_corners(xb, yb, lb, wb, yaw_b)
inter = _clip_polygon(poly_a, poly_b)
if len(inter) < 3:
return 0.0
inter_area = _polygon_area(inter)
area_a = la * wa
area_b = lb * wb
union = area_a + area_b - inter_area
return inter_area / union if union > 0 else 0.0
def _match_boxes(
preds: List[List[float]],
gts: List[List[float]],
iou_threshold: float,
) -> Tuple[List[Tuple[int, int, float]], List[int], List[int]]:
"""Greedy matching by IoU."""
matched_pairs = []
matched_gt = set()
matched_pred = set()
ious = []
for pi, p in enumerate(preds):
for gi, g in enumerate(gts):
iou = bev_iou(p, g)
if iou >= iou_threshold:
ious.append((iou, pi, gi))
ious.sort(reverse=True)
for iou, pi, gi in ious:
if pi not in matched_pred and gi not in matched_gt:
matched_pairs.append((pi, gi, iou))
matched_pred.add(pi)
matched_gt.add(gi)
unmatched_pred = [i for i in range(len(preds)) if i not in matched_pred]
unmatched_gt = [i for i in range(len(gts)) if i not in matched_gt]
return matched_pairs, unmatched_pred, unmatched_gt
def compute_map_mar(
all_preds: List[List[float]],
all_gts: List[List[float]],
iou_thresholds: List[float] = None,
) -> Dict[str, float]:
"""Set-level precision/recall at fixed IoU thresholds (NOT literature mAP).
Despite the key names ("map_3d"/"mar_3d", kept for API compatibility),
this computes greedy-matched precision and recall averaged over the given
IoU thresholds. It does NOT rank detections by confidence or integrate a
precision-recall curve, so it is not comparable to COCO/nuScenes mAP.
Matching is performed over the box sets as given: callers are responsible
for scoping the inputs to a single frame (see compute_map_mar_by_frame),
otherwise boxes from different timestamps can match each other.
"""
iou_thresholds = iou_thresholds or [0.5, 0.7]
aps, ars = [], []
for thresh in iou_thresholds:
pairs, unmatched_pred, unmatched_gt = _match_boxes(all_preds, all_gts, thresh)
tp = len(pairs)
fp = len(unmatched_pred)
fn = len(unmatched_gt)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
aps.append(precision)
ars.append(recall)
return {
"map_3d": float(np.mean(aps)) if aps else 0.0,
"mar_3d": float(np.mean(ars)) if ars else 0.0,
"mean_iou_3d": float(np.mean([p[2] for p in _match_boxes(all_preds, all_gts, 0.5)[0]])) if all_preds and all_gts and _match_boxes(all_preds, all_gts, 0.5)[0] else 0.0,
}
def compute_map_mar_by_frame(
preds_by_frame: Dict[str, List[List[float]]],
gts_by_frame: Dict[str, List[List[float]]],
iou_thresholds: List[float] = None,
) -> Dict[str, float]:
"""Frame-scoped precision/recall at fixed IoU thresholds (micro-averaged).
Matching runs independently within each frame, so a prediction can only
match ground truth from the same timestamp. TP/FP/FN counts are summed
across frames before ratios are taken. Same naming caveat as
compute_map_mar: "map_3d"/"mar_3d" are precision/recall at fixed IoU
thresholds, not ranked average precision.
"""
iou_thresholds = iou_thresholds or [0.5, 0.7]
frame_ids = sorted(set(preds_by_frame) | set(gts_by_frame))
aps, ars = [], []
for thresh in iou_thresholds:
tp = fp = fn = 0
for fid in frame_ids:
pairs, unmatched_pred, unmatched_gt = _match_boxes(
preds_by_frame.get(fid, []), gts_by_frame.get(fid, []), thresh)
tp += len(pairs)
fp += len(unmatched_pred)
fn += len(unmatched_gt)
aps.append(tp / (tp + fp) if (tp + fp) > 0 else 0.0)
ars.append(tp / (tp + fn) if (tp + fn) > 0 else 0.0)
matched_ious = []
for fid in frame_ids:
pairs, _, _ = _match_boxes(
preds_by_frame.get(fid, []), gts_by_frame.get(fid, []), 0.5)
matched_ious.extend(p[2] for p in pairs)
return {
"map_3d": float(np.mean(aps)) if aps else 0.0,
"mar_3d": float(np.mean(ars)) if ars else 0.0,
"mean_iou_3d": float(np.mean(matched_ious)) if matched_ious else 0.0,
}
def mean_orientation_error(matched_pairs: List[Tuple[List[float], List[float]]]) -> float:
"""Mean absolute yaw error in degrees."""
if not matched_pairs:
return 0.0
errors = []
for pred, gt in matched_pairs:
diff = abs(pred[6] - gt[6])
diff = min(diff, 2 * math.pi - diff)
errors.append(math.degrees(diff))
return float(np.mean(errors))
def mean_position_error(matched_pairs: List[Tuple[List[float], List[float]]]) -> float:
"""Mean L2 BEV center distance in meters."""
if not matched_pairs:
return 0.0
errors = []
for pred, gt in matched_pairs:
dist = math.sqrt((pred[0] - gt[0]) ** 2 + (pred[1] - gt[1]) ** 2)
errors.append(dist)
return float(np.mean(errors))
|