前言

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的时候才是有效约束
点和残差及梯度加入到laserCloudOricoeffSel

        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的结构