19f1c65f9e083.png

一句话概括: 用 Astra Pro 深度相机 + 轮式编码器,让仓库机器人在未知环境中边走边建图、秒速重定位,还能自动绕开动态障碍物——关键是,不用 IMU 也不会飞飘!

*来源: 第二届 3D 视觉创新应用竞赛 | 作者:** 胡明豪 *

*硬件平台:** Astra Pro 深度相机 + NVIDIA TX2 + 轮式编码器 *

*GitHub:** RobotV_Storage-robot*


📌 一、项目亮点速览

🔥 核心能力 🛠️ 技术实现 💡 亮点解读
🚫 禁用 IMU,告别"飞飘" 轮式编码器替代 IMU 作为前端位移估计 比惯性测量更稳,比纯视觉更准
🏗️ 半稠密点云建图 RGB-D 点云 + OctoMap 八叉树增量拼接 大规模场景实时性好,栅格地图随时可用
📍 任意地点重定位 加载已有姿态图 + 回环检测 关机再开机,秒回世界坐标
🏃 动态障碍物检测 借鉴 RTAB-Map 点云分割算法 仓库里飞奔的叉车?轻松绕开!
🧭 可配置回环策略 mapping / location 双模式切换 建图时全量回环,定位时只匹配关键帧
⚡ 灵活部署 ROS 模块化 launch 启动 一键启动建图 / 定位

🌍 二、背景与意义

2.1 仓库自动化的"最后一公里"

现代仓储物流行业正经历一场自动化革命 📦。AGV(自动导引车)和移动机器人需要在未知或半未知的仓库环境中完成以下任务:

  • 🗺️ 自主建图:第一次进入仓库,边走边构建环境地图
  • 📍 精确定位:在已有地图中快速确定自身位置
  • 🏃 动态避障:检测并绕过移动的叉车、行人等障碍物
  • 🧭 路径规划:基于栅格地图规划最优行驶路线

2.2 传统方案的痛点

😱 痛点 🔍 原因分析
IMU 积分漂移严重 长时间运行后姿态估计偏差累积,轨迹"飞飘"
纯视觉里程计不稳定 光照变化、纹理缺失场景下容易跟丢
稠密建图算力吃紧 全像素点云拼接对嵌入式平台(TX2)压力过大
重定位困难 缺乏回环检测或姿态图优化,关机后丢失全局坐标

2.3 本方案的解决思路

核心洞察: 仓库小车的轮式编码器能提供比 IMU 更稳定的位移估计,Astra Pro 的深度信息能直接生成半稠密点云——把两者结合,既稳又准!


🛠️ 三、系统硬件架构

3.1 硬件组成一览

19f1c72a6cf2d.png
🔧 设备 📋 型号 / 规格 🎯 功能
📷 深度相机 Astra Pro(单目结构光) RGB + Depth 数据采集,640×480@30fps
💻 计算平台 NVIDIA TX2 运行 VINS-Fusion 算法 + OctoMap 建图
🔄 位移传感器 轮式编码器 提供精确的前后 / 旋转位移估计
🦾 执行机构 5 自由度机械臂 + 气动夹爪 抓取 & 搬运(部分场景)
⚡ 电源系统 电控柜 + 锂电池 整车供电

3.2 Astra Pro 相机参数

参数 规格
成像技术 单目结构光
深度范围 0.6m ~ 8m
RGB 分辨率 640 × 480
Depth 分辨率 640 × 480
帧率 30 fps(固定)
深度单位 毫米(代码中需 ×0.001 转为米)

⚠️ 踩坑提醒: Astra Pro 的 RGB 相机没有出厂内参,需要用 ROS camera_calibration 包手动标定!标定结果示例见 astrapro.yaml。

3.3 轮式编码器

  • 提供小车前进/后退位移 + 旋转角度
  • 替代 IMU 作为 VINS 前端位姿估计输入
  • 优势:无积分漂移,长时间运行稳定

🎬 四、系统整体流程

[配图说明] Image Prompt: Clean flowchart diagram showing the complete VSLAM pipeline, 16:9 aspect ratio. Left to right flow: Astra Pro camera (RGB+Depth) → Wheel Encoder data → VINS-Fusion Frontend (feature tracking + pose estimation) → Sliding Window Optimization → Loop Closure Detection → Pose Graph Optimization → OctoMap 3D Mapping → Grid Map Generation → Navigation (move_base). Each stage in a rounded rectangle box with icons. Hand-drawn vector style with arrows showing data flow.

4.1 从"看见"到"建图"的完整链路

19f1c77206503.png

4.2 建图模式 vs 定位模式

模式 配置参数 回环策略 适用场景
🏗️ Mapping AGV_MODEL=mapping 与所有关键帧回环 首次进入未知环境
📍 Location AGV_MODEL=location 仅与已加载姿态图关键帧回环 已有地图,快速重定位

🔬 五、核心算法详解

5.1 VINS-Fusion 前端:特征跟踪

19f1c79b360dd.png

VINS-Fusion 的前端负责从连续图像帧中跟踪特征点,核心流程如下:

  1. FAST 角点检测:每帧提取角点作为特征
  1. BRIEF 描述子计算:用预定义的随机点对模式生成二进制描述子
  1. 光流追踪:KLT 光流将特征从前一帧追踪到当前帧
  1. 深度相机处理:利用 Astra Pro 的深度信息,将 2D 特征反投影为 3D 点

关键代码(feature_tracker.cpp 核心逻辑):

// 1. FAST 角点检测
cv::FAST(image, new_features, fast_threshold, true);

// 2. KLT 光流追踪
std::vector<uchar> status;
cv::calcOpticalFlowPyrLK(prev_img, cur_img, prev_pts, cur_pts, status, err);

// 3. 深度信息关联:将 2D 特征点映射到 3D
for (auto &pt : cur_pts) {
    float depth = depth_image.at<float>(pt.y, pt.x);
    if (depth > 0) {
        Eigen::Vector3d point_3d = camera_model->projectPixelTo3D(pt, depth);
        points_3d.push_back(point_3d);
    }
}

5.2 轮式编码器替代 IMU:告别"飞飘"

这是本项目的最大创新点之一 🏆

问题:为什么 IMU 会飞飘?

VINS-Fusion 原本设计为视觉-惯性融合(VIO),使用 IMU 的加速度计和陀螺仪数据作为运动先验。但在实际测试中:

现象 原因
轨迹突然"飞走" IMU 零漂 + 积分误差累积
静止时仍在漂移 陀螺仪 bias 估计不准确
增加迭代次数有改善但仍不稳定 优化问题在某些场景下非凸

解决方案:轮式编码器上场!

// 伪代码:轮式编码器数据接入前端
void EncoderCallback(const nav_msgs::Odometry::ConstPtr& odom) {
    // 获取编码器测量的位移和旋转
    double dx = odom->pose.pose.position.x - last_x;
    double dy = odom->pose.pose.position.y - last_y;
    double dyaw = getYaw(odom->pose.pose.orientation) - last_yaw;
    
    // 直接作为前端位姿估计输入(替代 IMU 积分结果)
    Eigen::Vector3d encoder_t(dx, dy, 0);
    Eigen::Matrix3d encoder_R = Eigen::AngleAxisd(dyaw, Eigen::Vector3d::UnitZ()).toRotationMatrix();
    
    // 送入 VINS 估计器
    estimator->inputEncoder(encoder_t, encoder_R, odom->header.stamp.toSec());
}

💡 核心优势: 轮式编码器的位移测量是相对增量,不会像 IMU 那样产生累积漂移。在小车匀速行驶时,编码器精度可达厘米级甚至毫米级!

5.3 关键帧管理 & 回环检测

19f1c7cebe078.png

关键帧(KeyFrame)是 VINS 系统的核心数据结构,每个关键帧包含:

字段 类型 说明
time_stamp double 时间戳
vio_T_w_i / vio_R_w_i Vector3d / Matrix3d VIO 估计的位姿
T_w_i / R_w_i Vector3d / Matrix3d 优化后的全局位姿
point_3d vector<cv::Point3f> 3D 点云
point_2d_uv vector<cv::Point2f> 图像平面坐标
brief_descriptors vector<BRIEF::bitset> BRIEF 描述子
loop_info Matrix<double, 8, 1> 回环相对位姿(t + q + yaw)

回环检测流程(简化伪代码):

输入:当前关键帧 cur_kf
输出:是否检测到回环 + 相对位姿

1. 从词袋模型中检索候选关键帧 candidates[]
2. 对 cur_kf 和每个 candidate 做 BRIEF 描述子匹配
3. 用 RANSAC 剔除误匹配 → 得到 inliers
4. 若 inliers ≥ MIN_LOOP_NUM:
   a. 用 PnP-RANSAC 求解候选帧 → 当前帧的相对位姿
   b. 检查几何一致性(yaw < 30°, 位移 < 20m)
   c. 通过 → 建立回环约束
5. 将回环信息推入优化队列

关键代码(keyframe.cpp — findConnection 函数核心逻辑):

// Step 1: BRIEF 描述子匹配
searchByBRIEFDes(matched_2d_old, matched_2d_old_norm, status,
                  old_kf->brief_descriptors, old_kf->keypoints, old_kf->keypoints_norm);

// Step 2: RANSAC 剔除误匹配
FundmantalMatrixRANSAC(matched_2d_cur_norm, matched_2d_old_norm, status);

// Step 3: PnP-RANSAC 求解相对位姿
PnPRANSAC(matched_2d_old_norm, matched_3d, status, PnP_T_old, PnP_R_old);

// Step 4: 几何一致性检查
relative_t = PnP_R_old.transpose() * (origin_vio_T - PnP_T_old);
relative_yaw = normalizeAngle(R2ypr(origin_vio_R).x() - R2ypr(PnP_R_old).x());

if (abs(relative_yaw) < 30.0 && relative_t.norm() < 20.0) {
    has_loop = true;
    loop_info << relative_t.x(), relative_t.y(), relative_t.z(),
              relative_q.w(), relative_q.x(), relative_q.y(), relative_q.z(),
              relative_yaw;
}

5.4 姿态图优化(Pose Graph Optimization)

当检测到回环后,系统需要对整个轨迹进行全局优化,消除累积漂移。

4-DoF 优化(无 IMU 模式)

由于禁用了 IMU,系统只优化 x、y、z 平移 + yaw 旋转(4 自由度),pitch 和 roll 不可观测:

// 优化的代价函数(简化)
// 只优化 x, y, z, yaw —— pitch 和 roll 由相机安装方式确定
for (auto &kf : keyframelist) {
    // 回环约束
    if (kf->has_loop) {
        Vector3d loop_t = kf->getLoopRelativeT();
        Quaterniond loop_q = kf->getLoopRelativeQ();
        // 将回环约束加入优化问题
        problem.AddResidualBlock(
            new CeresPoseGraphFactor(loop_t, loop_q, /*只优化x,y,z,yaw*/)
        );
    }
}
ceres::Solve(options, &problem, &summary);

漂移补偿逻辑

当检测到跨序列回环时,系统会一次性补偿整个序列的漂移:

// 跨序列回环:将整个序列的位姿整体平移+旋转
if (old_kf->sequence != cur_kf->sequence && sequence_loop[cur_kf->sequence] == 0) {
    w_r_vio = shift_r;   // 累积旋转漂移
    w_t_vio = shift_t;   // 累积平移漂移
    
    // 遍历该序列所有关键帧,统一补偿
    for (auto &kf : keyframelist) {
        if (kf->sequence == cur_kf->sequence) {
            Vector3d P; Matrix3d R;
            kf->getVioPose(P, R);
            P = w_r_vio * P + w_t_vio;
            R = w_r_vio * R;
            kf->updateVioPose(P, R);
        }
    }
    sequence_loop[cur_kf->sequence] = 1;  // 标记该序列已补偿
}

5.5 OctoMap 八叉树建图

19f1c7ed7fa16.png

核心思路

传统点云拼接的问题:每帧点云都累加 → 内存爆炸 💥

OctoMap 的解决方案:用八叉树分区存储 → 相同区域只保留最新状态

建图流程

// 1. 接收当前帧的 RGB-D 点云
void OctomapCallback(const sensor_msgs::PointCloud2::ConstPtr& cloud_msg) {
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
    pcl::fromROSMsg(*cloud_msg, *cloud);
    
    // 2. 获取当前相机位姿
    Eigen::Isometry3d camera_pose = getCurrentCameraPose();
    
    // 3. 将点云变换到世界坐标系
    pcl::transformPointCloud(*cloud, *cloud_world, camera_pose.inverse());
    
    // 4. 插入 OctoMap(内部自动处理遮挡和更新)
    for (auto &pt : cloud_world->points) {
        octomap->updateNode(pt.x, pt.y, pt.z, true);  // 标记为 occupied
    }
    
    // 5. 发布栅格地图供导航使用
    publishGridMap();
}

栅格地图生成

OctoMap 输出的是三维体素地图,而 move_base 导航需要二维栅格地图。系统通过 grid_mapper 模块完成转换:

// grid_mapper.cpp:基于射线投射的栅格更新
void GridMapper::updateMap(Vector3d pointWorld, Vector3d PG_T) {
    // 从传感器位置 PG_T 到观测点 pointWorld 发射"射线"
    double angle = atan2(pointWorld[1] - PG_T[1], pointWorld[0] - PG_T[0]);
    double z = sqrt((pointWorld[0]-PG_T[0])^2 + (pointWorld[1]-PG_T[1])^2);
    
    // 沿射线遍历每个栅格
    for (double r = 0; r < z + cell_size; r += inc_step) {
        Eigen::Vector2d p_w(r * cos(angle) + PG_T[0], r * sin(angle) + PG_T[1]);
        updateGrid(p_w, laserInvModel(r, z, cell_size));
    }
}

// 激光逆测量模型
double GridMapper::laserInvModel(double r, double R, double cell_size) {
    if (r < R - 0.5 * cell_size)  return P_free_;   // 射线经过 → 空闲
    if (r > R + 0.5 * cell_size)  return P_prior_;   // 超出范围 → 先验
    return P_occ_;  // 在观测点附近 → 占用
}

栅格地图参数说明:

参数 值 说明
地图尺寸 1500 × 500 cells 覆盖 75m × 25m 区域
栅格分辨率 0.05m(5cm) 兼顾精度与内存
原点偏移 (750, 250) 地图中心对齐世界原点
占用概率 P_occ = 0.6 观测到物体时的概率
空闲概率 P_free = 0.4 射线经过的空区域
先验概率 P_prior = 0.5 未观测区域

5.6 动态障碍物检测

仓库环境中有大量动态物体(叉车、行人、其他 AGV),静态地图无法处理这些情况。本方案借鉴 RTAB-Map 的点云分割算法:

// 伪代码:动态障碍检测
void DetectDynamicObstacles(pcl::PointCloud<pcl::PointXYZRGB>::Ptr current_cloud,
                           pcl::PointCloud<pcl::PointXYZRGB>::Ptr map_cloud) {
    // 1. 将当前帧点云与地图点云做 ICP 对齐
    pcl::IterativeClosestPoint<pcl::PointXYZRGB, pcl::PointXYZRGB> icp;
    icp.setInputSource(current_cloud);
    icp.setInputTarget(map_cloud);
    icp.align(*aligned_cloud);
    
    // 2. 计算残差:残差大的点 = 动态物体
    for (int i = 0; i < current_cloud->size(); i++) {
        double distance = (current_cloud->points[i].x - map_cloud->points[i].x)^2 + ...;
        if (distance > dynamic_threshold) {
            dynamic_obstacles.push_back(current_cloud->points[i]);
        }
    }
    
    // 3. 将动态障碍发布到 navigation stack
    publishDynamicObstacles(dynamic_obstacles);
}

5.7 KCF 目标跟踪(主动跟随模式)

19f1c8105fcaf.png

系统还包含一个主动跟随功能,使用 KCF(Kernelized Correlation Filter) 跟踪算法:

// kcftracker.cpp:KCF 跟踪器核心
// 1. 初始化:在首帧中框选目标
tracker.init(selected_roi, first_frame);

// 2. 每一帧更新目标位置
cv::Rect new_roi = tracker.update(current_frame);

// 3. 根据目标位置计算控制指令
double target_cx = new_roi.x + new_roi.width / 2.0;
double image_cx = current_frame.cols / 2.0;
double angular_error = (target_cx - image_cx) / image_cx;

// 4. 发布速度指令
geometry_msgs::Twist cmd;
cmd.linear.x = forward_speed;
cmd.angular.z = -angular_error * kp;  // P 控制器
cmd_vel_pub.publish(cmd);

KCF 算法特点:

特性 说明
🏎️ 速度极快 基于 FFT 的核相关滤波,CPU 实时运行
📐 HOG 特征 对光照变化鲁棒
🔄 多尺度 支持目标尺寸变化(scale_step = 1.05)
🎯 亚像素精度 二次插值实现峰值精确定位

📊 六、性能指标

6.1 建图精度

指标 数值 说明
回环检测最小匹配点数 可配置(默认 50+) MIN_LOOP_NUM 参数
位姿图优化频率 后台线程异步执行 不阻塞前端
栅格地图分辨率 5cm 满足 AGV 导航需求
点云拼接方式 增量式(OctoMap) 内存可控

6.2 系统帧率

模块 帧率 平台
Astra Pro 数据采集 30 FPS 硬件固定
特征跟踪 + 位姿估计 ~25-30 FPS TX2
OctoMap 更新 ~15-20 FPS TX2
KCF 跟踪 ~50+ FPS TX2

6.3 重定位性能

场景 重定位时间 精度
同位置启动 < 1s 厘米级
异地启动(需回环) 2-5s 依赖环境特征丰富度

🛠️ 七、技术栈总览

类别 技术选型 版本 / 说明
🐧 操作系统 Ubuntu 16.04 / 18.04
🤖 机器人框架 ROS Kinetic / Melodic
📷 相机驱动 OpenNI2 + libuvc_ros Astra Pro 适配
🔬 视觉算法 VINS-Fusion(改造版) 禁用 IMU,接入编码器
🌳 建图库 OctoMap 八叉树体素地图
🧭 导航库 move_base A* / Dijkstra 路径规划
📐 线性代数 Eigen 矩阵运算
🖼️ 图像处理 OpenCV 2.4.8
☁️ 点云处理 PCL 1.7
📈 优化库 Ceres Solver 姿态图优化
📦 编译系统 CMake ≥ 2.8.3

💡 八、技术创新亮点

🏆 创新点 📝 说明 🎯 解决的问题
🔄 轮式编码器替代 IMU 禁用 IMU,用编码器数据作为前端位移估计 彻底消除飞飘问题
🗺️ OctoMap 增量建图 只发布当前帧点云,后端异步拼接 大规模场景下实时性好
📍 双模式切换 mapping / location 两种 launch 配置 建图和定位灵活切换
🏃 动态障碍检测 借鉴 RTAB-Map 点云分割 仓库动态环境安全导航
🔧 4-DoF 优化 只优化 x, y, z, yaw 无 IMU 时依然鲁棒

与传统方案对比

对比项 传统 VINS-Fusion(IMU) 本方案(编码器)
静止时轨迹稳定性 ❌ 容易飞飘 ✅ 稳定不动
长时间运行漂移 ❌ 累积严重 ✅ 编码器无积分漂移
回环校正效果 ⚠️ 依赖 IMU bias 估计 ✅ 直接补偿平移+偏航
嵌入式平台适配 ⚠️ 算力要求高 ✅ TX2 流畅运行

🧩 九、踩坑与解决方案

9.1 已解决的问题

😱 问题 🔍 原因 ✅ 解决方案
IMU 导致轨迹飞飘 零漂 + 积分累积 禁用 IMU,使用轮式编码器
RGB 相机无内参 Astra Pro SDK 不提供 用 camera_calibration 标定
深度单位不匹配 Astra 输出毫米,代码期望米 读取后 ×0.001 转换
RGB 与 Depth 时间戳不同步 硬件限制,最大差 ~20ms 使用 ApproximateTimeSynchronizer
TX2 内存不足 稠密点云全量保存 改用 OctoMap 增量式
KCF 转角大时目标丢失 图像平面投影后目标出界 限制最大转角,增加重检测逻辑

9.2 已知局限

⚠️ 局限 📝 说明 🛠️ 改进方向
室外效果差 结构光受阳光干扰 改用 ToF 或双目方案
只测试了 Astra Pro 其他相机未验证 扩展支持 Realsense 等
KCF 跟踪不够鲁棒 遮挡 / 快速运动下易丢失 融合 YOLO 检测做重检测
TX2 算力瓶颈 大场景建图帧率下降 升级到 Orin Nano

🚀 十、如何使用

10.1 环境依赖

# Ubuntu 16.04 + ROS Kinetic
sudo apt install ros-kinetic-desktop-full
sudo apt install ros-kinetic-cv-bridge ros-kinetic-image-transport
sudo apt install ros-kinetic-opencv3 ros-kinetic-pcl-ros

# Ceres Solver
git clone https://ceres-solver.googlesource.com/ceres-solver
cd ceres-solver && mkdir build && cd build
cmake .. && make -j4 && sudo make install

# OctoMap
sudo apt install ros-kinetic-octomap ros-kinetic-octomap-msgs

10.2 编译

cd ~/catkin_ws/src
git clone https://github.com/3DCVdeveloper/RobotV_Storage-robot.git
cd ..
catkin_make
source devel/setup.bash

10.3 运行 —— 建图模式

# Terminal 1: 启动 VINS-Fusion(建图模式)
roslaunch vins vins_astrapro_map.launch

# Terminal 2: 启动回环融合
rosrun loop_fusion loop_fusion_node 
  ~/catkin_ws/src/RobotV_Storage-robot/config/astrapro/realsense_depth_imu_config_astrapro.yaml

# Terminal 3: 启动 OctoMap 建图服务
roslaunch octomap_service your_octomap.launch

# Terminal 4: 启动 RViz 可视化
roslaunch vins vins_rviz.launch

10.4 运行 —— 定位模式

# Terminal 1: 启动定位节点
roslaunch loop_fusion loop_fusion_astrapro_location.launch

# Terminal 2: 启动 VINS 节点(加载已有姿态图)
rosrun vins vins_node 
  ~/catkin_ws/src/RobotV_Storage-robot/config/astrapro/realsense_depth_imu_config_astrapro_location.yaml

# Terminal 3: RViz 可视化
roslaunch vins vins_rviz.launch

10.5 保存地图

# 保存 OctoMap 点云地图
rosservice call /octomap_save "map.bt"

# 保存栅格地图(由 grid_mapper 提供)
# 地图自动保存为 map.png + map.yaml

🌟 十一、总结与展望

核心贡献

  1. 🔄 轮式编码器替代 IMU —— 从根本上解决了 VINS 在嵌入式平台上的飞飘问题
  1. 🗺️ OctoMap 增量建图 —— 大规模场景下实时性好,栅格地图直接用于导航
  1. 📍 任意地点重定位 —— 加载已有姿态图,秒级恢复全局坐标
  1. 🏃 动态障碍感知 —— 仓库动态环境下的安全导航保障
  1. 🔧 工程化适配 —— 完整的 ROS launch 配置、双模式切换、参数化 yaml 配置

未来方向

🚀 方向 🎯 目标
⚡ 硬件升级 TX2 → Orin Nano,提升建图帧率
🧠 语义建图 融合深度学习目标检测,给地图加标签(货架、通道、障碍)
📱 多机协同 多台 AGV 共享地图,协同建图
🎯 更鲁棒的跟踪 KCF + YOLO 融合,解决遮挡和快速运动
🌐 多传感器融合 编码器 + 视觉 + IMU 三模融合,自适应切换

🌟 写在最后: 这个项目最让人佩服的地方在于——它解决了一个真实的工程痛点。IMU 飞飘不是什么新鲜话题,但真正在嵌入式平台上用轮式编码器干净利落地替代掉 IMU,还把整个建图-定位-导航链路跑通,是需要扎实功力的。对于仓储物流场景,这套方案提供了低成本、高鲁棒性的 VSLAM 解决思路,值得每一位做移动机器人的同学参考!🏆

相关资源:

  • 📖 VINS-Fusion 原版: https://github.com/HKUST-Aerial-Robotics/VINS-Fusion
  • 📖 RGB-D 适配参考: https://github.com/ManiiXu/VINS-Fusion-RGBD
  • 📖 OctoMap 文档: http://octomap.github.io

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注