1. 为什么要在 Ubuntu 20.04 上折腾 GAC-Mapping
GAC-Mapping 这套东西,第一次接触的人多半会被它的名字唬住。全称里带 Mapping,直觉上会以为是个纯建图工具,实际跑起来才发现它更像一套“几何-外观联合优化”的完整流水线:前端要吃图像和深度,中间要做特征匹配与位姿估计,后端还要维护一个可查询的地图结构。我第一次在 Ubuntu 20.04 上部署它的时候,光是环境就来回折腾了两天,中间踩的坑比代码本身还多。所以这篇东西不打算写成官方文档的翻译版,而是把我从裸机到跑通全流程的实操记录摊开讲,包括每一步为什么这么做、参数怎么定、报错怎么查。
先说清楚这套流程适合谁看。如果你手上有一台装了 Ubuntu 20.04 的机器,想复现 GAC-Mapping 的完整运行链路,并且希望少走弯路,那这篇内容基本可以照着抄。如果你只是想了解它的算法原理,那这篇偏工程,原理部分只会点到为止。我默认读者有基本的 Linux 命令行操作能力,会用 apt、会看 CMake 报错、知道什么是动态链接库,剩下的细节我会尽量补全。
Ubuntu 20.04 这个版本选择不是随便定的。GAC-Mapping 依赖的一批库——尤其是 PCL、OpenCV、Eigen 这几个——在 20.04 的 apt 源里版本比较合适,18.04 偏老、22.04 又太新,中间这个 LTS 版本是兼容性最稳的甜点区。我实测过在 22.04 上编译,Eigen 的 API 变动会导致好几处模板报错,改起来很烦。所以如果你还在选系统,直接上 20.04,省事。
整个跑通过程我拆成四块来讲:整体设计与依赖选型、核心模块的细节解析、从零到跑通的完整实操、以及我实际遇到的一堆问题和排查手法。每一块都会给出具体的命令、配置和判断依据,不玩虚的。
2. 整体设计与依赖选型拆解
2.1 这套流水线的模块划分与数据流
GAC-Mapping 的运行链路,我习惯把它拆成三层来看:数据输入层、几何外观联合优化层、地图维护层。数据输入层负责把图像序列、深度图、相机内参读进来并做预处理;联合优化层是核心,做特征提取、匹配、位姿求解,同时把几何约束和外观约束揉在一起优化;地图维护层则负责把优化后的结果落成可查询的结构,支持后续的定位和回环。
为什么强调这个分层?因为部署时绝大多数报错都能归到某一层里。比如“段错误”多半出在输入层的格式不对或者空指针,“优化不收敛”通常是联合优化层的参数没调好,“地图查询结果为空”则是维护层的索引没建起来。你脑子里有这张分层图,排查的时候就能快速定位到是哪一层的问题,而不是对着终端干瞪眼。
数据流的方向是单向的:图像和深度先进预处理,转成统一的内部格式,然后喂给特征提取,提取结果进匹配模块,匹配对再送进优化器,优化输出位姿和地图增量,最后写回地图结构。这条链上任何一个环节的输出格式变了,下游就会崩。所以我在实操里会习惯性地在每个环节之间加一个“格式自检”,比如打印一下特征点的数量和维度,确认没跑偏再往下走。
2.2 依赖库的版本选择逻辑
依赖这块是重灾区。GAC-Mapping 官方给的依赖列表不算长,但每个都有版本脾气。我把关键依赖和我在 20.04 上实际用的版本列出来,方便你对照:
| 依赖库 | 推荐版本 | 选择理由 | 踩坑提示 |
|---|---|---|---|
| Eigen | 3.3.7 | 20.04 apt 默认版本,API 稳定 | 别升 3.4,模板报错多 |
| OpenCV | 4.2.0 | 与 20.04 源匹配,contrib 齐全 | 自己编译要带 contrib |
| PCL | 1.10 | 20.04 默认,点云接口够用 | 注意和 VTK 的链接顺序 |
| Ceres | 2.0.0 | 优化器核心,2.0 对 Eigen 3.3 友好 | 编译时关掉 SuiteSparse 可选依赖 |
| CMake | 3.16+ | 20.04 自带 3.16.3 够用 | 低于 3.10 会报策略错误 |
这里重点说 Ceres。GAC-Mapping 的联合优化层重度依赖 Ceres 做非线性最小二乘,Ceres 2.0 和 2.1 在 API 上有细微差别,尤其是LocalParameterization到Manifold的迁移。如果你图省事装了 2.1,代码里老式的参数化写法会编译不过。我的建议是老老实实用 2.0.0,配合 Eigen 3.3.7,这套组合我跑下来最稳。
OpenCV 这块也有讲究。20.04 的 apt 源里libopencv-dev是 4.2.0,但默认不带 contrib 模块。GAC-Mapping 里如果用到 SIFT 或者某些特征描述子,contrib 是必须的。所以要么自己源码编译带 contrib,要么确认代码里没用到 contrib 的接口。我一开始用 apt 版跑,结果在特征提取那步报“未定义符号”,查了半天才发现是缺 contrib。后来干脆源码编译了一遍,虽然费时间,但一劳永逸。
2.3 为什么不用 Docker 一把梭
有人会问,这么麻烦为什么不直接拉个 Docker 镜像。我试过,结论是:调试阶段别用 Docker。原因很实际——GAC-Mapping 跑不通的时候,你需要频繁改代码、重编译、看中间变量,Docker 里的文件挂载和调试工具链会让这个过程变得笨重。等你把流程完全跑通、参数都调好了,再打成镜像做部署,那才是 Docker 的正确用法。前期在宿主机上裸装,改一行编译一行,反馈最快。
另外,Docker 里的 GUI 转发(如果你要看可视化结果)在 20.04 上配置起来也有额外成本,X11 转发偶尔会抽风。所以我的实操路径是:宿主机裸装跑通,确认无误后再考虑容器化。这个顺序别搞反。
2.4 编译系统的组织方式
GAC-Mapping 用的是 CMake,工程结构一般是src/放源码、include/放头文件、CMakeLists.txt在根目录。我建议在根目录下建一个build/做 out-of-source 编译,别在源码目录里直接cmake .,否则生成的中间文件会把目录搞得一团糟,后面想清理都难。
CMakeLists 里有几个关键点要盯:find_package的顺序、CMAKE_BUILD_TYPE的设置、以及链接库的顺序。链接顺序在 Linux 下很敏感,被依赖的库要放在后面。我遇到过 PCL 和 VTK 链接顺序反了导致的一堆 undefined reference,调换顺序就好了。CMAKE_BUILD_TYPE建议先设Debug,方便出问题时用 gdb 看栈;跑性能测试再切Release。
3. 核心细节解析与实操要点
3.1 相机内参与数据格式的对齐
GAC-Mapping 对输入数据的格式要求比较死。图像一般是灰度或 RGB 的 8 位图,深度图是 16 位无符号整型(单位毫米)或者 32 位浮点(单位米),相机内参是一个 3x3 的矩阵,包含焦距和主点。这里最容易出错的是深度图的单位。我见过有人把毫米当米喂进去,结果建出来的地图尺度差了 1000 倍,位姿全飘。
实操上,我会在数据加载后立刻做一次校验:读一张深度图,打印它的最大值和最小值,再结合场景的物理尺度判断单位对不对。比如室内场景,深度值如果是几万,那基本是毫米;如果是几,那就是米。这个判断花不了几秒钟,但能省掉后面几小时的调试。
相机内参的格式也要对齐。有些数据集给的是fx, fy, cx, cy四个数,有些给的是完整的 3x3 矩阵。GAC-Mapping 内部一般用矩阵形式,所以加载时要做好转换。转换公式很简单:
K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]但要注意cx, cy是像素坐标,别和归一化坐标搞混。我踩过一次坑,把归一化的主点当像素主点传进去,结果特征匹配的几何校验全错,匹配对数量骤降。
3.2 特征提取与匹配的参数调优
特征提取这块,GAC-Mapping 支持多种描述子,具体用哪个取决于你的场景。纹理丰富的场景用 SIFT 或 ORB 都行,弱纹理场景可能要靠深度信息补。参数上,我重点关注三个:特征点数量上限、匹配距离阈值、以及几何校验的阈值。
特征点数量上限别设太大。设成几千,提取慢不说,匹配阶段的计算量也爆炸。我一般从 1000 到 2000 起步,看场景复杂度再调。匹配距离阈值(如果是基于描述子距离的匹配)决定了多少匹配对能留下来,设太严匹配对不够,设太松误匹配多。我的经验是先设一个中等值,跑一遍看内点率,内点率低于 30% 就说明阈值或者特征质量有问题。
几何校验这块,本质是用对极几何或者 PnP 来剔除误匹配。阈值一般设 1 到 3 个像素。这个值太小会把正确匹配也剔掉,太大又起不到过滤作用。我实测下来 2 像素是个比较稳的起点。
提示:调参时别一次改多个参数,改一个跑一次看结果,否则你根本不知道是哪个参数起的作用。这是我在无数次日调试里总结出来的铁律。
3.3 联合优化的收敛控制
联合优化层是 GAC-Mapping 的核心,也是最能体现它和普通建图工具区别的地方。它把几何残差和外观残差放在一个优化问题里一起解,所以收敛行为比单一残差复杂。控制收敛的关键参数是最大迭代次数、残差下降阈值、以及各残差项的权重。
最大迭代次数我一般设 50 到 100。设太小没收敛就停了,设太大浪费时间还可能过拟合。残差下降阈值设 1e-6 量级,意思是连续两次迭代的残差变化小于这个值就认为收敛了。权重这块最玄学,几何和外观的权重比要根据场景调,纹理好的场景外观权重可以高一点,弱纹理场景就得靠几何撑。
我遇到过一次优化不收敛的情况,残差一直在震荡。查下来是外观权重设太高,而那个场景光照变化剧烈,外观残差本身就不靠谱。把外观权重降下来,几何权重提上去,立刻就收敛了。所以权重不是固定值,得看你的数据特性。
3.4 地图结构的初始化与查询
地图维护层容易被忽视,但它决定了你跑完之后能不能用结果。GAC-Mapping 的地图一般是个带索引的点云或者体素结构,初始化时要设定分辨率。分辨率太高内存吃不消,太低地图精度不够。室内场景我一般用 5 厘米的体素分辨率,室外可以放到 20 到 50 厘米。
初始化之后,每帧优化完的位姿和观测要增量写进地图。这里要注意线程安全,如果地图维护和优化在不同线程里跑,写地图的时候得加锁。我见过有人没加锁,跑着跑着就段错误,查了半天是数据竞争。
查询这块,地图建好后要能根据位姿查局部地图,或者根据坐标查最近的体素。索引建得好,查询就快。我建议在地图初始化时就把空间索引(比如 KD 树或者八叉树)建起来,别等查询时临时建,那样每次查询都慢。
4. 从零到跑通的完整实操
4.1 系统准备与基础依赖安装
拿到一台干净的 Ubuntu 20.04,第一步是把系统更新到最新,然后装基础编译工具。命令如下:
sudo apt update && sudo apt upgrade -y sudo apt install -y build-essential cmake git pkg-configbuild-essential带上了 gcc、g++、make,pkg-config在找库的时候很有用。这几样是地基,缺了后面全塌。
接着装核心依赖。Eigen、OpenCV、PCL 我优先用 apt 装,省得自己编译:
sudo apt install -y libeigen3-dev libopencv-dev libpcl-dev装完确认一下版本:
pkg-config --modversion opencv4 dpkg -l | grep eigen3OpenCV 应该显示 4.2.0,Eigen 是 3.3.7。如果版本对不上,说明源有问题,先别往下走,把源理清楚。
Ceres 我建议源码编译,因为 apt 源里的版本不一定对。编译 Ceres 2.0.0 的步骤:
sudo apt install -y libgoogle-glog-dev libgflags-dev libatlas-base-dev libsuitesparse-dev git clone --branch 2.0.0 https://ceres-solver.googlesource.com/ceres-solver cd ceres-solver && mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF make -j$(nproc) && sudo make install-DBUILD_TESTING=OFF能省不少编译时间,测试用例对部署没用。libsuitesparse-dev是 Ceres 的可选依赖,装上能让稀疏求解更快,但如果你遇到链接问题,可以在 cmake 时加-DUSE_SUITESPARSE=OFF绕开。
4.2 源码获取与工程配置
把 GAC-Mapping 的源码拉到本地,进根目录建 build 目录:
git clone <项目仓库地址> gac-mapping cd gac-mapping && mkdir build && cd build配置 CMake 之前,先看一眼根目录的CMakeLists.txt,确认find_package里要求的版本和你装的对得上。如果它要求 OpenCV 4.5 而你只有 4.2,要么改 CMakeLists 里的版本号(如果 API 兼容),要么升级 OpenCV。我倾向于先试着改版本号,跑不通再升级。
配置命令:
cmake .. -DCMAKE_BUILD_TYPE=DebugDebug 模式方便调试。配置过程中盯住终端输出,find_package失败会明确告诉你缺什么。常见的失败是找不到 Ceres 或者 PCL 的某个组件,按提示补装对应包即可。
配置成功后编译:
make -j$(nproc)-j$(nproc)用满所有核心,编译快。如果编译报错,先看第一个错误,后面的往往是连锁反应。第一个错误通常是头文件找不到或者 API 不匹配,针对性解决。
4.3 数据集准备与配置文件修改
GAC-Mapping 跑通需要一个数据集。我用的是一个模拟的室内序列,包含 RGB 图、深度图和对应的内参文件。数据目录结构一般是这样:
data/ rgb/0001.png, 0002.png, ... depth/0001.png, 0002.png, ... camera.txtcamera.txt里放内参,格式按代码要求来,通常是fx fy cx cy一行。确认深度图是 16 位 PNG,用file命令或者 Python 读一下验证:
import cv2 d = cv2.imread('data/depth/0001.png', cv2.IMREAD_UNCHANGED) print(d.dtype, d.min(), d.max())输出应该是uint16,最大值在几千到几万之间(毫米单位)。如果是uint8,说明格式不对,得转换。
然后改配置文件。GAC-Mapping 一般有个config.yaml或者类似的参数文件,里面要设数据路径、内参路径、特征提取参数、优化参数。我建议先把路径和内参填对,其他参数用默认值跑第一遍,确认链路通了再调参。
4.4 首次运行与结果验证
运行命令一般是:
./gac_mapping ../config/config.yaml第一次跑,别指望结果完美,目标是不崩、有输出。终端会打印每帧的处理信息,包括特征点数量、匹配对数量、优化迭代次数、当前位姿。盯住这些数字:特征点数量应该在几百到几千,匹配对数量别是个位数,优化迭代次数别每次都顶到最大值(顶到说明没收敛)。
跑完看输出目录,应该有轨迹文件(每帧位姿)和地图文件(点云或者体素)。轨迹文件用文本编辑器打开,看位姿数值有没有突变或者 NaN。有 NaN 说明某一步数值计算炸了,回去查那一步的输入。地图文件可以用 PCL 的工具或者简单的可视化脚本看一眼,确认点云不是空的、形状大致合理。
我第一遍跑的时候,轨迹里出现了几帧位姿跳变,查下来是那几帧的深度图有大量无效值(0 或者极大值),导致优化时残差异常。在预处理里加了无效深度过滤,问题就没了。所以首次运行后的结果验证很重要,别跑完就完事。
5. 常见问题与排查技巧实录
5.1 编译期报错速查
编译期的坑我整理成一张表,按报错关键词查:
| 报错关键词 | 可能原因 | 解决手法 |
|---|---|---|
undefined reference to cv::... | OpenCV 缺 contrib 或链接顺序错 | 源码编译带 contrib,调整链接顺序 |
Eigen::... no member | Eigen 版本过高 | 降回 3.3.7 |
Ceres Manifold相关 | Ceres 版本 2.1 用了新 API | 换 Ceres 2.0.0 |
PCL ... VTK链接错误 | PCL 和 VTK 链接顺序 | 被依赖库放后面 |
Could NOT find ... | 依赖没装或路径没配 | 按提示装包,设CMAKE_PREFIX_PATH |
CMAKE_PREFIX_PATH这个环境变量很关键,自己编译安装的库(比如 Ceres)默认装在/usr/local,CMake 有时找不到,得手动指定:
cmake .. -DCMAKE_PREFIX_PATH=/usr/local5.2 运行期崩溃与段错误定位
运行时段错误是最烦的,因为终端只给你一句Segmentation fault,啥也不说。定位手法是用 gdb 跑:
gdb --args ./gac_mapping ../config/config.yaml进去后输入run,崩了之后输入bt看调用栈。栈顶那几帧就是问题所在。我遇到过的段错误,一半是空指针(某帧数据没读到,代码没判空),一半是数组越界(特征点索引超了)。看栈能快速定位到具体函数,再回去看那个函数的输入。
还有一种隐蔽的崩溃是内存耗尽。地图分辨率设太高、特征点设太多,跑着跑着内存吃满,系统直接杀进程。用htop盯着内存,如果持续上涨不回落,就是有泄漏或者参数太大。降分辨率、减特征点数量能缓解。
5.3 优化不收敛与轨迹漂移
优化不收敛的表现是残差震荡或者迭代次数顶满。排查顺序:先看输入数据质量(深度有没有大量无效值、图像有没有严重模糊),再看权重参数(几何和外观的比例),最后看初值(上一帧位姿作为初值,如果上一帧就错了,这一帧也难对)。
轨迹漂移则是另一回事,表现为位姿慢慢偏离真实轨迹。原因可能是累积误差没被约束住,或者回环检测没生效。GAC-Mapping 如果有回环模块,确认它开着;如果没有,那漂移是正常的,只能靠更频繁的全局优化来压。我实测下来,室内短序列漂移可控,长序列必须靠回环或者全局优化兜底。
注意:调优化参数时,先把最大迭代次数调大,确认能收敛,再逐步收紧阈值。反过来做你会误以为参数有问题,其实是迭代不够。
5.4 我踩过的三个典型坑
第一个坑是深度图和彩色图没对齐。有些数据集的深度和 RGB 是不同视角拍的,直接当对齐的用,特征点的深度值全错。解决方法是确认数据集是否已对齐,没对齐的话要做配准,或者换用已对齐的数据集。
第二个坑是内参文件的行列顺序。有的文件是fx fy cx cy一行,有的是fx 0 cx换行0 fy cy的矩阵形式。读错了主点位置,几何校验全废。读内参时打印出来和数据集文档对一遍,几秒钟的事。
第三个坑是多线程下的数据竞争。地图维护和优化并行跑,没加锁,跑几分钟就崩。用-fsanitize=thread编译一版跑,能直接报出竞争位置。定位到之后加锁或者改成串行,问题解决。这个坑最隐蔽,因为崩的时间不固定,容易误以为是随机 bug。
6. 性能调优与后续扩展的一点经验
跑通之后如果想让它跑得更快或者更准,有几个方向可以试。编译优化上,把CMAKE_BUILD_TYPE切到Release,加-O3 -march=native,能榨出不少性能,尤其是优化器那部分。但注意-march=native编出来的二进制换机器可能跑不了,部署到别的机器要重新编。
算法层面,特征提取可以降分辨率做,匹配在粗尺度上先筛一遍,再在细尺度上精匹配,这样计算量能降下来。优化器那边,如果残差项很多,可以用 Ceres 的稀疏求解器,配合libsuitesparse效果明显。
地图维护上,增量式建图比每帧重建快得多,但要注意增量更新的正确性。我一般会在增量更新后定期做一次全局一致性检查,防止误差累积。
后续如果要扩展,比如加语义信息或者多传感器融合,建议先把当前的纯几何外观流水线跑稳,再往上叠。基础不稳,加什么都是空中楼阁。我在实际项目里就是先把单目加深度这条链路调透,确认轨迹和地图都靠谱了,才敢往里加别的传感器。
最后分享一个我个人的习惯:每次改完参数或者代码,把配置文件复制一份带时间戳存档,跑出来的结果也对应存档。这样出了问题能回溯到上一个能跑的版本,不至于改着改着把能跑的配置弄丢了。这个习惯救过我好几次,尤其是在调参调到怀疑人生的时候。