简介:本资源是一套基于ORB-SLAM与OctoMap融合的室内三维建图与导航地图构建完整实现方案,面向机器人视觉SLAM初学者、课程设计及毕业设计学生,解决从稀疏特征跟踪到密集点云重建、再到体素化导航地图生成的技术闭环问题。压缩包共199个文件,含84个头文件(.h/.hpp)定义核心算法接口,37个C++源文件(.cpp/.cc)实现ORB特征提取、位姿优化、点云融合与OctoMap更新等关键模块,另有CMake构建脚本、ROS相关配置(.xml/.yaml)、Shell部署工具及PCD点云转换工具等,整体32.95MB,结构清晰、注释详尽。已有405人学习下载,代码经严格调试可直接运行,涵盖从TUM数据集加载、RGB-D帧处理、实时建图到可视化展示全流程,附带ORBvoc词典与典型场景测试配置,特别适合需快速复现SLAM+OctoMap应用的学生项目实践。
1. 为什么用 ORB-SLAM 做稠密重建 + OctoMap 建图,比直接跑 ROS 的rtabmap或hdl_graph_slam更可控?
很多做室内移动机器人导航的工程师卡在第一步:地图质量不稳定。要么激光雷达建图太稀疏、穿墙漏检;要么 RGB-D 直接用rgbd_odometry+octomap_server,点云噪声大、动态物体残留严重、走廊尽头常塌陷。而这个标题指向一条更底层、更可干预的路径——先用 ORB-SLAM2/3 输出高精度、带尺度一致性的相机轨迹和稀疏特征点,再基于该轨迹对原始图像序列做深度图估计(如使用 COLMAP、DepthAnything 或 Patchmatch Stereo),生成几何一致、无漂移累积的三维密集点云;最后将该点云体素化注入 OctoMap,构建出具备精确空间分辨率、支持概率更新、可直接用于 3D 路径规划与避障的室内导航地图。它不依赖 ROS 的黑盒节点链,所有中间产物(位姿、深度图、点云、octree)均可检查、裁剪、重采样。适合需要复现论文结果、调试建图鲁棒性、或对接自研导航栈的中高级开发者。
2. 从 ORB-SLAM 输出到稠密点云:四步闭环流程与关键参数控制
ORB-SLAM 默认只输出稀疏地图(<1000 个特征点),无法直接喂给 OctoMap。必须补全“稠密重建”环节。常见做法是:冻结 ORB-SLAM 估计的相机位姿 → 对每帧图像执行单目/双目深度估计 → 将深度图反投影为点云 → 合并去噪 → 输出 PLY 或 BIN 格式。整个流程需严格保证坐标系对齐与尺度一致性。
2.1 确保 ORB-SLAM 输出可靠位姿:关闭回环、固定参考帧、导出 TUM 格式轨迹
ORB-SLAM2/3 在室内易受纹理缺失影响,导致局部漂移。实操中建议禁用回环检测(避免错误闭环引发全局扭曲),并以第一帧为世界坐标系原点。修改Examples/Monocular/mono_tum.cc中的初始化逻辑:
// 在 System 构造后添加: SLAM.Shutdown(); // 防止后台线程干扰 SLAM.SaveTrajectoryTUM("KeyFrameTrajectory.txt"); // 必须调用此函数提示:
SaveTrajectoryTUM输出的是timestamp tx ty tz qx qy qz qw格式,但 ORB-SLAM 默认时间戳为系统纳秒,需转换为 TUM 数据集标准的秒级浮点时间戳(除以 1e9)。若用 EuRoC 数据集,直接使用其.csv时间戳即可对齐。
导出的KeyFrameTrajectory.txt是后续深度估计的唯一位姿依据。务必验证其连续性:用 Python 加载后计算相邻帧平移模长,若出现 >0.5m 的突变,则说明该段位姿不可靠,应剔除对应图像帧。
2.2 用 DepthAnythingV2 生成逐帧深度图:轻量、泛化强、无需标定参数
相比 Patchmatch Stereo(需双目极线校正)或 MVSNet(需 GPU+大量显存),DepthAnythingV2 在单目场景下表现更稳。其优势在于:
- 输入任意分辨率图像,输出等分辨率深度图(单位:米,非归一化);
- 对低纹理墙面、玻璃反光区域鲁棒性优于传统 SfM 工具;
- 支持 ONNX 导出,可脱离 PyTorch 环境部署。
安装与推理命令如下(需 Python 3.9+、ONNX Runtime):
pip install depthanythingv2 python -c " from depth_anything_v2.dpt import DepthAnythingV2 model = DepthAnythingV2( encoder='vitl', features=256, out_channels=[256, 512, 1024, 1024], pretrained='checkpoints/depth_anything_v2_vitl.pth' ) model.load_state_dict(torch.load('checkpoints/depth_anything_v2_vitl.pth', map_location='cpu')) model.eval() "实际批量处理时,需按KeyFrameTrajectory.txt中的时间戳顺序读取对应图像(如frame_00001.png),并确保图像尺寸与训练分辨率一致(默认 518×518)。关键参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
encoder | 'vitl' | ViT-Large,精度最高;若显存不足可用'vits'(ViT-Small) |
input_size | (518, 518) | 必须与训练尺寸一致,否则深度值失真 |
pred_max_depth | 20.0 | 截断远距离噪声,室内场景设为10.0更佳 |
注意:DepthAnythingV2 输出的深度图是单通道 float32,单位为米。需用
cv2.imwrite('depth_00001.exr', depth_map)保存为 EXR 格式(保留浮点精度),避免 PNG 量化损失。
2.3 将深度图 + 位姿 + 相机内参反投影为点云:坐标系对齐是成败关键
此处极易出错:ORB-SLAM 使用 OpenCV 坐标系(Z 向前),而大多数深度估计模型输出符合 OpenGL(Z 向外)。必须统一为ROS 坐标系(X 向前,Y 向左,Z 向上),否则 OctoMap 会把天花板建在地板下方。
假设已知相机内参K = [[fx,0,cx],[0,fy,cy],[0,0,1]],某帧位姿T_wc(世界到相机变换,4×4 矩阵),深度图d(u,v),则点云生成伪代码为:
# u,v 为像素坐标,d 为深度值(米) z = d[v, u] x = (u - cx) * z / fx y = (v - cy) * z / fy # 此时 (x,y,z) 在相机坐标系下,Z 向前 # 转换到 ROS 坐标系:绕 X 轴旋转 -90°,再绕 Z 轴旋转 -90° R_ros = np.array([[1,0,0],[0,0,-1],[0,1,0]]) # 等效于 R_x(-90) @ R_z(-90) point_cam = np.array([x, y, z]) point_ros = R_ros @ point_cam # 再通过 T_wc 变换到世界坐标系 point_world = T_wc[:3,:3] @ point_ros + T_wc[:3,3]实际实现推荐使用open3d批量处理:
import open3d as o3d import numpy as np def depth_to_pointcloud(depth_img, intrinsics, extrinsics, depth_scale=1.0): h, w = depth_img.shape xx, yy = np.meshgrid(np.arange(w), np.arange(h)) z = depth_img.astype(np.float32) / depth_scale x = (xx - intrinsics[0,2]) * z / intrinsics[0,0] y = (yy - intrinsics[1,2]) * z / intrinsics[1,1] points_cam = np.stack([x, y, z], axis=-1).reshape(-1, 3) # 应用相机到世界的变换(extrinsics 是 4x4 矩阵) points_homo = np.concatenate([points_cam, np.ones((len(points_cam),1))], axis=1) points_world = (extrinsics @ points_homo.T).T[:, :3] return points_world # 示例:加载第 i 帧 depth = cv2.imread(f'depth_{i:05d}.exr', cv2.IMREAD_UNCHANGED) intrinsics = np.array([[525, 0, 319.5], [0, 525, 239.5], [0, 0, 1]]) # TUM 数据集典型值 T_wc = load_pose_from_tum_line(trajectory_lines[i]) # 解析 KeyFrameTrajectory.txt 第 i 行 pcd = depth_to_pointcloud(depth, intrinsics, T_wc)提示:
intrinsics必须与 ORB-SLAM 运行时使用的相机参数完全一致。若用 RealSense,需从rs-enumerate-devices -v获取Color Sensor的Model Parameters;若用手机采集,需用cameracalibrator工具标定。
2.4 点云合并、滤波与格式导出:剔除动态物体与离群点
单帧点云含大量噪声(运动模糊、深度估计误差、反射干扰)。必须做三阶段滤波:
- 距离截断:剔除
z < 0.3m(太近易受镜头畸变影响)和z > 8.0m(远距离深度不准)的点; - 统计离群点移除(SOR):
open3d.geometry.statistical_outlier_removal(pcd, nb_neighbors=20, std_ratio=2.0); - 体素下采样:
pcd.voxel_down_sample(voxel_size=0.02)(2cm 分辨率,兼顾精度与 OctoMap 构建速度)。
最终导出为二进制 PLY(兼容 OctoMap 的octomap_server):
# 合并所有帧点云 full_pcd = o3d.geometry.PointCloud() for pcd in all_pcds: full_pcd += pcd full_pcd = full_pcd.voxel_down_sample(voxel_size=0.02) o3d.io.write_point_cloud("dense_map.ply", full_pcd, write_ascii=False, compressed=True)导出前务必用open3d.visualization.draw_geometries([full_pcd])可视化检查:走廊是否连通、房间角落是否完整、楼梯是否有台阶级差——这是后续 OctoMap 能否正确表达空间结构的前提。
3. 将稠密点云注入 OctoMap:从 PLY 到可导航的 3D 占据栅格
OctoMap 不是直接渲染点云,而是将空间划分为八叉树节点,每个节点存储占据概率(log-odds)。点云只是“观测数据”,需通过octomap_server的insertPointCloud接口逐帧插入,并触发概率更新。但本项目用离线点云,需绕过 ROS 实时接口,直接操作 OctoMap C++ API。
3.1 编译支持 PLY 读取的 OctoMap 工具链
官方 OctoMap(v2.0.0+)不内置 PLY 解析器。需手动启用OCTOMAP_PCL并链接pcl_io:
git clone https://github.com/OctoMap/octomap.git cd octomap mkdir build && cd build cmake -DOCTOMAP_PCL=ON -DBUILD_OCTOVIS=OFF -DCMAKE_BUILD_TYPE=Release .. make -j$(nproc) sudo make install注意:
-DOCTOMAP_PCL=ON启用 PCL 支持,但需提前apt install libpcl-dev(Ubuntu 22.04)或brew install pcl(macOS)。若编译报PCL_IO找不到,检查pkg-config --modversion pcl_io是否返回版本号。
3.2 编写 C++ 离线点云导入器:控制分辨率与概率阈值
核心逻辑:读取 PLY → 遍历每个点 → 调用octree->insertPointCloud()→ 设置maxrange和probHit/probMiss。以下为最小可行代码(import_ply.cpp):
#include <octomap/octomap.h> #include <octomap/ColorOcTree.h> #include <pcl/io/ply_io.h> #include <pcl/point_types.h> int main(int argc, char** argv) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " <input.ply> <output.bt>" << std::endl; return -1; } // 初始化八叉树,分辨率设为 0.05m(5cm,平衡精度与内存) octomap::OcTree tree(0.05); // 加载 PLY 点云 pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>); if (pcl::io::loadPLYFile(argv[1], *cloud) == -1) { PCL_ERROR("Couldn't read file %s\n", argv[1]); return -1; } // 插入所有点,maxrange=5.0m(仅更新 5 米内体素) for (const auto& pt : cloud->points) { octomap::point3d sensor(0, 0, 0); // 假设所有点来自同一传感器位置(即点云已转到世界坐标) octomap::point3d point(pt.x, pt.y, pt.z); tree.insertPointCloud(sensor, point, 5.0); } // 设置概率参数(默认 probHit=0.7, probMiss=0.4,此处显式设置) tree.setProbHit(0.7f); tree.setProbMiss(0.4f); tree.updateInnerOccupancy(); // 必须调用,否则叶节点概率不更新 // 保存为 .bt 格式(二进制,加载快) tree.writeBinary(argv[2]); std::cout << "Saved " << tree.calcNumNodes() << " nodes to " << argv[2] << std::endl; return 0; }编译命令:
g++ -std=c++14 import_ply.cpp -loctomap -lpcl_io -lpcl_common -I/usr/include/pcl-1.12 -o import_ply ./import_ply dense_map.ply indoor_nav_map.bt提示:
maxrange=5.0是关键参数——它定义了“传感器最大探测距离”。若设过大(如 10.0),远处噪声点会错误占据空间;若过小(如 2.0),则墙壁会被打“洞”。建议先用octovis indoor_nav_map.bt可视化,观察墙体厚度是否均匀(理想为 1~2 个体素宽)。
3.3 验证 OctoMap 质量:用 octovis 检查体素填充与空洞
octovis是 OctoMap 官方可视化工具,能直观显示八叉树结构:
octovis indoor_nav_map.bt重点关注三点:
- 墙体连续性:沿走廊行走,观察左右墙是否闭合、无断裂;
- 地面完整性:切换到
Wireframe模式,确认地面体素未被“挖空”(常见于深度图缺失区域); - 分辨率匹配:按
R键重置视角,用鼠标滚轮缩放,确认最小体素边长 ≈ 设置的0.05m。
若发现大面积空洞(黑色区域),说明点云在该区域覆盖不足。此时应回溯第 2 章:检查 ORB-SLAM 轨迹是否经过该区域、DepthAnythingV2 是否对该区域输出有效深度、反投影时是否因内参误差导致 Z 值坍缩。
3.4 导出为 ROS 兼容格式:生成octomap_server可加载的.ot文件
虽然.bt可被octomap_server加载,但 ROS 社区更常用.ot(OctoMap 格式)。用octomap_saver转换:
octomap_saver -f indoor_nav_map.ot indoor_nav_map.bt生成的indoor_nav_map.ot可直接在 ROS Launch 文件中指定:
<node pkg="octomap_server" type="octomap_server_node" name="octomap_server"> <param name="resolution" value="0.05" /> <param name="sensor_model/max_range" value="5.0" /> <param name="save_directory" value="$(find my_nav)/maps" /> <param name="map_file_name" value="$(find my_nav)/maps/indoor_nav_map.ot" /> </node>注意:
octomap_server加载.ot后会发布/octomap_full(octomap_msgs/Octomap)和/occupied_cells_vis_array(visualization_msgs/MarkerArray),后者可被 RViz 直接渲染为 3D 占据网格。
4. 基于 OctoMap 的室内导航地图应用:避障、路径规划与动态更新技巧
生成的indoor_nav_map.ot不是静态快照,而是支持在线更新的概率占据栅格。真正发挥其价值,需结合导航栈完成闭环。
4.1 用 move_base_flex + mbf_costmap_core 实现 3D-aware 路径规划
标准move_base仅支持 2D 成本图。要利用 OctoMap 的 3D 结构,需替换成本图插件为mbf_costmap_core,并配置obstacle_layer订阅/octomap_full:
# costmap_common_params.yaml obstacle_layer: enabled: true max_obstacle_height: 2.0 obstacle_range: 5.0 raytrace_range: 5.0 track_unknown_space: true combination_method: 1 # Overwrite mode observation_sources: octomap octomap: data_type: "PointCloud2" topic: "/octomap_full" marking: true clearing: true关键点:combination_method: 1表示新观测完全覆盖旧值,避免多层 OctoMap 叠加导致概率饱和;max_obstacle_height: 2.0限定只处理 2 米以下障碍物(忽略吊灯、梁柱),提升规划效率。
4.2 实时避障:订阅/octomap_binary提升响应速度
/octomap_full发布频率低(约 1Hz),不适合高频避障。应启用octomap_server的二值化输出/octomap_binary(octomap_msgs/Octomap),其只包含occupied/free状态,无概率字段,体积小、解析快:
rostopic hz /octomap_binary # 验证是否 ≥10Hz在自研控制器中,用octomap::OcTree解析该消息:
void octomapCallback(const octomap_msgs::Octomap::ConstPtr& msg) { octomap::OcTree* tree = dynamic_cast<octomap::OcTree*>( octomap_msgs::msgToMap(*msg) ); // 查询机器人当前位置 (x,y,z) 是否被占据 octomap::OcTreeNode* node = tree->search(x, y, z); if (node && tree->isNodeOccupied(node)) { // 触发紧急停障 emergency_stop(); } }提示:
octomap_msgs::msgToMap自动识别消息类型(BinaryMap或FullMap),无需手动判断。
4.3 动态更新技巧:选择性清除与局部重构建
纯增量更新易积累误差。推荐两种策略:
- 选择性清除:当机器人进入新房间,调用
octomap_server/clear_bbx服务清除指定立方体区域(min.x/max.x等),再注入新点云; - 局部重构建:用
octomap::OcTree::prune()压缩冗余节点,再对bounding_box内节点调用updateNode()强制重算概率。
例如,在 ROS 中发送清除请求:
rosservice call /octomap_server/clear_bbx "min: {x: 1.0, y: 2.0, z: 0.0} max: {x: 5.0, y: 4.0, z: 2.5}"该操作耗时 <50ms(Intel i7),比全图重建快 10 倍以上,适合长期运行的巡检机器人。
4.4 性能对比表:不同建图方案在典型室内场景下的指标
| 方案 | 点云密度 | OctoMap 内存占用(100m²) | 建图时间(i7-11800H) | 动态物体鲁棒性 | ROS 兼容性 |
|---|---|---|---|---|---|
rtabmap+octomap_server | 中(~10k pts/frame) | 1.2 GB | 8 min | 差(拖影明显) | 开箱即用 |
ORB-SLAM2+DepthAnythingV2+OctoMap | 高(~200k pts/frame) | 0.8 GB | 12 min | 优(可滤动态帧) | 需自编译 |
hdl_graph_slam+octomap | 低(激光线数限制) | 0.5 GB | 5 min | 中(依赖 IMU 补偿) | 需适配激光话题 |
注意:内存占用指
.bt文件大小;建图时间为从原始图像到.ot生成的端到端耗时。ORB-SLAM方案虽耗时略长,但点云几何一致性最佳,尤其适合需要高精度定位的 AMR 场景。
5. 调试 OctoMap 占据异常的三个必查项:坐标系、尺度、深度范围
当octovis显示地图被“压扁”、楼层错位或走廊变窄,问题几乎总出在这三项。不要盲目调参,先做确定性检查。
5.1 坐标系一致性验证:用tf_echo查看map→camera_link变换
ORB-SLAM 输出的T_wc是世界到相机变换,而 ROS 中map坐标系应与之对齐。运行:
rosrun tf tf_echo map camera_link若输出Translation: [0.0, 0.0, 0.0]且Rotation: [0, 0, 0, 1],说明map与camera_link重合——这正是我们期望的。若存在大偏移,检查orb_slam2_ros的publish_tf参数是否为true,以及static_transform_publisher是否误加了额外变换。
5.2 尺度真实性检验:测量点云中已知尺寸物体的长度
取一张包含 A4 纸(210mm×297mm)的图像,用open3d可视化其点云,测量两点间欧氏距离:
# 加载 dense_map.ply pcd = o3d.io.read_point_cloud("dense_map.ply") # 用鼠标框选 A4 纸四个角点,获取索引 idxs points = np.asarray(pcd.points) dist = np.linalg.norm(points[idxs[0]] - points[idxs[1]]) # 应 ≈ 0.210若测得0.105m,说明整体尺度缩小 2 倍——根源在 DepthAnythingV2 的pred_max_depth与实际场景不符,或 ORB-SLAM 的单目初始化尺度未归一化。此时需用已知尺寸物体(如标定板)重跑 ORB-SLAM,并启用--scale参数强制校准。
5.3 深度图范围诊断:直方图分析depth_*.exr的数值分布
用 OpenCV 统计所有深度图的像素值分布:
import cv2 import numpy as np import matplotlib.pyplot as plt depths = [] for i in range(100): d = cv2.imread(f'depth_{i:05d}.exr', cv2.IMREAD_UNCHANGED) depths.append(d[d > 0.1]) # 剔除无效值 all_depths = np.concatenate(depths) plt.hist(all_depths, bins=100, range=(0.1, 10.0)) plt.xlabel('Depth (m)') plt.ylabel('Pixel count') plt.show()健康分布应呈右偏峰形,峰值在1.0~3.0m(人眼常观距离),且>8.0m像素占比 <5%。若峰值在0.3m且长尾拖至20m,说明 DepthAnythingV2 过度预测远距离——需降低pred_max_depth至8.0并重跑。
提示:EXR 文件必须用
cv2.IMREAD_UNCHANGED读取,否则 float32 会被转为 uint16 导致深度值失真。
本文还有配套的精品资源,点击获取