
简介本资源是一套基于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词典与典型场景测试配置特别适合需快速复现SLAMOctoMap应用的学生项目实践。1. 为什么用 ORB-SLAM 做稠密重建 OctoMap 建图比直接跑 ROS 的rtabmap或hdl_graph_slam更可控很多做室内移动机器人导航的工程师卡在第一步地图质量不稳定。要么激光雷达建图太稀疏、穿墙漏检要么 RGB-D 直接用rgbd_odometryoctomap_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 Runtimepip install depthanythingv2 python -c from depth_anything_v2.dpt import DepthAnythingV2 model DepthAnythingV2( encodervitl, features256, out_channels[256, 512, 1024, 1024], pretrainedcheckpoints/depth_anything_v2_vitl.pth ) model.load_state_dict(torch.load(checkpoints/depth_anything_v2_vitl.pth, map_locationcpu)) model.eval() 实际批量处理时需按KeyFrameTrajectory.txt中的时间戳顺序读取对应图像如frame_00001.png并确保图像尺寸与训练分辨率一致默认 518×518。关键参数说明参数值说明encodervitlViT-Large精度最高若显存不足可用vitsViT-Smallinput_size(518, 518)必须与训练尺寸一致否则深度值失真pred_max_depth20.0截断远距离噪声室内场景设为10.0更佳注意DepthAnythingV2 输出的深度图是单通道 float32单位为米。需用cv2.imwrite(depth_00001.exr, depth_map)保存为 EXR 格式保留浮点精度避免 PNG 量化损失。2.3 将深度图 位姿 相机内参反投影为点云坐标系对齐是成败关键此处极易出错ORB-SLAM 使用 OpenCV 坐标系Z 向前而大多数深度估计模型输出符合 OpenGLZ 向外。必须统一为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_scale1.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))], axis1) points_world (extrinsics points_homo.T).T[:, :3] return points_world # 示例加载第 i 帧 depth cv2.imread(fdepth_{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远距离深度不准的点统计离群点移除SORopen3d.geometry.statistical_outlier_removal(pcd, nb_neighbors20, std_ratio2.0)体素下采样pcd.voxel_down_sample(voxel_size0.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_size0.02) o3d.io.write_point_cloud(dense_map.ply, full_pcd, write_asciiFalse, compressedTrue)导出前务必用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 工具链官方 OctoMapv2.0.0不内置 PLY 解析器。需手动启用OCTOMAP_PCL并链接pcl_iogit clone https://github.com/OctoMap/octomap.git cd octomap mkdir build cd build cmake -DOCTOMAP_PCLON -DBUILD_OCTOVISOFF -DCMAKE_BUILD_TYPERelease .. make -j$(nproc) sudo make install注意-DOCTOMAP_PCLON启用 PCL 支持但需提前apt install libpcl-devUbuntu 22.04或brew install pclmacOS。若编译报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.05m5cm平衡精度与内存 octomap::OcTree tree(0.05); // 加载 PLY 点云 pcl::PointCloudpcl::PointXYZ::Ptr cloud(new pcl::PointCloudpcl::PointXYZ); if (pcl::io::loadPLYFile(argv[1], *cloud) -1) { PCL_ERROR(Couldnt read file %s\n, argv[1]); return -1; } // 插入所有点maxrange5.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); } // 设置概率参数默认 probHit0.7, probMiss0.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 -stdc14 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提示maxrange5.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 社区更常用.otOctoMap 格式。用octomap_saver转换octomap_saver -f indoor_nav_map.ot indoor_nav_map.bt生成的indoor_nav_map.ot可直接在 ROS Launch 文件中指定node pkgoctomap_server typeoctomap_server_node nameoctomap_server param nameresolution value0.05 / param namesensor_model/max_range value5.0 / param namesave_directory value$(find my_nav)/maps / param namemap_file_name value$(find my_nav)/maps/indoor_nav_map.ot / /node注意octomap_server加载.ot后会发布/octomap_fulloctomap_msgs/Octomap和/occupied_cells_vis_arrayvisualization_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_binaryoctomap_msgs/Octomap其只包含occupied/free状态无概率字段体积小、解析快rostopic hz /octomap_binary # 验证是否 ≥10Hz在自研控制器中用octomap::OcTree解析该消息void octomapCallback(const octomap_msgs::Octomap::ConstPtr msg) { octomap::OcTree* tree dynamic_castoctomap::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}该操作耗时 50msIntel i7比全图重建快 10 倍以上适合长期运行的巡检机器人。4.4 性能对比表不同建图方案在典型室内场景下的指标方案点云密度OctoMap 内存占用100m²建图时间i7-11800H动态物体鲁棒性ROS 兼容性rtabmapoctomap_server中~10k pts/frame1.2 GB8 min差拖影明显开箱即用ORB-SLAM2DepthAnythingV2OctoMap高~200k pts/frame0.8 GB12 min优可滤动态帧需自编译hdl_graph_slamoctomap低激光线数限制0.5 GB5 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(fdepth_{i:05d}.exr, cv2.IMREAD_UNCHANGED) depths.append(d[d 0.1]) # 剔除无效值 all_depths np.concatenate(depths) plt.hist(all_depths, bins100, 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 导致深度值失真。本文还有配套的精品资源点击获取