RGB-D SLAM——匹配篇(一)
来源:互联网 发布:linux命令配置ip地址 编辑:程序博客网 时间:2024/05/01 15:24
之前跑过高博的一起做RGB-D SLAM,趁着放假,也稍微整理一下,顺便理一下思路, 由于本人是在Ubuntu上跑的代码,所以用的工具和IDE是CMake+QtCreator,先稍微介绍下如果用QtCreate建立CMake工程,正确来说应该是如何导入CMake工程,然后再进行RGB-D SLAM的搭建。
1.通过QtCreator导入CMake工程
在任意一个Directory中创建一个package,分别在package中创建如下空间:
如上图所示:在C_lib package中包含CMakeLists.txt 和 src package (该Directory必须包含这俩个),然后进去 src 后,在该Directory再创建一个CMakeLists.txt 和任意一个 hello.cpp 文件,必须文档到这里结束。
其中,俩个CMakeList.txt 需要包含的内容有:
C_lib 目录下的CMakeLists
CMAKE_MINIMUM_REQUIRED( VERSION 3.5.1 ) #这里我用的CMake是3.5.1的PROJECT( C_lib) #工程名字#SET(CMAKE_CXX_COMPILER "g++") #设定编译器SET( CMAKE_BUILD_TYPE Debug ) #编译类型SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #将执行二进制文件输出到bin文件夹中SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) #生成的库输出到lib文件夹中INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/include ) #头文件放在include文件夹中ADD_SUBDIRECTORY( ${PROJECT_SOURCE_DIR}/src ) #增加子文件,也就是进入源码文件夹中继续构建
src 目录下的CMakeLists:
CMAKE_MINIMUM_REQUIRED( VERSION 3.5.1 ) 版本号PROJECT( C_lib ) 工程名set(CMAKE_CXX_FLAGS "-std=c++11 ")add_executable(hello hello.cpp) #生成可执行文件
src目录下的hello.cpp :
#include<iostream>int main(){ std::cout<<"hello"<<std::endl; return 0;}
至此,一个最简单的工程创建完毕,紧接着,打开QTCreator,file->open file or project ,然后选中 C_lib 下的CMakelists,至此则导入成功,然后再后面需要什么再往里加就行了。
2.相机模型及其公式推导
一个空间点[X,Y,Z]和它在图像中的像坐标[u,v,d],有以下公式:
其中 K 为相机内参,k=
从而得出以下公式,即三维空间点到像素平面点的转换关系:
其中 d(mm极) 为深度图深度,scale为尺度因子,其他为内参。
对于俩副图片的位姿,我们可通过以下公式来求得:
,在这个公式中,我们把Z理解为空间的点,而不是深度图。
3.从图片生成点云
这里我们先给出封装的函数,到后面我们再将其结合起来。。。
#include<iostream>#include<string>using namespace std#include<opencv2/core/pcd_io.h>#include<opencv2/point_types.h>typedef pcl::PointXYZRGBA pointT;typedef pcl::PointCloud<PointXYZRGBA> PointCloudPointCloud::Ptr image2PointCloud( cv::Mat& rgb, cv::Mat& depth, CAMERA_INTRINSIC_PARAMETERS& camera ) //创建一个PointCloud::Ptr智能指针类型的函数,方便管理内存,传入的参数有rgb图,depth图,以及相机内参{ PointCloud::Ptr cloud(new PointCloud); //创建一个PointCloud对象,顺便在堆区创建内存用来存储,返回指针给主函数 for(int i=0;i<depth.rows;i++) //i是y轴的值,j是x轴的值 for(int j=0;j<depth.cols;j++) { //遍历传进的深度图 ushort d=depth.ptr<ushort> (i)[j];//为什么用ushort,在下面有说明,通过depth.ptr<ushort>能读出i行j列的深度值 if(d==0) //如果是hole,则直接跳过 continue; PointT p; //声明一个 p.z=double(d)/camera.scale; //先求出空间点Z p.x=(j-camera.cx)*p.z/camera.fx; //求出空间点X p.y=(i-camera.cy)*p.z/camera.fy; //求出空间点Y p.b=rgb.ptr<uchar>(i)[3*j]; //无符号字符,第i行第j列,注意这里,rgb是连着一起放的。 p.g=rgb.ptr<uchar>(i)[3*j+1]; p.r=rgb.ptr<uchar>(i)[3*j+2]; cloud->points.push_back(p); //将p储存到cloud中 } cloud->height=1; //设点云数据 cloud->width=cloud->points.size(); cloud->is_dense=false; return cloud;}
这里有些知识点需要清楚:
1.在OpenCV中,图像是以矩阵(cv::Mat)作为基本数据的,Mat结构既可以帮你管理内存、像素信息,还支持常见的矩阵运算。
2.彩色图含有R,G,B三个通道,每个通道占8个bit(unsigned char),故称为8UC3(8位unsigned char 3)结构。而深度图则是单通道的图像,每个像素由16个bit组成(也就是C++里的unsigned short),像素值代表该点离传感器的距离,注意尺度。因此,rgb是8UC3彩色通道,depth是16US1
4.图像配准,估计俩帧之间的位姿
假设我们有俩个帧:F1和F2,并且,我们已经提取了俩组一一对应的特征点:
我们都是为了求出一个旋转矩阵R和位移矢量t,使得
但是实际上是不可能完全相等的,这里我们可以建立一个最小二乘,来进行优化,得到R,t,其中R,t是决策变量,使得下面的误差最小:
上面的式子是将所有的匹配特征点(是一个向量来着)求欧几里得距离,然后再求和。可以用ICP算法进行求解。
代码如下,在这里我们封装了俩个函数 computerKeyPointAndDesp()和estimateMotion(),其中computerKeyPointAndDesp()为计算每一帧的关键点,描述子,而estimationMotion()则是通过关键点描述子来进行匹配,筛选最合适匹配点,通过PnP来求出R和t。
(1)computerKeyPointAndDesp()函数代码如下:
void computeKeyPointsAndDesp(FRAME& frame) //函数传入为frame参数{//声明一个FeatureDetector类型的智能指针,用来指向存放在堆区的keypoints,特征器 cv::Ptr<cv::FeatureDetector> _detector; //声明一个DescriptorExtractor类型的智能指针,用来指向存放在堆区的描述子,提取器 cv::Ptr<cv::DescriptorExtractor> _descriptor; _detector=cv::FeatureDetector::create("ORB"); //ORB是特征器的类型 _descriptor=cv::DescriptorExtractor::create("ORB"); //ORB同上 if(!_detector||!_descriptor) { cout<<"unknow types"<<endl; return; } _detector->detect(frame.rgb,frame.kp);//设定好特征器类型后计算特征点,这里的提取keypoint存储到frame.kp中,frame.kp为vector<KeyPoint>类型的 _descriptor->compute(frame.rgb,frame.kp,frame.desp); //描述器,通过图像和特征点提取描述子,存储在frame.desp,这里的description是Mat类型的}
这段程序需要知道的知识:
1.要在一个图像提取特征,第一步是计算“关键点”,然后针对这些关键点周围的像素,计算其“描述子”。在OpenCV中,分别由FeatureDetector (API)和 DescritorExtractors (API)来实现,其中KeyPoint 包含了半径、角度等参数(可自行到响应的API查看)
对于上面的程序,我们进去看一下DescriptorExtractor类里面的内容(建议看一下函数的API):
class CV_EXPORTS DescriptorExtractor{public: virtual ~DescriptorExtractor(); void compute( const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors ) const; //对应frame.rgb、frame.kp、frame.desp void compute( const vector<Mat>& images, vector<vector<KeyPoint> >& keypoints, vector<Mat>& descriptors ) const; //我们不取这个 virtual void read( const FileNode& ); virtual void write( FileStorage& ) const; virtual int descriptorSize() const = 0; virtual int descriptorType() const = 0; static Ptr<DescriptorExtractor> create( const string& descriptorExtractorType ); //构建描述子提取器protected: ...};
(2)estimatemotion()函数代码如下:
首先封装一个数据结构:struct RESULT_OF_PNP{ Mat tves,rves; //由于计算PNP的时候,我们得到旋转向量和平移向量还有内点数量,所以我们可以建一个数据结构 int inliers;}
RESULT_OF_PNP estimateMotion(FRAME & frame1,FRAME&frame2,CAMERA_INTRINSIC_PARAMETERS &camera){ vector<cv::DMatch> matches; //matches是一个结构类型对象,包含很多数据类型 cv::DescriptionMatcher matcher; //关于DescriptionMatcher这个类的详细见官网API matcher.match(frame1.desp,frame2.desp,matches); //match是一个函数成员见下面 cout<<"find total"<<matches.size()<<"matches."<<endl; Mat imgMatchers;cv::drawMatches(frame1.rgb,frame1.kp,frame2.rgb,frame2.kp,matches,imgMatchers); //drawMatches函数,其中imgMatchers为Mat类型输出,matchers为vector<DMatch>类型 cv::imshow("good matches",imgMatchers);//显示图片,匹配到的图片 waitKey(0); int min_distance=999; vector<cv::DMatch> goodmatches; double good_match_threshold=10; for(size_t i=0;i<matches.size();i++) //遍历所有matches if(matches[i].distance<min_distance) { min_distance=matches[i].distance; //得出最小距离的matches } for(size_t j=0;j<matches.size();j++) //对于所有的matches,如果距离是小于最小距离的10倍的话,则被归为goodmatches if(matches[j].distance<good_match_threshold*min_distance) { goodmatches.push_back(matches[j]); } cout<<"good matches:"<<goodmatches.size()<<endl; cv::drawMatches(frame1.rgb,frame1.kp,frame2.rgb,frame2.kp,goodmatches,imgMatchers); imshow("good matches",imgMatchers); waitKey(0); vector<cv::Point3f> pts_obj; vector<cv::Point2f> pts_msg; for(size_t k=0;k<goodmatches.size();k++) //遍历所有的goodmatches { cv::Point2f p=frame1.kp[goodmatches[k].queryIdx].pt;//这里的.pt是Keypoint的一个数据成员,类型为Point2f,第一幅图片中对应的像素上的点goodmatches,queryIdx为源特征的索引,也就是第一张图片。 pts_msg.push_back(Point2f(frame2.kp[goodmatches[k].trainIdx].pt)); //第二幅图片中goodmatches对应的像素上的点 ushort d=frame1.depth.ptr<ushort>(int(p.y))[int(p.x)]; Point3f pt(p.x,p.y,d); Point3f pd=point2dto3d(pt,camera);//将第一副图像转换为空间点 pts_obj.push_back(pd); cout<<pts_obj.size()<<endl; } Mat cameraMatrix(3,3,CV_64F); Mat distortion(1,5,CV_64F,cv::Scalar::all(0)); cameraMatrix.at<double>(0,0)=camera.fx; cameraMatrix.at<double>(0,2)=camera.cx; cameraMatrix.at<double>(1,1)=camera.fy; cameraMatrix.at<double>(1,2)=camera.cy; cameraMatrix.at<double>(2,2)=1; Mat rvec,tvec, inliers; solvePnPRansac(pts_obj,pts_msg,cameraMatrix,distortion,rvec,tvec,false,100,1.0,100,inliers);//PnP求解,第一个参数为第一幅图像映射到空间点的结果,第二个参数为第二幅图像的二维空间点,参数在后面具体解释 RESULT_OF_PNP result; result.rvec=rvec; result.tvec=tvec; result.inliers=inliers.rows; vector<DMatch> matcheShow; for(size_t i=0;i<inliers.rows;i++)//遍历内点,找出内点的goodmatches { matcheShow.push_back(goodmatches[inliers.ptr<int>(i)[0]]); } cv::drawMatches(frame1.rgb,frame1.kp,frame2.rgb,frame2.kp,matcheShow,imgMatchers); imshow("inliers",imgMatchers); waitKey(0); return result;}
这段程序需要的知识:
1. _descriptor->compute(frame.rgb,frame.kp,frame.desp);
在keypoint计算描述子。描述子是一个cv::Mat 的矩阵结构,每一行代表一个对应于Keypoint的特征向量。当俩个描述子越相似,则关键点越像。
2.理解solvePnPRansac():
CV_EXPORTS_W void solvePnPRansac( InputArray objectPoints,//objectpoints(路标)空间点矩阵,3xN/NX3,N代表空间点的数量,空间点类型Pointf InputArray imagePoints,//空间点对应的图像坐标系的二维点矩阵,是2xN/Nx2矩阵,同理N为特征点数量,Poin2f类型 InputArray cameraMatrix, //相机内参 InputArray distCoeffs, //相机畸变 OutputArray rvec,//旋转向量,brings points from the model coordinate system(理解为世界坐标系) to the camera coordinate system(相机坐标系),即从空间坐标系的点转化到相机坐标系的点 OutputArray tvec, //平移向量 bool useExtrinsicGuess = false, int iterationsCount = 100, //迭代次数 float reprojectionError = 8.0,//重投影误差,RANSAC程序使用了inlier阈值,这个值是允许观测(即匹配)和计算(即计算重投影)差的最大值,如果小于这个值,则认为是内点 int minInliersCount = 100, //内点的数量,如果算法在一些状态找到比mininlierscount内点更多的点,则结束 OutputArray inliers = noArray(), //输出的矩阵包含了在空间点(Point3f)和二维像素点(Point2f)内点的索引,就是说通过这个矩阵,可以找到空间点和二维像素点的内点索引 int flags = ITERATIVE); //解决PnP问题的方法,这里是迭代的!
(3)紧接着,我们使用数据集(可自行下载)来验证我们的程序:完整代码如下(省去上述已经封装好的函数):
#include<iostream>#include<string>#include<fstream>#include<map>#include<vector>using namespace std;#include<opencv2/highgui/highgui.hpp>#include <opencv2/features2d/features2d.hpp>#include <opencv2/calib3d/calib3d.hpp>#include <opencv2/core/core.hpp>using namespace cv;#include<pcl/point_types.h>#include<pcl/common/transforms.h>#include <pcl/visualization/cloud_viewer.h>#include <pcl/io/pcd_io.h>//#include <pcl/filters/voxel_grid.h>typedef pcl::PointXYZRGBA PointT;typedef pcl::PointCloud<PointT> PointCloud;struct CAMERA_INTRINSIC_PARAMETERS{ double fx,fy,cx,cy,scale;};struct FRAME{ Mat rgb,depth; Mat desp; vector<cv::KeyPoint> kp;};struct RESULT_OF_PNP{ Mat rvec,tvec; int inliers;};//声明函数void computeKeyPointsAndDesp(FRAME& frame); RESULT_OF_PNP estimateMotion(FRAME& frame1,FRAME& frame2,CAMERA_INTRINSIC_PARAMETERS &camera);Point3f point2dto3d(Point3f& pt,CAMERA_INTRINSIC_PARAMETERS &camera);CAMERA_INTRINSIC_PARAMETERS camera; //声明一个相机内参的对象int main(){ camera.fx=325.5; camera.fy=253.5; camera.cx=518.0; camera.cy=519.0; camera.scale=1000; FRAME frame1,frame2; frame1.rgb=imread("/home/hansry/1.png"); frame1.depth=imread("/home/hansry/C_lib/data/depth_png/1.png",-1); frame2.rgb=imread("/home/hansry/2.png"); frame2.depth=imread("/home/hansry/C_lib/data/depth_png/2.png",-1); //“-1”,不改变图像载入 computeKeyPointsAndDesp(frame1); computeKeyPointsAndDesp(frame2); RESULT_OF_PNP result=estimateMotion(frame1,frame2,camera); cout<<"inliers:"<<" "<<result.inliers<<endl; cout<<"t:"<<" "<<result.tvec<<endl; cout<<"R:"<<" "<<result.rvec<<endl; return 0;}
OUTPUT如下:
第一张图片的点云:
第二张图片点云图:
直接匹配后得出的效果(比较杂乱,且匹配的点也比较多):
经过距离约束判断之后的效果:
通过内点阈值判断提取的matches:
t: [0.01429442692396542; 0.006648856410821061; 0.0008106287697152498]
R: [-0.0004893392316480649; 0.002091738380626495; -0.0001336475738645813]
至此,已完成俩帧的位姿估计
参考:http://www.cnblogs.com/gaoxiang12/p/4659805.html
- RGB-D SLAM——匹配篇(一)
- RGB-D SLAM——g2o篇(三)
- 视觉SLAM实战(一):RGB-D SLAM V2
- 视觉SLAM实战(一):RGB-D SLAM V2
- RGB-D SLAM——点云拼接篇(二)
- RGB-D SLAM——回环检测篇(四)
- SLAM(二)——RGB-D的含义
- 一起做RGB-D SLAM 第二季 (一)
- 调试笔记——《一起做RGB-D SLAM 》
- 一起做RGB-D SLAM(7) (完结篇)
- 一起做RGB-D SLAM(7) (完结篇)
- 一起做RGB-D SLAM(7) (完结篇)
- 一起做RGB-D SLAM(7) (完结篇)
- 一起做RGB-D SLAM(6)
- 一起做RGB-D SLAM(8)
- SLAM实战 RGB-D SLAM V2
- RGB-D SLAM环境配置
- rgb-d slam(2)笔记
- The container 'Maven Dependencies' references non existing library
- Hive四种数据导入方式
- Eclipse如何设置编码格式?(3种方式)
- C++之汇编内存访问
- Delphi 自制Office风格按键
- RGB-D SLAM——匹配篇(一)
- hdu 6216 A Cubic number and A Cubic Number
- Nginx优化图解
- solr(一)安装与基本使用
- HDU 6215 Brute Force Sorting
- YII框架第三方微博登录
- 将两个有序链表融合成一个有序链表
- 结构体的初始化问题
- 2017年9月17日 Ubuntu14.04安装&修复日志