Spaces:
Running on Zero
Running on Zero
Download src/model/stage2.py from hugging-apps/finesplat: direct link, hf CLI and curl.
- Browser
- Download file 43.2 kB
-
https://huggingface.co/spaces/hugging-apps/finesplat/resolve/main/src/model/stage2.py
- Command line
-
hf download hf://spaces/hugging-apps/finesplat/src/model/stage2.py
-
curl -L -o stage2.py https://huggingface.co/spaces/hugging-apps/finesplat/resolve/main/src/model/stage2.py
43.2 kB
| """Stage 2 of FineSplat: error-conditioned correction of the Stage-1 tokens. | |
| Stage 1 (2Xplat) predicts one token per 4x4 patch, decoded into 4 Gaussians. Each Stage-2 | |
| step renders the current Gaussians into the context views, builds a per-view error map, | |
| lifts it onto the tokens by projection and visibility pooling, runs PTv3 serialized-attention | |
| blocks over the token cloud, and predicts a token residual the frozen Stage-1 head decodes. | |
| python -m src.model.stage2 # self-check (needs a GPU) | |
| """ | |
| import contextlib | |
| import torch | |
| import torch.nn.functional as F | |
| import torch.utils.checkpoint | |
| from einops import rearrange | |
| from torch import nn | |
| from src.model.gaussians import GaussianRenderer | |
| from src.model.ptv3.ptv3 import Block, SerializedAttention | |
| from src.model.ptv3.structure import Point | |
| from src.model.sh_eval import _spherical_harmonics | |
| from src.utils.camera_utils import fxfycxcy_to_K | |
| EPS = 1e-6 | |
| # Cap on elements materialised by one sparse-gather chunk in ErrorLifter (~512 MB in fp32). | |
| _GATHER_CHUNK_ELEMS = 1 << 27 | |
| IMAGENET_MEAN = (0.485, 0.456, 0.406) | |
| IMAGENET_STD = (0.229, 0.224, 0.225) | |
| default_conf = { | |
| # "token": one point per patch token, residual on the token (decoded by the Stage-1 head). | |
| # "gaussian": one point per Gaussian, residual on its raw pre-activation parameters. | |
| "mode": "token", | |
| # Stage-2 steps per forward; each re-renders the context views from the previous step. | |
| "num_steps": 4, | |
| "network": { | |
| "width": 512, | |
| "depth": 4, | |
| "attn_channels": 256, # q/k/v width (num_heads x 16); == width gives plain PTv3 attention | |
| "num_heads": 16, | |
| "patch_size": 1024, # window = min(points per scene, patch_size) | |
| "order": ["z", "z-trans", "hilbert", "hilbert-trans"], | |
| "drop_path": 0.1, | |
| "grid_resolution": 384, # serialization + xCPE voxel grid (SplatFormer's value) | |
| }, | |
| "error": { | |
| "stride": 4, # maps at 56x56 for 224x224 inputs | |
| "width": 128, # per-view map = [error 128 | image 128] | |
| "lift_channels": 256, | |
| "occlusion_tol": 0.15, | |
| "reduce": "mean", # how visible views are pooled per token: mean | max | |
| "lift": "mean+home", # what reaches the token: mean+home | mean | home | |
| "use_rgb_diff": True, | |
| "use_image_embed": True, | |
| "backbone": "resnet18", # frozen error feature extractor: resnet18 | vgg16_lpips | |
| }, | |
| } | |
| def _sample(img: torch.Tensor, pix: torch.Tensor, size: int) -> torch.Tensor: | |
| """Bilinear sample img [N,C,h,w] at pixel coords pix [N,M,2] -> [N,M,C]; centres at i+0.5 (align_corners=False).""" | |
| grid = (pix / size * 2 - 1).unsqueeze(2) | |
| out = F.grid_sample(img, grid.to(img.dtype), mode="bilinear", padding_mode="border", align_corners=False) | |
| return out[..., 0].transpose(1, 2) # [N,M,C] | |
| def _sample_sparse(imgs: torch.Tensor, pix: torch.Tensor, img_idx: torch.Tensor, | |
| size: int) -> torch.Tensor: | |
| """Gather form of ``_sample``: imgs [N,C,h,w], pix [P,2] from image img_idx[p] -> [P,C].""" | |
| n, c, h, w = imgs.shape | |
| xs = (pix[:, 0] * (w / size) - 0.5).clamp(0, w - 1) | |
| ys = (pix[:, 1] * (h / size) - 0.5).clamp(0, h - 1) | |
| x0, y0 = xs.floor(), ys.floor() | |
| wx, wy = (xs - x0).to(imgs.dtype)[:, None], (ys - y0).to(imgs.dtype)[:, None] | |
| x0, y0 = x0.long(), y0.long() | |
| x1, y1 = (x0 + 1).clamp(max=w - 1), (y0 + 1).clamp(max=h - 1) | |
| flat = imgs.reshape(n, c, h * w) | |
| # advanced indices at dim 0 and 2 with a slice between them -> result is [P, C] | |
| top = flat[img_idx, :, y0 * w + x0] * (1 - wx) + flat[img_idx, :, y0 * w + x1] * wx | |
| bot = flat[img_idx, :, y1 * w + x0] * (1 - wx) + flat[img_idx, :, y1 * w + x1] * wx | |
| return top * (1 - wy) + bot * wy | |
| class ErrorMapEncoder(nn.Module): | |
| """Per-view error map at 1/stride resolution: [error `width` | image `img_width`] channels.""" | |
| def __init__(self, image_size: int, stride: int, width: int, | |
| use_rgb_diff: bool = True, use_image_embed: bool = True, | |
| backbone: str = "resnet18", use_feat_diff: bool = True, | |
| image_embed: str = "gt_rgb", mvp_dim: int = 256, | |
| img_width: int | None = None): | |
| super().__init__() | |
| assert backbone in ("resnet18", "vgg16_lpips"), backbone | |
| assert image_embed in ("gt_rgb", "mvp_s1"), image_embed | |
| self.backbone = backbone | |
| self.use_feat_diff = use_feat_diff | |
| self.image_embed = image_embed if use_image_embed else None | |
| self.use_err = use_feat_diff or use_rgb_diff | |
| assert self.use_err or use_image_embed, "no error branches and no image embed: empty map" | |
| if not use_feat_diff: | |
| self.stem = self.rest = self.layer2 = None | |
| self._stages = [] | |
| elif backbone == "resnet18": | |
| from torchvision.models import ResNet18_Weights, resnet18 | |
| # checkpoints store the frozen backbone under error.stem/rest/layer2 - do not rename | |
| r = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1) | |
| self.stem = nn.Sequential(r.conv1) | |
| self.rest = nn.Sequential(r.bn1, r.relu, r.maxpool, r.layer1) | |
| self.layer2 = r.layer2 | |
| self._stages = [self.stem, self.rest, self.layer2] | |
| feat_channels = 64 + 64 + 128 | |
| else: | |
| from torchvision.models import VGG16_Weights, vgg16 | |
| vf = vgg16(weights=VGG16_Weights.IMAGENET1K_V1).features | |
| self.slice1, self.slice2, self.slice3 = vf[:4], vf[4:9], vf[9:16] | |
| self._stages = [self.slice1, self.slice2, self.slice3] | |
| feat_channels = 64 + 128 + 256 | |
| for m in self._stages: | |
| m.requires_grad_(False) | |
| self.register_buffer("mean", torch.tensor(IMAGENET_MEAN).view(1, 3, 1, 1), persistent=False) | |
| self.register_buffer("std", torch.tensor(IMAGENET_STD).view(1, 3, 1, 1), persistent=False) | |
| assert image_size % stride == 0, (image_size, stride) | |
| self.stride = stride | |
| self.grid_hw = (image_size // stride, image_size // stride) | |
| img_width = width if img_width is None else img_width | |
| self.channels = (width if self.use_err else 0) + (img_width if use_image_embed else 0) | |
| self.feat_proj = nn.Linear(feat_channels, width) if use_feat_diff else None | |
| self.rgb_proj = nn.Linear(3 * stride * stride, width) if use_rgb_diff else None | |
| self.err_norm = nn.LayerNorm(width) if self.use_err else None | |
| # "gt_rgb": pixel-unshuffled context image; "mvp_s1": Stage 1's own patch tokens for the view | |
| img_in = {"gt_rgb": 3 * stride * stride, "mvp_s1": mvp_dim}.get(self.image_embed) | |
| self.img_proj = ( | |
| nn.Sequential(nn.Linear(img_in, img_width), nn.LayerNorm(img_width)) | |
| if self.image_embed is not None else None | |
| ) | |
| def train(self, mode: bool = True): | |
| super().train(mode) | |
| for m in self._stages: | |
| m.eval() # frozen backbone: keep BatchNorm running stats fixed | |
| return self | |
| def features(self, img: torch.Tensor) -> torch.Tensor | None: | |
| """img [N,3,H,W] in [0,1] -> [N,C,gh,gw] (C = 256 resnet / 448 vgg).""" | |
| if not self.use_feat_diff: | |
| return None | |
| x = (img.float() - self.mean) / self.std | |
| feats = [] | |
| for m in self._stages: | |
| x = m(x) | |
| feats.append(x) | |
| if self.backbone == "vgg16_lpips": | |
| feats = [f / f.pow(2).sum(dim=1, keepdim=True).sqrt().clamp(min=1e-10) for f in feats] | |
| return torch.cat( | |
| [F.interpolate(f, size=self.grid_hw, mode="bilinear", align_corners=True) for f in feats], | |
| dim=1, | |
| ) | |
| def image_map(self, gt: torch.Tensor, mvp_feat: torch.Tensor | None = None): | |
| """The image half of the map, [N, gh, gw, img_width]; constant across steps, computed once per forward.""" | |
| if self.img_proj is None: | |
| return None | |
| if self.image_embed == "mvp_s1": | |
| if mvp_feat.shape[-2:] != self.grid_hw: | |
| mvp_feat = F.interpolate(mvp_feat, size=self.grid_hw, mode="bilinear", align_corners=True) | |
| return self.img_proj(mvp_feat.permute(0, 2, 3, 1)) | |
| return self.img_proj(F.pixel_unshuffle(gt, self.stride).permute(0, 2, 3, 1)) | |
| def forward(self, render: torch.Tensor, gt: torch.Tensor, | |
| gt_feat: torch.Tensor | None = None, | |
| mvp_feat: torch.Tensor | None = None, | |
| img: torch.Tensor | None = None) -> torch.Tensor: | |
| """render, gt [N,3,H,W] -> [N, width + img_width, gh, gw]; ``gt_feat``/``img`` may be precomputed.""" | |
| if not self.use_err: | |
| img = self.image_map(gt, mvp_feat) if img is None else img | |
| return img.permute(0, 3, 1, 2) | |
| err = None | |
| if self.use_feat_diff: | |
| if gt_feat is None: | |
| gt_feat = self.features(gt) | |
| err = self.feat_proj((self.features(render) - gt_feat).permute(0, 2, 3, 1)) | |
| if self.rgb_proj is not None: | |
| rgb = self.rgb_proj(F.pixel_unshuffle(render - gt, self.stride).permute(0, 2, 3, 1)) | |
| err = rgb if err is None else err + rgb | |
| err = self.err_norm(err) | |
| if self.img_proj is None: | |
| return err.permute(0, 3, 1, 2) | |
| if img is None: | |
| img = self.image_map(gt, mvp_feat) | |
| return torch.cat([err, img.to(err.dtype)], dim=-1).permute(0, 3, 1, 2) | |
| class _GlobalBlock(nn.Module): | |
| """Plain pre-norm transformer block with full self-attention.""" | |
| def __init__(self, dim: int, num_heads: int): | |
| super().__init__() | |
| self.norm1 = nn.LayerNorm(dim) | |
| self.attn = nn.MultiheadAttention(dim, num_heads, batch_first=True) | |
| self.norm2 = nn.LayerNorm(dim) | |
| self.mlp = nn.Sequential(nn.Linear(dim, 4 * dim), nn.GELU(), nn.Linear(4 * dim, dim)) | |
| def forward(self, x): | |
| h = self.norm1(x) | |
| x = x + self.attn(h, h, h, need_weights=False)[0] | |
| return x + self.mlp(self.norm2(x)) | |
| class ErrorGlobalAttn(nn.Module): | |
| """Optional cross-view attention over the per-view error maps, at 1/``down`` resolution (off by default).""" | |
| def __init__(self, channels: int, down: int = 4, attn_channels: int = 64, | |
| num_heads: int = 1, depth: int = 1): | |
| super().__init__() | |
| self.down = down | |
| self.proj0 = nn.Linear(channels, attn_channels) | |
| self.proj1 = nn.Linear(attn_channels * down ** 2, attn_channels) | |
| self.norm1 = nn.LayerNorm(attn_channels) | |
| self.blocks = nn.ModuleList([_GlobalBlock(attn_channels, num_heads) for _ in range(depth)]) | |
| self.proj2 = nn.Linear(attn_channels, attn_channels * down ** 2) | |
| self.norm2 = nn.LayerNorm(attn_channels * down ** 2) | |
| self.conv = nn.Conv2d(attn_channels, attn_channels, 3, 1, 1) | |
| self.proj3 = nn.Linear(attn_channels, channels) | |
| nn.init.zeros_(self.proj3.weight) | |
| nn.init.zeros_(self.proj3.bias) | |
| def forward(self, E: torch.Tensor) -> torch.Tensor: | |
| """E [B, V, C, h, w] -> same shape.""" | |
| b, v, _, h, w = E.shape | |
| x = rearrange(E, "b v c h w -> b (v h w) c") | |
| x = self.proj0(x) | |
| x = rearrange(x, "b (v h w) c -> (b v) c h w", v=v, h=h, w=w) | |
| x = F.pixel_unshuffle(x, self.down) | |
| x = rearrange(x, "(b v) c h w -> b (v h w) c", v=v) | |
| x = self.norm1(self.proj1(x)) | |
| for blk in self.blocks: | |
| x = blk(x) | |
| x = self.norm2(self.proj2(x)) | |
| x = rearrange(x, "b (v h w) c -> (b v) c h w", v=v, h=h // self.down, w=w // self.down) | |
| x = self.conv(F.pixel_shuffle(x, self.down)) | |
| x = rearrange(x, "(b v) c h w -> b (v h w) c", v=v) | |
| x = self.proj3(x) | |
| return E + rearrange(x, "b (v h w) c -> b v c h w", v=v, h=h, w=w) | |
| class ErrorLifter(nn.Module): | |
| """Per-view maps -> one feature per token: pooled over the views the token is visible in | |
| (home view always counts), concatenated with the map at the token's own patch.""" | |
| def __init__(self, in_channels: int, out_channels: int, occlusion_tol: float, | |
| reduce: str = "mean", lift: str = "mean+home"): | |
| super().__init__() | |
| assert reduce in ("mean", "max"), reduce | |
| assert lift in ("mean+home", "mean", "home"), lift | |
| self.tol = occlusion_tol | |
| self.reduce = reduce | |
| self.lift = lift | |
| n_in = 2 if lift == "mean+home" else 1 | |
| self.fuse = nn.Sequential(nn.Linear(n_in * in_channels, out_channels), nn.LayerNorm(out_channels)) | |
| def forward(self, E, depth, coord, i_c2w, i_fxfycxcy, patch: int, side: int, size: int): | |
| """E [B,V,C,gh,gw], depth [B,V,H,W] (render depth), coord [B,K,3] world points, | |
| i_c2w [B,V,4,4], i_fxfycxcy [B,V,4], tokens ordered (v, i, j) -> [B,K,C'], [B,K].""" | |
| B, V, C = E.shape[:3] | |
| K = coord.shape[1] | |
| dev = E.device | |
| home_view = torch.arange(K, device=dev) // (side * side) | |
| if self.lift == "home": | |
| cnt = torch.ones(B, K, 1, device=dev, dtype=E.dtype) | |
| pooled = None | |
| else: | |
| # project every token into every view; only in-frustum pairs are sampled below | |
| w2c = i_c2w.float().inverse() | |
| p_cam = (torch.einsum("bkj,bvij->bvki", coord.float(), w2c[:, :, :3, :3]) | |
| + w2c[:, :, None, :3, 3]) | |
| z = p_cam[..., 2] | |
| k_mat = fxfycxcy_to_K(i_fxfycxcy.reshape(B * V, 4)).reshape(B, V, 3, 3) | |
| uvw = torch.einsum("bvkj,bvij->bvki", p_cam, k_mat) | |
| pix = uvw[..., :2] / uvw[..., 2:3].clamp(min=EPS) | |
| in_frustum = (z > EPS) & (pix[..., 0] >= 0) & (pix[..., 0] < size) \ | |
| & (pix[..., 1] >= 0) & (pix[..., 1] < size) | |
| is_home = home_view[None, :] == torch.arange(V, device=dev)[:, None] | |
| b_i, v_i, k_i = (in_frustum | is_home).nonzero(as_tuple=True) | |
| img_i = b_i * V + v_i | |
| pix_c = pix[b_i, v_i, k_i] | |
| d_render = _sample_sparse( | |
| depth.reshape(B * V, 1, *depth.shape[-2:]).float(), pix_c, img_i, size)[:, 0] | |
| z_c = z[b_i, v_i, k_i] | |
| # visible: in the frustum and not behind the rendered surface; the home view always counts | |
| vis = (in_frustum[b_i, v_i, k_i] | |
| & ((d_render <= EPS) | (z_c <= d_render * (1 + self.tol)))) | is_home[v_i, k_i] | |
| sel = vis.nonzero(as_tuple=True)[0] | |
| flat_idx = b_i[sel] * K + k_i[sel] | |
| cnt = torch.zeros(B * K, 1, device=dev, dtype=E.dtype).index_add( | |
| 0, flat_idx, torch.ones(sel.numel(), 1, device=dev, dtype=E.dtype)).view(B, K, 1) | |
| # chunked: a single flat [P, C] buffer would dominate memory at high view counts | |
| e_flat = E.reshape(B * V, C, *E.shape[-2:]) | |
| acc = (torch.full((B * K, C), -torch.inf, device=dev, dtype=E.dtype) | |
| if self.reduce == "max" else torch.zeros(B * K, C, device=dev, dtype=E.dtype)) | |
| step = max(1, _GATHER_CHUNK_ELEMS // C) | |
| for s in range(0, sel.numel(), step): | |
| part, idx = sel[s : s + step], flat_idx[s : s + step] | |
| e_s = _sample_sparse(e_flat, pix_c[part], img_i[part], size) | |
| acc = (acc.scatter_reduce(0, idx[:, None].expand(-1, C), e_s, reduce="amax") | |
| if self.reduce == "max" else acc.index_add(0, idx, e_s)) | |
| pooled = acc.view(B, K, C) | |
| if self.reduce != "max": | |
| pooled = pooled / cnt.clamp(min=1) | |
| # home view: token (v, i, j) <-> patch centre ((j+0.5)p, (i+0.5)p) of view v | |
| ij = torch.stack(torch.meshgrid(torch.arange(side), torch.arange(side), indexing="ij"), -1) | |
| centres = ((ij.flip(-1).float() + 0.5) * patch).to(E.device) # [side, side, 2] as (x, y) | |
| centres = centres.view(1, side * side, 2).expand(B * V, -1, -1) | |
| home = _sample(rearrange(E, "b v c h w -> (b v) c h w"), centres, size) | |
| home = rearrange(home, "(b v) n c -> b (v n) c", b=B) | |
| assert home.shape[1] == K, (home.shape, K) | |
| feats = {"mean+home": lambda: torch.cat([pooled, home], dim=-1), | |
| "mean": lambda: pooled, | |
| "home": lambda: home}[self.lift]() | |
| return self.fuse(feats), cnt[..., 0] | |
| def _knn_indices(coord: torch.Tensor, offset: torch.Tensor, k: int) -> torch.Tensor: | |
| """[K,3] coords, scene-end offsets -> [K,k] neighbour indices (self included).""" | |
| out = torch.empty(coord.shape[0], k, dtype=torch.long, device=coord.device) | |
| start = 0 | |
| for end in offset.tolist(): | |
| pts = coord[start:end] | |
| for q0 in range(0, pts.shape[0], 8192): | |
| q = pts[q0:q0 + 8192] | |
| d = torch.cdist(q, pts) | |
| out[start + q0:start + q0 + q.shape[0]] = start + d.topk(k, largest=False).indices | |
| start = end | |
| return out | |
| def _knn_block_forward(blk, feat: torch.Tensor, idx: torch.Tensor) -> torch.Tensor: | |
| """One PTv3 block evaluated with gather attention over ``idx`` [N,k] neighbours instead of | |
| serialized windows; reuses the block's weights and mirrors Block.forward's residual layout.""" | |
| n, k = idx.shape | |
| a = blk.attn | |
| heads = a.num_heads | |
| shortcut = feat | |
| for m in blk.cpe._modules.values(): | |
| feat = m(feat) | |
| feat = shortcut + feat | |
| shortcut = feat | |
| x = feat | |
| for m in blk.norm1._modules.values(): | |
| x = m(x) | |
| qkv = a.qkv(x) | |
| cp = qkv.shape[-1] // 3 | |
| q, kk, v = qkv.split(cp, dim=-1) | |
| hd = cp // heads | |
| q = q.view(n, heads, hd) | |
| kn = kk[idx.reshape(-1)].view(n, k, heads, hd) | |
| vn = v[idx.reshape(-1)].view(n, k, heads, hd) | |
| attn = torch.einsum("nhd,nkhd->nhk", q * a.scale, kn) | |
| attn = attn.softmax(dim=-1) | |
| out = torch.einsum("nhk,nkhd->nhd", attn, vn).reshape(n, cp) | |
| out = a.proj(out) | |
| for m in blk.drop_path._modules.values(): | |
| out = m(out) | |
| feat = shortcut + out | |
| shortcut = feat | |
| x = feat | |
| for m in blk.norm2._modules.values(): | |
| x = m(x) | |
| for m in blk.mlp._modules.values(): | |
| x = m(x) | |
| for m in blk.drop_path._modules.values(): | |
| x = m(x) | |
| return shortcut + x | |
| def _grid_neighbors(mode: str, B: int, V: int, side: int, device, coord=None, c2w=None, | |
| fxfycxcy=None, depth=None, patch: int = 1, size: int = 1, | |
| tol: float = 0.15) -> torch.Tensor: | |
| """Image-space neighbour indices for tokens laid out (b, v, i, j) -> [B*V*side^2, n]. | |
| "inframe": 3x3 block in the token's own view (9). "xframe": 4-neighbourhood in its own view | |
| plus the 4-neighbourhood of its projection into the partner view v ^ 1 (10), falling back | |
| to self when the projection is out of frustum or fails the depth test. | |
| """ | |
| S2 = side * side | |
| K = V * S2 | |
| ar = torch.arange(side, device=device) | |
| ii, jj = torch.meshgrid(ar, ar, indexing="ij") | |
| ii, jj = ii.reshape(-1), jj.reshape(-1) | |
| base = (torch.arange(B, device=device)[:, None, None] * K | |
| + torch.arange(V, device=device)[None, :, None] * S2) | |
| self_idx = (base + torch.arange(S2, device=device)).reshape(-1) | |
| def offsets(ci, cj, offs): | |
| cols = [] | |
| for di, dj in offs: | |
| ni, nj = ci + di, cj + dj | |
| ok = (ni >= 0) & (ni < side) & (nj >= 0) & (nj < side) | |
| cols.append((ni.clamp(0, side - 1) * side + nj.clamp(0, side - 1), ok)) | |
| return cols | |
| if mode == "inframe": | |
| offs = [(di, dj) for di in (-1, 0, 1) for dj in (-1, 0, 1)] | |
| cols = [(base + c).reshape(-1) for c, _ in offsets(ii, jj, offs)] | |
| oks = [ok.expand(B, V, -1).reshape(-1) for _, ok in offsets(ii, jj, offs)] | |
| idx = torch.stack([torch.where(o, c, self_idx) for c, o in zip(cols, oks)], 1) | |
| return idx | |
| assert mode == "xframe", mode | |
| cross = [(0, 0), (-1, 0), (1, 0), (0, -1), (0, 1)] | |
| own = [torch.where(ok.expand(B, V, -1).reshape(-1), (base + c).reshape(-1), self_idx) | |
| for c, ok in offsets(ii, jj, cross)] | |
| # project every token into its partner view | |
| partner = torch.arange(V, device=device) ^ 1 | |
| pc2w = c2w[:, partner].float() | |
| w2c = pc2w.inverse() | |
| pts = coord.float().view(B, V, S2, 3) | |
| p_cam = torch.einsum("bvkj,bvij->bvki", pts, w2c[..., :3, :3]) + w2c[..., None, :3, 3] | |
| z = p_cam[..., 2] | |
| kmat = fxfycxcy_to_K(fxfycxcy[:, partner].reshape(B * V, 4).float()).reshape(B, V, 3, 3) | |
| uvw = torch.einsum("bvkj,bvij->bvki", p_cam, kmat) | |
| pix = uvw[..., :2] / uvw[..., 2:3].clamp(min=EPS) # [B,V,S2,2] (x, y) | |
| inside = (z > EPS) & (pix[..., 0] >= 0) & (pix[..., 0] < size) & (pix[..., 1] >= 0) & (pix[..., 1] < size) | |
| px = pix[..., 0].floor().long().clamp(0, size - 1) | |
| py = pix[..., 1].floor().long().clamp(0, size - 1) | |
| dpart = depth.float()[:, partner] | |
| d = torch.gather(dpart.reshape(B, V, -1), 2, py * dpart.shape[-1] + px) | |
| visible = inside & ((d <= EPS) | (z <= d * (1 + tol))) | |
| ci, cj = (py // patch).clamp(0, side - 1), (px // patch).clamp(0, side - 1) | |
| pbase = (torch.arange(B, device=device)[:, None, None] * K + partner[None, :, None] * S2) | |
| theirs = [] | |
| for di, dj in cross: | |
| ni, nj = ci + di, cj + dj | |
| ok = visible & (ni >= 0) & (ni < side) & (nj >= 0) & (nj < side) | |
| c = pbase + ni.clamp(0, side - 1) * side + nj.clamp(0, side - 1) | |
| theirs.append(torch.where(ok.reshape(-1), c.reshape(-1), self_idx)) | |
| return torch.stack(own + theirs, 1) | |
| def _block(channels, attn_channels, num_heads, patch_size, drop_path, order_index): | |
| """PTv3 block (xCPE -> window attention -> FFN, pre-norm) with q/k/v in `attn_channels`.""" | |
| blk = Block( | |
| channels=channels, num_heads=num_heads, patch_size=patch_size, mlp_ratio=4, qkv_bias=True, | |
| drop_path=drop_path, norm_layer=nn.LayerNorm, act_layer=nn.GELU, pre_norm=True, order_index=order_index, | |
| cpe_indice_key="stage0", enable_rpe=False, enable_flash=False, upcast_attention=False, upcast_softmax=False, | |
| ) | |
| if attn_channels != channels: | |
| attn = SerializedAttention( | |
| channels=attn_channels, num_heads=num_heads, patch_size=patch_size, qkv_bias=True, | |
| order_index=order_index, enable_rpe=False, enable_flash=False, upcast_attention=False, | |
| upcast_softmax=False, | |
| ) | |
| attn.qkv = nn.Linear(channels, 3 * attn_channels, bias=True) | |
| attn.proj = nn.Linear(attn_channels, channels) | |
| blk.attn = attn | |
| return blk | |
| class Stage2(nn.Module): | |
| """Token + error embeddings -> `depth` PTv3 blocks over the whole token cloud -> token residual.""" | |
| def __init__(self, conf, token_dim: int, error_channels: int, z_dim: int, | |
| aux_channels: int = 0, raw_input: bool = False): | |
| super().__init__() | |
| self.conf = conf | |
| self.z_dim = z_dim | |
| self.order = list(conf["order"]) | |
| W = conf["width"] | |
| # raw_input: raw Gaussian parameters enter the fuse normalised, unprojected; aux: per-point conditioning | |
| if raw_input: | |
| self.token_proj = nn.LayerNorm(token_dim) | |
| fuse_in = token_dim + error_channels + aux_channels | |
| else: | |
| self.token_proj = nn.Sequential(nn.LayerNorm(token_dim), nn.Linear(token_dim, W)) | |
| fuse_in = W + error_channels + aux_channels | |
| self.fuse = nn.Sequential(nn.Linear(fuse_in, W), nn.LayerNorm(W), nn.GELU()) | |
| dp = torch.linspace(0, conf["drop_path"], conf["depth"]).tolist() | |
| self.blocks = nn.ModuleList( | |
| [_block(W, conf["attn_channels"], conf["num_heads"], conf["patch_size"], dp[i], i % len(self.order)) | |
| for i in range(conf["depth"])] | |
| ) | |
| self.norm = nn.LayerNorm(W) | |
| # z_dim=None: no head, forward returns the normalised backbone features. | |
| # attn_mode: "serialized" (trained) or an inference-time gather swap: knn | inframe | xframe. | |
| self.attn_mode = conf.get("attn_mode", "serialized") | |
| self.knn_k = conf.get("knn_k", 8) | |
| self.head = nn.Linear(W, z_dim) if z_dim is not None else None | |
| if self.head is not None: | |
| nn.init.zeros_(self.head.weight) # zero-init -> step 0 reproduces the frozen model | |
| nn.init.zeros_(self.head.bias) | |
| self.window = 0 | |
| def _point(self, grid_coord, offset, feat): | |
| """Serialize `feat` under one grid. Rebuilt whenever the grid changes between blocks.""" | |
| grid = grid_coord.reshape(-1, 3).int() | |
| point = Point({ | |
| "coord": grid.float(), | |
| "grid_coord": grid, | |
| "grid_size": torch.ones(3, device=grid.device), | |
| "feat": feat, | |
| "offset": offset, | |
| }) | |
| point.serialization(order=self.order, shuffle_orders=self.training) | |
| point.sparsify() | |
| return point | |
| def forward(self, tokens, err, grids, aux=None): | |
| """tokens [B,K,D], err [B,K,E], grids [(grid_coord, offset)] cycled over by the blocks -> [B,K,z_dim].""" | |
| B, K = tokens.shape[:2] | |
| parts = [self.token_proj(tokens), err.to(tokens.dtype)] | |
| if aux is not None: | |
| parts.append(aux.to(tokens.dtype)) | |
| feat = self.fuse(torch.cat(parts, dim=-1)) | |
| feat = feat.reshape(B * K, -1) | |
| if self.attn_mode in ("knn", "inframe", "xframe"): | |
| if self.attn_mode == "knn": | |
| grid_coord, offset = grids[0] | |
| idx = _knn_indices(grid_coord.reshape(-1, 3).float(), offset, self.knn_k) | |
| else: | |
| idx = grids[0][1] | |
| for blk in self.blocks: | |
| feat = _knn_block_forward(blk, feat, idx) | |
| self.window = -idx.shape[1] # negative marks gather attention in the log line | |
| feat = self.norm(feat) | |
| if self.head is None: | |
| return feat.view(B, K, -1) | |
| return self.head(feat).view(B, K, self.z_dim) | |
| point, active = None, None | |
| for i, blk in enumerate(self.blocks): | |
| grid_coord, offset = grids[i % len(grids)] | |
| if active is not grid_coord: | |
| point = self._point(grid_coord, offset, feat) | |
| active = grid_coord | |
| else: | |
| point["feat"] = feat | |
| point = blk(point) | |
| feat = point.feat | |
| self.window = self.blocks[0].attn.patch_size | |
| feat = self.norm(feat) | |
| if self.head is None: | |
| return feat.view(B, K, -1) | |
| return self.head(feat).view(B, K, self.z_dim) | |
| class FineSplat(nn.Module): | |
| """FineSplat: Stage 1 (2Xplat) + Stage 2; ``forward`` returns the target-view render after every step.""" | |
| def __init__(self, model, conf: dict): | |
| super().__init__() | |
| self.e2e = conf.get("e2e", False) # true: train Stage 1 jointly; default: Stage 1 frozen | |
| self.grad_ckpt = conf.get("grad_ckpt", False) # recompute each step's Stage-2 forward in backward | |
| self.model = model | |
| if not self.e2e: | |
| self.model.requires_grad_(False) | |
| self.model.eval() | |
| self.conf = conf | |
| self.mode = conf.get("mode", "token") | |
| assert self.mode in ("token", "gaussian"), self.mode | |
| assert model.width == model.height, "square images assumed (side, home-patch lookup)" | |
| self.image_size = model.width | |
| expert = model.appearance_expert | |
| self.G = expert.gaussian_side ** 2 | |
| if self.mode == "token": | |
| self.patch = expert.patch_size | |
| else: | |
| # one point per Gaussian; the (v hh ph ww pw) unfold makes a view's Gaussians a | |
| # plain grid of sub-cells, so the same home-patch lookup works with the sub-cell stride | |
| self.patch = expert.gaussian_stride | |
| self.side = self.image_size // self.patch | |
| self.num_steps = conf.get("num_steps", 4) | |
| # Context views re-rendered with gradients per forward so the loss can score them | |
| # (int for every view count, or dict view_count -> quota); 0 disables. | |
| self.context_loss_views = conf.get("context_loss_views", 0) | |
| self.keep_step_gaussians = False | |
| err = conf["error"] | |
| self.error = ErrorMapEncoder( | |
| self.image_size, err["stride"], err["width"], | |
| use_rgb_diff=err.get("use_rgb_diff", True), | |
| use_image_embed=err.get("use_image_embed", True), | |
| backbone=err.get("backbone", "resnet18"), | |
| use_feat_diff=err.get("use_feat_diff", True), | |
| image_embed=err.get("image_embed", "gt_rgb"), | |
| mvp_dim=model.dim1, | |
| img_width=err.get("img_width"), | |
| ) | |
| self.lifter = ErrorLifter( | |
| self.error.channels, err["lift_channels"], err["occlusion_tol"], | |
| err["reduce"], err.get("lift", "mean+home"), | |
| ) | |
| self.error_global = ( | |
| ErrorGlobalAttn( | |
| self.error.channels, | |
| down=err.get("global_attn_down", 4), | |
| attn_channels=err.get("global_attn_channels", 64), | |
| num_heads=err.get("global_attn_heads", 1), | |
| depth=err.get("global_attn_blocks", 1), | |
| ) if err.get("global_attn", False) else None | |
| ) | |
| # gaussian_token_feat: per-Gaussian points carry (raw params | parent token | error); | |
| # gaussian_delta_z: same points, output is a token residual (G features concatenated in (ph, pw) order) | |
| self.gaussian_token_feat = self.mode == "gaussian" and conf.get("gaussian_token_feat", False) | |
| self.gaussian_delta_z = self.gaussian_token_feat and conf.get("gaussian_delta_z", False) | |
| point_dim = model.dim3 if self.mode == "token" else self.raw_dim(model) | |
| if self.gaussian_token_feat: | |
| self.token_broadcast = nn.Sequential( | |
| nn.LayerNorm(model.dim3), nn.Linear(model.dim3, conf.get("token_feat_channels", 512))) | |
| self.stage2 = Stage2( | |
| conf["network"], point_dim, err["lift_channels"], | |
| None if self.gaussian_delta_z else point_dim, | |
| aux_channels=conf.get("token_feat_channels", 512), raw_input=True) | |
| if self.gaussian_delta_z: | |
| self.token_head = nn.Linear(self.G * conf["network"]["width"], model.dim3) | |
| nn.init.zeros_(self.token_head.weight) | |
| nn.init.zeros_(self.token_head.bias) | |
| else: | |
| self.stage2 = Stage2(conf["network"], point_dim, err["lift_channels"], point_dim) | |
| self._logged = False | |
| def _to_gaussians(self, x: torch.Tensor, v: int) -> torch.Tensor: | |
| """Broadcast a per-token tensor (B, K, C) onto its Gaussians in the (v hh ph ww pw) unfold | |
| order: a token's Gaussians are not adjacent, so a plain repeat would be wrong.""" | |
| side = self.image_size // self.model.appearance_expert.patch_size | |
| g = self.model.appearance_expert.gaussian_side | |
| x = rearrange(x, "b (v hh ww) c -> b v hh ww c", v=v, hh=side, ww=side) | |
| return rearrange(x.unsqueeze(3).unsqueeze(5).expand(-1, -1, -1, g, -1, g, -1), | |
| "b v hh ph ww pw c -> b (v hh ph ww pw) c") | |
| def _gs_to_token(self, x: torch.Tensor, v: int) -> torch.Tensor: | |
| """Inverse grouping of ``_to_gaussians``: per-Gaussian (B, N, C) -> per-token | |
| (B, K, G*C), the G features concatenated in their fixed (ph, pw) slot order.""" | |
| side = self.image_size // self.model.appearance_expert.patch_size | |
| g = self.model.appearance_expert.gaussian_side | |
| return rearrange(x, "b (v hh ph ww pw) c -> b (v hh ww) (ph pw c)", | |
| v=v, hh=side, ww=side, ph=g, pw=g) | |
| def raw_dim(model) -> int: | |
| """Width of one Gaussian's raw parameter vector: depth + offset + SH + scale + quat + opacity.""" | |
| e = model.appearance_expert | |
| return 1 + 2 + e.color_dim + 3 + 4 + e.opacity_dim | |
| def train(self, mode: bool = True): | |
| super().train(mode) | |
| if not self.e2e: | |
| self.model.eval() | |
| return self | |
| def trainable_parameters(self): | |
| return [p for p in self.parameters() if p.requires_grad] | |
| def _render_views(self, gaussians, c2w, fxfycxcy, with_grad: bool, render_mode="RGB", | |
| size: int | None = None): | |
| """Render every view -> (B,V,3,H,W) rgb and, for "RGB+ED", (B,V,H,W) depth; ``size`` sets the raster size only.""" | |
| model = self.model | |
| h = w = self.image_size if size is None else size | |
| b, v = c2w.shape[:2] | |
| if with_grad: | |
| dirs = gaussians.xyz[:, None, :, :] - c2w[..., :3, 3][..., None, :] | |
| opacity = torch.broadcast_to( | |
| gaussians.opacity[..., None, :, :, :], (b, v, gaussians.opacity.shape[1], -1, 1), | |
| ) | |
| opacity = _spherical_harmonics(model.opacity_degree, dirs, opacity) | |
| out = GaussianRenderer.apply( | |
| gaussians.xyz, gaussians.feature, gaussians.scale, gaussians.rotation, opacity, | |
| c2w, fxfycxcy, w, h, model.sh_degree, model.near_plane, model.far_plane, | |
| ) | |
| return out.permute(0, 1, 4, 2, 3).contiguous(), None | |
| # "RGB+ED" is not exposed through GaussianRenderer.apply: rasterize one view at a time | |
| rows = [] | |
| with torch.no_grad(), torch.autocast(device_type="cuda", enabled=False): | |
| for ib in range(b): | |
| xyz, feature = gaussians.xyz[ib], gaussians.feature[ib] | |
| scale, rotation = gaussians.scale[ib], gaussians.rotation[ib] | |
| opac = gaussians.opacity[ib] | |
| outs = [] | |
| for i in range(v): | |
| d = xyz - c2w[ib, i:i + 1, :3, 3][None, ...] | |
| opacity_i = _spherical_harmonics(model.opacity_degree, d, opac[None, ...])[0] | |
| outs.append(GaussianRenderer.render( | |
| xyz, feature, scale, rotation, opacity_i, c2w[ib, i], fxfycxcy[ib, i], | |
| w, h, model.sh_degree, model.near_plane, model.far_plane, render_mode, | |
| )) | |
| rows.append(torch.cat(outs, dim=0)) | |
| out = torch.stack(rows, dim=0) | |
| rgb = out[..., :3].permute(0, 1, 4, 2, 3).contiguous() | |
| depth = out[..., 3] if out.shape[-1] == 4 else None | |
| return rgb, depth | |
| def forward(self, input_data: dict, target_data: dict) -> dict: | |
| model, expert = self.model, self.model.appearance_expert | |
| h = w = self.image_size | |
| base_ctx = (contextlib.nullcontext() if (self.e2e and self.training) | |
| else torch.no_grad()) | |
| with base_ctx: | |
| cameras = model.geometry_expert.predict_cameras(input_data, target_data) | |
| raymap_images, Ks, i_w2c = model._build_raymap_input( | |
| input_data, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, | |
| ) | |
| v = raymap_images.shape[1] | |
| # model.dynamic_grouping: Stage 1 sees a sampled view-group size while it is being | |
| # trained (end-to-end); frozen or at inference it keeps model.group_size. | |
| with expert.grouping(v, enabled=self.e2e and self.training): | |
| x, s1 = expert._stage1_tokenize_and_encode(raymap_images) | |
| x, s2 = expert._stage2_cross_view_and_downsample(x, i_w2c, Ks, h, w) | |
| s3 = expert._stage3_global_cross_view(x, i_w2c, Ks) | |
| tokens = expert.fuse_tokens(s1, s2, s3, v, h, w).float() | |
| raw = expert.raw_from_tokens(tokens, v, h, w) | |
| g0 = expert.gaussians_from_raw( | |
| raw, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, cameras.pred_t_c2w, h, w, | |
| ) | |
| render0 = None | |
| if self.e2e and self.training: | |
| with torch.autocast(device_type="cuda", enabled=False): | |
| render0, _ = self._render_views( | |
| g0, cameras.pred_t_c2w, cameras.pred_t_fxfycxcy, with_grad=True, | |
| ) | |
| B = tokens.shape[0] | |
| K = tokens.shape[1] if self.mode == "token" else raw.shape[1] | |
| assert K == v * self.side ** 2, (K, v, self.side) | |
| gt_ctx = rearrange(input_data["image"], "b v c h w -> (b v) c h w").float() | |
| gt_feat = self.error.features(gt_ctx) | |
| mvp_feat = None | |
| if self.error.image_embed == "mvp_s1": | |
| gh = self.image_size // expert.patch_size | |
| mvp_feat = rearrange(s1.detach(), "n (hh ww) d -> n d hh ww", hh=gh, ww=gh) | |
| img_map = self.error.image_map(gt_ctx, mvp_feat) | |
| points = tokens if self.mode == "token" else raw.float() | |
| aux =(self._to_gaussians(self.token_broadcast(tokens), v) | |
| if self.gaussian_token_feat and not self.gaussian_delta_z else None) | |
| g, renders, n_vis = g0, [], None | |
| gaussians_all = [] | |
| ctx_renders, ctx_indices = [], None | |
| k_ctx = (self.context_loss_views if isinstance(self.context_loss_views, int) | |
| else self.context_loss_views.get(v, 0)) | |
| if k_ctx and self.training: | |
| ctx_indices = torch.randperm(v, device=gt_ctx.device)[:min(k_ctx, v)] | |
| for _ in range(self.num_steps): | |
| with torch.no_grad(): | |
| ctx_rgb, ctx_depth = self._render_views( | |
| g, cameras.pred_i_c2w, cameras.pred_i_fxfycxcy, | |
| with_grad=False, render_mode="RGB+ED", | |
| ) | |
| if ctx_indices is not None: | |
| # separate differentiable render: the error-map render above must stay detached | |
| with torch.autocast(device_type="cuda", enabled=False): | |
| sub, _ = self._render_views( | |
| g, cameras.pred_i_c2w[:, ctx_indices], | |
| cameras.pred_i_fxfycxcy[:, ctx_indices], with_grad=True, | |
| ) | |
| ctx_renders.append(sub) | |
| E = self.error(rearrange(ctx_rgb, "b v c h w -> (b v) c h w").float(), gt_ctx, | |
| gt_feat, mvp_feat, img=img_map) | |
| E = rearrange(E, "(b v) c h w -> b v c h w", b=B) | |
| if self.error_global is not None: | |
| E = self.error_global(E) | |
| coord = self._token_coord(g.xyz, v) if self.mode == "token" else g.xyz | |
| err, n_vis = self.lifter( | |
| E, ctx_depth, coord, cameras.pred_i_c2w, cameras.pred_i_fxfycxcy, | |
| self.patch, self.side, self.image_size, | |
| ) | |
| if self.stage2.attn_mode in ("inframe", "xframe"): | |
| grids = [("idx", _grid_neighbors( | |
| self.stage2.attn_mode, B, v, self.side, coord.device, coord=coord, | |
| c2w=cameras.pred_i_c2w, fxfycxcy=cameras.pred_i_fxfycxcy, | |
| depth=ctx_depth.view(B, v, *ctx_depth.shape[-2:]), patch=self.patch, | |
| size=self.image_size, tol=self.conf["error"]["occlusion_tol"]))] | |
| else: | |
| grids = self._grids(coord, g, B, K) | |
| if self.gaussian_delta_z: | |
| aux = self._to_gaussians(self.token_broadcast(tokens), v) | |
| feat = self._run_stage2(points.detach(), err, grids, aux=aux) | |
| tokens = tokens + self.token_head(self._gs_to_token(feat, v)) | |
| elif self.gaussian_token_feat: | |
| points = points + self._run_stage2(points.detach(), err, grids, aux=aux) | |
| else: | |
| points = points + self._run_stage2(points, err, grids) | |
| # Decode under the ambient autocast (as inference does) so a zero residual | |
| # reproduces Stage 1 bit for bit. | |
| if self.mode == "token": | |
| g = expert.decode_from_tokens( | |
| points, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, | |
| cameras.pred_t_c2w, h, w, | |
| ) | |
| else: | |
| # residual on the pre-activation parameters: only the activations are re-run, | |
| # so every bound they impose still holds; delta-z re-decodes raw from the tokens | |
| if self.gaussian_delta_z: | |
| points = expert.raw_from_tokens(tokens, v, h, w).float() | |
| g = expert.gaussians_from_raw( | |
| points, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, | |
| cameras.pred_t_c2w, h, w, | |
| ) | |
| with torch.autocast(device_type="cuda", enabled=False): | |
| render, _ = self._render_views( | |
| g, cameras.pred_t_c2w, cameras.pred_t_fxfycxcy, with_grad=True, | |
| ) | |
| renders.append(render) | |
| if self.keep_step_gaussians: | |
| gaussians_all.append(g) | |
| # each step is supervised on its own render and starts from a detached state | |
| points = points.detach() | |
| if self.gaussian_delta_z: | |
| tokens = tokens.detach() | |
| if not self._logged: | |
| print(f"flat PTv3 ({self.mode}): {K} points/scene, attention window " | |
| f"{self.stage2.window}, {self.num_steps} step(s) (batch of {B}); " | |
| f"tokens visible in {n_vis.mean():.2f} views on average", flush=True) | |
| self._logged = True | |
| return {"render": renders[-1], "renders": renders, "gaussians": g, | |
| "gaussians_all": gaussians_all, "gaussians_base": g0, | |
| "render0": render0, | |
| "tokens_final": points, "tokens_base": tokens, | |
| "context_renders": ctx_renders, "context_indices": ctx_indices, | |
| "cameras": cameras, "error_views": n_vis} | |
| def _run_stage2(self, *args, **kwargs): | |
| if self.grad_ckpt and self.training and torch.is_grad_enabled(): | |
| return torch.utils.checkpoint.checkpoint(self.stage2, *args, use_reentrant=False, **kwargs) | |
| return self.stage2(*args, **kwargs) | |
| def _token_coord(self, xyz: torch.Tensor, v: int) -> torch.Tensor: | |
| """Mean position of each token's Gaussians, (B, N, 3) -> (B, K, 3), honouring the | |
| (v hh ph ww pw) unfold order (a plain (K, G) reshape would mix neighbouring tokens).""" | |
| side = self.image_size // self.model.appearance_expert.patch_size | |
| g = self.model.appearance_expert.gaussian_side | |
| xyz = rearrange(xyz, "b (v hh ph ww pw) c -> b (v hh ww) (ph pw) c", | |
| v=v, hh=side, ww=side, ph=g, pw=g) | |
| return xyz.mean(dim=2) | |
| def _grids(self, coord, g0, b: int, k: int): | |
| """World-space serialization grid: the Stage-1 Gaussians' bounding box split | |
| ``grid_resolution`` ways per axis, one point cloud per scene -> [(grid, offset)].""" | |
| lo = g0.xyz.amin(dim=-2, keepdim=True) | |
| span = (g0.xyz.amax(dim=-2, keepdim=True) - lo).clamp(min=EPS) | |
| res = self.conf["network"]["grid_resolution"] | |
| grid = (((coord - lo) / span) * res).floor().int().clamp(0, res - 1) | |
| offset = torch.arange(1, b + 1, device=coord.device) * k | |
| return [(grid, offset)] | |
| def render_baseline_targets(self, gaussians_base, cameras) -> torch.Tensor: | |
| """Render the frozen Gaussians at the target views, for side-by-side logging.""" | |
| rgb, _ = self._render_views( | |
| gaussians_base, cameras.pred_t_c2w, cameras.pred_t_fxfycxcy, with_grad=False, | |
| ) | |
| return rgb | |