简介:在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 2019(VS2019 16.11),VS2022直接兼容
- CMake:3.18以上
- Ceres版本:2.1.0
- Eigen版本: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_package
Ceres安装时通过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·x2
- f2 = √5·(x3 - x4)
- f3 = (x2 - 2·x3)²
- f4 = √10·(x1 - x4)²
初始值故意给得比较离谱:x1=3,x2=-1,x3=0,x4=1。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::AutoDiffCostFunction<F1, 1, 1, 1>(new F1), nullptr, &x1, &x2); problem.AddResidualBlock( new ceres::AutoDiffCostFunction<F2, 1, 1, 1>(new F2), nullptr, &x3, &x4); problem.AddResidualBlock( new ceres::AutoDiffCostFunction<F3, 1, 1, 1>(new F3), nullptr, &x2, &x3); problem.AddResidualBlock( new ceres::AutoDiffCostFunction<F4, 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.075000e+02 0.00e+00 1.55e+02 0.00e+00 0.00e+00 1.00e+04 1 5.036190e+00 1.02e+02 2.00e+01 9.02e-01 9.51e-01 3.00e+04 2 3.046663e-01 4.73e+00 1.11e+01 5.51e-01 9.56e-01 9.00e+04 ... 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 Ceres | Ceres_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,这两个组合是实测最稳的。
本文还有配套的精品资源,点击获取