当前位置: 首页 > article >正文

视觉SLAMch4——李群和李代数

一、李群和李代数在SLAM中的定位(如何解决SLAM中的问题)

        在视觉SLAM中,我们的目标之一是估计传感器(通常是摄像头)在每一时刻的位置和姿态。为了量化估计的好坏,我们需要定义一个误差函数,该函数反映了估计位姿与真实位姿之间的差异。误差函数通常是关于位姿参数的函数,这意味着当我们得到一个更接近真实位姿的估计时,误差函数的值会更小。

error=f(x)

        设位姿 x 由位置 t 和姿态 R 组成,其中 RR 是一个旋转矩阵,t 是一个平移向量。我们可以将位姿 x 表示为:

x=[R,t]

        在寻找最小误差的过程中,实际上就是在执行SLAM前端中的位姿估计。这一过程可以通过优化技术来实现,其中最常用的方法之一是梯度下降法。这种方法类似于“下山”的过程——从一个高误差的位置开始,逐步调整位姿参数,沿着误差函数梯度的负方向移动,直到达到局部最小值。

具体来说,我们可以使用以下步骤来估计位姿:

  1. 初始化:选择一个初始估计值作为起点。
  2. 计算误差:根据当前估计值计算误差函数的值。
  3. 求导:计算误差函数关于位姿参数的梯度。
  4. 更新位姿:根据梯度方向调整位姿参数,通常使用一个学习率(步长)来控制更新的幅度。
  5. 重复:重复步骤2-4,直到满足某个终止条件(如梯度足够小或者达到最大迭代次数)。

        对于位置t来说,x,y,z处于线性空间中,他们对加法运算封闭(也就是t+Δt仍在该空间内部,例如,如果我们考虑R^{3}中的向量,即三维空间中的向量,任何两个三维向量相加的结果依然是一个三维向量,不会变成二维或四维向量。这是因为向量加法是基于坐标相加的,而坐标的维度是固定的。),在位置求导时可以使用导数定义,之后乘一个步长就可以位置这一梯度下进行下降。

        对于旋转R来说,旋转矩阵 R 属于特殊正交群 SO(3),它是一个非线性空间。在 SO(3) 中,旋转矩阵不支持普通的加法运算,因为两个旋转矩阵相加的结果通常不是一个有效的旋转矩阵(有效的应该符合单位正交阵)。因此,我们不能直接使用标准的微积分规则来计算旋转矩阵的导数。

        因此,对于T∈SE(3)来说,它也是不支持加法的(因为内部的R不支持),所以同样不能写出导数。

 如何解决无法求导这一问题?

        SO3是特殊正交群,是一种群,而且还是李群。每种李群都有一种与之对应的李代数,也就是李群中的元素能在李代数中找到对应关系。

SO(3):   R ----->so(3):\phi(phi)

SE(3):   T ----->se(3):   \xi (kxi)

        既然李代数处于线性空间中,满足线性空间的闭合性,而李群处于三维空间的“流形”上,因此李群只满足乘法(即空间变换)而不直接支持加法。当我们想要对李群进行求导时,由于李群本身不支持传统的加法运算,所以直接在李群中求导是不可行的。然而,与李群有明确对应关系的李代数处于线性空间中,满足线性关系,可以求导。因此,我们可以利用李代数作为桥梁,先对李代数求导,然后再通过指数映射将结果转化为李群中的元素。

二、李群和李代数的基本数学知识

1.对于李群和李代数的理解

什么是群?

        之前我们认为群类似于集合,在数学中,“群”是一个集合配上一个二元运算(通常称为群运算)(G=(A,·)  一种集合A,一种运算),满足四个公理:闭合性、结合律、存在单位元和存在逆元。具体来说:

  1. 闭合性:对于群中的任何两个元素 a 和 b,其运算结果 a∗b 也在群内。
  2. 结合律:对于群中的任何三个元素 a, b, 和 c,有 (a∗b)∗c=a∗(b∗c)。
  3. 单位元(幺元):存在一个特殊的元素 e(称为“单位1”),对于群中的任何元素 a,都有 a∗e=e∗a=a。
  4. 逆元:对于群中的每一个元素 a,存在另一个元素 b 使得 a∗b=b∗a=e。

通俗的例子:想象一下整数集 Z 和加法运算。整数集 Z 搭配加法运算构成了一个群,因为:

  • 闭合性:两个整数相加的结果仍然是整数。
  • 结合律:(a+b)+c=a+(b+c)。
  • 单位元:0 是加法的单位元,因为对于任何整数 a,都有 a+0=0+a=a。
  • 逆元:对于任何整数 a,存在 −a,使得 a+(−a)=(−a)+a=0。

什么是李群?

        李群是具有群结构的拓扑空间,同时也是一个光滑流形(即可以被看作是由一系列平滑连接的小块组成的)。换句话说,李群不仅满足群的四个公理,而且还具有连续性和可微性。这意味着群中的元素可以用一组连续变量来参数化,并且群运算可以被看作是这些参数的连续函数。

        通俗的例子:旋转矩阵构成的特殊正交群 SO(3) 就是一个李群。SO(3) 中的每个元素(即旋转矩阵)都可以通过一组连续的角度参数来表示,而这些角度参数之间的变换是平滑的。

什么是李代数?

        李代数是与李群相关联的代数结构。它是李群在单位元处的切空间,也就是李群中元素的无穷小变化。李代数可以用来近似描述李群中元素的邻近行为。李代数具有矢量空间的结构,并且还带有一个额外的操作——李括号(Lie bracket),它描述了向量之间的非交换性。

        以特殊正交群 SO(3) 为例,它的李代数是 so(3)。so(3) 可以用三个自由参数的向量来表示,这三个参数代表绕 x、y、z 轴旋转的角度的无穷小变化。通过这些参数,我们可以使用简单的线性代数运算来近似描述旋转矩阵的变化。

        在组成上,李代数为集合V+数域F+二元运算[ , ](李代数里把这个括号称为李括号)。在运算上满足封闭性、双线性(线性组合可以拆开并提出来)、自反性和雅可比等价。

        比如一个李代数g=(R^{3},R,×),也就是集合为三维向量,数域是实数域,二元运算符是叉乘。我们可以知道三维向量之间的叉乘还是三维向量,满足封闭性;三维向量叉乘满足分配律,同时满足线性可以提出来,满足双线性。自反性和雅可比等价均满足(线代)。

PS:叉乘的几何意义在于它提供了一种方式来构造一个与原来两个向量所在平面垂直的向量,并且这个新向量的长度与原来两个向量构成的平行四边形的面积成正比,所以叉乘不会提高维度。

2.常见李代数形式

(1)SO(3)的李代数so(3):

 so(3)可以说是一个三维向量(矩阵,因为^符号会把三维向量扩充成一个反对称矩阵)

其李括号为(了解即可,不必深究):

(2)SE(3)李代数se(3):

ρ在一定程度上表示了平移,so(3)在一定程度上表示了旋转,组成了这个六维向量。在 se(3) 中,同样使用 ∧ 符号,将一个六维向量转换成四维矩阵,但这里不再表示反对称。我们仍使用 ∧ 和 ∨ 符号来指代“从向量到矩阵”和“从矩阵到向量”的关系。

具体就是把\phi ^{\Lambda }先转换成一个3*3矩阵,再把ρ(3*1)矩阵放进去,最后用0填充。

se(3)对应的李括号(了解即可):

 3.李群和李代数的转换关系

(1)SO(3)和so(3)的对应关系:

①指数映射(李代数->李群)

R(t)=exp(\Phi _{0}^{\wedge }t)

指数映射怎么来的??

PS:

高阶泰勒展开公式:

这个式子说明了什么?? 

泰勒展开:

将向量Φ写成模乘单位向量:Φ=θa,因为θ是一个标量,可以提出来。化成下面第一行的形式。 

一个规律:

        三个a^可以化成一个。这个可以简单的从特殊到一般的方法类比一下,比如设a=[1,0,0],可证明出来等式成立。

证明过程:

 一定要注意,a是单位向量,所以注意三个字母的平方和为1。

描述一个旋转有:R、θn(角轴)、四元数、欧拉角四种。

        利用到了旋转矩阵转换成旋转向量的知识,在Eigen里只需.toRotationMatrix()即可。数学推导的话需要用到罗德里格斯公式。

R=\cos \Theta I+(1-\cos \Theta )nn^{T}+\sin\Theta n^{\wedge }

        我们可以发现与上面我们推导指数映射的式子一模一样,我们以此可以发现,李代数到李群的转化就是角轴到旋转矩阵的转化。换句话说,指数映射的过程实际上是在李代数层面上进行的无穷小旋转的积分,从而得到了有限旋转的矩阵表示。因此,可以说角轴是李代数的一个直观表示(角轴就是李代数!!),而旋转矩阵则是李群的一个直观表示。

        所以,下面的这个指数映射中的Φ^有两层本质:一是作为李代数充当李群在幺元处的切平面;二是作为角轴,所以我们可以知道so(3)的本质其实就是旋转矩阵SO(3)对应的旋转向量。

R(t)=exp(\Phi _{0}^{\wedge }t)

②对数映射(SO(3)->so(3))

\Phi =ln(R)^{\vee }

推导很简单:我们上面知道了:

R=exp(\Phi ^{\wedge })

两边取对数后调换顺序:

 \Phi ^{\wedge }=lnR

取逆变换操作:

\Phi =(lnR)^{\vee }

之后我们可以进行展开:

但是这个方法化简很难!我们通常使用别的方法:

套用R->θn(旋转矩阵->角轴)的方法:

1.罗德里格斯公式(已知R求θn):

R=\cos \Theta I+(1-\cos \Theta )nn^{T}+\sin\Theta n^{\wedge }

 2.求迹:

PS:

单位向量乘自身转置的迹等于1? 

3.旋转角求出来了,因为旋转前后角轴不发生变化(Rn=n)

Rn=n求解,相当于求\lambda=1时候的特征向量。(A-λE化简求基础解系)

4.so(3)=θn

(2)SE(3)和se(3)的对应关系

①指数映射

         我们发现这个ρ其实不直接是旋转向量,它整体差了一个J,即t=Jρ。

J的推导:

ps:泰勒展开

②对数映射

\xi =(lnT)^{\vee }

可以对lnT进行泰勒展开,但是还是同上,可以用别的方法。

从se(3)入手:

(3)总结

4.定义李代数加法和李群乘法之间的关系(求导准备)

        在控制理论和系统分析中,表达式 RΔR 通常用来表示一个旋转矩阵 R 经历了一个小的扰动 ΔR 后的新状态。这里的 R 是一个已知的旋转矩阵,而 ΔR 是一个表示扰动的小旋转矩阵。整个表达式 RΔR 表示原始旋转 R 被扰动 ΔR 影响后的结果。 

        在控制理论和系统分析中,表达式 RΔR 通常用来表示一个旋转矩阵 R 经历了一个小的扰动 ΔR 后的新状态。这里的 R 是一个已知的旋转矩阵,而 ΔR 是一个表示扰动的小旋转矩阵。整个表达式 RΔR 表示原始旋转 R 被扰动 ΔR 影响后的结果。

        使用乘法而不是加法的原因是因为旋转矩阵的组合是通过矩阵乘法实现的。在三维空间中,一个旋转矩阵 R 可以表示一个物体相对于某个固定坐标系的旋转。当我们说一个旋转矩阵 ΔR 是对 R 的一个扰动时,我们意味着 ΔR 表示一个额外的小旋转,这个小旋转是在原有旋转 R 的基础上进行的。

(1)SO(3)和so(3)

上面的内容告诉我们R=exp(Φ^),那么有没有这么一种可能,如果:

ln(exp(R_{1})+exp(R_{2}))=R_{1}+R_{2}

R_{1}\cdot R_{2}=exp(\Phi _{1}^{\wedge })\cdot exp(\Phi _{2}^{\wedge }) =exp(\Phi _{1}+\Phi _{2})^{\wedge }

那么对于我们求李群的导就有很大的便利了!!但是可惜这个式子并不成立 ,因为上面式子里面的R不是标量,它是李代数扩充而成的矩阵,它是不遵循这个法则的。

根据BCH公式:

         指数是李代数^时,除了A+B,还有很多李括号余项,有很多的高阶项。

        虽然李群相乘不等于李代数直接相加(因为后面有很多余项),但是如果A或B有一个小量的话,还是可以写成加法形式的,只不过需要在较小的李代数前有一个修正

那么J_{l}这些修正是什么?(J_r)

l就是light(左),right(右)   Jl与上面的J一模一样(逆不用记)

 Jl和Jr关于Φ=0对称

 但是这么写很不美观,由于这个系数的存在。所以我们一般趋向于像下面的方式写,这样写不用考虑左右雅可比的问题,如果把小量写在左边就直接用Jl乘左边就可以了。

下面这个式子非常非常非常非常重要!!!! 

 exp(\Phi _{1}+\Phi _{2})=exp(J_{l}\Phi _{1})exp(\Phi _{2}) =exp(\Phi _{1})exp(J_{r}\Phi _{2}) 

 (2)SE(3)和se(3)

        这个J不同于之前的那个雅可比,这个量更加麻烦,但是我们一般用不到这个雅各比,所以可以不考虑这个形式。 

三、求导

对位姿求导有两种模型,都需要借助李代数,分别是:李代数模型、小扰动模型

1.SO3求导:

①李代数求导

我们可以把误差函数F具体为下面的形式:

\frac{\partial (Rp)}{\partial R}

        上面的误差函数意味着我们想要知道误差函数如何随着旋转矩阵R的改变而改变。但注意,由于SO3是不能求导的,我们只是以此作为一个记号,实际计算不是通过SO3进行计算的。计算过程如下:

        因为R与Φ^之间是有指数对应关系的,所以我们可以用李代数的方法来进行求导。所以上图中的R用exp(Φ^)p代替,求导也是对李代数Φ进行求导。

        第二行其实就是用到了我们上面之前求的乘法和加法的转换公式,我再次复制在下面,非常重要。注意,上面第二行也可以把Jl换为Jr,只不过需要写在右边。

exp(\Phi _{1}+\Phi _{2})=exp(J_{l}\Phi _{1})exp(\Phi _{2}) =exp(\Phi _{1})exp(J_{r}\Phi _{2})

        第三行就是将exp((JlδΦ)^)进行一个近似。用到了下面这个近似公式(更多的近似可以看上面的泰勒公式,看看能不能引起一些回忆......):

e^{x}\approx 1+x 

第四行就是提公因式约掉一个I,第五行用到了下面这个公式:

a\times b=-(b\times a)=a^{\wedge }b 

这个方法推导麻烦但理论性强 

②小扰动求导

 两者的区别:

        第一个李代数求导时对于当前位置的李代数进行求导,导数等零后求出来的时最佳位姿,但是需要求Jl,而雅各比量求起来比较麻烦。第二个小扰动求导是对当前位姿的扰动求导,求出来的是在当前位姿基础上达到最佳位姿的最佳增量ΔR,最后的最佳位置是ΔR*R。

2.SE3求导:

直接采用扰动求导(第一个太麻烦了)

PS:R叫旋转矩阵,表示的是旋转。而T是变换矩阵,表示的是位姿变化(旋转+平移)

对于第四行,求解过程如下,下面还有一些需要的式子,供参考:

        最后的求导,要把δξ展开,因为ξ如上图可以进行展开。所以我们需要对于第一行先对δρ进行求导,最后结果也就是等于单位1,后对δΦ进行求导,需要先进行一个位置的调换,因为要对Φ求导而不是Φ^,之后结果就是前面的系数,理解起来还是比较简单的。第二行由于本身就是0所以两个结果都为0. 

我们对先对于一些特殊字符接触到了:

1.四元数(圈加)

 2.SE3求导(圈点)

\frac{\partial (Tp)}{\partial T}=(Tp)^{\bigodot }

3.向量与反对称矩阵(上下三角)

四、可视化理解李群与李代数

        李群的切空间是李代数,李代数是线性空间,支持加法。而李群是由一个集合和一种运算组成的,这个运算是封闭的,仅支持乘法。

         整体过程如上图所示,在李群当中有一个R0,它可以通过指数映射转换到李代数上。我们想要求一个最佳位姿,那么就需要求ΔRR0(想要达到最佳位姿需要再动多少?)。对于这个扰动ΔR,我们也一样转换到李代数上,这样我们可以求出ΔRR0对应的李代数。但是由于他们不能进行简单的相加,所以一定不要忘记在扰动(小量)前面加上一个修正。最后通过对数映射就可以得到最佳的位姿。

五、实践部分:Sophis

红色波浪线可以使用ctrl+shift+p进入配置后,使用locate对eigen等进行定位

注意:配置之后的“sophus”库还会有红色波浪线报错,这里我们放在六、cmake进阶拓展里进行解答

1.

#include <iostream>   //cout、endl
#include <cmath>        //数学计算如三角函数,可以按ctrl后在outline中看都定义了什么
#include <Eigen/Core>    //Eigen文件夹中的Core文件夹,定义了向量和矩阵的基本操作
#include <Eigen/Geometry> //四元数、欧拉角、旋转矩阵、角轴
#include "sophus/se3.hpp" //定义了与欧式变换相关的

由于se3的头文件中还引入了so3,所以我们才不需要写引入so3.

2.

using namespace std;
using namespace Eigen;//定义命名空间,不用手动写Eigen::

但是如果有多个namespace,且其中有定义了相同名字的变量的时候,就会发生歧义。所以这个东西在大项目中最好不要使用。

3.

/// 本程序演示sophus的基本用法

int main(int argc, char **argv) {

  // 沿Z轴转90度的旋转矩阵
  Matrix3d R = AngleAxisd(M_PI / 2, Vector3d(0, 0, 1)).toRotationMatrix();
  //先定义一个角轴(double),并且后面的向量必须是单位向量。定义后转换成旋转矩阵
  // 或者四元数
  Quaterniond q(R);                   //用R来定义旋转,做有参构造。
  Sophus::SO3d SO3_R(R);              // Sophus::SO3d可以直接从旋转矩阵构造
  //把Eigen定义的3*3矩阵放在Sophis定义的SO3里面,因为S03比普通的Matrix矩阵有更多调用方法
  Sophus::SO3d SO3_q(q);              // 也可以通过四元数构造
  // 二者是等价的(q和R)
  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;

注意:Sophus::SO3d中定义了一个SO3d(double),名字叫做SO3_R。这个SO3d和Matrix3d其实类似,不是一个名字,而是一个定义。

4.

// 使用对数映射获得它的李代数
// 对数映射时不直接映射,是使用so3和角轴的对应关系求解,因为我们上面已经声名了角轴
  Vector3d so3 = SO3_R.log();
  cout << "so3 = " << so3.transpose() << endl;
  // hat 为向量到反对称矩阵(上三角 把so3转换为一个反对称矩阵)
  cout << "so3 hat=\n" << Sophus::SO3d::hat(so3) << endl;
  // 相对的,vee为反对称到向量
  cout << "so3 hat vee= " << Sophus::SO3d::vee(Sophus::SO3d::hat(so3)).transpose() << endl;

5.(扰动未求导)

  // 增量扰动模型的更新
  Vector3d update_so3(1e-4, 0, 0); //假设更新量为这么多,定义了一个增量ΔR
  Sophus::SO3d SO3_updated = Sophus::SO3d::exp(update_so3) * SO3_R;
  //上面把增量转换为S03(把李群上的ΔR定义为SO3形式),并于之前定义的R进行叠加(ΔR*R)
  cout << "SO3 updated = \n" << SO3_updated.matrix() << endl;

 这个过程用之前的话说就是定义了一个θn(so3),转化为ΔR*R

4.

 // 对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)
  //这两个方法都一样,因为q(四元数)也被重载过了
  cout << "SE3 from R,t= \n" << SE3_Rt.matrix() << endl;
  cout << "SE3 from q,t= \n" << SE3_qt.matrix() << endl;

5.

// 李代数se(3) 是一个六维向量,方便起见先typedef一下
  typedef Eigen::Matrix<double, 6, 1> Vector6d;
  Vector6d se3 = SE3_Rt.log();   //SE3做完对数映射的se3是个六维向量
  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;

 6.

  // 最后,演示一下更新
  Vector6d update_se3; //更新量
  update_se3.setZero();
  update_se3(0, 0) = 1e-4;//小量
  Sophus::SE3d SE3_updated = Sophus::SE3d::exp(update_se3) * SE3_Rt;
 //                                          se3转化为T后乘位姿
  cout << "SE3 updated = " << endl << SE3_updated.matrix() << endl;

  return 0;
}

        在实际应用中,我们经常需要对现有的姿态(位置和方向)进行微小的调整。例如,在机器人导航或视觉里程计(Visual Odometry, VO)中,传感器数据通常提供相对于当前姿态的小变化。这种情况下,我们需要应用这些小的变化到当前的姿态估计中。

六、cmake进阶拓展

        如上图,我们虽然写了#include,但是不代表我们已经引了头文件了,我们必须把cmake配置好,否则即使写在这里也会报错,因为编译器找不到这个头文件和其对应的库。

下面是作者给出的cmakelists

cmake_minimum_required(VERSION 3.0)
project(useSophus)

# 为使用 sophus,需要使用find_package命令找到它
find_package(Sophus REQUIRED)

# Eigen
include_directories("/usr/include/eigen3")
add_executable(useSophus useSophus.cpp)
target_link_libraries(useSophus Sophus::Sophus)

add_subdirectory(example)

1.sophus如何引进来?

        首先使用find_package找到Sophus,required的意思就是如果包被找到,CMake 会继续配置过程,并设置相关的变量,如 Sophus_INCLUDE_DIRS 和 Sophus_LIBRARIES,以便在项目中使用 Sophus 库。如果包没有被找到,CMake 会停止配置过程,并输出一条错误消息,告知开发者缺少必要的依赖项。如果我们没有REQUIRED,则会只输出一个warning,不会报错。

        我们之前讲过,这个命令找的是XXXconfig.cmake文件,这个XXX的大小写如何,我们Sophus的大小写就怎么写。

        但是在这之前我们需要下sophis库,在github中搜索sophus,复制地址。

在第三方文件夹中克隆

出现两个报错:

报错1:

解决Ubuntu18.04的git clone报错Failed to connect to github.com port 443: Connection refusedicon-default.png?t=N7T8https://blog.csdn.net/weixin_42149550/article/details/133682119报错2:

        这是因为网速咱太慢了,如果你可以用魔术或者其他办法提升网速的话也可以(steam++什么的这种最好不用,我最开始用的这个,但是出现了另一个报错,就是大概意思就是dns或者ip地址不一致的问题)...我最后是用的下面那个浅层克隆的方法,把网址换成我们上面的网址,然后cd sophus按他的输入就可以了!!error: RPC failed; curl transfer closed with outstanding read data remainingicon-default.png?t=N7T8https://stackoverflow.com/questions/38618885/error-rpc-failed-curl-transfer-closed-with-outstanding-read-data-remaining

 报错3:存储空间不够用了怎么办?

7 种简单方法,释放和清理 Ubuntu 磁盘空间icon-default.png?t=N7T8https://www.sysgeek.cn/ubuntu-free-up-space/Ubuntu中文件系统根目录上的磁盘空间不足(解决方案icon-default.png?t=N7T8https://blog.csdn.net/thy0000/article/details/122882955

用心良苦的强烈建议:血与泪的教训!!! 

        其实本人强烈建议直接拓展内存(第二个方法),别瞎搜别的方法了,我看了一个博主的清理磁盘空间的结果把snap直接删掉了(不是我上面的那个,是另一个博主,上面那个可以试试,反正我本人是没太大的变化,也就1~2个G),code直接没了,只能用别的办法下回来,而且根本清不了多少内存,搞来搞去和原来差不了多少。研究一下午怎么把snap下回来也没搞好(应该是没有删除干净,最后干脆把snap全删掉了,而且内存没多多少...)搞了一下午结果还是拓展内存了,建议直接拓展内存吧,毕竟20G初始容量不大(在Windows里面虚拟机只占了1个G),所以我直接拓展成50G了。

        如果你也想删掉snap(最好...别有这种想法,当然如果你只按了一个code的话问题不大,如果你从snap-store里下载了很多应用那还是算了...),可以按照下面博客的方法(一下午的成果...)

从 Ubuntu Linux 中完全删除 Snap [教程]icon-default.png?t=N7T8https://cn.linux-terminal.com/?p=7755#google_vignette        如果你删除snap后发现code没了!!!不慌!!我找了一下午找到最简单的一种安装方法!(从二开始喔)

如何在 Ubuntu 20.04 上安装 Visual Studio Codeicon-default.png?t=N7T8https://developer.aliyun.com/article/759454

————————血与泪分割线————————

         然后新建build,进去然后cmake ..,然后make(可使用-j4拓展为四线程,数字可以换,但是要注意你的线程不要超,我的建议嘞还是慢慢来...我3060的游戏本反正是卡崩溃了,电脑一般的还是别拓展线程了...

        然后make install,注意要sudo,否则会出现下面的报错

我们发现这里其实只是把头文件放进去了,而没有把库放进去 

我们可以看到这个Sophus安装的路径(/usr/local/sophus)

        安装完之后,系统就不会找不到Sophus了,之前出现那个红色波浪线的原因就是因为我们的系统中根本没有安装Sophus,find_package根本找不到关于sophus的文件。现在我们可以看看sophus文件在哪?

但是我们发现,我使用locate之后,怎么什么都没有呢!!??

为什么locate找不到刚安装/创建好的文件 ??

        举个例子,假设我们在根目录使用touch新建一个haoshuai.txt,使用locate haoshuai后你会发现还是什么都没搜索到。这是因为这个locate语句是在数据库中进行搜索的,而这个文件当前的操作记录还保存在临时文件的缓存之中,除非开关机或等待系统的每天更新。所以新建的东西是找不到的。我们的sophus找不到就是因为这个是我们刚刚创建的,系统还没有进行保存。(我进行了开关机并等待了20个小时后还是没有搜索到,自动更新数据应该是要大于24小时)

        难道我们就没有办法现在找到他了吗!!当然有办法输入:

updatedb

 就可以把临时日志缓存到已有日志里面。

搜索到之后我们就可以发现Config之前的Sophus是大写的了,所以我们cmakelists里面find_package时要大写S。

        我们上一节课说到(补充那节),如果可以找到find_package里面的库,就会返回${Sophus_INCLUDE_DIRS}(头文件所在的目录)、${Sophus_LIBRARIES}(库文件路径)两个量,然后就使用include_directories(${Sophus_INCLUDE_DIRS})来找到头文件的路径。但是如果这里我们这么写就会发现行不通了。

上一节博客截图

如果当我们输出这两个量,看看能不能找到?

结果空空如也:

PS:又又一个报错Compatibility with CMake < 3.5 will be removed from a future version of CMake. 

        这是因为从最新版的cmake不再兼容低于3.5以下的版本,而我们代码中是3.0,我们只需要把3.0改为3.6并ctrl+s保存后再cmake ..就可以了

 ————————分割线————————

        我们发现 ${Sophus_INCLUDE_DIRS}(头文件所在的目录)、${Sophus_LIBRARIES}(库文件路径)这两个什么都没返回,但是我们不是已经安装好了吗?没返回路径怎么解决??

引入的方法一:

因为我们之前已经搜索到sophus在哪了,直接手动加上就ok了

include_directories("/usr/local/include/sophus")

 ————————方法一分割线————————

方法二

现在我们思考一下target_link_libraries(target item)中的item都可以是什么?

我们之前只关注到了第一个Config,关注这个是为了知道find的时候名字怎么写。当发现find不到的时候,我们就要关注最后一个。这个文件eigen也有。

如下图,当我们打开SophusConfig.cmake的时候发现,里面根本没有出现 ${Sophus_INCLUDE_DIRS}和${Sophus_LIBRARIES}。所以我们才没有找到。而eigen就有,可自行使用gedit命令打开路径(路径用locate上面那个搜)。

        而打开.target文件后,我们发现了“创建引用目标::Sophus”,我们把Sophus设定成了一个引用目标target,我们可以通过链接来引用他。

        所以当我们写了下面的链接的时候,就不用写方法一的directories了,这代表target可以自动链接头文件和库了。但是这个后面可以放什么呢?也就是target_link_libraries(target item)中的item都可以是什么?

1.可以放库文件 (第一讲)

在C++程序中新建并使用库icon-default.png?t=N7T8https://blog.csdn.net/Johaden/article/details/1408032052.路径

就比如我们之前写过的

target_link_libraries(eigenMatrix ${Eigen3_LIBRARIES})

这个${Eigen3_LIBRARIES}里面就存了库文件的路径。

3.库生成的target

        在官方文档中对于::的相关解释只有一行......意思就是::整体会被看作是一个库文件的target name。如果没定义这个::写上去就会报错。怎么找定没定义就看我们上面打开的那个文档(SophusTargets.cmake),有定义::就可以这么用。

2.高翔老师写的最后一句是什么意思?

add_subdirectory(example)

        该命令用于将指定的子目录添加到当前构建目录中。当在某个CMakeLists.txt文件中调用 add_subdirectory(example) 时,这意味着我们想要CMake处理位于当前目录下的名为 "example" 的子目录中的CMakeLists.txt文件。

  add_subdirectory() 命令除了接受一个必需的 source_dir 参数外,还可以接受两个可选参数:binary_dir 和 EXCLUDE_FROM_ALL

  1. binary_dir:这个参数允许你指定一个不同的二进制目录,用于存放构建生成的文件,而不是默认的构建目录。如果不指定 binary_dir,CMake 会使用 source_dir 的同级目录来存放构建生成的文件。指定 binary_dir 可以让你更好地控制构建输出的位置(输出到你希望的地址),特别是在你想要将源代码和构建输出分离的情况下。

  2. EXCLUDE_FROM_ALL:这个选项用于指示 CMake 不要将子目录中的目标添加到主构建目标中(子文件夹不编译)。这意味着即使你运行 make all 或类似的命令,子目录中的目标也不会被构建。这在某些情况下很有用,比如当你有一个不希望在常规构建过程中构建的测试或工具目录时。当然,如果子文件中有些程序需要在主文件中用到,那么即使你写了这个命令也会编译子文件夹。

        所以,example里面就应该有一个CMakeLists

        主配置文件写过的文件就不用再赘述了(project、版本号...),只需要写你需要做的操作就行。 

1.
option(USE_UBUNTU_20 "Set to ON if you are using Ubuntu 20.04" OFF)

        这行代码定义了一个名为 USE_UBUNTU_20 的选项,允许用户在配置CMake时指定是否正在使用Ubuntu 20.04操作系统。默认值为OFF,意味着如果用户不明确设置这个选项,它将被视为关闭状态。结合下面的判断语句使用,如果是off那么后边那个if就不编译了。

     所以如果我们使用的是20.04的话,就需要在编译的时候加上

        cmake .. -DUSE_UBUNTU_20=ON   

2.
if(USE_UBUNTU_20)
    message("You are using Ubuntu 20.04, fmt::fmt will be linked")
    find_package(fmt REQUIRED)
    set(FMT_LIBRARIES fmt::fmt)
endif()

        这段代码检查 USE_UBUNTU_20 选项是否被启用。如果是,它将显示一条消息告知用户正在使用Ubuntu 20.04,并且会链接fmt库。接着,它尝试查找fmt库,并将其设置为 FMT_LIBRARIES 变量。这个变量稍后将用于链接fmt库。这个fmt原本是为了方便编译的,但是貌似编译时会报出很多与实际无关的错,就是明明是A的错误,但是报错报的是fmt......但是我们还避不开这个bug,了解以下这个代码含义就行,后面遇到了再解决!

如果启用了fmt可能需要安装它:

sudo apt-get install libfmt-dev

——————————————————————————————

本博客参考与B站UP主全日制学生混的SLAM课程

up主推荐:推荐博客(不太简单....不好理解....短时间看不太明白..........不理解没关系,好好看书理解理论部分,这个博客慢慢看,就是了解一下):

Sophus库CMakeLists.txt内容详解笔记 FROM:一块灰色的石头icon-default.png?t=N7T8https://www.cnblogs.com/newneul/p/8367421.htmlCmake命令之add_subdirectory介绍 FROM:Domibabaicon-default.png?t=N7T8https://www.jianshu.com/p/07acea4e86a3机器人学——李群、李代数快速入门icon-default.png?t=N7T8https://www.bilibili.com/video/BV1tg411j7oY/?vd_source=033a75d9cfb0b0df5347cafb8b033109


http://www.kler.cn/a/287424.html

相关文章:

  • 数据库基本概念学习笔记
  • hadoop3.x 新特性
  • 剧本杀门店预约小程序,解锁沉浸式推理体验
  • WebAssembly在桌面级应用开发中的探索与实践
  • workerman的安装与使用
  • 树的直径计算:算法详解与实现
  • 单机无法拨号问题分析
  • UI自动化测试的边界怎么定义?
  • python中的值传递和引用传递
  • 城投公司相关指标数据(2023.8)
  • springboot+vue 进销存管理系统
  • 一起学习LeetCode热题100道(61/100)
  • 计算图像分割mask的灰度级个数、以及删除空的分割数据
  • HTML静态网页成品作业(HTML+CSS)——动漫猫和老鼠网页(1个页面)
  • 快速安全部署 Tomcat
  • 全志Linux磁盘操作基础命令
  • 程序化交易在中国的规模
  • 云计算实训39——Harbor仓库的使用、Docker-compose的编排、YAML文件
  • 什么场景可以使用函数式接口
  • 【数据结构】线性表的链式表示(单链表)
  • 《C++20 特性综述》
  • Matlab实现人工神经网络
  • 基于Java+SpringBoot+Vue的汽车销售网站
  • 【Python123题库】#统计文章字符数 #查询高校信息 #查询高校名
  • linux系统中USB模块鼠标驱动实现
  • PostgreSQL主从同步介绍