FreeOcc 深度解析:机器人具身智能的「无监督觉醒」——首个无需训练的开放词汇三维占据预测系统
一、引言:当机器人第一次「看懂」了你的房间
想象一个场景:你带着一台机器人进入一个它从未见过的房间——没有预先构建的三维地图,没有提前训练好的场景模型,没有任何标注数据。机器人只用摄像头扫视一圈,便能回答你「红色杯子在桌上」「台灯在书架左侧」这样的空间语义问题。
这听起来像是科幻,但实际上已经是 2026 年机器人领域的最新技术现实。
香港科技大学(广州)陈昶昊教授团队联合穆罕默德・本・扎耶德人工智能大学(MBZUAI)研究者提出了 FreeOcc,一种完全无需训练的开放词汇三维占据预测系统,已被机器人领域旗舰会议 RSS 2026 接收,并在 GitHub 开源。
这篇文章我们来从算法原理、工程架构、代码实现到实验分析,把 FreeOcc 的里里外外彻底拆解清楚。
二、背景:三维占据预测为什么这么难?
2.1 什么是语义占据预测
在机器人感知领域,有一个核心问题:如何让机器人理解三维空间中每个位置是被物体占据、是空闲、还是未知?
语义占据预测(Semantic Occupancy Prediction)将空间划分为三维体素网格,每个体素不仅要知道「这里有没有东西」,还要知道「这个东西是什么」。这比传统的二维图像分割要复杂得多,因为三维信息天然难以获取。
2.2 传统方法的困境
长期以来,基于深度学习的占据预测方法面临两座大山:
第一座:三维标注成本极高。 要训练一个好的占据预测模型,需要大量三维体素级别的语义标注——这意味着要把每个体素标记为「椅子」「桌子」「地板」等类别。做一个房间的三维标注,可能需要几个小时的人工标注,而高质量数据集如 ScanNet,总共也就几十个场景。
第二座:泛化能力极差。 监督学习方法容易过拟合特定数据集的相机内参、尺度分布和外观风格。一旦换到新场景,性能急剧下降——你没法让机器人在客厅训练完,去厨房直接用。
这就是为什么 FreeOcc 的核心思路极具颠覆性:不训练任何神经网络,而是构建一套通用感知系统,让机器人在任意环境中在线构建三维语义地图。
三、FreeOcc 核心架构:四层地图的级联pipeline
FreeOcc 将开放词汇占据预测拆解为 四层模块化地图表示,从观测数据出发,逐步构建全局一致的三维语义地图:
原始RGB-D图像
│
▼
┌─────────────────┐
│ 第一层:点云地图 │ ← SLAM backbone(位姿估计 + 稀疏几何)
└────────┬────────┘
│
▼
┌──────────────────────┐
│ 第二层:3DGS 地图 │ ← 几何一致高斯更新(稠密几何 + 几何锚定)
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ 第三层:语义地图 │ ← 视觉语言模型提取开放词汇语义
└────────┬─────────────┘
│
▼
┌──────────────────────┐
│ 第四层:占据地图 │ ← 概率式高斯→占据投影
└──────────────────────┘
│
▼
开放词汇三维占据地图 → 支持任意文本查询
这个设计非常优雅——每一层都承担明确的职责,层与层之间通过标准化的接口传递信息。让我逐层拆解。
四、第一层:SLAM 定位与稀疏几何锚点
4.1 为什么需要 SLAM
机器人进入新环境后,首先需要知道自己「在哪里」——这就是 SLAM(同步定位与建图)的职责。FreeOcc 使用 DROID-SLAM 作为定位主干,利用其单目输入条件下的强全局几何一致性。
SLAM 输出两样关键信息:
- 相机轨迹:每帧图像的 6-DoF 位姿(位置 + 姿态)
- 稀疏点云:三维空间中的关键几何特征点
4.2 为什么点云作为几何锚点至关重要
这里有一个反直觉的设计决策:FreeOcc 选择用点云(而不是稠密深度图)作为几何锚点。原因是:
传统 3DGS-SLAM 优化出来的「高斯」是一个连续、可微的渲染目标,其位置可以被优化器自由调整来拟合图像——但这恰恰意味着 几何边界不稳定。高斯的位置「漂移」在图像渲染上完全合理,但在占据预测上会导致严重的几何模糊。
FreeOcc 的核心洞察是:点云的每一个点都是物理空间中明确的测量结果,其几何精度远高于优化后的高斯位置。 所以点云提供了最可靠的几何锚点。
4.3 核心代码结构
# FreeOcc 第一层:SLAM 定位与建图(简化示意)
class SLAMBackbone:
"""
SLAM 主干网络,负责:
1. 输入 RGB 或 RGB-D 图像序列
2. 输出每帧的相机位姿 T_cw(从 world 到 camera)
3. 输出稀疏点云地图
"""
def __init__(self, config):
# 支持多种 SLAM 主干
self.slam = DROIDSLAM(config)
# 或者: self.slam = MASt3rSLAM(config)
# 或者: self.slam = VGGT_SLAM(config)
def process_sequence(self, images):
trajectories = []
point_clouds = []
for i, frame in enumerate(images):
# 单目或 RGB-D 模式
if self.is_rgbd(frame):
depth = frame.depth
else:
depth = self.estimate_depth(frame)
# 增量式位姿估计
pose = self.slam.track(frame, depth)
trajectories.append(pose)
# 构建稠密点云
if i % self.keyframe_interval == 0:
pc = self.dense_reconstruction(frame, depth, pose)
point_clouds.append(pc)
# 全局优化:BA(Bundle Adjustment)细化轨迹
trajectories = self.global_optimization(trajectories, point_clouds)
return trajectories, self.merge_point_clouds(point_clouds)
五、第二层:几何一致的 3DGS 地图构建
5.1 为什么用 3DGS 而不是 TSDF/八叉树
传统的三维重建方法中,TSDF(截断有符号距离函数)和八叉树是最常见的选择。但 FreeOcc 选择 3DGS(3D Gaussian Splatting,三维高斯泼溅) 作为几何表达,理由非常充分:
| 特性 | TSDF/八叉树 | 3DGS |
|---|---|---|
| 几何精度 | 中等,受体素分辨率限制 | 高,可微优化 |
| 新视角合成 | 需要额外计算 | 原生支持 |
| 语义关联 | 困难 | 通过特征提升天然支持 |
| 计算效率 | 高 | 中等,但够用 |
3DGS 用高斯基元(Gaussian primitives)来表示三维空间。每个高斯由以下属性定义:
import torch
import numpy as np
# 高斯基元的核心数据结构
class GaussianPrimitive:
"""
单个三维高斯基元。
3DGS 用一系列高斯来「覆盖」场景表面,
每个高斯的属性共同决定了场景的appearance和geometry。
"""
def __init__(self):
# 位置:三维高斯椭球的几何中心
self.mean = torch.zeros(3) # [x, y, z]
# 协方差矩阵:决定椭球的形状和方向
# 通过 SVD 分解为 R(旋转)× S(缩放)
self.rotation = torch.eye(3) # 3×3 旋转矩阵
self.scale = torch.ones(3) # 沿三个主轴的缩放
# 外观属性
self.opacity = torch.tensor(0.5) # 不透明度
self.color = torch.rand(3) # RGB 颜色
# [FreeOcc 新增] 语义属性:语言嵌入向量
self.semantic_feature = None # 由第三层语义地图填充
def compute_covariance(self):
"""将 rotation × scale 转换为协方差矩阵 Σ = R S S^T R^T"""
S = torch.diag(self.scale)
return self.rotation @ S @ S.T @ self.rotation.T
def get_3d_covariance(self):
"""返回用于 splatting 的协方差矩阵"""
return self.compute_covariance()
5.2 几何感知初始化(G-ini)
FreeOcc 的第一个关键创新:几何感知初始化(Geometry-aware Initialization)。
传统 3DGS 的初始化通常是随机的或者基于SfM(运动恢复结构)点云。问题是,初始化的高斯位置是「随意」的,在后续优化中容易偏离真实几何。
FreeOcc 的做法是:
- 以 SLAM 点云位置作为高斯初始中心:每个高斯的
mean初始值直接来自 SLAM 重建的三维点,而不是随机初始化 - 沿观测射线各向异性展开:高斯在初始化时不是球形的,而是根据相机观测方向,天然形成与成像几何一致的形状
class GeometryAwareInitialization:
"""
FreeOcc 的核心创新之一:几何感知初始化
关键思想:高斯的初始几何必须「天然合理」,
不能指望优化器从一团混乱中「学会」正确的几何形状。
"""
def __init__(self, slam_backbone, config):
self.slam = slam_backbone
self.config = config
def initialize_gaussians(self, point_cloud, image, camera_pose):
"""
基于点云和相机位姿初始化高斯地图
Args:
point_cloud: SLAM 输出的稀疏点云 (N, 3)
image: 当前视角的 RGB 图像
camera_pose: 当前相机的位姿 T_cw (4, 4)
"""
gaussians = []
for point in point_cloud.points:
# Step 1: 以 SLAM 点云点作为高斯中心
g = GaussianPrimitive()
g.mean = torch.tensor(point, dtype=torch.float32)
# Step 2: 各向异性初始化
# 沿观测射线方向展开高斯
ray_dir = self.compute_ray_direction(point, camera_pose)
g = self._anisotropic_expansion(g, ray_dir)
# Step 3: 从图像提取初始颜色
g.color = self._sample_color_from_image(point, image, camera_pose)
# Step 4: 初始不透明度设为中间值
g.opacity = torch.tensor(0.3, dtype=torch.float32)
gaussians.append(g)
return gaussians
def _anisotropic_expansion(self, gaussian, ray_dir):
"""
各向异性展开:根据观测方向,
在垂直于射线方向扩展更大,沿射线方向扩展较小
这样初始化的高斯形状天然符合针孔相机的成像几何,
而不是随机的球形或椭球形。
"""
# 构建沿射线方向的旋转矩阵
up = torch.tensor([0, 1, 0], dtype=torch.float32)
right = torch.cross(ray_dir, up, dim=-1)
right = right / (torch.norm(right) + 1e-8)
R = torch.stack([right, torch.cross(ray_dir, right), ray_dir], dim=0)
# 在垂直射线方向扩展大,沿射线方向扩展小
scale = torch.tensor([self.config.aniso_scale_perp,
self.config.aniso_scale_perp,
self.config.aniso_scale_axial],
dtype=torch.float32)
gaussian.rotation = R
gaussian.scale = scale
return gaussian
5.3 几何锚定高斯更新(GAGU)
FreeOcc 的第二个关键创新:几何锚定高斯更新(Geometrically Anchored Gaussian Updates)。
传统 3DGS 的优化目标是 渲染误差最小化——只要新视角合成的 RGB 和深度看起来对,高斯位置怎么动都行。但这在占据预测中是有害的。
GAGU 的核心约束是:高斯的几何中心永远不能偏离 SLAM 重建的三维点。
class GeometricallyAnchoredGaussianUpdate:
"""
FreeOcc 的核心创新之二:几何锚定高斯更新
关键思想:在优化过程中,高斯位置不能「自由漂移」,
必须始终锚定在 SLAM 重建的三维点上。
这解决了 3DGS 的根本矛盾:
渲染需要灵活的位置,但占据预测需要稳定的几何边界。
"""
def __init__(self, slam_point_cloud, anchor_strength=1.0):
self.slam_points = slam_point_cloud # SLAM 重建的三维点云
self.anchor_strength = anchor_strength # 锚定强度
def anchor_loss(self, gaussian):
"""
计算几何锚定损失:让高斯中心靠近最近的 SLAM 点
这个约束防止了优化过程中的几何漂移——
每一步更新后,高斯都会「被拉回」SLAM 锚点附近。
"""
# 找到最近的 SLAM 锚点
distances = torch.norm(self.slam_points - gaussian.mean, dim=1)
min_idx = torch.argmin(distances)
anchor_point = self.slam_points[min_idx]
# 锚定损失:如果高斯偏离锚点太远,给予惩罚
distance_to_anchor = distances[min_idx]
loss = self.anchor_strength * distance_to_anchor
return loss, anchor_point
def constrained_update(self, gaussian, gradients, lr=0.0001):
"""
受约束的高斯更新:在梯度更新后应用几何锚定
正常的梯度更新 + 锚定修正:
新位置 = 当前位置 - lr * 梯度 - 锚定修正
"""
# Step 1: 正常梯度更新
new_mean = gaussian.mean - lr * gradients['mean']
# Step 2: 锚定修正——将高斯拉回最近的 SLAM 点附近
# 这里用软约束:不是强制拉回,而是增加一个惩罚项的梯度
anchor_loss_val, anchor_point = self.anchor_loss(gaussian)
# 如果高斯偏离锚点超过阈值,启动锚定修正
if torch.norm(new_mean - anchor_point) > self.config.anchor_threshold:
# 沿偏离方向的反方向施加修正力
deviation = new_mean - anchor_point
deviation_magnitude = torch.norm(deviation)
correction = (deviation / deviation_magnitude) * \
min(deviation_magnitude - self.config.anchor_threshold,
self.config.max_correction)
new_mean = new_mean - self.config.anchor_strength * correction
# Step 3: 更新缩放和不透明度(这些可以自由优化)
new_scale = gaussian.scale - lr * gradients['scale']
new_opacity = gaussian.opacity - lr * gradients['opacity']
new_opacity = torch.clamp(new_opacity, 0.0, 1.0)
gaussian.mean = new_mean
gaussian.scale = torch.clamp(new_scale, 0.001, 10.0)
gaussian.opacity = new_opacity
return gaussian
5.4 G-ini + GAGU 的联合效果
消融实验数据最能说明问题:
| 配置 | IoU | mIoU | FPS |
|---|---|---|---|
| 无 GAGU,无 G-ini | 27.98 | 11.20 | 8.8 |
| 仅有 GAGU | 40.18 | 16.03 | 25.0 |
| 完整 FreeOcc(+G-ini) | 45.03 | 18.37 | 24.6 |
从 8.8 FPS 提升到 25.0 FPS 的关键在于:锚定在高斯位置后,每次优化迭代的搜索空间大幅缩小,优化收敛速度提升近3倍。 这是一个典型的「有约束的优化反而更快」的例子——约束不是限制,而是帮助优化器更快找到正确解。
六、第三层:开放词汇语义关联
6.1 不训练分类头,用 VLM 提取语义
FreeOcc 最优雅的设计之一:不需要训练语义分类头。传统方法需要为每个语义类别训练一个分类网络(如「识别椅子」「识别桌子」),换一个新类别就要重新训练。
FreeOcc 直接利用预训练的视觉语言模型(VLM)——这意味着可以用任意自然语言来查询,而不需要提前定义类别集合。
论文中使用 CLIP 或 OpenCLIP 作为语义特征提取器,将二维图像中的每个像素映射到一个高维语义空间:
import torch
import torch.nn.functional as F
class OpenVocabularySemanticMap:
"""
第三层:开放词汇语义地图
利用预训练视觉语言模型(VLM)从二维图像中提取语言对齐语义特征,
然后通过几何对应关系将二维语义提升到三维高斯基元上。
关键:不训练任何语义分类网络!
所有语义能力来自预训练的 VLM(如 CLIP)。
"""
def __init__(self, vlm_model_name="openai/clip-vit-base-patch32"):
# 加载预训练的 CLIP 模型(不需要微调!)
self.clip_model, self.clip_preprocess = self._load_clip(vlm_model_name)
self.clip_model.eval() # 推理模式,不更新权重
# 预计算文本查询 embedding(按需查询)
self.text_cache = {}
def extract_2d_semantic_features(self, image, camera_intrinsics):
"""
从单张图像中提取像素级语义特征
Args:
image: (3, H, W) 的 RGB 图像张量
camera_intrinsics: 相机内参矩阵 K (3, 3)
Returns:
pixel_features: (D, H, W) 的语义特征图,D=embedding维度
"""
# Step 1: 将图像输入 CLIP 图像编码器
with torch.no_grad():
image_embedding = self.clip_model.encode_image(image)
# Step 2: CLIP 输出的是全局特征,需要用注意力图/GradCAM 等方法
# 提取像素级语义。这里用一种简化的 patch-level 方式:
# 将图像划分为 patch grid
patch_size = 16
H, W = image.shape[1], image.shape[2]
n_patches_h = H // patch_size
n_patches_w = W // patch_size
# 对每个 patch 提取 CLIP 特征
patch_features = []
for i in range(n_patches_h):
for j in range(n_patches_w):
# 裁剪 patch
patch = image[:,
i*patch_size:(i+1)*patch_size,
j*patch_size:(j+1)*patch_size]
# 调整大小为 CLIP 期望的输入尺寸
patch_resized = F.interpolate(
patch.unsqueeze(0),
size=(224, 224),
mode='bilinear',
align_corners=False
).squeeze(0)
with torch.no_grad():
patch_feat = self.clip_model.encode_image(patch_resized.unsqueeze(0))
patch_features.append(patch_feat.squeeze(0))
# 重建为 (D, H, W) 的特征图
patch_features = torch.stack(patch_features, dim=1) # (D, n_patches_h * n_patches_w)
feature_map = patch_features.view(
patch_features.shape[0],
n_patches_h, n_patches_w
)
# 上采样回原始图像分辨率
feature_map = F.interpolate(
feature_map.unsqueeze(0),
size=(H, W),
mode='bilinear',
align_corners=False
).squeeze(0)
return feature_map # (D, H, W)
def project_2d_to_3d(self, pixel_features, depth_map, camera_pose, camera_intrinsics):
"""
将二维语义特征提升到三维高斯基元上
核心逻辑:对于每个三维高斯,找到其在图像上的投影位置,
然后将对应像素的语义特征赋给该高斯。
Args:
pixel_features: (D, H, W) 的语义特征图
depth_map: (H, W) 的深度图
camera_pose: 相机位姿 T_cw (4, 4)
camera_intrinsics: 相机内参 K (3, 3)
Returns:
gaussian_features: (N, D) 每个高斯的语义嵌入
"""
D = pixel_features.shape[0]
gaussian_features = []
for gaussian in self.gaussians:
# Step 1: 将高斯中心从世界坐标投影到像素坐标
point_cam = self.world_to_camera(gaussian.mean, camera_pose)
if point_cam[2] < 0: # 相机后方,跳过
gaussian_features.append(torch.zeros(D))
continue
# Step 2: 使用相机内参投影到像素平面
u = camera_intrinsics[0, 0] * point_cam[0] / point_cam[2] + camera_intrinsics[0, 2]
v = camera_intrinsics[1, 1] * point_cam[1] / point_cam[2] + camera_intrinsics[1, 2]
# Step 3: 双线性采样语义特征
if 0 <= u < depth_map.shape[1] and 0 <= v < depth_map.shape[0]:
feature = bilinear_sample(pixel_features, u, v)
else:
feature = torch.zeros(D)
gaussian_features.append(feature)
return torch.stack(gaussian_features) # (N, D)
def text_query(self, query_text, gaussian_features):
"""
文本查询:在三维语义地图中定位给定文本对应的目标区域
核心:计算查询文本的 CLIP embedding,然后与每个高斯的语义嵌入
做余弦相似度匹配,找到最相关的区域。
"""
if query_text not in self.text_cache:
# 编码查询文本( CLIP 只需要计算一次,可以缓存)
text_tokens = self.clip_model.tokenize([query_text])
with torch.no_grad():
text_embedding = self.clip_model.encode_text(text_tokens)
self.text_cache[query_text] = text_embedding
else:
text_embedding = self.text_cache[query_text]
# 余弦相似度
text_embedding = F.normalize(text_embedding, dim=-1)
gaussian_features_norm = F.normalize(gaussian_features, dim=-1)
similarity = (text_embedding @ gaussian_features_norm.T).squeeze(0)
return similarity # (N,) 每个高斯与查询文本的匹配分数
6.2 语义融合的时序累积
关键点:语义不是从单帧图像提取后就固定的,而是随着机器人持续观测而不断融合更新的。
class IncrementalSemanticFusion:
"""
语义信息的时序累积更新
随着更多视角的观测融合,每个高斯的语义置信度会持续提升。
这里用了指数滑动平均的方式。
"""
def __init__(self, momentum=0.9):
self.momentum = momentum
# 每个高斯维护一个累积的语义状态
self.semantic_state = {} # gaussian_id -> running_mean_feature
def update(self, gaussian_id, new_features, confidence):
"""
更新某个高斯的语义特征
Args:
confidence: 当前观测的置信度(与深度测量的不确定性相关)
"""
if gaussian_id not in self.semantic_state:
self.semantic_state[gaussian_id] = new_features
return
# 置信度加权的指数滑动平均
alpha = self.momentum * confidence
self.semantic_state[gaussian_id] = (
alpha * self.semantic_state[gaussian_id] +
(1 - alpha) * new_features
)
def get_fused_feature(self, gaussian_id):
return self.semantic_state.get(gaussian_id, None)
七、第四层:概率式高斯→占据投影
7.1 为什么需要概率投影
三维高斯是连续表达(位置、缩放都是连续的浮点数),而占据地图是离散表达(每个体素只有「占据/空闲/未知」三种状态)。从连续到离散的转换,需要一个可靠的投影机制。
FreeOcc 的核心算法:对于每个体素位置,检索其邻域范围内的高斯,根据高斯的空间支持范围计算该体素被占据的概率。
import torch
import torch.nn.functional as F
import numpy as np
class GaussianToOccupancyProjection:
"""
第四层:概率式高斯→占据投影
将连续的高斯地图转换为离散的三维占据地图。
核心思想:每个高斯「贡献」其周围的体素。
靠近高斯中心的体素被占据的概率高,远离的概率低。
"""
def __init__(self, voxel_size, grid_shape):
"""
Args:
voxel_size: 体素的物理尺寸(米)
grid_shape: (D, H, W) 体素网格尺寸
"""
self.voxel_size = voxel_size
self.grid_shape = grid_shape # 体素网格的尺寸
def project(self, gaussians, semantic_features=None):
"""
将高斯地图投影到体素占据地图
Args:
gaussians: 高斯列表
semantic_features: 可选,语义嵌入 (N, D)
Returns:
occupancy_volume: (D, H, W, 4) 的占据概率 + 语义
最后维度为 [occupancy_prob, semantic_D其中前D个]
"""
D, H, W = self.grid_shape
device = gaussians[0].mean.device
# 初始化占据体积
occupancy_volume = torch.zeros(D, H, W, device=device)
total_weight = torch.zeros(D, H, W, device=device)
if semantic_features is not None:
semantic_volume = torch.zeros(D, H, W, semantic_features.shape[1], device=device)
for i, g in enumerate(gaussians):
# Step 1: 计算高斯的协方差矩阵
cov = g.compute_covariance()
# 提取主轴长度(高斯sigma)
eigenvalues = torch.linalg.eigvalsh(cov)
sigma = torch.sqrt(eigenvalues.max()) # 最大方向的标准差
# Step 2: 确定高斯的影响范围
# 3-sigma 范围内覆盖 >99% 的概率质量
influence_radius = 3.0 * sigma
# Step 3: 找到受影响的体素范围
center_voxel = (g.mean / self.voxel_size).long()
radius_voxel = int(np.ceil(influence_radius / self.voxel_size))
z_min = max(0, center_voxel[0] - radius_voxel)
z_max = min(D, center_voxel[0] + radius_voxel + 1)
y_min = max(0, center_voxel[1] - radius_voxel)
y_max = min(H, center_voxel[1] + radius_voxel + 1)
x_min = max(0, center_voxel[2] - radius_voxel)
x_max = min(W, center_voxel[2] + radius_voxel + 1)
if z_min >= z_max or y_min >= y_max or x_min >= x_max:
continue
# Step 4: 对影响范围内的每个体素计算占据概率
z_coords = torch.arange(z_min, z_max, device=device)
y_coords = torch.arange(y_min, y_max, device=device)
x_coords = torch.arange(x_min, x_max, device=device)
zz, yy, xx = torch.meshgrid(z_coords, y_coords, x_coords, indexing='ij')
# 体素中心的世界坐标
voxel_centers = torch.stack([
(zz.float() + 0.5) * self.voxel_size,
(yy.float() + 0.5) * self.voxel_size,
(xx.float() + 0.5) * self.voxel_size,
], dim=-1) # (n_voxels, 3)
# 计算 Mahalanobis 距离
diff = voxel_centers - g.mean # (n_voxels, 3)
# 通过 SVD 分解求逆协方差
try:
cov_inv = torch.linalg.inv(cov + 1e-6 * torch.eye(3, device=device))
except:
cov_inv = torch.eye(3, device=device) / (sigma ** 2 + 1e-6)
mahal = torch.sum(diff @ cov_inv * diff, dim=-1) # (n_voxels,)
# 高斯概率密度(归一化)
gaussian_density = torch.exp(-0.5 * mahal)
gaussian_density = gaussian_density * g.opacity # 乘以不透明度
# 累加到占据体积
occupancy_volume[z_min:z_max, y_min:y_max, x_min:x_max] += \
gaussian_density.view(z_max-z_min, y_max-y_min, x_max-x_min)
total_weight[z_min:z_max, y_min:y_max, x_min:x_max] += 1.0
# 语义传播:如果高斯有语义特征,也同步传播
if semantic_features is not None:
feat = semantic_features[i] # (D,)
semantic_volume[z_min:z_max, y_min:y_max, x_min:x_max] += \
(gaussian_density * feat).view(z_max-z_min, y_max-y_min, x_max-x_min, -1)
# Step 5: 归一化 + 占据概率二值化
total_weight = total_weight.clamp(min=1.0)
occupancy_volume = occupancy_volume / total_weight
# 占据/空闲/未知的三分类
# 使用 Otsu 或固定阈值
threshold = self.compute_threshold(occupancy_volume)
occupied_mask = occupancy_volume > threshold
free_mask = occupancy_volume < (threshold * 0.3)
unknown_mask = ~(occupied_mask | free_mask)
result = torch.zeros(D, H, W, 4, device=device)
result[occupied_mask, 0] = 1.0
result[free_mask, 0] = 0.0
result[unknown_mask, 0] = 0.5 # 0.5 = unknown
if semantic_features is not None:
total_weight_expanded = total_weight.unsqueeze(-1).clamp(min=1.0)
semantic_volume = semantic_volume / total_weight_expanded
result[..., 1:] = semantic_volume # 后 D 个通道是语义嵌入
return result
def compute_threshold(self, volume):
"""Otsu 自动阈值,或者使用固定阈值"""
return torch.quantile(volume, 0.7) # 70% 分位数作为阈值
7.2 语义后验传播
占据地图的语义不是简单地从高斯复制过来的——FreeOcc 用了局部高斯混合模型的后验责任传播:
class SemanticPosteriorPropagation:
"""
语义后验传播:在占据地图生成过程中,
通过高斯混合模型将语义信息从高斯基元传播到体素。
这解决了「体素本身没有语义特征,只有高斯有语义特征」的问题。
"""
def __init__(self, volume_resolution):
self.resolution = volume_resolution
def propagate(self, gaussians, gaussian_semantic_features, occupancy_volume):
"""
将高斯的语义嵌入传播到占据体素
核心:每个体素的语义 = 其邻域高斯的语义加权平均
权重 = 高斯在该体素位置的概率密度 × 占据置信度
"""
# ... 实现省略,核心逻辑是加权平均
pass
八、实验结果与性能分析
8.1 EmbodiedOcc-ScanNet 数据集
这是目前具身场景中最常用的占据预测评测数据集之一。
| 方法 | 训练需求 | 相机位姿 | IoU | mIoU |
|---|---|---|---|---|
| Supervised(监督) | 大规模3D标注 | GT | SOTA | SOTA |
| GaussianOcc(自监督) | 无3D标注 | GT | 10.17 | 4.34 |
| GaussTR(自监督) | 无3D标注 | GT | 15.63 | 4.95 |
| FreeOcc Mono(单目) | 无需训练 | 无需GT | 31.29 | 13.86 |
| FreeOcc RGB-D | 无需训练 | 无需GT | 34.40 | 15.84 |
关键数据:无需任何训练的 FreeOcc,在 IoU 和 mIoU 上均超过现有自监督方法两倍以上。单目版本就已经大幅领先。
8.2 ReplicaOcc 基准数据集
这是论文自建的跨数据集泛化基准,基于 Replica 场景构建。相比 ScanNet,ReplicaOcc 提供了更细粒度的语义类别体系,更能考验开放词汇能力。
| 方法 | 来源 | IoU | mIoU |
|---|---|---|---|
| Supervised EmbodiedOcc | EmbodiedOcc-ScanNet训练 | ~崩溃 | ~0 |
| GaussianOcc | 自监督 | 极低 | 极低 |
| GaussTR | 自监督 | 极低 | 极低 |
| FreeOcc Mono | 无训练 | 46.81 | 16.93 |
| FreeOcc RGB-D | 无训练 | 55.65 | 20.90 |
这个表格非常有说服力:学习式方法从训练集(EmbodiedOcc-ScanNet)迁移到全新数据集(ReplicaOcc)后几乎完全失效,而 FreeOcc 的性能保持稳定。这就是「无需训练」范式的真正意义——泛化能力是内建的,而不是学出来的。
8.3 实时性能
| 配置 | 平均 IoU | 平均 FPS |
|---|---|---|
| FreeOcc(无GAGU/G-ini) | 27.98 | 8.8 |
| FreeOcc(+GAGU) | 40.18 | 25.0 |
| FreeOcc(完整) | 45.03 | 24.6 |
在消费级硬件上(RTX 3090 或类似),FreeOcc 实现了 ~25 FPS 的实时处理能力,这对于机器人在线导航来说是完全可用的。
九、真实机器人部署
9.1 硬件配置
- 深度相机:Intel RealSense D435i(RGB-D 输入)
- 计算平台:Intel i9-14900KF + NVIDIA RTX 5090
- 完整系统运行模式:在线,无需预录轨迹、真实位姿输入或离线优化
9.2 联合 Qwen3-VL 实现全流程开放词汇
在真实部署中,论文进一步引入了 Qwen3-VL 多模态大模型,用于自动从 RGB 图像中生成场景级开放词汇标签,然后将这些标签注入 FreeOcc 系统:
RGB图像 → Qwen3-VL → 场景物体列表("台灯""红杯子""蓝盒子"...)
↓
FreeOcc 语义地图构建(注入 Qwen3-VL 生成的类别)
↓
任意文本查询 → "红杯子在哪里?" → 三维定位结果
这形成了完整的闭环:感知 → 认知 → 推理。
9.3 真实场景实测
在真实室内和室外场景中,FreeOcc 能够:
- 根据「篮子」「时钟」「绿植」「挂画」等文本查询准确定位对应目标
- 根据「红色杯子」「黄色杯子」「蓝色杯子」等细粒度颜色区分查询,区分外观相似但颜色不同的物体
- 随着机器人的持续移动,在线增量完善三维地图
十、技术意义与未来展望
10.1 从「学模型」到「构建系统」的范式转变
FreeOcc 最重要的意义不是某个算法指标的提升,而是思路的根本转变:
传统路线:收集大量数据 → 训练神经网络 → 过拟合特定场景 → 泛化失败
FreeOcc 路线:通用几何锚点(SLAM)+ 连续几何表达(3DGS)+ 预训练语义(VLM)+ 概率推理 = 通用感知系统
这与 CV 领域「foundation model」的思路一脉相承——不要训练一个会做某件事的模型,而是构建一个能理解任意事物的系统。
10.2 对机器人行业的启示
当前状态:机器人在进入新环境前需要 SLAM 建图,需要针对特定任务训练模型,泛化能力差。
FreeOcc 展示的可能性:机器人进入新环境后,只需要传感器输入,就能实时构建「理解任意自然语言查询」的三维语义地图。这意味着:
- 部署成本大幅降低:不需要为每个新环境重新训练或微调模型
- 人机交互自然化:用户可以用自然语言直接命令机器人「去拿红色杯子」,而不需要懂任何技术
- 持续学习成为可能:机器人每次进入新环境都能「即时学习」该环境的语义,而不需要离线训练
10.3 局限性与未来方向
当然,FreeOcc 也有局限性:
- 计算资源需求:3DGS 优化对 GPU 显存和算力有一定要求,嵌入式部署仍是挑战
- 动态场景处理:当前版本主要针对静态场景,动态物体的处理尚待加强
- 大规模室外场景:目前主要在室内场景验证,室外光照变化、纹理缺失等挑战尚需探索
未来可能的发展方向:
- 联合端到端学习(而不是完全无监督),在 FreeOcc 框架上加入轻量级的自适应模块
- 多机器人协作:多个机器人的观测如何融合到统一的占据地图中
- 与 LLM/Agent 框架的深度整合:让机器人不仅能「看到」环境,还能「理解」任务并规划行动
十一、总结
FreeOcc 的出现,让我们看到了机器人具身感知的一条全新技术路线:
- 不依赖大规模三维标注,因为几何来自 SLAM,语义来自预训练的 VLM
- 不依赖真实相机位姿,因为几何一致性由 SLAM 保证
- 不依赖封闭类别集合,因为开放词汇能力来自 CLIP 这类视觉语言模型
- 零样本泛化,因为系统基于确定性几何和通用语义,而非统计学习的特定模式
这意味着,当机器人进入一个它从未见过的房间时,它不需要「回忆」之前训练过的类似场景——它可以直接依靠传感器输入,在几分钟内构建出「能回答任意自然语言空间查询」的三维语义地图。
这不是科幻,这是 2026 年机器人研究的最前沿。
参考资料
- 论文:FreeOcc: Training-Free Embodied Open-Vocabulary Occupancy Prediction(RSS 2026)
- arXiv:https://arxiv.org/abs/2604.28115
- 项目主页:https://the-masses.github.io/freeocc-web/
- GitHub:https://github.com/the-masses/FreeOcc
- 数据集:https://huggingface.co/datasets/the-masses/ReplicaOcc
作者:程序员茄子 | 首发于 程序员茄子 | 原创深度技术解析