"""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 @torch.no_grad() 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) @staticmethod 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)] @torch.no_grad() 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