Skip to content
Published on

机器人之眼 — 3D 感知与 SLAM

分享
Authors

引言

人一睁眼,几乎不费力气就能把握房间的结构 —— 桌子在哪儿、到门口要走几步、杯子是不是伸手可及,全都瞬间知道。但对机器人来说,这件事绝非理所当然。相机交给它的,只是一格一格的像素,一串颜色数值的排列。从这里出发,回答"我现在处于三维空间中的哪个位置、周围有什么"这个问题,正是机器人感知(perception)的核心。

本文按步骤拆解机器人看世界的方法。首先看看用哪些传感器获取深度信息,再解释如何把这些数据编织成地图的 SLAM。接着讨论点云、体素这类 3D 表示,估计物体位置与朝向的 6D 姿态估计,基于深度学习的检测与分割,最后聊聊近来备受关注的 NeRF 与 3D 高斯溅射对机器人感知意味着什么。

准确性优先,同时也会配上图示和简单的代码,帮助直观把握概念。

为什么 3D 感知很难

2D 图像是 3D 世界投影到平面上的结果。投影过程中深度信息会丢失,所以仅凭一个像素,无法知道该点距相机有多远。这被称为深度的模糊性(depth ambiguity)。

     世界(3D)                     图像(2D)
   ┌───────────┐               ┌───────────┐
   │    ●  物体A(近)      │  投影     │    ●      │
   │       ●  物体B(远) ──┼────────▶ │    ●      │  即使像素位置相同
   │                       │           │           │  深度也无法得知
   └───────────┘               └───────────┘
        相机光线上的所有点都重叠到同一个像素

要复原深度,需要额外的信息。双眼(立体视觉)的视差、主动发射光线的往返时间,或者学习得到的先验知识(单目深度估计)等,都是常见手段。机器人感知归根到底可以概括为"找回丢失的维度"这项工作。

获取深度的传感器

立体相机

像人的双眼一样,使用相隔一定距离(基线,baseline)的两台相机。同一个物体在两幅图像中会出现在不同位置,这个差异称为视差(disparity),与深度成反比。

深度 Z = (焦距 f x 基线 B) / 视差 d

  左相机          右相机
    │  \           /  │
    │   \         /   │
    │    \       /    │
    │     \  ●  /     │   ● 在左右图像中
    │      \   /      │     出现位置的差 = 视差 d
    └───B(基线)───┘

视差越大,物体越近。计算量较大,而且在没有纹理的墙面这类场景中很难找到对应点,这是它的局限;但因为是被动(passive)传感器,在室外也能很好地工作。

RGB-D 相机

同时提供颜色(RGB)和深度(Depth)的传感器。代表性方式有结构光(structured light)和 ToF(Time of Flight)。结构光把已知图案投射到物体上,读取其形变来计算深度;ToF 则测量光线发出后返回所需的时间。

RGB-D 输出 = 彩色帧 + 对齐后的深度帧

  像素 (u, v) ──▶ 颜色 (R, G, B)
              └─▶ 深度 Z(米)

  用相机内参(intrinsics)复原 3D 点:
    X = (u - cx) * Z / fx
    Y = (v - cy) * Z / fy
    Z = Z

RGB-D 在室内价格低廉、使用方便,但在强烈日光(红外干扰)或玻璃、镜面等表面上表现较差。

LiDAR

旋转发射激光并向四周扫射,通过各方向返回的时间测量距离。得到的结果是非常精密的 3D 点云。在自动驾驶中被广泛使用,不受光照条件影响,远距离精度也很高。缺点是没有颜色信息,价格相对昂贵。

传感器深度原理优点局限
立体两个视点的视差便宜、可用于室外、含颜色无纹理表面表现差
RGB-D(结构光/ToF)主动投光/往返时间室内精密、含颜色怕日光、怕玻璃
LiDAR激光往返时间远距离、高精度、不受光照影响昂贵、无颜色

实际的机器人会把这些传感器组合起来,互相弥补对方的短板。这称为传感器融合(sensor fusion)。

SLAM — 同步定位与建图

SLAM(Simultaneous Localization and Mapping)是机器人在未知环境中移动时,需要同时解决的两个问题。一个是"我现在在哪儿"(定位,localization),另一个是"周围长什么样"(建图,mapping)。

问题在于,这两者是先有鸡还是先有蛋的关系。有了准确的地图就能知道自己的位置,知道准确的位置就能画出地图,可一开始两者都没有。SLAM 要做的,就是同时优化这两个估计。

SLAM 流水线

 传感器输入          前端                        后端
 ┌────────┐    ┌──────────────────┐    ┌──────────────────┐
 │ 相机    │──▶ │ 特征提取/匹配      │──▶ │ 位姿图优化         │
 │ LiDAR  │    │ 里程计估计         │    │ 光束平差(BA)       │
 │ IMU    │──▶ │ (帧间运动)         │    │ 回环校正反馈       │
 └────────┘    └────────┬─────────┘    └────────┬─────────┘
                        │                        │
                        ▼                        ▼
                 ┌──────────────┐        ┌──────────────┐
                 │ 局部地图更新   │◀──────│ 全局地图对齐   │
                 └──────────────┘        └──────────────┘
                 回环检测 ──▶ 识别"以前来过这里" → 校正累积误差
  • 前端(front-end)快速估计连续帧之间的相对运动。这称为视觉里程计(visual odometry),每一帧都会累积一点误差(drift)。
  • 回环检测(loop closure)是识别机器人是否回到了以前访问过的地点这一步骤。一旦意识到"这里是刚才走过的那条走廊",就获得了一次性校正累积误差的线索。
  • 后端(back-end)把多个时刻的位姿汇总成一个大的优化问题,生成全局一致的地图。光束平差(bundle adjustment)和位姿图优化(pose graph optimization)是核心工具。

误差累积与回环检测

如果只用视觉里程计,位置估计会随时间推移而逐渐偏移。绕了 100 米回到出发点,但在地图上却显示偏离了几米,就是这种情况。回环检测会侦测这种不一致,并重新调整整个位姿图,把误差分散开来。

回环检测前                       回环检测后
   起点 ●                          起点 ●
        \                              \
         \  (因漂移               │  (误差已均匀
          \   而分离)               │   分散到整条路径)
           ● ← 其实是同一个地方     ● ← 已对齐

3D 表示 — 点云、体素、占据栅格

要让机器人处理传感器给出的深度信息,需要用合适的数据结构来表示。

点云

散布在 3D 空间中的一组点。每个点都有坐标 (x, y, z),有时还带有颜色或反射强度。这是最直接的表示方式,但点的数量庞大、没有顺序(unordered)、密度也不均匀。

import numpy as np

# 将深度图像反投影为点云
def depth_to_pointcloud(depth, fx, fy, cx, cy):
    h, w = depth.shape
    u, v = np.meshgrid(np.arange(w), np.arange(h))
    z = depth
    x = (u - cx) * z / fx
    y = (v - cy) * z / fy
    points = np.stack([x, y, z], axis=-1).reshape(-1, 3)
    # 去除深度为 0 的(无效)点
    valid = points[:, 2] > 0
    return points[valid]

体素

把 3D 空间划分成均匀立方体格网的表示方式。可以理解为 2D 像素的 3D 版本。规则、易于处理,但分辨率一旦提高,内存会以三次方增长。为了缓解这个问题,会使用基于八叉树(octree)的 OctoMap 等层级结构。

占据栅格(occupancy grid)

每个格子保存"已占据/空闲/未知"概率的地图。在机器人导航中特别有用,因为它直接表示了哪里可以通行。

占据栅格(2D 切片示例)
   . = 自由空间   # = 障碍物   ? = 未探索

   ? ? ? ? ? ? ?
   ? . . . # # ?
   ? . R . # . ?      R = 机器人当前位置
   ? . . . . . ?
   ? # # . . . ?
   ? ? ? ? ? ? ?
表示优点局限主要用途
点云忠实于原始数据、精密无序、体量大配准、重建
体素规则、易于处理内存三次方增长3D 深度学习、碰撞检测
占据栅格直接表示可通行性丢失精细形状路径规划、导航

6D 姿态估计

对于抓取物体的机器人(操作,manipulation)来说,仅知道物体在画面上的位置(2D)是不够的。必须知道三维位置 (x, y, z) 以及三轴朝向(roll、pitch、yaw),才能准确地把手伸过去。这六个自由度合起来称为 6D 姿态(6-DoF pose)。

6D 姿态 = 位置 3 + 旋转 3

        z
        │   ↺ yaw
        │  /
        │ /
        └──────── y
       /│
      / │  ↺ pitch
     x  │
        ↺ roll

  要抓取物体: 决定手放在哪里(位置) + 朝哪个方向(旋转)

方法大致分两条路线。一是把已知的 3D 模型与观测数据配准的方法(例如 ICP,Iterative Closest Point),二是用深度学习直接从图像回归姿态的方法。实务中常见的组合是,先用深度学习得到初始估计,再用 ICP 做精细校正。

基于深度学习的感知 — 检测与分割

知道了深度和形状之后,接下来要知道"那是什么"——这是语义层面的感知。

  • 目标检测(detection):在图像中找到物体,预测边界框和类别。
  • 语义分割(semantic segmentation):为每个像素赋予类别(这个像素是地板,那个像素是人)。
  • 实例分割(instance segmentation):即使是同一类别,也要区分出各个独立的物体(人 1、人 2)。
   输入图像                  检测              语义分割
  ┌──────────┐          ┌──────────┐        ┌──────────┐
  │  人 杯子  │   ──▶   │ [人] 框  │  ──▶  │ 逐像素上色 │
  │   桌子    │          │ [杯子] 框│        │ 人/杯子/桌子│
  └──────────┘          └──────────┘        └──────────┘

在机器人系统中,这一步会与 3D 信息结合。比如把分割出的"杯子"像素区域与深度帧叠加,就能得到杯子的 3D 位置,直接送入抓取规划。近来也出现了把提示式分割(如 Segment Anything 系列)这类预训练通用模型用作机器人感知前端的做法。不过在真实环境部署时,始终需要验证光照、遮挡、领域差异带来的性能下降。

NeRF 与 3D 高斯溅射

近来有两项技术搅动了 3D 重建这个领域。

  • NeRF(Neural Radiance Fields):用多个视点拍摄的图像,把场景学习为一个神经网络函数。给定空间中任意一点和视线方向,它会输出该处的颜色与密度,再沿光线积分,渲染出新视点的图像。可以实现非常逼真的重建,但训练和渲染都很重,这是它的局限。
  • 3D 高斯溅射(3D Gaussian Splatting):把场景表示为大量三维高斯(带有位置、大小、朝向、颜色、不透明度的柔软斑块)的集合。将其投影到屏幕上(splatting)进行渲染,速度比 NeRF 快得多,能实现实时渲染,因此备受瞩目。
传统表示           NeRF                  3D 高斯溅射
 点/体素        连续函数(MLP)          3D 高斯集合
 ┌─┬─┬─┐       f(x, y, z, θ, φ)        ○ ◦ ● ◦ ○  (每个斑块带有
 ├─┼─┼─┤    ──▶ = (颜色, 密度)    ──▶    ● ◦ ○ ● ◦   位置·颜色·不透明度)
 └─┴─┴─┘       沿光线积分渲染            投影·合成渲染(快)

从机器人的角度看,这两者都很有吸引力。可以用来构建高质量 3D 地图、重建用于仿真的逼真场景,或预测机器人尚未见过的视点。事实上,把高斯表示嫁接到 SLAM 上的研究相当活跃。不过这类表示通常假设场景是静态的,对运动物体或实时大规模环境仍有实用上的限制。具体要在哪套系统、什么时候引入,需要谨慎判断。

坐标系与变换 — 感知背后隐藏的脊梁

3D 感知中最容易藏 bug 的地方,出人意料地是坐标系(coordinate frame)。机器人系统中同时存在多个坐标系。相机看到的世界是相机坐标系,机器人的指尖是末端执行器坐标系,机器人的躯干是基座坐标系,还有一个覆盖整个房间的世界坐标系。要"抓取"某个物体,就必须把在相机坐标系中识别到的物体位置,变换到机器人挥动手臂所依据的基座坐标系中。

坐标系变换链(把物体从相机坐标系搬到机器人基座坐标系)

  世界坐标系(房间)
     │  T_world_base (机器人在房间的哪个位置)
  基座坐标系(机器人躯干) ◀──── 在这里进行手臂规划
     │  T_base_cam (相机装在机器人的哪个位置)
  相机坐标系 ────▶ 物体位置(在这里被识别)

  物体_基座 = T_base_cam x 物体_相机

这类变换用 4x4 齐次变换矩阵(homogeneous transformation)表示。把旋转和平移打包进一个矩阵,只需矩阵乘法就能在坐标系之间穿梭。机器人中间件提供了以树结构管理这些变换关系的工具(例如 ROS 的 tf)。

变换树(tf tree)
   world
     └── base_link
           ├── camera_link
           │     └── camera_optical_frame
           └── arm_base
                 └── ... ── end_effector

  每条边代表一次变换。任意两个坐标系之间的变换,
  可以沿树上的路径把矩阵连乘得到。

变换中常见的失误,一是搞混方向(从哪个坐标系到哪个坐标系),二是在旋转表示(欧拉角、四元数、旋转矩阵)之间转换时弄错轴的顺序。四元数不存在万向节死锁(gimbal lock),在机器人领域被广泛使用,但对人来说不太直观、难以直接读懂。

深入理解传感器融合

前面提到过,多个传感器会互相弥补彼此的短板。这种融合大致分两个层次。

  • 低层融合:直接结合原始测量值。例如把相机图像和 LiDAR 点对齐到同一坐标系,为每个像素赋予深度,或为每个 3D 点赋予颜色。
  • 高层融合:把各个传感器独立估计出的结果(例如物体位置)以概率方式结合起来。

时间轴上融合的核心工具是状态估计滤波器。其中卡尔曼滤波器(Kalman filter)及其扩展在机器人的位置、速度估计中被广泛使用。

卡尔曼滤波器的两个步骤(反复进行)

  预测(predict)                更新(update)
  ┌──────────────┐            ┌──────────────┐
  │ 用运动模型     │            │ 用新的观测     │
  │ 把状态向前推   │──────────▶│ 校正预测       │
  │ (不确定性↑)   │            │ (不确定性 ↓)  │
  └──────────────┘            └──────────────┘
        ▲                            │
        └──────── 下一周期 ◀─────────┘

  核心: 对预测不确定性和观测不确定性做加权平均 →
        把权重放在更可信的一方

IMU(惯性测量单元)能非常快速地给出加速度和角速度,但随时间推移会产生漂移。相对地,相机、LiDAR 速度较慢,但能提供绝对基准。用滤波器把两者编织在一起(例如视觉惯性里程计,VIO),就能得到既快又稳定的姿态估计。这会成为 SLAM 前端的强力基础。

实战流水线速写

把前面这些零件串成一条流程,移动机器人典型的感知-行动循环大致如下。

感知 → 规划 → 控制 循环

 [传感器] ──▶ [预处理] ──▶ [SLAM/状态估计] ──▶ [语义感知]
 相机          去噪            位置+地图            检测/分割
 LiDAR        配准/校正                            可供性(affordance)
 IMU                                                 │
 [控制] ◀── [运动规划] ◀── [任务规划] ◀── [场景理解]
 关节指令      无碰撞路径      要做什么          哪里有什么
 机器人动作 ──▶ 世界变化 ──▶ (循环重复)

每个模块都由前面章节讨论过的技术填充。重要的是,这个循环必须在实时预算内跑完。如果感知耗时 200 毫秒,而机器人以每秒 1 米的速度移动,那么等到做出判断时,世界已经移动了 20 厘米。所以实务中常见的设计是把感知拆成多个周期(快速的安全反应循环 + 慢速的语义理解循环)来运行。

# 概念性的感知-行动循环(接近伪代码的示例)
def perception_action_loop(robot):
    while robot.is_active():
        frame = robot.sensors.grab_synced()      # 时间同步的传感器采集
        cloud = preprocess(frame)                 # 去噪、坐标对齐
        pose, local_map = slam.update(cloud)      # 定位 + 地图更新
        objects = detector.detect(frame.rgb)      # 深度学习检测
        objects = lift_to_3d(objects, frame.depth)  # 把 2D 检测提升到 3D
        goal = task_planner.decide(objects, pose) # 决定要做什么
        path = motion_planner.plan(goal, local_map)  # 无碰撞路径
        robot.controller.follow(path)             # 遵守力/速度限制

这段代码把真实系统的骨架做了简化。实际系统中,每个环节都会拆分成独立进程通过消息通信,并密集加入失败时安全停止之类的异常处理。

如何评估感知性能

"感知效果好"这句话,必须是可测量的。不同任务使用不同的指标。

任务代表性指标含义
目标检测mAP(平均精度均值)多个阈值下精确率-召回率的均衡
语义分割mIoU(平均交并比)预测区域与真实区域的重叠程度
深度估计RMSE、绝对相对误差估计深度与真实深度的差异
SLAMATE(绝对轨迹误差)估计路径与真实路径的偏差
6D 姿态ADD 系列用估计姿态放置的模型点与真值点之间的距离

看指标时有一点需要留意。基准测试上的高分并不保证现场性能。数据集可能存在偏差,机器人实际遇到的光照和物体分布也可能与基准测试不同。因此成熟的团队会把基准测试得分,以及自家机器人在真实任务中的成功率,一并作为最终指标。

单目深度估计 — 用一只眼睛看深度

不用立体视觉或 LiDAR,仅凭一台相机估计深度的方法也存在。单目深度估计(monocular depth estimation)用深度学习直接从图像预测深度。它不依赖物理原理,而是使用从海量数据中学到的先验知识。举例来说,"远处的物体看起来更小、地平线方向更远、熟悉的物体(门、椅子)的大小大致固定"这类线索,会被神经网络内化。

单目深度估计的原理(直觉)

  一张图像 ──▶ 神经网络 ──▶ 逐像素深度图
                    ├─ 线索 1: 物体大小(参照熟悉的物体)
                    ├─ 线索 2: 透视、消失点
                    ├─ 线索 3: 模糊、遮挡关系
                    └─ 线索 4: 纹理密度变化

  注意: 绝对尺度可能是模糊的(多数情况下只有相对深度是确定的)

单目方式的局限在于尺度模糊性。仅凭一张照片,原理上很难分辨"近处的小物体"和"远处的大物体"。因此在机器人系统中,单目估计常常与其他传感器结合使用,或者与机器人移动产生的视点变化(motion parallax)一起使用。

动态环境 SLAM 概览

前面提到,大多数经典 SLAM 都假设世界是静态的。但现实中有走动的人、滚动的推车、开合的门。这类动态要素会从两方面扰乱 SLAM。第一,把运动物体的特征点错误地放进地图,污染地图。第二,以运动的背景为参照,错误地估计自身位置。

动态 SLAM 的处理方式

  1) 检测: 什么在动?
     - 用语义分割识别"人、车辆"等动态候选
     - 通过几何一致性的违反检测离群点
  2) 分离: 静态背景 vs 动态物体
     - SLAM 只使用静态特征(地图更稳定)
     - 动态物体单独跟踪或直接忽略
  3) (可选) 跟踪: 单独预测动态物体
     - 预测人会往哪儿走,用于规避

核心思路是一种平衡:把动态要素当作离群点过滤掉,以保护静态地图,但出于安全考虑,又不能把这些动态要素完全忽略。用静态背景构建地图,而把人这类运动目标交给规避规划去处理。

实务中会遇到的陷阱

  • 标定:传感器的内参、外参哪怕只有一点点偏差,3D 重建也会大幅走样。相机与 LiDAR 之间的外参标定尤其棘手。
  • 时间同步:多个传感器的时间戳一旦错位,机器人快速移动时,数据就会指向不同的时刻,导致配准崩溃。
  • 动态环境:大多数经典 SLAM 假设世界是静态的。一旦有人或车辆移动,就必须把它们当作离群点过滤掉。
  • 实时约束:感知必须在机器人的控制周期内完成。精度与延迟之间的权衡始终是关键。
  • 领域差异:在实验室里表现良好的感知模型,到了光照、材质、背景都不同的现场经常会崩溃。部署前用现场数据验证的习惯很重要。

结语

机器人感知从"找回丢失的三维信息"这项工作出发,把这个空间编织成地图(SLAM)、以合适的方式表示它(点云、体素、占据栅格)、估计物体的姿态(6D pose)、赋予其含义(检测、分割),由这几个层次组成。NeRF 与 3D 高斯这类新的表示方法,仍在不断拓展这幅图景。

关键在于,没有哪一种技术是万能的。每种传感器各有长短,每种表示方式也各有用途。优秀的机器人感知系统,会根据具体问题把这些技术组合起来,并在真实环境的种种陷阱中不断验证、打磨。下一篇文章将讨论,这样看清了世界的机器人,如何才能安全地行动。

参考资料