- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言
- 为什么 3D 感知很难
- 获取深度的传感器
- SLAM — 同步定位与建图
- 3D 表示 — 点云、体素、占据栅格
- 6D 姿态估计
- 基于深度学习的感知 — 检测与分割
- NeRF 与 3D 高斯溅射
- 坐标系与变换 — 感知背后隐藏的脊梁
- 深入理解传感器融合
- 实战流水线速写
- 如何评估感知性能
- 单目深度估计 — 用一只眼睛看深度
- 动态环境 SLAM 概览
- 实务中会遇到的陷阱
- 结语
- 参考资料
引言
人一睁眼,几乎不费力气就能把握房间的结构 —— 桌子在哪儿、到门口要走几步、杯子是不是伸手可及,全都瞬间知道。但对机器人来说,这件事绝非理所当然。相机交给它的,只是一格一格的像素,一串颜色数值的排列。从这里出发,回答"我现在处于三维空间中的哪个位置、周围有什么"这个问题,正是机器人感知(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、绝对相对误差 | 估计深度与真实深度的差异 |
| SLAM | ATE(绝对轨迹误差) | 估计路径与真实路径的偏差 |
| 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 高斯这类新的表示方法,仍在不断拓展这幅图景。
关键在于,没有哪一种技术是万能的。每种传感器各有长短,每种表示方式也各有用途。优秀的机器人感知系统,会根据具体问题把这些技术组合起来,并在真实环境的种种陷阱中不断验证、打磨。下一篇文章将讨论,这样看清了世界的机器人,如何才能安全地行动。