Upload folder using huggingface_hub
Browse files- mapvggt/__init__.py +3 -0
- mapvggt/crosscolor.py +57 -0
- mapvggt/heads.py +113 -0
- mapvggt/model.py +124 -0
- mapvggt/refine.py +56 -0
- mapvggt/uncertainty.py +49 -0
mapvggt/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mapvggt.model import MapVGGT, lift_to_world
|
| 2 |
+
|
| 3 |
+
__all__ = ["MapVGGT", "lift_to_world"]
|
mapvggt/crosscolor.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CrossColor: multi-view color fusion for per-pixel Gaussians.
|
| 2 |
+
|
| 3 |
+
Each Gaussian is born from one input pixel (view i) and currently carries that source
|
| 4 |
+
pixel's RGB (frozen). That single color is wrong for many target views (exposure/AWB
|
| 5 |
+
differences across the rig, grazing source angle) and causes seams where Gaussians from
|
| 6 |
+
different views overlap. CrossColor instead fuses the colors from ALL input views that
|
| 7 |
+
observe the Gaussian: reproject the Gaussian's world position into every input view,
|
| 8 |
+
sample, keep views where it is in-frustum AND depth-consistent (not occluded), and fuse.
|
| 9 |
+
|
| 10 |
+
Step-1 here is a PARAMETER-FREE fusion (visibility/occlusion-weighted, no learning) to
|
| 11 |
+
validate the hypothesis on an existing checkpoint before training a learned head.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def reproject_sample(means_w, img_j, K_j, c2w_j, z_j, occ_tol=0.10):
|
| 19 |
+
"""Project world points means_w [N,3] into view j; sample img_j color + visibility.
|
| 20 |
+
Returns color [N,3], visible [N] bool. Visible = in-frustum, in front, and the
|
| 21 |
+
point's camera-z agrees with view j's predicted depth at that pixel (occlusion test)."""
|
| 22 |
+
N = means_w.shape[0]
|
| 23 |
+
H, W = img_j.shape[-2:]
|
| 24 |
+
R = c2w_j[:3, :3]; t = c2w_j[:3, 3]
|
| 25 |
+
p_cam = (means_w - t) @ R # world->cam (R^T (x-t))
|
| 26 |
+
zc = p_cam[:, 2]
|
| 27 |
+
uv = p_cam[:, :2] / zc.clamp(min=1e-4).unsqueeze(-1)
|
| 28 |
+
u = K_j[0, 0] * uv[:, 0] + K_j[0, 2]
|
| 29 |
+
v = K_j[1, 1] * uv[:, 1] + K_j[1, 2]
|
| 30 |
+
inb = (zc > 1e-3) & (u >= 0) & (u <= W - 1) & (v >= 0) & (v <= H - 1)
|
| 31 |
+
# bilinear sample color + view-j depth at (u,v)
|
| 32 |
+
gx = (u / (W - 1)) * 2 - 1; gy = (v / (H - 1)) * 2 - 1
|
| 33 |
+
grid = torch.stack([gx, gy], dim=-1)[None, None] # [1,1,N,2]
|
| 34 |
+
col = F.grid_sample(img_j[None], grid, align_corners=True, mode="bilinear")[0, :, 0].t() # [N,3]
|
| 35 |
+
zj = F.grid_sample(z_j[None, None], grid, align_corners=True, mode="bilinear")[0, 0, 0] # [N]
|
| 36 |
+
consistent = (zc - zj).abs() <= occ_tol * zc.clamp(min=1e-3)
|
| 37 |
+
return col, inb & consistent
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def fuse_colors(means_w, imgs, K, c2w, z, source_view, source_color, occ_tol=0.10, source_boost=1.0):
|
| 41 |
+
"""means_w [N,3]; imgs [V,3,H,W]; K [V,3,3]; c2w [V,4,4]; z [V,H,W] per-view depth;
|
| 42 |
+
source_view [N] long; source_color [N,3] = the Gaussian's own (always-valid) source
|
| 43 |
+
pixel color. Seeds the fused color with the source color at weight source_boost (so a
|
| 44 |
+
Gaussian is never left colorless), then ADDS other views where in-frustum + depth-
|
| 45 |
+
consistent. Returns fused [N,3], n_extra_views [N] (how many OTHER views contributed)."""
|
| 46 |
+
V = imgs.shape[0]
|
| 47 |
+
N = means_w.shape[0]
|
| 48 |
+
acc = source_color * source_boost
|
| 49 |
+
wsum = torch.full((N,), float(source_boost), device=means_w.device)
|
| 50 |
+
extra = torch.zeros(N, device=means_w.device)
|
| 51 |
+
for j in range(V):
|
| 52 |
+
col, vis = reproject_sample(means_w, imgs[j], K[j], c2w[j], z[j], occ_tol)
|
| 53 |
+
vis = vis & (source_view != j) # source already seeded
|
| 54 |
+
w = vis.float()
|
| 55 |
+
acc += w.unsqueeze(-1) * col
|
| 56 |
+
wsum += w; extra += w
|
| 57 |
+
return acc / wsum.clamp(min=1e-6).unsqueeze(-1), extra
|
mapvggt/heads.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MapGS contribution heads for MapVGGT (VGGT-Omega backbone, frozen): external query
|
| 2 |
+
heads that realize full MapTokenGS parity without surgery on VGGT internals.
|
| 3 |
+
|
| 4 |
+
* MapAnchorHead -- contrib ① MAGT: HD-map-anchored Gaussian tokens whose centers are
|
| 5 |
+
bounded residuals mu = anchor + s(t)*tanh(Delta) around HD-map anchors, conditioned
|
| 6 |
+
on a pooled VGGT scene feature. Static, frame-independent.
|
| 7 |
+
* DynActorHead -- PointForward scene-graph dynamics: per-instance object-canonical
|
| 8 |
+
Gaussians placed into each target frame by tracked box poses, with the lifespan
|
| 9 |
+
opacity envelope. Frame-dependent (placed at render time).
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
from mapgs.model.blocks import fourier_encode
|
| 18 |
+
from mapgs.geometry.transforms import rotmat_to_quat, quat_multiply as quat_mul
|
| 19 |
+
|
| 20 |
+
N_ANCHOR_TYPES = 3 # ground / lane / boundary
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class MapAnchorHead(nn.Module):
|
| 24 |
+
"""anchors -> ng Gaussians each, centers = anchor + s(t)*tanh(Delta) (MAGT)."""
|
| 25 |
+
def __init__(self, c_ctx=2048, dim=256, ng=2, n_freq=10, pos_scale=50.0, base=0.5):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.ng, self.n_freq, self.pos_scale, self.base = ng, n_freq, pos_scale, base
|
| 28 |
+
pe_dim = 3 * (2 * n_freq + 1)
|
| 29 |
+
self.anchor_mlp = nn.Sequential(
|
| 30 |
+
nn.Linear(pe_dim + N_ANCHOR_TYPES + 3 + 1, dim), nn.GELU(), nn.Linear(dim, dim))
|
| 31 |
+
self.ctx_proj = nn.Linear(c_ctx, dim)
|
| 32 |
+
self.dec = nn.Linear(dim, ng * 14) # per gaussian: pos3,logscale3,rot4,op1,rgb3
|
| 33 |
+
nn.init.zeros_(self.dec.weight); nn.init.zeros_(self.dec.bias)
|
| 34 |
+
|
| 35 |
+
def forward(self, anchor_pos, anchor_type, anchor_normal, ctx, s_t):
|
| 36 |
+
"""anchor_pos [B,Na,3], type [B,Na], normal [B,Na,3], ctx [B,c_ctx]. Returns gaussian dict."""
|
| 37 |
+
B, Na, _ = anchor_pos.shape
|
| 38 |
+
pe = fourier_encode(anchor_pos / self.pos_scale, self.n_freq)
|
| 39 |
+
onehot = torch.zeros(B, Na, N_ANCHOR_TYPES, device=anchor_pos.device, dtype=pe.dtype)
|
| 40 |
+
onehot.scatter_(2, anchor_type.long().unsqueeze(-1).clamp(0, N_ANCHOR_TYPES - 1), 1.0)
|
| 41 |
+
feat = torch.cat([pe, onehot, anchor_normal, anchor_pos[..., 2:3]], dim=-1)
|
| 42 |
+
tok = self.anchor_mlp(feat) + self.ctx_proj(ctx)[:, None, :] # [B,Na,dim]
|
| 43 |
+
o = self.dec(tok).view(B, Na, self.ng, 14)
|
| 44 |
+
anc = anchor_pos[:, :, None, :] # [B,Na,1,3]
|
| 45 |
+
pos = anc + s_t * torch.tanh(o[..., 0:3]) # bounded residual (MAGT)
|
| 46 |
+
scale = self.base * torch.exp(o[..., 3:6].clamp(-3, 3))
|
| 47 |
+
quat = F.normalize(o[..., 6:10] + o.new_tensor([1., 0, 0, 0]), dim=-1)
|
| 48 |
+
opacity = torch.sigmoid(o[..., 10] - 6.0) # start ~off (sigmoid(-6)~0.002); sparsity-gated
|
| 49 |
+
rgb = torch.sigmoid(o[..., 11:14])
|
| 50 |
+
flat = lambda t, c: t.reshape(-1, c) if c > 1 else t.reshape(-1)
|
| 51 |
+
return dict(means=flat(pos, 3), scales=flat(scale, 3), quats=flat(quat, 4),
|
| 52 |
+
opacities=flat(opacity, 1), colors=flat(rgb, 3))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class DynActorHead(nn.Module):
|
| 56 |
+
"""Learned object-canonical Gaussians per instance slot, placed per frame by box poses."""
|
| 57 |
+
def __init__(self, c_ctx=2048, dim=256, max_inst=8, ng=32):
|
| 58 |
+
super().__init__()
|
| 59 |
+
self.max_inst, self.ng = max_inst, ng
|
| 60 |
+
self.query = nn.Parameter(torch.randn(max_inst, dim) * 0.02)
|
| 61 |
+
self.ctx_proj = nn.Linear(c_ctx, dim)
|
| 62 |
+
self.dec = nn.Linear(dim, ng * 14)
|
| 63 |
+
nn.init.zeros_(self.dec.weight); nn.init.zeros_(self.dec.bias)
|
| 64 |
+
|
| 65 |
+
def canonical(self, ctx):
|
| 66 |
+
"""Returns per-instance canonical gaussians (object-local unit cube). ctx [B,c_ctx]."""
|
| 67 |
+
tok = self.query + self.ctx_proj(ctx) # [max_inst,dim] (B=1)
|
| 68 |
+
o = self.dec(tok).view(self.max_inst, self.ng, 14)
|
| 69 |
+
return dict(
|
| 70 |
+
local=torch.tanh(o[..., 0:3]), # [-1,1] object cube
|
| 71 |
+
logscale=o[..., 3:6].clamp(-3, 3), rot=o[..., 6:10],
|
| 72 |
+
op=torch.sigmoid(o[..., 10] - 2.0), rgb=torch.sigmoid(o[..., 11:14]))
|
| 73 |
+
|
| 74 |
+
def place(self, canon, frame, box_c, box_R, canon_idx, box_valid, radius, base=0.5,
|
| 75 |
+
sigma=2.0, gain=1.0):
|
| 76 |
+
"""Place all instances' canonical gaussians into `frame`. Returns gaussian dict
|
| 77 |
+
(world frame) with lifespan-tempered opacity. Mirrors place_dynamics_14d."""
|
| 78 |
+
I = box_c.shape[0]
|
| 79 |
+
n = min(self.max_inst, I)
|
| 80 |
+
if n == 0:
|
| 81 |
+
z = torch.zeros(0, device=box_c.device)
|
| 82 |
+
return dict(means=z.view(0, 3), scales=z.view(0, 3), quats=z.view(0, 4),
|
| 83 |
+
opacities=z, colors=z.view(0, 3))
|
| 84 |
+
means, scales, quats, ops, rgbs = [], [], [], [], []
|
| 85 |
+
for i in range(n):
|
| 86 |
+
if not bool(box_valid[i, frame]):
|
| 87 |
+
continue
|
| 88 |
+
R = box_R[i, frame]; c = box_c[i, frame]
|
| 89 |
+
p_local = radius[i] * canon["local"][i] # [ng,3] metric object
|
| 90 |
+
p_world = torch.einsum("ij,nj->ni", R, p_local) + c
|
| 91 |
+
p_world = torch.nan_to_num(p_world).clamp(-200, 200)
|
| 92 |
+
q = quat_mul(rotmat_to_quat(R)[None].expand(self.ng, 4),
|
| 93 |
+
F.normalize(canon["rot"][i] + canon["rot"].new_tensor([1., 0, 0, 0]), dim=-1))
|
| 94 |
+
df = (float(frame) - float(canon_idx[i])) / (sigma + 1.0)
|
| 95 |
+
life = gain * torch.exp(torch.tensor(-0.5 * df * df, device=c.device))
|
| 96 |
+
means.append(p_world)
|
| 97 |
+
scales.append(base * torch.exp(canon["logscale"][i]))
|
| 98 |
+
quats.append(q); ops.append(canon["op"][i] * life); rgbs.append(canon["rgb"][i])
|
| 99 |
+
if not means:
|
| 100 |
+
z = torch.zeros(0, device=box_c.device)
|
| 101 |
+
return dict(means=z.view(0, 3), scales=z.view(0, 3), quats=z.view(0, 4),
|
| 102 |
+
opacities=z, colors=z.view(0, 3))
|
| 103 |
+
return dict(means=torch.cat(means), scales=torch.cat(scales), quats=torch.cat(quats),
|
| 104 |
+
opacities=torch.cat(ops), colors=torch.cat(rgbs))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def cat_gaussians(*dicts):
|
| 108 |
+
"""Union of gaussian dicts over the 5 render fields (skip empties; ignore extras like depth)."""
|
| 109 |
+
keys = ("means", "scales", "quats", "opacities", "colors")
|
| 110 |
+
ds = [d for d in dicts if d is not None and d["means"].shape[0] > 0]
|
| 111 |
+
if not ds:
|
| 112 |
+
return dicts[0]
|
| 113 |
+
return {k: torch.cat([d[k] for d in ds], dim=0) for k in keys}
|
mapvggt/model.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MapVGGT -- per-pixel feed-forward 3DGS warm-started from VGGT-Omega (1B-512),
|
| 2 |
+
replacing the TokenGS / DA-V2 backbones. VGGT-Omega is a multi-view ViT that predicts
|
| 3 |
+
per-pixel METRIC depth in each camera frame (no affine needed, unlike MapNuRec's DA-V2).
|
| 4 |
+
We feed the N context views together (cross-view attention), take its metric depth, and
|
| 5 |
+
attach a fresh per-pixel Gaussian head on [rgb, log-depth, depth-conf] -> opacity /
|
| 6 |
+
log-scale / rotation; color = source pixel RGB. Each pixel is lifted to a world-space
|
| 7 |
+
Gaussian using the KNOWN driving camera poses (we do not use VGGT's predicted poses),
|
| 8 |
+
and the union over views is rendered by gsplat. Map-grounding (map-depth / free-space /
|
| 9 |
+
extrap / vert losses) is applied in the trainer, exactly as in MapTokenGS / MapNuRec.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
# VGGT-Omega repo + base checkpoint locations are environment-overridable so the package
|
| 20 |
+
# works on any clone. Set VGGT_OMEGA_REPO (path to the facebookresearch/vggt-omega clone)
|
| 21 |
+
# and MAPVGGT_VGGT_CKPT (path to vggt_omega_1b_512.pt, obtained from its FAIR-licensed HF repo).
|
| 22 |
+
_VGGT_REPO = os.environ.get("VGGT_OMEGA_REPO", "/mnt/william/_vggt_omega_repo")
|
| 23 |
+
if _VGGT_REPO and _VGGT_REPO not in sys.path:
|
| 24 |
+
sys.path.insert(0, _VGGT_REPO)
|
| 25 |
+
VGGT_CKPT = os.environ.get("MAPVGGT_VGGT_CKPT", "/mnt/william/vggt_omega_ckpt/vggt_omega_1b_512.pt")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def lift_to_world(z, K, c2w):
|
| 29 |
+
"""z [V,H,W] metric depth (along optical axis) -> world points [V,H,W,3] (OpenCV +z fwd)."""
|
| 30 |
+
V, H, W = z.shape
|
| 31 |
+
dev = z.device
|
| 32 |
+
ys, xs = torch.meshgrid(torch.arange(H, device=dev), torch.arange(W, device=dev), indexing="ij")
|
| 33 |
+
u, v = (xs + 0.5).float(), (ys + 0.5).float()
|
| 34 |
+
fx, fy = K[:, 0, 0, None, None], K[:, 1, 1, None, None]
|
| 35 |
+
cx, cy = K[:, 0, 2, None, None], K[:, 1, 2, None, None]
|
| 36 |
+
x = (u - cx) / fx * z
|
| 37 |
+
y = (v - cy) / fy * z
|
| 38 |
+
p_cam = torch.stack([x, y, z], dim=-1)
|
| 39 |
+
R, t = c2w[:, :3, :3], c2w[:, :3, 3]
|
| 40 |
+
return torch.einsum("vij,vhwj->vhwi", R, p_cam) + t[:, None, None, :]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class MapVGGT(nn.Module):
|
| 44 |
+
def __init__(self, depth_min=1.5, depth_max=120.0, finetune_backbone=False, ckpt=VGGT_CKPT,
|
| 45 |
+
with_map=False, with_dyn=False, s_max=2.0):
|
| 46 |
+
super().__init__()
|
| 47 |
+
from vggt_omega.models import VGGTOmega
|
| 48 |
+
self.vggt = VGGTOmega()
|
| 49 |
+
sd = torch.load(ckpt, map_location="cpu")
|
| 50 |
+
self.vggt.load_state_dict(sd)
|
| 51 |
+
self.finetune_backbone = finetune_backbone
|
| 52 |
+
if not finetune_backbone:
|
| 53 |
+
self.vggt.eval()
|
| 54 |
+
for p in self.vggt.parameters():
|
| 55 |
+
p.requires_grad_(False)
|
| 56 |
+
self.dmin, self.dmax = depth_min, depth_max
|
| 57 |
+
# MapGS contribution heads (frozen-VGGT external query heads)
|
| 58 |
+
self.with_map, self.with_dyn = with_map, with_dyn
|
| 59 |
+
self.s_max = s_max; self.cur_s = s_max # tempering radius for MAGT residual
|
| 60 |
+
if with_map:
|
| 61 |
+
from mapvggt.heads import MapAnchorHead
|
| 62 |
+
self.map_head = MapAnchorHead(c_ctx=2 * 1024)
|
| 63 |
+
if with_dyn:
|
| 64 |
+
from mapvggt.heads import DynActorHead
|
| 65 |
+
self.dyn_head = DynActorHead(c_ctx=2 * 1024)
|
| 66 |
+
# fresh per-pixel head on [rgb(3), log-depth(1), conf(1)] -> opacity(1), log-scale(3), rot(4)
|
| 67 |
+
self.head = nn.Sequential(
|
| 68 |
+
nn.Conv2d(5, 64, 3, padding=1), nn.GELU(),
|
| 69 |
+
nn.Conv2d(64, 64, 3, padding=1), nn.GELU(),
|
| 70 |
+
nn.Conv2d(64, 9, 1))
|
| 71 |
+
nn.init.zeros_(self.head[-1].weight); nn.init.zeros_(self.head[-1].bias)
|
| 72 |
+
# [0]=opacity, [1:4]=log-scale (anisotropic), [4:8]=rot quat (wxyz), [8]=spare
|
| 73 |
+
self.head[-1].bias.data[0] = 2.0 # opacity logit -> sigmoid~0.88
|
| 74 |
+
self.head[-1].bias.data[4] = 1.0 # identity quaternion w=1
|
| 75 |
+
|
| 76 |
+
def _run_vggt(self, images):
|
| 77 |
+
"""-> depth [V,H,W], conf [V,H,W], ctx [2048] (pooled scene feature for the heads)."""
|
| 78 |
+
cm = torch.no_grad() if not self.finetune_backbone else torch.enable_grad()
|
| 79 |
+
with cm:
|
| 80 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 81 |
+
pred = self.vggt(images[None]) # auto-unsqueeze to [1,V,3,H,W]
|
| 82 |
+
depth = pred["depth"][0, ..., 0].float() # [V,H,W]
|
| 83 |
+
conf = pred.get("depth_conf")
|
| 84 |
+
conf = conf[0].float() if conf is not None else torch.ones_like(depth)
|
| 85 |
+
ctx = pred["camera_and_register_tokens"][0].float().mean(dim=(0, 1)) # [2048]
|
| 86 |
+
return depth.clamp(self.dmin, self.dmax), conf, ctx
|
| 87 |
+
|
| 88 |
+
def vggt_depth(self, images):
|
| 89 |
+
d, c, _ = self._run_vggt(images)
|
| 90 |
+
return d, c
|
| 91 |
+
|
| 92 |
+
def forward(self, images, K, c2w, anchor_pos=None, anchor_type=None, anchor_normal=None):
|
| 93 |
+
"""images [V,3,H,W] (0..1), K [V,3,3], c2w [V,4,4] -> gaussian dict (world frame).
|
| 94 |
+
If with_map and anchors given, unions the MAGT map-anchored gaussians. Stashes the
|
| 95 |
+
pooled ctx feature for the dynamic head (placed per-frame at render time)."""
|
| 96 |
+
V, _, H, W = images.shape
|
| 97 |
+
z, conf, ctx = self._run_vggt(images) # [V,H,W] metric, detached if frozen
|
| 98 |
+
self._ctx = ctx
|
| 99 |
+
logd = torch.log(z.clamp(self.dmin, self.dmax))
|
| 100 |
+
cn = (conf / conf.amax(dim=(-2, -1), keepdim=True).clamp_min(1e-6))
|
| 101 |
+
h = self.head(torch.cat([images, logd[:, None], cn[:, None]], dim=1)) # [V,9,H,W]
|
| 102 |
+
h = h.permute(0, 2, 3, 1) # [V,H,W,9]
|
| 103 |
+
opacity = torch.sigmoid(h[..., 0])
|
| 104 |
+
base = (z / K[:, 0, 0, None, None]).clamp(min=1e-4) # pixel footprint at depth
|
| 105 |
+
scale = base[..., None] * torch.exp(h[..., 1:4].clamp(-3, 3)) # [V,H,W,3] anisotropic
|
| 106 |
+
quat = F.normalize(h[..., 4:8] + torch.tensor([1.0, 0, 0, 0], device=images.device), dim=-1)
|
| 107 |
+
xyz = lift_to_world(z, K, c2w) # [V,H,W,3] world
|
| 108 |
+
rgb = images.permute(0, 2, 3, 1) # color = source pixel
|
| 109 |
+
static = dict(means=xyz.reshape(-1, 3), scales=scale.reshape(-1, 3),
|
| 110 |
+
quats=quat.reshape(-1, 4), opacities=opacity.reshape(-1),
|
| 111 |
+
colors=rgb.reshape(-1, 3), depth=z)
|
| 112 |
+
if self.with_map and anchor_pos is not None:
|
| 113 |
+
from mapvggt.heads import cat_gaussians
|
| 114 |
+
mg = self.map_head(anchor_pos, anchor_type, anchor_normal, ctx[None], self.cur_s)
|
| 115 |
+
self._map_opacity = mg["opacities"] # for the sparsity penalty (gate extras OFF by default)
|
| 116 |
+
u = cat_gaussians(static, mg)
|
| 117 |
+
u["depth"] = z # keep per-pixel depth for map-depth loss
|
| 118 |
+
return u
|
| 119 |
+
self._map_opacity = None
|
| 120 |
+
return static
|
| 121 |
+
|
| 122 |
+
def dyn_canonical(self):
|
| 123 |
+
"""Per-instance canonical dynamic gaussians from the stashed ctx (call after forward)."""
|
| 124 |
+
return self.dyn_head.canonical(self._ctx)
|
mapvggt/refine.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render-then-refine 2D UNet decoder (PointForward-style image-space refinement).
|
| 2 |
+
|
| 3 |
+
The splat renders RGB + depth + alpha; a small 2D UNet corrects splatting artifacts
|
| 4 |
+
(seams between per-view gaussians, sub-pixel misalignment, low-alpha haze, small holes)
|
| 5 |
+
that a direct-RGB splat bakes in permanently. Output is a residual on the rendered RGB,
|
| 6 |
+
zero-initialized so the model starts exactly at the un-refined baseline and only improves.
|
| 7 |
+
Trained end-to-end (gradients flow through gsplat to the gaussians/backbone too).
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _cbr(i, o):
|
| 16 |
+
return nn.Sequential(nn.Conv2d(i, o, 3, padding=1), nn.GroupNorm(8, o), nn.SiLU())
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class RefineUNet(nn.Module):
|
| 20 |
+
def __init__(self, in_ch=5, base=48):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.e1 = _cbr(in_ch, base)
|
| 23 |
+
self.e2 = _cbr(base, base * 2)
|
| 24 |
+
self.e3 = _cbr(base * 2, base * 4)
|
| 25 |
+
self.pool = nn.MaxPool2d(2)
|
| 26 |
+
self.bott = _cbr(base * 4, base * 4)
|
| 27 |
+
self.u3 = _cbr(base * 4 + base * 4, base * 2)
|
| 28 |
+
self.u2 = _cbr(base * 2 + base * 2, base)
|
| 29 |
+
self.u1 = _cbr(base + base, base)
|
| 30 |
+
self.out = nn.Conv2d(base, 3, 1)
|
| 31 |
+
nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias) # start == rendered RGB
|
| 32 |
+
|
| 33 |
+
def _up(self, x, ref):
|
| 34 |
+
return F.interpolate(x, size=ref.shape[-2:], mode="bilinear", align_corners=False)
|
| 35 |
+
|
| 36 |
+
def forward(self, rgb, depth, alpha):
|
| 37 |
+
"""rgb [S,3,H,W] in [0,1]; depth [S,H,W]; alpha [S,H,W]. Returns refined rgb [S,3,H,W]."""
|
| 38 |
+
H, W = rgb.shape[-2:]
|
| 39 |
+
dn = (depth.clamp(0, 100) / 50.0).unsqueeze(1) # normalized depth channel
|
| 40 |
+
a = alpha.clamp(0, 1).unsqueeze(1)
|
| 41 |
+
x = torch.cat([rgb, dn, a], dim=1) # [S,5,H,W]
|
| 42 |
+
# pad to a multiple of 8 (3 pooling stages) so any H,W works; crop back at the end
|
| 43 |
+
ph, pw = (8 - H % 8) % 8, (8 - W % 8) % 8
|
| 44 |
+
if ph or pw:
|
| 45 |
+
x = F.pad(x, (0, pw, 0, ph), mode="replicate")
|
| 46 |
+
rgb = F.pad(rgb, (0, pw, 0, ph), mode="replicate")
|
| 47 |
+
e1 = self.e1(x)
|
| 48 |
+
e2 = self.e2(self.pool(e1))
|
| 49 |
+
e3 = self.e3(self.pool(e2))
|
| 50 |
+
b = self.bott(self.pool(e3))
|
| 51 |
+
d3 = self.u3(torch.cat([self._up(b, e3), e3], 1))
|
| 52 |
+
d2 = self.u2(torch.cat([self._up(d3, e2), e2], 1))
|
| 53 |
+
d1 = self.u1(torch.cat([self._up(d2, e1), e1], 1))
|
| 54 |
+
res = self.out(d1)
|
| 55 |
+
out = (rgb + res).clamp(0, 1)
|
| 56 |
+
return out[..., :H, :W]
|
mapvggt/uncertainty.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Idea B -- Uncertainty-conditioned Gaussians.
|
| 2 |
+
|
| 3 |
+
VGGT-Omega emits a per-pixel depth confidence. Depth uncertainty is a 1-D uncertainty
|
| 4 |
+
ALONG THE VIEWING RAY. We shape each per-pixel Gaussian's covariance accordingly:
|
| 5 |
+
orient one axis along the world-space ray from the source camera and set its extent to
|
| 6 |
+
the depth standard deviation sigma_z (large for far / low-confidence pixels), while the
|
| 7 |
+
two perpendicular axes keep the pixel footprint. An uncertain far Gaussian thus renders
|
| 8 |
+
as a soft blur over the depth range it could occupy, instead of a hard point at a wrong
|
| 9 |
+
depth (a ghost). Confident pixels -> sigma_z~0 -> ordinary near-isotropic splat.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
|
| 15 |
+
from mapgs.geometry.transforms import rotmat_to_quat
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def ray_frame_quat(ray_dir):
|
| 19 |
+
"""Per-row rotation (as wxyz quat) whose LOCAL Z axis = ray_dir [N,3] (unit)."""
|
| 20 |
+
z = F.normalize(ray_dir, dim=-1)
|
| 21 |
+
up = torch.tensor([0.0, 0.0, 1.0], device=z.device).expand_as(z)
|
| 22 |
+
# if ray ~ parallel to world-up, switch reference to avoid degenerate cross product
|
| 23 |
+
deg = (z * up).sum(-1).abs() > 0.99
|
| 24 |
+
up = torch.where(deg.unsqueeze(-1), torch.tensor([0.0, 1.0, 0.0], device=z.device), up)
|
| 25 |
+
x = F.normalize(torch.cross(up, z, dim=-1), dim=-1)
|
| 26 |
+
y = torch.cross(z, x, dim=-1)
|
| 27 |
+
R = torch.stack([x, y, z], dim=-1) # columns = local axes in world (local->world)
|
| 28 |
+
return rotmat_to_quat(R) # [N,4] wxyz
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def uncertainty_to_sigma(z, conf, gamma, conf_ref=None):
|
| 32 |
+
"""z [N] metric depth, conf [N] VGGT confidence (>=1, higher=more certain).
|
| 33 |
+
Returns along-ray sigma_z [N] in meters. u in [0,1] = relative uncertainty."""
|
| 34 |
+
cref = conf_ref if conf_ref is not None else conf.amax().clamp(min=1e-3)
|
| 35 |
+
u = (1.0 - conf / cref).clamp(0.0, 1.0) # 0 at most-confident pixel, ->1 uncertain
|
| 36 |
+
return gamma * z * u
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def ray_uncertainty_cov(means, z, conf, cam_centers, base_perp, gamma, conf_ref=None,
|
| 40 |
+
sigma_max_frac=0.5):
|
| 41 |
+
"""means [N,3] world centers; z [N]; conf [N]; cam_centers [N,3] source camera centers;
|
| 42 |
+
base_perp [N] perpendicular (footprint) scale. Returns scales [N,3], quats [N,4]:
|
| 43 |
+
covariance elongated along the ray by sigma_z (capped at sigma_max_frac*z)."""
|
| 44 |
+
ray = means - cam_centers
|
| 45 |
+
quat = ray_frame_quat(ray)
|
| 46 |
+
sig = uncertainty_to_sigma(z, conf, gamma, conf_ref).clamp(max=sigma_max_frac * z)
|
| 47 |
+
s_along = base_perp + sig
|
| 48 |
+
scales = torch.stack([base_perp, base_perp, s_along], dim=-1)
|
| 49 |
+
return scales, quat
|