转载请注明出处:http://my.csdn.net/ye_shen_wei_mian
近期初步接触并初步学习了有关SLAM的知识,发现g2o库在SLAM中有非常的重要的应用,特别是应用在以图的形式表达的bundle adjustment的优化。经过一段时间的学习,发现大部分研究和使用g2o的人都是基于Linux系统下的,感觉这和g2o在Windows系统下的非常难配置不无关系。g2o库大致依赖于Qt5,Eigen,CSparse,cholmod等库,而其中以Cholmod库在Windows里尤其难配置,而错综复杂的依赖关系又涉及到了例如AMD,BTF,CAMD,CCOLAMD,COLAMD,KLU,CXSparse,SPQR等等等等,。在ubuntu底下,这些问题相对还容易解决,个人的经验是通过dpkg的命令一个一个依赖库进行安装。而在Windows系统下这些问题更加棘手。
在网上搜寻了很久资料,大致发现两篇很有价值的博客,可供参考。本人也是基于这两篇博客的基础上对g2o库在WIndows系统+Visual Studio下进行生成的。一篇博客是cc_sunny的关于SLAM的那些事——通用图优化(G2O)环境搭配(windows8.1 vs2013),网址是http://blog.csdn.net/aptx704610875/article/details/51245143。而另外一篇博客是xiamentingtao的博客 Eigen+suitesparse for windows 安装 ,网址是 http://m.blog.csdn.net/article/details?id=50100549。
值得注意的是,本人刚开始也是根据cc_sunny的博客对g2o进行生成的,生成的过程很顺利没有出什么问题,但是后来跑实际的代码的时候发现,作者遗漏了结合Cholmod库对g2o库进行编译,所以这样的话,g2o库中与cholmod库有关的函数和Optimizer都不能使用了,而与Cholmod库有关的g2o库功能恰恰又非常有用和常用,所以务必需要找到能够结合Cholmod库一起对g2o进行生成的方法。
但是从网上获取到的信息看,单独对Cholmod库进行生成貌似非常艰难。然而万幸的是,从xiamentingtao的博客 Eigen+suitesparse for windows 安装 一文中,惊奇的发现suitesparse库中已经包含了cholmod库,而且github上已经有可以基于Windows系统对suitesparse-metis库进行生成的开源项目。于是安装cholmod库的任务可以转化成在windows系统下安装suitesparse库了。
不得不说上述这两篇的博客体现了各自作者非常用心和仔细的态度,在这里我也对这两位博主的用心付出表示感谢,按照他们的教程的步骤进行配置和操作,可以顺利的完成所要达到的目的,因此相应的步骤我在这里并不再赘述了,只对这两个教程的东西进行融会贯通和补充说明,以及阐述总体流程以及一些别的注意事项。因此具体的步骤请翻阅这两篇博客,一步一步的操作相应的步骤即可。
本人电脑配置:Win7+VS2010 以及 Win10+ VS2013都成功配置好了,cmake用的是cmake3的版本,我配置的是Win32的版本,64位的版本流程大致相同
总体流程:
(1)上Eigen的网站下载Eigen3的库,解压到自己的电脑中,根据我的发现,Eigen库是不需要build的,解压出来,配置好属性表就可以了,这个不难。
(2)进行suitesparse-metis库的安装。
请翻阅 xiamentingtao的博客 Eigen+suitesparse for windows 安装 ,按照作者说的去下载 suitesparse-metis-for-windows 这个开源项目(网址:https://github.com/jlblancoc/suitesparse-metis-for-windows/tree/v1.3.0),虽然作者是用VS2008和cmake2.8,但是都无所谓。要仔细的按照作者所说的去解压路径以及修改CmakeLists.txt,CUDA用不用都行反正我是没用它所以也不知道CUDA7在这底下是不是真的如作者所说的有bug。另外,作者在步骤5.编译和安装那里选的是对INSTALL工程进行build,我建议最好对ALL_BUILD在Debug和Release下都进行build。而这样子做,作者说的第6部中的“ SuiteSparseConfig.cmake应该位于install路径下”就不会出现了,但是也并不太大关系一切还是正常的。而第6部那幅图也不是cmake的结果,应该是build完后的结果,不管它。
到这里suitesparse-metis库算是已经build完了。
然后把你解压出来的suitesparse-metis-for-windows-1.3.0\lapack_windows\x32(我用的是32位的所以选这个)下面的东西分别全部复制到 你解压出来的suitesparse-metis-for-windows-1.3.0\build\bin中的Debug和Release。
在环境变量路径Path中添加 你解压出来的suitesparse-metis-for-windows-1.3.0\build\bin\Debug 和 你解压出来的suitesparse-metis-for-windows-1.3.0\build\bin\Release
(3)进行g2o相关的依赖库和g2o自身的生成
这个时候我们就要翻看cc_sunny的关于SLAM的那些事——通用图优化(G2O)环境搭配(windows8.1 vs2013) 这篇博客了。
Note:cc_sunny的博客里面对各个库都进行了32位和64位版本都进行了build,但如果你只是想要32位或只是想要64位的,那么可以只对32或64进行Build即可。出于本人个人的需要,本人只Build了32位的版本。
第一步的编译Eigen个人认为就不需要做了,没有安装Qt5版本的朋友可以按照博主的博客对Qt5和它的VS插件的过程进行安装,而libQGLViewer_2.6.3只生成32位的版本的情况下,libQGLViewer生成成功后,博主所说的那些lib和dll文件都在 你解压libQGLVIewer的目录\libQGLViewer-2.6.3\QGLViewer文件夹里,在之后在cmake中对g2o进行配置的时候,相应于libQGLViewer的路径和lib文件就在这个目录里找到就行了。
这个时候来到了这篇博客的重头戏了,毫无疑问就是在cmake对g2o进行配置了。其实相当大的部分的配置步骤和cc_sunny博客中的g2o的cmake步骤并无差异,但是这个时候我们要在cmake中配置好之前步骤(2)中已经build好的suitesparse-metis库,这也是我们这篇博客的主要目的。
首先还是按照cc_sunny的博客在cmake中对g2o除了suitesparse-metis库以外的库进行配置。总体并无太大的差别,只有Eigen库的cmake配置有一点小出入,现在只要在Ungrouped Entries中的EIGEN3_INCLUDE_DIR配置对Eigen解压出来的那个文件夹的路径即可,我的是 F:/Eigen3/eigen3。然后按cc_sunny的方法把QGLVIEWER等都配置好。CSPARSE可以不用改它,用cmake默认的g2o里面的版本就行。
BLAS库我没有配置,有兴趣的朋友可以配置下看看。
而这里重点当然是CHOLMOD这个地方的配置啦。CHOLMOD_INCLUDE_DIR应当是你解压了的suitesparse-metis-for-windows-1.3.0\SuiteSparse\CHOLMOD\Include这个路径,然后CHOLMOD_LIBRARY应当是suitesparse-metis-for-windows-1.3.0\build\Debug\libcholmodd.lib这个文件。
注意!!!极其重要:使用VS2010来操作的朋友切记不能勾选G2O下的G2O_USE_OPENMP,否则之后会报一个有关于OpenMPMutex的错误又要删掉build好的g2o库重新再build一次,挺崩溃的,我也不知道为什么会这样。VS2013的朋友则没这个烦恼。
做好了上面的步骤以后,点Configure,这个时候应该只有Ungrouped Entries这一项有红色,发现Ungrouped Entries要我们填写AMD_LIBRARY这个库(.lib文件)的路径,还是回去suitesparse-metis-for-windows-1.3.0\build\Debug\这里找,找到libamdd.lib这个文件配置好。
再点Configure,发现还是像刚才的情况,只不过这个时候要我们配置COLAMD_LIBRARY的路径。配置好以后再按Configure再进行配置,不断地重复这一过程,直到最后Ungrouped Entries不再出现红色框为止。最后cmake中应当是这样的:
这样才算是在cmake中把g2o给配置好了,点击Generate。
按照cc_sunny的第5、6步,在VS下对g2o.sln进行build然后添加环境变量即可。
(4)在VS下配置suitesparse-metis库
这部就需要注意了,由于刚才是ALL_BUILD而不是INSTALL的关系,因此配置过程与这博客中略有不同。首先在属性管理器页面中debug和release下分别新建suitesparse_debug_v1和suite_release_v1属性表(名字喜欢怎么取都可以,不一定要和我一样),在这两个属性表中,我们将要配置suitesparse和metis库。如图:(g2o_debug和g2o_release同理,是用来配置g2o的,图里面PCL的属性表不要管它)
点开suitesparse_debug_v1,首先配置VC++目录底下的包含目录。本人是将suitesparse-metis的压缩包直接解压到F:\suitesparse文件夹的,所以我要配置的东西有这么多,如下图:
东西有点多,但务必一个一个弄好,值得注意的是上图倒数第三行的SuiteSparse_config目录虽然没有Include文件夹,但翻看目录底下发现其实这个目录下是有一个头文件的,不配置的话之后会报没找到这个头文件的错。这里也可以看到涉及库之多以及依赖关系的混乱。
然后配置库目录:(下图是Debug的属性表下的,Release属性表下就把第一行最后的那个Debug改成Release就可以了)
然后配置链接器->附加依赖项:
Debug属性表下:
libamdd.lib
libbtfd.lib
libcamdd.lib
libccolamdd.lib
libcholmodd.lib
libcolamdd.lib
libcxsparsed.lib
libklud.lib
libldld.lib
libspqrd.lib
libumfpackd.lib
suitesparseconfigd.lib
libblas.lib
liblapack.lib
metisd.lib
Release属性表下:
libamd.lib
libbtf.lib
libcamd.lib
libccolamd.lib
libcholmod.lib
libcolamd.lib
libcxsparse.lib
libklu.lib
libldl.lib
libspqr.lib
libumfpack.lib
metis.lib
suitesparseconfig.lib
libblas.lib
liblapack.lib
到这里,suitesparse-metis库在VS里的配置就算完成了。
(5)g2o库在VS下的配置。因为较为简单,也就是像之前那样在Debug和Release下分别配置属性表里的包含目录、库目录和依赖项。我就不多说了,截个Debug模式下的图吧,大家照着配置就行。
附加依赖项:
Debug:
g2o_interface_d.lib
g2o_parser_d.lib
g2o_simulator_d.lib
g2o_types_sim3_d.lib
g2o_types_icp_d.lib
g2o_types_slam3d_addons_d.lib
g2o_types_sba_d.lib
g2o_types_slam2d_addons_d.lib
g2o_types_slam3d_d.lib
g2o_calibration_odom_laser_d.lib
g2o_solver_slam2d_linear_d.lib
g2o_types_data_d.lib
g2o_types_sclam2d_d.lib
g2o_solver_structure_only_d.lib
g2o_types_slam2d_d.lib
g2o_solver_pcg_d.lib
g2o_solver_dense_d.lib
g2o_solver_eigen_d.lib
g2o_tutorial_slam2d_d.lib
g2o_viewer_d.lib
g2o_hierarchical_d.lib
g2o_solver_csparse_d.lib
g2o_cli_d.lib
g2o_core_d.lib
g2o_stuff_d.lib
g2o_csparse_extension_d.lib
g2o_ext_freeglut_minimal_d.lib
g2o_opengl_helper_d.lib
g2o_ext_csparse_d.lib
Release:
g2o_simulator.lib
g2o_types_sim3.lib
g2o_types_icp.lib
g2o_types_slam3d_addons.lib
g2o_types_sba.lib
g2o_types_slam3d.lib
g2o_types_slam2d_addons.lib
g2o_calibration_odom_laser.lib
g2o_solver_slam2d_linear.lib
g2o_types_data.lib
g2o_types_sclam2d.lib
g2o_types_slam2d.lib
g2o_solver_pcg.lib
g2o_solver_structure_only.lib
g2o_solver_eigen.lib
g2o_solver_dense.lib
g2o_tutorial_slam2d.lib
g2o_viewer.lib
g2o_hierarchical.lib
g2o_solver_csparse.lib
g2o_cli.lib
g2o_core.lib
g2o_stuff.lib
g2o_interface.lib
g2o_csparse_extension.lib
g2o_ext_freeglut_minimal.lib
g2o_parser.lib
g2o_ext_csparse.lib
g2o_opengl_helper.lib
这样g2o在VS下是配置好了。这里的最后一步是把g2o build的目标文件夹下,也就是下图的where to build the binaries的路径对应的那个文件夹下的g2o文件夹里有个config.h头文件,把它复制粘贴到where to build the source code对应的那个文件夹下的g2o文件夹底下。
(6)写到这里写的我自己也有点小崩溃,但是现在终于到了守得云开见月明的时候了。稍微改动了下高博的博客 深入理解图优化与g2o:g2o篇(网址:http://www.cnblogs.com/gaoxiang12/p/5304272.html) 里的代码,不然在VS2010下会出错,VS2013可以直接用高博的代码没问题。
/** * BA Example * Author: Xiang Gao * Date: 2016.3 * Email: [email protected] * * 在这个程序中,我们读取两张图像,进行特征匹配。然后根据匹配得到的特征,计算相机运动以及特征点的位置。这是一个典型的Bundle Adjustment,我们用g2o进行优化。 */ // for std #include <iostream> // for opencv #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/features2d/features2d.hpp> #include <boost/concept_check.hpp> // for g2o #include <g2o/core/sparse_optimizer.h> #include <g2o/core/block_solver.h> #include <g2o/core/robust_kernel.h> #include <g2o/core/robust_kernel_impl.h> #include <g2o/core/optimization_algorithm_levenberg.h> #include <g2o/solvers/cholmod/linear_solver_cholmod.h> #include <g2o/types/slam3d/se3quat.h> #include <g2o/types/sba/types_six_dof_expmap.h> using namespace std; using namespace cv; // 寻找两个图像中的对应点,像素坐标系 // 输入:img1, img2 两张图像 // 输出:points1, points2, 两组对应的2D点 int findCorrespondingPoints(const cv::Mat& img1, const cv::Mat& img2, vector<cv::Point2f>& points1, vector<cv::Point2f>& points2); // 相机内参 double cx = 325.5; double cy = 253.5; double fx = 518.0; double fy = 519.0; int main() { // 读取图像 cv::Mat img1 = cv::imread("near.jpg",-1); cv::Mat img2 = cv::imread("far.jpg",-1); // 找到对应点 vector<cv::Point2f> pts1, pts2; if (findCorrespondingPoints(img1, img2, pts1, pts2) == false) { cout << "匹配点不够!" << endl; return 0; } cout << "找到了" << pts1.size() << "组对应特征点。" << endl; // 构造g2o中的图 // 先构造求解器 g2o::SparseOptimizer optimizer; // 使用Cholmod中的线性方程求解器 g2o::BlockSolver_6_3::LinearSolverType* linearSolver = new g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>(); // 6*3 的参数 g2o::BlockSolver_6_3* block_solver = new g2o::BlockSolver_6_3(linearSolver); // L-M 下降 g2o::OptimizationAlgorithmLevenberg* algorithm = new g2o::OptimizationAlgorithmLevenberg(block_solver); optimizer.setAlgorithm(algorithm); optimizer.setVerbose(false); // 添加节点 // 两个位姿节点 for (int i = 0; i<2; i++) { g2o::VertexSE3Expmap* v = new g2o::VertexSE3Expmap(); v->setId(i); if (i == 0) v->setFixed(true); // 第一个点固定为零 // 预设值为单位Pose,因为我们不知道任何信息 v->setEstimate(g2o::SE3Quat()); optimizer.addVertex(v); } // 很多个特征点的节点 // 以第一帧为准 for (size_t i = 0; i<pts1.size(); i++) { g2o::VertexSBAPointXYZ* v = new g2o::VertexSBAPointXYZ(); v->setId(2 + i); // 由于深度不知道,只能把深度设置为1了 double z = 1; double x = (pts1[i].x - cx) * z / fx; double y = (pts1[i].y - cy) * z / fy; v->setMarginalized(true); v->setEstimate(Eigen::Vector3d(x, y, z)); optimizer.addVertex(v); } // 准备相机参数 g2o::CameraParameters* camera = new g2o::CameraParameters(fx, Eigen::Vector2d(cx, cy), 0); camera->setId(0); optimizer.addParameter(camera); // 准备边 // 第一帧 vector<g2o::EdgeProjectXYZ2UV*> edges; for (size_t i = 0; i<pts1.size(); i++) { g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV(); edge->setVertex(0, dynamic_cast<g2o::VertexSBAPointXYZ*> (optimizer.vertex(i + 2))); edge->setVertex(1, dynamic_cast<g2o::VertexSE3Expmap*> (optimizer.vertex(0))); edge->setMeasurement(Eigen::Vector2d(pts1[i].x, pts1[i].y)); edge->setInformation(Eigen::Matrix2d::Identity()); edge->setParameterId(0, 0); // 核函数 edge->setRobustKernel(new g2o::RobustKernelHuber()); optimizer.addEdge(edge); edges.push_back(edge); } // 第二帧 for (size_t i = 0; i<pts2.size(); i++) { g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV(); edge->setVertex(0, dynamic_cast<g2o::VertexSBAPointXYZ*> (optimizer.vertex(i + 2))); edge->setVertex(1, dynamic_cast<g2o::VertexSE3Expmap*> (optimizer.vertex(1))); edge->setMeasurement(Eigen::Vector2d(pts2[i].x, pts2[i].y)); edge->setInformation(Eigen::Matrix2d::Identity()); edge->setParameterId(0, 0); // 核函数 edge->setRobustKernel(new g2o::RobustKernelHuber()); optimizer.addEdge(edge); edges.push_back(edge); } cout << "开始优化" << endl; optimizer.setVerbose(true); optimizer.initializeOptimization(); optimizer.optimize(10); cout << "优化完毕" << endl; //我们比较关心两帧之间的变换矩阵 g2o::VertexSE3Expmap* v = dynamic_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(1)); Eigen::Isometry3d pose = v->estimate(); cout << "Pose=" << endl << pose.matrix() << endl; // 以及所有特征点的位置 for (size_t i = 0; i<pts1.size(); i++) { g2o::VertexSBAPointXYZ* v = dynamic_cast<g2o::VertexSBAPointXYZ*> (optimizer.vertex(i + 2)); cout << "vertex id " << i + 2 << ", pos = "; Eigen::Vector3d pos = v->estimate(); cout << pos(0) << "," << pos(1) << "," << pos(2) << endl; } // 估计inlier的个数 int inliers = 0; for (vector<g2o::EdgeProjectXYZ2UV*>::iterator e = edges.begin();e!=edges.end();++e) { (*e)->computeError(); // chi2 就是 error*\Omega*error, 如果这个数很大,说明此边的值与其他边很不相符 if ((*e)->chi2() > 1) { cout << "error = " << (*e)->chi2() << endl; } else { inliers++; } } cout << "inliers in total points: " << inliers << "/" << pts1.size() + pts2.size() << endl; optimizer.save("ba.g2o"); std::system("pause"); return 0; } int findCorrespondingPoints(const cv::Mat& img1, const cv::Mat& img2, vector<cv::Point2f>& points1, vector<cv::Point2f>& points2) { cv::ORB orb; vector<cv::KeyPoint> kp1, kp2; cv::Mat desp1, desp2; orb(img1, cv::Mat(), kp1, desp1); orb(img2, cv::Mat(), kp2, desp2); cout << "分别找到了" << kp1.size() << "和" << kp2.size() << "个特征点" << endl; cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create("BruteForce-Hamming"); double knn_match_ratio = 0.8; vector< vector<cv::DMatch> > matches_knn; matcher->knnMatch(desp1, desp2, matches_knn, 2); vector< cv::DMatch > matches; for (size_t i = 0; i<matches_knn.size(); i++) { if (matches_knn[i][0].distance < knn_match_ratio * matches_knn[i][1].distance) matches.push_back(matches_knn[i][0]); } if (matches.size() <= 20) //匹配点太少 return false; for (vector< cv::DMatch >::iterator m = matches.begin();m!=matches.end();++m) { points1.push_back(kp1[(*m).queryIdx].pt); points2.push_back(kp2[(*m).trainIdx].pt); } return true; }
至于代码里的near.jpg和far.jpg就别找我要啦,随便两张不同角度拍摄的图片都可以,找对应的特征点匹配而已。
跑下如无意外应该就能成功了。
嗯这篇博客就到此为止啦,希望大家都能顺利地在Windows+VS底下用上g2o库拉。谢谢观看。