想象一辆自动驾驶汽车面前堆着乱七八糟的东西:有正常的小轿车、有被撞歪的护栏、有从货车上滑落的箱子,甚至还有一棵倒下的大树。过去,自动驾驶习惯给每个东西“套盒子”——先判断这是车还是人,再画一个 3D 包围框。可一旦东西形状不规矩,盒子就套不准。现在,特斯拉带火了一种新思路:与其费劲套盒子,不如把车头前方整片空间当成一张巨大的乐高底板,切成一个个小立方体,逐格回答“这格有没有被东西占据”。这就是 Occupancy Network,中文常叫“占据网络”。 占据网络不输出“这是车”或“这是人”,而是输出一张三维的“积木拼图”:哪些格子被占住,哪些格子空着。它擅长处理长尾障碍物和异形物体,又能和 BEV 感知、数据自动标注、规划控制串成一套完整流水线。本文会用“乐高积木”这个比喻,带你从零看懂这个感知新范式。

岗位关联

掌握 Occupancy Network,等于掌握自动驾驶感知从“框选物体”走向“空间建模”的行业方向,对自动驾驶感知、BEV 与占据网络相关岗位是核心竞争力。

目录
  1. 可视化演示
  2. 核心概念
  3. 对比传统检测
  4. BEV融合
  5. 数据标注
  6. 感知管线
  7. 业界进展
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示自动驾驶Occupancy Network如何用体素网格表示3D空间并检测长尾障碍物,支持模式切换查看体素占据与检测对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

体素网格空间占据表示 占据体素:检测到障碍物的空间单元 空闲体素:可通行区域 异形物:长尾障碍物检测 occ(v) = sigmoid(MLP(voxel_feat)) Occupancy Network逐体素预测占据概率。

核心概念

想象你在搭乐高:比起先猜“这堆零件是一辆车还是一棵树”,更稳妥的办法是拿出一块底板,把空间一格一格铺满,然后问“这一格有没有被积木塞住”。Occupancy Network 干的就是这件事。它不是先去识别物体类别,而是把车头前方的三维空间切成大量小立方体,像点灯一样逐格标记“被占”或“空闲”。

这里的“小立方体”就是体素(Voxel),类似乐高积木的单个凸点。传统感知盯着“一个物体”,而占据网络盯着“一片空间”。只要某个体素内有东西,无论是小石子、倒下的树干、还是卡车掉落的货箱,它都会被点亮。这让自动驾驶在面对长尾障碍物时,比“先分类再套框”的思路更不容易漏。

从数学上看,网络对每个体素输出一个占用概率:

P(voxel)=σ(fθ(v))\displaystyle P(\text{voxel}) = \sigma\left(f_\theta(v)\right)

1. 体素 v:表示三维空间中某一个小立方格,它带有自己的坐标和周围特征。

v=(x,y,z)v=(x, y, z)

2. 特征网络 fθ:把体素坐标、图像特征或点云特征编码成一个实数分数。

fθ(v)∈Rf_\theta(v) \in \mathbb{R}

3. Sigmoid 归一化 σ:把任意实数分数压缩到 0 到 1,解释为“被占据的概率”。

σ(z)=11+e−z\sigma(z)=\frac{1}{1+e^{-z}}

回到乐高比喻:车辆眼前的世界变成一张会发光的积木地图,哪些格子亮起来,哪些暗下去,就是它理解空间的方式。这种“逐格建模”的思路,正是取代部分传统 3D 检测的底层原因。

对比传统检测

传统 3D 目标检测像一个熟练的包装员,手里只有几种固定尺寸的纸箱:车、人、骑行的人。遇到规规矩矩的小轿车,纸箱一扣刚刚好;可一旦遇到横在路上的大树、被撞弯的护栏,纸箱根本套不进去,于是这辆感知系统就“选择性失明”了。

占据网络换了一套玩法:它放弃纸箱,改拿乐高积木。不追求把物体塞进某个标准框,而是沿着物体轮廓,一格一格把占用格子点亮。哪怕这个物体从没见过,只要它真的占住了空间,体素就能把它“拼”出来。这就是它对异形物体和长尾障碍物更友好的根本原因。

训练时,每个体素其实都在回答一个二分类问题:这格到底是空还是被占。常用的损失函数是二元交叉熵:

L=−1N∑i=1N[yilog⁡pi+(1−yi)log⁡(1−pi)]\displaystyle L = -\frac{1}{N}\sum_{i=1}^{N}\left[ y_i \log p_i + (1-y_i)\log(1-p_i) \right]

1. 体素总数 N:参与训练的体素数量,通常等于真值网格中所有格子。

N=H×W×DN = H \times W \times D

2. 真值标签 yi:表示第 i 个体素是否真的被占用,1 为占用,0 为空闲。

yi∈{0,1}y_i \in \{0, 1\}

3. 预测概率 pi:模型输出的第 i 个体素占用概率。

pi=P(voxeli)p_i = P(\text{voxel}_i)

4. 对数项:当预测与真值方向一致时损失小,不一致时损失大。

log⁡pi 与 log⁡(1−pi)\log p_i \text{ 与 } \log(1-p_i)

回到纸箱和积木的对比:纸箱套不住的东西,积木总能一格一格贴合出来。占据网络并不会完全取代 3D 检测,但它把传统检测最头疼的“长尾异形障碍物”这块短板,用空间建模补上了。

BEV融合

用乐高搭一个场景时,如果只有正面照片,很难知道背面有什么。聪明的做法是让多个设计师从不同角度画草图,最后统一铺在同一张底板上。BEV(Bird's Eye View,鸟瞰视角)就是自动驾驶的那张公共底板。

Occupancy Network 和 BEV 感知经常成对出现。系统先通过多个摄像头提取图像特征,再把这些特征投影到统一的俯视网格上;得到 BEV 特征图后,占据网络再沿着高度方向把网格切成柱子,逐柱预测哪些体素被占。这样既能利用多相机看到的丰富纹理,又能在统一坐标系里做空间推理。

用公式表示,就是先把多张图像编码成一张 BEV 特征图:

B=Fbev(I1,I2,…,IN),B∈RH×W×C\displaystyle B = F_{\text{bev}}\left(I_1, I_2, \dots, I_N\right),\quad B\in\mathbb{R}^{H\times W\times C}

1. 多相机图像 I1 到 IN:环视摄像头拍摄到的不同视角画面。

I1,I2,…,IN∈R3×H0×W0I_1, I_2, \dots, I_N \in \mathbb{R}^{3 \times H_0 \times W_0}

2. BEV 变换网络 F_bev:负责把图像特征从透视视角变换到鸟瞰视角。

Fbev:{Ii}↦BF_{\text{bev}}: \{I_i\} \mapsto B

3. 特征图维度 H×W×C:俯瞰特征图的高度、宽度和通道数,对应真实世界的平面位置。

B∈RH×W×CB \in \mathbb{R}^{H\times W\times C}

最终,多张草图拼成一张统一的俯瞰积木底板。占据网络在这张底板上继续向上“搭积木”,把 2D 俯视图扩展成 3D 占用空间,这也是纯视觉方案能输出立体栅格的关键路径。

数据标注

给真实世界搭乐高图纸,最难的不是算法,而是“说明书”从哪里来。如果让标注员在 3D 空间里人工点出每一个体素,成本高到无法想象。现实中的做法更像“扫描实物,自动生成图纸”。

自动驾驶公司通常用激光雷达点云或多帧相机重建,离线生成场景的三维表面,再把三维表面体素化,自动得到每个格子的真值标签。整个过程只需要少量人工复核,尤其适合施工路段、事故现场这类困难场景。除此之外,还会用“影子模式”在量产车上悄悄收集罕见场景,持续挖掘难例,让标注闭环越跑越顺。

真值标签的生成规则可以写成:

Ogt(v)={1,v∈reconstructed surface0,v∉reconstructed surface\displaystyle O_{gt}(v)= \begin{cases} 1, & v \in \text{reconstructed surface}\\ 0, & v \notin \text{reconstructed surface} \end{cases}

1. 体素 v:三维空间中的一个小立方格,用于判断是否被物体占据。

v=(x,y,z)v=(x, y, z)

2. 重建表面 reconstructed surface:多帧点云或图像重建出的三维几何曲面。

surface⊂R3\text{surface} \subset \mathbb{R}^3

3. 命中判定:体素与重建表面相交则取 1,否则取 0。

Ogt(v)=1[v∩surface≠∅]O_{gt}(v) = \mathbb{1}[v \cap \text{surface} \neq \emptyset]

回到乐高比喻:我们不必让人手工画每一个凸点,而是用 3D 扫描仪自动生成图纸。占据网络的落地效率,很大程度上取决于这套“自动扫描 + 人工抽检”的数据标注体系能跑多快。

感知管线

在自动驾驶这间乐高工厂里,Occupancy Network 不是厂长,只是一名可靠的质检员。传感器是原料进货口,规划器是最终决策者,占据网络则站在中间,负责把“空间哪里被占、哪里可走”这份检查结果整理清楚,交给下一道工序。

在实际感知 pipeline 中,占据网络通常放在 BEV 特征提取之后、预测与规划之前。它既要接收当前帧的静态特征,也要接收多帧时序运动特征,这样才不会把“正在移动的车”和“停在路边的车”混为一谈。最终输出的占用体素,会被下游规划模块用来寻找可行驶区域。

整体关系可以简洁表达为:

Ot=OccNet(concat(Fbev,Fmotion))\displaystyle O_t = \mathrm{OccNet}\left(\mathrm{concat}(F_{\text{bev}}, F_{\text{motion}})\right)

1. 当前 BEV 特征 F_bev:多相机融合后得到的俯瞰特征图。

Fbev∈RH×W×CF_{\text{bev}} \in \mathbb{R}^{H\times W\times C}

2. 时序运动特征 F_motion:多帧信息编码后提供的速度、朝向等动态线索。

Fmotion∈RH×W×CF_{\text{motion}} \in \mathbb{R}^{H\times W\times C}

3. 拼接 concat:把静态特征和动态特征在通道维度上合并。

concat(Fbev,Fmotion)∈RH×W×2C\mathrm{concat}(F_{\text{bev}}, F_{\text{motion}}) \in \mathbb{R}^{H\times W\times 2C}

4. 当前时刻输出 Ot:第 t 帧的占据网格结果,供规划器做避障决策。

Ot∈RH×W×DO_t \in \mathbb{R}^{H\times W\times D}

回到工厂比喻:质检员不直接操控机器,但它给厂长递上的“积木占用图”决定了车敢不敢往前走。占据网络之所以能成为视觉感知的新枢纽,正是因为它卡在了“看懂空间”和“做出行动”之间最关键的位置。

业界进展

特斯拉最早把占据网络推到聚光灯下,在 AI Day 上展示了如何用它处理复杂路口的异形障碍物。随后,国内自动驾驶公司纷纷跟进,把 BEV 感知和 Occupancy 合并成新一代量产感知架构,用来补充 3D 检测的盲区。

在工程评价中,业界常用 mIoU 来衡量体素预测质量。所谓 mIoU,就是把每个类别的预测区域和真值区域求“交集除以并集”,再对所有类别取平均:

mIoU=1K∑i=1KIoUi\displaystyle \text{mIoU} = \frac{1}{K}\sum_{i=1}^{K} \text{IoU}_i

1. 类别数 K:占据网格中需要区分的类别数量,比如车、行人、可行驶区域等。

K∈NK \in \mathbb{N}

2. 单类 IoU_i:第 i 类物体的预测占据体素与真值体素的交并比。

IoUi=∣Pi∩Gi∣∣Pi∪Gi∣\text{IoU}_i = \frac{|P_i \cap G_i|}{|P_i \cup G_i|}

3. 取平均:把各类 IoU 加总后除以类别数,得到整体精度指标。

mIoU=1K∑i=1KIoUi\text{mIoU} = \frac{1}{K}\sum_{i=1}^{K} \text{IoU}_i

回到行业视角:占据网络就像一套刚发布的新款乐高套装,说明书还不完整,拼法仍在迭代。特斯拉跑在最前面,国内厂商在量产车上快速跟牌,但算力消耗、标注成本、评测标准都还没有统一,这既是挑战,也是从业者的机会。

AI 应用场景

长尾异形障碍物识别

阶段:感知与预测

解决的问题:传统 3D 检测要求物体能被标准包围框套住,遇到侧翻集装箱、倒伏树木、施工路障等异形物体时容易漏检。占据网络用体素逐格表示空间占用,不依赖固定形状,因此能把这些长尾障碍物完整“拼”出来,给规划器提前避让的理由。

具体案例:某 Robotaxi 在城区路段遇到被撞弯的隔离护栏,传统检测只输出一个不稳定的框;切换 BEV + Occupancy 方案后,护栏被连续的体素点亮,车辆在 30 米外就开始减速绕行,接管率明显下降。

自动标注与数据闭环

阶段:数据与评测

解决的问题:人工在三维空间里逐个标注体素几乎不可行。借助激光雷达点云和多帧重建,可以自动生成占据真值,再通过影子模式筛选难例,把人工集中在模型最容易犯错的地方,大幅降低标注成本。

具体案例:一家量产车企的地库场景中,标注团队先用多帧点云重建停车位和柱子表面,自动生成体素标签;夜间车灯照射造成的反光难例再交给人工复核,整体标注效率提升约 70%。

BEV+Occupancy 的全景栅格输出

阶段:多传感器融合

解决的问题:多路摄像头各自看到局部视角,缺少统一的三维空间表示;BEV 提供俯瞰特征,Occupancy 提供高度方向占用,两者结合可以输出 360° 的立体栅格地图,支撑高速和城区 NOA 决策。

具体案例:某高速 NOA 系统遇到一辆装满超宽货物的货车,普通检测无法判断货箱边缘是否侵入自车道;BEV+Occ 输出车身周围的连续占用体素,系统准确判断出剐蹭风险,主动减速并发出变道提示。

Python 演示

python77 行
import numpy as np  def build_sparse_points(seed=7):    rng = np.random.default_rng(seed)    points = []    for _ in range(120):        x = 4 + rng.normal(0, 0.6)        y = 4 + rng.normal(0, 0.6)        z = 3 + rng.normal(0, 0.7)        points.append([x, y, z])    for _ in range(80):        x = rng.uniform(1.0, 3.2)        y = 3.0 + rng.normal(0, 0.25)        z = 1.0 + rng.normal(0, 0.25)        points.append([x, y, z])    return np.array(points)  def voxelize(points, grid_size=8, voxel_size=1.0):    grid = np.zeros((grid_size, grid_size, grid_size), dtype=np.float64)    for p in points:        i, j, k = np.floor(p / voxel_size).astype(int)        if 0 <= i < grid_size and 0 <= j < grid_size and 0 <= k < grid_size:            grid[i, j, k] = 1.0    return grid  def build_dataset(grid):    feats, labels = [], []    n = grid.shape[0]    for i in range(n):        for j in range(n):            for k in range(n):                i0, i1 = max(0, i - 1), min(n, i + 2)                j0, j1 = max(0, j - 1), min(n, j + 2)                k0, k1 = max(0, k - 1), min(n, k + 2)                density = grid[i0:i1, j0:j1, k0:k1].sum()                feats.append([1.0, density])                labels.append(grid[i, j, k])    return np.array(feats), np.array(labels)  class TinyOccNet:    def __init__(self):        self.w = np.array([-2.0, 0.8])     def predict(self, x):        return 1.0 / (1.0 + np.exp(-(x @ self.w)))     def train(self, x, y, epochs=200, lr=0.3):        for _ in range(epochs):            p = self.predict(x)            grad = x.T @ (p - y) / len(y)            self.w = self.w - lr * grad     def bce(self, x, y):        p = self.predict(x)        p = np.clip(p, 1e-8, 1 - 1e-8)        return -(y * np.log(p) + (1 - y) * np.log(1 - p)).mean()     def iou(self, x, y):        p = self.predict(x)        pred = (p >= 0.5).astype(float)        inter = np.sum(pred * y)        union = np.sum(pred + y > 0)        return inter / union if union > 0 else 1.0  if __name__ == "__main__":    pts = build_sparse_points()    grid = voxelize(pts)    X, y = build_dataset(grid)    model = TinyOccNet()    model.train(X, y, epochs=200, lr=0.3)    print("BCE:", round(model.bce(X, y), 4))    print("IoU:", round(model.iou(X, y), 4))
目的:用 NumPy 模拟一个微型“占据网络”,把点云离散成 8×8×8 体素网格,再用体素邻域密度特征训练一个二分类模型,最后输出体素预测的交叉熵损失和 IoU。
  1. grid[i, j, k] = 1.0把点云命中的体素标记为占用,这是真值标签的来源。
  2. density = grid[i0:i1, j0:j1, k0:k1].sum()计算每个体素周围被占邻居的数量,作为预测特征。
  3. self.w = self.w - lr * grad用梯度下降更新线性模型权重,模拟最小化体素分类损失。
  4. pred = (p >= 0.5).astype(float)把概率大于 0.5 的体素视为“被占用”。
  5. inter = np.sum(pred * y)计算预测占用和真值占用同时为 1 的体素数量,即交集。
  6. union = np.sum(pred + y > 0)计算预测或真值至少一方为 1 的体素数量,即并集。
  7. model.train(X, y, epochs=200, lr=0.3)训练微型模型,让它在简单场景中学会用邻居密度推断体素是否被占。

常见误区

误区以为 OccNet 和 3D 检测一样,会输出“这是第几辆车”“这是行人还是卡车”的 ID 和类别。

正解占据网络回答的是“这个体素是否被占据”,本质是空间几何建模,不负责区分实例。物体类别、ID、运动状态通常还要靠检测或跟踪模块补充。它更像一张“哪里被占位”的乐高地图,而不是“谁是谁”的花名册。

为何常见:因为可视化里体素彩色点很像语义分割,容易让人误以为它自带身份和类别信息。

误区占据网络能表达任意形状,干脆把传统 3D 目标检测整个淘汰掉。

正解占据网络擅长补“形状”和“长尾场景”,但车辆、行人等核心目标的类别、ID、速度、朝向仍需 3D 检测或跟踪提供。工业系统普遍让两者并行,占据结果与检测结果相互校验,而不是互相替代。

为何常见:行业宣传强调“取代传统检测”,让很多人误以为 OccNet 是全新替代品。

误区训练体素模型需要标注员在 3D 空间里逐个格子涂色,成本高到无法量产落地。

正解主流方案是激光雷达点云或多帧重建自动生成表面,再体素化得到真值;影子模式负责收集困难场景,人工只复核少量难例。这个流程使量产数据闭环成为可能。

为何常见:人们习惯用 2D 图像标注的直觉去理解 3D 空间标注,忽略了自动重建工具的存在。

误区认为它只输出“哪里有障碍物”,对可行驶区域没有帮助。

正解占据网络对每个体素输出占用概率,空闲体素本身就是“这里可通行”的证据。把占用概率低于阈值的区域聚合起来,就能得到自由空间地图,规划器可以直接用它找路。

为何常见:名字里的“Occupancy”太强调“被占”,让人忽略“没被占”同样是重要信息。

误区总觉得 BEV感知 是 2D 俯视分割,Occupancy 是 3D 体素,二者互不相干。

正解当前主流方案常把多相机特征先投影到 BEV 网格,再在 BEV 特征上预测体素占用。BEV 提供统一俯瞰坐标,Occ 在高度方向补全立体信息,二者是特征共享、上下游配合的关系。

为何常见:两个新概念都太火,容易被误读成相互竞争的路线,而忽略它们在 pipeline 中是串联的。

面试题

Q1. 占据网络的空间最小输出单元通常是什么?

单选题 · easy

  • A.矩形包围框
  • B.单个像素
  • C.小立方体单元← 正确答案
  • D.特征图通道

答案:C。占据网络把三维空间切成小立方体,也就是体素。它不像检测输出 3D 框,也不像图像分割输出像素,而是输出体素级别的占用概率,所以正确答案是 C。

Q2. 传统 3D 目标检测的主要痛点是?

单选题 · easy

  • A.无法识别红绿灯
  • B.难以精准框住异形物← 正确答案
  • C.计算量必然比占网大
  • D.无法输出类别概率

答案:B。传统检测依赖固定形状的包围框,遇到倒树、侧翻货车等异形物体就容易漏检或框不准。计算量不是必然更大,检测本身也能输出类别概率,所以正确答案是 B。

Q3. 体素真值标签通常如何获得?

单选题 · mid

  • A.人工逐格涂色
  • B.点云重建自动生成← 正确答案
  • C.全部来自仿真引擎
  • D.高清地图直接提供

答案:B。逐格人工标注成本极高,行业主要使用激光雷达点云或多帧重建生成三维表面再体素化。仿真能补充数据,但真实场景仍靠自动重建,所以正确答案是 B。

Q4. 纯视觉 BEV+Occupancy 中,多相机特征如何进入占网?

单选题 · mid

  • A.先做像素级语义分割
  • B.多相机特征投影到鸟瞰网格← 正确答案
  • C.直接输出规则包围框
  • D.仅依赖激光雷达做后融合

答案:B。纯视觉方案先把多路相机图像编码成 BEV 特征图,然后占网在 BEV 特征上逐列预测体素。像素级分割不是必经步骤,仅靠激光雷达也不叫纯视觉,所以正确答案是 B。

Q5. 哪些场景适合用占据网络补充传统 3D 检测?

多选题 · mid

  • A.道路上倒伏的树木← 正确答案
  • B.侧翻集装箱货车← 正确答案
  • C.正常姿态的小轿车
  • D.路肩堆放的杂物← 正确答案

答案:ABD。倒树、侧翻车、堆放杂物都属于异形或不规则物体,传统 3D 框难以精确表达;正常姿态的小轿车反而是 3D 检测最擅长的目标,不需要占用网络额外补位,所以选 A、B、D。

Q6. 在感知 pipeline 中,占据网络通常放在哪里?

单选题 · hard

  • A.传感器信号发出前
  • B.特征编码与规划器之间← 正确答案
  • C.运动控制执行之后
  • D.取代整个端到端框架

答案:B。占据网络通常接收传感器特征和时序特征,输出占用网格给预测或规划器,因此夹在特征编码和规划之间。它不会取代整个端到端框架,也不会跑到传感器和执行器两端,所以正确答案是 B。

Q7. 相比 3D 目标检测,占据网络的优势有哪些?

多选题 · hard

  • A.可建模非规则障碍物← 正确答案
  • B.能表达可通行空闲区← 正确答案
  • C.直接输出目标唯一 ID
  • D.对长尾物体更鲁棒← 正确答案

答案:ABD。占网逐体素建模,天然适合非规则形状,空闲体素也能暗示可通行区域;但它不负责实例识别,不能直接输出目标唯一 ID,所以不选 C,答案是 A、B、D。

Q8. 体素逻辑分数转成占用概率,通常用哪个函数?

单选题 · mid

  • A.ReLU 激活函数
  • B.Sigmoid 激活函数← 正确答案
  • C.Tanh 激活函数
  • D.最大池化操作

答案:B。Sigmoid 能把任意实数映射到 0 到 1 之间,正好解释为占用概率。ReLU 输出无上界,Tanh 输出范围是 -1 到 1,最大池化不是逐点激活函数,所以选 B。

Q9. 下列哪些做法可以降低占据网络数据标注成本?

多选题 · hard

  • A.多帧点云离线重建← 正确答案
  • B.影子模式收集难例← 正确答案
  • C.人工标注每一体素
  • D.纯视觉生成伪标签← 正确答案

答案:ABD。多帧点云离线重建能自动生成几何真值,影子模式聚焦难例,纯视觉也可以产生伪标签;人工标注每一体素成本最高且不现实,所以不选 C,答案是 A、B、D。

Q10. 衡量体素预测质量的 mIoU 指标是?

单选题 · extreme

  • A.各类别 IoU 取平均← 正确答案
  • B.只统计被占格子召回
  • C.计算所有体素回归误差
  • D.与目标检测 AP 相同

答案:A。mIoU 先计算每个类别的预测体素和真值体素的交并比,再对所有类别平均。它不是召回率,不是回归误差,也不等同于目标检测 AP,所以正确答案是 A。