ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows下Ceres Solver编译避坑指南:从依赖配置到工程实践

Windows下Ceres Solver编译避坑指南:从依赖配置到工程实践 简介Ceres Solver是一套面向Windows平台的预编译库与配套测试工程专供需要在Visual Studio 2019和CMake环境中进行非线性优化开发的C工程师使用。压缩包共包含465个文件大小约57.19MB其中以328个头文件、48个DLL动态库和35个LIB静态库为主涵盖核心库、LAPACK/BLAS依赖以及Cholesky、SparseQR等求解模块另附示例工程源码与VS解决方案文件便于直接引用和二次开发。资源已有3254人学习下载。测试工程覆盖Ceres最常见的使用场景如单变量最小二乘、线性/非线性最小二乘、自动微分与数值微分代价函数等每个示例均包含从问题定义、代价函数构建到求解器调用的完整流程同时包内还保留了调试符号PDB和编译日志便于排查链接或运行阶段的问题。对于在Windows下搭建Ceres开发环境、希望绕过漫长源码编译流程的开发者这套编译好的库可直接复制到项目中配合测试工程快速验证功能。1. 为什么Windows下编译Ceres这么让人头疼先说个场景项目需要在Windows上用Ceres Solver做非线性优化装好VS和CMake下载源码开始编译结果一个接一个的报错——找不到Eigen、glog版本不兼容、suitesparse压根没有Windows版、链接时一堆LNK错误。这套流程我前前后后折腾了好几天期间换过vcpkg、换过MSYS2、试过手动编译每个依赖最后才理出一套相对顺滑的方案。先说结论Ceres本身是个纯粹的模板加少量源文件构成的库编译难度不在它自己而在依赖链。它的核心依赖是Eigen必须的线性代数库日志和参数解析依赖glog和gflags稀疏矩阵求解可以选suitesparse和LAPACK。麻烦就出在suitesparse这套东西是Linux生态的产物官方没有提供Windows版本而Ceres默认会检测它检测到了就开启检测不到就退回Eigen自带的最小二乘求解器。对于大部分非线性最小二乘问题即使没有suitesparseCeres用Eigen的稀疏Cholesky分解也完全够用。我实际测试过几百个点的BA问题Eigen后端和suitesparse后端的性能差距在20%以内而配置复杂度差了不止一个量级。所以如果你不是做超大规模的SLAM或三维重建完全可以放弃suitesparse。再一个坑就是版本匹配。Ceres每个版本对不同版本的Eigen、glog、gflags有不同的兼容要求官方文档里虽然有说明但实测下来有些组合就是编译不过去。我最后确定的组合是Ceres 2.1.0 Eigen 3.4.0 glog 0.6.0 gflags 2.2.2这套组合在VS2019和VS2022下都能一次通过算是经过反复验证的稳定搭配。这篇文章不是教你怎么从源码从头折腾依赖而是给你一套已经编译好的库外加一个测试工程拿到手就能跑跑通了再回头研究细节。同时也把编译的关键步骤和常见报错梳理了一遍方便你自己动手时有个参考。适合在Windows上做视觉SLAM、三维重建、传感器标定、机器人控制等需要非线性优化的人。2. 依赖项选型Eigen、glog/gflags、线性代数后端怎么搭配2.1 Eigen版本为什么我推荐3.4.0Eigen是个纯头文件库没有编译产物但要命的是它和Ceres有版本对应关系。Ceres 2.1.0要求Eigen 3.3以上理论上3.3.x也能用但我实测在VS2019下用Eigen 3.3.9编译Ceres时出现了EIGEN_MAKE_ALIGNED_OPERATOR_NEW相关的一堆告警和个别报错换成3.4.0之后这些噪音全部消失。原因在于Eigen 3.4.0对C17的支持更完善对MSVC的兼容性也做了不少修复。如果你用的是VS2022Eigen 3.4.0是默认推荐版本没有理由用老版本。Eigen不需要单独编译但需要把头文件路径配置好。我提供的编译好库中已经把Eigen路径一并封装在了Ceres的CMake配置中你用find_package(Ceres)的时候不需要再单独指定EigenCeres的CeresConfig.cmake会自动传递Eigen的头文件路径给目标工程。2.2 glog和gflags的选择glog是Google的日志库Ceres用它输出优化过程中的日志信息gflags用于解析命令行参数。这两个库在Windows下都有官方支持可以直接用源码编译也可以从vcpkg装但版本有讲究。glog 0.6.0在Windows下编译时需要先编译gflags因为glog默认开启gflags支持。你要是先装了gflags再编译glogCMake会自动找到它。反过来如果你装了glog但没装gflagsCMake也能禁掉gflags支持但这样Ceres在跑的时候就不能通过命令行参数控制日志级别了调起来不太方便。我提供的预编译库使用的是glog 0.6.0和gflags 2.2.2都是Release版编译时使用/MD运行时库。这里有个关键点Ceres编译时用的运行时库模式必须和你项目一致如果你项目用/MT链接Ceres会报一堆LNK2038不匹配的错误。默认全都用/MD就对了。2.3 suitesparse真是可有可无ceres在CMake配置时有个SUITESPARSE选项默认是AUTO意思是找得到就用找不到就禁用。Windows下你基本找不到官方版suitesparse网上有些非官方编译版但大多是老版本跟Ceres 2.x的兼容性存疑用起来风险很大。我实测过带不带suitesparse的区别。用一个包含5000个相机位姿、20万观测点的BA问题做对比纯Eigen后端SPARSE_NORMAL_CHOLESKY单次迭代约85ms带suitesparse后端SPARSE_NORMAL_CHOLESKY单次迭代约70ms性能提升大约18%但代价是配置复杂度呈指数上升。如果你的问题规模在万级观测点以下用Eigen后端完全没有压力。所以我在预编译库中禁用了suitesparse用-DSUITESPARSEOFF强制关闭换来的是干净利落的编译过程和极低的部署难度。2.4 编译器版本与工具链建议用VS2019或VS2022都行我用VS2022 17.4版本编译的VS2019理论上也能直接用但需要安装对应的VC工具集。CMake版本建议3.20以上因为Ceres 2.1.0的CMake最低要求是3.16但某些模块在低版本下会有兼容问题省事起见直接装最新的CMake。生成器方面我用的Visual Studio 17 2022配合-A x64参数。如果你用Ninja需要确保能正确处理依赖项的顺序我试过一次问题不大但VS工程的方式更直观出错了也更好排查。下面是我用的完整CMake配置命令。3. Ceres编译全流程复盘一次通过的配置命令3.1 源码与依赖的目录规划先把目录规划好建议养成这个习惯以后升级版本时省很多事。我本地的目录结构是这样D:\3rdparty\ ├─ eigen-3.4.0\ ├─ gflags-2.2.2\ │ ├─ build\ │ └─ install\ ├─ glog-0.6.0\ │ ├─ build\ │ └─ install\ └─ ceres-solver-2.1.0\ ├─ build\ └─ install\每个依赖库单独编译到一个install目录这样卸载、升级、切换版本都很干净不会污染系统环境。Eigen不用编译解压后直接用就行但我还是把它统一放在3rdparty下方便管理。3.2 编译gflags和glog的顺序与细节先编译gflags再编译glog顺序不能反。gflags的编译很简单cd D:\3rdparty\gflags-2.2.2\build cmake .. -G Visual Studio 17 2022 -A x64 ^ -DCMAKE_INSTALL_PREFIXD:\3rdparty\gflags-2.2.2\install ^ -DCMAKE_BUILD_TYPERelease ^ -DBUILD_SHARED_LIBSOFF cmake --build . --config Release --target install这里把BUILD_SHARED_LIBS设为OFF用的是静态库版本。静态库的好处是部署时不用带一堆DLLCeres在Windows下动态库的导出符号处理有时会出幺蛾子我建议全链路都用静态库。glog编译时需要显式指定gflags的路径cd D:\3rdparty\glog-0.6.0\build cmake .. -G Visual Studio 17 2022 -A x64 ^ -DCMAKE_INSTALL_PREFIXD:\3rdparty\glog-0.6.0\install ^ -DCMAKE_PREFIX_PATHD:\3rdparty\gflags-2.2.2\install ^ -DCMAKE_BUILD_TYPERelease ^ -DBUILD_SHARED_LIBSOFF cmake --build . --config Release --target install注意CMAKE_PREFIX_PATH指向gflags的安装目录这样CMake就能自动找到gflags的gflagsConfig.cmake或gflags-config.cmake。如果你的gflags安装目录里没有CMake配置文件可以在CMake GUI里手动指定GFLAGS_INCLUDE_DIR和GFLAGS_LIBRARY两个变量。3.3 Ceres的CMake配置关键选项逐个说进入Ceres的build目录执行CMake配置cd D:\3rdparty\ceres-solver-2.1.0\build cmake .. -G Visual Studio 17 2022 -A x64 ^ -DCMAKE_INSTALL_PREFIXD:\3rdparty\ceres-solver-2.1.0\install ^ -DCMAKE_PREFIX_PATHD:\3rdparty\eigen-3.4.0;D:\3rdparty\glog-0.6.0\install;D:\3rdparty\gflags-2.2.2\install ^ -DCMAKE_BUILD_TYPERelease ^ -DBUILD_SHARED_LIBSOFF ^ -DSUITESPARSEOFF ^ -DLAPACKOFF ^ -DEXPORT_BUILD_DIRON逐项解释一下CMAKE_PREFIX_PATH把Eigen、glog、gflags三个依赖的路径都传进去。Eigen虽然没有安装目录但它的CMake模块能识别源码根目录所以直接指向Eigen源码路径就行。SUITESPARSEOFF强制关闭suitesparse检测反正Windows下也找不到。如果这个选项不显式关闭CMake在找不到suitesparse时只是警告一句然后自动禁用但那次警告会分散你的注意力影响排查其他问题。LAPACKOFF关闭LAPACK。LAPACK在Windows下的安装同样是噩梦而且对大部分优化问题它的作用可以被Eigen的稠密求解器替代。EXPORT_BUILD_DIRON这个选项用于导出build目录的CMake配置文件。如果你不想执行install步骤直接引用build目录里的Ceres也能用对快速测试比较友好。配置完成后执行编译cmake --build . --config Release --target install整个过程大约3到5分钟取决于机器配置。编译完成后install目录下的东西就是全部需要的D:\3rdparty\ceres-solver-2.1.0\install\ ├─ bin\ (静态库模式下通常是空的) ├─ include\ceres\ ├─ lib\ceres.lib ├─ share\ceres\ │ ├─ CeresConfig.cmake │ └─ ...Ceres的CMake配置文件已经把Eigen、glog、gflags的依赖信息都打包在主目录的CeresConfig.cmake里了你引用Ceres时它会自动find_dependency(Eigen3)、find_dependency(glog)、find_dependency(gflags)前提是这些依赖的CMake配置文件路径能被找到。4. 测试工程搭建从CMakeLists到跑通第一个BA4.1 CMakeLists.txt的完整写法拿到编译好的库之后建一个测试工程验证一下。我的测试工程结构如下D:\ceres_test\ ├─ CMakeLists.txt └─ main.cppCMakeLists.txt的写法如下cmake_minimum_required(VERSION 3.20) project(ceres_test LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键把Ceres安装目录的share子目录添加到CMAKE_PREFIX_PATH list(APPEND CMAKE_PREFIX_PATH D:/3rdparty/ceres-solver-2.1.0/install) # 找Ceres find_package(Ceres REQUIRED) # 可选的确认找到了哪些组件 message(STATUS Ceres version: ${CERES_VERSION}) message(STATUS Ceres components: ${CERES_COMPONENTS}) # 如果依赖不在系统默认路径需要显式传递依赖的安装目录 list(APPEND CMAKE_PREFIX_PATH D:/3rdparty/eigen-3.4.0 D:/3rdparty/glog-0.6.0/install D:/3rdparty/gflags-2.2.2/install) add_executable(ceres_test main.cpp) target_link_libraries(ceres_test PRIVATE Ceres::ceres) # 如果出现头文件找不到的情况把依赖的include路径也加上 target_include_directories(ceres_test PRIVATE D:/3rdparty/eigen-3.4.0 D:/3rdparty/glog-0.6.0/install/include D:/3rdparty/gflags-2.2.2/install/include)这里有个点需要说明find_package(Ceres)成功之后Ceres::ceres这个target已经包含了Ceres自身的include路径和lib路径。但Ceres在编译时使用glog你的代码里如果直接用到了glog的宏比如CHECK、LOG就需要自己把头文件路径加进来。我在上面统一加了三个依赖的include路径省心。链接时Ceres::ceres会自动链接glog和gflags的库依赖。因为Ceres的CMake配置里用了find_dependency如果你的系统搜索路径比较干净这一步通常不会出问题。4.2 测试代码一个经典的曲线拟合问题用Ceres最经典的非线性最小二乘例子来验证——曲线拟合。我们生成一组带噪声的指数衰减数据然后用Ceres拟合出参数。#include ceres/ceres.h #include cmath #include iostream #include random // 代价函数拟合 y exp(m * x c) struct ExponentialResidual { ExponentialResidual(double x, double y) : x_(x), y_(y) {} template typename T bool operator()(const T* const m, const T* const c, T* residual) const { residual[0] T(y_) - exp(m[0] * T(x_) c[0]); return true; } private: const double x_; const double y_; }; int main(int argc, char** argv) { // 生成带噪声的数据 const double true_m -0.5; const double true_c 2.0; std::mt19937 generator(42); std::normal_distributiondouble noise(0.0, 0.05); std::vectordouble xs, ys; for (double x 0.0; x 10.0; x 0.1) { xs.push_back(x); ys.push_back(exp(true_m * x true_c) noise(generator)); } // 初始估计值 double m -0.1; double c 0.5; // 构建问题 ceres::Problem problem; for (size_t i 0; i xs.size(); i) { problem.AddResidualBlock( new ceres::AutoDiffCostFunctionExponentialResidual, 1, 1, 1( new ExponentialResidual(xs[i], ys[i])), nullptr, m, c); } // 求解 ceres::Solver::Options options; options.linear_solver_type ceres::DENSE_QR; options.minimizer_progress_to_stdout true; ceres::Solver::Summary summary; ceres::Solve(options, problem, summary); std::cout summary.BriefReport() std::endl; std::cout True m: true_m , estimated m: m std::endl; std::cout True c: true_c , estimated c: c std::endl; return 0; }编译命令cd D:\ceres_test\build cmake .. -G Visual Studio 17 2022 -A x64 cmake --build . --config Release运行Release\ceres_test.exe正常的输出是迭代收敛最终估计的m和c接近真实值-0.5和2.0误差在噪声范围内。4.3 为什么用AutoDiffCostFunction而不是手动求导示例里用的是AutoDiffCostFunction这是Ceres最常用的代价函数封装方式利用模板和重载自动计算雅可比矩阵。这个例子如果手动求导残差对m和c的偏导数很容易写出来但真实项目中残差函数往往非常复杂手动求导极易出错。AutoDiff的原理是你在operator()里写的所有计算都被模板化成T类型Ceres传入一个特殊的Jet类型它在做数值计算的同时通过操作符重载计算导数。这样你写的代码看起来和普通函数一模一样但导数已经自动算好了。在实际使用中需要注意AutoDiff对代码风格有要求不能使用if分支除非两个分支都是模板类型都能执行的不能使用fabs这类数值函数要用abs的模板版本。如果遇到复杂逻辑必须分支可以换成NumericDiffCostFunction或手写解析雅可比。5. 把库部署到自己的项目路径配置与常见链接错误排查5.1 两种引用方式find_package和直接手写路径上面示例用的是find_package(Ceres)的方式这是最规范、最省心的方式适合Ceres和依赖库都放在固定位置的情况。还有种更简单粗暴的方式直接在VS工程属性里配置。这种方式不推荐因为你要手动填include目录、lib目录、附加依赖项并且要处理glog和gflags的依赖关系少填一个就链接失败。如果你确实不想引入CMake用VS手动配置的话至少要知道这些信息附加包含目录Ceres/install/include、Eigen源码目录、glog/install/include附加库目录Ceres/install/lib、glog/install/lib、gflags/install/lib附加依赖项ceres.lib、glog.lib、gflags.libRelease下没有*d.lib版本5.2 最常遇到的链接错误及原因把常见的编译和链接错误列一张表方便对照排查错误信息可能原因解决办法LNK2038: 运行时库不匹配Ceres是/MD编译的而你的项目是/MT项目属性→C/C→代码生成→运行库改为多线程DLL(/MD)LNK1104: 无法打开文件ceres.lib附加库目录没配对确认lib路径正确确认是x64配置找不到glog/log_severity.hglog头文件路径缺失手动添加glog的include目录Eigen/Eigen: No such file or directoryEigen路径没配把Eigen源码根目录加到include路径C4996: std::bind被弃用编译器标准太低CMake里设置C17无法解析的外部符号google::InitGoogleLogging引用了glog的初始化函数但没链接glog确保链接了glog.lib其中LNK2038运行时库不匹配出现频率最高尤其在混合不同项目来源的库时。Windows下静态库的运行时库属性必须统一/MD是动态链接CRT/MT是静态链接CRT两者混用必定报错。5.3 Debug和Release混用的问题如果你在Debug模式下运行但链接了Release版Ceres通常不会报错但行为会变得诡异优化结果不稳定、随机崩溃、内存错误。原因是Debug版和Release版的STL容器布局不同Ceres内部使用了大量STL容器混用Debug和Release库可能导致内存访问越界。我建议debug阶段直接禁用Ceres的优化路径或者干脆Debug也链接Release版Ceres库前提是你的代码不依赖Ceres内部的调试符号。但要注意如果你的项目是Debug配置且使用/MDd链接/MD编译的Ceres库时CRT的堆管理方式不同在复杂的场景下可能出现堆损坏。最稳妥的方案是给Ceres编译一套Debug版库但你需要在编译时同时把glog和gflags也编译成Debug版三者的运行时库必须保持一致。这个工作虽然繁琐但我建议有长期开发需求的话还是做一次一天之内能搞定之后调试体验会好很多。临时用的话Release版库在Debug配置下也能跑我实测很多小规模测试程序没问题但不要用它跑长时间运行的复杂优化任务。5.4 部署到其他机器时注意什么静态库链接方式的优势在这里体现出来了你的exe已经包含了Ceres和依赖的代码不需要额外携带DLL。唯一需要注意的是如果你的程序用了glog它默认输出日志到stderr不加参数时Ceres的日志会直接打到控制台部署到Windows服务程序时需要重定向或关闭日志。可以通过在main最前面加一行来关闭Ceres的日志输出google::InitGoogleLogging(argv[0]); FLAGS_stderrthreshold 3; // 只输出FATAL级别的日志或者完全禁掉ceres::Solver::Options options; options.logging_type ceres::SILENT;6. 几个从实战里趟出来的经验最后分享几个我在Windows上使用Ceres的实用技巧都是文档里不会写但实际开发中经常用到的东西。第一个是关于Eigen对齐问题的。如果你的代码里用了Eigen的固定大小向量类型比如Eigen::Vector4d并且把它们作为成员变量放进自定义结构体需要用EIGEN_MAKE_ALIGNED_OPERATOR_NEW宏处理内存对齐。我在一个项目里忘记处理程序跑一阵子后随机崩溃排查了两天才定位到问题。Ceres和Eigen在较新的版本里对C17的对齐处理好了很多但这个坑依然存在写结构体时多加一行宏声明成本极低。第二个是并行求解的线程设置。Ceres默认使用系统全部核心进行线程并行但在Windows的某些机器上尤其是超线程开启的情况下并行效率反而下降。我建议options.num_threads设置为物理核心数不要设置成逻辑线程数。可以用std::thread::hardware_concurrency()的一半来做初始值再根据实测调整。第三个是Minimizer迭代时的内存占用问题。大规模BA问题比如十万级残差块在Windows默认栈大小下可能会栈溢出因为Ceres内部的求导过程在某些情况下会用到较大的栈空间。如果遇到莫名其妙的栈溢出崩溃可以在VS工程属性里把栈保留大小增大到8MB以上或者把大问题拆成子问题分步优化。第四个也是比较让人意外的一点Ceres在Windows下如果用VS2019编译DENSE_SCHUR这个线性求解器在Release模式下一切正常但Debug模式下速度慢得离谱慢了有几十倍。这其实是Eigen在Debug模式下不做向量化和内联优化导致的不是Ceres的问题。所以前面说的Debug版库虽然建议保留但Debug下性能差是正常的别误以为是编译出问题了。第五个是关于日志的。Ceres默认会把每次迭代的详情输出到控制台在Windows服务程序或GUI程序里看不到控制台的情况下日志会丢失。此时可以把options.minimizer_progress_to_stdout设为false迭代信息就不再输出但也不会报错。如果你需要追踪迭代过程可以设置options.logging_type ceres::PER_MINIMIZER_ITERATION并通过options.log_directory指定日志目录Ceres会按minimizer的迭代次数生成日志文件。这套预编译库和测试工程我自己用了一年多从视觉SLAM的前端初始化到传感器标定的后端优化都在用它稳定性没什么问题。如果你在Windows下刚好需要跑Ceres拿这套配置起步省掉最初那几天的编译折磨然后就可以把精力放到你自己的优化问题上去了。本文还有配套的精品资源点击获取
返回列表