lio-sam框架:点云匹配之手写高斯牛顿优化求状态量更新
前言
LIO-SAM的全称是:Tightly-coupled Lidar Inertial Odometry via Smoothing and Mapping
从全称上可以看出,该算法是一个紧耦合的雷达惯导里程计(Tightly-coupled Lidar Inertial Odometry),借助的手段就是利用GT-SAM库中的方法。
LIO-SAM 提出了一个利用GT-SAM的紧耦合激光雷达惯导里程计的框架。
实现了高精度、实时的移动机器人的轨迹估计和建图。
其中建图优化节点整体如下图
在这篇博客中 https://www.guyuehome.com/39723 分析了 点云匹配前戏之初值计算及局部地图构建
在这篇博客中 https://www.guyuehome.com/39767 分析了 点云配准之角点面点的残差及梯度构建
经过之前两片博客,
- 激光雷达当前帧有了初值的估计,那么则可以利用初值投到地图坐标系下
- 构建了局部地图
- 构建了角点和面点的残差及梯度方向
下面可以通过高斯牛顿下降算法来求解状态更新量了
本篇主要分析:如何通过高斯牛顿下降算法来求解状态更新量
代码分
在上一篇分析了 角点和面点的残差及梯度方向,就是
cornerOptimization();
surfOptimization();
这两个函数
下面是通过这个函数将角点和面点的残差及梯度统一到一起
combineOptimizationCoeffs();
来看具体内容:
void combineOptimizationCoeffs()
{
for (int i = 0; i < laserCloudCornerLastDSNum; ++i){
//
if (laserCloudOriCornerFlag[i] == true){
laserCloudOri->push_back(laserCloudOriCornerVec[i]);//点
coeffSel->push_back(coeffSelCornerVec[i]);//残差及梯度
}
}
只有标记为true的时候才是有效约束
点和残差及梯度加入到laserCloudOri
和coeffSel
中
for (int i = 0; i < laserCloudSurfLastDSNum; ++i){
if (laserCloudOriSurfFlag[i] == true){
laserCloudOri->push_back(laserCloudOriSurfVec[i]);
coeffSel->push_back(coeffSelSurfVec[i]);
}
}
同理处理面点
std::fill(laserCloudOriCornerFlag.begin(), laserCloudOriCornerFlag.end(), false);
std::fill(laserCloudOriSurfFlag.begin(), laserCloudOriSurfFlag.end(), false);
标志位清零
当数据准备好之后,即可开始下面的优化
if (LMOptimization(iterCount) == true)
break;
执行优化求解
虽然函数名写的是LM优化算法,但是函数内部是高斯牛顿下降优化算法
来看具体内容:
bool LMOptimization(int iterCount)
{
float srx = sin(transformTobeMapped[1]);
float crx = cos(transformTobeMapped[1]);
float sry = sin(transformTobeMapped[2]);
float cry = cos(transformTobeMapped[2]);
float srz = sin(transformTobeMapped[0]);
float crz = cos(transformTobeMapped[0]);
首先求一些 x y z 的三角函数
注意这里有个坐标变换
camera <- lidar 就是雷达到相机坐标系的变换是这样的:
x = y
y = z
z = x
roll = pitch
pitch = yaw
yaw = roll
lidar <- camera 相机到雷达坐标系的变换是这样的:
x = z
y = x
z = y
roll = yaw
pitch = roll
yaw = pitch
transformTobeMapped
是一个6维的数组 分别存放:roll 、pitch、yaw、x、y、z
代码在这里已经做了 雷达到相机的坐标转化
int laserCloudSelNum = laserCloudOri->size();
if (laserCloudSelNum < 50) {
return false;
}
检测有多少个约束,如果约束小于50,则直接return false
cv::Mat matA(laserCloudSelNum, 6, CV_32F, cv::Scalar::all(0));
cv::Mat matAt(6, laserCloudSelNum, CV_32F, cv::Scalar::all(0));
cv::Mat matAtA(6, 6, CV_32F, cv::Scalar::all(0));
cv::Mat matB(laserCloudSelNum, 1, CV_32F, cv::Scalar::all(0));
cv::Mat matAtB(6, 1, CV_32F, cv::Scalar::all(0));
cv::Mat matX(6, 1, CV_32F, cv::Scalar::all(0));
用cv Mat做了些变量定义
for (int i = 0; i < laserCloudSelNum; i++) {
迭代每一个约束点进行优化求解
pointOri.x = laserCloudOri->points[i].y;
pointOri.y = laserCloudOri->points[i].z;
pointOri.z = laserCloudOri->points[i].x;
首先将当前点转到相机系 lidar -> camera
coeff.x = coeffSel->points[i].y;
coeff.y = coeffSel->points[i].z;
coeff.z = coeffSel->points[i].x;
coeff.intensity = coeffSel->points[i].intensity;
将当前点到线(面)的单位向量转到相机系
float arx = (crx*sry*srz*pointOri.x + crx*crz*sry*pointOri.y - srx*sry*pointOri.z) * coeff.x
+ (-srx*srz*pointOri.x - crz*srx*pointOri.y - crx*pointOri.z) * coeff.y
+ (crx*cry*srz*pointOri.x + crx*cry*crz*pointOri.y - cry*srx*pointOri.z) * coeff.z;
float ary = ((cry*srx*srz - crz*sry)*pointOri.x
+ (sry*srz + cry*crz*srx)*pointOri.y + crx*cry*pointOri.z) * coeff.x
+ ((-cry*crz - srx*sry*srz)*pointOri.x
+ (cry*srz - crz*srx*sry)*pointOri.y - crx*sry*pointOri.z) * coeff.z;
float arz = ((crz*srx*sry - cry*srz)*pointOri.x + (-cry*crz-srx*sry*srz)*pointOri.y)*coeff.x
+ (crx*crz*pointOri.x - crx*srz*pointOri.y) * coeff.y
+ ((sry*srz + cry*crz*srx)*pointOri.x + (crz*sry-cry*srx*srz)*pointOri.y)*coeff.z;
求 雅克比矩阵 旋转部分
相机系下的旋转顺序是 Y - X - Z 对应 lidar 系下的 Z-Y-X
matA.at<float>(i, 0) = arz;
matA.at<float>(i, 1) = arx;
matA.at<float>(i, 2) = ary;
matA.at<float>(i, 3) = coeff.z;
matA.at<float>(i, 4) = coeff.x;
matA.at<float>(i, 5) = coeff.y;
matB.at<float>(i, 0) = -coeff.intensity;
这里就是把camera转到lidar了
赋值 雅克比矩阵
下面 构造 JTJ 以及 -JTe 矩阵
cv::transpose(matA, matAt);
matA 是 雅克比矩阵 J
matAt 是 JT
matAtA = matAt * matA;
JTJ
matAtB = matAt * matB;
-JTe
cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);
利用CV的方法求解增量
JTJX=-JTe
if (iterCount == 0) {
cv::Mat matE(1, 6, CV_32F, cv::Scalar::all(0));
cv::Mat matV(6, 6, CV_32F, cv::Scalar::all(0));
cv::Mat matV2(6, 6, CV_32F, cv::Scalar::all(0));
cv::eigen(matAtA, matE, matV);
matV.copyTo(matV2);
检测一下 是否有退化得情况
对JTJ 进行特征值分解
for (int i = 5; i >= 0; i--) {
if (matE.at<float>(0, i) < eignThre[i]) {
for (int j = 0; j < 6; j++) {
matV2.at<float>(i, j) = 0;
}
isDegenerate = true;
} else {
break;
}
}
特征值从小到大遍历,如果小于阈值就认为退化
对应的特征向量全部置0
退化标志位 为 true
if (isDegenerate)
{
cv::Mat matX2(6, 1, CV_32F, cv::Scalar::all(0));
matX.copyTo(matX2);
matX = matP * matX2;
}
如果发生退化,就对增量进行修改,退化方向不更新
transformTobeMapped[0] += matX.at<float>(0, 0);
transformTobeMapped[1] += matX.at<float>(1, 0);
transformTobeMapped[2] += matX.at<float>(2, 0);
transformTobeMapped[3] += matX.at<float>(3, 0);
transformTobeMapped[4] += matX.at<float>(4, 0);
transformTobeMapped[5] += matX.at<float>(5, 0);
增量更新
float deltaR = sqrt(
pow(pcl::rad2deg(matX.at<float>(0, 0)), 2) +
pow(pcl::rad2deg(matX.at<float>(1, 0)), 2) +
pow(pcl::rad2deg(matX.at<float>(2, 0)), 2));
float deltaT = sqrt(
pow(matX.at<float>(3, 0) * 100, 2) +
pow(matX.at<float>(4, 0) * 100, 2) +
pow(matX.at<float>(5, 0) * 100, 2));
计算更新的旋转和平移大小
if (deltaR < 0.05 && deltaT < 0.05) {
return true;
}
如果更新的旋转和平移过小,怎么认为达到最优解
return false;
}
否则继续优化
求解出来优化结果后 把结果和imu进行一次加权融合
transformUpdate();
if (cloudInfo.imuAvailable == true)
{
判断 可以获取九轴imu的世界坐标系下的姿态
if (std::abs(cloudInfo.imuPitchInit) < 1.4)
{
角度没有很大
transformQuaternion.setRPY(transformTobeMapped[0], 0, 0);
imuQuaternion.setRPY(cloudInfo.imuRollInit, 0, 0);
tf::Matrix3x3(transformQuaternion.slerp(imuQuaternion, imuWeight)).getRPY(rollMid, pitchMid, yawMid);
transformTobeMapped[0] = rollMid;
lidar 匹配获得的roll角转成四元数
imu 获得的roll角转成四元数
使用四元的球面插值
插值的结果作为roll的最终结果
transformQuaternion.setRPY(0, transformTobeMapped[1], 0);
imuQuaternion.setRPY(0, cloudInfo.imuPitchInit, 0);
tf::Matrix3x3(transformQuaternion.slerp(imuQuaternion, imuWeight)).getRPY(rollMid, pitchMid, yawMid);
transformTobeMapped[1] = pitchMid;
同理pitch 角度
transformTobeMapped[0] = constraintTransformation(transformTobeMapped[0], rotation_tollerance);
transformTobeMapped[1] = constraintTransformation(transformTobeMapped[1], rotation_tollerance);
transformTobeMapped[5] = constraintTransformation(transformTobeMapped[5], z_tollerance);
对roll pitch 和z进行一些约束,主要针对室内2d场景下,已知2d先验可以加上这些约束
incrementalOdometryAffineBack = trans2Affine3f(transformTobeMapped);
最终的结果也可以转成eigen的结构
- 点赞
- 收藏
- 关注作者
评论(0)