視覺SLAM學習【3】-----視覺SLAM通過三角測量和PnP法估計特徵點的空間位置(採用make編譯方式)


嵌入式開發學習也慢慢的進入尾聲了,從之前的ros學習,到gazebo再到機械臂的學習,到現在的slam視覺學習,層次逐漸上升,難度逐漸加大,本次博客,林君學長將打大家理解,如何通過三角測量和PnP法估計特徵點的空間位置,並且自己創建項目工程用make方式編譯,不用g++編譯,一起來看如下步驟吧!

一、G2O的安裝

g2o的核裏帶有各種各樣的求解器,而它的頂點、邊的類型則多種多樣。通過自定義頂點和邊,事實上,只要一個優化問題能夠表達成圖,那麼就可以用g2o去求解它。常見的,比如bundle adjustment,ICP,數據擬合,都可以用g2o來做;
本次博客的內容,我們需要用到G20,所以我們首先需要進度G2o的安裝,一起看步驟吧!

1、g2o的下載

1)、方法一,通過如下鏈接在GitHub上面下載:
https://github.com/RainerKuemmerle/g2o
在這裏插入圖片描述
2)、方法二,林君學長已經將g2o的源代碼(未編譯)包上傳到CSDN我的資源模塊,小夥伴們可以去下載,鏈接如下所示:
https://download.csdn.net/download/qq_42451251/12413010
在這裏插入圖片描述

2、文件上傳ubuntu

1)、將下載好的g2o的源代碼資源包上傳至ubuntu的任意位置解壓並重新命名爲g2o,下面以林君學長的路徑爲例

在這裏插入圖片描述

3、安裝依賴庫

sudo apt-get install libeigen3-dev libsuitesparse-dev libqt4-dev qt4-qmake libqglviewer-dev

4、g2o的編譯

1)、進入g2o文件夾,創建build文件夾,存放編譯產生的文件

cd ~/lenovo/g2o
mkdir build
cd build

2)、編譯

cmake ..
make -j8

以上的編譯,林君學長是用的8線程編譯的,如果小夥伴的電腦不支持8線程的話,可以改爲make-j4make
在這裏插入圖片描述
在這裏插入圖片描述
如上則爲成功編譯!
3)、安裝

sudo make install

通過以上,我們的g2o就安裝好了,編譯的時候有點慢,我們耐心等待就好了!

二、項目創建

1、創建項目文件夾

新建終端,創建項目並創建編譯文件夾

cd ~/lenovo/opencv-3.4.1
mkdir test
cd test
mkdir build
cd build

2、創建三角測量的cpp文件

1)、創建三角測量算法的cpp文件夾

touch testTriang.cpp
gedit testTriang.cpp

2)、編寫三角測量算法代碼

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/calib3d/calib3d.hpp>

using namespace std;
using namespace cv;

void find_feature_matches(const Mat& img_1,const Mat& img_2,
                          vector<KeyPoint>& keypoints_1,
                          vector<KeyPoint>& keypoints_2,
                          vector<DMatch>& matches)
{
    //初始化
    Mat descriptors_1,descriptors_2;
    Ptr<FeatureDetector> detector=ORB::create();
    Ptr<DescriptorExtractor> descriptor=ORB::create();
    Ptr<DescriptorMatcher> matcher=DescriptorMatcher::create("BruteForce-Hamming");

    //第一步:檢測 Oriented FAST 角點位置
    detector->detect(img_1,keypoints_1);
    detector->detect(img_2,keypoints_2);

    //第二步:根據角點位置計算 BRIEF 描述子
    descriptor->compute(img_1,keypoints_1,descriptors_1);
    descriptor->compute(img_2,keypoints_2,descriptors_2);

    //第三步:對兩幅圖像中的BRIEF描述子進行匹配,使用 Hamming 距離
    vector<DMatch> match;
    matcher->match(descriptors_1,descriptors_2,match);

    //第四步:匹配點對篩選
    double min_dist=1000,max_dist=0;

    //找出所有匹配之間的最小距離和最大距離, 即是最相似的和最不相似的兩組點之間的距離
    for(int i=0;i<descriptors_1.rows;++i)
    {
        double dist=match[i].distance;
        if(dist<min_dist) min_dist=dist;
        if(dist>max_dist) max_dist=dist;
    }

    printf("Max dist :%f\n",max_dist);
    printf("Min dist :%f\n",min_dist);

    //當描述子之間的距離大於兩倍的最小距離時,即認爲匹配有誤.但有時候最小距離會非常小,設置一個經驗值30作爲下限.
    for(int i=0;i<descriptors_1.rows;++i)
    {
        if(match[i].distance<=max(2*min_dist,30.0))
        {
            matches.push_back(match[i]);
        }
    }
    cout<<"一共找到了"<<matches.size() <<"組匹配點"<<endl;
}

//估計兩張圖像間運動
void pose_estimation_2d2d(const vector<KeyPoint>& keypoints_1,
                          const vector<KeyPoint>& keypoints_2,
                          const vector<DMatch>& matches,
                          Mat& R,Mat& t)
{
    //相機內參,TUM Freiburg2
    Mat K=(Mat_<double> (3,3)<<520.9,325.1,0,521.0,249.7,0,0,1);

    //把匹配點轉換爲vector<Point2f>的形式
    vector<Point2f> points1;
    vector<Point2f> points2;

    for(int i=0;i<(int)matches.size();++i)
    {
        points1.push_back(keypoints_1[matches[i].queryIdx].pt);//?
        points2.push_back(keypoints_2[matches[i].trainIdx].pt);//?
    }

    //計算基礎矩陣
    Mat fundamental_matrix;
    fundamental_matrix=findFundamentalMat(points1,points2,CV_FM_8POINT);
    cout<<"fundamental_matrix ="<<endl<<fundamental_matrix<<endl;

    //計算本質矩陣
    Point2d principal_point(325.1,249.7);//相機光心,TUM dataset標定值
    int focal_length=521;////相機焦距, TUM dataset標定值
    Mat essential_matrix;
    essential_matrix=findEssentialMat(points1,points2,focal_length,principal_point);
    cout<<"essential_matrix = "<<endl<<essential_matrix<<endl;

    //計算單應矩陣
    Mat homography_matrix;
    homography_matrix=findHomography(points1,points2,RANSAC,3);
    cout<<"homography_matrix = "<<homography_matrix<<endl;

    //從本質矩陣中恢復旋轉和平移信息
    recoverPose(essential_matrix,points1,points2,R,t,focal_length,principal_point);
    cout<<"R = "<<endl<<R<<endl;
    cout<<"t = "<<endl<<t<<endl;
}

Point2f pixel2cam(const Point2d& p,const Mat& K)
{
    return Point2f(
                (p.x-K.at<double>(0,2))/K.at<double>(0,0),
                (p.y-K.at<double>(1,2))/K.at<double>(1,1)
                );
}

void triangulation(const vector<KeyPoint>& keypoint_1,
                   const vector<KeyPoint>& keypoint_2,
                   const vector<DMatch>& matches,
                   const Mat& R,const Mat& t,
                   vector<Point3d>& points)
{
    Mat T1=(Mat_<float> (3,4)<<
            1,0,0,0,
            0,1,0,0,
            0,0,1,0);
    Mat T2=(Mat_<float>(3,4)<<
            R.at<double>(0,0),R.at<double>(0,1),R.at<double>(0,2),t.at<double>(0,0),
            R.at<double>(1,0),R.at<double>(1,1),R.at<double>(1,2),t.at<double>(1,0),
            R.at<double>(2,0),R.at<double>(2,1),R.at<double>(2,2),t.at<double>(2,0)
            );

    Mat K=(Mat_<double>(3,3)<<520.9,0,325.1,0,521.0,249.7,0,0,1);
    vector<Point2f> pts_1,pts_2;
    for(DMatch m:matches)
    {
        //將像素座標轉換至相機座標
        pts_1.push_back(pixel2cam(keypoint_1[m.queryIdx].pt,K));
        pts_2.push_back(pixel2cam(keypoint_2[m.trainIdx].pt,K));
    }

    Mat pts_4d;
    triangulatePoints(T1,T2,pts_1,pts_2,pts_4d);
    cout<<pts_4d.cols<<endl;
    //轉換成非齊次座標
    for(int i=0;i<pts_4d.cols;++i)
    {
        Mat x=pts_4d.col(i);
        x/=x.at<float>(3,0);//歸一化
        Point3d p(
                    x.at<float>(0,0),
                    x.at<float>(1,0),
                    x.at<float>(2,0)
                    );
        cout<<"p = "<<endl<<p<<endl;
        points.push_back(p);
    }
}



int main(int argc,char** argv)
{
    if(argc!=3)
    {
        cout<<"usage:triangulation img1 img2"<<endl;
        return 1;
    }

    //讀取圖像
    Mat img_1=imread(argv[1],CV_LOAD_IMAGE_COLOR);
    Mat img_2=imread(argv[2],CV_LOAD_IMAGE_COLOR);

    //尋找匹配點對
    vector<KeyPoint> keypoints_1,keypoints_2;
    vector<DMatch> matches;
    find_feature_matches(img_1,img_2,keypoints_1,keypoints_2,matches);

    //估計兩張圖像間運動
    Mat R,t;
    pose_estimation_2d2d(keypoints_1,keypoints_2,matches,R,t);

    //三角化
    vector<Point3d> points;
    triangulation(keypoints_1,keypoints_2,matches,R,t,points);

    //驗證三角化點與特徵點的重投影關係
    Mat K=(Mat_<double>(3,3)<<520.9,0,325.1,0,521.0,249.7,0,0,1);
    for(int i=0;i<matches.size();++i)
    {
        Point2d pt1_cam=pixel2cam(keypoints_1[matches[i].queryIdx].pt,K);
        Point2d pt1_cam_3d(
                    points[i].x/points[i].z,
                    points[i].y/points[i].z
                    );
        cout<<"point in the first camera frame: "<<pt1_cam<<endl;
        cout<<"point projected from 3D"<<pt1_cam_3d<<",d="<<points[i].z<<endl;

        //第二個圖
        Point2f pt2_cam=pixel2cam(keypoints_2[matches[i].trainIdx].pt,K);
        Mat pt2_trans=R*(Mat_<double>(3,1)<<points[i].x,points[i].y,points[i].z)+t;
        pt2_trans/=pt2_trans.at<double>(2,0);
        cout<<"point in the second camera a frame: "<<pt2_cam<<endl;
        cout<<"point projected from second frame: "<<pt2_trans.t()<<endl;
        cout<<endl;

    }


    return 0;
}

3、創建PnP法的cpp文件

1)、創建PnP法的cpp文件夾

touch testPnp.cpp
gedit testPnp.cpp

2)、編寫PnP法的c++代碼

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <g2o/core/base_vertex.h>
#include <g2o/core/base_unary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/csparse/linear_solver_csparse.h>
#include <g2o/types/sba/types_six_dof_expmap.h>
#include <chrono>

using namespace std;
using namespace cv;

void find_feature_matches (
    const Mat& img_1, const Mat& img_2,
    std::vector<KeyPoint>& keypoints_1,
    std::vector<KeyPoint>& keypoints_2,
    std::vector< DMatch >& matches );

// 像素座標轉相機歸一化座標
Point2d pixel2cam ( const Point2d& p, const Mat& K );

int main ( int argc, char** argv )
{
    if ( argc != 4 )
    {
        cout<<"usage: pose_estimation_3d2d img1 img2 depth1"<<endl;
        return 1;
    }
    //-- 讀取圖像
    Mat img_1 = imread ( argv[1], CV_LOAD_IMAGE_COLOR );
    Mat img_2 = imread ( argv[2], CV_LOAD_IMAGE_COLOR );

    vector<KeyPoint> keypoints_1, keypoints_2;
    vector<DMatch> matches;
    find_feature_matches ( img_1, img_2, keypoints_1, keypoints_2, matches );
    cout<<"一共找到了"<<matches.size() <<"組匹配點"<<endl;

    // 建立3D點
    Mat d1 = imread ( argv[3], CV_LOAD_IMAGE_UNCHANGED );       // 深度圖爲16位無符號數,單通道圖像
    Mat K = ( Mat_<double> ( 3,3 ) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1 );
    vector<Point3f> pts_3d;
    vector<Point2f> pts_2d;
    for ( DMatch m:matches )
    {
        ushort d = d1.ptr<unsigned short> (int ( keypoints_1[m.queryIdx].pt.y )) [ int ( keypoints_1[m.queryIdx].pt.x ) ];
        if ( d == 0 )   // bad depth
            continue;
        float dd = d/5000.0;
        Point2d p1 = pixel2cam ( keypoints_1[m.queryIdx].pt, K );
        pts_3d.push_back ( Point3f ( p1.x*dd, p1.y*dd, dd ) );
        pts_2d.push_back ( keypoints_2[m.trainIdx].pt );
    }

    cout<<"3d-2d pairs: "<<pts_3d.size() <<endl;

    Mat r, t;
    solvePnP ( pts_3d, pts_2d, K, Mat(), r, t, false ); // 調用OpenCV 的 PnP 求解,可選擇EPNP,DLS等方法
    Mat R;
    cv::Rodrigues ( r, R ); // r爲旋轉向量形式,用Rodrigues公式轉換爲矩陣

    cout<<"R="<<endl<<R<<endl;
    cout<<"t="<<endl<<t<<endl;
}

void find_feature_matches ( const Mat& img_1, const Mat& img_2,
                            std::vector<KeyPoint>& keypoints_1,
                            std::vector<KeyPoint>& keypoints_2,
                            std::vector< DMatch >& matches )
{
    //-- 初始化
    Mat descriptors_1, descriptors_2;
    // used in OpenCV3
    Ptr<FeatureDetector> detector = ORB::create();
    Ptr<DescriptorExtractor> descriptor = ORB::create();
    // use this if you are in OpenCV2
    // Ptr<FeatureDetector> detector = FeatureDetector::create ( "ORB" );
    // Ptr<DescriptorExtractor> descriptor = DescriptorExtractor::create ( "ORB" );
    Ptr<DescriptorMatcher> matcher  = DescriptorMatcher::create ( "BruteForce-Hamming" );
    //-- 第一步:檢測 Oriented FAST 角點位置
    detector->detect ( img_1,keypoints_1 );
    detector->detect ( img_2,keypoints_2 );

    //-- 第二步:根據角點位置計算 BRIEF 描述子
    descriptor->compute ( img_1, keypoints_1, descriptors_1 );
    descriptor->compute ( img_2, keypoints_2, descriptors_2 );

    //-- 第三步:對兩幅圖像中的BRIEF描述子進行匹配,使用 Hamming 距離
    vector<DMatch> match;
    // BFMatcher matcher ( NORM_HAMMING );
    matcher->match ( descriptors_1, descriptors_2, match );

    //-- 第四步:匹配點對篩選
    double min_dist=10000, max_dist=0;

    //找出所有匹配之間的最小距離和最大距離, 即是最相似的和最不相似的兩組點之間的距離
    for ( int i = 0; i < descriptors_1.rows; i++ )
    {
        double dist = match[i].distance;
        if ( dist < min_dist ) min_dist = dist;
        if ( dist > max_dist ) max_dist = dist;
    }

    printf ( "-- Max dist : %f \n", max_dist );
    printf ( "-- Min dist : %f \n", min_dist );

    //當描述子之間的距離大於兩倍的最小距離時,即認爲匹配有誤.但有時候最小距離會非常小,設置一個經驗值30作爲下限.
    for ( int i = 0; i < descriptors_1.rows; i++ )
    {
        if ( match[i].distance <= max ( 2*min_dist, 30.0 ) )
        {
            matches.push_back ( match[i] );
        }
    }
}

Point2d pixel2cam ( const Point2d& p, const Mat& K )
{
    return Point2d
           (
               ( p.x - K.at<double> ( 0,2 ) ) / K.at<double> ( 0,0 ),
               ( p.y - K.at<double> ( 1,2 ) ) / K.at<double> ( 1,1 )
           );
}

4、創建CMakeLists.txt配置文件

1)、創建CMakeLists.txt配置文件

touch CMakeLists.txt
gedit CMakeLists.txt

2)、編寫CMakeLists.txt配置文件內容

cmake_minimum_required(VERSION 2.8)
project(test)
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules)
set(CMAKE_CXX_STANDARD 11)
set( G2O_ROOT /usr/local/include/g2o )
find_package( OpenCV REQUIRED)
find_package(Eigen3  REQUIRED)
find_package(G2O REQUIRED)
find_package( CSparse)
include_directories(
${EIGEN3_INCLUDE_DIR}
${OpenCV_INCLUDE_DIRS}
${G2O_INCLUDE_DIRS} 
${CSPARSE_INCLUDE_DIR} 
)
add_executable(testPnp testPnp.cpp)
add_executable(testTriang testTriang.cpp)
target_link_libraries( testTriang ${OpenCV_LIBS})
target_link_libraries( testPnp ${EIGEN3_LIB} ${OpenCV_LIBS} ${G2O_LIB} ${CSPARSE_LIB})
target_link_libraries(testPnp g2o_core g2o_stuff)

三、編譯項目

1、文件準備

1)、將上面我們編譯g2o中的cmake_modules文件夾複製到我們test項目文件夾中,如下所示:
在這裏插入圖片描述
在這裏插入圖片描述
2)、在test項目下的build中添加兩張圖片,如下所示:
在這裏插入圖片描述
3)、將上面的兩張拍攝的圖片變爲深度圖(灰度圖),後存放該處:
在這裏插入圖片描述
什麼是深度圖: 深度圖爲16位無符號數,單通道圖像,也就是,我們只需要將我們的圖片變爲灰度圖就行了,方法很多,小夥伴們可以自己摸索哦!
圖片的拍攝必須是同一個物體,不同的角度,也就是要具有相同的特徵點

2、項目進行編譯

1)、進入build文件夾進行編譯

cd build 

2)、編譯

cmake ..
make -j4

在這裏插入圖片描述
3)、編譯後的整體項目如下:
test目錄下:
在這裏插入圖片描述
build目錄下:
在這裏插入圖片描述
如上所示出現百分百則爲成功,由於林君學長是編譯了一次的,所以編譯很快,小夥伴則需要等待一小會兒

2、結果運行

1)、三角測量算法的結果運行

./testTriang 1.jpg 2.jpg

在這裏插入圖片描述
2)、PnP法的結果運行

./testPnp 1.jpg 2.jpg 11.jpg

在這裏插入圖片描述

以上就是本次博客的全部內容啦,希望小夥伴們對本次博客的閱讀可以幫助大家理解SLAM中三角測量和PnP法估計特徵點的空間位置,瞭解求解機制,並知道如何編寫make編譯的項目哦!
遇到問題的小夥伴記得評論區留言,林君學長看到會爲大家解答的,這個學長不太冷!

陳一月的又一天編程歲月^ _ ^

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章