单目初始化 单应矩阵 本质矩阵 恢复R t 三角变换求 3D点

博文末尾支持二维码赞赏哦 ^_^

/*
* This file is part of ORB-SLAM2
*
* 单目相机初始化
* 用于平面场景的单应性矩阵H(8中运动假设) 和用于非平面场景的基础矩阵F(4种运动假设),
* 然后通过一个评分规则来选择合适的模型,恢复相机的旋转矩阵R和平移向量t 和 对应的3D点(尺度问题)。
*
* *【0】2D-2D点对 求变换矩阵前先进行标准化  去 均值后再除以绝对矩
* 单目初始化特征点 归一化 坐标均值为0  一阶绝对矩为1
* mean_x  =  sum( ui) / N   mean_y =  sum(vi)/N
* 绝对矩  mean_x_dev = sum(abs(ui - mean_x))/ N     mean_y_dev = sum(abs(vi - mean_y))/ N
*
* 绝对矩倒数  sX = 1/mean_x_dev     sY = 1/mean_y_dev
*
* 标准化后的点坐标
* u = (ui - mean_x) × sX
* v =  (vi - mean_y) * sY
*
* 标准化矩阵   其逆矩阵×标准化点 得到原始坐标
*      用于 计算变换矩阵后  从原始坐标计算对称的转换误差 来计算变换矩阵得分
* T = sX   0    -mean_x * sX
*       0   sY   -mean_y * sY
*       0    0         1
*
* 标准化矩阵  * 点坐标    =   标准化后的的坐标
*         ui         ui × sX - mean_x * sX  = (ui - mean_x) × sX       u
*  T ×  vi    =    vi  × sY - mean_y * sY  = (vi - mean_y) × sY   =   v
*         1               1                                          1
*
* 点坐标    =    标准化矩阵 逆矩阵 * 标准化后的的坐标
*
*
*
*【1】 2D- 2D 点对 单应矩阵  H   2D点对其次坐标  间的 转换 矩阵  3*3
*  采用归一化的直接线性变换(normalized DLT)
* * p2 = H *p1 关键点  对的 变换关系矩阵H* 一个点对 2个约束* 4 点法求解  单应矩阵 H 再对 H进行分解
*
*【2】 对极几何 求解 基本矩阵 F  两组单目相机 2D图像
* (随机采样序列 8点法求解)
*  2D 点对 求 两相机的 旋转和平移矩阵* 空间点 P  两相机 像素点对  p1  p2 两相机 归一化平面上的点对 x1 x2 与P点对应* 相机内参数 K  两镜头旋转平移矩阵  R t 或者 变换矩阵 T*  p1 = KP  (世界坐标系)     p2 = K( RP + t)  = KTP*  而 x1 =  K逆* p1  x2 =  K逆* p2  相机坐标系下 归一化平面上的点     x1= (px -cx)/fx    x2= (py -cy)/fy* 所以   x1 = P  得到   x2 =  R * x1  + t   * *  t 外积  x2  = t 外积 R * x1 +   t 外积 t  =   t 外积 R * x1   ;t 外积 t   =0    sin(cet) =0 垂线段投影 方向垂直两个向量*  x2转置 *  t 外积  x2 = x2转置 * t 外积 R  x1   = 0 ;因为  t 外积  x2 得到的向量垂直 t 也垂直 x2*   有 x2转置 * t 外积 R  x1   = x2转置 * E * x1 =  0 ; E 为本质矩阵* p2转置 * K 转置逆 * t 外积 R * K逆 * p1   = p2转置 * F * p1 =  0 ;* F 为基础矩阵* * x2转置 * E * x1 =  0    x1 x2  为 由 像素坐标转化的归一化坐标* 一个点对一个约束 ,8点法  可以算出 E的各个元素 ,* 再 奇异值分解 E 得到 R t* * * * 【3】变换矩阵 评分 方法* 和卡方分布的对应值比较,由此判定该点是否为内点。累计内点的总得分* SM=∑i( ρM( d2cr(xic,xir,M)   +   ρM( d2rc(xic,xir,M ) )*  d2cr 为 2D-2D 点对  通过哦转换矩阵的 对成转换误差* * ρM 函数为 ρM(d^2)  = 0                       当  d^2 > 阈值(单应矩阵时 为 5.99  基础矩阵时为 3.84)*                                   得分上限  - d^2   当  d^2 < 阈值*                                    得分上限 均为 5.99 * *【4】 从两个模型 H F 得分为 Sh   Sf 中选着一个 最优秀的 模型 的方法为* * 文中认为,当场景是一个平面、或近似为一个平面、或者视差较小的时候,可以使用单应性矩阵H,* 当场景是一个非平面、视差大的场景时,使用基础矩阵F恢复运动* RH=SH /(SH+SF)* 当RH大于0.45时,选择从单应性变换矩阵还原运动。* 不过ORB_SLAM2源代码中使用的是0.4作为阈值* * * 【5】单应矩阵求解*  *  1点 变成 2点   p2   =  H21 * p1u2        h1  h2  h3        u1v2  =    h4  h5  h6    *  v11          h7  h8  h9       1   u2 = (h1*u1 + h2*v1 + h3) /( h7*u1 + h8*v1 + h9)v2 = (h4*u1 + h5*v1 + h6) /( h7*u1 + h8*v1 + h9)-((h4*u1 + h5*v1 + h6) - ( h7*u1*v2 + h8*v1*v2 + h9*v2))=0  式子为0  左侧加 - 号不变h1*u1 + h2*v1 + h3 - ( h7*u1*u2 + h8*v1*u2 + h9*u2)=00    0   0  -u1  -v1  -1   u1*v2   v1*v2    v2u1 v1  1    0    0    0   -u1*u2  - v1*u2  -u2    ×(h1 h2 h3 h4 h5 h6 h7 h8 h9)转置  = 08对点  约束 A A × h = 0 求h   奇异值分解 A 得到 单元矩阵 H* * 【6】单应变换 求 变化距离误差*  * 1点 变成 2点   p2   =  H12 * p1u2        h11  h12  h13        u1v2  =    h21  h22  h23    *  v11          h31  h32  h33         1   第三行 * 2 点 变成 1点  p1   =  H21 * p2u1‘        h11inv   h12inv   h13inv         u2v1’  =    h21inv   h22inv   h23inv     *  v21          h31inv   h32inv   h33inv          1    第三行 h31inv*u2+h32inv*v2+h33inv 前两行 同除以 第三行 消去非零因子p2 由单应转换到 p1u1‘ = (h11inv*u2+h12inv*v2+h13inv)* 第三行倒数v1’ = (h21inv*u2+h22inv*v2+h23inv)*第三行倒数然后计算 和 真实 p1点 坐标的差值(u1-u2in1)*(u1-u2in1) + (v1-v2in1)*(v1-v2in1)   横纵坐标差值平方和* 【7】单应矩阵恢复  旋转矩阵 R 和平移向量tp2   =  H21 * p1   p2 = K( RP + t)  = KTP = H21 * KP  T =  K 逆 * H21*K【8】 基础矩阵 F求解*  * p2------> p1*                          f1   f2    f3      u1*   (u2 v2 1)    *   f4   f5    f6  *  v1    = 0  应该=0 不等于零的就是误差*                  f7   f8    f9       1*     a1 = f1*u2 + f4*v2 + f7;b1 = f2*u2 + f5*v2 + f8;c1 =  f3*u2 + f6*v2 + f9;a1*u2+ b1*v2+ c1= 0一个点对 得到一个约束方程f1*u1*u2 + f2*v1*u2  + f3*u2 + f4*u1*v2  + f5*v1*v2 + f6*v2 +  f7*u1 + f8*v1 + f9 =0[  u1*u2   v1*u2   u2   u1*v2    v1*v2    v2  u1  v1 1 ] * [f1 f2 f3 f4 f5 f6 f7 f8 f9]转置  = 08个点对 得到八个约束A *f = 0 求 f   奇异值分解得到F 基础矩阵 且其秩为2 需要再奇异值分解 后 取对角矩阵 秩为2 后在合成F* 【9】 基础矩阵 F 求变换误差*  * p2 ------> p1 *                          f11   f12    f13      u1*   (u2 v2 1)    *   f21   f22    f23  *  v1    = 0  应该=0 不等于零的就是误差*                  f31   f32    f33      1*   a1 = f11*u2+f21*v2+f31;b1 = f12*u2+f22*v2+f32;c1 = f13*u2+f23*v2+f33;num1 = a1*u1 + b1*v1+ c1;// 应该等0num1*num1/(a1*a1+b1*b1);// 误差
* 【10】  从基本矩阵恢复 旋转矩阵R 和 平移向量t计算 本质矩阵 E  =  K转置 * F  * K从本质矩阵恢复 旋转矩阵R 和 平移向量t恢复四种假设 并验证【11】2D-2D点三角化 得到对应的 三维点坐标平面二维点摄影矩阵到三维点  P1 = K × [I 0]     P2 = K * [R  t]kp1 = P1 * p3dC1       p3dC1  特征点匹配对 对应的 世界3维点kp2 = P2 * p3dC1  kp1 叉乘  P1 * p3dC1 =0kp2 叉乘  P2 * p3dC1 =0  p = ( x,y,1)其叉乘矩阵为//  叉乘矩阵 = [0  -1  y;//                      1   0  -x; //                      -y  x  0 ]  一个方程得到两个约束对于第一行 0  -1  y; 会与P的三行分别相乘 得到四个值 与齐次3d点坐标相乘得到 0有 (y * P.row(2) - P.row(1) ) * D =0(-x *P.row(2) + P.row(0) ) * D =0 ===> (x *P.row(2) - P.row(0) ) * D =0两个方程得到 4个约束A × D = 0对A进行奇异值分解 求解线性方程 得到 D  (D是3维齐次坐标,需要除以第四个尺度因子 归一化)2也可转化到 相机归一化平面下的点  x1  x2p1 = k × [R1 t1] × D       k逆 × p1 =  [R1 t1] × D     x1 = T1 × D    x1叉乘x1 =  x1叉乘T1 × D = 0p2 = k × [ R2 t2]  × D     k逆 × p2 =  [R2 t2] × D     x2 = T2 × D    x2叉乘x2 =  x2叉乘T2 × D = 0
*/#include "Initializer.h"#include "Thirdparty/DBoW2/DUtils/Random.h"#include "Optimizer.h"
#include "ORBmatcher.h"#include<thread>namespace ORB_SLAM2
{/*** @brief 类构造函数  给定参考帧构造Initializer  单目相机初始化参考帧* * 用reference frame来初始化,这个reference frame就是SLAM正式开始的第一帧* @param ReferenceFrame  参考帧* @param sigma                    测量误差* @param iterations              RANSAC迭代次数*/Initializer::Initializer(const Frame &ReferenceFrame, float sigma, int iterations){mK = ReferenceFrame.mK.clone();// 相机内参数mvKeys1 = ReferenceFrame.mvKeysUn;// 畸变校正后的 关键 点mSigma = sigma;// 标准差mSigma2 = sigma*sigma;// 方差mMaxIterations = iterations;// 随机采样序列 最大迭代次数}/*** @brief 类初始化函数 * 并行地计算基础矩阵和单应性矩阵,选取其中一个模型,恢复出最开始两帧之间的相对姿态以及点云* @param CurrentFrame   当前帧       和 第一帧 参考帧 匹配 三角变换得到 3D点* @param vMatches12       当前帧 特征点的匹配信息* @param R21                     旋转矩阵 * @param t21                     平移矩阵  * @param vP3D                  恢复出的3D点* @param vbTriangulated 符合三角变换 的 3D点*/bool Initializer::Initialize(const Frame &CurrentFrame, const vector<int> &vMatches12, cv::Mat &R21, cv::Mat &t21,vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated){// Fill structures with current keypoints and matches with reference frame// Reference Frame: 1,  Current Frame: 2
// Frame2  当前帧 畸变校正后的 关键 点mvKeys2 = CurrentFrame.mvKeysUn;// 当前帧(2) 关键点mvMatches12.clear();// 当前帧(2)  关键点 的匹配信息mvMatches12.reserve(mvKeys2.size());// mvbMatched1记录每个特征点是否有匹配的特征点,// 这个变量后面没有用到,后面只关心匹配上的特征点    mvbMatched1.resize(mvKeys1.size());// 匹配参考帧(1)关键点的匹配信息// 步骤1: 组织特征点对for(size_t i=0, iend=vMatches12.size();i<iend; i++){if(vMatches12[i]>=0)// 帧2特征点 有匹配{mvMatches12.push_back(make_pair(i,vMatches12[i]));mvbMatched1[i]=true;}elsemvbMatched1[i]=false;// 未匹配到}// 匹配上的特征点的个数const int N = mvMatches12.size();// 有效的 匹配点对 个数// 新建一个容器vAllIndices,生成0到N-1的数作为特征点的索引vector<size_t> vAllIndices;vAllIndices.reserve(N);vector<size_t> vAvailableIndices;for(int i=0; i<N; i++){vAllIndices.push_back(i);}// Generate sets of 8 points for each RANSAC iteration
// 步骤2: 在所有匹配特征点对中随机选择8对匹配特征点为一组,共选择mMaxIterations组// 用于FindHomography和FindFundamental求解// mMaxIterations:200     // 随机采样序列 最大迭代次数 随机序列 8点法 mvSets = vector< vector<size_t> >(mMaxIterations,vector<size_t>(8,0));DUtils::Random::SeedRandOnce(0);//随机数for(int it=0; it<mMaxIterations; it++){//随机参数 候选 点对vAvailableIndices = vAllIndices;//候选匹配点对 // Select a minimum setfor(size_t j=0; j<8; j++){ // 产生0到N-1的随机数int randi = DUtils::Random::RandomInt(0,vAvailableIndices.size()-1);// 随机数// idx 表示哪一个索引对应的特征点被选中int idx = vAvailableIndices[randi];// 对应的随机数mvSets[it][j] = idx;//候选 匹配点对// randi对应的索引已经被选过了,从容器中删除// randi对应的索引用最后一个元素替换,并删掉最后一个元素vAvailableIndices[randi] = vAvailableIndices.back();vAvailableIndices.pop_back();}}// 步骤3:调用多线程分别用于计算fundamental matrix和homography// Launch threads to compute in parallel a fundamental matrix and a homography// 启动两个线程 分别计算 基本矩阵 F 和 单应矩阵vector<bool> vbMatchesInliersH, vbMatchesInliersF;//内点标志  匹配点对是否在 计算出的 变换矩阵的 有效映射上float SH, SF;// 最优变换矩阵 对应的 得分cv::Mat H, F;//随机采样中计算得到 的 最优单应矩阵 H  和 基本矩阵 F// 计算 单应矩阵 homograpy 并打分thread threadH(&Initializer::FindHomography,this,ref(vbMatchesInliersH), ref(SH), ref(H));// 计算 基础矩阵 fundamental matrix并打分thread threadF(&Initializer::FindFundamental,this,ref(vbMatchesInliersF), ref(SF), ref(F));// Wait until both threads have finished// 等待两个线程结束threadH.join();threadF.join();
// 步骤4:计算得分比例,选取某个模型      //  从两个模型 H F 得分为 Sh   Sf 中选着一个 最优秀的 模型 的方法为// Compute ratio of scoresfloat RH = SH/(SH+SF);// 计算 选着标志// 步骤5:从单应矩阵H 或 基础矩阵F中恢复R,t// Try to reconstruct from homography or fundamental depending on the ratio (0.40-0.45)if(RH>0.40)// 更偏向于 平面  使用  单应矩阵恢复return ReconstructH(vbMatchesInliersH,H,mK,R21,t21,vP3D,vbTriangulated,1.0,50);else //if(pF_HF>0.6) // 偏向于非平面  使用 基础矩阵 恢复return ReconstructF(vbMatchesInliersF,F,mK,R21,t21,vP3D,vbTriangulated,1.0,50);return false;}/*** @brief 计算单应矩阵   随机采样序列 8点  采用归一化的直接线性变换(normalized DLT)* 假设场景为平面情况下通过前两帧求取Homography矩阵(current frame 2 到 reference frame 1)* 在最大迭代次数内 调用 ComputeH21 计算  使用 CheckHomography 计算单应 得分* 并得到该模型的评分* 在最大迭代次数内 保留 最高得分的 单应矩阵* @param vbMatchesInliers     返回的 符合 变换的 匹配点 内点 标志* @param score                         变换得分* @param H21                           单应矩阵*/void Initializer::FindHomography(vector<bool> &vbMatchesInliers, float &score, cv::Mat &H21){// Number of putative matchesconst int N = mvMatches12.size();// 2中匹配的1中的点对 匹配点对总数// 步骤1: // 将mvKeys1和mvKey2归一化到均值为0,一阶绝对矩为1,归一化矩阵分别为T1、T2     //2D-2D点对 求变换矩阵前先进行标准化  去均值点坐标 * 绝对矩倒数//标准化矩阵  * 点坐标    =   标准化后的的坐标// 点坐标    =    标准化矩阵 逆矩阵 * 标准化后的的坐标// Normalize coordinatesvector<cv::Point2f> vPn1, vPn2;// 2d-2d点对cv::Mat T1, T2;// 标准化矩阵Normalize(mvKeys1,vPn1, T1);// 标准化点坐标  去均值点坐标 * 绝对矩倒数Normalize(mvKeys2,vPn2, T2);// cv::Mat T2inv = T2.inv();// 标准化矩阵 逆矩阵// Best Results variables// 最终最佳的MatchesInliers与得分score = 0.0;vbMatchesInliers = vector<bool>(N,false);// 内点 标志// Iteration variablesvector<cv::Point2f> vPn1i(8);// 随机 采样 8点对 vector<cv::Point2f> vPn2i(8);cv::Mat H21i, H12i;// 原点对 的 单应矩阵 //  H21i 原始点    p1 ----------------> p2 的单应vector<bool> vbCurrentInliers(N,false);//当前随机点里的 内点float currentScore;// 步骤2:随机采样序列迭代求解// Perform all RANSAC iterations and save the solution with highest scorefor(int it=0; it<mMaxIterations; it++)//在最大迭代次数内{// Select a minimum set
//步骤3:随机8对点对for(size_t j=0; j<8; j++){int idx = mvSets[it][j];//随机数集合 总匹配点数范围内vPn1i[j] = vPn1[mvMatches12[idx].first];vPn2i[j] = vPn2[mvMatches12[idx].second];}                                    //       Hn                     T2逆*Hn*T1
// 步骤4:计算 单应矩阵        T1*p1  ----> T2*p2     p1 ----------------> p2cv::Mat Hn = ComputeH21(vPn1i,vPn2i);//  计算标准化后的点对 的 单应矩阵H21i = T2inv*Hn*T1;// 原始点    p1 ----------------> p2 的单应H12i = H21i.inv();// 原始点    p2 ----------------> p1 的单应// 计算单应 转换矩阵得分/**  变换矩阵 评分 方法* SM=∑i( ρM( d2cr(xic,xir,M)   +   ρM( d2rc(xic,xir,M ) )*  d2cr 为 2D-2D 点对  通过哦转换矩阵的 对成转换误差* * ρM 函数为 ρM(d^2)  = 0                  当  d^2 > 阈值(单应矩阵时 为 5.99  基础矩阵时为 3.84)*                                       最高分 - d^2    当  d^2 < 阈值*                                                                               最高分 均为 5.99 */
// 步骤5:计算单应H的得分  有由 对应的匹配点对  的 对称的转换误差 求得    currentScore = CheckHomography(H21i, H12i, vbCurrentInliers, mSigma);
// 步骤6:保留最高得分 对应的 单应if(currentScore > score)//此次迭代 计算的单应H的得分较高{H21 = H21i.clone();//保留较高得分的单应vbMatchesInliers = vbCurrentInliers;//对应的匹配点对   score = currentScore;// 最高的得分}}}// 计算基础矩阵   随机采样序列 8点  采用归一化的直接线性变换(normalized DLT)
// 在最大迭代次数内 调用 ComputeH21 计算  使用 CheckHomography 计算单应 得分
// 在最大迭代次数内 保留 最高得分的 单应矩阵
/*** @brief 计算基础矩阵** 假设场景为非平面情况下通过前两帧求取Fundamental矩阵(current frame 2 到 reference frame 1),并得到该模型的评分*/void Initializer::FindFundamental(vector<bool> &vbMatchesInliers, float &score, cv::Mat &F21){// Number of putative matches// 总匹配点数const int N = vbMatchesInliers.size();
/**【1】2D-2D点对 求变换矩阵前先进行标准化  去均值点坐标 * 绝对矩倒数* 标准化矩阵  * 点坐标    =   标准化后的的坐标* 点坐标    =    标准化矩阵 逆矩阵 * 标准化后的的坐标*/// Normalize coordinatesvector<cv::Point2f> vPn1, vPn2;//  标准化后的的坐标cv::Mat T1, T2;Normalize(mvKeys1,vPn1, T1);// 标准化 去均值点坐标 * 绝对矩倒数Normalize(mvKeys2,vPn2, T2);cv::Mat T2t = T2.t();// 标准化矩阵 逆矩阵// Best Results variablesscore = 0.0;vbMatchesInliers = vector<bool>(N,false);// 最优 基本矩阵变换  对应 的点对的标记  1是 内点  0 是野点// Iteration variables// 随机8对 点对vector<cv::Point2f> vPn1i(8);vector<cv::Point2f> vPn2i(8);cv::Mat F21i;vector<bool> vbCurrentInliers(N,false);//每次迭代 求解的 点对的标记  1是 内点  0 是野点float currentScore;// 【2】随机采样序列迭代求解// Perform all RANSAC iterations and save the solution with highest scorefor(int it=0; it<mMaxIterations; it++){// Select a minimum set//【3】随机8对点对      for(int j=0; j<8; j++){int idx = mvSets[it][j];vPn1i[j] = vPn1[mvMatches12[idx].first];vPn2i[j] = vPn2[mvMatches12[idx].second];}//       Fn                     T2逆*Fn*T1// 【4】计算 基础矩阵        T1*p1  ----> T2*p2     p1 ----------------> p2cv::Mat Fn = ComputeF21(vPn1i,vPn2i);F21i = T2t*Fn*T1;// 计算基础矩阵 F转换矩阵得分/**  变换矩阵 评分 方法* SM=∑i( ρM( d2cr(xic,xir,M)   +   ρM( d2rc(xic,xir,M ) )*  d2cr 为 2D-2D 点对  通过哦转换矩阵的 对成转换误差* * ρM 函数为 ρM(d^2)  = 0                  当  d^2 > 阈值(单应矩阵时 为 5.99  基础矩阵时为 3.84)*                                       最高分 - d^2    当  d^2 < 阈值*                                                                               最高分 均为 5.99 */// 【5】计算基础矩阵 F的得分  有由 对应的匹配点对  的 对称的转换误差 求得    currentScore = CheckFundamental(F21i, vbCurrentInliers, mSigma);// 【6】保留最高得分 对应的 基础矩阵 Fif(currentScore>score){F21 = F21i.clone();// 最优的 基础矩阵 FvbMatchesInliers = vbCurrentInliers;//保持最优的 每次迭代 求解的 点对的标记  1是 内点  0 是野点score = currentScore;//当前得分}}}// 计算单应矩阵  8对点对 每个点提供两个约束   A × h = 0 求h 奇异值分解 求 h
// // 通过svd进行最小二乘求解
// 参考   http://www.fengbing.net/
// |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'A最小特征值对应的特征向量即为解
/*** @brief 从特征点匹配求homography(normalized DLT)* * @param  vP1 归一化后的点, in reference frame* @param  vP2 归一化后的点, in current frame* @return     单应矩阵* @see        Multiple View Geometry in Computer Vision - Algorithm 4.2 p109*/cv::Mat Initializer::ComputeH21(const vector<cv::Point2f> &vP1, const vector<cv::Point2f> &vP2){const int N = vP1.size();// 8 点对cv::Mat A(2*N,9,CV_32F);// 每个点 可以提供两个约束  单应为 3*3 9个 元素
/**  1点 变成 2点   p2   =  H21 * p1u2        h1  h2  h3        u1v2  =    h4  h5  h6    *  v11          h7  h8  h9       1   或是使用叉乘 得到0    * x = H y ,则对向量 x和Hy 进行叉乘为0,即:* | 0 -1  v2|    |h1 h2 h3|    |u1|     |0|* | 1  0 -u2| * |h4 h5 h6| * |v1| =  |0|* |-v2  u2 0|   |h7 h8 h9|      |1 |     |0|u2 = (h1*u1 + h2*v1 + h3) /( h7*u1 + h8*v1 + h9)v2 = (h4*u1 + h5*v1 + h6) /( h7*u1 + h8*v1 + h9)-((h4*u1 + h5*v1 + h6) - ( h7*u1*v2 + h8*v1*v2 + h9*v2))=0  式子为0  左侧加 - 号不变h1*u1 + h2*v1 + h3 - ( h7*u1*u2 + h8*v1*u2 + h9*u2)=00    0   0  -u1  -v1  -1   u1*v2   v1*v2    v2u1 v1  1    0    0    0   -u1*u2  - v1*u2  -u2    ×(h1 h2 h3 h4 h5 h6 h7 h8 h9)转置  = 08对点  约束 A A × h = 0 求h   奇异值分解 A 得到 单元矩阵 H*/        for(int i=0; i<N; i++)//8对点{const float u1 = vP1[i].x;const float v1 = vP1[i].y;const float u2 = vP2[i].x;const float v2 = vP2[i].y;// 每个点对 两个而约束方程A.at<float>(2*i,0) = 0.0;A.at<float>(2*i,1) = 0.0;A.at<float>(2*i,2) = 0.0;A.at<float>(2*i,3) = -u1;A.at<float>(2*i,4) = -v1;A.at<float>(2*i,5) = -1;A.at<float>(2*i,6) = v2*u1;A.at<float>(2*i,7) = v2*v1;A.at<float>(2*i,8) = v2;A.at<float>(2*i+1,0) = u1;A.at<float>(2*i+1,1) = v1;A.at<float>(2*i+1,2) = 1;A.at<float>(2*i+1,3) = 0.0;A.at<float>(2*i+1,4) = 0.0;A.at<float>(2*i+1,5) = 0.0;A.at<float>(2*i+1,6) = -u2*u1;A.at<float>(2*i+1,7) = -u2*v1;A.at<float>(2*i+1,8) = -u2;}cv::Mat u,w,vt;
// A × h = 0 求h// 在matlab中,[U,S,V]=svd(A),其中U和V代表二个相互正交矩阵,而S代表一对角矩阵。 //和QR分解法相同者, 原矩阵A不必为正方矩阵。//使用SVD分解法的用途是解最小平方误差法和数据压缩。// cv::SVDecomp(A,S,U,VT,SVD::FULL_UV);  //后面的FULL_UV表示把U和VT补充称单位正交方阵;cv::SVDecomp(A,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);// 奇异值分解return vt.row(8).reshape(0, 3);// v的最后一列}// 通过svd进行最小二乘求解// 8 对点 每个点 提供 一个约束
//8个点对 得到八个约束
//A *f = 0 求 f   奇异值分解 得到 f
/***      构建基础矩阵的约束方程,给定一对点对应m=(u1,v1,1)T, m'=(u2,v2,1)T*        满足基础矩阵F   m'T F m=0,令F=(f_ij),则约束方程可以化简为:*         u2u1 f_11 + u2v1 f_12 + u2 f_13+v2u1f_21+v2v1f_22+v2f_23+u1f_31+v1f_32+f_33=0*         令f = (f_11,f_12,f_13,f_21,f_22,f_23,f_31,f_32,f_33)*       则(u2u1,u2v1,u2,v2u1,v2v1,v2,u1,v1,1)f=0;*          这样,给定N个对应点就可以得到线性方程组Af=0*       A就是一个N*9的矩阵,由于基础矩阵是非零的,所以f是一个非零向量,即*       线性方程组有非零解,另外基础矩阵的秩为2,重要的约束条件*/// 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最小特征值对应的特征向量即为解
/*** @brief 从特征点匹配求fundamental matrix(normalized 8点法)* @param  vP1 归一化后的点, in reference frame* @param  vP2 归一化后的点, in current frame* @return     基础矩阵* @see          Multiple View Geometry in Computer Vision - Algorithm 11.1 p282 (中文版 p191)*/cv::Mat Initializer::ComputeF21(const vector<cv::Point2f> &vP1,const vector<cv::Point2f> &vP2){const int N = vP1.size();cv::Mat A(N,9,CV_32F);
/**  * p2------> p1*                           f1   f2    f3      u1*   (u2 v2 1)    *   f4   f5    f6  *  v1    = 0  应该=0 不等于零的就是误差*                f7   f8    f9      1*     a1 = f1*u2 + f4*v2 + f7;b1 = f2*u2 + f5*v2 + f8;c1 =  f3*u2 + f6*v2 + f9;a1*u1+ b1*v1+ c1= 0一个点对 得到一个约束方程f1*u1*u2 + f2*v1*u2  + f3*u2 + f4*u1*v2  + f5*v1*v2 + f6*v2 +  f7*u1 + f8*v1 + f9 =0[  u1*u2   v1*u2   u2   u1*v2    v1*v2    v2  u1  v1 1 ] * [f1 f2 f3 f4 f5 f6 f7 f8 f9]转置  = 08个点对 得到八个约束A *f = 0 求 f   奇异值分解得到F 基础矩阵 且其秩为2 需要再奇异值分解 后 取对角矩阵 秩为2 在合成F*/for(int i=0; i<N; i++){const float u1 = vP1[i].x;const float v1 = vP1[i].y;const float u2 = vP2[i].x;const float v2 = vP2[i].y;A.at<float>(i,0) = u2*u1;A.at<float>(i,1) = u2*v1;A.at<float>(i,2) = u2;A.at<float>(i,3) = v2*u1;A.at<float>(i,4) = v2*v1;A.at<float>(i,5) = v2;A.at<float>(i,6) = u1;A.at<float>(i,7) = v1;A.at<float>(i,8) = 1;}cv::Mat u,w,vt;cv::SVDecomp(A,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);cv::Mat Fpre = vt.row(8).reshape(0, 3);// F 基础矩阵的秩为2 需要在分解 后 取对角矩阵 秩为2 在合成Fcv::SVDecomp(Fpre,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);w.at<float>(2)=0;//  基础矩阵的秩为2,重要的约束条件return  u * cv::Mat::diag(w)  * vt;// 在合成F}// 计算单应矩阵 得分
/** 【3】变换矩阵 评分 方法*  SM=∑i( ρM( d2cr(xic,xir,M)   +   ρM( d2rc(xic,xir,M ) )*  d2cr 为 2D-2D 点对  通过哦转换矩阵的 对成转换误差* *  ρM 函数为 ρM(d^2)  = 0                  当  d^2 > 阈值(单应矩阵时 为 5.991  基础矩阵时为 3.84)*                                       阈值 - d^2    当  d^2 < 阈值* *//*** @brief 对给定的homography matrix打分* * @see* - Author's paper - IV. AUTOMATIC MAP INITIALIZATION (2)* - Multiple View Geometry in Computer Vision - symmetric transfer errors: 4.2.2 Geometric distance* - Multiple View Geometry in Computer Vision - model selection 4.7.1 RANSAC*/float Initializer::CheckHomography(const cv::Mat &H21, const cv::Mat &H12, vector<bool> &vbMatchesInliers, float sigma){   const int N = mvMatches12.size();// 总匹配点对数量// |h11 h12 h13|// |h21 h22 h23|// |h31 h32 h33|const float h11 = H21.at<float>(0,0);  //  p1  ----> p2const float h12 = H21.at<float>(0,1);const float h13 = H21.at<float>(0,2);const float h21 = H21.at<float>(1,0);const float h22 = H21.at<float>(1,1);const float h23 = H21.at<float>(1,2);const float h31 = H21.at<float>(2,0);const float h32 = H21.at<float>(2,1);const float h33 = H21.at<float>(2,2);// |h11inv h12inv h13inv|// |h21inv h22inv h23inv|// |h31inv h32inv h33inv|const float h11inv = H12.at<float>(0,0);const float h12inv = H12.at<float>(0,1);const float h13inv = H12.at<float>(0,2);const float h21inv = H12.at<float>(1,0);const float h22inv = H12.at<float>(1,1);const float h23inv = H12.at<float>(1,2);const float h31inv = H12.at<float>(2,0);const float h32inv = H12.at<float>(2,1);const float h33inv = H12.at<float>(2,2);vbMatchesInliers.resize(N);// 匹配点对是否在 变换矩阵对于的 变换上  是否是内点float score = 0;// 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)const float th = 5.991;// 单应变换误差 阈值//信息矩阵,方差平方的倒数const float invSigmaSquare = 1.0/(sigma*sigma);//方差 倒数// N对特征匹配点for(int i=0; i<N; i++)//计算单应矩阵 变换 每个点对时产生 的 对称的转换误差{bool bIn = true;// 关键点坐标const cv::KeyPoint &kp1 = mvKeys1[mvMatches12[i].first];const cv::KeyPoint &kp2 = mvKeys2[mvMatches12[i].second];
/** * 1点 变成 2点u2        h11  h12  h13        u1v2  =    h21  h22  h23    *  v11          h31  h32  h33         1   第三行 * 2 点 变成 1点u1‘        h11inv   h12inv   h13inv         u2v1’  =    h21inv   h22inv   h23inv     *  v21          h31inv   h32inv   h33inv          1    第三行 h31inv*u2+h32inv*v2+h33inv 前两行 同除以 第三行 消去非零因子p2 由单应转换到 p1u1‘ = (h11inv*u2+h12inv*v2+h13inv)* 第三行倒数v1’ = (h21inv*u2+h22inv*v2+h23inv)*第三行倒数然后计算 和 真实 p1点 坐标的差值(u1-u2in1)*(u1-u2in1) + (v1-v2in1)*(v1-v2in1)   横纵坐标差值平方和*/// 步骤1: p2 由单应转换到 p1 距离误差  以及得分const float u1 = kp1.pt.x;const float v1 = kp1.pt.y;const float u2 = kp2.pt.x;const float v2 = kp2.pt.y;// Reprojection error in first image// x2in1 = H12*x2// 将图像2中的特征点单应到图像1中// |u1|    |h11inv h12inv h13inv||u2|// |v1| = |h21inv h22inv h23inv||v2|// |1 |     |h31inv h32inv h33inv||1 |const float w2in1inv = 1.0/(h31inv*u2+h32inv*v2+h33inv);//第三行倒数const float u2in1 = (h11inv*u2+h12inv*v2+h13inv)*w2in1inv;// p2 由单应转换到 p1‘const float v2in1 = (h21inv*u2+h22inv*v2+h23inv)*w2in1inv;const float squareDist1 = (u1-u2in1)*(u1-u2in1) + (v1-v2in1)*(v1-v2in1);// 横纵坐标差值平方和const float chiSquare1 = squareDist1*invSigmaSquare;// 根据方差归一化误差if(chiSquare1>th)//距离大于阈值  改点 变换的效果差bIn = false;elsescore += th - chiSquare1;// 阈值 - 距离差值 得到 得分,差值越小  得分越高// 步骤2:p1由单应转换到 p2 距离误差  以及得分// Reprojection error in second image// x1in2 = H21*x1   p1点 变成p2点 误差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;elsescore += th - chiSquare2;if(bIn)vbMatchesInliers[i]=true;// 是内点  误差较小elsevbMatchesInliers[i]=false;// 是野点 误差较大}return score;}// 计算 基础矩阵 得分
// 和卡方分布的对应值比较,由此判定该点是否为内点。累计内点的总得分
// p2转置 * F * p1 =  0
/** p2 ------> p1 *                           f11   f12    f13      u1*   (u2 v2 1)    *   f21   f22    f23  *  v1    = 0应该=0 不等于零的就是误差*                 f31   f32    f33      1*  a1 = f11*u2+f21*v2+f31;b1 = f12*u2+f22*v2+f32;c1 = f13*u2+f23*v2+f33;num1 = a1*u1 + b1*v1+ c1;// 应该等0num1*num1/(a1*a1+b1*b1);// 误差*//*** @brief 对给定的fundamental matrix打分* p2 转置 * F21 * p1 = 0 * F21 * p1为 帧1 关键点 p1在 帧2 上的极线 l1* * *  p2 应该在这条极限附近 求p2 到极线 l的距离 可以作为误差*      极线l:ax + by + c = 0*      (u,v)到l的距离为:d = |au+bv+c| / sqrt(a^2+b^2) *      d^2 = |au+bv+c|^2/(a^2+b^2)* * p2 转置 * F21 为帧2 关键点 p2在 帧1 上的极线 l2* * * @see* - Author's paper - IV. AUTOMATIC MAP INITIALIZATION (2)* - Multiple View Geometry in Computer Vision - symmetric transfer errors: 4.2.2 Geometric distance* - Multiple View Geometry in Computer Vision - model selection 4.7.1 RANSAC*/float Initializer::CheckFundamental(const cv::Mat &F21, vector<bool> &vbMatchesInliers, float sigma){const int N = mvMatches12.size();const float f11 = F21.at<float>(0,0);const float f12 = F21.at<float>(0,1);const float f13 = F21.at<float>(0,2);const float f21 = F21.at<float>(1,0);const float f22 = F21.at<float>(1,1);const float f23 = F21.at<float>(1,2);const float f31 = F21.at<float>(2,0);const float f32 = F21.at<float>(2,1);const float f33 = F21.at<float>(2,2);vbMatchesInliers.resize(N);float score = 0;// 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)const float th = 3.841;const float thScore = 5.991;//信息矩阵,方差平方的倒数const float invSigmaSquare = 1.0/(sigma*sigma);for(int i=0; i<N; i++){bool bIn = true;const cv::KeyPoint &kp1 = mvKeys1[mvMatches12[i].first];const cv::KeyPoint &kp2 = mvKeys2[mvMatches12[i].second];const float u1 = kp1.pt.x;const float v1 = kp1.pt.y;const float u2 = kp2.pt.x;const float v2 = kp2.pt.y;
//  p1 ------> p2 误差 得分-------------------------------// Reprojection error in second image// l2=F21 x1=(a2,b2,c2)// F21x1可以算出x1在图像中x2对应的线lconst float a2 = f11*u1+f12*v1+f13;const float b2 = f21*u1+f22*v1+f23;const float c2 = f31*u1+f32*v1+f33;// x2应该在l这条线上:x2点乘l = 0 // 计算x2特征点到 极线 的距离:// 极线l:ax + by + c = 0// (u,v)到l的距离为:d = |au+bv+c| / sqrt(a^2+b^2) // d^2 = |au+bv+c|^2/(a^2+b^2)const float num2 = a2*u2+b2*v2+c2;const float squareDist1 = num2*num2/(a2*a2+b2*b2);// 点到线的几何距离 的平方// 根据方差归一化误差const float chiSquare1 = squareDist1*invSigmaSquare;if(chiSquare1>th)bIn = false;elsescore += thScore - chiSquare1;//得分// Reprojection error in second image// l1 =x2转置 × F21=(a1,b1,c1)
//  p2 ------> p1 误差 得分-------------------------const float a1 = f11*u2+f21*v2+f31;const float b1 = f12*u2+f22*v2+f32;const float c1 = f13*u2+f23*v2+f33;const float num1 = a1*u1+b1*v1+c1;const float squareDist2 = num1*num1/(a1*a1+b1*b1);const float chiSquare2 = squareDist2*invSigmaSquare;if(chiSquare2>th)bIn = false;elsescore += thScore - chiSquare2;// 得分if(bIn)vbMatchesInliers[i]=true;//内点  误差较小elsevbMatchesInliers[i]=false;// 野点  误差较大}return score;}/*从基本矩阵恢复 旋转矩阵R 和 平移向量t计算 本质矩阵 E  =  K转置逆 * F  * K从本质矩阵恢复 旋转矩阵R 和 平移向量t恢复四种假设 并验证
理论参考 Result 9.19 in Multiple View Geometry in Computer Vision*/
//                                         |0 -1  0|
// E = U Sigma V'   let W = |1  0  0| 为RZ(90)  绕Z轴旋转 90度(x变成原来的y y变成原来的-x z轴没变)
//                                         |0  0  1|
// 得到4个解 E = [R|t]
// R1 = UWV' R2 = UW'V' t1 = U3 t2 = -U3/*** @brief 从基本矩阵 F 恢复R t* * 度量重构* 1. 由Fundamental矩阵结合相机内参K,得到Essential矩阵: \f$ E = k转置F k \f$* 2. SVD分解得到R t* 3. 进行cheirality check, 从四个解中找出最合适的解* * @see Multiple View Geometry in Computer Vision - Result 9.19 p259*/   bool Initializer::ReconstructF(vector<bool> &vbMatchesInliers, cv::Mat &F21, cv::Mat &K,cv::Mat &R21, cv::Mat &t21, vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated, float minParallax, int minTriangulated){int N=0;for(size_t i=0, iend = vbMatchesInliers.size() ; i<iend; i++)if(vbMatchesInliers[i])// 是内点N++;// 符合 基本矩阵 F的 内点数量// Compute Essential Matrix from Fundamental Matrix// 步骤1: 计算 本质矩阵 E  =  K转置 * F  * Kcv::Mat E21 = K.t()*F21*K;cv::Mat R1, R2, t;// Recover the 4 motion hypotheses 四种运动假设/*
// 步骤2:  从本质矩阵恢复 旋转矩阵R 和 平移向量t*  对 本质矩阵E 进行奇异值分解   得到可能的解* t = u * RZ(90) * u转置 * R= u * RZ(90) * V转置 * 组合情况有四种*/// 虽然这个函数对t有归一化,但并没有决定单目整个SLAM过程的尺度// 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变DecomposeE(E21,R1,R2,t);  cv::Mat t1=t;cv::Mat t2=-t;// 步骤3: 恢复四种假设 并验证 Reconstruct with the 4 hyphoteses and check// 这4个解中只有一个是合理的,可以使用可视化约束来选择,// 与单应性矩阵做sfm一样的方法,即将4种解都进行三角化,然后从中选择出最合适的解。vector<cv::Point3f> vP3D1, vP3D2, vP3D3, vP3D4;vector<bool> vbTriangulated1,vbTriangulated2,vbTriangulated3, vbTriangulated4;float parallax1,parallax2, parallax3, parallax4;int nGood1 = CheckRT(R1,t1,mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,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);int maxGood = max(nGood1,max(nGood2,max(nGood3,nGood4)));R21 = cv::Mat();t21 = cv::Mat();// minTriangulated为可以三角化恢复三维点的个数int nMinGood = max(static_cast<int>(0.9*N),minTriangulated);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++;// If there is not a clear winner or not enough triangulated points reject initializationif(maxGood<nMinGood || nsimilar>1){// 四个结果中如果没有明显的最优结果,则返回失败return false;// 初始化失败}// If best reconstruction has enough parallax initialize// 比较大的视差角  四种假设if(maxGood==nGood1){if(parallax1>minParallax){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;// 初始化成功}}return false;// 初始化失败}/*从单应矩阵恢复 旋转矩阵R 和 平移向量t理论参考 // Faugeras et al, Motion and structure from motion in a piecewise planar environment. International Journal of Pattern Recognition and Artificial Intelligence, 1988.https://hal.archives-ouvertes.fr/inria-00075698/documentp2   =  H21 * p1
p2 = K( RP + t)  = KTP = H21 * KP
T =  K 逆 * H21*K在求得单应性变化H后,本文使用FAUGERAS的论文[1]的方法,提取8种运动假设。
这个方法通过可视化约束来测试选择合理的解。但是如果在低视差的情况下,
点云会跑到相机的前面或后面,测试就会出现错误从而选择一个错误的解。
文中使用的是直接三角化 8种方案,检查两个相机前面具有较少的重投影误差情况下,
在视图低视差情况下是否大部分云点都可以看到。如果没有一个解很合适,就不执行初始化,
重新从第一步开始。这种方法在低视差和两个交叉的视图情况下,初始化程序更具鲁棒性。*/
// H矩阵分解常见有两种方法:Faugeras SVD-based decomposition 和 Zhang SVD-based decomposition
// 参考文献:Motion and structure from motion in a piecewise plannar environment
// 这篇参考文献和下面的代码使用了Faugeras SVD-based decomposition算法/*** @brief 从H恢复R t** @see* - Faugeras et al, Motion and structure from motion in a piecewise planar environment. International Journal of Pattern Recognition and Artificial Intelligence, 1988.* - Deeper understanding of the homography decomposition for vision-based control*/bool Initializer::ReconstructH(vector<bool> &vbMatchesInliers, cv::Mat &H21, cv::Mat &K,cv::Mat &R21, cv::Mat &t21, vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated, float minParallax, int minTriangulated){int N=0;for(size_t i=0, iend = vbMatchesInliers.size() ; i<iend; i++)if(vbMatchesInliers[i])N++;//匹配点对 内点// 8种运动假设  We recover 8 motion hypotheses using the method of Faugeras et al.// Motion and structure from motion in a piecewise planar environment.// International Journal of Pattern Recognition and Artificial Intelligence, 1988// 因为特征点是图像坐标系,所以将H矩阵由相机坐标系换算到图像坐标系cv::Mat invK = K.inv();cv::Mat A = invK*H21*K;cv::Mat U,w,Vt,V;cv::SVD::compute(A,w,U,Vt,cv::SVD::FULL_UV);V=Vt.t();float s = cv::determinant(U)*cv::determinant(Vt);float d1 = w.at<float>(0);float d2 = w.at<float>(1);float d3 = w.at<float>(2);// SVD分解的正常情况是特征值降序排列if(d1/d2<1.00001 || d2/d3<1.00001){return false;// 初始化失败}vector<cv::Mat> vR, vt, vn;vR.reserve(8);vt.reserve(8);vn.reserve(8);//n'=[x1 0 x3] 4 posibilities e1=e3=1, e1=1 e3=-1, e1=-1 e3=1, e1=e3=-1// 法向量n'= [x1 0 x3] 对应ppt的公式17float aux1 = sqrt((d1*d1-d2*d2)/(d1*d1-d3*d3));float aux3 = sqrt((d2*d2-d3*d3)/(d1*d1-d3*d3));float x1[] = {aux1,aux1,-aux1,-aux1};float x3[] = {aux3,-aux3,aux3,-aux3};//case d'=d2// 计算ppt中公式19float aux_stheta = sqrt((d1*d1-d2*d2)*(d2*d2-d3*d3))/((d1+d3)*d2);float ctheta = (d2*d2+d1*d3)/((d1+d3)*d2);float stheta[] = {aux_stheta, -aux_stheta, -aux_stheta, aux_stheta};// 计算旋转矩阵 R‘,计算ppt中公式18//          | ctheta         0   -aux_stheta|         | aux1|// Rp = |    0               1       0             |  tp = |  0     |//          | aux_stheta  0    ctheta       |         |-aux3|//          | ctheta          0    aux_stheta|          | aux1|// Rp = |    0                1       0              |  tp = |  0  |//          |-aux_stheta  0    ctheta         |          | aux3|//          | ctheta         0    aux_stheta|         |-aux1|// Rp = |    0               1       0             |  tp = |  0     |//          |-aux_stheta  0    ctheta       |         |-aux3|//          | ctheta         0   -aux_stheta|         |-aux1|// Rp = |    0               1       0             |  tp = |  0  |//          | aux_stheta  0    ctheta       |          | aux3|for(int i=0; i<4; i++){cv::Mat Rp=cv::Mat::eye(3,3,CV_32F);Rp.at<float>(0,0)=ctheta;Rp.at<float>(0,2)=-stheta[i];Rp.at<float>(2,0)=stheta[i];Rp.at<float>(2,2)=ctheta;cv::Mat R = s*U*Rp*Vt;vR.push_back(R);cv::Mat tp(3,1,CV_32F);tp.at<float>(0)=x1[i];tp.at<float>(1)=0;tp.at<float>(2)=-x3[i];tp*=d1-d3;// 这里虽然对t有归一化,并没有决定单目整个SLAM过程的尺度// 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变cv::Mat t = U*tp;vt.push_back(t/cv::norm(t));cv::Mat np(3,1,CV_32F);np.at<float>(0)=x1[i];np.at<float>(1)=0;np.at<float>(2)=x3[i];cv::Mat n = V*np;if(n.at<float>(2)<0)n=-n;vn.push_back(n);}//case d'=-d2// 计算ppt中公式22float aux_sphi = sqrt((d1*d1-d2*d2)*(d2*d2-d3*d3))/((d1-d3)*d2);float cphi = (d1*d3-d2*d2)/((d1-d3)*d2);float sphi[] = {aux_sphi, -aux_sphi, -aux_sphi, aux_sphi};// 计算旋转矩阵 R‘,计算ppt中公式21for(int i=0; i<4; i++){cv::Mat Rp=cv::Mat::eye(3,3,CV_32F);Rp.at<float>(0,0)=cphi;Rp.at<float>(0,2)=sphi[i];Rp.at<float>(1,1)=-1;Rp.at<float>(2,0)=sphi[i];Rp.at<float>(2,2)=-cphi;cv::Mat R = s*U*Rp*Vt;vR.push_back(R);cv::Mat tp(3,1,CV_32F);tp.at<float>(0)=x1[i];tp.at<float>(1)=0;tp.at<float>(2)=x3[i];tp*=d1+d3;cv::Mat t = U*tp;vt.push_back(t/cv::norm(t));cv::Mat np(3,1,CV_32F);np.at<float>(0)=x1[i];np.at<float>(1)=0;np.at<float>(2)=x3[i];cv::Mat n = V*np;if(n.at<float>(2)<0)n=-n;vn.push_back(n);}int bestGood = 0;int secondBestGood = 0;    int bestSolutionIdx = -1;float bestParallax = -1;vector<cv::Point3f> bestP3D;vector<bool> bestTriangulated;// Instead of applying the visibility constraints proposed in the Faugeras' paper (which could fail for points seen with low parallax)// We reconstruct all hypotheses and check in terms of triangulated points and parallax// d'=d2和d'=-d2分别对应8组(R t)for(size_t i=0; i<8; i++){float parallaxi;vector<cv::Point3f> vP3Di;vector<bool> vbTriangulatedi;int nGood = CheckRT(vR[i],vt[i],mvKeys1,mvKeys2,mvMatches12,vbMatchesInliers,K,vP3Di, 4.0*mSigma2, vbTriangulatedi, parallaxi);// 保留最优的和次优的if(nGood>bestGood){secondBestGood = bestGood;bestGood = nGood;bestSolutionIdx = i;bestParallax = parallaxi;bestP3D = vP3Di;bestTriangulated = vbTriangulatedi;}else if(nGood>secondBestGood){secondBestGood = nGood;}}if(secondBestGood<0.75*bestGood && bestParallax>=minParallax && bestGood>minTriangulated && bestGood>0.9*N){vR[bestSolutionIdx].copyTo(R21);vt[bestSolutionIdx].copyTo(t21);vP3D = bestP3D;vbTriangulated = bestTriangulated;return true;// 初始化成功}return false;// 初始化失败}/** 三角化得到3D点 *  *三角测量法 求解 两组单目相机  图像点深度* s1 * x1 = s2  * R * x2 + t* x1 x2 为两帧图像上 两点对 在归一化坐标平面上的坐标 k逆* p* s1  和 s2为两个特征点的深度 ,由于误差存在, s1 * x1 = s2  * R * x2 + t不精确相等* 常见的是求解最小二乘解,而不是零解*  s1 * x1叉乘x1 = s2 * x1叉乘* R * x2 + x1叉乘 t=0 可以求得x2* */
/*平面二维点摄影矩阵到三维点  P1 = K × [I 0]    P2 = K * [R  t]kp1 = P1 * p3dC1       p3dC1  特征点匹配对 对应的 世界3维点kp2 = P2 * p3dC1  kp1 叉乘  P1 * p3dC1 =0kp2 叉乘  P2 * p3dC1 =0  p = ( x,y,1)其叉乘矩阵为//  叉乘矩阵 = [0  -1  y;//                       1   0  -x; //                      -y   x  0 ]  一个方程得到两个约束对于第一行 0  -1  y; 会与P的三行分别相乘 得到四个值 与齐次3d点坐标相乘得到 0有 (y * P.row(2) - P.row(1) ) * D =0(-x *P.row(2) + P.row(0) ) * D =0 ===> (x *P.row(2) - P.row(0) ) * D =0两个方程得到 4个约束A × D = 0对A进行奇异值分解 求解线性方程 得到 D  (D是3维齐次坐标,需要除以第四个尺度因子 归一化)*/
// Trianularization: 已知匹配特征点对{x x'} 和 各自相机矩阵{P P'}, 估计三维点 X
// x' = P'X  x = PX
// 它们都属于 x = aPX模型
//                                             |X|
// |x|       |p1 p2   p3  p4   ||Y|          |x|      |--p0--||.|
// |y| = a |p5 p6   p7  p8   ||Z| ===>|y| = a|--p1--||X|
// |z|       |p9 p10 p11 p12||1|          |z|      |--p2--||.|
// 采用DLT的方法:x叉乘PX = 0
// |yp2 -  p1|        |0|
// |p0  -  xp2| X = |0|
// |xp1 - yp0|       |0|
// 两个点:
// |yp2   -  p1  |       |0|
// |p0    -  xp2 | X = |0| ===> AX = 0
// |y'p2' -  p1' |        |0|
// |p0'   - x'p2'|        |0|
// 变成程序中的形式:
// |xp2  - p0 |       |0|
// |yp2  - p1 | X = |0| ===> AX = 0
// |x'p2'- p0'|        |0|
// |y'p2'- p1'|        |0|
/*** @brief 给定投影矩阵P1,P2和图像上的点kp1,kp2,从而恢复3D坐标** @param kp1 特征点, in reference frame* @param kp2 特征点, in current frame* @param P1  投影矩阵P1* @param P2  投影矩阵P2* @param x3D 三维点* @see       Multiple View Geometry in Computer Vision - 12.2 Linear triangulation methods p312*/void Initializer::Triangulate(const cv::KeyPoint &kp1, const cv::KeyPoint &kp2, const cv::Mat &P1, const cv::Mat &P2, cv::Mat &x3D){// 在DecomposeE函数和ReconstructH函数中对t有归一化// 这里三角化过程中恢复的3D点深度取决于 t 的尺度,// 但是这里恢复的3D点并没有决定单目整个SLAM过程的尺度// 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变cv::Mat A(4,4,CV_32F);A.row(0) = kp1.pt.x*P1.row(2)-P1.row(0);A.row(1) = kp1.pt.y*P1.row(2)-P1.row(1);A.row(2) = kp2.pt.x*P2.row(2)-P2.row(0);A.row(3) = kp2.pt.y*P2.row(2)-P2.row(1);cv::Mat u,w,vt;cv::SVD::compute(A,w,u,vt,cv::SVD::MODIFY_A| cv::SVD::FULL_UV);x3D = vt.row(3).t();x3D = x3D.rowRange(0,3)/x3D.at<float>(3);//  转换成非齐次坐标  归一化}// 对  2D 点对进行标准化
/**【0】2D-2D点对 求变换矩阵前先进行标准化  去 均值后再除以绝对矩
* 单目初始化特征点 归一化 坐标均值为0  一阶绝对矩为1
* mean_x  =  sum( ui) / N   mean_y =  sum(vi)/N
* 绝对矩  mean_x_dev = sum(abs(ui - mean_x))/ N     mean_y_dev = sum(abs(vi - mean_y))/ N
*
*绝对矩倒数 sX = 1/mean_x_dev     sY = 1/mean_y_dev
*
* 标准化后的点坐标
* u = (ui - mean_x) × sX
* v =  (vi - mean_y) * sY
*
* 标准化矩阵   其逆矩阵×标准化点 得到原始坐标
*      用于 计算变换矩阵后  从原始坐标计算对称的转换误差 来计算变换矩阵得分
* T = sX   0    -mean_x * sX
*        0   sY   -mean_y * sY
*        0    0         1
*
* 标准化矩阵  * 点坐标    =   标准化后的的坐标
*         ui          ui × sX - mean_x * sX  = (ui - mean_x) × sX       u
*  T ×  vi    =    vi  × sY - mean_y * sY  = (vi - mean_y) × sY   =   v
*         1               1                                               1
*
* * 点坐标    =    标准化矩阵 逆矩阵 * 标准化后的的坐标
* */
/*** @brief 归一化特征点到同一尺度(作为normalize DLT的输入)** [x' y' 1]' = T * [x y 1]' \n* 归一化后x', y'的均值为0,sum(abs(x_i'-0))=1,sum(abs((y_i'-0))=1* * @param vKeys             特征点在图像上的坐标* @param vNormalizedPoints 特征点归一化后的坐标* @param T                 将特征点归一化的矩阵*/void Initializer::Normalize(const vector<cv::KeyPoint> &vKeys, vector<cv::Point2f> &vNormalizedPoints, cv::Mat &T){const int N = vKeys.size();// 点总数vNormalizedPoints.resize(N);//标准化后的点float meanX = 0;//横坐标均值float meanY = 0;//纵坐标均值for(int i=0; i<N; i++){meanX += vKeys[i].pt.x;// 横坐标之和meanY += vKeys[i].pt.y;//纵坐标之和}meanX = meanX/N;//横坐标均值meanY = meanY/N;//纵坐标均值float meanDevX = 0;//绝对矩float meanDevY = 0;//绝对矩// 将所有vKeys点减去中心坐标,使x坐标和y坐标均值分别为0for(int i=0; i<N; i++){vNormalizedPoints[i].x = vKeys[i].pt.x - meanX;// 去均值点坐标vNormalizedPoints[i].y = vKeys[i].pt.y - meanY;// meanDevX += fabs(vNormalizedPoints[i].x);// 总绝对矩meanDevY += fabs(vNormalizedPoints[i].y);}meanDevX = meanDevX/N;//均值绝对矩meanDevY = meanDevY/N;float sX = 1.0/meanDevX;float sY = 1.0/meanDevY;// 将x坐标和y坐标分别进行尺度缩放,使得x坐标和y坐标的一阶绝对矩分别为1for(int i=0; i<N; i++){// 标注化后的点坐标vNormalizedPoints[i].x = vNormalizedPoints[i].x * sX;// 去均值点坐标 * 绝对矩倒数vNormalizedPoints[i].y = vNormalizedPoints[i].y * sY;}// |sX  0  -meanx*sX|// |0   sY -meany*sY|// |0   0          1         |      // 标准化矩阵// 标准化矩阵  * 点坐标    =   标准化后的的坐标//  点坐标    =    标准化矩阵 逆矩阵 * 标准化后的的坐标T = cv::Mat::eye(3,3,CV_32F);T.at<float>(0,0) = sX;T.at<float>(1,1) = sY;T.at<float>(0,2) = -meanX*sX;T.at<float>(1,2) = -meanY*sY;}/** 检查求得的R t 是否符合* 接受 R,t ,一组成功的匹配。最后给出的结果是这组匹配中有多少匹配是* 能够在这组 R,t 下正确三角化的(即 Z都大于0),并且输出这些三角化之后的三维点。如果三角化生成的三维点 Z小于等于0,且三角化的“前方交会角”(余弦是 cosParallax)不会太小,
那么这个三维点三角化错误,舍弃。通过了 Z的检验,之后将这个三维点分别投影到两张影像上,
计算投影的像素误差,误差大于2倍中误差,舍弃。*/
/*** @brief 进行cheirality check,从而进一步找出F分解后最合适的解*/int Initializer::CheckRT(const cv::Mat &R, const cv::Mat &t, const vector<cv::KeyPoint> &vKeys1, const vector<cv::KeyPoint> &vKeys2,const vector<Match> &vMatches12, vector<bool> &vbMatchesInliers,const cv::Mat &K, vector<cv::Point3f> &vP3D, float th2, vector<bool> &vbGood, float ¶llax){// Calibration parameters// 校正参数const float fx = K.at<float>(0,0);const float fy = K.at<float>(1,1);const float cx = K.at<float>(0,2);const float cy = K.at<float>(1,2);vbGood = vector<bool>(vKeys1.size(),false);vP3D.resize(vKeys1.size());// 对应的三维点vector<float> vCosParallax;vCosParallax.reserve(vKeys1.size());// Camera 1 Projection Matrix K[I|0]
// 步骤1:得到一个相机的投影矩阵// 以第一个相机的光心作为世界坐标系        // 相机1  变换矩阵 在第一幅图像下 的变换矩阵  Pc1  =   Pw  =  T1 * Pw      T1 = [I|0]// Pp1  = K *  Pc1 = K * T1 * Pw  =   [K|0] *Pw  = P1 × Pwcv::Mat P1(3,4,CV_32F,cv::Scalar(0));K.copyTo(P1.rowRange(0,3).colRange(0,3));// 第一个相机的光心在世界坐标系下的坐标cv::Mat O1 = cv::Mat::zeros(3,1,CV_32F);// 相机1原点 000// 步骤2:得到第二个相机的投影矩阵// Camera 2 Projection Matrix K[R|t]// 相机2  变换矩阵  Pc2  =   Pw  =  T2 * Pw      T2 = [R|t]// Pp2  = K *  Pc2 = K * T2 * Pw  =  K* [R|t] *Pw  = P2 × Pw cv::Mat P2(3,4,CV_32F);R.copyTo(P2.rowRange(0,3).colRange(0,3));t.copyTo(P2.rowRange(0,3).col(3));P2 = K*P2;// 第二个相机的光心在世界坐标系下的坐标cv::Mat O2 = -R.t()*t;//相机2原点  R逆 * - t  R 为正交矩阵  逆 = 转置int nGood=0;for(size_t i=0, iend=vMatches12.size();i<iend;i++)// 每一个匹配点对{if(!vbMatchesInliers[i])// 离线点  非内点continue;// kp1和kp2是匹配特征点const cv::KeyPoint &kp1 = vKeys1[vMatches12[i].first];const cv::KeyPoint &kp2 = vKeys2[vMatches12[i].second];cv::Mat p3dC1;// 步骤3:利用三角法恢复三维点p3dC1// kp1 = P1 * p3dC1     kp2 = P2 * p3dC1   Triangulate(kp1,kp2,P1,P2,p3dC1);if(!isfinite(p3dC1.at<float>(0)) || !isfinite(p3dC1.at<float>(1)) || !isfinite(p3dC1.at<float>(2))){// 求出的3d点坐标 值有效vbGood[vMatches12[i].first]=false;continue;}// 步骤4:计算视差角余弦值// Check parallaxcv::Mat normal1 = p3dC1 - O1;float dist1 = cv::norm(normal1);cv::Mat normal2 = p3dC1 - O2;float dist2 = cv::norm(normal2);float cosParallax = normal1.dot(normal2)/(dist1*dist2);// 步骤5:判断3D点是否在两个摄像头前方// Check depth in front of first camera (only if enough parallax, as "infinite" points can easily go to negative depth)// 步骤5.1:3D点深度为负,在第一个摄像头后方,淘汰if(p3dC1.at<float>(2)<=0 && cosParallax<0.99998)continue;// 步骤5.2:3D点深度为负,在第二个摄像头后方,淘汰// Check depth in front of second camera (only if enough parallax, as "infinite" points can easily go to negative depth)cv::Mat p3dC2 = R*p3dC1+t;if(p3dC2.at<float>(2)<=0 && cosParallax<0.99998)continue;// 步骤6:计算重投影误差// Check reprojection error in first image// 计算3D点在第一个图像上的投影误差float im1x, im1y;float invZ1 = 1.0/p3dC1.at<float>(2);im1x = fx*p3dC1.at<float>(0)*invZ1+cx;im1y = fy*p3dC1.at<float>(1)*invZ1+cy;float squareError1 = (im1x-kp1.pt.x)*(im1x-kp1.pt.x)+(im1y-kp1.pt.y)*(im1y-kp1.pt.y);// 步骤6.1:重投影误差太大,跳过淘汰// 一般视差角比较小时重投影误差比较大if(squareError1>th2)continue;// 计算3D点在第二个图像上的投影误差// Check reprojection error in second imagefloat im2x, im2y;float invZ2 = 1.0/p3dC2.at<float>(2);im2x = fx*p3dC2.at<float>(0)*invZ2+cx;im2y = fy*p3dC2.at<float>(1)*invZ2+cy;float squareError2 = (im2x-kp2.pt.x)*(im2x-kp2.pt.x)+(im2y-kp2.pt.y)*(im2y-kp2.pt.y);// 步骤6.2:重投影误差太大,跳过淘汰// 一般视差角比较小时重投影误差比较大if(squareError2>th2)continue;// 步骤7:统计经过检验的3D点个数,记录3D点视差角vCosParallax.push_back(cosParallax);vP3D[vMatches12[i].first] = cv::Point3f(p3dC1.at<float>(0),p3dC1.at<float>(1),p3dC1.at<float>(2));//nGood++;if(cosParallax<0.99998){vbGood[vMatches12[i].first]=true;// WYW  20180130 修改nGood++;}}// 步骤8:得到3D点中较大的视差角if(nGood>0){sort(vCosParallax.begin(),vCosParallax.end());// 从小到大排序// trick! 排序后并没有取最大的视差角// 取一个较大的视差角size_t idx = min(50,int(vCosParallax.size()-1));parallax = acos(vCosParallax[idx])*180/CV_PI;}elseparallax=0;return nGood;}/** 从本质矩阵恢复 旋转矩阵R 和 平移向量t*  对 本质矩阵E 进行奇异值分解   得到可能的解* t = u * RZ(90) * u转置 * R= u * RZ(90) * V转置 * 组合情况有四种*//*** @brief 分解Essential矩阵* * F矩阵通过结合内参可以得到Essential矩阵,分解E矩阵将得到4组解 \n* 这4组解分别为[R1,t],[R1,-t],[R2,t],[R2,-t]* @param E  Essential Matrix* @param R1 Rotation Matrix 1* @param R2 Rotation Matrix 2* @param t  Translation* @see Multiple View Geometry in Computer Vision - Result 9.19 p259*/void Initializer::DecomposeE(const cv::Mat &E, cv::Mat &R1, cv::Mat &R2, cv::Mat &t){// 【1】对 本质矩阵E 进行奇异值分解  cv::Mat u,w,vt;cv::SVD::compute(E,w,u,vt);// 其中u和v代表二个相互正交矩阵,而w代表一对角矩阵// 对 t 有归一化,但是这个地方并没有决定单目整个SLAM过程的尺度// 因为CreateInitialMapMonocular函数对3D点深度会缩放,然后反过来对 t 有改变u.col(2).copyTo(t);t=t/cv::norm(t);
// 沿着Z轴旋转 90度得到的旋转矩阵(逆时针为正方向)
// z 轴还是 原来的z轴   y轴变成原来的 x 轴的负方向   x轴变成原来的y轴
// 所以 旋转矩阵  为 0  -1   0
//                 1   0   0
//                 0   0   1
// 沿着Z轴旋转- 90度
// z 轴还是 原来的z轴   y轴变成原来的 x 轴   x轴变成原来的y轴的负方向
// 所以 旋转矩阵  为 0   1   0  为上 旋转矩阵的转置矩阵
//               -1    0   0
//                 0   0   1    cv::Mat W(3,3,CV_32F,cv::Scalar(0));W.at<float>(0,1)=-1;W.at<float>(1,0)=1;W.at<float>(2,2)=1;R1 = u*W*vt;if(cv::determinant(R1)<0)R1=-R1;R2 = u*W.t()*vt;if(cv::determinant(R2)<0)R2=-R2;}} //namespace ORB_SLAM

单目初始化 单应矩阵 本质矩阵 恢复R t 三角变换求 3D点相关推荐

  1. ORB_SLAM2单目初始化策略

    基本流程   单目初始化程序存储在Initializer.cc中   需要注意,对于双目/RGB-D相机,初始化时,由于可以直接获得相机的深度信息,因此无需求H/F,直接作为关键帧插入就行.   使用 ...

  2. 【ORB-SLAM2源码梳理6】Track()函数的第一步:单目初始化MonocularInitialization()

    文章目录 前言 一.Track()函数 二.单目初始化MonocularInitialization() 1. 判断单目初始化器是否创建,若没有就创建. 2. 已创建初始化器,判断特征点数目 3. 在 ...

  3. ORB-SLAM3 细读单目初始化过程(下)

    本文原创,转载请说明地址:https://blog.csdn.net/shanpenghui/article/details/110003959 一.前言 ORBSLAM3单目视觉有很多知识点需要展开 ...

  4. ORB_SLAM2 源码解析 单目初始化器Initializer(三)

    目录 一.地图点初始化 二.重新记录特征点的匹配关系 1.构建旋转直方图 1.1.在半径窗口内搜索当前帧F2中所有的候选匹配特征点GetFeaturesInArea 1.2.表示一个图像像素相当于多少 ...

  5. ORB-SLAM3 细读单目初始化过程(终结篇)

    本文原创,转载请说明地址:https://blog.csdn.net/shanpenghui/article/details/110522368 一.前言 请阅读本文之前最好把ORB-SLAM3的单目 ...

  6. ORBSLAM源码理论分析2—单目初始化

    ORBSLAM源码理论分析2-单目初始化 1.构造初始化帧1 2.第一次初始化 3.构造初始化帧2 4.F1与F2特征匹配 5.初始化解算位姿 5.1.计算单应矩阵 5.2.计算基础矩阵 5.3.评估 ...

  7. 超详细解读ORB-SLAM3单目初始化(下篇)

    一 前言 本文承接ORB-SLAM3 细读单目初始化过程(上),ORBSLAM3单目视觉有很多知识点需要展开和深入,初始化过程是必然要经历的,而网上资料不够系统,因此本文主旨是从代码实现出发,把初始化 ...

  8. 重磅直播|ORB-SLAM3经典单目初始化模块原理及实现

    点击上方"计算机视觉工坊",选择"星标" 干货第一时间送达 大家好,本公众号现已开启线上视频公开课,主讲人通过B站直播间,对3D视觉领域相关知识点进行讲解,并在 ...

  9. ORB-SLAM3 细读单目初始化过程(上)

    学习ORB-SLAM3单目视觉SLAM中,发现有很多知识点需要展开和深入,同时又需要对系统有整体的认知,为了强化记忆,记录该系列笔记,为自己图方便,也希望对大家有所启发. TrackMonocular ...

最新文章

  1. 鸿蒙电视哔哩哔哩,[4K视频] 65寸智能电视只要3299元?荣耀智慧屏X1开箱
  2. MySQL 性能监控4大指标——第一部分
  3. oracle 10G 表空间迁移 索引需要重建
  4. js里面拼接代码和使用ModelAndView
  5. 类型配置命名空间 —— XML schema
  6. Hyper-V 2016 系列教程40 使用 PowerShell 实现虚拟机自动化和管理虚拟机
  7. 网站测试自动化系统—在测试代码中硬编码测试数据
  8. Bp神经网络+C++实现
  9. 安装与配置OCS服务器时可能会出现的问题
  10. 一个超好玩的音乐网站源码 类似小游戏
  11. java项目-图书馆管理系统源码
  12. DevOps知识地图
  13. 我是如何搭建一台家庭NAS的
  14. 谷歌图片的爬虫库(附加必应图片爬虫)--针对近期谷歌变了
  15. 『深度实战』天池小目标检测大赛·宫颈癌风险智能诊断推荐
  16. Matlab 网络通信(TCP IP)
  17. 【火炉炼AI】机器学习023-使用层次聚类算法构建模型
  18. python 爬取贝壳的一些思路和方法设计(用地址找到小区名字)
  19. Java+MySQL实现学生管理系统
  20. 用 C语言的写出几个小程序

热门文章

  1. Java 和 .Net那个就业前景更好?
  2. 基于Python的小游戏
  3. 【读书笔记】打开心智
  4. 【碎碎念】今天服务器又down了……
  5. 【I/O管理】和【磁盘调度】
  6. 几款好用的Markdown 写作工具推荐(下)
  7. stm32f769 寄存器配置SD卡---移植fatfs
  8. python字符串的特点_字符串特点_清华尹成python入门教程_少儿编程视频-51CTO学院...
  9. Java开发专业通过swot分析岗位_南昌招聘 | 江西江中食疗科技公司9大岗位招聘(月薪6000+、五险一金等福利)...
  10. 2020年阴历三月初七 投资理财~常年累月定投银行股年化收益率10%+