Windows下Ceres编译实战:预编译库与CMake集成
简介在Windows平台使用Visual Studio 2019编译Ceres Solver通常需要解决依赖库、CMake配置与链接选项等问题这套预编译库与配套测试工程可以帮助开发者避免重复构建直接进入算法集成环节。压缩包共465个文件包含约328个头文件、48个动态链接库、35个静态库/导入库以及Visual Studio解决方案、工程文件和部分编译中间文件整体大小约57.19MB。测试工程给出了SimplestExample、LinearLeastSquares、NonlinearLeastSquares、AutodiffCostFunction、NumericDiffCostFunction等典型示例完整呈现从构造代价函数、选择求解策略到调用Ceres求解器的流程便于理解非线性最小二乘问题的建模与求解。目前已有3254人学习使用适合在Windows下做参数估计、数据拟合或数值优化的C开发者快速上手省去手工编译大量依赖项的时间。 Ceres Solver在Windows下的编译是不少做SLAM、三维重建和机器人优化的同学绕不过去的一道坎。很多时候你只是想跑通一个非线性最小二乘的demo或者把Ceres作为依赖接进现有Windows工程结果一编译就是一天Eigen版本对不上、glog链接报错、SuiteSparse在Windows下压根编不过去最后心态直接崩。我这篇分享就围绕Windows下Ceres编译好的库和配套测试工程展开库是我已经编好并实测过的工程配置也是可复现的。适合刚入门非线性优化、想在Windows上搭Ceres环境或者打算把Ceres集成到自己项目里的人直接参考。1. 先弄清楚Ceres在Windows编译到底难在哪1.1 不是编译器不行是依赖链太长Ceres本身是纯C库官方对跨平台的支持并不差真正让Windows用户头疼的是它背后的依赖链。最核心的依赖是Eigen这个还好纯模板库只要头文件路径配对基本不会出大问题。麻烦的是可选依赖gflags、glog、SuiteSparse、LAPACK这些在Linux上一条apt命令就能装齐在Windows上每一个都要单独编译而且它们之间还有版本匹配关系。我印象最深的就是SuiteSparse。这个库在Linux下是稀疏求解器的标配但Windows上官方并不提供预编译包社区里也没有特别统一的构建脚本。如果你不开它Ceres会退化到用Eigen自带的稀疏求解器对于很多测试场景其实完全够用。所以我在编译这套库的时候策略很简单保留Eigen、关闭glog和SuiteSparse这些非必需项让依赖链尽量短。1.2 官方文档的小傲慢Ceres官方文档里Windows的编译说明一直写得比较简略很多配置参数要靠自己试。而且Ceres的CMake逻辑比较细会对Eigen版本做严格检查版本太新或太旧都会在configure阶段直接报错。加之社区里大多数人用Linux或macOS你遇到一个Windows特有的链接错误搜半天可能也找不到对症的答案。这就导致了一个很尴尬的现状Ceres本身并不难用难的是先把它在Windows上跑起来。我自己第一次编译也折腾了很久所以这次把编好的库和测试工程一起整理出来就是希望后来的人少走这段弯路。2. 构建方案选型自己编译、vcpkg还是直接拿成品2.1 三条路线的真实对比在决定直接用编译好的库之前我把另外两条常见路线也都试了一遍简单做个对比方案耗时难度可控性推荐场景直接源码编译至少半天踩坑另算中高高可裁剪依赖需要定制Ceres、更换Eigen版本、插桩调试vcpkg安装半小时到一小时低中portfile限制版本组合能接受包管理器想省心装整套依赖直接用预编译库10分钟低中高需匹配编译器和架构快速跑demo、Windows工程接依赖vcpkg方案本身不差但有一个比较现实的问题它编译出来的库默认带一大堆依赖而且如果你是把它接入一个已经存在的大型CMake工程包管理器的介入可能会打破你现有的第三方库目录约定。所以我在项目里最终还是选择了手动控制。2.2 我这套库的编译环境先说清楚适用前提避免拿去用的时候对不上。我编译这套库的基准环境是操作系统Windows 10 64位编译器MSVC 2019VS2019 16.11VS2022直接兼容CMake3.18以上Ceres版本2.1.0Eigen版本3.4.0构建架构x64运行时库/MD和/MDd动态CRT这个配置是目前Windows下最主流的组合。你如果用的是VS2017或者MinGW那我说实话不建议直接拿这套库因为MSVC生成的.lib和MinGW的链接格式不通用还是要自己编一份对应环境的。3. 编译好的库目录结构与CMake集成3.1 拿到手后应该看到什么解压之后建议放到某个固定的第三方库目录比如D:/ThirdParty。目录结构大概是这样的D:/ThirdParty/ ├── ceres-2.1.0/ │ ├── include/ceres/ # Ceres头文件 │ ├── lib/ # ceres.lib / ceres.dll │ └── share/Ceres/ # CeresConfig.cmake等CMake配置 ├── eigen-3.4.0/ │ ├── Eigen/ # Eigen主头文件 │ └── unsupported/ # Eigen扩展模块 └── test_ceres/ # 测试工程lib目录下同时放了静态库ceres.lib和动态库ceres.dll这是为了方便两种使用方式。如果你希望最终exe体积小一些就链接动态库并把DLL放到运行目录如果想部署时少带一个DLL也可以直接链静态库。不过静态库配的是/MD运行时跟你工程里的运行时库必须保持一致这个细节后面还会讲到。3.2 接入工程的第一种方式find_packageCeres安装时通过CMake导出了CeresConfig.cmake所以最正规的接入方式是用find_package。测试工程的CMakeLists.txt这样写cmake_minimum_required(VERSION 3.15) project(hello_ceres LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 指向Ceres的share/Ceres目录 set(Ceres_DIR D:/ThirdParty/ceres-2.1.0/share/Ceres) find_package(Ceres REQUIRED) # Eigen头文件路径find_package有时候不会自动带出来 set(EIGEN_INCLUDE_DIR D:/ThirdParty/eigen-3.4.0) add_executable(hello_ceres main.cpp) target_include_directories(hello_ceres PRIVATE ${EIGEN_INCLUDE_DIR}) target_link_libraries(hello_ceres PRIVATE ${CERES_LIBRARIES})需要注意find_package(Ceres)找到的是Ceres的CMake配置这个配置内部已经包含了Ceres的头文件路径但Eigen的头文件路径需要通过EIGEN_INCLUDE_DIR显式加进来。如果Ceres_DIR指向的位置不对CMake会在configure阶段直接报Could not find Ceres。3.3 接入工程的第二种方式直接指路径有些时候你的工程不想引入find_package的查找机制比如已经有一套写死的第三方库路径管理方式那直接用include和lib路径也完全可以include_directories( D:/ThirdParty/ceres-2.1.0/include D:/ThirdParty/eigen-3.4.0 ) add_executable(hello_ceres main.cpp) target_link_libraries(hello_ceres D:/ThirdParty/ceres-2.1.0/lib/ceres.lib)这种方式最直接也最不容易受CMake查找顺序干扰。缺点是不同机器的路径写死了换环境要改CMakeLists。我自己的习惯是定义一个THIRD_PARTY_DIR变量所有第三方库都挂在它下面这样换机器最多改一行。4. 测试工程用Powell函数验证整条链路4.1 为什么选Powell函数选测试用例时我特意绕开了那些看起来很炫酷的SLAM例子。原因是测试工程的首要目标不是展示算法而是验证库能不能链接、能不能跑、结果对不对。Ceres官方教程里的Powell函数最小二乘问题是最经典的最小化测试它有一个已知的最优解四个变量都收敛到0迭代过程能清楚展示Ceres的工作机制代码又短非常适合作为环境验证。这个问题的定义是求四个残差项的平方和最小值f1 x1 10·x2f2 √5·(x3 - x4)f3 (x2 - 2·x3)²f4 √10·(x1 - x4)²初始值故意给得比较离谱x13x2-1x30x41。Ceres的任务就是通过迭代把这些变量调整到让总代价接近0的位置。4.2 完整代码示例#include ceres/ceres.h #include iostream struct F1 { template typename T bool operator()(const T* const x1, const T* const x2, T* residual) const { residual[0] x1[0] 10.0 * x2[0]; return true; } }; struct F2 { template typename T bool operator()(const T* const x3, const T* const x4, T* residual) const { residual[0] sqrt(5.0) * (x3[0] - x4[0]); return true; } }; struct F3 { template typename T bool operator()(const T* const x2, const T* const x3, T* residual) const { residual[0] (x2[0] - 2.0 * x3[0]) * (x2[0] - 2.0 * x3[0]); return true; } }; struct F4 { template typename T bool operator()(const T* const x1, const T* const x4, T* residual) const { residual[0] sqrt(10.0) * (x1[0] - x4[0]) * (x1[0] - x4[0]); return true; } }; int main() { double x1 3.0; double x2 -1.0; double x3 0.0; double x4 1.0; ceres::Problem problem; problem.AddResidualBlock( new ceres::AutoDiffCostFunctionF1, 1, 1, 1(new F1), nullptr, x1, x2); problem.AddResidualBlock( new ceres::AutoDiffCostFunctionF2, 1, 1, 1(new F2), nullptr, x3, x4); problem.AddResidualBlock( new ceres::AutoDiffCostFunctionF3, 1, 1, 1(new F3), nullptr, x2, x3); problem.AddResidualBlock( new ceres::AutoDiffCostFunctionF4, 1, 1, 1(new F4), nullptr, x1, x4); ceres::Solver::Options options; options.linear_solver_type ceres::DENSE_QR; options.minimizer_progress_to_stdout true; options.max_num_iterations 100; ceres::Solver::Summary summary; ceres::Solve(options, problem, summary); std::cout summary.BriefReport() \n; std::cout x1 x1 \n; std::cout x2 x2 \n; std::cout x3 x3 \n; std::cout x4 x4 \n; return 0; }用AutoDiffCostFunction的原因是它不需要手推雅可比Ceres通过模板自动做数值微分对新手最友好。代码里AddResidualBlock的模板参数分别是残差维度、第一个参数块维度、第二个参数块维度这个顺序不要写反。4.3 编译运行后的预期结果在VS2019的x64环境下编译运行后应该会看到类似这样的输出iter cost cost_change |gradient| |step| tr_ratio tr_radius 0 1.075000e02 0.00e00 1.55e02 0.00e00 0.00e00 1.00e04 1 5.036190e00 1.02e02 2.00e01 9.02e-01 9.51e-01 3.00e04 2 3.046663e-01 4.73e00 1.11e01 5.51e-01 9.56e-01 9.00e04 ... Solver Summary (v 2.1.0-eigen-(3.4.0)) ... Final x1 0.000... Final x2 0.000... Final x3 0.000... Final x4 0.000...看到迭代次数在个位数到十几位之间final cost接近0四个变量都收敛到0附近就说明Ceres库安装完全正常整条工具链是通的。此时你再用这套环境去跑BA、ICP或者位姿图优化都可以放心。5. 避坑清单与排查实录5.1 常见错误速查表整个测试工程的搭建过程中有几个错误属于高频问题我整理成表格遇到可以直接对号入座现象可能原因解决办法链接阶段大量LNK2019/LNK2001库位数与工程位数不匹配或运行时库不一致确认是x64库配x64工程保持/MD、/MDd一致运行时提示找不到ceres.dll动态库路径不在搜索范围内把bin目录加入PATH或将DLL复制到exe同目录CMake阶段报Could not find CeresCeres_DIR指向错误确认指向share/Ceres目录不是Ceres根目录编译错误提示Eigen内联函数找不到Eigen版本过新或过旧使用3.4.0配合Ceres 2.1.0工程里用了glog但库里没启用预编译库关闭了glog去除工程里的glog依赖或改用启用glog的构建调试版正常、Release版崩溃Debug和Release库混用Debug工程链Debug库Release工程链Release库5.2 Debug和Release必须分开这条我单独拎出来说因为它坑了太多人。很多第三方库在Windows上的Debug和Release版本不能混用Ceres也一样。如果你Debug工程链了Release版本的Ceres库轻则链接警告重则运行崩溃反过来Release工程链Debug库也一样。我编译时把lib目录下分成了Debug和Release两个子目录方便切换。测试工程在Debug和Release下各跑了一遍结果一致。建议你接自己工程时也这样做避免Debug能跑Release崩的经典问题。5.3 关于vcpkg和自编译我的真实感受最后聊点个人的操作体会。如果你看完上面这些还是决定用vcpkg或者干脆自己从源码编我也建议你试一次。自己编译一遍Ceres能让你彻底搞清楚它的依赖结构和CMake导出机制这对后续理解大型C项目的构建是有帮助的。但如果是时间紧、任务重直接拿现成的预编译库是最省心的选择。我在实际项目里至今保留着这套预编译库主要原因是团队里多人协作时统一的第三方库版本能避免我机器上能编过、你机器上报错这种尴尬。Ceres版本、Eigen版本、编译器版本全部统一问题排查成本会低很多。你把测试工程跑通之后也可以按同样的方式把这套目录打包进自己的工程模板里以后每开一个新项目直接复制基础设施就能开工。提示如果你之后升级Ceres版本记得同步检查Eigen版本Ceres 2.1.0官方推荐的是Eigen 3.3.x或3.4.x这两个组合是实测最稳的。本文还有配套的精品资源点击获取