编程 FreeOcc 深度解析:机器人具身智能的「无监督觉醒」——首个无需训练的开放词汇三维占据预测系统

2026-05-15 21:48:41 +0800 CST views 5

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 的做法是:

  1. 以 SLAM 点云位置作为高斯初始中心:每个高斯的 mean 初始值直接来自 SLAM 重建的三维点,而不是随机初始化
  2. 沿观测射线各向异性展开:高斯在初始化时不是球形的,而是根据相机观测方向,天然形成与成像几何一致的形状
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 的联合效果

消融实验数据最能说明问题:

配置IoUmIoUFPS
无 GAGU,无 G-ini27.9811.208.8
仅有 GAGU40.1816.0325.0
完整 FreeOcc(+G-ini)45.0318.3724.6

从 8.8 FPS 提升到 25.0 FPS 的关键在于:锚定在高斯位置后,每次优化迭代的搜索空间大幅缩小,优化收敛速度提升近3倍。 这是一个典型的「有约束的优化反而更快」的例子——约束不是限制,而是帮助优化器更快找到正确解。


六、第三层:开放词汇语义关联

6.1 不训练分类头,用 VLM 提取语义

FreeOcc 最优雅的设计之一:不需要训练语义分类头。传统方法需要为每个语义类别训练一个分类网络(如「识别椅子」「识别桌子」),换一个新类别就要重新训练。

FreeOcc 直接利用预训练的视觉语言模型(VLM)——这意味着可以用任意自然语言来查询,而不需要提前定义类别集合。

论文中使用 CLIPOpenCLIP 作为语义特征提取器,将二维图像中的每个像素映射到一个高维语义空间:

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 数据集

这是目前具身场景中最常用的占据预测评测数据集之一。

方法训练需求相机位姿IoUmIoU
Supervised(监督)大规模3D标注GTSOTASOTA
GaussianOcc(自监督)无3D标注GT10.174.34
GaussTR(自监督)无3D标注GT15.634.95
FreeOcc Mono(单目)无需训练无需GT31.2913.86
FreeOcc RGB-D无需训练无需GT34.4015.84

关键数据:无需任何训练的 FreeOcc,在 IoU 和 mIoU 上均超过现有自监督方法两倍以上。单目版本就已经大幅领先。

8.2 ReplicaOcc 基准数据集

这是论文自建的跨数据集泛化基准,基于 Replica 场景构建。相比 ScanNet,ReplicaOcc 提供了更细粒度的语义类别体系,更能考验开放词汇能力。

方法来源IoUmIoU
Supervised EmbodiedOccEmbodiedOcc-ScanNet训练~崩溃~0
GaussianOcc自监督极低极低
GaussTR自监督极低极低
FreeOcc Mono无训练46.8116.93
FreeOcc RGB-D无训练55.6520.90

这个表格非常有说服力:学习式方法从训练集(EmbodiedOcc-ScanNet)迁移到全新数据集(ReplicaOcc)后几乎完全失效,而 FreeOcc 的性能保持稳定。这就是「无需训练」范式的真正意义——泛化能力是内建的,而不是学出来的

8.3 实时性能

配置平均 IoU平均 FPS
FreeOcc(无GAGU/G-ini)27.988.8
FreeOcc(+GAGU)40.1825.0
FreeOcc(完整)45.0324.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 能够:

  1. 根据「篮子」「时钟」「绿植」「挂画」等文本查询准确定位对应目标
  2. 根据「红色杯子」「黄色杯子」「蓝色杯子」等细粒度颜色区分查询,区分外观相似但颜色不同的物体
  3. 随着机器人的持续移动,在线增量完善三维地图

十、技术意义与未来展望

10.1 从「学模型」到「构建系统」的范式转变

FreeOcc 最重要的意义不是某个算法指标的提升,而是思路的根本转变

传统路线:收集大量数据 → 训练神经网络 → 过拟合特定场景 → 泛化失败

FreeOcc 路线:通用几何锚点(SLAM)+ 连续几何表达(3DGS)+ 预训练语义(VLM)+ 概率推理 = 通用感知系统

这与 CV 领域「foundation model」的思路一脉相承——不要训练一个会做某件事的模型,而是构建一个能理解任意事物的系统

10.2 对机器人行业的启示

当前状态:机器人在进入新环境前需要 SLAM 建图,需要针对特定任务训练模型,泛化能力差。

FreeOcc 展示的可能性:机器人进入新环境后,只需要传感器输入,就能实时构建「理解任意自然语言查询」的三维语义地图。这意味着:

  1. 部署成本大幅降低:不需要为每个新环境重新训练或微调模型
  2. 人机交互自然化:用户可以用自然语言直接命令机器人「去拿红色杯子」,而不需要懂任何技术
  3. 持续学习成为可能:机器人每次进入新环境都能「即时学习」该环境的语义,而不需要离线训练

10.3 局限性与未来方向

当然,FreeOcc 也有局限性:

  1. 计算资源需求:3DGS 优化对 GPU 显存和算力有一定要求,嵌入式部署仍是挑战
  2. 动态场景处理:当前版本主要针对静态场景,动态物体的处理尚待加强
  3. 大规模室外场景:目前主要在室内场景验证,室外光照变化、纹理缺失等挑战尚需探索

未来可能的发展方向:

  • 联合端到端学习(而不是完全无监督),在 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

作者:程序员茄子 | 首发于 程序员茄子 | 原创深度技术解析

推荐文章

Go配置镜像源代理
2024-11-19 09:10:35 +0800 CST
JS 箭头函数
2024-11-17 19:09:58 +0800 CST
使用 Git 制作升级包
2024-11-19 02:19:48 +0800 CST
动态渐变背景
2024-11-19 01:49:50 +0800 CST
任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
在 Docker 中部署 Vue 开发环境
2024-11-18 15:04:41 +0800 CST
MySQL死锁 - 更新插入导致死锁
2024-11-19 05:53:50 +0800 CST
基于Webman + Vue3中后台框架SaiAdmin
2024-11-19 09:47:53 +0800 CST
Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
如何使用go-redis库与Redis数据库
2024-11-17 04:52:02 +0800 CST
一个简单的打字机效果的实现
2024-11-19 04:47:27 +0800 CST
Vue3中如何处理组件的单元测试?
2024-11-18 15:00:45 +0800 CST
程序员茄子在线接单