3D目标检测评价指标详解:IoU、AP、mAP、NDS
在3D目标检测中,评价指标是衡量算法性能的关键。下面详细介绍四个核心指标,并重点说明3D空间中的IoU计算方法。
一、IoU(Intersection over Union,交并比)
1. 定义
IoU 衡量预测框与真实框(Ground Truth)的重叠程度,定义为两个框的交集体积除以并集体积:
$$ \text{IoU} = \frac{V_{\text{intersection}}}{V_{\text{union}}} $$值域 [0, 1],通常设定一个阈值(如 0.7)判定检测是否正确(True Positive)。
2. 3D检测中IoU如何计算?—— 详细步骤
3D检测框通常用 中心点 (x, y, z)、尺寸 (l, w, h) 和 朝向角 (yaw) 表示(旋转框)。计算两个旋转3D框的IoU比2D情况复杂得多。
常用方法(按精度/效率排序):
| 方法 | 原理 | 精度 | 速度 | 适用场景 |
|---|---|---|---|---|
| 精确三角形剖分法 | 将两个多面体分割为四面体/三角形,精确计算相交体积 | 极高 | 慢 | 离线评测、学术基准 |
| 近似采样法 | 在框内均匀采样点,统计同时落在两个框内的点比例 | 近似(随采样密度增加) | 中等 | 快速验证 |
| 投影+积分法 | 将3D问题分解为2D平面投影和高度方向的积分 | 高 | 较快 | 常用开源实现(如nuScenes) |
实际工程中最常用的是「精确三角形剖分法」,典型实现(如numpy/scipy)步骤如下:
- 获取两个旋转框的8个角点:根据中心、尺寸、旋转矩阵计算。
- 构建两个凸多面体:每个框是一个六面体(矩形棱柱)。
- 计算两个凸多面体的交集:使用 Sutherland-Hodgman 多边形裁剪算法 扩展到3D,通过平面依次裁剪。
- 将交集多面体分解为四面体:从内部一点(如交集重心)连接每个面,分割成四面体。
- 累加四面体体积:得到交集体积 \( V_{\text{intersection}} \)。
- 计算并集体积:\( V_{\text{union}} = V_1 + V_2 - V_{\text{intersection}} \)。
代码示意(概念):
# 伪代码:使用 open3d 或 scipy.spatial
def compute_3d_iou(box1, box2):
# box: (center, size, rotation_matrix)
corners1 = get_rotated_bbox_corners(box1)
corners2 = get_rotated_bbox_corners(box2)
# 计算交集凸多面体
intersection_poly = convex_intersection(corners1, corners2)
if intersection_poly is None: return 0.0
intersection_vol = convex_polyhedron_volume(intersection_poly)
vol1 = box1.l * box1.w * box1.h
vol2 = box2.l * box2.w * box2.h
iou = intersection_vol / (vol1 + vol2 - intersection_vol)
return iou
注意:对于非旋转框(轴对齐),IoU计算更简单:直接计算三个轴向上的重叠区间长度,乘积为交集体积,再除以并集体积。
3. 为什么不用中心距离或框角点距离?
IoU 对尺度、朝向、位置同时敏感,能全面反映检测质量。而中心距离无法反映形状差异,角点距离无法反映内部重叠程度。
二、AP(Average Precision,平均精度)
1. 基础概念
- Precision:预测为正例中实际为正的比例 \( P = \frac{TP}{TP+FP} \)
- Recall:实际为正中被预测出的比例 \( R = \frac{TP}{TP+FN} \)
2. 计算流程
对于3D目标检测,AP 通常按以下步骤计算:
- 对某一类别(如car),将所有预测框按置信度降序排列。
- 设定 IoU 阈值(如 0.7),判定每个预测框是否为 TP(与任一未匹配的GT的IoU≥阈值)。
- 遍历排序后的预测框,计算每个位置对应的 Precision 和 Recall。
- 绘制 PR 曲线,计算曲线下面积(AUC)即为 AP。
3. 常见 AP 计算方法(不同数据集有差异)
| 数据集 | 计算方法 | 说明 |
|---|---|---|
| PASCAL VOC | 11点插值 | 在 Recall = 0, 0.1, …, 1.0 处取 Precision 最大值,平均 |
| COCO / KITTI / nuScenes | 积分法(所有点) | 计算 PR 曲线下精确面积,更精细 |
KITTI 的特殊性:根据难度(Easy, Moderate, Hard)分别计算 AP,且对于 car 类别 IoU 阈值通常为 0.7,对于 pedestrian/cyclist 为 0.5。
三、mAP(mean Average Precision,平均精度均值)
定义
将所有类别的 AP 取算术平均:
$$ \text{mAP} = \frac{1}{N_{\text{classes}}} \sum_{i=1}^{N_{\text{classes}}} \text{AP}_i $$作用
单类别 AP 无法衡量模型在多类别上的整体表现,mAP 提供了一个综合性指标。
注意
- 不同数据集的 mAP 可能对应不同的 IoU 阈值(如 COCO mAP 是 IoU 从 0.5 到 0.95 间隔 0.05 的 10 个 AP 的平均)。
- 在 nuScenes 中,mAP 也是重要指标,但另有 NDS。
四、NDS(NuScenes Detection Score,nuScenes 检测分数)
1. 背景
nuScenes 是一个大规模自动驾驶数据集,其评价指标 NDS 旨在更全面地评估检测质量,不仅考虑位置重叠(IoU),还考虑朝向、速度、尺寸、属性等误差。
2. 组成
NDS 由 mAP 和 一组 True Positive (TP) 指标 加权平均得到:
$$ \text{NDS} = \frac{1}{2} \left( \text{mAP} + \text{mTP} \right) $$其中 mTP 是以下 5 个 TP 误差指标的平均(每个指标经过归一化,值域 [0,1]):
| TP 指标 | 含义 | 计算方式 |
|---|---|---|
| ATE | 平均平移误差 (m) | 中心点欧氏距离 |
| ASE | 平均尺度误差 (1 - IoU) | 1 - 尺度方向的 IoU(对齐朝向) |
| AOE | 平均朝向误差 (rad) | 偏航角差(模 π) |
| AVE | 平均速度误差 (m/s) | 速度向量的欧氏距离 |
| AAE | 平均属性误差 | 属性分类错误率 |
每个 TP 指标计算方式:对于所有匹配成功的预测框(IoU≥阈值),计算对应误差,然后取平均。之后通过指数衰减函数 \( \exp(-error) \) 映射到 [0,1] 区间(误差越小,得分越高)。
3. 特点
- 更贴近自动驾驶需求:速度、朝向对于轨迹预测至关重要。
- 对微小误差敏感:例如 AOE 对大型车辆要求严格(朝向错 10° 可能扣分)。
- nuScenes 的 mAP 也与传统不同:它是在 2D 中心平面距离(而非 3D IoU)上定义的? 实际上 nuScenes 的匹配采用地面中心点距离(阈值 0.5m, 1m, 2m, 4m),而不是 IoU,因为激光雷达稀疏时 IoU 波动大。但后来版本也支持 IoU。具体请参考 nuScenes 官方:匹配基于 BEV 中心距离,但检测评估仍然计算 AP(基于匹配结果)。
4. NDS 计算示例(简化)
假设某类别的 mAP = 0.6,5 个 TP 指标得分分别为 [0.9, 0.8, 0.7, 0.6, 0.5] → mTP = (0.9+0.8+0.7+0.6+0.5)/5 = 0.7
则 NDS = 0.5*(0.6 + 0.7) = 0.65。
五、回答
问:3D检测中 IoU 如何计算?
“3D 中 IoU 计算比 2D 复杂,因为检测框通常是旋转的。常见方法是:首先获得两个框的 8 个角点,然后用多边形裁剪算法计算两个凸多面体的交集多面体,再将交集分解为四面体求和得到交集体积,最后用 \( \text{IoU} = \frac{V_{\text{intersection}}}{V_1 + V_2 - V_{\text{intersection}}} \) 得到结果。对于轴对齐框,可以简化,直接计算三个轴向上重叠区间的乘积。实际工程中,我们可以调用
scipy.spatial或open3d中的实现。”
问:mAP 和 NDS 的区别?
“mAP 仅基于 IoU 阈值判断正误,平均了各类别的 AP。而 nuScenes 提出的 NDS 在 mAP 基础上,还引入了平移、尺度、朝向、速度、属性五个误差指标,更能反映自动驾驶对轨迹预测和运动估计的需求。NDS 是 mAP 与这五个指标平均值的算术平均,更全面。”
六、结合项目
在“智能检测线项目”中,点云尺寸测量算法评估时,可能需要计算测量值与真值的相对误差,而非 IoU。但对于“障碍物检测算法开发”项目,AP / mAP 是典型评价指标。建议准备如下回答:
- “在无人扫路车项目中,我们使用 KITTI 风格的 AP(IoU=0.5 用于行人,0.7 用于车辆)来评估障碍物检测精度,最终 car 类别的 moderate AP 达到 85%。”
- “对于垃圾分选机器人,因为垃圾形状不规则,我们采用 3D IoU ≥ 0.5 作为正确检测标准,并计算 mAP。”
代码实现
一、IoU(Intersection over Union)
1.1 2D IoU vs 3D IoU
| 维度 | 2D IoU | 3D IoU |
|---|---|---|
| 输入 | 两个矩形框 (x,y,w,h) | 两个3D框 (x,y,z,w,l,h,theta) |
| 计算对象 | 矩形面积 | 立体体积 |
| 复杂性 | 简单,有解析解 | 复杂,需数值计算或近似 |
1.2 3D IoU计算详解
核心问题:两个3D旋转框的交集体积没有闭式解!
3D框A: (cx₁, cy₁, cz₁, w₁, l₁, h₁, θ₁)
3D框B: (cx₂, cy₂, cz₂, w₂, l₂, h₂, θ₂)
计算步骤:
┌─────────────────────────────────────────┐
│ Step 1: 高度方向交集 (z轴,无旋转) │
│ z_overlap = min(z₁+h₁/2, z₂+h₂/2) - │
│ max(z₁-h₁/2, z₂-h₁/2) │
│ 若 z_overlap ≤ 0 → IoU = 0 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Step 2: BEV平面交集 (x-y平面,有旋转) │
│ 将3D框投影到BEV,得到两个旋转矩形 │
│ 计算旋转矩形的交集面积 │
│ │
│ 方法A: 凸多边形裁剪 (Sutherland-Hodgman)│
│ 方法B: 离散化采样 (近似) │
│ 方法C: 分离轴定理 (SAT) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Step 3: 计算IoU │
│ Intersection = z_overlap × bev_overlap │
│ Union = Volume_A + Volume_B - Intersection│
│ IoU = Intersection / Union │
└─────────────────────────────────────────┘
1.3 BEV旋转框交集计算(Sutherland-Hodgman算法)
#include <vector>
#include <array>
#include <cmath>
#include <algorithm>
#include <Eigen/Dense>
struct Box3D {
float cx, cy, cz; // 中心点
float w, l, h; // 长宽高
float theta; // 偏航角
};
// 获取3D框的8个顶点
std::vector<Eigen::Vector3f> get3DBoxCorners(const Box3D& box) {
std::vector<Eigen::Vector3f> corners(8);
// 局部坐标系下的8个顶点
float w2 = box.w / 2, l2 = box.l / 2, h2 = box.h / 2;
std::array<Eigen::Vector3f, 8> local_corners = {
Eigen::Vector3f(-w2, -l2, -h2), Eigen::Vector3f(-w2, -l2, h2),
Eigen::Vector3f(-w2, l2, -h2), Eigen::Vector3f(-w2, l2, h2),
Eigen::Vector3f( w2, -l2, -h2), Eigen::Vector3f( w2, -l2, h2),
Eigen::Vector3f( w2, l2, -h2), Eigen::Vector3f( w2, l2, h2)
};
// 旋转矩阵 (绕z轴)
float cos_t = std::cos(box.theta);
float sin_t = std::sin(box.theta);
Eigen::Matrix3f R;
R << cos_t, -sin_t, 0,
sin_t, cos_t, 0,
0, 0, 1;
for (int i = 0; i < 8; ++i) {
corners[i] = R * local_corners[i] + Eigen::Vector3f(box.cx, box.cy, box.cz);
}
return corners;
}
// 多边形裁剪 (Sutherland-Hodgman)
std::vector<Eigen::Vector2f> sutherlandHodgman(
const std::vector<Eigen::Vector2f>& subjectPolygon,
const std::vector<Eigen::Vector2f>& clipPolygon) {
std::vector<Eigen::Vector2f> outputList = subjectPolygon;
for (size_t edge = 0; edge < clipPolygon.size(); ++edge) {
std::vector<Eigen::Vector2f> inputList = outputList;
outputList.clear();
if (inputList.empty()) break;
Eigen::Vector2f A = clipPolygon[edge];
Eigen::Vector2f B = clipPolygon[(edge + 1) % clipPolygon.size()];
for (size_t i = 0; i < inputList.size(); ++i) {
Eigen::Vector2f P = inputList[i];
Eigen::Vector2f Q = inputList[(i + 1) % inputList.size()];
// 计算点在边的哪一侧
float cross1 = (B.x() - A.x()) * (P.y() - A.y()) -
(B.y() - A.y()) * (P.x() - A.x());
float cross2 = (B.x() - A.x()) * (Q.y() - A.y()) -
(B.y() - A.y()) * (Q.x() - A.x());
bool insideP = cross1 >= 0;
bool insideQ = cross2 >= 0;
if (insideP && insideQ) {
// 都在内部,保留Q
outputList.push_back(Q);
} else if (insideP && !insideQ) {
// P在内,Q在外,保留交点
// 计算线段PQ与边AB的交点
float t = cross1 / (cross1 - cross2);
Eigen::Vector2f intersection = P + t * (Q - P);
outputList.push_back(intersection);
} else if (!insideP && insideQ) {
// P在外,Q在内,保留交点和Q
float t = cross1 / (cross1 - cross2);
Eigen::Vector2f intersection = P + t * (Q - P);
outputList.push_back(intersection);
outputList.push_back(Q);
}
// 都在外部,不保留
}
}
return outputList;
}
// 计算多边形面积 (Shoelace公式)
float polygonArea(const std::vector<Eigen::Vector2f>& poly) {
if (poly.size() < 3) return 0.0f;
float area = 0.0f;
for (size_t i = 0; i < poly.size(); ++i) {
size_t j = (i + 1) % poly.size();
area += poly[i].x() * poly[j].y();
area -= poly[j].x() * poly[i].y();
}
return std::abs(area) / 2.0f;
}
// 计算BEV平面两个旋转矩形的IoU
float computeBEVIoURotated(const Box3D& box1, const Box3D& box2) {
// 获取BEV corners (只取x,y,取底部4个点: 0,2,6,4)
auto corners1_3d = get3DBoxCorners(box1);
auto corners2_3d = get3DBoxCorners(box2);
std::vector<Eigen::Vector2f> poly1, poly2;
std::array<int, 4> indices = {0, 2, 6, 4};
for (int idx : indices) {
poly1.push_back(corners1_3d[idx].head<2>());
poly2.push_back(corners2_3d[idx].head<2>());
}
// 使用Sutherland-Hodgman计算交集
std::vector<Eigen::Vector2f> intersection = sutherlandHodgman(poly1, poly2);
if (intersection.empty()) {
intersection = sutherlandHodgman(poly2, poly1);
}
float area1 = polygonArea(poly1);
float area2 = polygonArea(poly2);
float interArea = polygonArea(intersection);
float unionArea = area1 + area2 - interArea;
return (unionArea > 0) ? (interArea / unionArea) : 0.0f;
}
// 完整3D IoU计算
float compute3DIoU(const Box3D& box1, const Box3D& box2) {
// 高度交集
float z1_min = box1.cz - box1.h / 2;
float z1_max = box1.cz + box1.h / 2;
float z2_min = box2.cz - box2.h / 2;
float z2_max = box2.cz + box2.h / 2;
float z_overlap = std::min(z1_max, z2_max) - std::max(z1_min, z2_min);
if (z_overlap <= 0) return 0.0f;
// BEV交集
float bev_iou = computeBEVIoURotated(box1, box2);
float area1 = box1.w * box1.l;
float area2 = box2.w * box2.l;
float bev_overlap = bev_iou * (area1 + area2) / (1.0f + bev_iou);
// 精确计算应使用多边形交集面积
// 这里简化处理
float intersection_3d = z_overlap * bev_overlap;
float vol1 = area1 * box1.h;
float vol2 = area2 * box2.h;
float union_3d = vol1 + vol2 - intersection_3d;
return (union_3d > 0) ? (intersection_3d / union_3d) : 0.0f;
}
1.4 高效近似方法
| 方法 | 原理 | 精度 | 速度 |
|---|---|---|---|
| 精确多边形 | Sutherland-Hodgman裁剪 | 100% | 慢 |
| 2D IoU近似 | 忽略旋转,用轴对齐框 | 低 | 极快 |
| BEV角点距离 | 用角点距离代替IoU | 中 | 快 |
| CUDA加速 | 并行计算多个框对 | 100% | 快(批量) |
实际部署常用:
// 简化版:BEV 2D IoU + 高度检查(忽略旋转细节)
float approx3DIoU(const Box3D& box1, const Box3D& box2) {
// 轴对齐近似
float x1_min = box1.cx - box1.w / 2;
float x1_max = box1.cx + box1.w / 2;
float y1_min = box1.cy - box1.l / 2;
float y1_max = box1.cy + box1.l / 2;
float z1_min = box1.cz - box1.h / 2;
float z1_max = box1.cz + box1.h / 2;
float x2_min = box2.cx - box2.w / 2;
float x2_max = box2.cx + box2.w / 2;
float y2_min = box2.cy - box2.l / 2;
float y2_max = box2.cy + box2.l / 2;
float z2_min = box2.cz - box2.h / 2;
float z2_max = box2.cz + box2.h / 2;
float x_overlap = std::max(0.0f, std::min(x1_max, x2_max) - std::max(x1_min, x2_min));
float y_overlap = std::max(0.0f, std::min(y1_max, y2_max) - std::max(y1_min, y2_min));
float z_overlap = std::max(0.0f, std::min(z1_max, z2_max) - std::max(z1_min, z2_min));
float intersection = x_overlap * y_overlap * z_overlap;
float vol1 = box1.w * box1.l * box1.h;
float vol2 = box2.w * box2.l * box2.h;
float uni = vol1 + vol2 - intersection;
return (uni > 0) ? (intersection / uni) : 0.0f;
}
二、AP(Average Precision)
2.1 从Precision-Recall到AP
给定类别(如Car),按置信度排序所有预测框:
置信度: 0.95 0.90 0.85 0.80 0.75 0.70 0.65 ...
匹配: TP TP FP TP FP TN TP ...
↓
计算每个阈值下的Precision和Recall
Precision = TP / (TP + FP) ← 查准率
Recall = TP / 总GT数 ← 查全率
绘制PR曲线,AP = 曲线下面积
2.2 3D检测的AP计算细节
匹配规则:
#include <vector>
#include <algorithm>
#include <numeric>
struct Detection {
Box3D box;
float score;
int class_id;
bool is_tp = false;
};
struct GroundTruth {
Box3D box;
int class_id;
bool matched = false;
};
class APEvaluator {
public:
float computeAP(std::vector<Detection>& predictions,
std::vector<GroundTruth>& ground_truths,
float iou_threshold) {
// 按置信度降序排列
std::sort(predictions.begin(), predictions.end(),
[](const Detection& a, const Detection& b) {
return a.score > b.score;
});
std::vector<bool> tp(predictions.size(), false);
std::vector<bool> fp(predictions.size(), false);
// 重置GT匹配状态
for (auto& gt : ground_truths) {
gt.matched = false;
}
// 贪心匹配
for (size_t i = 0; i < predictions.size(); ++i) {
const auto& pred = predictions[i];
float max_iou = 0.0f;
size_t max_gt_idx = -1;
// 找同类别、未匹配、IoU最大的GT
for (size_t j = 0; j < ground_truths.size(); ++j) {
auto& gt = ground_truths[j];
if (gt.matched || gt.class_id != pred.class_id) continue;
float iou = compute3DIoU(pred.box, gt.box);
if (iou > max_iou) {
max_iou = iou;
max_gt_idx = j;
}
}
if (max_iou >= iou_threshold && max_gt_idx != -1) {
tp[i] = true;
ground_truths[max_gt_idx].matched = true;
} else {
fp[i] = true;
}
}
// 计算PR曲线
return interpolateAP(tp, fp, ground_truths.size());
}
private:
float interpolateAP(const std::vector<bool>& tp,
const std::vector<bool>& fp,
size_t num_gt) {
std::vector<float> precisions;
std::vector<float> recalls;
int tp_sum = 0, fp_sum = 0;
for (size_t i = 0; i < tp.size(); ++i) {
if (tp[i]) tp_sum++;
if (fp[i]) fp_sum++;
float precision = static_cast<float>(tp_sum) / (tp_sum + fp_sum);
float recall = static_cast<float>(tp_sum) / num_gt;
precisions.push_back(precision);
recalls.push_back(recall);
}
// 101点插值 (COCO风格)
std::vector<float> recall_thresholds;
for (int i = 0; i <= 100; ++i) {
recall_thresholds.push_back(i / 100.0f);
}
float ap = 0.0f;
for (float r_t : recall_thresholds) {
float max_p = 0.0f;
for (size_t i = 0; i < recalls.size(); ++i) {
if (recalls[i] >= r_t) {
max_p = std::max(max_p, precisions[i]);
}
}
ap += max_p;
}
return ap / recall_thresholds.size();
}
};
AP计算方式对比:
| 方法 | 计算方式 | 特点 |
|---|---|---|
| VOC AP | 11点插值 (Recall 0,0.1,…,1.0) | 传统,不稳定 |
| COCO AP | 101点插值 + 所有点平均 | 更平滑,标准 |
| KITTI AP | 40点插值 | 自动驾驶专用 |
三、mAP(mean Average Precision)
3.1 定义
$$ \text{mAP} = \frac{1}{N_{classes}} \sum_{i=1}^{N_{classes}} \text{AP}_i $$3.2 3D检测中的mAP变体
| 变体 | 说明 | 使用场景 |
|---|---|---|
| mAP@0.7 | IoU阈值=0.7的AP,严格匹配 | KITTI Car |
| mAP@0.5 | IoU阈值=0.5,宽松匹配 | KITTI Pedestrian/Cyclist |
| mAP@0.25 | IoU阈值=0.25,极宽松 | 一些分割任务 |
| mAP@COCO | IoU阈值0.5:0.05:0.95平均 | 通用目标检测 |
KITTI 3D检测标准:
// Car: AP@0.7 (3D IoU >= 0.7才算匹配)
// Pedestrian: AP@0.5
// Cyclist: AP@0.5
// 且分难度:
// Easy: 近处,完整可见
// Moderate: 中等距离,部分遮挡
// Hard: 远处,严重遮挡
struct KITTIDifficulty {
float min_height;
float max_occlusion;
float max_truncation;
float min_depth;
};
std::map<std::string, KITTIDifficulty> KITTI_CONFIG = {
{"Easy", {40.0f, 0, 0.15f, 0.0f}},
{"Moderate", {25.0f, 1, 0.30f, 0.0f}},
{"Hard", {25.0f, 2, 0.50f, 0.0f}}
};
四、NDS(NuScenes Detection Score)
4.1 为什么需要NDS?
KITTI的mAP有两个问题:
- 阈值固定:0.7对Car合理,但对小目标(行人)太严格
- 忽略定位质量:匹配后不再区分IoU=0.7还是0.9
NDS设计目标:综合考量检测、定位、分类、属性预测
4.2 NDS组成公式
$$ \text{NDS} = \frac{1}{10} \left[ 5\text{mAP} + \sum_{i=1}^{5} (1 - \min(1, \text{TP}_i)) \right] \times 100\% $$5个True Positive指标:
| 指标 | 含义 | 计算方式 | 说明 |
|---|---|---|---|
| ATE | Average Translation Error | 中心点欧氏距离 | 定位精度 |
| ASE | Average Scale Error | 1 - IoU_scale | 尺寸估计 |
| AOE | Average Orientation Error | 朝向角差(弧度) | 方向精度 |
| AVE | Average Velocity Error | 速度差绝对值 | 速度估计 |
| AAE | Average Attribute Error | 属性分类错误率 | 属性精度 |
4.3 各TP指标详解
#include <Eigen/Dense>
struct NuScenesBox {
Box3D box;
Eigen::Vector2f velocity; // (vx, vy)
std::vector<int> attributes;
};
struct TPMetrics {
float ate; // Average Translation Error
float ase; // Average Scale Error
float aoe; // Average Orientation Error
float ave; // Average Velocity Error
float aae; // Average Attribute Error
};
class NuScenesEvaluator {
public:
// ATE(平移误差)
float computeATE(const NuScenesBox& gt, const NuScenesBox& pred) {
Eigen::Vector3f gt_center(gt.box.cx, gt.box.cy, gt.box.cz);
Eigen::Vector3f pred_center(pred.box.cx, pred.box.cy, pred.box.cz);
return (gt_center - pred_center).norm();
}
// ASE(尺度误差)
float computeASE(const NuScenesBox& gt, const NuScenesBox& pred) {
// 计算尺寸维度的IoU(假设中心对齐)
float w_i = std::min(gt.box.w, pred.box.w);
float l_i = std::min(gt.box.l, pred.box.l);
float h_i = std::min(gt.box.h, pred.box.h);
float w_u = std::max(gt.box.w, pred.box.w);
float l_u = std::max(gt.box.l, pred.box.l);
float h_u = std::max(gt.box.h, pred.box.h);
float iou_3d = (w_i * l_i * h_i) / (w_u * l_u * h_u);
return 1.0f - iou_3d;
}
// AOE(朝向误差)
float computeAOE(const NuScenesBox& gt, const NuScenesBox& pred,
bool is_vehicle = true) {
float diff = std::abs(gt.box.theta - pred.box.theta);
// 归一化到 [0, π]
while (diff > M_PI) diff -= 2 * M_PI;
while (diff < -M_PI) diff += 2 * M_PI;
diff = std::abs(diff);
// 对于车辆,考虑对称性(0和π等价)
if (is_vehicle) {
diff = std::min(diff, static_cast<float>(M_PI) - diff);
}
return diff;
}
// AVE(速度误差)
float computeAVE(const NuScenesBox& gt, const NuScenesBox& pred) {
return (gt.velocity - pred.velocity).norm();
}
// AAE(属性误差)
float computeAAE(const NuScenesBox& gt, const NuScenesBox& pred) {
if (gt.attributes.empty()) return 0.0f;
int correct = 0;
size_t min_len = std::min(gt.attributes.size(), pred.attributes.size());
for (size_t i = 0; i < min_len; ++i) {
if (gt.attributes[i] == pred.attributes[i]) correct++;
}
return 1.0f - static_cast<float>(correct) / gt.attributes.size();
}
// 计算单个类别的TP指标
TPMetrics computeTPMetrics(
const std::vector<NuScenesBox>& gts,
const std::vector<NuScenesBox>& preds,
float distance_threshold = 2.0f) {
// 基于BEV中心距离匹配(非IoU)
std::vector<bool> matched(gts.size(), false);
std::vector<std::pair<size_t, size_t>> matches;
for (size_t i = 0; i < preds.size(); ++i) {
float min_dist = distance_threshold;
size_t best_gt = -1;
for (size_t j = 0; j < gts.size(); ++j) {
if (matched[j]) continue;
float dist = std::hypot(
preds[i].box.cx - gts[j].box.cx,
preds[i].box.cy - gts[j].box.cy
);
if (dist < min_dist) {
min_dist = dist;
best_gt = j;
}
}
if (best_gt != -1) {
matched[best_gt] = true;
matches.push_back({best_gt, i});
}
}
// 计算各指标
TPMetrics metrics = {0, 0, 0, 0, 0};
if (matches.empty()) return metrics;
for (const auto& [gt_idx, pred_idx] : matches) {
metrics.ate += computeATE(gts[gt_idx], preds[pred_idx]);
metrics.ase += computeASE(gts[gt_idx], preds[pred_idx]);
metrics.aoe += computeAOE(gts[gt_idx], preds[pred_idx]);
metrics.ave += computeAVE(gts[gt_idx], preds[pred_idx]);
metrics.aae += computeAAE(gts[gt_idx], preds[pred_idx]);
}
float n = matches.size();
metrics.ate /= n;
metrics.ase /= n;
metrics.aoe /= n;
metrics.ave /= n;
metrics.aae /= n;
return metrics;
}
// 计算NDS
float computeNDS(float mAP, const TPMetrics& tp) {
// 归一化各误差
float norm_ate = std::min(1.0f, tp.ate / 2.0f); // 2m阈值
float norm_ase = std::min(1.0f, tp.ase);
float norm_aoe = std::min(1.0f, tp.aoe / static_cast<float>(M_PI));
float norm_ave = std::min(1.0f, tp.ave / 0.5f); // 0.5m/s阈值
float norm_aae = std::min(1.0f, tp.aae);
float nds = 0.5f * mAP +
0.1f * (1 - norm_ate) +
0.1f * (1 - norm_ase) +
0.1f * (1 - norm_aoe) +
0.1f * (1 - norm_ave) +
0.1f * (1 - norm_aae);
return nds;
}
};
4.4 NDS计算完整流程
对于每个类别(Car, Ped, Truck, Bus, Trailer等):
│
├──→ 计算AP(使用距离阈值匹配,而非IoU)
│ 匹配阈值:2m (BEV中心距离)
│
└──→ 对匹配上的TP,计算:
ATE, ASE, AOE, AVE, AAE
类别NDS = 0.5×AP + 0.1×(1-min(1,ATE/2)) + 0.1×(1-min(1,ASE)) +
0.1×(1-min(1,AOE/π)) + 0.1×(1-min(1,AVE/0.5)) + 0.1×(1-min(1,AAE))
(注:实际NuScenes官方实现会归一化各误差)
最终NDS = 所有类别NDS的平均
关键差异:
| 特性 | KITTI mAP | NuScenes NDS |
|---|---|---|
| 匹配标准 | 3D IoU阈值 | BEV中心距离<2m |
| 评估维度 | 仅检测存在性 | 检测+定位+速度+属性 |
| 对小目标 | 不友好(IoU难达标) | 友好(距离匹配) |
| 速度估计 | 无 | 有(AVE) |
| 朝向估计 | 无 | 有(AOE) |
五、完整评估系统代码
#include <vector>
#include <string>
#include <map>
#include <algorithm>
#include <numeric>
#include <Eigen/Dense>
class DetectionEvaluator {
public:
DetectionEvaluator(const std::string& dataset = "kitti")
: dataset_(dataset) {
if (dataset == "kitti") {
iou_thresholds_ = {0.7f}; // Car
// 对于Pedestrian/Cyclist使用0.5
} else if (dataset == "nuscenes") {
iou_thresholds_ = {}; // NuScenes使用距离匹配
} else if (dataset == "coco") {
for (float t = 0.5f; t <= 0.95f; t += 0.05f) {
iou_thresholds_.push_back(t);
}
}
}
struct EvaluationResult {
float mAP = 0.0f;
float NDS = 0.0f; // 仅NuScenes
std::map<std::string, float> class_AP;
std::map<std::string, TPMetrics> class_TP;
};
EvaluationResult evaluate(
const std::map<std::string, std::vector<Detection>>& predictions,
const std::map<std::string, std::vector<GroundTruth>>& ground_truths) {
EvaluationResult result;
std::vector<float> all_aps;
for (const auto& [class_name, preds] : predictions) {
const auto& gts = ground_truths.at(class_name);
if (dataset_ == "nuscenes") {
// NuScenes评估
float ap = computeNuScenesAP(preds, gts);
result.class_AP[class_name] = ap;
all_aps.push_back(ap);
// 计算TP指标
NuScenesEvaluator nusc_eval;
auto tp = nusc_eval.computeTPMetrics(
convertToNuScenesBoxes(gts),
convertToNuScenesBoxes(preds)
);
result.class_TP[class_name] = tp;
} else {
// KITTI/COCO风格:多IoU阈值平均
std::vector<float> aps;
for (float thresh : iou_thresholds_) {
aps.push_back(computeAP(preds, gts, thresh));
}
float mean_ap = std::accumulate(aps.begin(), aps.end(), 0.0f) / aps.size();
result.class_AP[class_name] = mean_ap;
all_aps.push_back(mean_ap);
}
}
result.mAP = std::accumulate(all_aps.begin(), all_aps.end(), 0.0f)
/ all_aps.size();
// 计算NDS(仅NuScenes)
if (dataset_ == "nuscenes") {
NuScenesEvaluator nusc_eval;
TPMetrics avg_tp = averageTPMetrics(result.class_TP);
result.NDS = nusc_eval.computeNDS(result.mAP, avg_tp);
}
return result;
}
private:
std::string dataset_;
std::vector<float> iou_thresholds_;
APEvaluator ap_evaluator_;
NuScenesEvaluator nusc_evaluator_;
float computeAP(const std::vector<Detection>& preds,
const std::vector<GroundTruth>& gts,
float iou_thresh) {
return ap_evaluator_.computeAP(
const_cast<std::vector<Detection>&>(preds),
const_cast<std::vector<GroundTruth>&>(gts),
iou_thresh
);
}
float computeNuScenesAP(const std::vector<Detection>& preds,
const std::vector<GroundTruth>& gts) {
// NuScenes使用距离匹配而非IoU
// 实现类似但匹配逻辑不同
return 0.0f; // 简化
}
std::vector<NuScenesBox> convertToNuScenesBoxes(
const std::vector<GroundTruth>& gts) {
// 转换函数
return {};
}
TPMetrics averageTPMetrics(const std::map<std::string, TPMetrics>& tp_map) {
TPMetrics avg = {0, 0, 0, 0, 0};
if (tp_map.empty()) return avg;
for (const auto& [name, tp] : tp_map) {
avg.ate += tp.ate;
avg.ase += tp.ase;
avg.aoe += tp.aoe;
avg.ave += tp.ave;
avg.aae += tp.aae;
}
float n = tp_map.size();
avg.ate /= n; avg.ase /= n; avg.aoe /= n; avg.ave /= n; avg.aae /= n;
return avg;
}
};
六、总结对比
| 方法 | 速度 | 精度 | 适用场景 |
|---|---|---|---|
| KF+匈牙利 | 快 | 预测维持 | 实时系统,简单场景 |
| EKF+马氏距离 | 快 | 不确定性建模 | 高机动目标 |
| MHT | 慢 | 多假设保留 | 复杂交叉场景 |
| CenterPoint | 极快 | 学习速度 | 大规模部署 |
| Transformer | 中等 | 端到端学习 | 研究前沿 |
工程建议:
- 基础方案:EKF + 3D GIoU + 预测维持(3帧)
- 进阶方案:增加Re-ID特征 + 几何遮挡推理
- 部署方案:CenterPoint风格端到端跟踪