slam学习笔记(1)

本文为高翔博士slam14讲学习笔记

旋转向量与旋转矩阵转化:

四元数表示的旋转:

三维旋转可以由单位四元数来描述,四元数也具有实部和虚部,类似于二维平面中的复数

用eigen包可以进行所有不同类型数之间的转化

群:集合加代数运算结构,G=(A,.)

李群:一种光滑连续的群

李代数:由一个集合,一个数域,一个二元运算(也称为李括号)组成 g=(R^3,R,x),构成一个李代数

李代数表示在李群某一位置的导数特征

李群李代数转换,指数映射与对数映射

对于so,指数映射值罗德里格斯公式

BCH近似:

 左乘微小旋转,李群上的相乘等于李代数上的变换相加

实际中常见的例子

构建有关位姿的导数:

1.李代数表示位姿,通过李代数加法求导(因为求导是要依赖于减法运算)

2.扰动模型

SE(3)上的李代数求导:

 

 利用sophus,可以求解李代数相关的问题,利用Sophus::SO3d,从旋转矩阵或四元数构造李群

.matrix获取矩阵值,.log获取李代数

hat -> ^ ,vee -> ,exp直接转化为旋转矩阵,log直接旋转矩阵转化为向量

图像与相机

像素坐标与世界坐标的转化:

 相机的归一化平面是Z=1情况下的平面

双目相机成像模型:

 

 RGB-D相机的使用局限:
红外光深度测量相机,易受日光和其他红外光的干扰,没有调制的情况下,无法使用多个RGB-D相机,对于透材和不易反射的物质,接受 不到反射光线

opencv图像操作:

图像数据在内存中的存储:

unsigned char image[480][640],注意与像素坐标中的x,y刚好是相反的
#include<opencv2/core/core.hpp>

#include<opencv2/highgui/highgui.hpp>

cv::imread  image::cols  image::rows  image::channels

cv::waitKey(0) 程序暂停,等待输入

 opencv图像数据访问:
row_ptr = image.ptr<unsigned char>(y)  row_ptr[0]  or

image.ptr<unsigned char>(y) [x]

image.clone(),深拷贝数据,完全独立的拷贝

对规律命名图片的批量拷贝:定义格式函数

boost::format fmt("./%s/%d.%s")

读取时对int变量更新,覆盖%d函数的位置

非线性优化:
核心方程 

 第一个为运动方程,第二个为观测方程,u表示所有时刻的输入,z表示所有时刻的观测

对机器人状态的估计,从概率学角度来看

x为机器人位姿,y为机器人路标点(landmark)

对于sfm问题,只有观测z,没有输入u

 

 直接求后验分布很困难,后验变似然加先验

 求最大后验概率等价于求最大化似然和先验的乘积,在没有先验的情况下,可以求最大似然估计

上述公式中:已知道观测数据z和输入数据u(通常由IMU输入),可理解为在什么样的状态下,最可能产生现在观测到的数据 

在高斯分布下,最大似然能够有较简单的形式:求解最大似然估计标准操作,求解概率密度函数的负对数

概念补充:

 

 中间矩阵为高斯分布协方差矩阵的逆

进一步总结:

因为要同时优化多个项,每个项都仅由少数几个变量来控制,同时z和u是相互独立的,由此可得如下的式子:

 表述每一项的意义:
已知u,x(k-1)和x(k)概率上最有可能的差距,已知z,最有可能当下的位姿和路标点,

故由此可以定义误差:

 上上个公式,是服从于高斯分布的,连乘取对数,可以变为相加的形式,每一项都是相对简单的变量,使上述公式最大化,由高斯分布的性质可得,相当于求:

 Q为信息矩阵,高斯分布协方差矩阵之逆

 非线性最小二乘问题的数值求解:

典型代表,牛顿法和最速下降法,分别为泰勒二阶近似和泰勒一阶近似

两种方法各有优缺点,最速下降法过于贪心,容易走锯齿形状路线,而牛顿法又H矩阵计算较为麻烦,在slam问题中,我们探究最小二乘问题的优化,主要使用高斯牛顿和列文伯格-马夸尔特

牛顿法和高斯牛顿法,LM原理及对比牛顿法和高斯牛顿法对比_猪猪侠的猪猪女孩的博客-程序员宝宝_牛顿法 高斯牛顿法 - 程序员宝宝 (cxybb.com)

LM更加鲁棒,当slam接近于病态问题时,采用LM算法效果较好,但是速度会有所折扣

Ceres曲线拟合:

使用条件:1.定义每一个参数块 2.定义残差块的计算方式 3.定义残差块雅可比计算方式(可选择)4.参数块和残差块加入Ceres定义的 Problem对象中,调用solve函数求解

g2o曲线拟合:节点为优化变量,边为误差项

g2o::BaseUnaryEdge:(5条消息) g2o源码分析之BaseBinaryEdge_肥鼠路易的博客-CSDN博客https://blog.csdn.net/weixin_44991673/article/details/117250252

BaseUnaryEdge<D,E>,D为测量值,E为测量值的数据类型

g2o::BaseVertex:(5条消息) g2o源码分析之BaseVertex_肥鼠路易的博客-CSDN博客

<D,E> D代表节点的维度,E代表存储该节点的数据类型

g2o 小结 - 知乎 (zhihu.com)

typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType; // 每个误差项优化变量维度为3,误差值维度为1

对于slam问题,带有许多个相机位姿和观测点,g2o提供大量的边和节点,较为方便。而同样使用Ceres,还需要定义一个损失函数,比较麻烦。

实践章节:

视觉里程计

前端(即视觉里程计),粗略估计相机的运动,给后端优化提供较好的初始值。

特征点法和直接法两种

路标:图像特征,一个图像的特征由关键点和描述子组成

关键点匹配,采用快速近似最近邻

实际操作:

 单目相机:
2D-2D,对极几何,针对对极几何的情况

 

 与a^相乘相当于做差集

 x1,x2是归一化平面上的点(相机坐标系),计算方面通常用更加简洁的本质矩阵E来定义,p1和p2是图像坐标系上的点

1.E是由对极约束来定义的,乘以任意非零系数后,对极约束仍然满足,不同尺度下是等价的

2.由t和R得到,自由度为6

综合以上两点,得到E的自由度为5 (5条消息) 3月12日 对极几何,本征矩阵,基础矩阵,F/E矩阵计算,恢复旋转与平移,三角化视图重建_Hali_Botebie的博客-CSDN博客

通过八点法计算其值,然后进行奇异值分解,得到相应的R和T

单应矩阵:用于刻画共面的情形

 

 单应矩阵自由度为8,相比较于本质矩阵,自由度增加,在相机纯旋转时,本质矩阵自由度退化,多余自由度由数据噪声来决定,所以通常同时计算单应矩阵和本质矩阵,选择重投影误差较小的值来计算。

单目运算缺点:本质矩阵分解得到的t是具有尺度的,常数恒成立性

由于E本身具有尺度等价性,运动构建也具有尺度不变性,用首两张图片的平移来得到t,并以其为单位一来进行后续计算(初始化方法1)

特征点深度均值为1(初始化方法2),控制整个地图的规模

仅有纯旋转的情况下,导致t为零,那么E也为零,无法计算R,但可以通过H矩阵来求旋转,但是特征点的空间位置我们无法得到

左右平移进行较好的初始化,运动不能太小,否则结果会很不稳定

 RANSAC YYDS

三角测量:

三角测量,平移得到的,纯旋转是无法得到三角测量形状,进而测量深度的,机器人纯旋转效果很不好

提高精度的话:(1)提高相机分辨率,降低单个像素的尺寸 (2)t增大,图像变换较大,导致交集很少,容易丢失信息

代码实现: (1)函数参数传递多以引用为主,减少内存分配,加快速度

                   (2)m:matchs,m.queryIdx,m.trainIdx,分别指向两对成功匹配点的索引,用于从关键点vector中提取成功匹配的坐标

                  (3)keypoint(vector).pt,可以得到该关键点的坐标

3D-2D: PNP

可以用于表示世界坐标系三维点和相机坐标系中的位姿关系,也可以表示两个坐标系之间的关系

已知点的像素坐标及其深度:求解相机旋转和平移,变换矩阵

直接线性变换(DLT):

 

 需要至少六对点才能得到数值解

p3p也是一种求解pnp问题的方法,仅需要三对点就可以得到函数的解,鲁棒性不强

BA(对初步得到的相机位姿和空间点进行优化) 核心,重投影误差

先估计初步值,在通过BA进行优化,得到较为精确位姿估计值

重投影误差示意图

 

 至此,雅可比矩阵解析形式即可得到

编程求解:深度图如何转化为三维坐标:

需要知道3d坐标点和相对应的变换后的2D坐标点

    Point2d p1 = pixel2cam(keypoints_1[m.queryIdx].pt, K);
    pts_3d.push_back(Point3f(p1.x * dd, p1.y * dd, dd));

solvePnP(pts_3d, pts_2d, K, Mat(), r, t, false);

cv::Rodrigues(r, R); // r为旋转向量形式,用Rodrigues公式转换为矩阵

 手写高斯牛顿法实现非线性优化:核心步骤

H = J *J.T  b = J * e

Eigen::Vector3d pc = pose * points_3d[i]; //转化为相机坐标系

Eigen::Vector2d proj(fx * pc[0] / pc[2] + cx, fy * pc[1] / pc[2] + cy);//转化为像素坐标系

Eigen::Vector2d e = points_2d[i] - proj;//计算误差,误差为二维

cost += e.squaredNorm();//计算损失

Eigen::Matrix<double, 2, 6> J;//雅可比矩阵的解析计算

H += J.transpose() * J;
b += -J.transpose() * e;

dx = H.ldlt().solve(b);

pose = Sophus::SE3d::exp(dx) * pose;//参数更新,指数化dx

使用g2o优化:

BaseVertex<优化量的个数,优化量的类型>

BaseUnaryEdge<计算结果和对比值得维度,上面定义得vertext类型>

/// vertex and edges used in g2o ba
/// 定义顶点和边
class VertexPose : public g2o::BaseVertex<6, Sophus::SE3d> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;

  virtual void setToOriginImpl() override {
    _estimate = Sophus::SE3d();//估计值的初始化
  }

  /// left multiplication on SE3
  virtual void oplusImpl(const double *update) override {
    Eigen::Matrix<double, 6, 1> update_eigen;
    update_eigen << update[0], update[1], update[2], update[3], update[4], update[5];
    _estimate = Sophus::SE3d::exp(update_eigen) * _estimate;//估计值的更新
  }

  virtual bool read(istream &in) override {}

  virtual bool write(ostream &out) const override {}
};

class EdgeProjection : public g2o::BaseUnaryEdge<2, Eigen::Vector2d, VertexPose> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;

  EdgeProjection(const Eigen::Vector3d &pos, const Eigen::Matrix3d &K) : _pos3d(pos), _K(K) {}//不需要定义,可直接使用

  virtual void computeError() override {
    const VertexPose *v = static_cast<VertexPose *> (_vertices[0]);//创建顶点对象
    Sophus::SE3d T = v->estimate();//返回当前估计值
    Eigen::Vector3d pos_pixel = _K * (T * _pos3d);
    pos_pixel /= pos_pixel[2];
    _error = _measurement - pos_pixel.head<2>();//计算误差
  }

  //计算雅可比矩阵,数值法或者解析法
  virtual void linearizeOplus() override {
    const VertexPose *v = static_cast<VertexPose *> (_vertices[0]);
    Sophus::SE3d T = v->estimate();
    Eigen::Vector3d pos_cam = T * _pos3d;
    double fx = _K(0, 0);
    double fy = _K(1, 1);
    double cx = _K(0, 2);
    double cy = _K(1, 2);
    double X = pos_cam[0];
    double Y = pos_cam[1];
    double Z = pos_cam[2];
    double Z2 = Z * Z;
    _jacobianOplusXi
      << -fx / Z, 0, fx * X / Z2, fx * X * Y / Z2, -fx - fx * X * X / Z2, fx * Y / Z,
      0, -fy / Z, fy * Y / (Z * Z), fy + fy * Y * Y / Z2, -fy * X * Y / Z2, -fy * X / Z;
  }

  virtual bool read(istream &in) override {}

  virtual bool write(ostream &out) const override {}

private:
  Eigen::Vector3d _pos3d;
  Eigen::Matrix3d _K;
};
//g2o图优化问题求解

void bundleAdjustmentG2O(
  const VecVector3d &points_3d,
  const VecVector2d &points_2d,
  const Mat &K,
  Sophus::SE3d &pose) {

  // 构建图优化,先设定g2o
  typedef g2o::BlockSolver<g2o::BlockSolverTraits<6, 3>> BlockSolverType;  // pose is 6, landmark is 3
  typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // 线性求解器类型
  // 梯度下降方法,可以从GN, LM, DogLeg 中选
  auto solver = new g2o::OptimizationAlgorithmGaussNewton(
    g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
  g2o::SparseOptimizer optimizer;     // 图模型
  optimizer.setAlgorithm(solver);   // 设置求解器
  optimizer.setVerbose(true);       // 打开调试输出

  // vertex
  VertexPose *vertex_pose = new VertexPose(); // camera vertex_pose
  vertex_pose->setId(0);
  vertex_pose->setEstimate(Sophus::SE3d());
  optimizer.addVertex(vertex_pose);

  // K
  Eigen::Matrix3d K_eigen;
  K_eigen <<
          K.at<double>(0, 0), K.at<double>(0, 1), K.at<double>(0, 2),
    K.at<double>(1, 0), K.at<double>(1, 1), K.at<double>(1, 2),
    K.at<double>(2, 0), K.at<double>(2, 1), K.at<double>(2, 2);

  // edges
  int index = 1;
  for (size_t i = 0; i < points_2d.size(); ++i) {
    auto p2d = points_2d[i];
    auto p3d = points_3d[i];
    EdgeProjection *edge = new EdgeProjection(p3d, K_eigen);
    edge->setId(index);
    edge->setVertex(0, vertex_pose);
    edge->setMeasurement(p2d);
    edge->setInformation(Eigen::Matrix2d::Identity());
    optimizer.addEdge(edge);
    index++;
  }

  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
  optimizer.setVerbose(true);
  optimizer.initializeOptimization();
  optimizer.optimize(10);
  chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
  chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
  cout << "optimization costs time: " << time_used.count() << " seconds." << endl;
  cout << "pose estimated by g2o =\n" << vertex_pose->estimate().matrix() << endl;
  pose = vertex_pose->estimate();
}

g2o代码阅读 高翔Slambook第七讲:3d2d非线性优化 - 云+社区 - 腾讯云 (tencent.com)

g2o中定义g2o::BlockSolverTraits<p,l>的时候,p和l要如何设置? - 知乎 (zhihu.com)

从零开始一起学习SLAM | 掌握g2o边的代码套路 - 知乎 (zhihu.com)

总结:一元边,<优化变量维度,值得维度>

二元边,<优化变量维度,另一端优化变量维度>

BA优化可以对整个slam系统进行优化,大规模计算主要用于后端,对于前端,主要考虑局部范围内得小型BA优化,实时求解(提供初值)并进行优化

Eigen的vector取头尾或切片

获取向量的前n个元素:vector.head(n);
获取向量尾部的n个元素:vector.tail(n);
获取从向量的第i个元素开始的n个元素:vector.segment(i,n);

3D-3D :ICP

同样具有两种方法:利用线性代数求解(SVD),和利用非线性优化得解

误差:

 矩阵的秩:

矩阵的秩是线性代数中的一个概念。在线性代数中,一个矩阵A列秩A线性独立纵列的极大数,通常表示为r(A),rk(A)或rank A。 [1] 

在线性代数中,一个矩阵A的列秩是A的线性独立的纵列的极大数目。类似地,行秩是A的线性无关的横行的极大数目。即如果把矩阵看成一个个行向量或者列向量,秩就是这些行向量或者列向量的秩,也就是极大无关组中所含向量的个数。

满秩矩阵是指最大秩,是矩阵是否可逆的重要标志

SVD分解相关知识:SVD-矩阵奇异值分解 —— 原理与几何意义 - 知乎 (zhihu.com)

非线性优化:

目标函数:

 李代数扰动求导:

ICP 问题存在无穷多和唯一解,唯一解的情况下,不用寻找初始值,随便取值即可 

匹配已知的情况下,有解析解,迭代解是为了和pnp问题配对,用同一个模式进行深度点未知和深度点已知的求解。

 矩阵行列式不为零,证明矩阵满秩,矩阵方程必有唯一解,当其行列式为零,矩阵不满秩,无解或有无穷多解

BlockSolver需要自定义数据维度的大小,BlockSolverX采用动态分配的方式

边:计算误差和雅可比矩阵,供给内部的增量方程进行运算

顶点:初始化待优化值(手动设置),利用增量方程计算出来的值和结构进行参数更新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值