相对位姿误差主要描述的是相隔固定时间差 Δ \Delta Δ两帧位姿差的精度(相比真实位姿),相当于直接测量里程计的误差。
因此第 i i i帧的RPE定义如下:
E i : = ( Q − 1 Q i + Δ ) − 1 ( P i − 1 P i + Δ ) E_i:=(Q^{-1}Q_{i+\Delta})^{-1}(P_i^{-1}P_{i+\Delta}) Ei:=(Q−1Qi+Δ)−1(Pi−1Pi+Δ)
已知总数 n n n与间隔 Δ \Delta Δ的情况下,可以得到 m = n − Δ m=n-\Delta m=n−Δ个RPE,然后我们可以用均方根误差RMSE统计这个误差,得到一个总体值:
RMSE ( E 1 : n , Δ ) : = ( 1 m ∑ i = 1 m ∥ t r a n s ( E i ) ∥ 2 ) 1 2 \text{RMSE}(E_{1:n},\Delta):=(\frac1m \sum_{i=1}^m \rVert trans(E_i) \Vert ^2)^{\frac12} RMSE(E1:n,Δ):=(m1i=1∑m∥trans(Ei)∥2)21
其中 t r a n s ( E i ) trans(E_i) trans(Ei)代表取相对位姿误差中的平移部分translation。当然也有人不用RMSE,直接使用平均值、甚至中位数来描述相对误差情况。
需要注意的是,RPE包含了两部分的误差,分别是旋转误差和平移误差,通常使用平移误差进行评价已经足够,但是如果需要,旋转角的误差也可以使用相同的方法进行统计。
到这一步,我们基本可以从RMSE值的大小来评价算法的表现,然而实际情况中,我们发现对 Δ \Delta Δ的选取有多种选择,为了能综合衡量算法表现,我们可以计算遍历所有 Δ \Delta Δ的RMSE的平均值:
RMSE ( E 1 : n ) : = 1 n ∑ Δ = 1 n RMSE ( E 1 : n , Δ ) . \text{RMSE}(E_{1:n}):=\frac1n\sum_{\Delta=1}^n \text{RMSE}(E_{1:n},\Delta). RMSE(E1:n):=n1Δ=1∑nRMSE(E1:n,Δ).
但这样新的问题又出现了,这样的计算复杂度非常高,很耗时间,因此TUM在自己给定的工具中,通过计算固定数量的RPE样本计算一个估计值作为最终结果。
绝对轨迹误差是估计位姿和真实位姿的直接差值,可以非常直观地反应算法精度和轨迹全局一致性。需要注意的是,估计位姿和groundtruth通常不在同一坐标系中,因此我们需要先将两者对其:对于双目SLAM和RGB-D SLAM,尺度统一,因此我们需要通过最小二乘法计算一个从估计位姿到真实位姿的转换矩阵 S ∈ S E ( 3 ) \text{S}\in SE(3) S∈SE(3);对于单目相机,具有尺度不确定性,我们需要计算一个从估计位姿到真实位姿的相似转换矩阵 S ∈ S i m ( 3 ) \text{S}\in Sim(3) S∈Sim(3)。
因此第 i i i帧的ATE定义如下:
F i : = Q i − 1 S P i F_i:=Q_i^{-1}SP_i Fi:=Qi−1SPi
与RPE相似,建议使用RMSE统计ATE
RMSE ( F 1 : n , Δ ) : = ( 1 m ∑ i = 1 m ∥ t r a n s ( F i ) ∥ 2 ) 1 2 \text{RMSE}(F_{1:n},\Delta):=(\frac1m \sum_{i=1}^m \rVert trans(F_i) \Vert ^2)^{\frac12} RMSE(F1:n,Δ):=(m1i=1∑m∥trans(Fi)∥2)21
当然,使用平均值、中位数等来反应ATE亦可,现在很多evaluation工具会将RMSE、Mean、Median都给出。
综上,我们需要注意的是,RPE误差包含了translation和rotation两部分的误差,而ATE只包含translation的误差(从二者的全称中也可以看出),二者具有强烈的相关性,却也不尽相同。我们仍需结合实际,选择合适的指标进行算法评价。
这里推荐一个方便全能的evaluation工具。这里贴出github链接
EVO:Python package for the evaluation of odometry and SLAM
系统:ubuntu,首先确保系统安装了Python,然后根据需要可以选择性安装**PyQt5 / PyQt4 **和 ROS,当然, 不装也不会影响基本功能。
直接从pip安装,不需要查看源代码。
pip install evo --upgrade --no-binary evo
源代码安装
git clone [email protected]:MichaelGrupp/evo.git
cd evo
pip install --editable . --upgrade --no-binary evo
输入下面这个命令,如果成功会有响应。
evo_ate -h
可以先用evo仓库中自带的数据尝试
cd test/data
evo_traj kitti KITTI_00_ORB.txt KITTI_00_SPTAM.txt --ref=KITTI_00_gt.txt -p --plot_mode=xz
evo_ape kitti KITTI_00_gt.txt KITTI_00_ORB.txt -va --plot --plot_mode xz
evo_rpe tum fr2_desk_groundtrutr2_desk_ORB.txt -va --plot --plot_mode xyz
参数较多,每个命令都有比较详细的帮助,善用 --help,如:evo_ape --help
这里列举了一些常见选项的含义:
选项 | 含义 |
---|---|
-a, --align | 轨迹对齐 |
-s, --correct_scale | 尺度对齐 |
–sync | 通过时间戳关联轨迹 |
-p, --plot | 可视化轨迹 |
-v, --verbose | 命令号输出细节信息 |
–plot_mode | 轨迹可视化模式:xyz, xy, xz等 |
–save_plot | 保存plot结果 |
上文提到pose error包含平移和旋转两部分误差,在evo中的option为-r,–pose_relation,并有如下模式:
-r {…} | 功能 |
---|---|
full | 同时考虑旋转与平移,无单位。 |
trans_part | 平移误差,单位m。(默认) |
rot_part | 旋转误差,无单位。 |
angle_deg | 旋转误差,单位degree。 |
angle_rad | 旋转误差,单位弧度rad。 |
其中trans_part很容易理解,但是无单位的full和rot_part的误差指标比较特殊,使用了矩阵的二范数:
e r r o r f u l l = ∥ δ T i − E ∥ error_{full}=\Vert{\delta T_i-E\Vert} errorfull=∥δTi−E∥
其中KaTeX parse error: Undefined control sequence: \deltaT at position 1: \̲d̲e̲l̲t̲a̲T̲_i表示groundtruth和estimate位姿之间的变换矩阵, E E E为单位阵,矩阵的范数也可以表示某种抽象的空间距离,从而用来度量误差(仅供参考,求赐教包括为什么要减去单位阵)。
同理rot_part的误差:
e r r o r r o t = ∥ δ R i − E ∥ error_{rot}=\Vert{\delta R_i-E\Vert} errorrot=∥δRi−E∥
相比较而言,用平移误差单位为m,旋转误差单位可以是度或弧度,更容易理解。从旋转矩阵 R R R到旋转角的转换使用了罗德里格斯公式(旋转向量的二范数为旋转角的大小)。
R = cos θ I + ( 1 − cos θ ) n n T + sin θ n ∧ R=\cos{\theta I}+(1-\cos{\theta})nn^T+\sin{\theta}n^{\wedge} R=cosθI+(1−cosθ)nnT+sinθn∧
从而:
θ = arccos ( t r a c e R − 1 2 ) \theta =\arccos(\frac{trace{R}-1}{2}) θ=arccos(2traceR−1)
#附EVO metric的相关代码
if self.pose_relation == PoseRelation.translation_part:
# E is an array of position vectors only in this case
self.error = [np.linalg.norm(E_i) for E_i in self.E]
elif self.pose_relation == PoseRelation.rotation_part:
self.error = np.array([
np.linalg.norm(lie.so3_from_se3(E_i) - np.eye(3))
for E_i in self.E])
elif self.pose_relation == PoseRelation.full_transformation:
self.error = np.array(
[np.linalg.norm(E_i - np.eye(4)) for E_i in self.E])
elif self.pose_relation == PoseRelation.rotation_angle_rad:
self.error = np.array(
[abs(lie.so3_log(E_i[:3, :3])) for E_i in self.E])
elif self.pose_relation == PoseRelation.rotation_angle_deg:
self.error = np.array([
abs(lie.so3_log(E_i[:3, :3])) * 180 / np.pi for E_i in self.E
])
else:
raise MetricsException("unsupported pose_relation")