李代数:与李群对应的一种的结构,位于向量空间,通常记作so(3),se(3).
具体李群与李代数基础可参考博客 SLAM学习——李群与李代数
理论推导部分可参考博客:视觉SLAM十四讲CH4代码解析及课后习题详解
(这里需要的注意点,模板库与非模板库调用报错问题:可能出现问题的有:头文件引用)
编译运行结果如下:
结果里多了两行输出:
update_so3=
0.0001 0 0
update_se3=
0.0001 0 0 0 0 0
这是因为我不确定下面两行代码的功能,所以多输出了两行:
// 增量扰动模型的更新
Vector3d update_so3(1e-4, 0, 0); //假设更新量为这么多
Sophus::SO3 SO3_updated = Sophus::SO3::exp(update_so3) * SO3_R;//指数映射
cout << "update_so3= " << endl << update_so3.transpose() << endl;
cout << "SO3 updated = \n" << SO3_updated.matrix() << endl;
Vector6d update_se3; //更新量
update_se3.setZero();
update_se3(0, 0) = 1e-4;//第一项为0.0001,(0.0001,0,0,0,0,0,0),想知道这行代码作用
Sophus::SE3 SE3_updated = Sophus::SE3::exp(update_se3) * SE3_Rt;//左乘更新
cout << "update_se3= " << endl << update_se3.transpose() << endl;
cout << "SE3 updated = " << endl << SE3_updated.matrix() << endl;
代码及注释如下:
#include
#include
#include
#include
#include "sophus/se3.h"
using namespace std;
using namespace Eigen;
/// 本程序演示sophus的基本用法
int main(int argc, char **argv) {
// 沿Z轴转90度的旋转矩阵
Matrix3d R = AngleAxisd(M_PI / 2, Vector3d(0, 0, 1)).toRotationMatrix();
// 或者四元数
Quaterniond q(R);
Sophus::SO3 SO3_R(R); // Sophus::SO3d可以直接从旋转矩阵构造
Sophus::SO3 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;
// 使用对数映射获得它的李代数
Vector3d so3 = SO3_R.log();
cout << "so3 = " << so3.transpose() << endl;
// hat 为向量到反对称矩阵
cout << "so3 hat=\n" << Sophus::SO3::hat(so3) << endl;
// 相对的,vee为反对称到向量
cout << "so3 hat vee= " << Sophus::SO3::vee(Sophus::SO3::hat(so3)).transpose() << endl;
// 增量扰动模型的更新
Vector3d update_so3(1e-4, 0, 0); //假设更新量为这么多
Sophus::SO3 SO3_updated = Sophus::SO3::exp(update_so3) * SO3_R;//指数映射
cout << "update_so3= " << endl << update_so3.transpose() << endl;
cout << "SO3 updated = \n" << SO3_updated.matrix() << endl;
cout << "*******************************" << endl;
// 对SE(3)操作大同小异
Vector3d t(1, 0, 0); // 沿X轴平移1
Sophus::SE3 SE3_Rt(R, t); // 从R,t构造SE(3)
Sophus::SE3 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::SE3::hat(se3) << endl;
cout << "se3 hat vee = " << Sophus::SE3::vee(Sophus::SE3::hat(se3)).transpose() << endl;
// 最后,演示一下更新
Vector6d update_se3; //更新量
update_se3.setZero();
update_se3(0, 0) = 1e-4;//第一项为0.0001,(0.0001,0,0,0,0,0,0)
Sophus::SE3 SE3_updated = Sophus::SE3::exp(update_se3) * SE3_Rt;//左乘更新
cout << "update_se3= " << endl << update_se3.transpose() << endl;
cout << "SE3 updated = " << endl << SE3_updated.matrix() << endl;
return 0;
}
注意
:文件存储路径
已经采集好的数据存储在了两个.txt文件中,在plotTrajectory.cpp文件中,源代码如下:
string groundtruth_file = "./example/groundtruth.txt";
string estimated_file = "./example/estimated.txt";
因为我们创建了build
目录,以便把编译生成的文件都放到这里,所以我们的相对路径也有了一些变化将上述两行代码改为文件绝对路径就可以运行了:(备注:下面是我自己的路径,在自己的代码中需要替换成自己电脑文件存储路径)
string groundtruth_file = "/home/lmf37/桌面/slambook2/ch4/example/groundtruth.txt";
string estimated_file = "/home/lmf37/桌面/slambook2/ch4/example/estimated.txt";
或者是
string groundtruth_file = "/home/lmf37/桌面/slambook2/ch4/example/groundtruth.txt";
string estimated_file = "../example/estimated.txt";
关于目录补充内容:
当前级:./(
上级:…/
上上级:…/…/
上上上级:…/…/…/
上上上上级:…/…/…/…/
代码及注释如下:
#include
#include
#include
#include
#include
using namespace Sophus;
using namespace std;
string groundtruth_file = "/home/lmf37/桌面/slambook2/ch4/example/groundtruth.txt";
string estimated_file = "/home/lmf37/桌面/slambook2/ch4/example/estimated.txt";
typedef vector<Sophus::SE3, Eigen::aligned_allocator<Sophus::SE3>> TrajectoryType;
void DrawTrajectory(const TrajectoryType >, const TrajectoryType &esti);
TrajectoryType ReadTrajectory(const string &path);
int main(int argc, char **argv) {
TrajectoryType groundtruth = ReadTrajectory(groundtruth_file); //读取groundtruth_file
TrajectoryType estimated = ReadTrajectory(estimated_file);//读取estimated_file
assert(!groundtruth.empty() && !estimated.empty()); //assert()为断言函数。如果它的条件返回错误,则终止程序执行!
assert(groundtruth.size() == estimated.size());
// compute rmse
//计算绝对轨迹误差,即为李代数的均方根误差
double rmse = 0;
for (size_t i = 0; i < estimated.size(); i++) {
Sophus::SE3 p1 = estimated[i], p2 = groundtruth[i];//p1为estimated_file中的值(为估计轨迹T(esti,i)) p2为groundtruth_file中的值(为真实轨迹T(gt,i))
//error = (p2.inverse() * p1).log().norm();为视觉slam十四讲p89的4.44式ATE(all)
double error = (p2.inverse() * p1).log().norm();//n = norm(v) 返回向量 v 的欧几里德范数
rmse += error * error;
}
rmse = rmse / double(estimated.size());//rmse / 估计轨迹的点数
rmse = sqrt(rmse);//开根
cout << "RMSE = " << rmse << endl; //输出RMSE
DrawTrajectory(groundtruth, estimated);
return 0;
}
TrajectoryType ReadTrajectory(const string &path) {
ifstream fin(path);
TrajectoryType trajectory;
if (!fin) { //如果文件打开失败,则执行
cerr << "trajectory " << path << " not found." << endl;//cerr是标准错误流。该流中的信息只能输出到显示器上,而不能输出到文件中。该流中信息不经过缓冲区,直接输出至显示器。
return trajectory;
}
while (!fin.eof()) { //当fin并未指向EOF时,则执行
double time, tx, ty, tz, qx, qy, qz, qw; //time, 平移向量,四元数
fin >> time >> tx >> ty >> tz >> qx >> qy >> qz >> qw;
Sophus::SE3 p1(Eigen::Quaterniond(qw, qx, qy, qz), Eigen::Vector3d(tx, ty, tz));
trajectory.push_back(p1);
}
return trajectory;
}
void DrawTrajectory(const TrajectoryType >, const TrajectoryType &esti) {//此部分代码释义可参考ch3-plotTrajectory.cpp
// create pangolin window and plot the trajectory
pangolin::CreateWindowAndBind("Trajectory Viewer", 1024, 768);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
pangolin::OpenGlRenderState s_cam(
pangolin::ProjectionMatrix(1024, 768, 500, 500, 512, 389, 0.1, 1000),
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));
//检测是否关闭OpenGL窗口
while (pangolin::ShouldQuit() == false) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//清空颜色和深度缓存,每次都会刷新显示,不至于前后帧的颜信息相互干扰。
d_cam.Activate(s_cam); //激活显示并设置状态矩阵,以下代码到Finish是显示内容
glClearColor(1.0f, 1.0f, 1.0f, 1.0f); //设置背景颜色为白色
// 画出连线
glLineWidth(2); //设置线条宽度为2
for (size_t i = 0; i < gt.size() - 1; i++) {
glColor3f(0.0f, 0.0f, 1.0f); // blue for ground truth,真实值,颜色设置为蓝色
glBegin(GL_LINES);
auto p1 = gt[i], p2 = gt[i + 1];
glVertex3d(p1.translation()[0], p1.translation()[1], p1.translation()[2]);
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); // red for estimated,估算数据 ,颜色设置为红色
glBegin(GL_LINES);
auto p1 = esti[i], p2 = esti[i + 1];
glVertex3d(p1.translation()[0], p1.translation()[1], p1.translation()[2]);
glVertex3d(p2.translation()[0], p2.translation()[1], p2.translation()[2]);
glEnd();
}
pangolin::FinishFrame();//执行后期渲染,事件处理和帧交换,进行最终的显示
usleep(5000); // sleep 5 ms
}
}
1.李哈哈视觉SLAM十四讲源码的正确打开方式:第4讲 李群和李代数
2.SLAM学习——李群与李代数
3.视觉SLAM十四讲CH4代码解析及课后习题详解