3. 绝对轨迹误差(ATE):从定义到实践
各位同学,今天我们来聊聊 SLAM 评估里最核心的指标之一——绝对轨迹误差(ATE)。
说实话,我刚开始做 SLAM 的时候,对 ATE 的理解也就停留在「算个 RMSE」的层面。直到有一次,我拿着一个看起来 RMSE 很低的轨迹去汇报,结果被导师一眼看穿问题——「你这轨迹整体漂移不大,但局部抖动很厉害啊」。嗯,从那天起我才真正开始认真琢磨 ATE 到底在算什么。
3.1 ATE 到底在衡量什么?
绝对轨迹误差,说白了就是:你估计的轨迹,和真实轨迹之间,差了多少。
你想想看,我们做 SLAM 最终要的是什么?是让机器人知道自己「在哪儿」。ATE 就是直接回答这个问题的——它衡量的是每个时间点上,估计位姿和真值位姿之间的偏差。
这里有个关键点:ATE 是「绝对」的。什么意思?它关心的是全局坐标系下的位置误差,而不是相对运动误差。比如你走了一个 10 米的走廊,ATE 会告诉你每个时刻你估计的位置和真实位置差了多少厘米。
核心定义:给定估计轨迹 P = {p₁, p₂, ..., pₙ} 和真实轨迹 Q = {q₁, q₂, ..., qₙ},ATE 计算的是经过最优刚体变换对齐后,每个对应位姿之间的平移误差。
3.2 计算方法:一步步拆解
计算 ATE 其实就三步,我习惯这么记:对齐 → 求差 → 统计。
第一步:轨迹对齐
为什么需要对齐?因为估计轨迹和真实轨迹通常不在同一个坐标系下。你想想,你的 SLAM 系统可能以第一帧为原点,而真值可能是 GPS 坐标系。所以我们需要找一个刚体变换 T,让两个轨迹尽可能重合。
这个变换通常用 Umeyama 算法求解,其实就是最小二乘意义上的最优对齐。我在项目中遇到过一个问题:如果轨迹太短或者运动太单一,这个对齐会很不稳定。所以建议至少要有几米的运动距离再做 ATE 评估。
第二步:计算每个时间点的误差
对齐之后,对于每个时间点 i,我们计算:
e_i = ||trans(p_i) - trans(T * q_i)||
这里 trans() 是取平移向量。注意,我们通常只关心平移误差,旋转误差一般用 RPE 来评估。
第三步:统计汇总
得到所有时间点的误差后,我们通常用 RMSE 或均值来汇总。下面这张图展示了整个流程:
3.3 RMSE 与均值:到底该用哪个?
这是很多初学者会困惑的地方。我直接说结论:RMSE 比均值更常用,也更有意义。
为什么?因为 RMSE 对大的误差更敏感。你想想看,如果你的轨迹大部分时间误差只有 5cm,但中间有一次跳到了 20cm,均值可能只涨到 6cm,而 RMSE 会明显变大。这其实是我们想要的——大的漂移应该被惩罚。
| 指标 | 计算公式 | 特点 | 我推荐的使用场景 |
|---|---|---|---|
| RMSE | sqrt(mean(e_i²)) | 对大误差敏感,能反映最差情况 | 论文、竞赛、正式评估 |
| Mean | mean(e_i) | 反映平均表现,但会掩盖局部问题 | 快速调试、初步筛选 |
| Median | median(e_i) | 对异常值鲁棒 | 数据有离群点时使用 |
| Max | max(e_i) | 反映最坏情况 | 安全关键应用 |
我的个人习惯:在论文里同时报告 RMSE 和 Mean。RMSE 作为主要指标,Mean 作为辅助参考。如果两者差距很大,说明轨迹存在局部大误差,需要进一步分析。
3.4 代码实现:手写一个 ATE 计算
光说不练假把式。下面我给出一段 Python 代码,实现 ATE 的核心计算。这段代码我用了很多年,基本没改过。
import numpy as np
from scipy.spatial.transform import Rotation as R
def compute_ate(est_poses, gt_poses):
"""
计算绝对轨迹误差
est_poses: 估计位姿列表,每个元素是 4x4 变换矩阵
gt_poses: 真实位姿列表,格式同上
"""
n = len(est_poses)
errors = []
# 提取平移向量
est_trans = np.array([p[:3, 3] for p in est_poses])
gt_trans = np.array([p[:3, 3] for p in gt_poses])
# 轨迹对齐(Umeyama 算法)
# 这里简化处理,实际建议用 evo 库
centroid_est = np.mean(est_trans, axis=0)
centroid_gt = np.mean(gt_trans, axis=0)
est_centered = est_trans - centroid_est
gt_centered = gt_trans - centroid_gt
# 计算最优旋转
H = est_centered.T @ gt_centered
U, _, Vt = np.linalg.svd(H)
R_opt = Vt.T @ U.T
if np.linalg.det(R_opt) < 0:
Vt[-1, :] *= -1
R_opt = Vt.T @ U.T
# 计算最优平移
t_opt = centroid_gt - R_opt @ centroid_est
# 对齐后计算每个点的误差
for i in range(n):
est_aligned = R_opt @ est_trans[i] + t_opt
error = np.linalg.norm(est_aligned - gt_trans[i])
errors.append(error)
errors = np.array(errors)
rmse = np.sqrt(np.mean(errors ** 2))
mean = np.mean(errors)
median = np.median(errors)
std = np.std(errors)
return {
'rmse': rmse,
'mean': mean,
'median': median,
'std': std,
'errors': errors
}
我曾经踩过的坑:直接用上面的简化对齐代码处理大角度旋转的轨迹,结果 RMSE 算出来偏大很多。后来才发现,Umeyama 算法在处理纯旋转时会有数值问题。建议直接用 evo 库的 evo_ape 命令,它处理了各种边界情况。
3.5 实际使用中的注意事项
最后,我总结几个实际项目中容易忽略的点:
- 时间戳对齐:估计轨迹和真实轨迹的时间戳往往不一致,需要插值。我习惯用线性插值,但如果运动很快,建议用样条插值。
- 部分遮挡:如果真值数据有缺失,不要直接跳过。我曾经犯过这个错,结果 RMSE 看起来很好,但实际上是因为跳过了最难的部分。
- 尺度问题:单目 SLAM 有尺度模糊性。计算 ATE 前需要先估计尺度因子,或者用 Sim(3) 对齐。这个我们后面章节会详细讲。
- 不要只看数字:RMSE 0.1m 不一定比 0.15m 好。如果 0.1m 的轨迹在某个区域有 0.5m 的跳变,而 0.15m 的轨迹很平滑,那后者在实际应用中可能更可靠。
嗯,关于 ATE 的核心内容就这些。记住,评估指标只是工具,真正重要的是理解你的 SLAM 系统在什么场景下表现好、什么场景下会出问题。下一节我们会聊相对轨迹误差(RPE),它和 ATE 是互补的关系。
一句话总结:ATE 回答「你估计的位置有多准」,RMSE 比均值更能反映最差情况,但永远不要只看一个数字。
可可精品资料库