GAC-Mapping 这套东西第一次跑起来的时候,我在一台装了 Ubuntu 20.04 的旧工作站上折腾了整整两天。标题里写的是"跑通",但真正卡住人的从来不是那几行编译命令,而是环境里那些看不见的版本咬合关系——ROS 的版本、PCL 的版本、Eigen 的对齐选项、Ceres 的编译开关,任何一个不对,编译能过,跑起来直接段错误或者点云全空。这篇就把我从裸系统到能出一张像样的建图结果的全过程拆开讲,重点放在那些官方 README 里不会写、但实际一定会撞上的地方。
GAC-Mapping 属于那种"论文开源代码"类型的项目,特点是算法思路清晰、代码质量参差、依赖声明含糊。它做的是基于几何一致性的点云配准与建图,核心链路大致是:读入激光点云序列,做帧间配准,维护局部地图,最后输出全局一致的建图结果。适合谁看?如果你手上有一台装了 Ubuntu 的机器、有一点 ROS 和 C++ 基础、想复现一个激光建图的开源方案,那这篇基本能让你少走大半的弯路。下面按我实际的操作顺序来,但章节会按"为什么这么做"来组织,方便你按需跳读。
1. 先把 Ubuntu 20.04 的底座摸清楚再动手
很多人一上来就 clone 仓库、catkin_make,结果报一堆找不到头文件的错,然后开始满世界搜。我的建议是反过来的:先花二十分钟把系统底座的状态确认清楚,后面能省掉几个小时的瞎猜。
1.1 系统版本与 ROS 版本的对应关系不是随便选的
Ubuntu 20.04 对应的是 ROS Noetic,这是 ROS 1 的最后一个长期支持版本,Python 3 是默认解释器。这一点很关键,因为 GAC-Mapping 这类项目如果混用了 Python 2 时代的脚本,在 Noetic 下会直接语法报错。先确认:
lsb_release -a输出里Release应该是20.04。然后确认 ROS 是否已装:
echo $ROS_DISTRO正常应该输出noetic。如果为空,说明环境变量没 source,检查~/.bashrc里有没有source /opt/ros/noetic/setup.bash这一行。我见过有人装是装了,但每次开新终端都忘了 source,然后怀疑是项目的问题——这种低级错误排查起来最浪费时间。
提示:不要试图在 Ubuntu 20.04 上装 Melodic 或更老的 ROS,glibc 和 Python 版本对不上,装到一半就会崩。Noetic 是唯一正确选择。
1.2 编译器与 C++ 标准的隐性约束
GAC-Mapping 的代码里大量用了 C++14 的特性(比如泛型 lambda、std::make_unique),而 Ubuntu 20.04 自带的 GCC 9.4 默认标准是 C++14,理论上够用。但有些第三方依赖(尤其是 Ceres 的新版本)要求 C++17。我的做法是统一把项目的CMakeLists.txt里CMAKE_CXX_STANDARD显式设成 17,避免不同子模块标准不一致导致的链接期符号问题。
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON)这里解释一下为什么"显式"很重要:catkin 的构建系统会把多个 package 的编译单元混在一起链接,如果 A 包用 C++14 编译、B 包用 C++17 编译,某些模板实例化的 ABI 可能对不上,表现就是链接时报undefined reference,而且报的符号看起来完全莫名其妙。统一标准能规避掉一整类玄学问题。
1.3 磁盘和内存的现实检查
建图类项目对内存的胃口不小。GAC-Mapping 在维护局部地图时,点云累积会吃掉大量 RAM。我实测跑一段几百米的数据,峰值内存能到 4-6 GB。如果你的机器只有 4 GB 内存又开了图形界面,跑到一半被 OOM killer 干掉是很常见的。先看一眼:
free -h df -hdf -h是看根分区剩余空间,编译过程加上 PCL 和 Ceres 的中间产物,轻松吃掉 10 GB 以上。我建议至少留 20 GB 空闲。这些数字不是吓唬人,是我第一次编译到 87% 时磁盘满了、整个 build 目录作废重来的教训。
2. 依赖安装:顺序错了就是给自己挖坑
依赖这块是重灾区。GAC-Mapping 依赖 ROS 的 PCL、Eigen、Ceres,还有可能用到 OpenCV 和 Boost。问题在于,Ubuntu 的 apt 源里这些库的版本和项目期望的版本经常对不上,而"先装哪个后装哪个"会直接影响后面能不能编过。
2.1 用 apt 装的和源码编的必须分清
我的原则是:能用 apt 装的绝不源码编,除非版本明确不满足。原因很简单,apt 装的库有统一的安装路径、有 pkg-config 文件、和 ROS 的构建系统配合良好;源码编的库如果make install到/usr/local,很容易和 apt 版本冲突,出现"头文件用的是新版、链接的是旧版"这种最难查的问题。
先装基础依赖:
sudo apt update sudo apt install -y build-essential cmake git sudo apt install -y libeigen3-dev libboost-all-dev sudo apt install -y libpcl-dev pcl-tools sudo apt install -y libopencv-dev这里libeigen3-dev在 20.04 上是 3.3.7 版本,libpcl-dev是 1.10。这两个版本对 GAC-Mapping 来说基本够用。装完可以用下面的命令确认版本:
dpkg -l | grep -E "eigen|pcl|opencv"2.2 Ceres 的版本选择是个分水岭
Ceres 是最容易出问题的一环。apt 源里的libceres-dev在 20.04 上是 1.14 版本,而不少开源建图项目用的是 2.x 的 API。1.14 和 2.x 之间有一个破坏性变更:LocalParameterization在 2.x 里被Manifold取代。如果你的项目代码里用的是旧接口,装 2.x 会编译失败;反之用新接口装 1.14 也会失败。
我的处理方式是先看项目CMakeLists.txt里find_package(Ceres)后面有没有版本要求,再决定。如果没有明确要求,先试 apt 的 1.14:
sudo apt install -y libceres-dev如果编译时报Manifold相关错误,说明代码要 2.x,那就得源码编。源码编的时候有个关键开关:
git clone https://ceres-solver.googlesource.com/ceres-solver cd ceres-solver mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ -DBUILD_EXAMPLES=OFF \ -DUSE_CUDA=OFF .. make -j4 sudo make install-DBUILD_TESTING=OFF和-DBUILD_EXAMPLES=OFF能省掉大量编译时间,-DUSE_CUDA=OFF是因为大多数人的机器没配 CUDA 环境,开着反而会报错。装到/usr/local之后,记得让 CMake 优先找到它,可以在项目 CMakeLists 里加:
set(Ceres_DIR "/usr/local/lib/cmake/Ceres")注意:源码装完 Ceres 后,如果之前 apt 装过 1.14,两个版本会共存。链接时到底用哪个取决于
LD_LIBRARY_PATH和 CMake 的查找顺序,这种不确定性是 bug 的温床。我的做法是源码编之前先sudo apt remove libceres-dev,保证系统里只有一个版本。
2.3 PCL 的隐藏依赖和 VTK 的坑
libpcl-dev装的时候会连带装 VTK,而 PCL 的可视化模块依赖 VTK 的渲染后端。在无图形界面的服务器上,VTK 初始化会失败,导致程序在pcl::visualization相关调用处崩溃。如果你只是跑建图、不需要实时可视化,最稳妥的办法是在代码里把可视化部分注释掉,或者编译时加宏关掉。
另外 PCL 1.10 对 Eigen 的对齐有要求,涉及Eigen::Vector4f这类固定大小向量作为类成员时,必须加EIGEN_MAKE_ALIGNED_OPERATOR_NEW宏,否则在 32 位对齐的指令下会段错误。这个坑在 PCL 官方文档里有提,但很多人不看,跑起来随机崩溃还以为是算法问题。
3. 编译 GAC-Mapping:从 catkin 工作空间到第一个可执行文件
依赖齐了之后进入正题。GAC-Mapping 是 ROS 包结构,标准做法是放进 catkin 工作空间的src下编译。
3.1 工作空间初始化的标准动作
mkdir -p ~/gac_ws/src cd ~/gac_ws/src # 把 GAC-Mapping 源码放进来,假设目录名是 GAC-Mapping cd ~/gac_ws catkin_make -DCMAKE_BUILD_TYPE=Release-DCMAKE_BUILD_TYPE=Release这个参数千万别省。建图算法里有大量 Eigen 矩阵运算,Debug 模式下慢一个数量级,跑一段数据能等到你怀疑人生。我第一次忘了加,跑 200 帧花了快四十分钟,加上之后三分钟出结果。
编译过程中最常见的三类错误,我按出现频率排一下:
| 错误类型 | 典型报错 | 根因 | 处理 |
|---|---|---|---|
| 找不到头文件 | fatal error: xxx.h: No such file | 依赖没装或路径没导出 | 确认 apt 包装了,检查 CMakeLists 的 include 路径 |
| 符号未定义 | undefined reference to ... | C++ 标准不一致或库版本冲突 | 统一 CXX_STANDARD,清理 build 重编 |
| 模板实例化失败 | 一长串模板报错 | Eigen/Ceres 版本 API 不匹配 | 按 2.2 节调整 Ceres 版本 |
3.2 编译通过不等于能跑:先做冒烟测试
catkin_make显示 100% 之后,别急着上真实数据。先确认可执行文件生成了:
ls ~/gac_ws/devel/lib/找到对应的节点名(比如gac_mapping_node之类)。然后 source 环境:
source ~/gac_ws/devel/setup.bash这一步很多人漏掉,直接rosrun会提示找不到包。source 之后可以用rospack find确认包能被找到。
冒烟测试我一般用一个极小的数据集,或者干脆用项目自带的示例数据。目的是确认程序能启动、能读到配置、能初始化,而不是一上来就跑完整序列。如果启动就崩,问题一定在环境或配置,跟算法无关,这时候排查范围小得多。
3.3 launch 文件里的参数不是摆设
GAC-Mapping 通常带一个 launch 文件,里面有一堆参数:点云话题名、体素滤波的 leaf size、配准的迭代次数、地图分辨率等等。这些参数直接决定跑不跑得起来。最常见的两个坑:
第一个是话题名对不上。你的激光数据发布在/velodyne_points,launch 里订阅的是/points_raw,那程序就一直在等数据,看起来像"卡住了",其实是没收到输入。用rostopic list和rostopic hz确认数据在发、频率正常。
第二个是体素滤波的 leaf size 设得太大。比如设成 1.0 米,点云被抽稀得只剩骨架,配准直接失败。一般激光雷达数据用 0.1 到 0.3 米比较合理,具体看你的场景尺度。这个参数没有标准答案,得根据数据调,我后面会讲怎么调。
4. 跑真实数据:从"能启动"到"建图结果像样"之间隔着什么
编译通过、冒烟测试过了,接下来才是真正花时间的部分——让建图结果达到可用质量。这一段我按问题现象来组织,因为实际调试就是"看到什么现象、往哪个方向查"的过程。
4.1 点云全空或者只有一帧:先查坐标系
第一次跑真实数据,rviz 里啥都没有,或者只有零星几个点。这种情况九成是坐标系问题。激光雷达发布的点云带一个frame_id,比如velodyne,而建图节点期望的坐标系可能是map或odom。如果 TF 树里没有从frame_id到期望坐标系的变换,点云变换会失败,结果就是空的。
排查步骤:
rosrun tf view_frames生成一个 pdf 看 TF 树结构。确认你的点云frame_id在树里,并且能连到map。如果缺变换,要么在 launch 里加一个静态 TF 发布,要么改建图节点的配置让它接受原始frame_id。
提示:静态 TF 用
static_transform_publisher发布,格式是x y z yaw pitch roll parent child,注意单位是米和弧度,别把角度当弧度填。
4.2 建图漂移:配准参数和场景特性的匹配
点云有了,但建出来的图明显漂移,墙是歪的、回环对不上。这是建图项目的经典问题,原因可能有好几层,得逐层剥。
第一层看配准的初值。GAC-Mapping 这类方法通常需要帧间有一个不错的初始位姿估计,如果运动太快、帧间重叠太少,配准会失败。解决办法是降低数据播放速度(rosbag play -r 0.5),或者提高激光雷达的帧率。我实测把播放速率降到 0.5 倍,漂移明显改善,说明算法本身对帧间运动是敏感的。
第二层看体素滤波和法向量估计的参数。GAC-Mapping 的"几何一致性"依赖法向量或局部几何特征,如果点云太稀疏,法向量估计不准,一致性约束就失效。把 leaf size 调小(比如从 0.3 降到 0.15),点云密度上来,法向量更稳,但计算量也上去了。这是个权衡,得根据你的机器性能找平衡点。
第三层看回环检测的阈值。如果场景里有明显的回环但没被检测到,全局优化就不会触发,漂移累积。检查配置里回环相关的距离阈值和相似度阈值,适当放宽。但放宽太多会引入错误回环,把图拉得更歪,所以要一点点试。
4.3 内存持续增长直到崩溃:地图管理的边界
跑长序列时,程序内存一路涨,最后被系统杀掉。这是局部地图没有做裁剪导致的。GAC-Mapping 维护局部地图时,如果只加不删,点云越积越多。检查代码里有没有基于距离或时间的滑窗裁剪逻辑,配置里通常有map_size或local_map_radius之类的参数,把它设成一个合理值(比如 50 米),超出范围的老点云要剔除。
如果代码里压根没有裁剪逻辑,那就得自己加。最简单的做法是在每次更新地图后,遍历一遍点云,把距离当前位置超过阈值的点删掉。用 PCL 的PassThrough或者手写一个半径过滤都行。这段代码不难,但得有,否则长序列必崩。
5. 那些让我多花了一天的细节问题
前面讲的是主线,这一段专门收那些零碎但致命的坑。它们不一定每个人都会遇到,但遇到了就是几个小时起步。
5.1 Eigen 对齐导致的随机段错误
前面提过一嘴,这里展开说。Eigen 为了 SIMD 加速,对固定大小的向量化类型(Vector4f、Matrix4f等)要求 16 字节对齐。如果一个类里有这种成员,而这个类是用new分配的,编译器不一定保证对齐,运行时就会在访问该成员时崩。表现是"有时候崩有时候不崩",极其难查。
解决办法是在类定义里加:
class MyClass { public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW Eigen::Vector4f vec; // ... };或者在 CMake 里全局关掉向量化:-DEIGEN_DONT_VECTORIZE。后者性能有损失,但能一劳永逸。我一般先加宏,实在查不出来再关向量化。
5.2 rosbag 的时间戳和 TF 的时间同步
用 rosbag 回放数据时,如果 bag 里的 TF 和点云时间戳对不齐,tf查询会报Lookup would require extrapolation into the future。这是因为回放时系统时间和 bag 时间不一致。解决办法是加--clock参数,并设置use_sim_time:
rosparam set use_sim_time true rosbag play --clock your_data.baguse_sim_time让 ROS 节点用 bag 里的时间而不是系统时间,TF 查询就能对上。这个坑我第一次遇到时,报错信息看得一头雾水,查了半天才发现是时间源的问题。
5.3 编译缓存导致的"改了没生效"
catkin 的增量编译有时候会抽风,改了代码但可执行文件没更新,或者改了 CMakeLists 但配置没重新生成。表现是"我明明改了参数,怎么结果一样"。遇到这种情况,别犹豫,直接清:
cd ~/gac_ws rm -rf build devel catkin_make -DCMAKE_BUILD_TYPE=Release全量重编虽然慢,但能排除掉一整类"改了不生效"的假象。我现在的习惯是,只要改了 CMakeLists 或者动了依赖,就全量重编一次,省得后面怀疑人生。
5.4 可视化拖慢主循环
rviz 里订阅了点云话题做实时显示,数据量大时 rviz 本身会吃掉大量 CPU,导致建图主循环被拖慢,帧处理不过来,队列积压。表现是"跑着跑着越来越慢"。解决办法是关掉 rviz 的实时显示,或者降低显示的点云降采样率。调试阶段可以开,正式跑数据时关掉,用 rosbag record 录下来事后看。
6. 调参的实操思路:别瞎试,按影响面排序
参数多的时候最容易犯的错是"每个都调一点",结果变量太多根本不知道是哪个起了作用。我的做法是按影响面从大到小排,一次只动一个。
6.1 先定分辨率,再定配准,最后调回环
影响面最大的是体素滤波的 leaf size,它决定了整个管线的数据密度,后面所有参数都建立在它之上。先把 leaf size 定下来(根据场景尺度和机器性能,0.1 到 0.3 之间选),然后调配准的迭代次数和收敛阈值,最后才是回环检测的阈值。
配准迭代次数一般 30 到 50 次够用,太少不收敛,太多浪费时间且可能过拟合噪声。收敛阈值看你的点云精度,厘米级精度的话设 1e-4 到 1e-6 比较合理。
回环阈值是最玄学的,因为它依赖场景。我的经验是先关掉回环跑一遍,看纯里程计的漂移有多大,再打开回环看能修正多少。如果打开回环后图反而更歪,说明阈值太松,检测到了错误回环,收紧。
6.2 用定量指标而不是"看着差不多"
调参不能只靠肉眼。至少记录两个指标:一是相邻帧配准的 fitness score(配准残差),二是回环前后的轨迹闭合误差。前者反映帧间配准质量,后者反映全局一致性。把这两个数记下来,调参才有依据。我一般写个小脚本从日志里 grep 出来,画个曲线,比盯着 rviz 靠谱得多。
7. 我个人在实际操作中的几点体会
跑通 GAC-Mapping 这件事,技术门槛其实没有想象中高,真正消耗时间的是环境的不确定性和问题的定位效率。我最大的体会是:先把底座和环境确认死,再动代码和参数。很多人一上来就怀疑算法,其实九成的问题出在版本、路径、时间戳这些"非算法"的地方。
另外一个实用的习惯是,每解决一个问题就把现象和根因记下来,形成自己的排查清单。下次遇到类似现象,先对照清单过一遍,能省掉大量重复劳动。我现在的清单里,"点云全空先查 TF""随机崩溃先查 Eigen 对齐""改了不生效先全量重编"这几条,几乎每次都能命中。
最后说一个容易被忽略的点:跑通之后,把整个环境(依赖版本、编译参数、launch 配置)完整记录一份。开源项目的依赖是会变的,今天能编过的组合,半年后换个机器可能就编不过了。留一份可复现的记录,比什么都值钱。