Bootstrap

【LeGO-LOAM论文阅读(二)--特征提取(三)】

本文主要讲的是特征提取中线面特征匹配以及优化的代码理解,因为上次代码看麻了·,这次静下心来又重新看了一遍。
不想听我废话的,直接去最后参考链接部分自己去理解。

面特征匹配(findCorrespondingSurfFeatures)

找三个距离i点最近的三个点就不讲了,说明白了就是算一下距离,其中closestPointInd是通过kd树找的,另外两个都是根据到closestPointInd的最小距离找到的。

                pointSearchSurfInd1[i] = closestPointInd;
                pointSearchSurfInd2[i] = minPointInd2;
                pointSearchSurfInd3[i] = minPointInd3;

算法先把三个点复制一下:

            if (pointSearchSurfInd2[i] >= 0 && pointSearchSurfInd3[i] >= 0) {

                tripod1 = laserCloudSurfLast->points[pointSearchSurfInd1[i]];
                tripod2 = laserCloudSurfLast->points[pointSearchSurfInd2[i]];
                tripod3 = laserCloudSurfLast->points[pointSearchSurfInd3[i]];

为了后文便于讲解,我将tripod1,tripod2,tripod3分别记为j,l,m三个点。
首先我们要计算的是点i到j,i,m三点构成的平面的距离。下面要计算的就是这个。
首先复习下数学知识:
向量的点乘与叉乘,向量的叉乘的模长表示的是以两个向量为边长的平行四边形的面积,
a×b=|a||b|sin<a,b>
a·b=|a||b|cos<a,b>
如下图:
向量ji和向量jm的的叉乘表示的是地面四边形的面积,而叉乘出来的向量是与底面垂直的(符合右手准则)。叉乘向量与向量ji的点乘的数值就是立方体的体积,拿得到的体积除以底面面积就可以轻松得到点i到平面的距离。

在这里插入图片描述
而三维向量叉乘与点乘的公式如下:
(a1,a2,a3)×(b1,b2,b3)=(a2b3-a3b2 , a3b1-a1b3 , a1b2-a2b1)
(a1,a2,a3)·(b1,b2,b3)=(a1b1 , a2b2 , a3b3)

回到论文
点到面的距离公式:
在这里插入图片描述
i,j,l,m坐标分别记为(x0,y0,z0)(x1,y1,z1)(x2,y2,z2)(x3,y3,z3)
在面匹配中主要关注的会是 d对 rx,rz,ty 的偏导(因为是2步lm求解参数),以 rx 为例:
在这里插入图片描述

在面匹配过程中计算出三项中的第一项偏导,
坐标带入可得:
在这里插入图片描述
在这里插入图片描述
所以有:
在这里插入图片描述
有了表达式就很容易计算三个偏导:
在这里插入图片描述

了解了数学原理来解释下代码

                float pa = (tripod2.y - tripod1.y) * (tripod3.z - tripod1.z) 
                         - (tripod3.y - tripod1.y) * (tripod2.z - tripod1.z);
                float pb = (tripod2.z - tripod1.z) * (tripod3.x - tripod1.x) 
                         - (tripod3.z - tripod1.z) * (tripod2.x - tripod1.x);
                float pc = (tripod2.x - tripod1.x) * (tripod3.y - tripod1.y) 
                         - (tripod3.x - tripod1.x) * (tripod2.y - tripod1.y);

tripod1,tripod2,tripod3分别记为j,l,m三个点
所以tripod2-tripod1表示jl向量,tripod3-tripod1表示jm向量。
这时候就会发现pa,pb,pc就是三个偏导
接着看后面的代码:

                float pd = -(pa * tripod1.x + pb * tripod1.y + pc * tripod1.z);
                float ps = sqrt(pa * pa + pb * pb + pc * pc);

                pa /= ps;
                pb /= ps;
                pc /= ps;
                pd /= ps;

                // 距离没有取绝对值
                // 两个向量的点乘,分母除以ps中已经除掉了,
                // 加pd原因:intSelpo与tripod1构成的线段需要相减
                float pd2 = pa * pointSel.x + pb * pointSel.y + pc * pointSel.z + pd;

可以发现ps表示的是面积,那么pd是什么呢,别着急往下接着看,把pd2展开看看:
pd2
= pa * pointSel.x + pb * pointSel.y + pc * pointSel.z - pd1/ps
= (pa1 * pointSel.x + pb1 * pointSel.y + pc1 * pointSel.z)/ps - pd1/ps
= (pa1 * pointSel.x + pb1 * pointSel.y + pc1 * pointSel.z)/ps -(pa1 * tripod1.x + pb1 * tripod1.y + pc1 * tripod1.z)/ps
= (pa1*(pointSel.x - tripod1.x) + pb1*(pointSel.y - tripod1.y) + pc1*(pointSel.z - tripod1.z))/ps

公式中pa1,pb1,pc1表示最开始没单位化的pa,pb,pc。
pointSel - tripod1表示的是什么,不就是ji向量吗,所以这不就是叉乘向量与ji向量点乘得到体积,然后除以面积ps得到的点到面的距离。
所以pd2表示的就是点到面的距离。
在后面讲就是一些加权值影响,coeffSel保存这些平面特征,laserCloudOri保存点i的原始点云数据:

                float s = 1;
                if (iterCount >= 5) {
                // /加上影响因子
                    s = 1 - 1.8 * fabs(pd2) / sqrt(sqrt(pointSel.x * pointSel.x
                            + pointSel.y * pointSel.y + pointSel.z * pointSel.z));
                }

                if (s > 0.1 && pd2 != 0) {
                    // [x,y,z]是整个平面的单位法量
                    // intensity是平面外一点到该平面的距离
                    coeff.x = s * pa;
                    coeff.y = s * pb;
                    coeff.z = s * pc;
                    coeff.intensity = s * pd2;

                    // 未经变换的点放入laserCloudOri队列,距离,法向量值放入coeffSel
                    laserCloudOri->push_back(surfPointsFlat->points[i]);
                    coeffSel->push_back(coeff);

然后进行面特征优化。

面特征优化(calculateTransformationSurf)

优化部分主要求出剩下的三个偏导,然后计算得到的 rx,rz,ty位姿增量。
帧间的点云变换矩阵在本博客线特征优化部分讲了,可以跳过去看一下。
得到k+1时刻变换到k时刻 i 点的坐标:
在这里插入图片描述

然后就可以计算d对 rx,rz,ty的偏导
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接着来看看代码,代码中b6应该写错了:

    bool calculateTransformationSurf(int iterCount){

        int pointSelNum = laserCloudOri->points.size();

        cv::Mat matA(pointSelNum, 3, CV_32F, cv::Scalar::all(0));
        cv::Mat matAt(3, pointSelNum, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtA(3, 3, CV_32F, cv::Scalar::all(0));
        cv::Mat matB(pointSelNum, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtB(3, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matX(3, 1, CV_32F, cv::Scalar::all(0));

        float srx = sin(transformCur[0]);
        float crx = cos(transformCur[0]);
        float sry = sin(transformCur[1]);
        float cry = cos(transformCur[1]);
        float srz = sin(transformCur[2]);
        float crz = cos(transformCur[2]);
        float tx = transformCur[3];
        float ty = transformCur[4];
        float tz = transformCur[5];

        float a1 = crx*sry*srz; float a2 = crx*crz*sry; float a3 = srx*sry; float a4 = tx*a1 - ty*a2 - tz*a3;
        float a5 = srx*srz; float a6 = crz*srx; float a7 = ty*a6 - tz*crx - tx*a5;
        float a8 = crx*cry*srz; float a9 = crx*cry*crz; float a10 = cry*srx; float a11 = tz*a10 + ty*a9 - tx*a8;

        float b1 = -crz*sry - cry*srx*srz; float b2 = cry*crz*srx - sry*srz;
        float b5 = cry*crz - srx*sry*srz; float b6 = cry*srz + crz*srx*sry;

        float c1 = -b6; float c2 = b5; float c3 = tx*b6 - ty*b5; float c4 = -crx*crz; float c5 = crx*srz; float c6 = ty*c5 + tx*-c4;
        float c7 = b2; float c8 = -b1; float c9 = tx*-b2 - ty*-b1;

        // 构建雅可比矩阵,求解
        for (int i = 0; i < pointSelNum; i++) {

            pointOri = laserCloudOri->points[i];
            coeff = coeffSel->points[i];

            float arx = (-a1*pointOri.x + a2*pointOri.y + a3*pointOri.z + a4) * coeff.x
                      + (a5*pointOri.x - a6*pointOri.y + crx*pointOri.z + a7) * coeff.y
                      + (a8*pointOri.x - a9*pointOri.y - a10*pointOri.z + a11) * coeff.z;

            float arz = (c1*pointOri.x + c2*pointOri.y + c3) * coeff.x
                      + (c4*pointOri.x - c5*pointOri.y + c6) * coeff.y
                      + (c7*pointOri.x + c8*pointOri.y + c9) * coeff.z;

            float aty = -b6 * coeff.x + c4 * coeff.y + b2 * coeff.z;

            float d2 = coeff.intensity;

            matA.at<float>(i, 0) = arx;
            matA.at<float>(i, 1) = arz;
            matA.at<float>(i, 2) = aty;
            matB.at<float>(i, 0) = -0.05 * d2;
        }

利用opencv函数计算x:

        cv::transpose(matA, matAt);
        matAtA = matAt * matA;
        matAtB = matAt * matB;
        cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);

然后退化问题,接着更新位姿增量:

        transformCur[0] += matX.at<float>(0, 0);
        transformCur[2] += matX.at<float>(1, 0);
        transformCur[4] += matX.at<float>(2, 0);

判断位姿是否合法以及迭代是否满足条件:

        for(int i=0; i<6; i++){
            if(isnan(transformCur[i]))
                transformCur[i]=0;
        }

        float deltaR = sqrt(
                            pow(rad2deg(matX.at<float>(0, 0)), 2) +
                            pow(rad2deg(matX.at<float>(1, 0)), 2));
        float deltaT = sqrt(
                            pow(matX.at<float>(2, 0) * 100, 2));

        if (deltaR < 0.1 && deltaT < 0.1) {
            return false;
        }
        return true;
    }

线特征匹配

线特征匹配的原理和面特征差不多、
如下图:
在这里插入图片描述
找到点i的最近邻两个点i,j。同样向量ji和向量li的叉乘是四边形的面积,面积除以向量lj的模长就是点i到线lj的距离。下面来看看算法代码:
找距离i最近的两个点的方法就不再最熟,将两个最近点复制一下:

                pointSearchCornerInd1[i] = closestPointInd;
                pointSearchCornerInd2[i] = minPointInd2;

我们知道线匹配的距离公式如下,后面我们要优化变换矩阵(帧间矩阵)使d最小。
在这里插入图片描述
这里因为线匹配优化的是x,z的位移以及y轴的旋转角度,所以有:
在这里插入图片描述
这样一来问题就变成的找到合适的tx,ty,ry是的f与d的差值最小:
在这里插入图片描述
接着就是对tx,ty,ry分别偏导,然后找到最优的tx,ty,ry。
上式的三个一阶偏导数,以tx为例,f对tx的一阶偏导为:
在这里插入图片描述
而线特征匹配这一步计算了在这里插入图片描述

来看看如何计算的

假设i,j,l的坐标分别为
在这里插入图片描述

那么将坐标带入d的距离公式中(p1,p2表示j,l)
可得:
在这里插入图片描述
设I12表示|p1-p2|
则有:
在这里插入图片描述
为方便书写,我们做以下设定:
在这里插入图片描述
则d的表达式为:
在这里插入图片描述
那么函数对变量的偏导变为:
在这里插入图片描述
而:
在这里插入图片描述
上式化简后为:
在这里插入图片描述
接着再来看代码,这个时候就容易理解了:

                tripod1 = laserCloudCornerLast->points[pointSearchCornerInd1[i]];
                tripod2 = laserCloudCornerLast->points[pointSearchCornerInd2[i]];

                float x0 = pointSel.x;
                float y0 = pointSel.y;
                float z0 = pointSel.z;
                float x1 = tripod1.x;
                float y1 = tripod1.y;
                float z1 = tripod1.z;
                float x2 = tripod2.x;
                float y2 = tripod2.y;
                float z2 = tripod2.z;

                float m11 = ((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1));
                float m22 = ((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1));
                float m33 = ((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1));

                float a012 = sqrt(m11 * m11  + m22 * m22 + m33 * m33);

                float l12 = sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2) + (z1 - z2)*(z1 - z2));

                float la =  ((y1 - y2)*m11 + (z1 - z2)*m22) / a012 / l12;

                float lb = -((x1 - x2)*m11 - (z1 - z2)*m33) / a012 / l12;

                float lc = -((x1 - x2)*m22 + (y1 - y2)*m33) / a012 / l12;

                float ld2 = a012 / l12;

                float s = 1;
                if (iterCount >= 5) {
                    s = 1 - 1.8 * fabs(ld2);
                }

                if (s > 0.1 && ld2 != 0) {
                    coeff.x = s * la; 
                    coeff.y = s * lb;
                    coeff.z = s * lc;
                    coeff.intensity = s * ld2;
                  
                    laserCloudOri->push_back(cornerPointsSharp->points[i]);
                    coeffSel->push_back(coeff);

la,lb,lc表示的就是三个偏导,而ld2表示的是点到线的距离

线特征优化

优化方程如下:
在这里插入图片描述
其中三项的中首个偏导在线条特征匹配中已经计算好了,接下来需要计算剩下的偏导。
首先要知道,匹配优化过程是帧间匹配的,之前transformCur保存的是相邻两帧之间的旋转角度以及位移量。现在要将当前点云结束时刻(k+1)的点云投影到当前点云开始时刻(k)
即:
在这里插入图片描述
旋转矩阵----绕z轴转roll (rz) —绕x轴转pitch(rx) — 绕 y轴转heading (ry):
在这里插入图片描述
你可能会发现角度符号有点不对,这是因为我们要从k+1变到k,所以角度应该是相反数,然后根据sin,cos函数性质在进行化简得来的。
需要注意的是代码里的坐标转化乱七八糟的,要分清楚rx,ry,rz分别是什么角度。
将上式展开:
在这里插入图片描述
带入公式得:
在这里插入图片描述
得到x,y,z的解析式后,可以分别求取他们相对于tx,ty,ry的偏导数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有了各项之后就可以计算出F相对未知参数 tx,tz,ry的求导:
在这里插入图片描述
再来看代码就容易理解了:

    bool calculateTransformationCorner(int iterCount){

        int pointSelNum = laserCloudOri->points.size();

        cv::Mat matA(pointSelNum, 3, CV_32F, cv::Scalar::all(0));
        cv::Mat matAt(3, pointSelNum, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtA(3, 3, CV_32F, cv::Scalar::all(0));
        cv::Mat matB(pointSelNum, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matAtB(3, 1, CV_32F, cv::Scalar::all(0));
        cv::Mat matX(3, 1, CV_32F, cv::Scalar::all(0));

        // 以下为开始计算A,A=[J的偏导],J的偏导的计算公式是什么?
        float srx = sin(transformCur[0]);
        float crx = cos(transformCur[0]);
        float sry = sin(transformCur[1]);
        float cry = cos(transformCur[1]);
        float srz = sin(transformCur[2]);
        float crz = cos(transformCur[2]);
        float tx = transformCur[3];
        float ty = transformCur[4];
        float tz = transformCur[5];

        float b1 = -crz*sry - cry*srx*srz; float b2 = cry*crz*srx - sry*srz; float b3 = crx*cry; float b4 = tx*-b1 + ty*-b2 + tz*b3;
        float b5 = cry*crz - srx*sry*srz; float b6 = cry*srz + crz*srx*sry; float b7 = crx*sry; float b8 = tz*b7 - ty*b6 - tx*b5;

        float c5 = crx*srz;

        for (int i = 0; i < pointSelNum; i++) {

            pointOri = laserCloudOri->points[i];
            coeff = coeffSel->points[i];

            float ary = (b1*pointOri.x + b2*pointOri.y - b3*pointOri.z + b4) * coeff.x
                      + (b5*pointOri.x + b6*pointOri.y - b7*pointOri.z + b8) * coeff.z;

            float atx = -b5 * coeff.x + c5 * coeff.y + b1 * coeff.z;

            float atz = b7 * coeff.x - srx * coeff.y - b3 * coeff.z;

            float d2 = coeff.intensity;


            // A=[J的偏导]; B=[权重系数*(点到直线的距离)] 求解公式: AX=B
            // 为了让左边满秩,同乘At-> At*A*X = At*B
            matA.at<float>(i, 0) = ary;
            matA.at<float>(i, 1) = atx;
            matA.at<float>(i, 2) = atz;
            matB.at<float>(i, 0) = -0.05 * d2;
        }

后面就是通过opencv自带的函数计算出x:

        // transpose函数求得matA的转置matAt
        cv::transpose(matA, matAt);
        matAtA = matAt * matA;
        matAtB = matAt * matB;
        // 通过QR分解的方法,求解方程AtA*X=AtB,得到X
        cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);

紧接着是退化问题,然后更新姿态:

        transformCur[1] += matX.at<float>(0, 0);
        transformCur[3] += matX.at<float>(1, 0);
        transformCur[5] += matX.at<float>(2, 0);

最后检查姿态是否合法以及迭代条件是否满足等等:

        for(int i=0; i<6; i++){
            if(isnan(transformCur[i]))
                transformCur[i]=0;
        }

        float deltaR = sqrt(
                            pow(rad2deg(matX.at<float>(0, 0)), 2));
        float deltaT = sqrt(
                            pow(matX.at<float>(1, 0) * 100, 2) +
                            pow(matX.at<float>(2, 0) * 100, 2));

        if (deltaR < 0.1 && deltaT < 0.1) {
            return false;
        }
        return true;
    }

参考

loam讲解
FeatureAssociation节点(3)
FeatureAssociation节点(4)

;