AnyRecon:从任意稀疏视角重建大规模 3D 场景
一句话总结
AnyRecon 用视频扩散模型将稀疏、无序的多视角图像重建为完整 3D 场景——核心突破是把「生成」与「重建」深度耦合:用显式 3D 几何记忆指导生成,用生成结果反哺重建。
为什么这个问题重要?
随手拍几张照片就能重建出完整 3D 场景,是机器人、AR/VR、数字孪生共同追求的能力。但这件事比听起来难得多。
现有方法的困境:
- NeRF / 3DGS:需要密集视角(几十到几百张),稀疏情况下严重退化
- Zero123、One-2-3-45:基于扩散生成,但只能条件化 1-2 帧,大场景泛化差
- CAT3D、ReconX:改进了多帧条件化,但对视角数量和顺序仍有较强假设
AnyRecon 的核心创新:支持任意数量、任意顺序的稀疏输入,同时保证几何一致性——通过「持久化场景记忆 + 几何感知检索 + 高效稀疏注意力」三件套解决这个问题。
论文链接:AnyRecon: Arbitrary-View 3D Reconstruction with Video Diffusion Model
背景知识
稀疏视角重建的三条路
纯重建路线:NeRF/3DGS ──── 需要密集输入,稀疏时退化严重
生成先验路线:Zero123 ───── 单图→多视角,但帧间几何不一致
生成+重建耦合:AnyRecon ─── 两者互相约束,本文方向
时序压缩:视频扩散用于 3D 的核心障碍
视频扩散模型(如 SVD)把时序帧之间的一致性建模得很好。如果把「时序帧」换成「多视角渲染帧」,模型自然能学到视角间的几何一致性——这是用视频扩散做 3D 的核心动机。
但标准视频模型为了省内存,会在时间维度下采样(如把 16 帧压缩成 4 个 latent 帧)。对自然视频无妨,但对 3D 重建来说,这破坏了像素级帧间对应关系,直接导致几何错位。AnyRecon 的关键修改之一就是移除这个压缩。
核心方法
直觉解释
想象你在博物馆随手拍了 6 张展品照片,角度各异、顺序随机。AnyRecon 的推理流程如下:
稀疏输入(6张) ──→ 建立持久化场景记忆(所有视角入库)
↓
几何感知检索(目标视角 → 找最相关的 k 个条件帧)
↓
无时序压缩的视频扩散(生成新视角)
↓
3D 重建(NeRF/3DGS 拟合生成结果)
三个核心创新
创新 1:持久化捕获视角缓存(Capture View Cache)
所有捕获视角存入全局记忆,在注意力机制中被「前置」为 prefix token,生成任何新视角时都可访问——不再受限于「只能看最近 2 帧」。
创新 2:去除时序压缩
条件帧(捕获视角)不经过时序 VAE 压缩,以帧级别分辨率直接参与注意力计算,保证像素级几何对应。
创新 3:几何感知视角检索
不用全部缓存视角(太多,放不进上下文窗口),而是根据 3D 几何重叠度动态检索最相关的 $k$ 个。
数学细节
设捕获视角集合为 $\mathcal{C} = {(I_i, \mathbf{P}i)}{i=1}^N$,$\mathbf{P}_i \in \mathbb{R}^{4 \times 4}$ 是相机位姿矩阵。
几何相关性得分(用于视角检索):
\[s(q, c) = \lambda_d \cdot \exp\!\left(-\frac{\|\mathbf{t}_q - \mathbf{t}_c\|_2}{\sigma}\right) + \lambda_\theta \cdot \frac{\mathbf{d}_q \cdot \mathbf{d}_c + 1}{2}\]其中 $\mathbf{t}$ 为相机位置,$\mathbf{d}$ 为相机朝向(Z 轴方向),两项分别衡量空间距离和朝向相似度。
稀疏注意力复杂度(局部窗口 + 全局条件帧):
\[\mathcal{O}\left(N_c \cdot T + T \cdot W\right) \quad \text{vs} \quad \mathcal{O}(T^2) \text{(全注意力)}\]其中 $N_c$ 是条件帧数,$T$ 是生成帧数,$W$ 是窗口大小。条件帧全局可见,生成帧只看局部窗口。
4 步蒸馏:通过一致性蒸馏将扩散步数从 50 步降至 4 步,推理提速约 10×。
实现
持久化场景记忆
import torch
import numpy as np
class CaptureViewCache:
"""持久化全局场景记忆,存储所有捕获视角"""
def __init__(self, max_views=64, intrinsics=None):
self.views = []
self.max_views = max_views
# (fx, fy, cx, cy)
self.K = intrinsics or (500.0, 500.0, 320.0, 240.0)
def add_view(self, image: torch.Tensor, pose: torch.Tensor,
depth: torch.Tensor = None):
if len(self.views) >= self.max_views:
self._evict_redundant()
entry = {'image': image, 'pose': pose}
if depth is not None:
entry['points'] = self._unproject(pose, depth)
self.views.append(entry)
def _unproject(self, pose: torch.Tensor, depth: torch.Tensor):
"""深度图反投影到世界坐标系点云"""
H, W = depth.shape
fx, fy, cx, cy = self.K
u = torch.arange(W, dtype=torch.float32)
v = torch.arange(H, dtype=torch.float32)
uu, vv = torch.meshgrid(u, v, indexing='xy') # (H, W) 各自
x_cam = (uu - cx) * depth / fx
y_cam = (vv - cy) * depth / fy
# 齐次坐标拼接,变换到世界系
pts_cam = torch.stack([x_cam, y_cam, depth,
torch.ones_like(depth)], dim=-1) # (H,W,4)
pts_world = (pose @ pts_cam.reshape(-1, 4).T).T # (H*W, 4)
return pts_world[:, :3].reshape(H, W, 3)
def _evict_redundant(self):
"""简化策略:移除最旧的视角(生产中应换为几何感知淘汰)"""
self.views.pop(0)
def __len__(self):
return len(self.views)
几何感知视角检索
def retrieve_views(query_pose: torch.Tensor,
cache: CaptureViewCache,
k: int = 4,
w_dist: float = 0.5,
sigma: float = 2.0) -> list:
"""
为目标视角检索最相关的 k 个条件帧。
核心假设:位置近 + 朝向相似 ≈ 3D 重叠度高 ≈ 更好的几何约束。
"""
t_q = query_pose[:3, 3] # 查询相机位置
d_q = query_pose[:3, 2] # 查询相机朝向(Z轴)
scored = []
for idx, view in enumerate(cache.views):
t_c = view['pose'][:3, 3]
d_c = view['pose'][:3, 2]
# 空间距离得分(高斯衰减)
s_dist = torch.exp(-torch.norm(t_q - t_c) / sigma)
# 朝向相似度(余弦相似度归一化到 [0, 1])
s_angle = (d_q @ d_c + 1.0) / 2.0
score = w_dist * s_dist + (1.0 - w_dist) * s_angle
scored.append((score.item(), idx))
scored.sort(reverse=True)
return [cache.views[idx] for _, idx in scored[:k]]
无时序压缩的稀疏注意力
import torch
class CaptureViewCache:
"""持久化全局场景记忆,存储所有捕获视角"""
def __init__(self, max_views=64, intrinsics=None):
self.views = []
self.max_views = max_views
self.K = intrinsics or (500.0, 500.0, 320.0, 240.0) # (fx, fy, cx, cy)
def add_view(self, image: torch.Tensor, pose: torch.Tensor, depth: torch.Tensor = None):
if len(self.views) >= self.max_views:
self.views.pop(0) # 简化淘汰策略
entry = {'image': image, 'pose': pose}
if depth is not None:
entry['points'] = self._unproject(pose, depth)
self.views.append(entry)
def _unproject(self, pose: torch.Tensor, depth: torch.Tensor):
"""深度图反投影到世界坐标系点云"""
H, W = depth.shape
fx, fy, cx, cy = self.K
# ... (像素网格生成代码省略)
pts_cam = torch.stack([...], dim=-1) # (H,W,4) 齐次坐标
pts_world = (pose @ pts_cam.reshape(-1, 4).T).T # 变换到世界系
return pts_world[:, :3].reshape(H, W, 3)
端到端 Demo
def demo_anyrecon_pipeline():
"""模拟 AnyRecon 核心推理流程(用随机张量替代真实图像)"""
torch.manual_seed(42)
dim, B = 256, 1
# 1. 构建场景记忆:6 张环绕拍摄的稀疏视角
cache = CaptureViewCache(max_views=32)
for i in range(6):
angle = i * np.pi / 3 # 每60°一张
pose = torch.eye(4)
pose[0, 3] = np.cos(angle) * 2 # 绕圆心半径2m
pose[1, 3] = np.sin(angle) * 2
pose[2, 3] = 0.5
cache.add_view(
image=torch.randn(3, 240, 320),
pose=pose,
depth=torch.rand(240, 320) * 3 + 0.5
)
print(f"场景记忆:共 {len(cache)} 个捕获视角")
# 2. 目标视角:45° 方向(介于已有视角之间)
query_pose = torch.eye(4)
query_pose[0, 3] = np.cos(np.pi / 4) * 2
query_pose[1, 3] = np.sin(np.pi / 4) * 2
# 3. 几何感知检索
relevant = retrieve_views(query_pose, cache, k=4)
print(f"检索到 {len(relevant)} 个相关条件帧")
# 4. 稀疏注意力推理
sparse_attn = GeometryAwareSparseAttention(dim=dim)
gen_frames = torch.randn(B, 8, dim) # 8 个目标帧
cond_frames = torch.randn(B, 4, dim) # 4 个条件帧
output = sparse_attn(gen_frames, cond_frames)
print(f"输出形状: {output.shape}") # → (1, 8, 256)
return output
demo_anyrecon_pipeline()
3D 可视化:视角分布与检索结果
class GeometryAwareSparseAttention(nn.Module):
# 条件帧全局可见;生成帧仅关注局部窗口 ±W/2,复杂度 O(T·W)
def __init__(self, dim, num_heads=8, window_size=8):
super().__init__()
self.W = window_size
self.attn = nn.MultiheadAttention(dim, num_heads, batch_first=True)
def forward(self, gen_frames, cond_frames):
# gen_frames: (B, T, D) cond_frames: (B, N, D)
outputs = []
for t in range(gen_frames.shape[1]):
local = gen_frames[:, max(0, t-self.W//2):min(gen_frames.shape[1], t+self.W//2+1), :]
kv = torch.cat([cond_frames, local], dim=1) # 条件帧全局可见 → 跨距离几何约束
out, _ = self.attn(gen_frames[:, t:t+1, :], kv, kv)
outputs.append(out)
return torch.cat(outputs, dim=1) # (B, T, D)
预期效果:6 个蓝色箭头呈圆形排布,系统检索命中的 4 个最近视角变为红色,绿色星号标记目标生成位置。这张图是调试 AnyRecon pipeline 时最实用的工具。
实验
数据集说明
| 数据集 | 场景规模 | 视角数 | 重点测试 |
|---|---|---|---|
| RealEstate10K | 大型室内 | 100+ | 长轨迹泛化 |
| CO3D | 单物体 | 50-100 | 多类别重建 |
| DTU | 小型物体 | 49/64 | 定量基准 |
| Tanks & Temples | 大型户外 | 数百 | 场景级别 |
RealEstate10K 是核心训练集,覆盖长轨迹和大视差变化,正好对应「任意视角」的泛化需求。
定量评估(论文报告的对比结果)
| 方法 | PSNR ↑ | SSIM ↑ | LPIPS ↓ | 最大条件帧数 |
|---|---|---|---|---|
| Zero123++ | 19.8 | 0.71 | 0.28 | 1 |
| CAT3D | 22.1 | 0.76 | 0.22 | 3 |
| ReconX | 23.0 | 0.78 | 0.20 | 2 |
| AnyRecon | 24.6 | 0.82 | 0.17 | 任意 |
关键点:其他方法受固定条件帧数限制,增加输入无法带来质量提升;AnyRecon 随输入视角增加,指标持续改善。
工程实践
几何感知视角淘汰(替换简单 LRU)
大场景下缓存不能无限增长,但朴素 LRU 会丢弃覆盖远处区域的早期视角。
def demo_anyrecon_pipeline():
torch.manual_seed(42)
dim, B = 256, 1
# 1. 构建场景记忆:6 张环绕稀疏视角
cache = CaptureViewCache(max_views=32)
for i in range(6):
angle = i * np.pi / 3
pose = torch.eye(4)
pose[0, 3], pose[1, 3] = np.cos(angle) * 2, np.sin(angle) * 2
cache.add_view(image=torch.randn(3, 240, 320), pose=pose, depth=torch.rand(240, 320) * 3 + 0.5)
# 2. 目标视角(45°,介于已有视角之间)
query_pose = torch.eye(4)
query_pose[0, 3], query_pose[1, 3] = np.cos(np.pi / 4) * 2, np.sin(np.pi / 4) * 2
# 3. 几何感知检索
relevant = retrieve_views(query_pose, cache, k=4)
# 4. 稀疏注意力推理
sparse_attn = GeometryAwareSparseAttention(dim=dim)
output = sparse_attn(torch.randn(B, 8, dim), torch.randn(B, 4, dim)) # (生成帧, 条件帧)
return output # → (1, 8, 256)
无序输入的预排序
AnyRecon 支持无序输入,但贪心地按最大覆盖度排序后,几何记忆构建更稳定:
def sort_by_coverage(views: list) -> list:
"""贪心排序:每次选与已选视角集合平均距离最大的下一帧"""
if not views:
return views
sorted_v, remaining = [views[0]], views[1:]
while remaining:
last_pos = sorted_v[-1]['pose'][:3, 3]
dists = [torch.norm(v['pose'][:3, 3] - last_pos).item()
for v in remaining]
best = int(np.argmax(dists))
sorted_v.append(remaining.pop(best))
return sorted_v
常见坑
坑 1:相机坐标系不统一
COLMAP 输出 OpenCV 约定(Z 轴朝前),OpenGL(Z 轴朝后),两者混用会让整个几何记忆乱掉:
# OpenGL 位姿 → OpenCV 位姿(翻转 Y 和 Z 轴)
def opengl_to_opencv(pose: torch.Tensor) -> torch.Tensor:
flip = torch.diag(torch.tensor([1., -1., -1., 1.]))
return pose @ flip
坑 2:稀疏视角下单目深度不可靠
几何感知检索依赖 3D 几何记忆,但稀疏输入时单目深度误差大(尤其是无纹理区域)。优先用多视角三角化的稀疏点云;实在不行,降低 $\lambda_\theta$ 权重,回退到纯空间距离检索。
坑 3:4 步蒸馏在大视角跨度下失效
视角变化超过 120° 时,4 步蒸馏模型偶尔产生几何伪影。此时需回退到完整 DDIM 采样(50 步),速度换质量,是个实际部署中需要根据场景调参的超参数。
什么时候用 / 不用?
| 适用场景 | 不适用场景 |
|---|---|
| 随手拍的稀疏照片(5-20 张) | 需要实时重建(推理较慢) |
| 静态场景、光照稳定 | 动态物体(人、树叶) |
| 大视角跨度的宽基线采集 | 极端光照变化(高反光、HDR) |
| 室内 / 小型户外场景 | 超大规模城市级场景 |
| 核心需求是新视角合成 | 只需要网格(用 MVS 更快更准) |
与其他方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| NeRF / 3DGS | 几何精确,无幻觉 | 需密集输入,无泛化 | 受控密集采集 |
| Zero123++ | 单图驱动,泛化强 | 几何不一致,细节差 | 单物体快速草图 |
| CAT3D | 条件帧数固定,速度快 | 不支持任意输入 | 固定配置规范采集 |
| AnyRecon | 任意视角数和顺序,可扩展 | 推理慢,依赖位姿估计 | 野外稀疏随意拍摄 |
我的观点
AnyRecon 解决的问题方向是正确的:世界上大多数照片都是稀疏、无序、随意拍的,让重建系统适应真实的拍摄习惯,而不是强迫用户按规范采集,这才是走向实用化的路。
三个技术点里,「几何与生成的双向耦合」是最有意思的洞察。以前的方法要么纯生成(不管几何),要么纯重建(不借助生成先验),两者分离。AnyRecon 让生成结果反哺几何记忆、几何记忆再指导生成,形成正反馈——这个设计思路值得借鉴。
离实际应用还有多远?
主要障碍是速度。即使 4 步蒸馏,大场景推理时间仍以分钟计,无法满足实时 AR/VR。另一个门槛是位姿依赖——方法假设已知精确相机位姿,但「随手拍」的照片还得先跑 COLMAP,这在移动端是实实在在的工程负担。
值得关注的开放问题:
- 动态场景:场景记忆假设静态,运动物体会在生成结果中留下幻影
- 端到端位姿估计:把相机位姿估计融进 pipeline,才能做到真正「拍了就用」
- 更激进的蒸馏:1-2 步扩散是否可行?一致性模型(Consistency Models)的进展值得跟踪
这个方向的未来形态可能是:手机 App 随手拍几张,30 秒内在云端生成完整 3D 资产。AnyRecon 是朝这个方向走出的扎实一步,但还有相当长的路要走。
Comments