感知¶
感知是自主系统感知和解释物理世界的方式。本章涵盖传感器模态、标定、传感器融合、3D目标检测、深度估计、占据网络、车道检测和语义建图——这是每个机器人、无人机和自动驾驶汽车赖以构建的感知基础。
-
对人类而言,感知世界毫不费力:你看到一辆车驶近,听到引擎声,感受到脚下的地面,瞬间在脑海中构建出周围环境的心智模型。自主系统也必须做到同样的事,但它使用的是电子传感器和算法,而非眼睛和耳朵。
-
根本性挑战在于:传感器提供的是原始数字(像素强度、点云、信号反射),系统必须将这些数字转化为结构化的理解:"前方12米处有一个行人,以1.5米/秒的速度向左移动。"这就是感知问题。
-
下游的所有任务(预测、规划、控制)都依赖于感知。一个拥有完美规划器但感知能力差的自动驾驶汽车仍然会撞车。感知是瓶颈。
传感器模态¶
- 自主系统使用多种传感器类型,各有其优势与失效模式。没有哪种传感器能单独胜任。
-
相机以高分辨率捕获密集的颜色信息。单张图像包含数百万像素,每个像素记录RGB值(如我们在第8章所见)。相机价格低廉、重量轻,提供丰富的纹理和颜色信息,这对识别标牌、检测交通信号灯和识别物体至关重要。
-
相机类型包括单目(单个镜头,无原生深度)、立体(两个镜头相隔一个基线,通过视差计算深度,详见第8章)和鱼眼(超宽视场角,180°以上,具有严重径向畸变,用于环绕视图泊车系统)。
-
相机的主要弱点是投影过程中丢失深度信息。3D场景通过针孔相机模型映射到2D图像平面(回顾第8章的内参矩阵\(K\)):
-
除以\(Z\)的过程丢弃了绝对深度。两个不同大小、不同距离的物体可能产生完全相同的投影。从单张图像恢复深度是病态问题,这就是为什么需要立体相机或学习型单目深度模型。
-
相机在恶劣条件下也会受到影响:直射阳光产生眩光,黑暗降低信号,雨雾散射光线。
-
LiDAR(光探测与测距)发射激光脉冲并测量每个脉冲返回的时间。由于光速已知(\(c \\approx 3 \\times 10^8\) m/s),每个反射点的距离为:
-
因子2考虑了往返行程(去程和回程)。通过将激光扫过场景,LiDAR构建了一个点云:一组3D坐标\((x, y, z)\),通常带有强度(反射率)值。
-
旋转式LiDAR(如Velodyne)旋转激光阵列360°以提供完整的环视视图。典型设备每秒生成超过30万个点,覆盖64-128个垂直通道。结果是场景的稀疏但几何精度高的3D表示。
-
固态LiDAR没有运动部件,使用光学相控阵或MEMS镜面。这使得它们更便宜、更紧凑、更可靠,但通常视场角较窄(120° vs 360°)。
-
LiDAR提供精确的深度,但生成的是稀疏数据("像素"数量远少于相机),没有颜色信息,且价格昂贵。在大雨、雪或灰尘中,粒子会散射激光脉冲,其性能也会下降。
-
雷达(无线电探测与测距)基于与LiDAR相同的飞行时间原理,但使用无线电波(毫米波,汽车领域通常为77 GHz)。无线电波穿透雨、雾、尘和雪的能力远优于光,使雷达成为最耐天气的传感器。
-
雷达还能通过多普勒效应直接测量速度。当物体向传感器移动时,反射波被压缩(频率升高);当远离时,反射波被拉伸(频率降低)。速度公式为:
-
其中\(\\Delta f\)是频移,\(f_0\)是发射频率。这提供了瞬时径向速度,无需任何跟踪或帧间计算。
-
折中是分辨率:雷达的角分辨率远低于相机或LiDAR,难以区分邻近物体或检测精细细节。但它在任何天气条件下都能出色地探测远距离(200米以上)的车辆。
-
超声波传感器发射高频声脉冲(40-70 kHz)并测量回波返回时间。它们工作在极短距离(0.2-5米),主要用于泊车辅助。其物理原理与LiDAR相同,只是用声音代替光,因此\(d = \\frac{v_{\\text{声}} \\cdot \\Delta t}{2}\),其中\(v_{\\text{声}} \\approx 343\) m/s。
-
IMU(惯性测量单元)包含加速度计和陀螺仪,分别测量线加速度和角速度。IMU提供高频运动数据(通常200-1000 Hz),填补了较慢传感器更新之间的空白。它们不直接感知环境,而是跟踪机器人自身的运动,因此对航位推算和状态估计至关重要。
-
IMU存在漂移问题:小的测量误差随时间累积,导致估计位置偏离真实位置。这就是为什么IMU几乎总是与其他传感器(相机、GPS、LiDAR)融合使用,而非单独使用。
-
GNSS(全球导航卫星系统,包括GPS)通过三角测量来自多颗卫星的信号,提供地球表面的绝对位置。标准GPS精度为2-5米,不足以进行车道级驾驶。RTK-GPS(实时动态差分GPS)使用固定基站校正误差,达到厘米级精度,但需要清晰的天空视野和基站基础设施。
传感器标定¶
-
在传感器协同工作之前,必须进行标定:将每个传感器的测量值与共同坐标系关联起来。
-
内参标定确定传感器的内部参数。对于相机,这意味着焦距、主点和畸变系数(如第8章所述)。对于LiDAR,这意味着激光束之间的精确角度偏移。常用的方法是张氏棋盘格标定法,即从多个角度观察已知平面图案,求解内参矩阵。
-
外参标定确定两个传感器之间的刚体变换(旋转\(R\)和平移\(\\mathbf{t}\))。如果相机和LiDAR安装在同一辆车上,外参标定会找到一个\(4 \\times 4\)变换矩阵,将LiDAR坐标中的点映射到相机坐标:
-
这是齐次坐标中的仿射变换,正是我们在第2章(线性变换)中研究过的那种。如果这个矩阵出错,LiDAR点将投影到错误的像素上,整个融合流程就会崩溃。
-
时间标定同步传感器时钟。以30 Hz工作的相机和以10 Hz工作的LiDAR在不同时间戳产生数据。如果汽车以30 m/s(高速公路速度)行驶,10毫秒的时间误差对应30厘米的空间误差。硬件触发(共享时钟脉冲)或软件同步(时间戳之间的插值)至关重要。
传感器融合¶
-
没有哪种传感器能覆盖所有条件。相机看得到颜色和纹理但丢失深度。LiDAR精确测量深度但稀疏且色盲。雷达在任何天气下都能工作但分辨率低。传感器融合结合各自的优势,弥补各自的弱点。
-
早期融合(数据级融合)在进行任何处理之前合并原始传感器数据。例如,将LiDAR点投影到相机图像上,创建RGBD表示(每像素的颜色+深度),或者为每个LiDAR点赋予其投影到的相机像素的颜色。这种方式保留了最多的信息,但需要精确的标定,且对未对准非常敏感。
-
后期融合(决策级融合)每个传感器独立运行各自的检测流程,然后合并最终输出(边界框、类别标签、置信度分数)。每个传感器投票,融合模块协调分歧。这种方式更简单、更模块化,但每个流程无法从其他传感器的原始数据中获益。
-
中级融合在中间特征表示层面进行操作。每个传感器的原始数据被编码到学习到的特征空间(使用CNN或transformer),然后特征被合并。这是现代系统的主流方法,因为它让网络学习从每种模态中提取什么。
-
BEVFusion是一种代表性的中级融合架构。它将相机特征和LiDAR特征投影到一个共同的鸟瞰图(BEV)表示中,即场景的俯视网格。相机特征通过预测的深度分布被"提升"到3D,然后散布到BEV网格上。LiDAR特征已经是3D的,直接被体素化到同一个网格上。融合后的BEV特征随后由检测头处理。
-
BEV表示之所以强大,是因为它提供了一个统一的、度量尺度的坐标框架,空间推理(距离、尺寸、重叠)在其中变得直观。在相机图像中,一辆近处的自行车和一辆远处的卡车可能占据相同数量的像素。而在BEV中,它们的真实大小和位置一目了然。
3D目标检测¶
-
感知的核心任务是在3D空间中检测物体:它们在哪里,有多大,是什么,朝向何方?每次检测得到一个3D边界框,包括位置\((x, y, z)\)、尺寸\((l, w, h)\)、航向角\(\\theta\)、类别标签和置信度分数。
-
基于LiDAR的检测直接在点云上操作。挑战在于点云是无序的、不规则的,且密度变化大(近处物体有数千个点,远处物体只有几个)。回顾第8章,PointNet通过共享MLP和置换不变的聚合(最大池化)来处理这一问题。
-
PointPillars通过将地面平面离散化为垂直柱("pillars")的网格,将点云转化为结构化表示。每个pillar内的所有点由一个小型PointNet编码为固定大小的特征向量。结果是一个2D伪图像,可以由标准的2D CNN主干网络处理,然后是检测头(如第8章中的SSD架构)。这种方法快速且有效。
-
CenterPoint将物体检测为点而非边界框。它在BEV中预测物体中心的热图,然后在每个峰值处回归边界框属性(尺寸、高度、航向、速度)。这是CenterNet(第8章)的3D类比:无锚点,训练时无需NMS,并通过跨帧关联中心点自然扩展到跟踪。
-
纯相机3D检测必须从2D图像推断深度,这从根本上更难。现代方法如BEVDet和BEVFormer使用transformer架构将2D图像特征"提升"到3D。BEVFormer使用空间交叉注意力:BEV查询关注投影到每个相机图像上的特定3D参考点,从相关位置提取特征。
-
基于LiDAR和基于相机的3D检测之间的精度差距正在迅速缩小,这得益于更好的深度估计、更大的模型和时间融合(利用多帧累积深度线索,类似立体匹配但跨时间进行)。
深度估计¶
-
深度估计是为每个像素或点分配距离值的问题。
-
立体匹配使用两个相隔已知基线\(b\)的相机。同一个3D点在两幅图像中的水平位置略有不同,形成视差\(d\)。深度计算公式为(来自第8章):
-
其中\(f\)是焦距。挑战在于找到两幅图像之间的正确对应关系,特别是在无纹理区域、遮挡区域和重复图案中。现代立体网络(如RAFT-Stereo)使用带有相关体积的迭代精化方法。
-
单目深度估计从单张图像预测深度。由于这是病态问题(无限多个3D场景可以产生相同的图像),网络必须学习统计先验:"地面是平的"、"物体随距离增大而变小"、"纹理梯度表明表面在后退"。
-
Depth Anything(第8章中介绍过)通过在大规模无标注数据集上进行自监督训练,然后在标注数据上进行微调,实现了强大的单目深度估计。关键洞察在于尺度不变损失处理了固有不明确性:模型预测的是相对深度(排序)而非绝对米数。
-
LiDAR-相机深度融合将稀疏的LiDAR深度测量值投影到相机图像上作为监督信号。网络学习"填充"稀疏点之间的空白,生成结合LiDAR精度和相机分辨率的密集深度图。
占据网络¶
- 传统感知输出一个边界框列表,每个检测到的物体一个框。但现实世界中许多事物并不适合用框来整齐地表示:形状不规则的碎片、施工围挡、悬垂的树枝、部分坍塌的墙壁。
-
占据网络将场景表示为密集的3D体素网格。每个体素(一个小立方体空间,例如0.2m × 0.2m × 0.2m)被分类为空闲、占据或未知,并可选择赋予语义标签(道路、人行道、车辆、植被等)。
-
这意味着从以物体为中心的感知("检测汽车")转向以场景为中心的感知("3D空间的哪些部分被占据?")。优势在于通用性:系统不需要预定义对象类别列表来避免与任意障碍物碰撞。
-
从架构上看,占据网络接收传感器输入(相机、LiDAR或两者),将其编码为3D特征体积,并预测每个体素的标签。3D特征体积通常通过将2D特征提升到3D(类似于BEV构建但扩展到垂直方向)来构建,然后使用3D卷积或稀疏卷积进行处理。
-
TPVFormer(三视角)通过将3D体积分解为三个正交平面(俯视图、前视图、侧视图)来避免完整3D注意力的立方级计算成本。每个平面使用2D注意力,其特征在每个体素处组合。这让人想起SVD如何将矩阵分解为更简单的因子(第2章):将一个困难的3D问题分解为可管理的2D部分。
-
输出的体素网格直接告诉规划器哪些空间区域是安全的、哪些不是,使其成为感知和规划之间的自然接口。
车道检测与道路拓扑¶
-
对于在结构化道路上行驶的车辆,理解车道几何至关重要。系统必须知道车道在哪里、如何弯曲、在哪里合并和分叉,以及车辆处于哪条车道。
-
经典方法将参数曲线拟合到检测到的车道标线上。常用的模型是三次多项式:
-
其中\(y\)是前方纵向距离,\(x\)是横向偏移。这是多项式近似(回顾第3章的泰勒级数),选择它是因为道路是平滑曲线,很好地由低次多项式描述。系数通过检测到的车道点上的最小二乘回归估计得出。
-
现代方法使用神经网络直接检测车道。LaneNet将每条车道视为一个实例,使用嵌入分支对属于同一条车道的像素进行分组,然后进行曲线拟合。GANet使用基于图的方法,将车道拓扑表示为有向图,其中节点是车道点,边编码连接关系(哪些车道在交叉口合并、分叉或连接)。
-
道路拓扑超越了单个车道曲线,捕获完整结构:车道之间如何连接,哪些车道允许左转,高速公路入口匝道在哪里汇入。这被建模为有向图,交叉口是节点,车道段是带有属性(限速、车道类型、转弯限制)的边。
-
图结构对路线规划至关重要:规划器需要知道的不仅是"车道在哪里",还有"哪条车道序列能到达目的地"。
语义建图¶
-
感知并不止于检测单帧中的物体。随着时间的推移,自主系统构建一个语义地图:对其环境的持久化、结构化表示,积累来自多次观测的信息。
-
最简单的情况下,语义地图是一个2D网格(占据网格),每个单元存储被占据的概率。随着机器人移动并通过传感器扫描,它使用贝叶斯更新来更新这些概率:
- 这就是贝叶斯定理的实际应用(来自第5章):每次新的测量\(z_t\)更新对每个单元的先验信念。通常使用对数几率表示来避免乘以许多小概率带来的数值问题:
-
对数几率相加等价于概率相乘(回顾\(\\log(ab) = \\log a + \\log b\)),累加和自然地随时间积累证据。
-
更丰富的地图为每个单元分配语义标签(道路、人行道、建筑、植被),并可扩展到3D。这与占据网络密切相关,但强调持久性和时间聚合而非单帧预测。
-
SLAM(同时定位与建图),在第8章中介绍过,是在构建地图的同时跟踪机器人在地图中的位置的算法。视觉-惯性SLAM融合相机和IMU数据;LiDAR SLAM使用点云配准。感知流程将检测结果和深度估计输入SLAM系统,由后者维护全局地图。
-
现代方法越来越多地使用神经隐式表示(如第8章中的NeRF)来构建可在任何3D点查询的密集、逼真地图。这些神经地图将整个场景的压缩表示存储在网络权重中,支持新视角合成和详细空间查询等任务。
编程任务(使用CoLab或notebook)¶
-
使用投影矩阵将3D LiDAR点投影到2D相机图像上。可视化哪些点落在图像边界内。
import jax.numpy as jnp import matplotlib.pyplot as plt # 模拟3D LiDAR点(x=向前,y=向左,z=向上) rng = jax.random.PRNGKey(0) points_3d = jax.random.uniform(rng, (200, 3), minval=jnp.array([5, -10, -2]), maxval=jnp.array([50, 10, 3])) # 相机内参矩阵(焦距500,图像中心320x240) K = jnp.array([[500, 0, 320], [0, 500, 240], [0, 0, 1.0]]) # 外参:LiDAR到相机(单位旋转,小平移) R = jnp.eye(3) t = jnp.array([0.0, 0.0, -0.5]) # 投影:p_cam = K @ (R @ p_lidar + t) p_cam = (R @ points_3d.T).T + t p_img = (K @ p_cam.T).T p_img = p_img[:, :2] / p_img[:, 2:3] # 除以Z # 过滤相机前方且在图像内的点 mask = (p_cam[:, 2] > 0) & (p_img[:, 0] > 0) & (p_img[:, 0] < 640) & \ (p_img[:, 1] > 0) & (p_img[:, 1] < 480) depth = p_cam[mask, 2] plt.figure(figsize=(8, 5)) plt.scatter(p_img[mask, 0], p_img[mask, 1], c=depth, cmap="viridis", s=5) plt.colorbar(label="深度 (米)") plt.xlim(0, 640); plt.ylim(480, 0) plt.title("投影到相机图像上的LiDAR点") plt.xlabel("u (像素)"); plt.ylabel("v (像素)") plt.show() -
使用贝叶斯对数几率更新构建一个简单的2D占据网格。模拟一个距离传感器扫描环境,观察地图的生成过程。
import jax import jax.numpy as jnp import matplotlib.pyplot as plt # 网格设置:50x50个单元,每个0.2米 grid_size = 50 log_odds = jnp.zeros((grid_size, grid_size)) # 传感器模型:对数几率更新值 l_occ = 0.85 # 命中意味着占据的置信度 l_free = -0.4 # 穿过意味着空闲的置信度 # 模拟障碍物:从(5,20)到(5,30)的墙(网格坐标) wall_y = jnp.arange(20, 30) # 机器人在(25, 25),向外扫描 robot = jnp.array([25, 25]) for angle_deg in range(0, 360, 5): angle = jnp.radians(angle_deg) direction = jnp.array([jnp.cos(angle), jnp.sin(angle)]) for step in range(1, 25): cell = (robot + direction * step).astype(int) r, c = int(cell[0]), int(cell[1]) if r < 0 or r >= grid_size or c < 0 or c >= grid_size: break # 检查此单元是否为墙 is_wall = (r == 5) and (c >= 20) and (c < 30) if is_wall: log_odds = log_odds.at[r, c].add(l_occ) break else: log_odds = log_odds.at[r, c].add(l_free) # 将对数几率转换为概率 prob = 1.0 / (1.0 + jnp.exp(-log_odds)) plt.figure(figsize=(6, 6)) plt.imshow(prob.T, origin="lower", cmap="RdYlGn_r", vmin=0, vmax=1) plt.colorbar(label="P(被占据)") plt.plot(25, 25, "b*", markersize=10, label="机器人") plt.legend() plt.title("贝叶斯更新生成的2D占据网格") plt.show() -
使用视差从立体图像对计算深度。模拟两个相机视角下的3D点,计算视差并恢复深度。
import jax import jax.numpy as jnp # 相机参数 f = 500.0 # 焦距(像素) b = 0.12 # 基线(米,12厘米) # 已知深度的3D点 depths_true = jnp.array([5.0, 10.0, 20.0, 50.0, 100.0]) # 视差 = f * b / Z disparities = f * b / depths_true # 从视差恢复深度 depths_recovered = f * b / disparities for z, d, z_r in zip(depths_true, disparities, depths_recovered): print(f"真实深度: {z:6.1f}米 视差: {d:6.2f}像素 恢复值: {z_r:6.1f}米") # 注意:视差与深度成反比 # 近处物体视差大,远处物体视差小 # 这就是为什么立体视觉在近距离最准确