天天看點

基于ArUco的視覺定位(三)

一、ArUco之Marker Mapper

1、Marker Mapper簡介

Mapping and Localization from Planar Markers是A.V.A小組基于ArUco開發的一個利用二維碼建圖與定位的項目。

論文位址:Mapping and Localization from Planar Markers

源碼位址:https://sourceforge.net/projects/markermapper/files/?source=navbar (源碼持續更新)

基于ArUco的視覺定位(三)

2、利用MarkerMapper實作建圖

(1)編譯源碼

我們從源碼位址下載下傳目前最新的1.0.12版本,解壓編譯:

$ unzip marker_mapper1.0.12.zip
$ cd marker_mapper1.0.12
$ mkdir build
$ cd build
$ cmake ..
$ make
           

如果編譯出錯,打開marker_mapper1.0.12檔案夾下的CMakeLists.txt,做如下修改:

基于ArUco的視覺定位(三)

修改完以後重新編譯即可:

$ cmake ..; make

(2)用自己的攝像頭建圖

先找到你之前标定相機生成的yml檔案,将其拷貝到 build/utils檔案夾下,然後在此檔案夾下運作指令

$ ./mapper_from_video

,結果如下:

基于ArUco的視覺定位(三)

解釋一下這幾個參數:

  • (live | intput-video.avi[:v2.avi[:v3.avi…]]):live表示采用攝像頭裝置,但這裡隻能打開裝置号為0的相機裝置,如果你用筆記本想打開USB攝像頭的話需要改一下源碼。具體怎麼改我就不說了,相信這點小問題肯定難不倒大家。後面input-video.avi表示讀取視訊檔案,你需要事先錄取一段視訊,然後最好也放到 build/utils檔案夾下。
  • camera_parameters.yml:相機的标定檔案。
  • marker_size(meters):二維碼的邊長(以米為機關)。請務必保證所有二維碼的邊長一緻,不要出現大小不一的二維碼。
  • [-ref reference_marker_id]:參考二維碼的id。在建圖過程中需要標明一個二維碼作為基準,其他二維碼的位置均是相對于這個二維碼來說的。即以參考二維碼的坐标系作為固定的世界坐标系。預設條件下以第一個被識别的二維碼為基準。
  • [-fi frameIncrement]:幀增量,預設值為1。
  • [-out basename]:程式輸出使用的基本名稱。程式會以“basename”生成basename.pcd、basename.log、basename.yml、basename-cam.yml四個檔案。預設值為“markerset”。
  • [-d maker_dictionary] [-c arucoConfig.yml]:兩者二選一,指定二維碼的字典或指定一個二維碼的配置檔案。-d的預設值是ALL_DICTS,表示可以識别ArUco庫内所有字典中的二維碼。
  • -noshow:這個參數沒有在Usage中展現出來,它表示在程式最後不啟動MapperViewer。

根據上面對每個參數的解釋,我們可以根據自己的實際情況輸入合适的指令行參數,以我的為例:

$ ./mapper_from_video live out_camera_calibration.yml 0.135 -ref 10 -d ARUCO -noshow
           

運作效果如下:

基于ArUco的視覺定位(三)

我把二維碼的前三個角點按照黃、綠、藍的順序标了出來,你的程式中應該不會有這個效果,因為我改了ArUco的源碼。程式運作時,為確定建圖的精度,相機移動不宜過快,且應盡量保證在運動過程中每一幀圖像至少有兩個二維碼。按’s’鍵暫停,'ESC’鍵退出。程式運作結束後,你會發現檔案夾中多了四個檔案:

  • markerset.log:記錄了關鍵幀的相機位姿,資料格式為:#frame_id tx ty tz qx qy qz qw。
  • markerset.pcd:二維碼分布及相機關鍵幀的點雲圖。
  • markerset.yml:二維碼的分布檔案,這個檔案描述了所有二維碼相對于參考二維碼的空間位姿。以參考二維碼的中心為基準,以該二維碼的坐标系為世界坐标系,所有二維碼角點的三維空間坐标便确定了。
  • markerset-cam.yml:優化以後的相機标定檔案。

我們用PCL看一下生成的點雲圖,運作指令

$ pcl_viewer markerset.pcd

基于ArUco的視覺定位(三)

如上圖所示,我在實驗室牆壁上貼了20張ARUCO字典中的marker,marker的ID從0到19。點雲圖中的marker分布完全重制了真實實體環境中的二維碼分布。

(3)用二維碼分布圖實作室内定位

用MarkerMapper我們得到了二維碼分布圖,即我們知道了所有二維碼之間的相對位姿關系。近一步說,我們以參考二維碼的坐标系作為世界坐标系,我們便知道了每個二維碼在三維空間中精确的位姿,這是非常重要的!

由上一篇我們知道,利用一個二維碼可以估計相機的位姿,而現在我們有了好多二維碼,并且它們的空間位姿我們也都知道(實際我們已經知道了每個二維碼每個角點的三維坐标)。那麼,我們是不是可以利用這些二維碼來實作相機的位姿估計,進而達到室内定位的目的呢?答案是肯定的!

我們先用ArUco源碼中的aruco_test_markermap測試一下看看效果,執行指令:

$ ~/OpenCV/aruco-3.0.6/build/utils_markermap/aruco_test_markermap live:1 markerset.yml markerset-cam.yml -s 0.135
           

不要照抄,根據你的實際情況做适當修改,運作結果如下圖所示:

基于ArUco的視覺定位(三)

從圖中我們可以看出,相機準确地找到了自己的位置,即很好地實作了相機的位姿估計。接下來,我就在這個程式的基礎上做一些删改,按照上一篇的做法将相機在世界坐标系中的位姿資料顯示出來。

  • 首先,添加源檔案

cd 到上一篇建立的工程目錄test/檔案夾下,運作指令:

$ touch cam_localization.cpp
$ gedit cam_localization.cpp
           

然後粘貼以下代碼:

/*****************************************************/
/*                   By Li Xujie                     */
/*****************************************************/

#include "cvdrawingutils.h"
#include "aruco.h"
#include "timers.h"
#include <fstream>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <sstream>
#include <string>
#include <stdexcept>

using namespace cv;
using namespace aruco;
using namespace std;

string TheMarkerMapConfigFile;
float TheMarkerSize = -1;
VideoCapture TheVideoCapturer;
Mat TheInputImage, TheInputImageCopy;
CameraParameters TheCameraParameters;
MarkerMap TheMarkerMapConfig;
MarkerDetector TheMarkerDetector;
MarkerMapPoseTracker TheMSPoseTracker;
int waitTime = 0;
int ref_id = 0;

char camPositionStr[100];
char camDirectionStr[100];

class CmdLineParser
{
    int argc;char** argv;public:
    CmdLineParser(int _argc, char** _argv): argc(_argc), argv(_argv){}
    //is the param?
    bool operator[](string param)
    {int idx = -1;   for (int i = 0; i < argc && idx == -1; i++)if (string(argv[i]) == param) idx = i;return (idx != -1); }
    //return the value of a param using a default value if it is not present
    string operator()(string param, string defvalue = "-1"){int idx = -1;for (int i = 0; i < argc && idx == -1; i++)if (string(argv[i]) == param) idx = i;if (idx == -1) return defvalue;else return (argv[idx + 1]);}
};

/************************************
 ************************************/
int main(int argc, char** argv)
{
    try
    {
        CmdLineParser cml(argc, argv);
        if (argc < 4 || cml["-h"])
        {
            cerr << "Invalid number of arguments" << endl;
            cerr << "Usage: (in.avi|live[:camera_index(e.g 0 or 1)])) marksetconfig.yml camera_intrinsics.yml  "
                    "[-s marker_size] [-ref_id reference_marker_id] [-e use EnclosedMarker or not]\n\t" << endl;
            return false;
        }
        TheMarkerMapConfigFile = argv[2];
        TheMarkerMapConfig.readFromFile(TheMarkerMapConfigFile);
        TheMarkerSize = stof(cml("-s", "1"));
        ref_id = std::stoi(cml("-ref_id"));
        if(-1 == ref_id){cout<<"You need indicate a referene_marker by use the parameter [-ref_id].\n"<<endl;return false;}
	
        // read from camera or from  file
        string TheInputVideo=string(argv[1]);
        if ( TheInputVideo.find( "live")!=std::string::npos)
        {
            int vIdx = 0;
            // check if the :idx is here
            char cad[100];
            if (TheInputVideo.find(":") != string::npos)
            {
                std::replace(TheInputVideo.begin(), TheInputVideo.end(), ':', ' ');
                sscanf(TheInputVideo.c_str(), "%s %d", cad, &vIdx);
            }
            cout << "Opening camera index " << vIdx << endl;
            TheVideoCapturer.open(vIdx);
            waitTime = 10;
        }
        else TheVideoCapturer.open(argv[1]);        // check video is open
        if (!TheVideoCapturer.isOpened())throw std::runtime_error("Could not open video");

        // read first image to get the dimensions
        TheVideoCapturer >> TheInputImage;

        // read camera parameters if passed
        TheCameraParameters.readFromXMLFile(argv[3]);
        TheCameraParameters.resize(TheInputImage.size());
        
		// prepare the detector
        TheMarkerDetector.setDictionary( TheMarkerMapConfig.getDictionary());
        TheMarkerDetector.getParameters().detectEnclosedMarkers(std::stoi(cml("-e", "0"))); //If use enclosed marker, set -e 1(true), or set -e 0(false). Default value is 0.

        if (cml["-config"])TheMarkerDetector.loadParamsFromFile(cml("-config"));
        // prepare the pose tracker if possible
        // if the camera parameers are avaiable, and the markerset can be expressed in meters, then go

        if (TheMarkerMapConfig.isExpressedInPixels() && TheMarkerSize > 0)
            TheMarkerMapConfig = TheMarkerMapConfig.convertToMeters(TheMarkerSize);

        cout << "TheCameraParameters.isValid()=" << TheCameraParameters.isValid() << " "<< TheMarkerMapConfig.isExpressedInMeters() << endl;

        if (TheCameraParameters.isValid() && TheMarkerMapConfig.isExpressedInMeters()){
            TheMSPoseTracker.setParams(TheCameraParameters, TheMarkerMapConfig);
            TheMarkerSize=cv::norm(TheMarkerMapConfig[0][0]- TheMarkerMapConfig[0][1]);
        }
	
		char key = 0;
        int index = 0;
        // capture until press ESC or until the end of the video
        cout << "Press 's' to start/stop video" << endl;
        Timer avrgTimer;
        do
        {
            TheVideoCapturer.retrieve(TheInputImage);
            TheInputImage.copyTo(TheInputImageCopy);
            index++;  // number of images captured
            if (index>1) avrgTimer.start();//do not consider first frame that requires initialization
            // Detection of the markers
            vector<aruco::Marker> detected_markers = TheMarkerDetector.detect(TheInputImage,TheCameraParameters,TheMarkerSize);
            // estimate 3d camera pose if possible
            if (TheMSPoseTracker.isValid())
                if (TheMSPoseTracker.estimatePose(detected_markers)) {
				Mat rMatrix,camPosMatrix,camVecMatrix;//定義旋轉矩陣,平移矩陣,相機位置矩陣和姿态矩陣
				Rodrigues(TheMSPoseTracker.getRvec(),rMatrix);//獲得相機坐标系與世界坐标系之間的旋轉向量并轉化為旋轉矩陣
				camPosMatrix=rMatrix.inv()*(-TheMSPoseTracker.getTvec().t());//計算出相機在世界坐标系下的三維坐标
				Mat vect=(Mat_<float>(3,1)<<0,0,1);//定義相機坐标系下的機關方向向量,将其轉化到世界坐标系下便可求出相機在世界坐标系中的姿态
				camVecMatrix=rMatrix.inv()*vect;//計算出相機在世界坐标系下的姿态
		    
				    sprintf(camPositionStr,"Camera Position: px=%f, py=%f, pz=%f", camPosMatrix.at<float>(0,0), 
                        camPosMatrix.at<float>(1,0), camPosMatrix.at<float>(2,0));
                    sprintf(camDirectionStr,"Camera Direction: dx=%f, dy=%f, dz=%f", camVecMatrix.at<float>(0,0), 
                        camVecMatrix.at<float>(1,0), camVecMatrix.at<float>(2,0));
				}  
		 
            if (index>1) avrgTimer.end();
            cout<<"Average computing time "<<avrgTimer.getAverage()<<" ms"<<endl;
	    
            // print the markers detected that belongs to the markerset
            for (auto idx : TheMarkerMapConfig.getIndices(detected_markers))
			{
                detected_markers[idx].draw(TheInputImageCopy, Scalar(0, 0, 255));
				if(ref_id == detected_markers[idx].id) CvDrawingUtils::draw3dAxis(TheInputImageCopy,detected_markers[idx],TheCameraParameters);
		    }
		    putText(TheInputImageCopy,camPositionStr,Point(10,13),CV_FONT_NORMAL,0.5,Scalar(0,255,255));
		    putText(TheInputImageCopy,camDirectionStr,Point(10,30),CV_FONT_NORMAL,0.5,Scalar(0,255,255));
imshow("out",TheInputImageCopy);

key = waitKey(waitTime);
if (key=='s') waitTime = waitTime?0:10;
        } while (key != 27 && TheVideoCapturer.grab());
    }
    catch (std::exception& ex) { cout << "Exception :" << ex.what() << endl;  }
}
           
  • 然後,修改CMakeLists.txt

在終端運作

$ gedit ../CMakeLists.txt

,在文檔最後粘貼下面兩行代碼:

add_executable( cam_localization test/cam_localization.cpp )
target_link_libraries( cam_localization ${OpenCV_LIBS} ${aruco_LIBS} )
           
  • 最後,編譯執行

依次運作以下指令:

$ cd ../build
$ cmake ..;make
$ ../bin/cam_localization live:1 ~/OpenCV/marker_mapper1.0.12/build/utils/markerset.yml ~/OpenCV/marker_mapper1.0.12/build/utils/markerset-cam.yml -s 0.135 -ref_id 10 -e 1
           

第三條指令請根據自己的情況适當修改,該程式的運作效果如下圖:

基于ArUco的視覺定位(三)

圖中黃色字元串顯示的是相機在世界坐标系下的位置和姿态,因為前面我用MarkerMapper建圖的時候是以ID為10的marker作為參考,是以這裡的世界坐标系就是10号二維碼的坐标系。

部落格寫到這兒,我們已經用單目相機實作了基于ArUco的室内定位。可以說,這個定位的精度還是相當令人滿意的,如果後期再加上一些濾波優化算法,則可以進一步提高它的定位精度和穩定性。**相對于目前很多其他基于二維碼的定位與導航方法,基于ArUco基準辨別碼的視覺定位不需要人工測量或預先鋪設二維碼路徑,我們隻要任意地将ArUco碼貼在牆(門)面、天花闆、地面或立柱上就可實時擷取移動機器人的三維空間坐标和姿态。該方法非常友善、靈活,可以極大地降低人工成本。**但是,它有一個緻命的弱點:在實際應用中,我們不可能在室内到處都貼上二維碼,換句話說,實際環境中的二維碼分布通常是很稀疏的,是以難免會出現圖像中沒有二維碼的情況。那麼問題來了,在相機檢測不到二維碼時如何估計它的位姿?這個問題留給各位大神來解決,如有好的方案還望不吝賜教!

二、ArUco之SPM-SLAM

SPM-SLAM: Squared Planar Marker SLAM,是一個基于ArUco的單目視覺SLAM方案。

項目位址:http://www.uco.es/investiga/grupos/ava/node/58

源碼位址:https://sourceforge.net/projects/spm-slam/files/

1、編譯運作

$ unzip spm-slam_1.0.1.zip
$ cd spm-slam_1.0.1/
$ mkdir build
$ cd build/
$ cmake ..
$ make
           

從這裡下載下傳一個名叫“4_video_office_large”的資料集,解壓以後裡面是一些圖檔序列以及相機的标定檔案等。因為SPM-SLAM需要輸入一個視訊,是以你需要先把圖檔序列轉成視訊(或者直接将源碼改為讀取圖檔序列),然後把視訊和相機标定檔案拷貝到spm-slam檔案夾下。為了防止混亂,我在此建立一個檔案夾叫dataset,把視訊和标定檔案放在這裡。然後運作指令:

$ ../build/spm-slam video.avi camera.yml -dic ARUCO -markersize 0.123
           

效果如下圖:

基于ArUco的視覺定位(三)

運作完了你會發現在dataset檔案夾多了一個markermap.yml檔案,裡面記錄了所有marker的分布情況。那麼接下來,我要把它的代碼改一下,用它跑我們自己的攝像頭。

2、SPM-SLAM跑自己的攝像頭

  • Step1 建立源檔案

cd 到build檔案夾,在終端運作指令:

$ touch ../spm_slam.cpp
$ gedit ../spm_slam.cpp
           

粘貼以下代碼:

/********************************************************************/
/                             By Li Xujie                            /
/********************************************************************/

#include "slam.h"
#include "mapviewer.h"
#include "stuff/timers.h"
#include <Eigen/Geometry>


class CmdLineParser {
    int argc;
    char **argv;
public:
    CmdLineParser(int _argc, char **_argv) : argc(_argc), argv(_argv) {}

    bool operator[](string param) {
        int idx = -1;
        for (int i = 0; i < argc && idx == -1; i++) if (string(argv[i]) == param) idx = i;
        return (idx != -1);
    }

    string operator()(string param, string defvalue = "-1") {
        int idx = -1;
        for (int i = 0; i < argc && idx == -1; i++) if (string(argv[i]) == param) idx = i;
        if (idx == -1) return defvalue; else return (argv[idx + 1]);
    }
};

int main(int argc, char **argv) {
    try {
        CmdLineParser cml(argc, argv);
        if (argc < 3) {
            cerr << "Usage: camIndex  camera_params.yml [-conf configParams.yml] [-dic DICT] [-markersize val]" << endl;
            cerr << "\tcamIndex: camera index" << endl;
            cerr << "\tcamera_params.yml: file with calibration parameters. Use OpenCv tool or ArUco tool." << endl;
            cerr << "\t-conf <file>: specifies a configuration file for the aruco detector. Overrides [-dic and -markersize]" << endl;
            cerr << "\t-markersize <float>: specifies the marker size in meters. Default value is 1." << endl;
            cerr << "\t-dic <string>: indicates the dictionary to be employed. Default value is ARUCO_MIP_36h12" << endl;
            cerr << "\t Possible Dictionaries: ";
            for (auto dict : aruco::Dictionary::getDicTypes()) cerr << dict << " ";
            cerr << endl;
            return -1;
        }
        
        cv::VideoCapture vcap;
        cv::VideoWriter videoout;
        vcap.open(std::stod(argv[1]));
        if (!vcap.isOpened()) throw std::runtime_error("Video not opened");

        ucoslam::Slam Slam;
        ucoslam::ImageParams image_params;
        ucoslam::Params params;
        cv::Mat in_image;
        image_params.readFromXMLFile(argv[2]);
        if (cml["-conf"]) params.readFromYMLFile(cml("-conf"));
        else {
            params.aruco_DetectorParams.dictionary = cml("-dic", "ARUCO_MIP_36h12");
            params.aruco_markerSize = stof(cml("-markersize", "1"));
        }

        auto TheMap = std::make_shared<ucoslam::Map>();

        //Create the viewer to see the images and the 3D
        auto TViewer = ucoslam::MapViewer::create("Cv");
        TViewer->set("showNumbers", "1");
        TViewer->set("canLeave", "1");
        TViewer->set("mode", "0");
        TViewer->set("modelMatrix", "0.998437 -0.0490304 0.0268194 0  0.00535287 0.561584 0.827403 0  -0.0556289 -0.825967 0.560969 0  0 0 0 1");
        TViewer->set("viewMatrix", " 1 0 0 0.01  0 4.63287e-05 -1 0.910185  0 1 4.63287e-05 9.18  0 0 0 1 ");

        Slam.setParams(TheMap, params);

        //Ok, let's start
        char k = 0;
        ucoslam::TimerAvrg Fps;
		int currentFrameIndex=0;

        while (vcap.grab() && k != 27) {
            vcap.retrieve(in_image);
            currentFrameIndex++;
            //cout << "currentFrameIndex = " << currentFrameIndex << endl;
            Fps.start();
            Slam.process(in_image, image_params, currentFrameIndex);
            Fps.stop();
            k = TViewer->show(TheMap, in_image, Slam.getCurrentPose_f2g(), "#" + std::to_string(currentFrameIndex) + " fps=" + to_string(1. / Fps.getAvrg()));
        }

        cerr << "The markermap is saved to markermap.yml" << endl;
        TheMap->saveToMarkerMap("markermap.yml");
    }
    catch (std::exception &ex) { cerr << ex.what() << endl; }
}
           
  • Step2 修改CMakeLists.txt

在終端運作指令

$ gedit ../CMakeLists.txt

,先在文檔中找到“ADD_EXECUTABLE(spm-slam spm-slam.cpp )”,然後做如下修改:

基于ArUco的視覺定位(三)
  • Step3 編譯運作

把相機标定檔案拷貝到build檔案夾下,在終端執行指令:

$ cmake ..;make
$ ./spm_slam 1 Logitech.yml -dic ARUCO -markersize 0.135
           

程式入口參數請根據你的實際情況适當修改,運作效果如下圖:

基于ArUco的視覺定位(三)

你應該能夠發現,程式最後的運作效果跟前面的MarkerMapper差不多,它也會生成一個二維碼的分布檔案叫markermap.yml。這個檔案和MarkerMapper生成的markerset.yml是一樣的,都可以用于室内定位。

這裡再總結一下,SPM-SLAM相比于其他的視覺SLAM來說,它不存在明顯的尺度漂移且沒有其他單目SLAM的通病——尺度不确定性。原因很簡單,SPM-SLAM預先知道了環境中的一個條件——二維碼的邊長,這就使得它對整個空間的尺度有了準确地把握;再加上每個二維碼四個角點的相對位置已知,這使得相機在運動過程中可以有效地消除累積誤差。

三、MarkerMapper的建圖原理

本來想在這兒簡單地介紹一下原理,但考慮到問題的複雜性,一句兩句總也說不清楚,是以我又追加了一篇部落格,詳細介紹MarkerMapper的實作原理,感興趣的請移步基于ArUco的視覺定位(四)

繼續閱讀