SLAM源码分析(八)

2021SC@SDUSC

Initializer.cc,ComputeH21函数,用DLT方法求解单应矩阵H。

vP1:参考帧中归一化后的特征点;vP2:当前帧中归一化后的特征点;

基本原理:

|x'|         | h1 h2 h3 ||x|

|y'| = a   | h4 h5 h6 ||y|  简写: x' = a H x, a为一个尺度因子

1 |          | h7 h8 h9 ||1|

使用DLT(direct linear tranform)求解该模型

 x' = a H x

---> (x') 叉乘 (H x)  = 0  (因为方向相同) (取前两行就可以推导出下面的了)

---> Ah = 0

A = | 0  0  0 -x -y -1 xy' yy' y'|  h = | h1 h2 h3 h4 h5 h6 h7 h8 h9 |

      |-x -y -1  0  0  0 xx' yx' x'|

通过SVD求解Ah = 0,A^T*A最小特征值对应的特征向量即为解。

cv::Mat Initializer::ComputeH21(
    const vector &vP1, //归一化后的点, in reference frame
    const vector &vP2) //归一化后的点, in current frame
{
    cv::Mat A(2*N,				//行,注意每一个点的数据对应两行
			  9,				//列
			  CV_32F);      	//float数据类型

	// 构造矩阵A,将每个特征点添加到矩阵A中的元素
    for(int i=0; i(2*i,0) = 0.0;
        A.at(2*i,1) = 0.0;
        A.at(2*i,2) = 0.0;
        A.at(2*i,3) = -u1;
        A.at(2*i,4) = -v1;
        A.at(2*i,5) = -1;
        A.at(2*i,6) = v2*u1;
        A.at(2*i,7) = v2*v1;
        A.at(2*i,8) = v2;

		//生成这个点的第二行
        A.at(2*i+1,0) = u1;
        A.at(2*i+1,1) = v1;
        A.at(2*i+1,2) = 1;
        A.at(2*i+1,3) = 0.0;
        A.at(2*i+1,4) = 0.0;
        A.at(2*i+1,5) = 0.0;
        A.at(2*i+1,6) = -u2*u1;
        A.at(2*i+1,7) = -u2*v1;
        A.at(2*i+1,8) = -u2;

    }

    // 定义输出变量,u是左边的正交矩阵U, w为奇异矩阵,vt中的t表示是右正交矩阵V的转置
    cv::Mat u,w,vt;

	//使用opencv提供的进行奇异值分解的函数
    cv::SVDecomp(A,							//输入,待进行奇异值分解的矩阵
				 w,							//输出,奇异值矩阵
				 u,							//输出,矩阵U
				 vt,						//输出,矩阵V^T
				 cv::SVD::MODIFY_A | 		//输入,MODIFY_A是指允许计算函数可以修改待分解的矩阵,官方文档上说这样可以加快计算速度、节省内存
				     cv::SVD::FULL_UV);		//FULL_UV=把U和VT补充成单位正交方阵

	// 返回最小奇异值所对应的右奇异向量
    return vt.row(8).reshape(0, 			//转换后的通道数,这里设置为0表示是与前面相同
							 3); 			//转换后的行数,对应V的最后一列
}

ComputeF21函数,通过八点法,根据特征点匹配求fundamental matrix;因为F矩阵有秩为2的约束,所以需要两次SVD分解。

x'Fx = 0 整理可得:Af = 0

A = | x'x x'y x' y'x y'y y' x y 1 |, f = | f1 f2 f3 f4 f5 f6 f7 f8 f9 |

通过SVD求解Af = 0,A'A最小特征值对应的特征向量即为解

cv::Mat Initializer::ComputeF21(
    const vector &vP1, //归一化后的点, in reference frame
    const vector &vP2) //归一化后的点, in current frame
{
	//获取参与计算的特征点对数
    const int N = vP1.size();

	//初始化A矩阵
    cv::Mat A(N,9,CV_32F); // N*9维

    // 构造矩阵A,将每个特征点添加到矩阵A中的元素
    for(int i=0; i(i,0) = u2*u1;
        A.at(i,1) = u2*v1;
        A.at(i,2) = u2;
        A.at(i,3) = v2*u1;
        A.at(i,4) = v2*v1;
        A.at(i,5) = v2;
        A.at(i,6) = u1;
        A.at(i,7) = v1;
        A.at(i,8) = 1;
    }

    //存储奇异值分解结果的变量
    cv::Mat u,w,vt;

    // 定义输出变量,u是左边的正交矩阵U, w为奇异矩阵,vt中的t表示是右正交矩阵V的转置
    cv::SVDecomp(A,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
	// 转换成基础矩阵的形式
    cv::Mat Fpre = vt.row(8).reshape(0, 3); // v的最后一列

    //基础矩阵的秩为2,而我们不敢保证计算得到的这个结果的秩为2,所以需要通过第二次奇异值分解,来强制使其秩为2
    // 对初步得来的基础矩阵进行第2次奇异值分解
    cv::SVDecomp(Fpre,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);

	// 秩2约束,强制将第3个奇异值设置为0
    w.at(2)=0; 
    
    // 重新组合好满足秩约束的基础矩阵,作为最终计算结果返回 
    return  u*cv::Mat::diag(w)*vt;
}

CheckHomography函数,对给定的homography matrix打分。

在已值n维观测数据误差服从N(0,sigma)的高斯分布时

其误差加权最小二乘结果为  sum_error = SUM(e(i)^T * Q^(-1) * e(i))

其中:e(i) = [e_x,e_y,...]^T, Q维观测数据协方差矩阵,即sigma * sigma组成的协方差矩阵

误差加权最小二次结果越小,说明观测数据精度越高

那么,score = SUM((th - e(i)^T * Q^(-1) * e(i)))的分数就越高

算法目标: 检查单应变换矩阵

检查方式:通过H矩阵,进行参考帧和当前帧之间的双向投影,并计算起加权最小二乘投影误差

算法流程

input: 单应性矩阵 H21, H12, 匹配点集 mvKeys1

do:

      for p1(i), p2(i) in mvKeys:

         error_i1 = ||p2(i) - H21 * p1(i)||2

         error_i2 = ||p1(i) - H12 * p2(i)||2     

         w1 = 1 / sigma / sigma

         w2 = 1 / sigma / sigma

        if error1 < th

           score +=   th - error_i1 * w1

       if error2 < th

            score +=   th - error_i2 * w2

       if error_1i > th or error_2i > th

            p1(i), p2(i) are inner points

            vbMatchesInliers(i) = true

        else

             p1(i), p2(i) are outliers

            vbMatchesInliers(i) = false

         end

  end

  output: score, inliers

float Initializer::CheckHomography(
    const cv::Mat &H21,                 //从参考帧到当前帧的单应矩阵
    const cv::Mat &H12,                 //从当前帧到参考帧的单应矩阵
    vector &vbMatchesInliers,     //匹配好的特征点对的Inliers标记
    float sigma)                        //估计误差
{   
	// 特点匹配个数
    const int N = mvMatches12.size();

    const float h11 = H21.at(0,0);
    const float h12 = H21.at(0,1);
    const float h13 = H21.at(0,2);
    const float h21 = H21.at(1,0);
    const float h22 = H21.at(1,1);
    const float h23 = H21.at(1,2);
    const float h31 = H21.at(2,0);
    const float h32 = H21.at(2,1);
    const float h33 = H21.at(2,2);

	// 获取从当前帧到参考帧的单应矩阵的各个元素
    const float h11inv = H12.at(0,0);
    const float h12inv = H12.at(0,1);
    const float h13inv = H12.at(0,2);
    const float h21inv = H12.at(1,0);
    const float h22inv = H12.at(1,1);
    const float h23inv = H12.at(1,2);
    const float h31inv = H12.at(2,0);
    const float h32inv = H12.at(2,1);
    const float h33inv = H12.at(2,2);

	// 给特征点对的Inliers标记预分配空间
    vbMatchesInliers.resize(N);

	// 初始化score值
    float score = 0;

    // 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)
	// 自由度为2的卡方分布,显著性水平为0.05,对应的临界阈值
    const float th = 5.991;

    //信息矩阵,方差平方的倒数
    const float invSigmaSquare = 1.0/(sigma*sigma);

    // Step 2 通过H矩阵,进行参考帧和当前帧之间的双向投影,并计算起加权最小二乘投影误差
    // H21 表示从img1 到 img2的变换矩阵
    // H12 表示从img2 到 img1的变换矩阵 
    for(int i=0; ith)
            bIn = false;
        else
            // 误差越大,得分越低
            score += th - chiSquare1;

        // 计算从img1 到 img2 的投影变换误差
        // x1in2 = H21*x1
        // 将图像2中的特征点单应到图像1中
        // |u2|   |h11 h12 h13||u1|   |u1in2|
        // |v2| = |h21 h22 h23||v1| = |v1in2| * w1in2inv
        // |1 |   |h31 h32 h33||1 |   |  1  |
		// 计算投影归一化坐标
        const float w1in2inv = 1.0/(h31*u1+h32*v1+h33);
        const float u1in2 = (h11*u1+h12*v1+h13)*w1in2inv;
        const float v1in2 = (h21*u1+h22*v1+h23)*w1in2inv;

        // 计算重投影误差 
        const float squareDist2 = (u2-u1in2)*(u2-u1in2)+(v2-v1in2)*(v2-v1in2);
        const float chiSquare2 = squareDist2*invSigmaSquare;
 
        // 用阈值标记离群点,内点的话累加得分
        if(chiSquare2>th)
            bIn = false;
        else
            score += th - chiSquare2;   

        // Step 2.4 如果从img2 到 img1 和 从img1 到img2的重投影误差均满足要求,则说明是Inlier point
        if(bIn)
            vbMatchesInliers[i]=true;
        else
            vbMatchesInliers[i]=false;
    }

    return score;
}

ReconstructF函数,从基础矩阵F中求解位姿R,t及三维点。

Step 1 统计有效匹配点个数,并用 N 表示

Step 2 根据基础矩阵和相机的内参数矩阵计算本质矩阵

Step 3 从本质矩阵求解两个R解和两个t解,共四组解

Step 4 分别验证求解的4种R和t的组合,选出最佳组合

bool Initializer::ReconstructF(vector &vbMatchesInliers, cv::Mat &F21, cv::Mat &K,
                            cv::Mat &R21, cv::Mat &t21, vector &vP3D, vector &vbTriangulated, float minParallax, int minTriangulated)
{
    // vbMatchesInliers 中存储匹配点对是否是有效
    int N=0;
    for(size_t i=0, iend = vbMatchesInliers.size() ; i vP3D1, vP3D2, vP3D3, vP3D4;

	// 定义四组解分别对同一匹配点集的有效三角化结果,True or False
    vector vbTriangulated1,vbTriangulated2,vbTriangulated3, vbTriangulated4;

	// 定义四种解对应的比较大的特征点对视差角
    float parallax1,parallax2, parallax3, parallax4;

	// Step 4.1 使用同样的匹配点分别检查四组解,记录当前计算的3D点在摄像头前方且投影误差小于阈值的个数,记为有效3D点个数
    int nGood1 = CheckRT(R1,t1,							//当前组解
						 mvKeys1,mvKeys2,				//参考帧和当前帧中的特征点
						 mvMatches12, vbMatchesInliers,	//特征点的匹配关系和Inliers标记
						 K, 							//相机的内参数矩阵
						 vP3D1,							//存储三角化以后特征点的空间坐标
						 4.0*mSigma2,					//三角化测量过程中允许的最大重投影误差
						 vbTriangulated1,				//参考帧中被成功进行三角化测量的特征点的标记
						 parallax1);					//认为某对特征点三角化测量有效的比较大的视差角
    int nGood2 = CheckRT(R2,t1,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D2, 4.0*mSigma2, vbTriangulated2, parallax2);
    int nGood3 = CheckRT(R1,t2,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D3, 4.0*mSigma2, vbTriangulated3, parallax3);
    int nGood4 = CheckRT(R2,t2,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K, vP3D4, 4.0*mSigma2, vbTriangulated4, parallax4);

    // Step 4.2 选取最大可三角化测量的点的数目
    int maxGood = max(nGood1,max(nGood2,max(nGood3,nGood4)));

	// 重置变量,并在后面赋值为最佳R和T
    R21 = cv::Mat();
    t21 = cv::Mat();

    // Step 4.3 确定最小的可以三角化的点数 
    // 在0.9倍的内点数 和 指定值minTriangulated =50 中取最大的,也就是说至少50个
    int nMinGood = max(static_cast(0.9*N), minTriangulated);

	// 统计四组解中重建的有效3D点个数 > 0.7 * maxGood 的解的数目
    // 如果有多个解同时满足该条件,认为结果太接近,nsimilar++,nsimilar>1就认为有问题了,后面返回false
    int nsimilar = 0;
    if(nGood1>0.7*maxGood)
        nsimilar++;
    if(nGood2>0.7*maxGood)
        nsimilar++;
    if(nGood3>0.7*maxGood)
        nsimilar++;
    if(nGood4>0.7*maxGood)
        nsimilar++;

    // Step 4.4 四个结果中如果没有明显的最优结果,或者没有足够数量的三角化点,则返回失败
    // 条件1: 如果四组解能够重建的最多3D点个数小于所要求的最少3D点个数(mMinGood),失败
    // 条件2: 如果存在两组及以上的解能三角化出 >0.7*maxGood的点,说明没有明显最优结果,失败
    if(maxGood1)	
    {
        return false;
    }


    //  Step 4.5 选择最佳解记录结果
    // 条件1: 有效重建最多的3D点,即maxGood == nGoodx,也即是位于相机前方的3D点个数最多
    // 条件2: 三角化视差角 parallax 必须大于最小视差角 minParallax,角度越大3D点越稳定

    //看看最好的good点是在哪种解的条件下发生的
    if(maxGood==nGood1)
    {
		//如果该种解下的parallax大于函数参数中给定的最小值
        if(parallax1>minParallax)
        {
            // 存储3D坐标
            vP3D = vP3D1;

			// 获取特征点向量的三角化测量标记
            vbTriangulated = vbTriangulated1;

			// 存储相机姿态
            R1.copyTo(R21);
            t1.copyTo(t21);
			
            // 结束
            return true;
        }
    }else if(maxGood==nGood2)
    {
        if(parallax2>minParallax)
        {
            vP3D = vP3D2;
            vbTriangulated = vbTriangulated2;

            R2.copyTo(R21);
            t1.copyTo(t21);
            return true;
        }
    }else if(maxGood==nGood3)
    {
        if(parallax3>minParallax)
        {
            vP3D = vP3D3;
            vbTriangulated = vbTriangulated3;

            R1.copyTo(R21);
            t2.copyTo(t21);
            return true;
        }
    }else if(maxGood==nGood4)
    {
        if(parallax4>minParallax)
        {
            vP3D = vP3D4;
            vbTriangulated = vbTriangulated4;

            R2.copyTo(R21);
            t2.copyTo(t21);
            return true;
        }
    }

    // 如果有最优解但是不满足对应的parallax>minParallax,那么返回false表示求解失败
    return false;
}

你可能感兴趣的:(1024程序员节,slam)