下面是来自各个渠道总结的,如果有什么不对的或需要补充的欢迎评论区补充,一起学习一起进步…
H矩阵指单应矩阵,当相机发生纯旋转时,此时平移为零,基础矩阵自由度下降,基础矩阵误差较大,选用单应矩阵来恢复位姿。一般如果所有特征点都落在同一平面上,可以利用这种矩阵进行运动估计。
IMU预积分与通常积分相比,解决了初始状态更新时以及加速度计、陀螺仪偏差变化时所导致的积分重新计算问题。其主要思路是计算两个关键帧之间的状态增量,当初始状态变化,就在原来增量基础上乘以初始状态的变化量;当偏差变化时,则通过求取预积分量关于偏差的雅各比,实现预积分的一阶线性近似更新。
so3旋转矩阵的李代数表示,se3位姿变换矩阵的李代数表示,二者可以通过指数-对数关系实现转换,sophus中将旋转矩阵利用Log函数转换成对应的旋转向量,在算法中扰动模型更新时,常将一个扰动向量通过指数映射关系转换成相应的扰动矩阵,左乘或右乘旋转矩阵实现更新
初始化异同 | ORB-SLAM3 | VINS-MONO |
---|---|---|
成功的准则 | 加速度大于重力的0.5%(引用论文里有)代码中没看到 | 估计出来的重力大小误差在10%以内,IMU的测量值有一定的变化范围 |
是否估计加速计bias | 同时估计了加速度计和陀螺仪的bias | 不估计加速度计的bias,认为加速度bias与重力(g)值相比很小,难以估计 |
有无单独估计陀螺仪bias | 陀螺仪bias放在目标函数中一起估计 | 陀螺仪bias单独拿出来估计 |
尺度恢复策略 | 初始值设置为1,初始化后100内每隔10秒优化一次尺度 | 只在初始化的时候恢复该尺度 |
姿态参数化 | SO(3) | 基于四元数 |
惯性系对其方式 | 优化过程中同时优化惯性系到第一帧的旋转矩阵 | 优化得到重力在第一帧的测量值,然后对重力重新参数化,以限制其模长,最后重新来一次优化 |
求解过程 | 三次非线性优化(LocalMapping, Inertial only optimization, FULL BA) | 三次非线性优化(sfm, 求解陀螺仪bias和Full BA), 两次线性求解(求尺度和速度的初值)和求相机-IMU外参 |
相同点 | 都以视觉作为基础,最终都对齐到惯性系,都在固定窗口内进行初始化。最后都进行FullBA,完成初始化 |
从理论上来讲,单目视觉SLAM的初始化运动是一个2D-2D的求解问题,是通过对极几何解决的。我们通过本质或者基础矩阵的分解来得到相机运动,对极约束是等式为0的约束,因此具有尺度等价性,由分解出的平移量乘以任意非零常数任然满足对极约束,因此平移量是具有一个不确定的尺度的,即我们放大任意倍数,都是成立的,导致了单目尺度的不确定性。从直觉感官角度来讲,从一张图像中,我们无法判断图像中物体的实际大小,因为不知道物体的远近。双目相机拍摄一次能够获得具有视差的左右目两张图像,在已知基线的情况下,计算视差,可以通过三角测量原理直接得出深度信息,因此不存在尺度不确定性。
如纯视觉情况下,误差量要么是重投影误差,要么是光度误差,维度都为2。
此时状态量有两个:位姿和空间点,那么空间点维度为3,李代数表示位姿的话维度为6。
所以最后雅可比就是2维误差量分别对6维位姿(2x6)、3维空间点(2x3)求导,结果就是2x9。
所以最终结论就是:雅可比维度=误差维度x状态量维度
1.和3D位置相比,逆深度维度更小;
2.在实际应用过程中,可能会看到类似天空这样比较远的点,导致z很大。那么即使产生了微小的变化量,也会给优化带来巨大的干扰。但是使用逆深度就可以这种情况。
3.逆深度的分布更符合高斯分布。
在SLAM初始化完成后,我们可以通过世界坐标系中的3D点位置和图像中的2D点匹配关系,由PNP算法初步求解两帧图像间的位姿变换。这个位姿变换并不准确,因此采用视觉重投影误差的方式进行进一步优化。重投影,意为利用我们估计的并不完全准确的位姿对三维空间点再次投影。我们已知特征点在图像中的实际位置,即观测值,利用初步求解的位姿,通过投影关系,再次将世界坐标系中的三维点投影到二维成像平面上,得到特征点像素坐标的估计值,与实际的观测值作差,对于n个特征点,构建最小二乘问题,认为差值越小,越符合实际的投影,估计越准确,最后得到优化后的位姿。
ORB-SLAM3跟踪线程的输入为图像帧(单目/双目/RGB-D)及IMU数据。对于图像数据,首先对图像进行金字塔分层,对每层金字塔提取ORB(Fast角点+二进制描述子)信息,并基于四叉树进行特征点均匀化,同时进行高斯模糊以及去畸变。之后进行地图初始化,ORB-SLAM3和ORB-SLAM2一样都是使用纯视觉的方法进行初始化,再之后开始进行两个阶段的跟踪。第一阶段包括恒速模型跟踪、参考关键帧跟踪以及重定位跟踪,目的是为了跟得上,但估计出的位姿可能没有那么准确。第二阶段是局部地图跟踪,是利用PNP进行最小化重投影误差求解,得到更准确的位姿。最后决定是否新建关键帧。对于IMU信息,ORB-SLAM3还需要进行IMU预积分,但IMU三个阶段的初始化是在局部建图线程完成。此外,在恒速模型跟踪中,如果有IMU数据,恒速模型的速度将根据IMU信息得到。有IMU的话,还会有一个RECENT_LOST来记录刚刚丢失的状态。
VINS-Mono前端的输入为单目RGB图像和IMU数据。对于RGB信息,首先对图像应用金字塔LK光流检测算法,这里金字塔的使用不同于ORB-SLAM3那样进行不同分辨率的特征提取,而是避免跟踪陷入局部最优。之后进行特征点均匀化,VINS的特征点均匀化策略是以特征点为中心画圈,这个圆内不允许提取其他特征点,并使用逐次逼近去畸变。再之后使用OpenCV提供的光流跟踪状态位、图像边界、对极约束进行outlier的剔除。最终前端输出结果包含特征点像素坐标、去畸变归一化坐标、特征点ID以及特征点速度。对于IMU信息,VINS-Mono还需要计算IMU预积分。ORB-SLAM3和VINS的预积分都没有依赖GTSAM,都是自己实现的。
具体的优缺点上,ORB-SLAM3是对每一帧图像都提取Fast角点和BRIEF描述子,并进行描述子匹配。而BRIEF描述子的计算和匹配非常耗时。虽然ORB-SLAM3使用了SearchByBoW等策略加速匹配,但仍然需要较多的计算时间。相较之下,LK光流只需要提取每帧图像中跟丢的特征点,计算时间大大降低。不过ORB特征的全局一致性更好,在中期、长期数据关联上要优于光流法。我觉得VINS课程讲师的描述很贴切:“VINS更适合一条路跑到头进行定位建图,ORB-SLAM3更适合具有回环的轨迹”
残差加权重的话,需要在代价函数表达式里加入信息矩阵。但Ceres Solver中只接受最小二乘优化,也就是说代价函数定义(自动求导和数值求导是重载括号运算符bool operator(),解析求导是重载虚函数virtual bool Evaluate())里没办法直接写信息矩阵表达式。因此需要对信息矩阵进行Cholesky分解,把分解后的矩阵跟原来的残差乘到一起变为新的残差。
ORB中有个方法采用运动模型,利用上一帧位姿粗略估计当前针位姿,然后利用3D-2D的匹配,计算投影点的附近圆形区域内所有特征点的匹配程度,减少匹配的计算量
提升精度方面,最近看到一个GMS的匹配算法。考虑特征点附近区域内特征描述子点来提高特征描述子的区分度
硬件加持:
PnP(Perspective-n-Point)
是求解 3D 到 2D 点对运动的方法。它描述了当我们知道n 个 3D 空间点以及它们的投影位置时,如何估计相机所在的位姿。两张图像中,其中一张特征点的 3D 位置已知,那么最少只需三个点对(需要至少一个额外点验证结果)就可以估计相机运动。特征点的 3D 位置可以由三角化,或者由 RGB-D 相机的深度图确定。因此,在双目或 RGB-D 的视觉里程计中,我们可以直接使用 PnP 估计相机运动。而在单目视觉里程计中,必须先进行初始化,然后才能使用 PnP。3D-2D 方法不需要使用对极约束,又可以在很少的匹配点中获得较好的运动估计,是最重要的一种姿态估计方法。
最简单的思路就是做动态特征点剔除,在图像帧上使用YOLO等目标检测网络,或者使用SegNet等语义分割网络检测动态物体,对于候选区域就不再提取特征点了,然后进行运动一致性检查进一步提高检测精度。
这种方法高效、鲁棒。缺点主要有两个,一个是只能检测训练过的类别,对于未训练的运动目标无法检测。另一个是很难检测目前静止的潜在运动对象。
ORB的B指的是brief描述子,是一个二进制的描述子,计算和匹配的速度都很快。区别在于ORB特征描述子具备旋转不变性,而FAST不具备。并且orb-slam利用灰度质心法计算了旋转不变性,利用金字塔在不同分辨率图像上进行特征提取和匹配。
ORB特征点由关键点和描述子两部分组成,关键点称为“Oriented FAST”,是一种改进的FAST角点,相较于原版的FAST,ORB通过灰度质心法计算特征点的主方向(对特征点周围像素的坐标根据灰度值加权平均得到中心坐标)然后计算旋转后的BRIEF描述子。
BRIEF描述子通过对前一步关键点的周围图像区域选取128对点,根据每对点灰度值大小关系分别取0、1,得到128维0、1组成的向量即描述子。
SLAM运行过程中会产生轨迹漂移,回环检测就是当机器人回到以前运行过的场景时,利用约束将轨迹漂移拉回来。
视觉SLAM回环的主流方法就是词袋,即用词袋建立数据库,看关键帧中有没有在数据库中有比较“接近”的关键帧。但是词袋的使用方式有所不同。
ORBSLAM3的闭环线程流程:
VINS-Mono的闭环检测:
对于双目相机来说,首先要对左右目的两个相机进行单目标定,得到各自的内参矩阵和畸变系数。然后标定左右目相机之间的外参,当两个相机位于同一平面时,旋转矩阵可近似为单位阵,标定平移外参可得到基线长度b。
主要流程为:
IMU可以获得每一时刻的加速度和角速度,通过积分就可以得到两帧之间的由IMU测出的位移和旋转。但IMU的频率远高于相机,在基于优化的VIO算法中,当被估计的状态量不断调整时,每次调整都需要在它们之间重新积分,传递IMU测量值,这个过程会导致计算量的爆炸式增长。因此提出了IMU预积分,希望对IMU的相对测量进行处理,使它与绝对位姿解耦,或者只要线性运算就可以进行矫正,从而避免绝对位姿被优化时进行重复积分。
ORB-SLAM3没有依赖GTSAM库,整个预积分的过程都是自己实现的。具体思路为:
光流法和直接法都是基于灰度不变假设。光流法提取关键点后,根据灰度不变假设得到关于像素点速度的欠定方程,之后通过对极约束、PNP、ICP计算R、t。但光流法只是一个粗略匹配,很容易陷入局部极值(VINS使用金字塔来改善),直接法相当于在光流基础上对前面估计的相机位姿进一步优化,即将下一帧位置假设为1/z*K(RP+t),之后利用光度误差来优化R、t。
光流法和直接法根据提取关键点数量可分为稀疏、半稠密、稠密。相较于特征点法来说,提取速度很快,但要求相机运动较慢,或者采样频率较高,对光照变化也不够鲁棒。
SLAM属于渐进式算法,需要使用一定数目的关键帧进行BA运算以求得位姿和地图点。但随着关键帧数目的增多,滑窗(或者说ORB-SLAM3的共视图)的计算量会越来越大。因此需要想办法去掉老的关键帧,来给新关键帧腾地方。直接做法是把最老帧以及相关地图点直接丢弃,但最老帧可能包含了很强的共视关系,直接删除可能会导致BA效果变差。
而边缘化就是指,既从滑窗/共视图里剔除了老的关键帧,又能保留这个关键帧的约束效果,其核心算法是舒尔补。总体来说,就是本身要被扔掉的最老帧,他可能和后面的帧有约束关系(一起看到某个地图点,和下一帧之间有IMU约束等),扔掉这个第0帧的数据的话,这些约束信息需要被保留下来。相当于建立起地图点和地图点之间的约束关系。
可以减小环境匹配约束(如scan-to-map)的权重,具体可以通过设置核函数:环境匹配残差较小时正常引入环境约束,过大时根据残差与设定阈值的比例设置权重。
PnP位姿估计最少需要7个点(3对点以及一个验证点),通过SVD分解求解位姿时会得到4组解。
误差来源主要是:
关键帧是在一系列普通帧中选择出的最有代表性的一帧。
关键帧和地图点都是以类的方式存储的。
关键帧中除了包括位姿之外,还包括相机内参,以及从图像帧提取的所有ORB特征(不管是否已经关联了地图点云,这些特征点都已经被畸变矫正过)。
地图点中除了存储空间点坐标,同时还存储了3D点的描述子(其实就是BRIFE描述子),用来快速进行与特征点的匹配,同时还用一个map存储了与其有观测关系的关键帧以及其在关键帧中的Index等等。
H矩阵和F矩阵适用于当前场景的不同状态。
F矩阵(基础矩阵)在纯旋转以及空间点在一个平面上的情况时效果不好,H矩阵(单应矩阵)推导即针对特征点在同一平面时计算出的,一定程度上弥补其缺陷。
因此当计算出两个矩阵的值后分别计算重投影误差得到分数,比较哪个矩阵的得分占比更高则取哪个矩阵恢复位姿,在ORB-SLAM系列中作者倾向于采取H矩阵恢复,当比值大于0.4时即选择H矩阵。
list
、list
分别保存按照权重从大到小排列后的共视关键帧及其权重。vins:设置mask限制特征点之间的距离,在现有的特征点范围内画一个mask在这个mask范围内不再提取特征点。
以IMU和相机两种传感器为例。视觉传感器和惯性传感器有一定的互补性,IMU更适合估计短时间的高速运动,视觉传感器更适合估计长时间低速的运动。
两种耦合方式的区别:
(2) 紧耦合:IMU和相机共同构建运动方程和观测方程,再进行状态估计。紧耦合方式只存在一个优化问题,它将视觉和惯性放在同一个状态向量中,利用视觉测量信息(如特征点)和惯性测量信息构建(如加速度、角速度)包含视觉残差和惯性测量残差的误差项,同时优化视觉状态变量和惯性测量状态变量。
一般采取紧耦合方案,因为这样可以更好的利用两类传感器的互补性质,使两类传感器相互校正,达到更好的效果。
对于更多传感器,紧耦合只是根据各传感器的特点,在系统状态向量中加入新的状态,同时利用各传感器的测量信息构建各自的误差项,最终通过使总误差最小,对状态向量进行优化。
BA是指通过调整相机的位姿和特征点的3D位置,使总体的重投影误差最小。
从非线性优化的角度来看,代价函数是特征点的像素坐标与三维点经过投影后的像素坐标进行作差得到的重投影误差,优化变量是相机位姿和三维点位置,优化方法一般可以采取高斯牛顿法(GN)或列文伯格-马夸尔特法(LM)。
BA优化的过程是通过对最小二乘形式的代价函数进行求解,将相机位姿和三维点位置(优化变量)从粗略的初始值调整到更精确的值。
单目vo的尺度漂移是由于,尺度不确定,以及位姿估计,地图点的估计。位姿估计和地图点估计相辅相成,计算误差,测量误差,会在这个过程中一步一步的传播下去。
要解决这个尺度漂移的话问题,可以考虑中断这个误差传播链条,比如双目,rgbd相机可以直接计算特征点的深度,所以地图点的误差就被固定在一定范围,从而限制了整个估计的误差传播发散。
同时imu传感器可以提供绝对尺度,虽然imu位姿长时间发散严重,但是短时间内的位姿估计可以认为误差有个上限,从而vio也可以在位姿估计上限制误差传播。
全流程BA优化最耗时,但优化是单独一个线程,且不要求实时进行,所以没有特别强的加速需求。一般加速考虑从前端跟踪部分着手,特征提取方面ORB速度已经是目前比较快的策略,但特征匹配比较耗时,可以考虑采用GMS等更新的特征匹配算法替换。
当相机做纯旋转,平移为0的时候无法计算E矩阵,因为推导本质矩阵E和基础矩阵F的过程是通过两不同位置对同一3D点观测列两组方程,联立推导出对极约束,而当没有平移时不满足不同位置,推到中的t=0,本质矩阵E也为0,无法计算。
区别:
联系:
基本理论都是一致–多视图几何。传统方法都是做特征点提取与匹配,都需要最小化视觉投影误差。
ORB-SLAM2和3系列在特征匹配部分可以采用GMS算法,准确率更高速度也能适当提升;另外前端跟踪部分可以用光流法跟踪普通帧,关键帧仍采用特征点进行跟踪,这个应该已经有论文实现了;但对这种方法应该可以调整关键帧插入策略,在用光流法跟踪大部分普通帧之后可以根据设计当前环境的光流跟踪难度系数,避免出现光流跟不上的情况。
Ceres
库中的自动求导是基于Jet
类型实现的,利用链式法则来计算函数的导数。在计算图中,每个节点代表一个计算步骤,节点的输入是前一步的输出,输出是当前步骤的计算结果,节点是Jet
类型,其向量部分存储该点处函数的导数值
相机内参在相机出厂之后是固定的,有fx,fy,cx,cy四个参数,相机内参描述了从图像坐标系到像素坐标系之间的关系,即进行单位变换和平移。
四个参数的单位均为像素。所以当图片方法两倍,四个内参数均放大两倍。
相机外参即相机的位姿R,t,描述的是世界坐标系和相机坐标系之间的关系,外参会随着相机运动发生改变。
定义Global
坐标系为固定坐标系,如导航中常用的东北天坐标系;local
坐标系为绑定在运动体上的坐标系,是随动的。
一般旋转扰动的定义是在机体local
坐标系上添加一个小的旋转θ
,这样扰动的旋转角比较小,可以保证比较好的线性而且避免奇异性。
pose
表达在Global
坐标系时,根据旋转的叠加方式,添加的是右扰动;pose
表达在local
坐标系时,根据旋转的叠加方式,添加的是左扰动。BA:是把所有的三维点和位姿放在一起作为自变量进行非线性优化;
pose graph:是优化所有的位姿,目的是将位姿之间的不同方式计算得到约束残差最小;
本质上BA包含了pose graph
优化,两种优化方法都会转换为求解正规方程,两者形式完全一样;
pose graph与BA的区别,仅仅体现在BA中使用三维点间接地表达pose间的约束关系。
Huber是设置LossFunction
时设置的,是核函数的一种,主要就是减少异常值对优化结果的影响,一般定义语句就是loss_function = new ceres::HuberLoss(value)
一、原理:
在构建点-线残差之前,Lego-LOAM已经进行了特征提取操作,将属于边缘点的点云加至Fme,将属于平面点的点云加至Fmp,因此找最近点构建点线残差时,从直线特征集中选择的5个点。
虚函数用来实现C++的多态,在父类对象成员函数前面加上virtual,然后在子类中重写这个函数,具体思想是在程序运行时用父类指针来执行子类对象中不同的函数。如果不使用虚函数,那么函数地址在编译阶段就已经确定,执行时还是调用的父类函数。使用虚函数后,函数地址是在运行阶段才确定的,这样执行时就可以调用子类函数。
析构函数不一定都要定义为虚函数,只有子类对象在堆区开辟了数据才需要。这是因为父类指针在释放时无法调用到子类的析构函数,造成内存泄漏。虚析构函数就是通过父类指针释放子类对象。
尖括号< >
括起来表明这个文件是一个工程或标准头文件。查找过程会检查预定义的目录,我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。
如果文件名用一对" "
引号括起来则表明该文件是用户提供的头文件,查找该文件时将从当前文件目录(或文件名指定的其他目录)中寻找文件,然后再到标准位置寻找文件。
const的优点:
int a = 1;
int b = 2;
int c = 3;
int const *p1 = &b; // const在前,p1为常量指针
int *const p2 = &c; // * 在前,p2为指针常量
//注意:允许将非const对象的地址赋给指向const对象的指针,所以第4行代码是正确的
常量指针p1:即指向const对象的指针,指向的地址可以改变,但其指向的内容(即对象的值)不可以改变。
//p1可以改变,但不能通过p1修改其指向的对象(即 b)的值;不过,通过其他方式修改b的值是允许的
p1 = &a; //正确,p1是常量指针,可以指向新的地址(即&a),即p1本身可以改变
*p1 = a; //错误,*p1是指针p1指向对象的值,不可以改变,因此不能对*p重新赋值
指针常量p2:指针本身是常量,即指向的地址本身不可以改变,但内容(即对象的值)可以改变。
p2 = &a; //错误,p2是指针常量,本身不可以改变,因此将a的地址赋给p2是错误的
*p2 = a; //正确,p2指向的对象允许改变
补充:要分辨是常量指针还是指针常量,可以从右向左来看其定义,具体如下:
①对于 int const *p1=&b
,先将*
和p1
结合,即p1
首先是一个指针,然后再左结合const
,即常量指针,它指向了const
对象,因此我们不能改变 *p1
的值。
②对于 int *const p2=&c
,现将const
和p2
结合,即p2
首先是一个常量,然后再左结合*
,即指针常量,它本身是一个常量,因此我们不能改变p2
本身。另外因为p2
本身是const
,而const
必须初始化,因此p2
在定义时必须初始化,即不能直接 int *const p2
;
普通指针如果delete不当会造成内存泄露或者生成野指针,而智能指针实际上是对指针加了一层封装机制,使得它可以方便的管理对象的生命周期。在智能指针中,一个对象什么时候被析构是受智能指针本身决定的,用户不需要进行管理。
优点:智能指针不需要手动释放空间,能够避免内存泄露
缺点:1、有传染性,用了一处之后导致很多传参的位置也得修改;2、如果类的内部组合或者聚合了一些别的类,这些类的析构又不想在这个类处理的时候,智能指针析构的不确定性会导致这些类无法按要求进行管理
引用就是某一变量(目标)的别名,对引用的操作与对变量直接操作完全一样。
引用传参的好处:
引用作为函数返回值:
<div style="background-color:white">
#include
using namespace std;
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
int main()
{
float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
//float &b=fn1(10.0); //第2种情况,编译不通过,左值引用不能绑定到临时值
float &d=fn2(10.0); //第3种情况,系统不生成返回值的副本,可以从被调函数中返回一个全局变量的引用
float c=fn2(10.0); //第4种情况,变量c前面不用加&号,这种也是可以的
cout<<a<<endl<<c<<endl<<d<<endl;
return 0;
}
</div>
引用作为返回值,必须遵守以下规则:
std::map
对应的数据结构是红黑树。红黑树是一种近似于平衡的二叉查找树,里面的数据是有序的。在红黑树上做查找、插入、删除操作的时间复杂度为O(logn)
。
而std::unordered_map
对应哈希表,哈希表的特点就是查找效率高,时间复杂度为常数级别O(1)
, 而额外空间复杂度则要高出许多。
对于像 int/char/long
等等这些基本数据类型,使用new
分配的不管是数组还是非数组形式的内存空间,delete
和delete[]
都可以正常释放,不会存在内存泄漏。原因是:分配这些基本数据类型时,其内存大小已经确定,系统可以记忆并且进行管理,在析构时不会调用析构函数,它通过指针可以直接获取实际分配的内存空间。
int *a = new int[10];
delete a; //正确
delete [] a; //正确
对于自定义的Class类,delete和delete[]会有一定的差异。先看一段代码示例:
class T {
public:
T() { cout << "constructor" << endl; }
~T() { cout << "destructor" << endl; }
};
int main() {
T* p1 = new T[3]; //数组中包含了3个类对象
cout << hex << p1 << endl; //输出P1的地址
delete[] p1; //这里会输出3个destructor,调用三次析构函数
T* p2 = new T[3];
cout << p2 << endl; //输出P2的地址
delete p2; //这里只输出1个destructor,调用1次析构函数
return 0;
}
delete[]
会调用析构函数对数组的所有对象进行析构;而delete
只调用了一次析构函数,即数组中第一个对象的析构函数,而数组中剩下的对象的析构函数没有被调用,因此造成了内存泄漏。
vector<int> vec; //C++98/03给vector对象的初始化方式
vec.push_back(1);
vec.push_back(2);
vector<int> vec{1,2}; //C++11给vector对象的初始化方式
vector<int> vec = {1,2};
类的拷贝构造函数是在类对象被创建时调用的,是在新对象被创建时,用一个已有对象来初始化新对象。拷贝构造函数的形式为:
class_name (const class_name & object);
赋值构造函数只能是在已存在的对象调用,赋值构造函数是在一个已有对象被赋值给另一个对象时被调用。赋值构造函数的形式为:
class_name & operator= (const class_name & object);
判断两者的方法是看是否有新对象实例生成。
注意点:如果类中有指针成员变量,那么需要重载拷贝构造函数和赋值构造函数来进行深拷贝,避免出现悬空指针或野指针的问题
Mat image = imread("image.jpg");
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY); // Step 2: 将图像转换为灰度图
GaussianBlur(gray, gray, Size(3,3), 0);// Step 3: 使用高斯滤波对图像进行平滑
// Step 4: 计算图像的梯度幅值和方向
Mat grad_x, grad_y;
Sobel(gray, grad_x, CV_32F, 1, 0);
Sobel(gray, grad_y, CV_32F, 0, 1);
Mat grad;
magnitude(grad_x, grad_y, grad);
// Step 5: 使用阈值进行边缘检测
Mat edges;
threshold(grad, edges, 50, 255, THRESH_BINARY);
// Step 6: 使用霍夫变换检测直线
vector lines;
HoughLinesP(edges, lines, 1, CV_PI/180, 50, 50, 10);
// Step 7: 找到四边形的边界
for (Vec4i line : lines) {
// 使用线段坐标来找到四边形的边界
}
C++的标准模板库(STL)中提供了4种智能指针:auto_ptr
、unique_ptr
、share_ptr
、weak_ptr
,其中后面3种是C++11的新特性,而 auto_ptr
是C++98中提出的,已经被C++11弃用了,取而代之的是更加安全的 unique_ptr
1. auto_ptr
智能指针 auto_ptr
由C++98引入,定义在头文件 中,在C++11中已经被弃用了,因为它不够安全,而且可以被 unique_ptr
代替。那它为什么会被 unique_ptr
代替呢?先看下面这段代码:
#include
#include
#include
using namespace std;
int main() {
auto_ptr<string> p1(new string("hello world."));
auto_ptr<string> p2;
p2 = p1; //p2接管p1的所有权
cout << *p2<< endl; //正确,输出: hello world.
//cout << *p1 << endl; //程序运行到这里会报错
//system("pause");
return 0;
}
2. unique_ptr
unique_ptr
同 auto_ptr
一样也是采用所有权模式,即同一时间只能有一个智能指针可以指向这个对象 ,但之所以说使用 unique_ptr
智能指针更加安全,是因为它相比于 auto_ptr
而言禁止了拷贝操作, unique_ptr
采用了移动赋值 std::move()
函数来进行控制权的转移。
3. share_ptr
共享指针 share_ptr
是一种可以共享所有权的智能指针,定义在头文件memory
中。
它允许多个智能指针指向同一个对象,多个指针引用同一个变量时会增加其引用次数,指针消亡时会自动减少引用次数,直到引用次数到零时将内存空间回收。并使用引用计数的方式来管理指向对象的指针(成员函数use_count()
可以获得引用计数),该对象和其相关资源会在“最后一个引用被销毁”时候释放。
share_ptr
是为了解决 auto_ptr
在对象所有权上的局限性(auto_ptr
是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
4. weak_ptr
weak_ptr
弱指针是一种不控制对象生命周期的智能指针,它指向一个 share_ptr
管理的对象,进行该对象的内存管理的是那个强引用的 share_ptr
,也就是说 weak_ptr
不会修改引用计数,只是提供了一种访问其管理对象的手段,这也是它称为弱指针的原因所在。
此外,weak_ptr
和 share_ptr
之间可以相互转化,share_ptr
可以直接赋值给weak_ptr
,而weak_ptr
可以通过调用 lock
成员函数来获得share_ptr
。
share_ptr无法解决循环引用的问题,即A引用了B,B同时也引用了A,这种情况会导致引用计数机制失效,解决方法是将其中一个指针转换为weak_ptr,就是仅保存指针但是不负责计数,在需要使用的时候lock住,使用完成后释放。
new
和delete
是C++的关键字,是一种操作符,可以被重载
malloc
和free
是C语言的库函数,并且不能重载
malloc
使用时需要自己显示地计算内存大小,而new
使用时由编译器自动计算
int *q = (int *)malloc(sizeof(int) * 2); //显示计算内存大小
int *p = new int[2]; //编译器会自动计算
malloc
分配成功后返回的是void*
指针,需要强制类型转换成需要的类型;而new
直接就返回了对应类型的指针
new
和delete
使用时会分别调用构造函数和析构函数,而malloc
和free
只能申请和释放内存空间,不会调用构造函数和析构函数
注意:delete
和free
被调用后,内存不会立即回收,指针也不会指向空,delete
或free
仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,这时候就会出现野指针的情况。因此,释放完内存后,应该把指针指向NULL
。
多态的实现主要分为静态多态和动态多态,静态多态主要是重载overload
,在编译时就已经确定;而动态多态是通过虚函数机制来实现,在运行时动态绑定。
int main() {
Base a;
Derived b; //派生类的对象
Base *p1 = &a; //基类的指针,指向基类的对象
Base *p2 = &b; //基类的指针,指向派生类的对象
//f1()是虚函数,只有运行时才知道真正调用的是基类的f1(),还是派生类的f1()
p1->f1(); //p1指向的是基类的对象,所以此时调用的是基类的f1()
p2->f1(); //基类的指针调用派生类重写的虚函数 Derived::f1(), 打印结果为 Derived::f1
return 0;
}
如果基类通过引用或者指针调用的是非虚函数,无论实际的对象是什么类型,都执行基类所定义的函数。即:
int main() {
Base a;
Derived b;
Base *p = &a; //基类的指针,指向基类的对象
Base *p0 = &b; //基类的指针,指向派生类的对象
//f()是非虚函数,所以无论指向是基类的对象a还是派生类的对象b,执行的都是基类的函数f()
p->f();
p0->f();
return 0;
}
**多态性其实就是想让基类的指针具有多种形态,能够在尽量少写代码的情况下让基类可以实现更多的功能。**比如说,派生类重写了基类的虚函数f1()之后,基类的指针就不仅可以调用自身的虚函数f1(),还可以调用其派生类的虚函数f1()。
多线程是实现并发的一种手段,一个进程可以有多个线程,每个线程负责不同的事情,多个线程可以提高代码执行效率。 不同进程数据很难共享,同一进程下的不同线程数据可以共享,不同线程可以使用互斥锁,共享锁来处理多个线程的共享内存 。
ORB中有主线程(跟踪),局部建图线程,闭环线程
虚函数知识点:
virtual
指明的函数override
其基类的成员函数virtual
关键字,但不是必须这么做虚函数的工作机制:
主要思路:虚函数表 + 虚表指针
具体实现:
注意:每个类都只有一个虚函数表,该类的所有对象共享这个虚函数表,而不是每个实例化对象都分别有一个虚函数表。
C++类的多态性是通过虚函数来实现的。如果基类通过引用或指针调用的是虚函数时,我们并不知道执行该函数的对象是什么类型的,只有在运行时才能确定调用的是基类的虚函数还是派生类中的虚函数,这就是运行时多态。
class Cat {
public:
virtual void eat()=0;
};
含有纯虚函数的类称为抽象类。抽象类有以下几个特点:
接口类:
构造函数不能是虚函数,原因是构造函数在创建对象时必须确定对象类型。具体的理解就是:
析构函数可以是虚函数吗?
可以,如果基类存在虚函数以实现多态,而基类的析构函数没有声明为虚函数,那么在析构一个指向派生类的基类指针时,就只会调用基类的析构函数,不会调用派生类的析构函数,因此会造成内存泄漏的问题。
区别:
strlen
是一个函数,要在运行时才能计算。只能以char*
(字符串)作为参数(当数组名作为参数传入时,实际上数组就退化成指针了),用来计算指定字符串 str
的长度,但不包括结束字符 '\0'
。所以其参数必须是以'\0'
作为结束符才可以正确统计其字符长度,否则是个随机数,具体看下面的代码。sizeof
是一个运算符,它的参数可以是数组、指针、字符串、对象等等,计算的是参数所对应内存空间的实际字节数。由于在编译时计算,因此sizeof
不能用来返回动态分配的内存空间的大小#include
using namespace std;
int main() {
char* s1 = "0123456789";
cout<<sizeof(s1)<<endl; // 输出 8,因为这时的参数 s 是一个指向字符串常量的字符指针,因此计算的是指针的大小,注意这里不同编译器得到的值可能不同,也有可能是4
cout<<sizeof(*s1)<<endl; // 输出 1,*s 是第一个字符
cout<<strlen(s1)<<endl; // 输出 10,有10个字符,strlen是个函数,内部实现是用一个循环计算到\0之前为止
//strlen(*s1); // 报错,因为strlen函数的参数类型只能是 char* 即字符串
char s2[] = "0123456789"; // 数组
cout<<sizeof(s2)<<endl; // 结果为11,数组名虽然本质上是一个指针,但是作为sizeof的参数时,计算的是整个数组的大小,这点要特别注意。且在求动态数组的大小时,sizeof统计到第一个结束字符'\0'处结束
cout<<strlen(s2)<<endl; // 结果为10
cout<<sizeof(*s2)<<endl; // 结果为1,*s是第一个字符
char s3[100] = "0123456789";
cout<<sizeof(s3)<<endl; // 结果为100,因为内存给数组 s3分配了字节数为100的空间大小
cout<<strlen(s3)<<endl; // 结果为10
int s4[100] = {0,1,2,3,4,5,6,7,8,9};
cout<<sizeof(s4)<<endl; // 结果为400,因为int数组中每个元素都是int型,int型占用4字节
//strlen(s4); // 报错,strlen不能以int* 作为函数参数
char p[] = {'a', 'b','c','d','e', 'f', 'g','h'};
char q[] = {'a', 'b','c','d','\0', 'e', 'f', 'g'};
cout<<sizeof(p)<<endl; // 结果为8
cout<<strlen(p)<<endl; // 结果是一个随机数,因为字符串数组中没有结束字符 '\0', 因此该函数会一直统计下去,直到碰到内存中的结束字符
cout<<sizeof(q)<<endl; // 结果还是8
cout<<strlen(q)<<endl; // 结果为4, 结束字符 '\0'前有4个字符
return 0;
}
另外sizeof
在统计结构体的大小时还有一个内存对齐的问题,具体如下:
struct Stu {
int i;
int j;
char k;
};
Stu stu;
cout<<sizeof(stu)<<endl; // 输出 12
这个例子是结构体的内存对齐所导致的,计算结构变量的大小就必须讨论数据对齐问题。为了CPU存取的速度最快(这同CPU取数操作有关),C语言在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但理论上速度快了。当然这样的设置会在读写一些别的应用程序生成的数据文件或交换数据时带来不便。
c/c++中获取字符串长度。有以下函数:str.size()、sizeof() 、strlen()、str.length();
strlen(str)
和str.length()
和str.size()
都可以求字符串长度。str.length()
和str.size()
是用于求string类对象的成员函数strlen(str)
是用于求字符数组的长度,其参数是char*。char* ss = "0123456789";
//sizeof(ss)为8,ss是指向字符串常量的字符指针,sizeof 获得的是指针所占的空间,则为8
//sizeof(*ss)为1,*ss是第一个char字符,则为1
char ss[] = "0123456789";
//sizeof(ss)为11,ss是数组,计算到'\0'位置,因此是(10+1)
//sizeof(*ss)为1,*ss是第一个字符
char ss[100] = "0123456789";
//sizeof(ss)为100,ss表示在内存中预分配的大小,100*1
//strlen(ss)为10,它的内部实现用一个循环计算字符串的长度,直到'\0'为止。
int ss[100] = {0,1,2,3,4,5,6,7,8,9};
//sizeof(ss)为400,ss表示在内存中预分配的大小,100*4
//strlen(ss)错误,strlen参数只能是char*,且必须是以'\0'结尾
char[] a={'a','b','c'};
//sizeof(a)的值应该为3。
char[] b={"abc"};
//sizeof(b)的值应该是4。
string str={'a','b','c','\0','X'};
//sizeof(str)为5,strlen(str)为3。
sizeof(),strlen()两者区别:
sizeof
操作符的结果类型是size_t
,它在头文件中typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。sizeof
是运算符,strlen
是函数。sizeof
可以用类型做参数,strlen
只能用char*
做参数,且必须是以\0
结尾的。sizeof
还可以用函数做参数,比如:printf("%d\n", sizeof(f()));
sizeof
的参数不退化,传递给strlen
就退化为指针了。sizeof
计算过了,是类型或是变量的长度。这就是sizeof(x)
可以用来定义数组维数的原因strlen
的结果要在运行的时候才能计算出来,用来计算字符串的长度,不是类型占内存的大小。sizeof
后如果是类型必须加括弧,如果是变量名可以不加括弧。这是因为sizeof
是个操作符不是个函数。c++中的size()和length()没有区别
为了兼容,这两个函数一样。 length()
是因为沿用C语言的习惯而保留下来的,string类最初只有length()
,引入STL之后,为了兼容又加入了size()
,它是作为STL容器的属性存在的,便于符合STL的接口规则,以便用于STL的算法。 string类的size()/length()
方法返回的是字节数,不管是否有汉字
函数原型:char *strcpy(char *dest, const char *src)
char* strcpy(char* dest,const char* src)//src到dest的复制
{
if(dest == nullptr || src == nullptr)
return nullptr;
char* strdest = dest;
while((*strdest++ = *src++) != '\0') {};
return strdest;
}
函数功能:把 src
地址开始且包括结束符的字符串复制到以 dest
开始的地址空间,返回指向 dest
的指针。需要注意的是,src
和 dest
所指内存区域不可以重叠且 dest
必须需有足够的空间来容纳 src
的字符串,strcpy
只用于字符串复制。
安全性:strcpy
是不安全的,strcpy
在遇到结束符时才会正常的结束运行,会因为 src
长于 dest
而造成 dest
栈空间溢出以致于崩溃异常,它的结果未定,可能会改变程序中其他部分的内存的数据,导致程序数据错误,不建议使用。
代码举例:
char src1[10] = "hello";
strcpy(src1, src1+1);
cout<<"src1:"<<src1<<endl; // 输出 ello。
char src2[10] = "hello";
strcpy(src2+1, src2);
cout<<"src2:"<<src2<<endl; // 输出 hhello,按照内存重叠逻辑理解,应该输出 hhhhh……,后面是随机两才对,因为'\0'被覆盖,而strcpy要遇到'\0'才会停止复制。
// 可能对于内存重叠的问题,每种编译器的定义不一样
char src3[10] = "hello";
char dest3[3];
strcpy(dest3, src3);
cout<<"dest3:"<<dest3<<endl; // 输出 hello,非常奇怪的是居然没报错,dest3的空间不是比src3的小吗?
// 注意下面这个用例体现了 strcpy 与 strncpy 的区别
char *src4 = "best";
char dest4[30] = "you are the best one.";
strcpy(dest4+8, src4);
cout<<"dest4:"<<dest4<<endl; // 输出 you are best。字符串最后一个字节存放的是一个空字符——“\0”,用来表示字符串的结束。
// 把src4复制到dest4之后, src4中的空字符会把把复制后的字符串隔断,所以会显示到best就会结束。
函数原型:char* strncpy(char* dest,const char* src,size_t n)
char *strncpy(char *dest, const char *src, int len)
{
assert(dest!=NULL && src!=NULL);
char *temp;
temp = dest;
for(int i =0;*src!='\0' && i<len; i++,temp++,src++)
*temp = *src;
*temp = '\0';
return dest;
}
函数功能:将字符串src
中最多n
个字符复制到字符数组 dest
中(它并不像strcpy
一样只有遇到 NULL
才停止复制,而是多了一个条件停止,就是说如果复制到第n
个字符还未遇到 NULL
,也一样停止),返回指向 dest
的指针。只适用于字符串拷贝。如果src
指向的数组是一个比n
短的字符串,则在 dest
定义的数组后面补'\0'
字符,直到写入了n
个字符。
注意:如果 n>dest
串长度,dest
栈空间溢出产生崩溃异常。一般情况下,使用 strncpy
时,建议将n
置为dest
串长度,复制完毕后,为保险起见,将dest
串最后一字符置NULL
。
安全性:比较安全,当dest
的长度小于n
时,会抛出异常。
如果想把一个字符串的一部分复制到另一个字符串的某个位置,显然strcpy()
函数是满足不了这个功能的,因为strcpy()
遇到结束字符才停止。但是 strncpy
可以:
char *src5 = "best";
char dest5[30] = "you are the best one.";
strncpy(dest5+8, src5, strlen(src5));
cout<<"dest5:"<<dest5<<endl; // 输出 you are bestbest one,注意这里与上面代码最后一个示例的区别
函数原型:void* memcpy(void* dest, const void* src, size_t n)
void *memcpy(void *memTo, const void *memFrom, size_t size)
{
if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效
return NULL;
char *tempFrom = (char *)memFrom; //保存memFrom首地址
char *tempTo = (char *)memTo; //保存memTo首地址
while(size-- > 0) //循环size次,复制memFrom的值到memTo中
*tempTo++ = *tempFrom++ ;
return memTo;
}
函数功能:与strncpy
类似,不过这里提供了一般内存的复制,即memcpy
对于需要复制的内容没有任何限制,可以复制任意内容,因此,用途广泛。
注意:memcpy没有考虑内存重叠的情况,所以如果两者内存重叠,会出现错误。
char *src6 = "best";
memcpy(src6+1, src6, 3); // 报错,内存重叠
三者的区别:
strcpy
只能复制字符串,而memcpy
可以复制任意内容,例如字符数组、整型、结构体、类等。strcpy
不需要指定长度,它遇到被复制字符的串结束符"\0"
才结束,所以容易溢出。memcpy
则是根据其第3个参数决定复制的长度。strcpy
,而需要复制其他类型数据时则一般用memcpy
使用情况:
dest
指向的空间要足够拷贝;使用strcpy
时,dest
指向的空间要大于等于src
指向的空间;使用strncpy
或memcpy
时,dest
指向的空间要大于或等于n
。strncpy
或memcpy
时,n
应该大于strlen(src)
,或者说最好n >= strlen(s1)+1
;这个1 就是最后的“\0”
。strncpy
时,确保dest
的最后一个字符是“\0”
。哈希冲突产生的原因:
因为通过哈希函数产生的值是有限的,而数据可能比较多,导致通过哈希函数映射后仍有很多不同的数据对应了相同的哈希值,这时候就产生了哈希冲突。
解决哈希冲突的四种方法:
key
的哈希地址p=H(key)
出现冲突时,以p
为基础,产生另一个哈希地址p1
,如果p1
仍然冲突,再以p
为基础,产生另一个哈希地址 p2
,直到找出一个不冲突的哈希地址,将相应元素存入其中。H(key)
为哈希函数, m m m为表长, d i d_{i} di为增量序列。增量序列 d i d_{i} di的取值方式不同,相应的再散列方式也不同,主要有以下三种:2. 链式地址法
基本思想:将所有哈希地址为的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 个单元中,因而查找、插入和删除都比较方便。
3. 建立公共溢出区
基本思想:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表中。
4. 再哈希表
基本思想:对于冲突的哈希值,再构造另一个哈希函数进行处理,直至没有哈希冲突,也就是同时使用多个哈希函数来处理。
右值为临时值,不能存在于等号左边的值,不可以取地址;右值引用可以提高效率,提高的手段是:把拷贝对象变成移动对象来提高程序运行效率。右值引用就是为了应付移动构造函数和移动赋值运算符用的,而移动构造函数和移动赋值运算符是为了提高程序运行效率。
Eigen
提供了直接求逆的方法inverse()
,但是只针对小矩阵。求解大矩阵的话需要用一些数学分解算法,然后调用solve()
函数。solve()
函数其实是用来求解线性方程组ax=b
,ab
是系数矩阵,x
是变量,b
为单位阵时x
为a
的逆矩阵。具体的分解算法包括部分主元LU
分解(对应Eigen
中的PartialPivLU
)、全主元LU分解(FullPivLU
)、Householder
QR分解(HouseholderQR
)、列主元Householder QR
分解(ColPivHouseholderQR
)、全主元Householder QR
分解(FullPivHouseholderQR
)、完全正交分解COD(CompleteOrthogonalDecomposition
)、标准Cholesky分解(LLT
)、鲁棒Cholesky分解(LDLT
)、双对角分治SVD分解(BDCSVD
)、雅各比svd分解(JacobiSVD
)。
卡尔曼滤波状态预测的协方差矩阵,表示了状态量的间的相关性,对角线表示状态量的协方差
卡尔曼滤波预测过程中:该协方差矩阵和状态量的转移矩阵 Φ k , k + 1 \Phi_{k,k+1} Φk,k+1,系统噪声的协方差矩阵有关。该过程状态量的协方差矩阵是个放大的过程;
卡尔曼滤波更新过程中:可以简写为 ( I − K H ) P k + 1 , k (I-KH)P_{k+1,k} (I−KH)Pk+1,k,是一个协方差矩阵减小的过程。因为观测进来以后修正了状态量,从而协方差矩阵减小。控制住了原系统状态量误差的传播过程。
根据该协方差矩阵,一般可以判断滤波器是否收敛,过度收敛等情况,一般在滤波器中可以对该矩阵设置下限。
该协方差矩阵初始值设定中,对于可观性强的状态量,可以设置的较大;对于可观性不强的状态量,设置不宜过大,状态量初始值应该尽量准确;
可以强跟踪的卡尔曼滤波器,可以通过增加一个系数给该协方差矩阵,使得其变大,滤波器将基于观测更多的权重,从而实现短时强跟踪。
主要是进行硬同步和软同步。软同步是利用时间戳进行不同传感器的匹配,将各传感器数据统一到某个基准上,可以借助ROS::message_filter实现。硬同步可以借助PTP协议。
LU 分解法是将矩阵 A 分解成两个矩阵 L 和 U,其中 L 是一个下三角矩阵,U 是一个上三角矩阵。然后通过消元的方法来求解线性方程组。
优点是求解速度快,算法稳定,常用于求解方程组的解。缺点是只能求解正定矩阵。
QR 分解法是将矩阵 A 分解成两个矩阵 Q 和 R,其中 Q 是一个正交矩阵,R 是一个上三角矩阵。然后通过消元的方法来求解线性方程组。
优点是求解速度快,算法稳定,常用于求解线性方程组的解。缺点是只能求解满秩矩阵。
SVD 分解法是将矩阵 A 分解成三个矩阵 U、Σ 和 V,其中 U 和 V 是正交矩阵,Σ 是一个对角矩阵。SVD 分解法通常用于求解线性方程组的最小二乘解,广泛应用于数据拟合和回归分析。
优点是算法稳定,能求解非满秩矩阵,能求解最小二乘解。缺点是求解速度较慢,不适用于求解大规模线性方程组。
利用扰动模型,对李群左乘以微小扰动,然后对扰动求导
对于二阶矩协方差,描述了样本数据偏离均值的统计情况,越大则代表数据偏离程度越高,即数据越不稳定.
因此考虑slam的状态变量时,状态的协方差也就代表了当前状态估计正确程度。SLAM中协方差矩阵表达了对状态估计的不确定程度,也就是说在多传感器融合过程中我们认为哪个传感器估计的状态更准,就给哪个估计施加更大的权重。比如VINS-Mono中,引用沈邵劼老师的话就是“IMU和视觉信息融合也可以粗暴的叫做加权平均,那么加权就是每个测量值对应的协方差矩阵”。
另一方面来说,在进行图优化时,误差是要分摊给每一条边的。那么每条边应该分摊多少呢?显然均摊是不合适的,因为有的观测可能很准。这时候协方差矩阵就确定了应该给每条边分摊多少。
矩阵乘法对应了一个变换,是把任意一个向量变成另一个方向或长度都大多不同的新向量。在这个变换的过程中,原向量主要发生旋转、伸缩的变化。如果矩阵对某一个向量或某些向量只发生伸缩变换,不对这些向量产生旋转的效果,那么这些向量就称为这个矩阵的特征向量,伸缩的比例就是特征值。
Ax=λx
,矩阵*
向量=特征值*
向量
一个向量左乘一个矩阵,相当于将该向量伸长或缩短多少。
Rn=1·n
,旋转矩阵*
旋转向量=1*
该旋转向量。
利用旋转矩阵求其对应旋转向量时则是用的该意义。旋转向量(转轴)因为与R表示的转动一致,因此转动后还是其本身(1*
n)
矩阵形式 | 几何意义 |
---|---|
普通矩阵 | 线性变换 |
正交矩阵 | 坐标的旋转 |
对角矩阵 | 缩放 |
三角矩阵 | 切边 |
一个普通矩阵的几何意义是对坐标进行某种线性变换,而正交矩阵的几何意义是坐标的旋转,对角矩阵的几何意义是坐标的缩放,三角矩阵的几何意义是对坐标的切边。因此对矩阵分解的几何意义就是将这种变换分解成缩放、切边和旋转的过程。
QR分解:
是一项广泛用于稳定求解病态最小二乘问题的方法,可采用QR分解的前提是A列满秩,此时采用施密特正交化方法使A=QR
,Q
为酉矩阵(正交矩阵),R
为上三角阵,在视觉SLAM中相当于将A分解成一个旋转矩阵和一个上三角的标定阵。
优点是求解速度快,算法稳定,常用于求解线性方程组的解。缺点是只能求解满秩矩阵
SVD分解:
即奇异值分解,A=UDV'
,其中U、V
均为酉矩阵,D
为对角阵,对角线元素为各奇异值,且衰减速度通常较快,SVD几何意义可以写成AV=UD,即当矩阵A作用于任何基向量v时,会把v变换到u的方向,同时将u的长度变成对应奇异值σ。也可以看成先旋转然后缩放再旋转的过程。
SVD分解可靠性最好,能求解非满秩矩阵,但计算时间复杂度比QR要高
LU 分解:
法是将矩阵 A 分解成两个矩阵 L 和 U,其中 L 是一个下三角矩阵,U 是一个上三角矩阵。然后通过消元的方法来求解线性方程组。
优点是求解速度快,算法稳定,常用于求解方程组的解。缺点是只能求解正定矩阵