ChenmingWu commited on
Commit
861b46f
·
verified ·
1 Parent(s): ee2f935

Upload folder using huggingface_hub

Browse files
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