• ORB-SLAM2 ---- Initializer::ReconstructH函数


    目录

    1.函数作用

    2.函数解析 

    2.1 调用函数解析

    2.2 Initializer::ReconstructH函数总体思路

    2.2.1 代码

    2.2.2 总体思路解析 

    3.Initializer::CheckRT

    3.1 函数作用

    3.2 构造函数 

    3.3 代码 

    3.4 流程解析 

    3.4.0 初始化参数

    3.4.1 计算初始化两帧的投影矩阵 

    3.4.2  三角化恢复三维点Initializer::Triangulate

    3.4.3  遍历所有的特征点对检查三维点是否合适

    3.4.4 最后处理 


    1.函数作用

            用H矩阵恢复R, t和三维点。

    2.函数解析 

    2.1 调用函数解析

    1. return ReconstructH(vbMatchesInliersH, //输入,匹配成功的特征点对Inliers标记
    2. H, //输入,前面RANSAC计算后的单应矩阵
    3. mK, //输入,相机的内参数矩阵
    4. R21,t21, //输出,计算出来的相机从参考帧1到当前帧2所发生的旋转和位移变换
    5. vP3D, //特征点对经过三角测量之后的空间坐标,也就是地图点
    6. vbTriangulated, //特征点对是否成功三角化的标记
    7. 1.0, //这个对应的形参为minParallax,即认为某对特征点的三角化测量中,认为其测量有效时
    8. //需要满足的最小视差角(如果视差角过小则会引起非常大的观测误差),单位是角度
    9. 50); //为了进行运动恢复,所需要的最少的三角化测量成功的点个数

            该函数的调用函数为Initializer::Initialize,该函数的目的是初始化SLAM系统,即用单目初始化器的第一帧作为SLAM系统的基点并计算出第一帧和第二帧的变换矩阵并初始化地图点。此函数是在计算出H矩阵的前提下,我们想通过H矩阵来恢复单目初始化器两帧间的位姿。

            输入参数为匹配成功的特征点对Inliers标记、RANSAC计算出的单应矩阵H、相机的内参、认为某对特征点的三角化测量中有效时需要满足的最小视差角、为了进行运动恢复,所需要的最少的三角化测量成功的点个数(如果恢复的3D点小于这个则认为初始化失败)

            输出参数为计算出来的相机从参考帧1到当前帧2所发生的旋转和位移变换、特征点对经过三角测量之后的空间坐标,也就是地图点。

    2.2 Initializer::ReconstructH函数总体思路

    2.2.1 代码

    1. /**
    2. * @brief 用H矩阵恢复R, t和三维点
    3. * H矩阵分解常见有两种方法:Faugeras SVD-based decomposition 和 Zhang SVD-based decomposition
    4. * 代码使用了Faugeras SVD-based decomposition算法,参考文献
    5. * Motion and structure from motion in a piecewise planar environment. International Journal of Pattern Recognition and Artificial Intelligence, 1988
    6. *
    7. * @param[in] vbMatchesInliers 匹配点对的内点标记
    8. * @param[in] H21 从参考帧到当前帧的单应矩阵
    9. * @param[in] K 相机的内参数矩阵
    10. * @param[in & out] R21 计算出来的相机旋转
    11. * @param[in & out] t21 计算出来的相机平移
    12. * @param[in & out] vP3D 世界坐标系下,三角化测量特征点对之后得到的特征点的空间坐标
    13. * @param[in & out] vbTriangulated 特征点是否成功三角化的标记
    14. * @param[in] minParallax 对特征点的三角化测量中,认为其测量有效时需要满足的最小视差角(如果视差角过小则会引起非常大的观测误差),单位是角度
    15. * @param[in] minTriangulated 为了进行运动恢复,所需要的最少的三角化测量成功的点个数
    16. * @return true 单应矩阵成功计算出位姿和三维点
    17. * @return false 初始化失败
    18. */
    19. bool Initializer::ReconstructH(vector<bool> &vbMatchesInliers, cv::Mat &H21, cv::Mat &K,
    20. cv::Mat &R21, cv::Mat &t21, vector &vP3D, vector<bool> &vbTriangulated, float minParallax, int minTriangulated)
    21. {
    22. // 目的 :通过单应矩阵H恢复两帧图像之间的旋转矩阵R和平移向量T
    23. // 参考 :Motion and structure from motion in a piecewise plannar environment.
    24. // International Journal of Pattern Recognition and Artificial Intelligence, 1988
    25. // https://www.researchgate.net/publication/243764888_Motion_and_Structure_from_Motion_in_a_Piecewise_Planar_Environment
    26. // 流程:
    27. // 1. 根据H矩阵的奇异值d'= d2 或者 d' = -d2 分别计算 H 矩阵分解的 8 组解
    28. // 1.1 讨论 d' > 0 时的 4 组解
    29. // 1.2 讨论 d' < 0 时的 4 组解
    30. // 2. 对 8 组解进行验证,并选择产生相机前方最多3D点的解为最优解
    31. // 统计匹配的特征点对中属于内点(Inlier)或有效点个数
    32. int N=0;
    33. for(size_t i=0, iend = vbMatchesInliers.size() ; i
    34. if(vbMatchesInliers[i])
    35. N++;
    36. // We recover 8 motion hypotheses using the method of Faugeras et al.
    37. // Motion and structure from motion in a piecewise planar environment.
    38. // International Journal of Pattern Recognition and Artificial Intelligence, 1988
    39. // 参考SLAM十四讲第二版p170-p171
    40. // H = K * (R - t * n / d) * K_inv
    41. // 其中: K表示内参数矩阵
    42. // K_inv 表示内参数矩阵的逆
    43. // R 和 t 表示旋转和平移向量
    44. // n 表示平面法向量
    45. // 令 H = K * A * K_inv
    46. // 则 A = k_inv * H * k
    47. cv::Mat invK = K.inv();
    48. cv::Mat A = invK*H21*K;
    49. // 对矩阵A进行SVD分解
    50. // A 等待被进行奇异值分解的矩阵
    51. // w 奇异值矩阵
    52. // U 奇异值分解左矩阵
    53. // Vt 奇异值分解右矩阵,注意函数返回的是转置
    54. // cv::SVD::FULL_UV 全部分解
    55. // A = U * w * Vt
    56. cv::Mat U,w,Vt,V;
    57. cv::SVD::compute(A, w, U, Vt, cv::SVD::FULL_UV);
    58. // 根据文献eq(8),计算关联变量
    59. V=Vt.t();
    60. // 计算变量s = det(U) * det(V)
    61. // 因为det(V)==det(Vt), 所以 s = det(U) * det(Vt)
    62. float s = cv::determinant(U)*cv::determinant(Vt);
    63. // 取得矩阵的各个奇异值
    64. float d1 = w.at<float>(0);
    65. float d2 = w.at<float>(1);
    66. float d3 = w.at<float>(2);
    67. // SVD分解正常情况下特征值di应该是正的,且满足d1>=d2>=d3
    68. if(d1/d2<1.00001 || d2/d3<1.00001) {
    69. return false;
    70. }
    71. // 在ORBSLAM中没有对奇异值 d1 d2 d3按照论文中描述的关系进行分类讨论, 而是直接进行了计算
    72. // 定义8中情况下的旋转矩阵、平移向量和空间向量
    73. vector vR, vt, vn;
    74. vR.reserve(8);
    75. vt.reserve(8);
    76. vn.reserve(8);
    77. // Step 1.1 讨论 d' > 0 时的 4 组解
    78. // 根据论文eq.(12)有
    79. // x1 = e1 * sqrt((d1 * d1 - d2 * d2) / (d1 * d1 - d3 * d3))
    80. // x2 = 0
    81. // x3 = e3 * sqrt((d2 * d2 - d2 * d2) / (d1 * d1 - d3 * d3))
    82. // 令 aux1 = sqrt((d1*d1-d2*d2)/(d1*d1-d3*d3))
    83. // aux3 = sqrt((d2*d2-d3*d3)/(d1*d1-d3*d3))
    84. // 则
    85. // x1 = e1 * aux1
    86. // x3 = e3 * aux2
    87. // 因为 e1,e2,e3 = 1 or -1
    88. // 所以有x1和x3有四种组合
    89. // x1 = {aux1,aux1,-aux1,-aux1}
    90. // x3 = {aux3,-aux3,aux3,-aux3}
    91. float aux1 = sqrt((d1*d1-d2*d2)/(d1*d1-d3*d3));
    92. float aux3 = sqrt((d2*d2-d3*d3)/(d1*d1-d3*d3));
    93. float x1[] = {aux1,aux1,-aux1,-aux1};
    94. float x3[] = {aux3,-aux3,aux3,-aux3};
    95. // 根据论文eq.(13)有
    96. // sin(theta) = e1 * e3 * sqrt(( d1 * d1 - d2 * d2) * (d2 * d2 - d3 * d3)) /(d1 + d3)/d2
    97. // cos(theta) = (d2* d2 + d1 * d3) / (d1 + d3) / d2
    98. // 令 aux_stheta = sqrt((d1*d1-d2*d2)*(d2*d2-d3*d3))/((d1+d3)*d2)
    99. // 则 sin(theta) = e1 * e3 * aux_stheta
    100. // cos(theta) = (d2*d2+d1*d3)/((d1+d3)*d2)
    101. // 因为 e1 e2 e3 = 1 or -1
    102. // 所以 sin(theta) = {aux_stheta, -aux_stheta, -aux_stheta, aux_stheta}
    103. float aux_stheta = sqrt((d1*d1-d2*d2)*(d2*d2-d3*d3))/((d1+d3)*d2);
    104. float ctheta = (d2*d2+d1*d3)/((d1+d3)*d2);
    105. float stheta[] = {aux_stheta, -aux_stheta, -aux_stheta, aux_stheta};
    106. // 计算旋转矩阵 R'
    107. //根据不同的e1 e3组合所得出来的四种R t的解
    108. // | ctheta 0 -aux_stheta| | aux1|
    109. // Rp = | 0 1 0 | tp = | 0 |
    110. // | aux_stheta 0 ctheta | |-aux3|
    111. // | ctheta 0 aux_stheta| | aux1|
    112. // Rp = | 0 1 0 | tp = | 0 |
    113. // |-aux_stheta 0 ctheta | | aux3|
    114. // | ctheta 0 aux_stheta| |-aux1|
    115. // Rp = | 0 1 0 | tp = | 0 |
    116. // |-aux_stheta 0 ctheta | |-aux3|
    117. // | ctheta 0 -aux_stheta| |-aux1|
    118. // Rp = | 0 1 0 | tp = | 0 |
    119. // | aux_stheta 0 ctheta | | aux3|
    120. // 开始遍历这四种情况中的每一种
    121. for(int i=0; i<4; i++)
    122. {
    123. //生成Rp,就是eq.(8) 的 R'
    124. cv::Mat Rp=cv::Mat::eye(3,3,CV_32F);
    125. Rp.at<float>(0,0)=ctheta;
    126. Rp.at<float>(0,2)=-stheta[i];
    127. Rp.at<float>(2,0)=stheta[i];
    128. Rp.at<float>(2,2)=ctheta;
    129. // eq.(8) 计算R
    130. cv::Mat R = s*U*Rp*Vt;
    131. // 保存
    132. vR.push_back(R);
    133. // eq. (14) 生成tp
    134. cv::Mat tp(3,1,CV_32F);
    135. tp.at<float>(0)=x1[i];
    136. tp.at<float>(1)=0;
    137. tp.at<float>(2)=-x3[i];
    138. tp*=d1-d3;
    139. // 这里虽然对t有归一化,并没有决定单目整个SLAM过程的尺度
    140. // 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变
    141. // eq.(8)恢复原始的t
    142. cv::Mat t = U*tp;
    143. vt.push_back(t/cv::norm(t));
    144. // 构造法向量np
    145. cv::Mat np(3,1,CV_32F);
    146. np.at<float>(0)=x1[i];
    147. np.at<float>(1)=0;
    148. np.at<float>(2)=x3[i];
    149. // eq.(8) 恢复原始的法向量
    150. cv::Mat n = V*np;
    151. //看PPT 16页的图,保持平面法向量向上
    152. if(n.at<float>(2)<0)
    153. n=-n;
    154. // 添加到vector
    155. vn.push_back(n);
    156. }
    157. // Step 1.2 讨论 d' < 0 时的 4 组解
    158. float aux_sphi = sqrt((d1*d1-d2*d2)*(d2*d2-d3*d3))/((d1-d3)*d2);
    159. // cos_theta项
    160. float cphi = (d1*d3-d2*d2)/((d1-d3)*d2);
    161. // 考虑到e1,e2的取值,这里的sin_theta有两种可能的解
    162. float sphi[] = {aux_sphi, -aux_sphi, -aux_sphi, aux_sphi};
    163. // 对于每种由e1 e3取值的组合而形成的四种解的情况
    164. for(int i=0; i<4; i++)
    165. {
    166. // 计算旋转矩阵 R'
    167. cv::Mat Rp=cv::Mat::eye(3,3,CV_32F);
    168. Rp.at<float>(0,0)=cphi;
    169. Rp.at<float>(0,2)=sphi[i];
    170. Rp.at<float>(1,1)=-1;
    171. Rp.at<float>(2,0)=sphi[i];
    172. Rp.at<float>(2,2)=-cphi;
    173. // 恢复出原来的R
    174. cv::Mat R = s*U*Rp*Vt;
    175. // 然后添加到vector中
    176. vR.push_back(R);
    177. // 构造tp
    178. cv::Mat tp(3,1,CV_32F);
    179. tp.at<float>(0)=x1[i];
    180. tp.at<float>(1)=0;
    181. tp.at<float>(2)=x3[i];
    182. tp*=d1+d3;
    183. // 恢复出原来的t
    184. cv::Mat t = U*tp;
    185. // 归一化之后加入到vector中,要提供给上面的平移矩阵都是要进行过归一化的
    186. vt.push_back(t/cv::norm(t));
    187. // 构造法向量np
    188. cv::Mat np(3,1,CV_32F);
    189. np.at<float>(0)=x1[i];
    190. np.at<float>(1)=0;
    191. np.at<float>(2)=x3[i];
    192. // 恢复出原来的法向量
    193. cv::Mat n = V*np;
    194. // 保证法向量指向上方
    195. if(n.at<float>(2)<0)
    196. n=-n;
    197. // 添加到vector中
    198. vn.push_back(n);
    199. }
    200. // 最好的good点
    201. int bestGood = 0;
    202. // 其次最好的good点
    203. int secondBestGood = 0;
    204. // 最好的解的索引,初始值为-1
    205. int bestSolutionIdx = -1;
    206. // 最大的视差角
    207. float bestParallax = -1;
    208. // 存储最好解对应的,对特征点对进行三角化测量的结果
    209. vector bestP3D;
    210. // 最佳解所对应的,那些可以被三角化测量的点的标记
    211. vector<bool> bestTriangulated;
    212. // Instead of applying the visibility constraints proposed in the WFaugeras' paper (which could fail for points seen with low parallax)
    213. // We reconstruct all hypotheses and check in terms of triangulated points and parallax
    214. // Step 2. 对 8 组解进行验证,并选择产生相机前方最多3D点的解为最优解
    215. for(size_t i=0; i<8; i++)
    216. {
    217. // 第i组解对应的比较大的视差角
    218. float parallaxi;
    219. // 三角化测量之后的特征点的空间坐标
    220. vector vP3Di;
    221. // 特征点对是否被三角化的标记
    222. vector<bool> vbTriangulatedi;
    223. // 调用 Initializer::CheckRT(), 计算good点的数目
    224. int nGood = CheckRT(vR[i],vt[i], //当前组解的旋转矩阵和平移向量
    225. mvKeys1,mvKeys2, //特征点
    226. mvMatches12,vbMatchesInliers, //特征匹配关系以及Inlier标记
    227. K, //相机的内参数矩阵
    228. vP3Di, //存储三角化测量之后的特征点空间坐标的
    229. 4.0*mSigma2, //三角化过程中允许的最大重投影误差
    230. vbTriangulatedi, //特征点是否被成功进行三角测量的标记
    231. parallaxi); // 这组解在三角化测量的时候的比较大的视差角
    232. // 更新历史最优和次优的解
    233. // 保留最优的和次优的解.保存次优解的目的是看看最优解是否突出
    234. if(nGood>bestGood)
    235. {
    236. // 如果当前组解的good点数是历史最优,那么之前的历史最优就变成了历史次优
    237. secondBestGood = bestGood;
    238. // 更新历史最优点
    239. bestGood = nGood;
    240. // 最优解的组索引为i(就是当前次遍历)
    241. bestSolutionIdx = i;
    242. // 更新变量
    243. bestParallax = parallaxi;
    244. bestP3D = vP3Di;
    245. bestTriangulated = vbTriangulatedi;
    246. }
    247. // 如果当前组的good计数小于历史最优但却大于历史次优
    248. else if(nGood>secondBestGood)
    249. {
    250. // 说明当前组解是历史次优点,更新之
    251. secondBestGood = nGood;
    252. }
    253. }
    254. // Step 3 选择最优解。要满足下面的四个条件
    255. // 1. good点数最优解明显大于次优解,这里取0.75经验值
    256. // 2. 视角差大于规定的阈值
    257. // 3. good点数要大于规定的最小的被三角化的点数量
    258. // 4. good数要足够多,达到总数的90%以上
    259. if(secondBestGood<0.75*bestGood &&
    260. bestParallax>=minParallax &&
    261. bestGood>minTriangulated &&
    262. bestGood>0.9*N)
    263. {
    264. // 从最佳的解的索引访问到R,t
    265. vR[bestSolutionIdx].copyTo(R21);
    266. vt[bestSolutionIdx].copyTo(t21);
    267. // 获得最佳解时,成功三角化的三维点,以后作为初始地图点使用
    268. vP3D = bestP3D;
    269. // 获取特征点的被成功进行三角化的标记
    270. vbTriangulated = bestTriangulated;
    271. //返回真,找到了最好的解
    272. return true;
    273. }
    274. return false;
    275. }

    2.2.2 总体思路解析 

            1. 根据H矩阵的奇异值d'= d2 或者 d' = -d2 分别计算 H 矩阵分解的 8 组解(不需要明白)
                  1.1 讨论 d' > 0 时的 4 组解
                  1.2 讨论 d' < 0 时的 4 组解
            2. 对 8 组解进行验证,并选择产生相机前方最多3D点的解为最优解。(CheckRT)

            这里分解为八组解为论文中所做的事情,我们不加解释,主要将这八组解解出的R,t,这8 组解进行验证,并选择产生相机前方最多3D点的解为最优解。

            总体思路:我们按照论文所作计算出了八组R,t解,对每一个解进行验证(计算能恢复的3D点数量),记录恢复3D点最多和次多的R,t解。判断:

            如果:

            ①0.75倍最优解的数量大于次优解(保证系统鲁棒性)

            ②视角差大于规定的阈值

            ③good点数要大于规定的最小的被三角化的点数量
            ④good数要足够多,达到总数的90%以上

            则:

            选取这组解的R,t作为地图初始化的R,t,这组解初始化的3D点作为初始地图点使用,获取特征点的被成功进行三角化的标记,向上层函数返回true表示单目初始化成功。

    3.Initializer::CheckRT

    3.1 函数作用

            用位姿来对特征匹配点三角化,从中筛选中合格的三维点。

    3.2 构造函数 

    1. * @brief 用位姿来对特征匹配点三角化,从中筛选中合格的三维点
    2. *
    3. * @param[in] R 旋转矩阵R
    4. * @param[in] t 平移矩阵t
    5. * @param[in] vKeys1 参考帧特征点
    6. * @param[in] vKeys2 当前帧特征点
    7. * @param[in] vMatches12 两帧特征点的匹配关系
    8. * @param[in] vbMatchesInliers 特征点对内点标记
    9. * @param[in] K 相机内参矩阵
    10. * @param[in & out] vP3D 三角化测量之后的特征点的空间坐标
    11. * @param[in] th2 重投影误差的阈值
    12. * @param[in & out] vbGood 标记成功三角化点?
    13. * @param[in & out] parallax 计算出来的比较大的视差角(注意不是最大,具体看后面代码)
    14. * @return int
    15. */
    16. int Initializer::CheckRT(const cv::Mat &R, const cv::Mat &t, const vector &vKeys1, const vector &vKeys2,
    17. const vector &vMatches12, vector<bool> &vbMatchesInliers,
    18. const cv::Mat &K, vector &vP3D, float th2, vector<bool> &vbGood, float ¶llax)

            传入参数:

            ①参考帧到当前帧的旋转矩阵R和平移矩阵t

            ②参考帧(第一帧)和当前帧(第二帧)的特征点容器vKeys1、vKeys2

            ③两帧特征点的匹配标记vMatches12以及特征点对内点标记vbMatchesInliers

            ④相机内参K,重投影误差阈值th2

            传出参数:

            ①三角化测量之后的特征点的空间坐标v3D

            ②标记成功三角化点vGoog

            ③返回三角化点的数量

    3.3 代码 

    1. int Initializer::CheckRT(const cv::Mat &R, const cv::Mat &t, const vector &vKeys1, const vector &vKeys2,
    2. const vector &vMatches12, vector<bool> &vbMatchesInliers,
    3. const cv::Mat &K, vector &vP3D, float th2, vector<bool> &vbGood, float ¶llax)
    4. {
    5. // 对给出的特征点对及其R t , 通过三角化检查解的有效性,也称为 cheirality check
    6. // Calibration parameters
    7. //从相机内参数矩阵获取相机的校正参数
    8. const float fx = K.at<float>(0,0);
    9. const float fy = K.at<float>(1,1);
    10. const float cx = K.at<float>(0,2);
    11. const float cy = K.at<float>(1,2);
    12. //特征点是否是good点的标记,这里的特征点指的是参考帧中的特征点
    13. vbGood = vector<bool>(vKeys1.size(),false);
    14. //重设存储空间坐标的点的大小
    15. vP3D.resize(vKeys1.size());
    16. //存储计算出来的每对特征点的视差
    17. vector<float> vCosParallax;
    18. vCosParallax.reserve(vKeys1.size());
    19. // Camera 1 Projection Matrix K[I|0]
    20. // Step 1:计算相机的投影矩阵
    21. // 投影矩阵P是一个 3x4 的矩阵,可以将空间中的一个点投影到平面上,获得其平面坐标,这里均指的是齐次坐标。
    22. // 对于第一个相机是 P1=K*[I|0]
    23. // 以第一个相机的光心作为世界坐标系, 定义相机的投影矩阵
    24. cv::Mat P1(3,4, //矩阵的大小是3x4
    25. CV_32F, //数据类型是浮点数
    26. cv::Scalar(0)); //初始的数值是0
    27. //将整个K矩阵拷贝到P1矩阵的左侧3x3矩阵,因为 K*I = K
    28. K.copyTo(P1.rowRange(0,3).colRange(0,3));
    29. // 第一个相机的光心设置为世界坐标系下的原点
    30. cv::Mat O1 = cv::Mat::zeros(3,1,CV_32F);
    31. // Camera 2 Projection Matrix K[R|t]
    32. // 计算第二个相机的投影矩阵 P2=K*[R|t]
    33. cv::Mat P2(3,4,CV_32F);
    34. R.copyTo(P2.rowRange(0,3).colRange(0,3));
    35. t.copyTo(P2.rowRange(0,3).col(3));
    36. //最终结果是K*[R|t]
    37. P2 = K*P2;
    38. // 第二个相机的光心在世界坐标系下的坐标
    39. cv::Mat O2 = -R.t()*t;
    40. //在遍历开始前,先将good点计数设置为0
    41. int nGood=0;
    42. // 开始遍历所有的特征点对
    43. for(size_t i=0, iend=vMatches12.size();i
    44. {
    45. // 跳过outliers
    46. if(!vbMatchesInliers[i])
    47. continue;
    48. // Step 2 获取特征点对,调用Triangulate() 函数进行三角化,得到三角化测量之后的3D点坐标
    49. // kp1和kp2是匹配好的有效特征点
    50. const cv::KeyPoint &kp1 = vKeys1[vMatches12[i].first];
    51. const cv::KeyPoint &kp2 = vKeys2[vMatches12[i].second];
    52. //存储三维点的的坐标
    53. cv::Mat p3dC1;
    54. // 利用三角法恢复三维点p3dC1
    55. Triangulate(kp1,kp2, //特征点
    56. P1,P2, //投影矩阵
    57. p3dC1); //输出,三角化测量之后特征点的空间坐标
    58. // Step 3 第一关:检查三角化的三维点坐标是否合法(非无穷值)
    59. // 只要三角测量的结果中有一个是无穷大的就说明三角化失败,跳过对当前点的处理,进行下一对特征点的遍历
    60. if(!isfinite(p3dC1.at<float>(0)) || !isfinite(p3dC1.at<float>(1)) || !isfinite(p3dC1.at<float>(2)))
    61. {
    62. //其实这里就算是不这样写也没问题,因为默认的匹配点对就不是good点
    63. vbGood[vMatches12[i].first]=false;
    64. //继续对下一对匹配点的处理
    65. continue;
    66. }
    67. // Check parallax
    68. // Step 4 第二关:通过三维点深度值正负、两相机光心视差角大小来检查是否合法
    69. //得到向量PO1
    70. cv::Mat normal1 = p3dC1 - O1;
    71. //求取模长,其实就是距离
    72. float dist1 = cv::norm(normal1);
    73. //同理构造向量PO2
    74. cv::Mat normal2 = p3dC1 - O2;
    75. //求模长
    76. float dist2 = cv::norm(normal2);
    77. //根据公式:a.*b=|a||b|cos_theta 可以推导出来下面的式子
    78. float cosParallax = normal1.dot(normal2)/(dist1*dist2);
    79. // Check depth in front of first camera (only if enough parallax, as "infinite" points can easily go to negative depth)
    80. // 如果深度值为负值,为非法三维点跳过该匹配点对
    81. // ?视差比较小时,重投影误差比较大。这里0.99998 对应的角度为0.36°,这里不应该是 cosParallax>0.99998 吗?
    82. // ?因为后面判断vbGood 点时的条件也是 cosParallax<0.99998
    83. // !可能导致初始化不稳定
    84. if(p3dC1.at<float>(2)<=0 && cosParallax<0.99998)
    85. continue;
    86. // Check depth in front of second camera (only if enough parallax, as "infinite" points can easily go to negative depth)
    87. // 讲空间点p3dC1变换到第2个相机坐标系下变为p3dC2
    88. cv::Mat p3dC2 = R*p3dC1+t;
    89. //判断过程和上面的相同
    90. if(p3dC2.at<float>(2)<=0 && cosParallax<0.99998)
    91. continue;
    92. // Step 5 第三关:计算空间点在参考帧和当前帧上的重投影误差,如果大于阈值则舍弃
    93. // Check reprojection error in first image
    94. // 计算3D点在第一个图像上的投影误差
    95. //投影到参考帧图像上的点的坐标x,y
    96. float im1x, im1y;
    97. //这个使能空间点的z坐标的倒数
    98. float invZ1 = 1.0/p3dC1.at<float>(2);
    99. //投影到参考帧图像上。因为参考帧下的相机坐标系和世界坐标系重合,因此这里就直接进行投影就可以了
    100. im1x = fx*p3dC1.at<float>(0)*invZ1+cx;
    101. im1y = fy*p3dC1.at<float>(1)*invZ1+cy;
    102. //参考帧上的重投影误差,这个的确就是按照定义来的
    103. float squareError1 = (im1x-kp1.pt.x)*(im1x-kp1.pt.x)+(im1y-kp1.pt.y)*(im1y-kp1.pt.y);
    104. // 重投影误差太大,跳过淘汰
    105. if(squareError1>th2)
    106. continue;
    107. // Check reprojection error in second image
    108. // 计算3D点在第二个图像上的投影误差,计算过程和第一个图像类似
    109. float im2x, im2y;
    110. // 注意这里的p3dC2已经是第二个相机坐标系下的三维点了
    111. float invZ2 = 1.0/p3dC2.at<float>(2);
    112. im2x = fx*p3dC2.at<float>(0)*invZ2+cx;
    113. im2y = fy*p3dC2.at<float>(1)*invZ2+cy;
    114. // 计算重投影误差
    115. float squareError2 = (im2x-kp2.pt.x)*(im2x-kp2.pt.x)+(im2y-kp2.pt.y)*(im2y-kp2.pt.y);
    116. // 重投影误差太大,跳过淘汰
    117. if(squareError2>th2)
    118. continue;
    119. // Step 6 统计经过检验的3D点个数,记录3D点视差角
    120. // 如果运行到这里就说明当前遍历的这个特征点对靠谱,经过了重重检验,说明是一个合格的点,称之为good点
    121. vCosParallax.push_back(cosParallax);
    122. //存储这个三角化测量后的3D点在世界坐标系下的坐标
    123. vP3D[vMatches12[i].first] = cv::Point3f(p3dC1.at<float>(0),p3dC1.at<float>(1),p3dC1.at<float>(2));
    124. //good点计数++
    125. nGood++;
    126. //判断视差角,只有视差角稍稍大一丢丢的才会给打good点标记
    127. //? bug 我觉得这个写的位置不太对。你的good点计数都++了然后才判断,不是会让good点标志和good点计数不一样吗
    128. if(cosParallax<0.99998)
    129. vbGood[vMatches12[i].first]=true;
    130. }
    131. // Step 7 得到3D点中较小的视差角,并且转换成为角度制表示
    132. if(nGood>0)
    133. {
    134. // 从小到大排序,注意vCosParallax值越大,视差越小
    135. sort(vCosParallax.begin(),vCosParallax.end());
    136. // !排序后并没有取最小的视差角,而是取一个较小的视差角
    137. // 作者的做法:如果经过检验过后的有效3D点小于50个,那么就取最后那个最小的视差角(cos值最大)
    138. // 如果大于50个,就取排名第50个的较小的视差角即可,为了避免3D点太多时出现太小的视差角
    139. size_t idx = min(50,int(vCosParallax.size()-1));
    140. //将这个选中的角弧度制转换为角度制
    141. parallax = acos(vCosParallax[idx])*180/CV_PI;
    142. }
    143. else
    144. //如果没有good点那么这个就直接设置为0了
    145. parallax=0;
    146. //返回good点计数
    147. return nGood;
    148. }

    3.4 流程解析 

    3.4.0 初始化参数

            ①特征点是否是good点的标记,这里的特征点指的是参考帧中的特征点,将vGood初始化为第一帧特征点的数量,其为bool类型。

            ②三角化测量之后的特征点的空间坐标v3D初始化大小为第一帧中特征点的数量。

    3.4.1 计算初始化两帧的投影矩阵 

            投影矩阵P是一个 3x4 的矩阵,可以将空间中的一个点投影到平面上,获得其平面坐标,这里均指的是齐次坐标。

            由于以第一个相机的光心作为世界坐标系。 其投影矩阵计算推导如下:

    ZP_{uv} = K(RP_{w}+t)=KTP_{w}

            我们默认第一个相机的R,t矩阵为[E|0],因此第一个相机的投影矩阵为K。第一个相机的光心坐标为(0,0,0)^{T}

            我们从传入参数可知第一个相机到第二个相机的R,t变换T,因此再左乘相机内参矩阵K就能得到像素坐标,即世界坐标向像素坐标的投影矩阵为KT

            同时计算第二帧的光心坐标在原点(第一帧)的坐标,计算如下图所示:

            第二个相机的光心在世界坐标系下的坐标,即我们要求第二个相机的光心在第一个相机坐标系下的坐标。

    3.4.2  三角化恢复三维点Initializer::Triangulate

    1.数学原理

            我们将投影方程进行如下描述:

    \begin{bmatrix} x\\y \\1 \end{bmatrix}=\lambda \begin{bmatrix} p_{1} &p_{2} &p_{3} &p_{4} \\ p_{5} & p_{6} &p_{7} & p_{8} \\ p_{9} &p_{10} &p_{11} & p_{12} \end{bmatrix} \begin{bmatrix} X\\Y \\Z \\1 \end{bmatrix}

            为方便推导,简单记为:

            为了化为齐次方程,左右两边同时叉乘,得到:

            利用两对匹配点,得到:

            SVD求解,右奇异矩阵的最后一行就是最终的解。

    2.代码

    1. /** 给定投影矩阵P1,P2和图像上的匹配特征点点kp1,kp2,从而计算三维点坐标
    2. * @brief
    3. *
    4. * @param[in] kp1 特征点, in reference frame
    5. * @param[in] kp2 特征点, in current frame
    6. * @param[in] P1 投影矩阵P1
    7. * @param[in] P2 投影矩阵P2
    8. * @param[in & out] x3D 计算的三维点
    9. */
    10. void Initializer::Triangulate(
    11. const cv::KeyPoint &kp1, //特征点, in reference frame
    12. const cv::KeyPoint &kp2, //特征点, in current frame
    13. const cv::Mat &P1, //投影矩阵P1
    14. const cv::Mat &P2, //投影矩阵P2
    15. cv::Mat &x3D) //三维点
    16. {
    17. // 原理
    18. // Trianularization: 已知匹配特征点对{x x'} 和 各自相机矩阵{P P'}, 估计三维点 X
    19. // x' = P'X x = PX
    20. // 它们都属于 x = aPX模型
    21. // |X|
    22. // |x| |p1 p2 p3 p4 ||Y| |x| |--p0--||.|
    23. // |y| = a |p5 p6 p7 p8 ||Z| ===>|y| = a|--p1--||X|
    24. // |z| |p9 p10 p11 p12||1| |z| |--p2--||.|
    25. // 采用DLT的方法:x叉乘PX = 0
    26. // |yp2 - p1| |0|
    27. // |p0 - xp2| X = |0|
    28. // |xp1 - yp0| |0|
    29. // 两个点:
    30. // |yp2 - p1 | |0|
    31. // |p0 - xp2 | X = |0| ===> AX = 0
    32. // |y'p2' - p1' | |0|
    33. // |p0' - x'p2'| |0|
    34. // 变成程序中的形式:
    35. // |xp2 - p0 | |0|
    36. // |yp2 - p1 | X = |0| ===> AX = 0
    37. // |x'p2'- p0'| |0|
    38. // |y'p2'- p1'| |0|
    39. // 然后就组成了一个四元一次正定方程组,SVD求解,右奇异矩阵的最后一行就是最终的解.
    40. //这个就是上面注释中的矩阵A
    41. cv::Mat A(4,4,CV_32F);
    42. //构造参数矩阵A
    43. A.row(0) = kp1.pt.x*P1.row(2)-P1.row(0);
    44. A.row(1) = kp1.pt.y*P1.row(2)-P1.row(1);
    45. A.row(2) = kp2.pt.x*P2.row(2)-P2.row(0);
    46. A.row(3) = kp2.pt.y*P2.row(2)-P2.row(1);
    47. //奇异值分解的结果
    48. cv::Mat u,w,vt;
    49. //对系数矩阵A进行奇异值分解
    50. cv::SVD::compute(A,w,u,vt,cv::SVD::MODIFY_A| cv::SVD::FULL_UV);
    51. //根据前面的结论,奇异值分解右矩阵的最后一行其实就是解,原理类似于前面的求最小二乘解,四个未知数四个方程正好正定
    52. //别忘了我们更习惯用列向量来表示一个点的空间坐标
    53. x3D = vt.row(3).t();
    54. //为了符合其次坐标的形式,使最后一维为1
    55. x3D = x3D.rowRange(0,3)/x3D.at<float>(3);
    56. }

            至此我们恢复一对匹配特征点的3D点。

    3.4.3  遍历所有的特征点对检查三维点是否合适

            我们遍历所有匹配的特征点对,如果不是外点,则将匹配好的特征点对传入Initializer::Triangulate函数内得到三维点。我们对三维点进行判断:

            ①检查三角化的三维点坐标是否合法:防止三角化出的点有一维坐标是无穷。

            ②通过三维点深度值正负、两相机光心视差角大小来检查是否合法 。即判断如果深度值为负值,为非法三维点跳过该匹配点对。计算视差角,当视差角比较小时,重投影误差比较大。

            ③计算空间点在参考帧和当前帧上的重投影误差,如果大于阈值则舍弃。计算3D点在第一、二个图像上的投影误差,看是否超过阈值选择是否抛弃。

            如果如上满足了,这个3D点可以被留下来,用vCosParallax向量存储合格3D点生成时计算出来的视差,存储这个三角化测量后的3D点在世界坐标系下的坐标。并将这两帧累计成功初始化的3D点nGood累加。

    vP3D[vMatches12[i].first] = cv::Point3f(p3dC1.at<float>(0),p3dC1.at<float>(1),p3dC1.at<float>(2));

    3.4.4 最后处理 

            如果我们的变量nGood大于0(成功三角化得到的3D点的数目),我们将视差角从小到大排序,排序后并没有取最小的视差角,而是取一个较小的视差角。如果经过检验过后的有效3D点小于50个,那么就取最后那个最小的视差角(cos值最大),如果大于50个,就取排名第50个的较小的视差角即可,为了避免3D点太多时出现太小的视差角。将这个选中的角弧度制转换为角度制输出。

  • 相关阅读:
    rv1126-rknpu-v1.7.3添加opencv库
    社区故事|SmartX 用户社区技术发烧友独家专访
    Windows 堆管理机制 [3] Windows XP SP2 – Windows 2003 版本
    【计算机网络笔记】IPv6简介
    TCPIP网络编程(一)
    ucos任务调度原理
    以前红火的农村大集,现在为何赶集的人越来越少,生意还好不好做
    MPI简谈
    AAPT: error: resource android:attr/lStar not found
    react组件父子传值
  • 原文地址:https://blog.csdn.net/qq_41694024/article/details/127873711