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)步骤如下:

  1. 获取两个旋转框的8个角点:根据中心、尺寸、旋转矩阵计算。
  2. 构建两个凸多面体:每个框是一个六面体(矩形棱柱)。
  3. 计算两个凸多面体的交集:使用 Sutherland-Hodgman 多边形裁剪算法 扩展到3D,通过平面依次裁剪。
  4. 将交集多面体分解为四面体:从内部一点(如交集重心)连接每个面,分割成四面体。
  5. 累加四面体体积:得到交集体积 \( V_{\text{intersection}} \)。
  6. 计算并集体积:\( 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 通常按以下步骤计算:

  1. 对某一类别(如car),将所有预测框按置信度降序排列。
  2. 设定 IoU 阈值(如 0.7),判定每个预测框是否为 TP(与任一未匹配的GT的IoU≥阈值)。
  3. 遍历排序后的预测框,计算每个位置对应的 Precision 和 Recall。
  4. 绘制 PR 曲线,计算曲线下面积(AUC)即为 AP。

3. 常见 AP 计算方法(不同数据集有差异)

数据集计算方法说明
PASCAL VOC11点插值在 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.spatialopen3d 中的实现。”

问: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 IoU3D 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 AP11点插值 (Recall 0,0.1,…,1.0)传统,不稳定
COCO AP101点插值 + 所有点平均更平滑,标准
KITTI AP40点插值自动驾驶专用

三、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.7IoU阈值=0.7的AP,严格匹配KITTI Car
mAP@0.5IoU阈值=0.5,宽松匹配KITTI Pedestrian/Cyclist
mAP@0.25IoU阈值=0.25,极宽松一些分割任务
mAP@COCOIoU阈值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有两个问题:

  1. 阈值固定:0.7对Car合理,但对小目标(行人)太严格
  2. 忽略定位质量:匹配后不再区分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指标

指标含义计算方式说明
ATEAverage Translation Error中心点欧氏距离定位精度
ASEAverage Scale Error1 - IoU_scale尺寸估计
AOEAverage Orientation Error朝向角差(弧度)方向精度
AVEAverage Velocity Error速度差绝对值速度估计
AAEAverage 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 mAPNuScenes 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风格端到端跟踪