SLAM 3.5:Sophus基础


SLAM 3.5 Sophus实践

前面理论讲述了很多,这里针对李群李代数部分做一个代码开发的补充

一、Sophus基础

前面配置环境的时候,我们就有安装一个Sophus库。李代数最早提出的是Sophus Lie,所以这个库就叫做Sophus了。

由于Eigen提供了很不错的几何模块,但没有提供李群李代数的支持,而这个Sophus库就支持。他是直接基于Eigen库开发的,所以不需要额外的依赖库了。

以下案例的头文件使用如下:

#include <iostream>
#include <cmath>
#include <Eigen/Core>
#include <Eigen/Geometry>
#include "sophus/se3.hpp"

using namespace std;
using namespace Eigen;
  • 声明一个旋转向量,并展示其旋转后的结果:
// 沿Z轴转90度的旋转矩阵
// 三维旋转向量可以用角轴来表示,利用Eigen库的AngleAxisd()角轴(旋转向量)函数
// 绕着单位方向向量\alpha = z轴(0,0,1)旋转模长为\theta = 90° = pi/2,
// 则\phi = \theta \alpha 再调用toRotationMatrix()函数从角轴表示的旋转向量转换为旋转矩阵
Matrix3d R = AngleAxisd(M_PI / 2, Vector3d(0, 0, 1)).toRotationMatrix();
// 或者四元数
Quaterniond q(R);
Sophus::SO3d SO3_R(R);              // Sophus::SO3d可以直接从旋转矩阵构造
Sophus::SO3d SO3_q(q);              // 也可以通过四元数构造
// 二者是等价的
cout << "SO(3) from matrix:\n" << SO3_R.matrix() << endl;
cout << "SO(3) from quaternion:\n" << SO3_q.matrix() << endl;
cout << "they are equal" << endl;

输出效果

SO(3) from matrix:
2.22045e-16          -1           0
          1 2.22045e-16           0
          0           0           1
SO(3) from quaternion:
2.22045e-16          -1           0
          1 2.22045e-16           0
          0           0           1
they are equal
  • 使用对数映射获得李代数
// 使用对数映射获得它的李代数
Vector3d so3 = SO3_R.log();
cout << "so3 = " << so3.transpose() << endl;
// hat 为向量到反对称矩阵
cout << "so3 hat=\n" << Sophus::SO3d::hat(so3) << endl;
// 相对的,vee为反对称到向量
cout << "so3 hat vee= " << Sophus::SO3d::vee(Sophus::SO3d::hat(so3)).transpose() << endl;

输出结果:

so3 =      0      0 1.5708
so3 hat=
      0 -1.5708       0
 1.5708       0      -0
     -0       0       0
so3 hat vee=      0      0 1.5708
  • 增量扰动模型的更新
// 增量扰动模型的更新
Vector3d update_so3(1e-4, 0, 0); //假设更新量为这么多
Sophus::SO3d SO3_updated = Sophus::SO3d::exp(update_so3) * SO3_R;
cout << "SO3 updated = \n" << SO3_updated.matrix() << endl;

输出结果:

SO3 updated = 
          0          -1           0
          1           0     -0.0001
     0.0001 2.03288e-20           1
  • 对SE(3)操作是类似的
// 对SE(3)操作大同小异
Vector3d t(1, 0, 0);           // 沿X轴平移1
Sophus::SE3d SE3_Rt(R, t);           // 从R,t构造SE(3)
Sophus::SE3d SE3_qt(q, t);            // 从q,t构造SE(3)
cout << "SE3 from R,t= \n" << SE3_Rt.matrix() << endl;
cout << "SE3 from q,t= \n" << SE3_qt.matrix() << endl;

// 李代数se(3) 是一个六维向量,方便起见先typedef一下
typedef Eigen::Matrix<double, 6, 1> Vector6d;
Vector6d se3 = SE3_Rt.log();
cout << "se3 = " << se3.transpose() << endl;

// 观察输出,会发现在Sophus中,se(3)的平移在前,旋转在后.

// 同样的,有hat和vee两个算符
cout << "se3 hat = \n" << Sophus::SE3d::hat(se3) << endl;
cout << "se3 hat vee = " << Sophus::SE3d::vee(Sophus::SE3d::hat(se3)).transpose() << endl;

输出效果:

SE3 from R,t= 
2.22045e-16          -1           0           1
          1 2.22045e-16           0           0
          0           0           1           0
          0           0           0           1
SE3 from q,t= 
2.22045e-16          -1           0           1
          1 2.22045e-16           0           0
          0           0           1           0
          0           0           0           1
se3 =  0.785398 -0.785398         0         0         0    1.5708
se3 hat = 
        0   -1.5708         0  0.785398
   1.5708         0        -0 -0.785398
       -0         0         0         0
        0         0         0         0
se3 hat vee =  0.785398 -0.785398         0         0         0    1.5708
  • 更新
// 最后,演示一下更新
Vector6d update_se3; //更新量
update_se3.setZero();
update_se3(0, 0) = 1e-4;
Sophus::SE3d SE3_updated = Sophus::SE3d::exp(update_se3) * SE3_Rt;
cout << "SE3 updated = " << endl << SE3_updated.matrix() << endl;

输出效果:

SE3 updated = 
2.22045e-16          -1           0      1.0001
          1 2.22045e-16           0           0
          0           0           1           0
          0           0           0           1

二、使用Sophus评估轨迹误差

实际工程中,需要评估一个算法的估计轨迹与真实轨迹的差异来评价算法的精度。

真实轨迹Ground Truth往往是通过某些更高精度的系统获得**,**

估计轨迹是通过待评价算法的计算得到。

如何计算两条轨迹的误差?


考虑一条估计轨迹$T_{esti,i}$和$T_{gt,i}$,其中i=1, … , N。那我们可以定义一些误差指标来描述它们之间的差别。

误差指标有很多种。

  1. 常见的绝对轨迹误差(Absolute Trajectory Error,ATE)

$$
\mathbf{ATE}_{all}=\sqrt{\frac{1}{N}\sum_{i=1}^{N}\left\| \log(T_{gt,i}^{-1}T_{esti,i}) \right\|_2^2}
$$

实际上是计算每个位姿李代数的均方根误差(Root-Mean-Squared Error,RMSE),可以刻画两条估计的旋转和平移误差。

  1. 考虑平移误差的,定义绝对平移误差(Absolute Translational Error)

$$
\mathbf{ATE}_{trans}=\sqrt{\frac{1}{N}\sum_{i=1}^{N}\left\| trans(T_{gt,i}^{-1}T_{esti,i}) \right\|_2^2}
$$

式子中tran 函数表示取括号中元素的平移部分。从整条轨迹上看,

  1. 还可以定义相对误差,仅考虑i时刻到i+△t时刻的运动,那么**相对位姿误差(Relative Pose Error,RPE)**可以定义为

$$
\mathbf{RPE}_{all}=\sqrt{\frac{1}{N-\Delta t}\sum_{i=1}^{N-\Delta t}\left\| \log\left( (T_{gt,i}^{-1}T_{gt,i+\Delta t})^{-1}(T_{esti,i}^{-1}T_{esti,i+\Delta t})\right)^\vee \right\|_2^2}
$$

  1. 同样的,也可以只取平移部分:

$$
\mathbf{RPE}_{trans}=\sqrt{\frac{1}{N-\Delta t}\sum_{i=1}^{N-\Delta t}\left\| trans\left( (T_{gt,i}^{-1}T_{gt,i+\Delta t})^{-1}(T_{esti,i}^{-1}T_{esti,i+\Delta t})\right)^\vee \right\|_2^2}
$$

使用Sophus李代数库可以实现这部分轨迹误差的评估。首先需要准备数据,这里使用SLAM十四讲源码中已经给出了真实轨迹 groundtruth.txt 和 估计轨迹 estimate.txt,下面读取两条轨迹数据后,通过Sophus计算轨迹间误差,然后显示在Pangolin窗口中。

所需前置定义的一些内容:

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <pangolin/pangolin.h>
#include <sophus/se3.hpp>

using namespace Sophus;
using namespace std;
//真实轨迹路径
string groundtruth_file = "./example/groundtruth.txt";
//估计轨迹路径
string estimated_file = "./example/estimated.txt";

void DrawTrajectory(const TrajectoryType &gt, const TrajectoryType &esti);
TrajectoryType ReadTrajectory(const string &path);

初始化SE(3)

TrajectoryType ReadTrajectory(const string &path) {
  ifstream fin(path);//打开轨迹数据文件
  TrajectoryType trajectory;
  if (!fin) {
    cerr << "trajectory " << path << " not found." << endl;
    return trajectory;
  }
  while (!fin.eof()) {
    // 轨迹数据txt文件中每一行表示由时间戳、平移向量、旋转四元数构成的一个相机wi位姿
    double time, tx, ty, tz, qx, qy, qz, qw;
    fin >> time >> tx >> ty >> tz >> qx >> qy >> qz >> qw;
    // 使用轨迹数据中的旋转四元数和平移向量来初始化一个特殊欧式群SE(3)
    Sophus::SE3d p1(Eigen::Quaterniond(qw, qx, qy, qz), Eigen::Vector3d(tx, ty, tz));
    // 将使用李群表示的当前相机位姿存入轨迹变量中
    trajectory.push_back(p1);
  }
  return trajectory;
}
  • 计算

    //定义一个特殊欧式群SE(3)的vector容器,并取个别名
    typedef vector<Sophus::SE3d, Eigen::aligned_allocator<Sophus::SE3d>> TrajectoryType;
    
    int main(int argc, char **argv) {
      // 获取轨迹数据
      TrajectoryType groundtruth = ReadTrajectory(groundtruth_file);
      TrajectoryType estimated = ReadTrajectory(estimated_file);
    
      // 检测读取的轨迹数据是否正常
      assert(!groundtruth.empty() && !estimated.empty());
      assert(groundtruth.size() == estimated.size());
    
      // 算每个位姿李代数的均方根误差 RMSE (Root Mean Squared Error)
      // 也就是绝对轨迹误差 ATE (Absolute Trajectory Error)
      double rmse = 0;
      for (size_t i = 0; i < estimated.size(); i++) {
        // 获取轨迹中每个相机当前的估计位姿和真实位姿数据
        Sophus::SE3d p1 = estimated[i], p2 = groundtruth[i];
    
        // 根据每个真实数据和估计数据之间的位姿李代数的均方根误差公式RMSE(也就是绝对轨迹误差计算公式ATE)
        // 真实位姿的变换矩阵T的逆与估计位姿的变换矩阵相乘,取对数映射计算位姿李代数并归一化
        double error = (p2.inverse() * p1).log().norm();
        // 累加轨迹中每个相机位姿李代数误差的平方
        rmse += error * error;
      }
      // 平均值
      rmse = rmse / double(estimated.size());
      // 根号
      rmse = sqrt(rmse);
      cout << "RMSE = " << rmse << endl;
    
      DrawTrajectory(groundtruth, estimated);
      return 0;
    }
  • 绘制Pangolin

    void DrawTrajectory(const TrajectoryType &gt, const TrajectoryType &esti) {
      // 创建一个名为Trajectory Viewer的3D可视化窗口,大小为1024 x 768
      pangolin::CreateWindowAndBind("Trajectory Viewer", 1024, 768);
      // 开启更新深度缓冲区的功能,显示3D效果的时候需要打开,根据目标的远近自动隐藏被遮挡的模型
      glEnable(GL_DEPTH_TEST);
      // 开启颜色混合模式,让物体显示具有半透明效果
      glEnable(GL_BLEND);
      // 设置颜色RGBA混合因子,第一个参数为源因子,第二个参数为目标因子
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
      // 创建相机视图
      pangolin::OpenGlRenderState s_cam(
          //设置相机内参(相机分辨率、焦距、相机光心)以及最近最远距离
          pangolin::ProjectionMatrix(1024, 768, 500, 500, 512, 389, 0.1, 1000),
          // 设置视角,在世界坐标(0, -0.1, -1.8)处观察坐标原点(0, 0, 0)
          // 并设置Y轴朝上(0, -1, 0)
          pangolin::ModelViewLookAt(0, -0.1, -1.8, 0, 0, 0, 0.0, -1.0, 0.0)
      );
      // 创建交互视图
      pangolin::View &d_cam = pangolin::CreateDisplay()
          .SetBounds(0.0, 1.0, pangolin::Attach::Pix(175), 1.0, -1024.0f / 768.0f)
          .SetHandler(new pangolin::Handler3D(s_cam));
    
      while (pangolin::ShouldQuit() == false) {
        // 清空颜色和深度缓存,为默认值
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // 激活相机视图
        d_cam.Activate(s_cam);
        // 设置背景颜色为白色
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        // 设置线条宽度为2
        glLineWidth(2);
        for (size_t i = 0; i < gt.size() - 1; i++) {
          //设置真实轨迹的线段颜色为蓝色
          glColor3f(0.0f, 0.0f, 1.0f); 
          //开始画线
          glBegin(GL_LINES);
          //获取真实轨迹中第 i 时刻和第 i+1 时刻用李群SE(3)表示的位姿
          auto p1 = gt[i], p2 = gt[i + 1];
          //取出第 i 时刻位姿的平移向量,作为该线段起点的顶点坐标
          glVertex3d(p1.translation()[0], p1.translation()[1], p1.translation()[2]);
          //取出第 i+1 时刻位姿的平移向量,作为该线段终点的顶点坐标
          glVertex3d(p2.translation()[0], p2.translation()[1], p2.translation()[2]);
          //画线结束
          glEnd();
        }
    
        //遍历估计轨迹中的所有位姿数据
        for (size_t i = 0; i < esti.size() - 1; i++) {
          //设置估计的轨迹线段为红色
          glColor3f(1.0f, 0.0f, 0.0f); 
          //开始画线
          glBegin(GL_LINES);
          //获取估计轨迹中第 i 时刻和第 i+1 时刻用李群SE(3)表示的位姿
          auto p1 = esti[i], p2 = esti[i + 1];
          //取出第 i 时刻位姿的平移向量,作为该线段起点的顶点坐标
          glVertex3d(p1.translation()[0], p1.translation()[1], p1.translation()[2]);
          //取出第 i+1 时刻位姿的平移向量,作为该线段终点的顶点坐标
          glVertex3d(p2.translation()[0], p2.translation()[1], p2.translation()[2]);
          //画线结束
          glEnd();
        }
        //交换帧和处理事件
        pangolin::FinishFrame();
        //睡眠5毫秒
        usleep(5000);
      }
    }

    效果:


文章作者: 拓佑豪
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 拓佑豪 !
评论
  目录