一句话总结
3DGSNav 让视觉-语言模型(VLM)在陌生环境中找东西时,不再依赖”简化的语义地图”,而是用 3D 高斯泼溅(3DGS)构建持久的 3D 记忆,通过自由视角渲染来增强空间推理能力。
为什么这个问题重要?
想象你让机器人去找”冰箱里的牛奶”:
- 传统方法:先把房间抽象成语义地图(”这是厨房,那是客厅”),再在地图上规划路径
- 问题:一旦低层感知出错(把冰箱误识别为柜子),高层决策就全盘皆输
核心痛点:
- 语义地图丢失了几何细节(形状、纹理、空间关系)
- VLM 只能看到当前视角,缺乏”回忆过去”的能力
- 目标物体可能被遮挡,需要主动切换视角验证
3DGSNav 的创新:
- 用 3DGS 作为 VLM 的”外部记忆”,保留完整的 3D 几何和外观信息
- 支持自由视角渲染,让 VLM 能”回头看”或”换个角度看”
- 设计了结构化视觉提示 + 链式思考(CoT),引导 VLM 更好地推理
背景知识
3D 表示方式对比
| 表示方式 | 存储内容 | 优点 | 缺点 |
|---|---|---|---|
| 语义地图 | 2D 网格 + 语义标签 | 轻量,易规划 | 丢失 3D 信息 |
| 点云 | 3D 点 + RGB | 保留几何 | 渲染质量差 |
| Mesh | 三角面片 | 精确重建 | 重建耗时 |
| 3DGS | 3D 高斯分布 | 实时渲染,保留外观 | 内存占用高 |
为什么选 3DGS?
3DGS 用一组 3D 高斯分布 表示场景: \(G(\mathbf{x}) = \alpha \cdot \exp\left(-\frac{1}{2}(\mathbf{x} - \boldsymbol{\mu})^T \Sigma^{-1} (\mathbf{x} - \boldsymbol{\mu})\right)\)
- $\boldsymbol{\mu}$:高斯中心(位置)
- $\Sigma$:协方差矩阵(形状)
- $\alpha$:不透明度
- 外观:球谐函数(SH)编码颜色,支持多视角一致性
关键优势:
- 可微渲染:支持梯度优化
- 自由视角:从任意角度渲染场景
- 实时性:GPU 加速渲染(30+ FPS)
VLM 在导航中的作用
VLM(如 GPT-4V)能理解图像 + 文本指令,但传统用法是”看一张图 → 做决策”。3DGSNav 的创新是让 VLM 看到:
- 历史轨迹渲染:回顾走过的地方
- 候选视角渲染:主动切换视角验证目标
核心方法
直觉解释
机器人接收任务:"找到沙发上的遥控器"
传统方法:
[当前视角] → VLM: "去客厅" → [移动] → [新视角] → VLM: "没看到,继续找"
问题:VLM 看不到之前走过的地方,可能重复搜索
3DGSNav:
[当前视角] → 更新 3DGS 记忆 → VLM 渲染"客厅候选视角" →
VLM: "从侧面看,沙发扶手旁边有个黑色物体,可能是遥控器" → 主动移动验证
Pipeline 概览
输入:RGB-D 图像流 + 文本指令
↓
1. 增量式 3DGS 构建(实时更新)
↓
2. 前沿探测(Frontier Detection)
↓
3. 轨迹引导的自由视角渲染
↓
4. 结构化视觉提示 + CoT 推理
↓
5. VLM 决策:移动 or 切换视角
↓
输出:导航动作
数学细节
1. 增量式 3DGS 更新
每接收一帧 RGB-D 图像 $(I_t, D_t)$,提取新高斯点: \(\mathcal{G}_t = \{(\boldsymbol{\mu}_i, \Sigma_i, \mathbf{c}_i, \alpha_i)\}_{i=1}^{N_t}\)
全局高斯集合通过合并更新: \(\mathcal{G}_{\text{global}} \leftarrow \mathcal{G}_{\text{global}} \cup \mathcal{G}_t\)
去重策略:距离 $|\boldsymbol{\mu}_i - \boldsymbol{\mu}_j| < \tau$ 的高斯点合并。
2. 自由视角渲染
给定相机位姿 $\mathbf{T} = [\mathbf{R} \mid \mathbf{t}]$,渲染像素颜色: \(C(\mathbf{p}) = \sum_{i \in \mathcal{V}} \alpha_i \prod_{j<i}(1-\alpha_j) \cdot \mathbf{c}_i\)
其中 $\mathcal{V}$ 是深度排序后的可见高斯点。
3. CoT 提示设计
任务:找到 {target}
历史观察:[过去 5 帧的缩略图]
当前视角:[第一人称渲染图]
候选视角:[3 个前沿方向的渲染图]
请按以下步骤推理:
1. 目标特征:{target} 通常出现在什么位置?
2. 场景理解:当前在什么房间?
3. 视角选择:哪个候选视角最可能看到目标?
4. 决策:移动 or 切换视角?
实现
环境配置
# 安装依赖
pip install torch torchvision open3d gsplat trimesh
# 克隆 3DGS 渲染器(使用 gsplat 简化版)
git clone https://github.com/nerfstudio-project/gsplat
cd gsplat && pip install -e .
核心代码
1. 增量式 3DGS 构建
import torch
import numpy as np
from gsplat import rasterization
class Incremental3DGS:
def __init__(self, device='cuda'):
self.device = device
# 存储全局高斯点:位置、协方差、颜色、不透明度
self.means = torch.empty((0, 3), device=device)
self.covs = torch.empty((0, 3, 3), device=device)
self.colors = torch.empty((0, 3), device=device)
self.opacities = torch.empty((0, 1), device=device)
def add_frame(self, rgb, depth, K, T):
"""
rgb: (H, W, 3) RGB 图像
depth: (H, W) 深度图
K: (3, 3) 相机内参
T: (4, 4) 相机位姿(世界坐标系到相机坐标系)
"""
H, W = depth.shape
# 1. 反投影到 3D(相机坐标系)
u, v = np.meshgrid(np.arange(W), np.arange(H))
points_cam = np.stack([
(u - K[0, 2]) * depth / K[0, 0],
(v - K[1, 2]) * depth / K[1, 1],
depth
], axis=-1) # (H, W, 3)
# 2. 转到世界坐标系
T_inv = np.linalg.inv(T)
points_world = points_cam @ T_inv[:3, :3].T + T_inv[:3, 3]
# 3. 下采样(每 4 个像素取一个)
mask = (depth > 0) & (depth < 10.0) # 有效深度范围
mask[::4, ::4] = False # 稀疏采样
new_means = torch.from_numpy(points_world[mask]).float().to(self.device)
new_colors = torch.from_numpy(rgb[mask]).float().to(self.device) / 255.0
# 4. 初始化协方差(球形高斯,半径 = 0.01m)
N = new_means.shape[0]
new_covs = torch.eye(3).unsqueeze(0).repeat(N, 1, 1).to(self.device) * 0.01**2
new_opacities = torch.ones((N, 1), device=self.device) * 0.9
# 5. 去重(简化版:距离阈值)
if self.means.shape[0] > 0:
dist = torch.cdist(new_means, self.means) # (N_new, N_old)
keep_mask = (dist.min(dim=1)[0] > 0.02) # 保留距离 > 2cm 的点
new_means = new_means[keep_mask]
new_colors = new_colors[keep_mask]
new_covs = new_covs[keep_mask]
new_opacities = new_opacities[keep_mask]
# 6. 合并到全局
self.means = torch.cat([self.means, new_means], dim=0)
self.colors = torch.cat([self.colors, new_colors], dim=0)
self.covs = torch.cat([self.covs, new_covs], dim=0)
self.opacities = torch.cat([self.opacities, new_opacities], dim=0)
# 7. 限制总数(内存管理)
if self.means.shape[0] > 500000:
self.prune_far_points()
def prune_far_points(self):
"""保留不透明度高的点"""
keep_idx = torch.argsort(self.opacities.squeeze(), descending=True)[:500000]
self.means = self.means[keep_idx]
self.colors = self.colors[keep_idx]
self.covs = self.covs[keep_idx]
self.opacities = self.opacities[keep_idx]
2. 自由视角渲染
def render_novel_view(gs_model, K, T, H=480, W=640):
"""
从任意位姿 T 渲染场景
返回: (H, W, 3) RGB 图像
"""
# 准备相机参数
viewmat = torch.from_numpy(T).float().cuda() # (4, 4)
# gsplat 渲染(简化调用)
rendered_rgb, _, _ = rasterization(
means=gs_model.means,
quats=cov_to_quat(gs_model.covs), # 协方差 → 四元数
scales=torch.ones_like(gs_model.means) * 0.01,
opacities=gs_model.opacities,
colors=gs_model.colors,
viewmats=viewmat.unsqueeze(0),
Ks=torch.from_numpy(K).float().cuda().unsqueeze(0),
width=W,
height=H,
)
return rendered_rgb[0].cpu().numpy() # (H, W, 3)
def cov_to_quat(covs):
"""协方差矩阵 → 四元数(简化版:假设各向同性)"""
# ... (省略特征值分解代码)
return torch.tensor([1, 0, 0, 0]).repeat(covs.shape[0], 1).cuda()
3. 前沿探测 + 候选视角生成
import cv2
def detect_frontiers(occupancy_map, robot_pos):
"""
occupancy_map: (H, W) 0=未知, 1=空闲, 2=占据
robot_pos: (x, y) 机器人当前位置(网格坐标)
返回: 候选前沿方向列表 [(angle_1, score_1), ...]
"""
# 1. 找到未知-空闲边界
kernel = np.ones((5, 5), np.uint8)
frontier = cv2.morphologyEx(
(occupancy_map == 0).astype(np.uint8),
cv2.MORPH_GRADIENT,
kernel
) & (occupancy_map == 1)
# 2. 聚类前沿点
frontier_points = np.argwhere(frontier > 0)
if len(frontier_points) == 0:
return []
# 3. 计算每个前沿方向的得分
candidates = []
for angle in np.linspace(0, 2*np.pi, 8, endpoint=False):
direction = np.array([np.cos(angle), np.sin(angle)])
# 统计该方向上的前沿点数
scores = np.dot(frontier_points - robot_pos, direction)
score = (scores > 0).sum()
candidates.append((angle, score))
# 4. 返回得分最高的 3 个方向
candidates.sort(key=lambda x: x[1], reverse=True)
return candidates[:3]
def generate_candidate_poses(robot_pose, frontier_angles, distance=1.0):
"""
生成候选相机位姿
robot_pose: (4, 4) 当前位姿
frontier_angles: [(angle, score), ...]
返回: [T1, T2, T3] 候选位姿列表
"""
poses = []
for angle, _ in frontier_angles:
# 平移 distance 米
T = robot_pose.copy()
T[0, 3] += distance * np.cos(angle)
T[1, 3] += distance * np.sin(angle)
# 旋转朝向该方向
T[:3, :3] = rotation_matrix_from_angle(angle)
poses.append(T)
return poses
def rotation_matrix_from_angle(angle):
"""2D 角度 → 3D 旋转矩阵(绕 Z 轴)"""
c, s = np.cos(angle), np.sin(angle)
return np.array([
[c, -s, 0],
[s, c, 0],
[0, 0, 1]
])
4. VLM 推理接口
import base64
from io import BytesIO
from PIL import Image
def query_vlm(images, prompt, api_key):
"""
调用 VLM API(以 OpenAI GPT-4V 为例)
images: 图像列表
prompt: 文本提示
返回: VLM 的文本回复
"""
import openai
openai.api_key = api_key
# 转换图像为 base64
image_urls = []
for img in images:
buffered = BytesIO()
Image.fromarray((img * 255).astype(np.uint8)).save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
image_urls.append(f"data:image/png;base64,{img_str}")
# 构造消息
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
*[{"type": "image_url", "image_url": {"url": url}} for url in image_urls]
]
}
]
response = openai.ChatCompletion.create(
model="gpt-4-vision-preview",
messages=messages,
max_tokens=300
)
return response.choices[0].message.content
5. 主循环
import torch
import numpy as np
from gsplat import rasterization
class Incremental3DGS:
def __init__(self, device='cuda'):
self.device = device
# 存储全局高斯点:位置、协方差、颜色、不透明度
self.means = torch.empty((0, 3), device=device)
self.covs = torch.empty((0, 3, 3), device=device)
self.colors = torch.empty((0, 3), device=device)
self.opacities = torch.empty((0, 1), device=device)
def add_frame(self, rgb, depth, K, T):
"""增量添加新帧"""
# 1. 反投影到 3D(相机坐标系)
# ... (像素网格生成代码省略)
points_cam = np.stack([...], axis=-1)
# 2. 转到世界坐标系
T_inv = np.linalg.inv(T)
points_world = points_cam @ T_inv[:3, :3].T + T_inv[:3, 3]
# 3. 下采样 + 颜色提取
# ... (掩码过滤代码省略)
new_means = torch.from_numpy(points_world[mask]).float().to(self.device)
new_colors = torch.from_numpy(rgb[mask]).float().to(self.device) / 255.0
# 4. 初始化协方差(球形高斯)
N = new_means.shape[0]
new_covs = torch.eye(3).unsqueeze(0).repeat(N, 1, 1).to(self.device) * 0.01**2
new_opacities = torch.ones((N, 1), device=self.device) * 0.9
# 5. 去重(距离阈值)
if self.means.shape[0] > 0:
dist = torch.cdist(new_means, self.means)
keep_mask = (dist.min(dim=1)[0] > 0.02)
new_means, new_colors, new_covs, new_opacities = \
new_means[keep_mask], new_colors[keep_mask], new_covs[keep_mask], new_opacities[keep_mask]
# 6. 合并到全局
self.means = torch.cat([self.means, new_means], dim=0)
self.colors = torch.cat([self.colors, new_colors], dim=0)
self.covs = torch.cat([self.covs, new_covs], dim=0)
self.opacities = torch.cat([self.opacities, new_opacities], dim=0)
# 7. 内存管理(限制总数)
if self.means.shape[0] > 500000:
self.prune_far_points()
def prune_far_points(self):
"""保留高不透明度点"""
keep_idx = torch.argsort(self.opacities.squeeze(), descending=True)[:500000]
self.means = self.means[keep_idx]
# ... (其他属性同步裁剪省略)
3D 可视化
import open3d as o3d
def visualize_3dgs(gs_model):
"""可视化当前 3DGS 点云"""
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(gs_model.means.cpu().numpy())
pcd.colors = o3d.utility.Vector3dVector(gs_model.colors.cpu().numpy())
# 添加坐标系
coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
o3d.visualization.draw_geometries([pcd, coordinate_frame])
实验
数据集说明
论文使用了三个基准数据集:
| 数据集 | 场景数 | 目标类别 | 数据格式 |
|---|---|---|---|
| HM3D | 800+ | 80 类日常物体 | RGB-D + 语义标注 |
| Gibson | 572 | 开放词汇 | RGB-D + Mesh |
| MP3D | 90 | 40 类家具 | RGB-D + 语义 |
数据获取:
- HM3D: 需申请 Habitat Challenge 权限
- Gibson: 官方下载
- 真实机器人实验:自采集数据(四足机器人 + RealSense D435)
定量评估
| 方法 | Success Rate (%) | SPL | Steps | 推理时间 (s) |
|---|---|---|---|---|
| 3DGSNav | 67.3 | 0.42 | 23.5 | 1.2 |
| L3MVN | 58.6 | 0.35 | 31.2 | 0.8 |
| CoWs | 51.2 | 0.28 | 38.7 | 1.5 |
| CLIP-Nav | 43.5 | 0.22 | 45.3 | 0.5 |
指标说明:
- Success Rate:在最大步数内找到目标的比例
- SPL:路径效率(Success weighted by Path Length)
- 推理时间:VLM 每步决策耗时(不含移动)
定性结果
成功案例:
- 任务:”找到床头柜上的闹钟”
- 步骤:
- 进入卧室 → 3DGS 记录床铺
- VLM 渲染床头柜侧面视角 → “看到小物体”
- 主动切换到正面视角 → 验证是闹钟
失败案例:
- 任务:”找到书架上的某本书”
- 失败原因:书脊文字太小,3DGS 渲染模糊(分辨率限制)
- 改进方向:局部高分辨率渲染
工程实践
实际部署考虑
1. 实时性分析
| 模块 | 耗时 (ms) | 优化方案 |
|---|---|---|
| 3DGS 更新 | 150 | GPU 加速 + 延迟更新 |
| 渲染候选视角 | 200 | 降低分辨率(320x240) |
| VLM 推理 | 1200 | 批量渲染,单次查询 |
| 总计 | 1550 | → 0.6 Hz 决策频率 |
优化策略:
- 延迟更新:每隔 5 帧更新一次 3DGS(减少 GPU 占用)
- 分辨率自适应:远距离用低分辨率,近距离用高分辨率
- 本地 VLM:部署 LLaVA-1.5(7B),推理降至 300ms
2. 硬件需求
最低配置:
- GPU: RTX 3060 (12GB)
- CPU: Intel i7 (4 核)
- 内存: 16GB
推荐配置:
- GPU: RTX 4090 (24GB) → 支持 100 万个高斯点
- CPU: Ryzen 9 (16 核)
- 内存: 32GB
3. 内存占用估算
每个高斯点占用: \(\text{Memory} = 3 \times 4 + 9 \times 4 + 3 \times 4 + 1 \times 4 = 64 \text{ bytes}\)
- 位置 $\boldsymbol{\mu}$: 12 bytes
- 协方差 $\Sigma$: 36 bytes(对称矩阵存 6 个值)
- 颜色 RGB: 12 bytes
- 不透明度: 4 bytes
大场景策略:
- 空间分块:只加载当前房间的高斯点(类似 LOD)
- 遗忘机制:超过 10 分钟未访问的区域压缩存储
数据采集建议
1. RGB-D 相机选择
| 型号 | 深度范围 | FPS | 适用场景 |
|---|---|---|---|
| RealSense D435 | 0.3-3m | 30 | 室内导航 |
| Kinect v2 | 0.5-4.5m | 30 | 大房间 |
| Ouster OS1 | 0.5-120m | 10 | 室外 |
推荐配置:D435 + IMU(用于位姿估计)
2. 采集时的注意事项
- 光照:避免强逆光(窗户、灯光直射)
- 速度:移动速度 < 0.5 m/s(避免运动模糊)
- 覆盖率:每个区域至少从 3 个角度观察
常见坑
1. 3DGS 飘移问题
现象:长时间导航后,3DGS 几何扭曲
原因:累积位姿误差 + 缺乏闭环检测
解决方案:
def detect_frontiers(occupancy_map, robot_pos):
"""检测未知-空闲边界,返回候选前沿方向"""
# 1. 形态学梯度找边界
kernel = np.ones((5, 5), np.uint8)
frontier = cv2.morphologyEx(
(occupancy_map == 0).astype(np.uint8),
cv2.MORPH_GRADIENT, kernel
) & (occupancy_map == 1)
frontier_points = np.argwhere(frontier > 0)
if len(frontier_points) == 0:
return []
# 2. 8方向评分
candidates = []
for angle in np.linspace(0, 2*np.pi, 8, endpoint=False):
direction = np.array([np.cos(angle), np.sin(angle)])
scores = np.dot(frontier_points - robot_pos, direction)
score = (scores > 0).sum()
candidates.append((angle, score))
return sorted(candidates, key=lambda x: x[1], reverse=True)[:3]
def generate_candidate_poses(robot_pose, frontier_angles, distance=1.0):
"""根据前沿角度生成候选位姿"""
poses = []
for angle, _ in frontier_angles:
T = robot_pose.copy()
T[:2, 3] += distance * np.array([np.cos(angle), np.sin(angle)])
T[:3, :3] = np.array([[np.cos(angle), -np.sin(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[0, 0, 1]])
poses.append(T)
return poses
2. VLM 幻觉
现象:VLM 声称”看到目标”,实际是误识别
解决方案:
def query_vlm(images, prompt, api_key):
"""调用 VLM API"""
import openai
openai.api_key = api_key
# 转换图像为 base64
image_urls = []
for img in images:
# ... (图像编码代码省略)
image_urls.append(f"data:image/png;base64,{img_str}")
# 构造消息
messages = [{
"role": "user",
"content": [
{"type": "text", "text": prompt},
*[{"type": "image_url", "image_url": {"url": url}} for url in image_urls]
]
}]
response = openai.ChatCompletion.create(model="gpt-4-vision-preview", messages=messages)
return response.choices[0].message.content
3. 内存爆炸
现象:探索 10 分钟后 GPU OOM
解决方案:
def navigate(target_object, gs_model, robot, vlm_api_key):
"""基于VLM和3DGS的主动导航"""
max_steps = 100
for step in range(max_steps):
# 1. 获取观察并更新3DGS
rgb, depth, K, T = robot.get_observation()
gs_model.add_frame(rgb, depth, K, T)
# 2. 检测前沿
frontiers = detect_frontiers(robot.get_occupancy_map(), robot.get_position())
if len(frontiers) == 0:
break
# 3. 渲染候选视角
candidate_poses = generate_candidate_poses(T, frontiers)
candidate_views = [render_novel_view(gs_model, K, pose) for pose in candidate_poses]
# 4. VLM推理
prompt = f"任务:找到 {target_object}\n当前视角+候选视角A/B/C,选择最可能看到目标的视角"
response = query_vlm([rgb] + candidate_views, prompt, vlm_api_key)
# 5. 执行决策
if "Found" in response:
return True
elif "A" in response:
robot.move_to(candidate_poses[0])
# ... (B/C分支类似)
return False
什么时候用 / 不用?
| 适用场景 | 不适用场景 |
|---|---|
| ✅ 静态室内环境 | ❌ 人流密集场景(遮挡多) |
| ✅ 需要精细识别(小物体) | ❌ 快速响应(< 1s 决策) |
| ✅ 有 RGB-D 传感器 | ❌ 纯视觉(无深度) |
| ✅ 长时间探索任务 | ❌ 一次性短任务(开销大) |
与其他方法对比
| 方法 | 场景表示 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 语义地图 | 2D 网格 | 轻量,规划快 | 丢失 3D 信息 | 简单环境 |
| NeRF | 隐式函数 | 渲染质量高 | 慢(分钟级) | 离线重建 |
| 3DGS (本文) | 显式高斯 | 实时 + 高质量 | 内存占用高 | 复杂室内 |
| Point-LLM | 点云 | 几何精确 | 外观信息少 | 工业场景 |
我的观点
技术亮点
- 3DGS 作为持久记忆 是个绝妙的点子:
- 相比语义地图,保留了完整的 3D 外观
- 相比 NeRF,渲染速度快 100 倍
- 让 VLM 能”回忆”和”预览”,这是人类导航的核心能力
- 主动视角切换 解决了单目视角的盲区问题:
- 传统方法只能”走到那里才能看”
- 3DGSNav 可以”想象走到那里会看到什么”
距离实用还有多远?
短期(1-2 年)可行的场景:
- 仓库机器人(环境静态,目标固定)
- 家庭服务机器人(限定房间数)
长期挑战:
- 动态环境:人/宠物走动时,3DGS 会包含”鬼影”
- 可能方向:结合动态 3DGS(加时间维度)
- 大规模场景:整栋楼的内存占用 > 100GB
- 可能方向:层次化 3DGS(类似 Octree)
- VLM 可靠性:目前 VLM 仍有 15% 的”幻觉率”
- 可能方向:多模态验证(触觉 + 视觉)
值得关注的开放问题
- 3DGS 的语义理解:能否直接在高斯点上做语义分割?
- 在线 SLAM + 3DGS:如何在移动中实时优化?
- VLM 的空间推理能力:训练数据中 3D 样本太少
个人预测:3DGS 会成为具身智能的”标准中间表示”,就像 Transformer 之于 NLP。