"""Stage 1 of FineSplat: the 2Xplat feed-forward 3DGS model. A DA3 geometry expert predicts cameras; an MVP appearance expert maps ray-encoded images to a 3D Gaussian field. Each patch is a ``gaussian_side x gaussian_side`` sub-grid of Gaussians; each Gaussian owns a ``gaussian_stride x gaussian_stride`` pixel cell and predicts a bounded image-space offset inside it.""" import contextlib import os import random import torch import traceback import torch.nn.functional as F from typing import Any from torch import nn from easydict import EasyDict as edict from einops.layers.torch import Rearrange from einops import rearrange from safetensors.torch import load_file from collections import OrderedDict from dataclasses import dataclass from src.model.sh_eval import _spherical_harmonics from src.model.transformer import TransformerBlock from src.model.dpt_head import DPTHead from src.model.prope_custom import PropeDotProductAttention from src.model.depth_anything.da_model.da3 import DepthAnything3 from src.model.gaussians import GaussianRenderer, GaussianField from src.utils.camera_utils import ( invert_SE3, compute_plucmap, fxfycxcy_to_K, mat_to_quat, quat_to_mat ) def _init_weights(module: nn.Module) -> None: if isinstance(module, nn.Linear): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, (nn.RMSNorm, nn.LayerNorm)): module.reset_parameters() elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) def _fit_similarity( source_c2w: torch.Tensor, # (B, V, 4, 4) reference_c2w: torch.Tensor, # (B, V, 4, 4) ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Fit the Sim(3) (rotation (B, 3, 3), scale (B,), translation (B, 3)) that maps ``source`` context poses onto ``reference`` ones. Fitted on detached poses: it is a gauge correction, and the SVD backward of a near-rotation matrix is ill-conditioned.""" source_c2w, reference_c2w = source_c2w.detach(), reference_c2w.detach() b = source_c2w.shape[0] src_rot, ref_rot = source_c2w[:, :, :3, :3], reference_c2w[:, :, :3, :3] u, _, vh = torch.linalg.svd((ref_rot @ src_rot.transpose(-1, -2)).sum(dim=1)) correction = torch.eye(3, dtype=u.dtype, device=u.device).expand(b, 3, 3).clone() correction[:, 2, 2] = torch.det(u @ vh) rotation = u @ correction @ vh src_c, ref_c = source_c2w[:, :, :3, 3], reference_c2w[:, :, :3, 3] src_mean, ref_mean = src_c.mean(dim=1), ref_c.mean(dim=1) rotated = (src_c - src_mean[:, None]) @ rotation.transpose(-1, -2) scale = ( (rotated * (ref_c - ref_mean[:, None])).sum(dim=(1, 2)) / rotated.square().sum(dim=(1, 2)).clamp_min(1e-12) ) translation = ref_mean - scale[:, None] * (src_mean[:, None] @ rotation.transpose(-1, -2))[:, 0] return rotation, scale, translation def _apply_similarity( c2w: torch.Tensor, rotation: torch.Tensor, scale: torch.Tensor, translation: torch.Tensor, ) -> torch.Tensor: """Apply ``c -> scale * rotation @ c + translation`` to c2w (B, N, 4, 4), with rotation (B, 3, 3), scale (B,), translation (B, 3) as from ``_fit_similarity``.""" rot = rotation[:, None] @ c2w[:, :, :3, :3] trans = ( scale[:, None, None] * (c2w[:, :, :3, 3] @ rotation.transpose(-1, -2)) + translation[:, None] ) # Out-of-place: in-place writes on sliced views break autograd version tracking. return torch.cat( [torch.cat([rot, trans.unsqueeze(-1)], dim=-1), c2w[:, :, 3:, :]], dim=-2, ) @dataclass class CameraBundle: pred_i_fxfycxcy: torch.Tensor # (B, V, 4) pred_i_c2w: torch.Tensor # (B, V, 4, 4) pred_t_fxfycxcy: torch.Tensor # (B, T, 4) pred_t_c2w: torch.Tensor # (B, T, 4, 4) gt_i_fxfycxcy: torch.Tensor # (B, V, 4) gt_i_c2w: torch.Tensor # (B, V, 4, 4) gt_t_fxfycxcy: torch.Tensor # (B, T, 4) gt_t_c2w: torch.Tensor # (B, T, 4, 4) class GeometryExpert: """Predicts poses and intrinsics for all views with DA3, normalized to the scene scale.""" def __init__( self, pose_regressor: DepthAnything3, scene_scale: float, inference_mode: bool, camera_mode: str | None, ): """``camera_mode`` (inference only) selects GT vs. predicted cameras: ``gt_pose_gt_intr``, ``pred_pose_gt_intr`` or ``pred_pose_pred_intr``.""" self.pose_regressor = pose_regressor self.scene_scale = scene_scale self.inference_mode = inference_mode self.camera_mode = camera_mode def predict_cameras( self, input_data_dict: dict, target_data_dict: dict, ) -> CameraBundle: """Run DA3 on all views, normalize poses to scene scale, and apply camera mode.""" gt_i_fxfycxcy = input_data_dict["fxfycxcy"].float() gt_i_c2w = input_data_dict["c2w"].float() gt_t_fxfycxcy = target_data_dict["fxfycxcy"].float() gt_t_c2w = target_data_dict["c2w"].float() if self.inference_mode and self.camera_mode == "gt_pose_gt_intr": # Fully posed: skip DA3, _select_cameras would discard its output anyway. return CameraBundle( pred_i_fxfycxcy=gt_i_fxfycxcy, pred_i_c2w=gt_i_c2w, pred_t_fxfycxcy=gt_t_fxfycxcy, pred_t_c2w=gt_t_c2w, gt_i_fxfycxcy=gt_i_fxfycxcy, gt_i_c2w=gt_i_c2w, gt_t_fxfycxcy=gt_t_fxfycxcy, gt_t_c2w=gt_t_c2w, ) pred_i_fxfycxcy, pred_i_c2w, pred_t_fxfycxcy, pred_t_c2w = self._run_da3_and_normalize( input_data_dict["image"], target_data_dict["image"], torch.cat([gt_i_fxfycxcy, gt_t_fxfycxcy], dim=1), ) if self.inference_mode: pred_i_fxfycxcy, pred_i_c2w, pred_t_fxfycxcy, pred_t_c2w = self._select_cameras( pred_i_fxfycxcy, pred_i_c2w, pred_t_fxfycxcy, pred_t_c2w, gt_i_fxfycxcy, gt_i_c2w, gt_t_fxfycxcy, gt_t_c2w, ) return CameraBundle( pred_i_fxfycxcy=pred_i_fxfycxcy, pred_i_c2w=pred_i_c2w, pred_t_fxfycxcy=pred_t_fxfycxcy, pred_t_c2w=pred_t_c2w, gt_i_fxfycxcy=gt_i_fxfycxcy, gt_i_c2w=gt_i_c2w, gt_t_fxfycxcy=gt_t_fxfycxcy, gt_t_c2w=gt_t_c2w, ) def _run_da3_and_normalize( self, input_images: torch.Tensor, target_images: torch.Tensor, gt_fxfycxcy: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Predict cameras for all views. At inference the context cameras come from a context-only pass; a context+target pass gives the target poses, which are mapped onto the context-only gauge via Sim(3). Training uses one joint pass.""" if not self.inference_mode or self.pose_regressor.training: return self._forward_and_normalize(input_images, target_images, gt_fxfycxcy) v = input_images.shape[1] context = self._forward_and_normalize( input_images, input_images[:, :0], gt_fxfycxcy[:, :v], ) if target_images.shape[1] == 0: return context _, pass2_i_c2w, t_fxfycxcy, t_c2w = self._forward_and_normalize( input_images, target_images, gt_fxfycxcy, ) with torch.autocast(device_type="cuda", enabled=False): rotation, scale, translation = _fit_similarity(pass2_i_c2w, context[1]) t_c2w = _apply_similarity(t_c2w, rotation, scale, translation) return context[0], context[1], t_fxfycxcy, t_c2w def _forward_and_normalize( self, input_images: torch.Tensor, target_images: torch.Tensor, gt_fxfycxcy: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Forward DA3 on all input+target images and normalize cameras to scene scale.""" _, v, _, h, w = input_images.shape with torch.autocast(device_type="cuda", enabled=False): all_images = torch.cat([input_images, target_images], dim=1) output = self._forward_pose_regressor(all_images, gt_fxfycxcy) i_fxfycxcy_raw = output["fxfycxcy"][:, :v].float() i_c2w_raw = output["extrinsics"][:, :v].float() t_fxfycxcy_raw = output["fxfycxcy"][:, v:].float() t_c2w_raw = output["extrinsics"][:, v:].float() # DA3 outputs intrinsics at its own resolution; rescale to the model's input resolution. da_h, da_w = input_images.shape[-2:] intr_scale = torch.tensor( [w / float(da_w), h / float(da_h), w / float(da_w), h / float(da_h)], dtype=i_fxfycxcy_raw.dtype, device=i_fxfycxcy_raw.device, ).view(1, 1, 4) i_fxfycxcy = i_fxfycxcy_raw * intr_scale t_fxfycxcy = t_fxfycxcy_raw * intr_scale i_c2w, t_c2w = self._shared_scene_normalization( input_c2ws=i_c2w_raw, target_c2ws=t_c2w_raw, scene_scale=self.scene_scale, ) return i_fxfycxcy, i_c2w, t_fxfycxcy, t_c2w def _forward_pose_regressor(self, images: torch.Tensor, gt_fxfycxcy: torch.Tensor) -> dict: return self.pose_regressor(images) @staticmethod def _shared_scene_normalization( input_c2ws: torch.Tensor, # (V, 4, 4) or (B, V, 4, 4) target_c2ws: torch.Tensor | None = None, # (T, 4, 4) or (B, T, 4, 4) scene_scale: float = 1.0, ): """Scene normalization as in dataset.py: Gram-Schmidt canonical frame from the input poses, applied to input and target, then translations scaled by max |t| of the inputs.""" squeeze_input = input_c2ws.ndim == 3 squeeze_target = target_c2ws is not None and target_c2ws.ndim == 3 if squeeze_input: input_c2ws = input_c2ws.unsqueeze(0) if target_c2ws is not None and squeeze_target: target_c2ws = target_c2ws.unsqueeze(0) position_avg = input_c2ws[:, :, :3, 3].mean(dim=1) forward_avg = input_c2ws[:, :, :3, 2].mean(dim=1) down_avg = input_c2ws[:, :, :3, 1].mean(dim=1) forward_avg = F.normalize(forward_avg, dim=-1) down_proj = (down_avg * forward_avg).sum(dim=-1, keepdim=True) * forward_avg down_avg = F.normalize(down_avg - down_proj, dim=-1) right_avg = torch.cross(down_avg, forward_avg, dim=-1) pos_avg = torch.eye( 4, dtype=input_c2ws.dtype, device=input_c2ws.device ).expand(input_c2ws.shape[0], 4, 4).clone() pos_avg[:, :3, 0] = right_avg pos_avg[:, :3, 1] = down_avg pos_avg[:, :3, 2] = forward_avg pos_avg[:, :3, 3] = position_avg pos_avg_inv = torch.linalg.inv(pos_avg) input_c2ws = torch.matmul(pos_avg_inv.unsqueeze(1), input_c2ws) if target_c2ws is not None: target_c2ws = torch.matmul(pos_avg_inv.unsqueeze(1), target_c2ws) translations = input_c2ws[:, :, :3, 3].clone().detach() scene_extent = translations.abs().amax(dim=(1, 2)) scale = 1.0 / (scene_scale * scene_extent) # Out-of-place: in-place writes on sliced views break autograd version tracking. input_scaled_t = input_c2ws[:, :, :3, 3] * scale[:, None, None] input_c2ws = torch.cat( [ torch.cat([input_c2ws[:, :, :3, :3], input_scaled_t.unsqueeze(-1)], dim=-1), input_c2ws[:, :, 3:, :], ], dim=-2, ) if target_c2ws is not None: target_scaled_t = target_c2ws[:, :, :3, 3] * scale[:, None, None] target_c2ws = torch.cat( [ torch.cat([target_c2ws[:, :, :3, :3], target_scaled_t.unsqueeze(-1)], dim=-1), target_c2ws[:, :, 3:, :], ], dim=-2, ) if squeeze_input: input_c2ws = input_c2ws.squeeze(0) if target_c2ws is not None and squeeze_target: target_c2ws = target_c2ws.squeeze(0) if target_c2ws is None: return input_c2ws return input_c2ws, target_c2ws def _select_cameras( self, pred_i_fxfycxcy: torch.Tensor, pred_i_c2w: torch.Tensor, pred_t_fxfycxcy: torch.Tensor, pred_t_c2w: torch.Tensor, gt_i_fxfycxcy: torch.Tensor, gt_i_c2w: torch.Tensor, gt_t_fxfycxcy: torch.Tensor, gt_t_c2w: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Choose between predicted and GT cameras according to camera_mode.""" if self.camera_mode == "gt_pose_gt_intr": return gt_i_fxfycxcy, gt_i_c2w, gt_t_fxfycxcy, gt_t_c2w if self.camera_mode == "pred_pose_gt_intr": return gt_i_fxfycxcy, pred_i_c2w, gt_t_fxfycxcy, pred_t_c2w if self.camera_mode == "pred_pose_pred_intr": return pred_i_fxfycxcy, pred_i_c2w, pred_t_fxfycxcy, pred_t_c2w raise ValueError( f"Unsupported camera_mode: {self.camera_mode!r}. " "Use one of: gt_pose_gt_intr, pred_pose_gt_intr, pred_pose_pred_intr." ) class AppearanceExpert: """MVP three-stage transformer mapping ray-encoded images to a 3D Gaussian field. Holds non-owning references to modules registered on ``TwoExpertModel``. ``gaussian_side`` is the per-patch sub-grid side; ``gaussian_stride`` the pixels per side of each cell.""" def __init__( self, image_tokenizer: nn.Sequential, stage1: nn.ModuleList, stage2: nn.ModuleList, stage3: nn.ModuleList, merge_block1: nn.Conv2d, resize_block1: nn.Linear, merge_block2: nn.Conv2d, resize_block2: nn.Linear, attention2: PropeDotProductAttention, attention3: PropeDotProductAttention, dpt_head: DPTHead, gaussian_decoder: nn.Sequential, register_token_init: nn.Parameter, patch_size: int, gaussian_side: int, gaussian_stride: int, num_register_tokens: int, group_size: int, color_dim: int, opacity_dim: int, sh_degree: int, opacity_degree: int, scale_bias: float, scale_max: float, opacity_bias: float, max_dist: float, inference_mode: bool, dynamic_grouping: bool = False, ): self.image_tokenizer = image_tokenizer self.stage1 = stage1 self.stage2 = stage2 self.stage3 = stage3 self.merge_block1 = merge_block1 self.resize_block1 = resize_block1 self.merge_block2 = merge_block2 self.resize_block2 = resize_block2 self.attention2 = attention2 self.attention3 = attention3 self.dpt_head = dpt_head self.gaussian_decoder = gaussian_decoder self.register_token_init = register_token_init self.patch_size = patch_size self.gaussian_side = gaussian_side self.gaussian_stride = gaussian_stride self.num_register_tokens = num_register_tokens self.group_size = group_size self.color_dim = color_dim self.opacity_dim = opacity_dim self.sh_degree = sh_degree self.opacity_degree = opacity_degree self.scale_bias = scale_bias self.scale_max = scale_max self.opacity_bias = opacity_bias self.max_dist = max_dist # Training only: sample the stage-2 view-group size per forward (see grouping()). self.dynamic_grouping = dynamic_grouping self.training = True # mirrors the owning TwoExpertModel (set in its train()) self.inference_mode = inference_mode def sample_group_size(self, v: int) -> int: """A divisor of v in [2, v), larger groups more likely; the fixed size if none.""" candidates = [g for g in range(2, v) if v % g == 0] if not candidates: return self.group_size return random.choices(candidates, weights=candidates)[0] @contextlib.contextmanager def grouping(self, v: int, enabled: bool): """Run stages 1-3 under a sampled view-group size (dynamic_grouping and enabled). All three stages read self.group_size, so the override must span the whole encode.""" if not (self.dynamic_grouping and enabled): yield self.group_size return fixed = self.group_size self.group_size = self.sample_group_size(v) try: yield self.group_size finally: self.group_size = fixed def predict_gaussians( self, raymap_images: torch.Tensor, # (B, V, C, H, W) i_w2c: torch.Tensor, # (B, V, 4, 4) Ks: torch.Tensor, # (B, V, 3, 3) i_fxfycxcy: torch.Tensor, # (B, V, 4) i_c2w: torch.Tensor, # (B, V, 4, 4) t_c2w: torch.Tensor, # (B, T, 4, 4) ) -> GaussianField: """Run the full three-stage transformer pipeline and decode Gaussian parameters.""" _, v, _, h, w = raymap_images.shape with self.grouping(v, enabled=self.training): x, s1_patch_tokens = self._stage1_tokenize_and_encode(raymap_images) x, s2_patch_tokens = self._stage2_cross_view_and_downsample(x, i_w2c, Ks, h, w) s3_patch_tokens = self._stage3_global_cross_view(x, i_w2c, Ks) return self._decode_gaussians( s1_patch_tokens, s2_patch_tokens, s3_patch_tokens, i_fxfycxcy, i_c2w, t_c2w, h, w, ) def _stage1_tokenize_and_encode( self, raymap_images: torch.Tensor, ): """Patch-embed ray-map images with register tokens, then run stage-1 transformer.""" b, v, _, h, w = raymap_images.shape register_tokens = self.register_token_init.repeat(b, v, 1, 1) x = self.image_tokenizer(raymap_images) x = rearrange(x, "b (v l) d -> b v l d", v=v) x = torch.cat([register_tokens, x], dim=2) x = rearrange(x, "b v l d -> (b v) l d") x = self._run_stage1_blocks(x, None) r_tokens, s1_patch_tokens = x[:, :self.num_register_tokens], x[:, self.num_register_tokens:] r_tokens = self.resize_block1(r_tokens) h_patches = h // self.patch_size w_patches = w // self.patch_size i_tokens = rearrange(s1_patch_tokens, "b (hh ww) d -> b d hh ww", hh=h_patches, ww=w_patches) i_tokens = self.merge_block1(i_tokens) i_tokens = rearrange(i_tokens, "b d hh ww -> b (hh ww) d", hh=h_patches // 2, ww=w_patches // 2) x = torch.cat([r_tokens, i_tokens], dim=1) x = rearrange(x, "(b g v) l d -> (b g) (v l) d", g=v // self.group_size, v=self.group_size) return x, s1_patch_tokens def _stage2_cross_view_and_downsample( self, x: torch.Tensor, i_w2c: torch.Tensor, Ks: torch.Tensor, h: int, w: int, ): """Run grouped cross-view attention (stage 2) and spatially downsample patch tokens.""" v = i_w2c.shape[1] info = { "num_input_views": v, "w2c": rearrange(i_w2c, "b (g v) ... -> (b g) v ...", g=v // self.group_size, v=self.group_size), "Ks": rearrange(Ks, "b (g v) ... -> (b g) v ...", g=v // self.group_size, v=self.group_size), "attn2": self.attention2, } x = self._run_stage2_blocks(x, info) r_tokens, s2_patch_tokens = x[:, :self.num_register_tokens], x[:, self.num_register_tokens:] r_tokens = self.resize_block2(r_tokens) h_patches = (h // self.patch_size) // 2 w_patches = (w // self.patch_size) // 2 i_tokens = rearrange(s2_patch_tokens, "b (hh ww) d -> b d hh ww", hh=h_patches, ww=w_patches) i_tokens = self.merge_block2(i_tokens) i_tokens = rearrange(i_tokens, "b d hh ww -> b (hh ww) d", hh=h_patches // 2, ww=w_patches // 2) x = torch.cat([r_tokens, i_tokens], dim=1) x = rearrange(x, "(b v) l d -> b (v l) d", v=v) return x, s2_patch_tokens def _stage3_global_cross_view( self, x: torch.Tensor, i_w2c: torch.Tensor, Ks: torch.Tensor, ) -> torch.Tensor: """Run global cross-view attention (stage 3) across all input views.""" v = i_w2c.shape[1] info = { "num_input_views": v, "attn3": self.attention3, "w2c": i_w2c, "Ks": Ks, } x = self._run_stage3_blocks(x, info) return x[:, self.num_register_tokens:] def _compute_offset_rays( self, fxfycxcy: torch.Tensor, # (B, V, 4) c2w: torch.Tensor, # (B, V, 4, 4) h: int, w: int, image_offset: torch.Tensor, # (B, V*gh*gw, 2) ) -> tuple[torch.Tensor, torch.Tensor]: """Rays through sub-cell centers displaced by the (pre-tanh, xy) offset; returns ``ray_o, ray_d`` of shape (B, V*gh*gw, 3). The offset is squashed to +-0.5 cell so a Gaussian never leaves its cell; intrinsics are in input-image pixels like the grid.""" b, v = fxfycxcy.size(0), fxfycxcy.size(1) stride = self.gaussian_stride gh, gw = h // stride, w // stride offset_px = 0.5 * stride * image_offset.tanh() offset_px = offset_px.reshape(b * v, gh * gw, 2) fxfycxcy = fxfycxcy.reshape(b * v, 4) c2w = c2w.reshape(b * v, 4, 4) idx_x = torch.arange(gw, device=c2w.device)[None, :].expand(gh, -1) idx_y = torch.arange(gh, device=c2w.device)[:, None].expand(-1, gw) x = ((idx_x.flatten() + 0.5) * stride + offset_px[..., 0] - fxfycxcy[:, 2:3]) / fxfycxcy[:, 0:1] y = ((idx_y.flatten() + 0.5) * stride + offset_px[..., 1] - fxfycxcy[:, 3:4]) / fxfycxcy[:, 1:2] z = torch.ones_like(x) ray_d = torch.stack([x, y, z], dim=1) ray_d = torch.bmm(c2w[:, :3, :3], ray_d) ray_d = ray_d / torch.norm(ray_d, dim=1, keepdim=True) ray_o = c2w[:, :3, 3:4].expand(b * v, -1, gh * gw) ray_o = rearrange(ray_o, "(b v) c n -> b (v n) c", b=b, v=v) ray_d = rearrange(ray_d, "(b v) c n -> b (v n) c", b=b, v=v) return ray_o, ray_d def _decode_gaussians( self, s1_patch_tokens: torch.Tensor, s2_patch_tokens: torch.Tensor, s3_patch_tokens: torch.Tensor, i_fxfycxcy: torch.Tensor, i_c2w: torch.Tensor, t_c2w: torch.Tensor, h: int, w: int, ) -> GaussianField: output_tokens = self.fuse_tokens( s1_patch_tokens, s2_patch_tokens, s3_patch_tokens, i_c2w.shape[1], h, w, ) return self.decode_from_tokens(output_tokens, i_fxfycxcy, i_c2w, t_c2w, h, w) def fuse_tokens( self, s1_patch_tokens: torch.Tensor, s2_patch_tokens: torch.Tensor, s3_patch_tokens: torch.Tensor, v: int, h: int, w: int, ) -> torch.Tensor: """Fuse the three stages with the DPT head into per-patch tokens (B, V*L, dim3): the tokens right before ``gaussian_decoder``, which Stage 2 and TTO operate on.""" output_tokens = self.dpt_head( [s1_patch_tokens, s2_patch_tokens, s3_patch_tokens], [h, w], self.patch_size, ) return rearrange(output_tokens, "(b v) l d -> b (v l) d", v=v) def decode_from_tokens( self, output_tokens: torch.Tensor, i_fxfycxcy: torch.Tensor, i_c2w: torch.Tensor, t_c2w: torch.Tensor, h: int, w: int, ) -> GaussianField: """Decode fused tokens into 3D Gaussian parameters.""" raw = self.raw_from_tokens(output_tokens, i_c2w.shape[1], h, w) return self.gaussians_from_raw(raw, i_fxfycxcy, i_c2w, t_c2w, h, w) def raw_from_tokens(self, output_tokens: torch.Tensor, v: int, h: int, w: int) -> torch.Tensor: """Gaussian head + unfold: raw pre-activation parameters (B, N, 23) laid out as ``[depth 1 | image_offset 2 | feature C | scale 3 | rotation 4 | opacity O]``.""" gaussians = self.gaussian_decoder(output_tokens) return rearrange( gaussians, "b (v hh ww) (ph pw d) -> b (v hh ph ww pw) d", v=v, hh=h // self.patch_size, ww=w // self.patch_size, ph=self.gaussian_side, pw=self.gaussian_side, ) def gaussians_from_raw( self, raw: torch.Tensor, i_fxfycxcy: torch.Tensor, i_c2w: torch.Tensor, t_c2w: torch.Tensor, h: int, w: int, ) -> GaussianField: """Apply the activations and ray parameterisation to raw (B, N, 23) parameters.""" b, v = i_c2w.shape[:2] t = t_c2w.shape[1] if t_c2w is not None else 0 depth, image_offset, feature, scale, rotation, opacity = torch.split( raw, [1, 2, self.color_dim, 3, 4, self.opacity_dim], dim=-1, ) depth = depth.float() image_offset = image_offset.float() feature = feature.float() scale = scale.float() rotation = rotation.float() opacity = opacity.float() with torch.autocast(device_type="cuda", enabled=False): rayo_gs, rayd_gs = self._compute_offset_rays( i_fxfycxcy, i_c2w, h, w, image_offset, ) scale = (scale + self.scale_bias).clamp(max=self.scale_max) # Bias only the DC (sh0) opacity. Out-of-place: `opacity` is a torch.split # view, and in-place writes into it fail once tokens require grad (TTO). dc_bias = torch.zeros_like(opacity) dc_bias[..., 0] = self.opacity_bias opacity = opacity + dc_bias feature = rearrange(feature, "b n (c d) -> b n d c", c=3).contiguous() opacity = rearrange(opacity, "b n (c d) -> b n d c", c=1).contiguous() dist = depth.sigmoid() * self.max_dist xyz = dist * rayd_gs + rayo_gs if not self.inference_mode: dirs = xyz[:, None, :, :] - t_c2w[..., :3, 3][..., None, :] # (B, T, N, 3) opacity_broad = torch.broadcast_to( opacity[..., None, :, :, :], (b, t, opacity.shape[1], -1, 1), ) opacity_precompute = _spherical_harmonics(self.opacity_degree, dirs, opacity_broad) else: opacity_precompute = None return GaussianField( xyz=xyz, feature=feature, scale=scale, rotation=rotation, opacity=opacity, opacity_precompute=opacity_precompute, ) def _run_stage1_blocks(self, x: torch.Tensor, info: dict | None) -> torch.Tensor: for block in self.stage1: x = block(x, False, 1, info) return x def _run_stage2_blocks(self, x: torch.Tensor, info: dict) -> torch.Tensor: g = self.group_size v = info["num_input_views"] for i, block in enumerate(self.stage2): if i % 2 == 0: x = rearrange(x, "(b g) (v l) d -> (b g v) l d", g=v // g, v=g) x = block(x, False, 2, info) x = rearrange(x, "(b g v) l d -> (b g) (v l) d", g=v // g, v=g) else: x = block(x, True, 2, info) return rearrange(x, "(b g) (v l) d -> (b g v) l d", g=v // g, v=g) def _run_stage3_blocks(self, x: torch.Tensor, info: dict) -> torch.Tensor: v = info["num_input_views"] for i, block in enumerate(self.stage3): if i % 2 == 0: x = rearrange(x, "b (v l) d -> (b v) l d", v=v) x = block(x, False, 3, info) x = rearrange(x, "(b v) l d -> b (v l) d", v=v) else: x = block(x, True, 3, info) return rearrange(x, "b (v l) d -> (b v) l d", v=v) class TwoExpertModel(nn.Module): """Stage 1 model: wires the GeometryExpert and AppearanceExpert together.""" def __init__(self, config: Any) -> None: """A ``config.inference`` section selects inference mode; its absence means training.""" super().__init__() self.dim1 = config.model.dim1 self.dim2 = config.model.dim2 self.dim3 = config.model.dim3 self.patch_size = config.model.patch_size self.num_register_tokens = config.model.num_register_tokens self.group_size = config.model.group_size self.dynamic_grouping = bool(config.model.get("dynamic_grouping", False)) self.head_dim = config.model.head_dim self.inter_multi = config.model.inter_multi self.qk_norm = config.model.qk_norm self.in_channels = config.model.in_channels self.stage1_nlayer = config.model.stage1_nlayer self.stage2_nlayer = config.model.stage2_nlayer self.stage3_nlayer = config.model.stage3_nlayer self.sh_degree = config.model.gaussians.sh_degree self.opacity_degree = config.model.gaussians.opacity_degree self.near_plane = config.model.gaussians.near_plane self.far_plane = config.model.gaussians.far_plane self.scale_bias = config.model.gaussians.scale_bias self.scale_max = config.model.gaussians.scale_max self.opacity_bias = config.model.gaussians.opacity_bias self.max_dist = config.model.gaussians.max_dist self.da_model_name = config.model.da_model_name # training.grad_checkpoint_every: 0 disables DA3 gradient checkpointing, N checkpoints every N-th block. self.grad_ckpt_every = ( config.training.get("grad_checkpoint_every", 0) if hasattr(config, "training") else 0 ) self.da_weights_path = getattr(config.model, "da_model_weights_path", None) self.mvp_weights_path = getattr(config.model, "mvp_weights_path", None) self.scene_scale = config.data.scene_scale self.width = config.data.resize_w self.height = config.data.resize_h self.inference_mode = hasattr(config, "inference") self.camera_mode = ( getattr(config.inference, "camera_mode", "pred_pose_pred_intr") if self.inference_mode else None ) # inference.epa_steps: Adam steps of EPA (evaluation-time pose alignment); default 200. epa_steps = getattr(config.inference, "epa_steps", None) if hasattr(config, "inference") else None self.epa_steps = 200 if epa_steps is None else int(epa_steps) self.use_pose_optimization = bool( getattr(config.inference, "pose_optimization", False) if self.inference_mode else False ) self.color_dim = 3 * (self.sh_degree + 1) ** 2 self.opacity_dim = 1 * (self.opacity_degree + 1) ** 2 # Gaussians per patch as a gaussian_side x gaussian_side sub-grid; default one per pixel. self.gaussians_per_patch = getattr( config.model, "gaussians_per_patch", self.patch_size ** 2, ) self.gaussian_side = int(round(self.gaussians_per_patch ** 0.5)) assert self.gaussian_side ** 2 == self.gaussians_per_patch, \ "gaussians_per_patch must be a perfect square" assert self.patch_size % self.gaussian_side == 0, \ "patch_size must be divisible by sqrt(gaussians_per_patch)" self.gaussian_stride = self.patch_size // self.gaussian_side self._build_geometry_modules() self._build_appearance_modules() self.geometry_expert = GeometryExpert( pose_regressor=self.pose_regressor, scene_scale=self.scene_scale, inference_mode=self.inference_mode, camera_mode=self.camera_mode, ) self.appearance_expert = AppearanceExpert( image_tokenizer=self.image_tokenizer, stage1=self.stage1, stage2=self.stage2, stage3=self.stage3, merge_block1=self.merge_block1, resize_block1=self.resize_block1, merge_block2=self.merge_block2, resize_block2=self.resize_block2, attention2=self.attention2, attention3=self.attention3, dpt_head=self.dpt_head, gaussian_decoder=self.gaussian_decoder, register_token_init=self.register_token_init, patch_size=self.patch_size, gaussian_side=self.gaussian_side, gaussian_stride=self.gaussian_stride, num_register_tokens=self.num_register_tokens, group_size=self.group_size, color_dim=self.color_dim, opacity_dim=self.opacity_dim, sh_degree=self.sh_degree, opacity_degree=self.opacity_degree, scale_bias=self.scale_bias, scale_max=self.scale_max, opacity_bias=self.opacity_bias, max_dist=self.max_dist, inference_mode=self.inference_mode, dynamic_grouping=self.dynamic_grouping, ) if not self.inference_mode: from src.model.loss import LossComputer self.loss_computer = LossComputer(config) def _build_appearance_modules(self): """Build the appearance-expert modules (names must match checkpoint keys).""" # Marks a state dict as using the depth + image_offset layout. self.register_buffer("_image_offset_version", torch.tensor(1)) self.image_tokenizer = self._create_patch_tokenizer( self.in_channels, self.patch_size, self.dim1, ) self.gaussian_decoder = nn.Sequential( nn.LayerNorm(self.dim3, bias=False), nn.Linear( self.dim3, self.gaussians_per_patch * (1 + 2 + self.color_dim + 3 + 4 + self.opacity_dim), bias=False, ), ) self.stage1 = self._build_transformer_stage(self.dim1, self.stage1_nlayer) self.stage2 = self._build_transformer_stage(self.dim2, self.stage2_nlayer) self.stage3 = self._build_transformer_stage(self.dim3, self.stage3_nlayer) self.register_token_init = nn.Parameter( torch.randn(1, 1, self.num_register_tokens, self.dim1), ) nn.init.normal_(self.register_token_init, mean=0.0, std=0.02) self.attention2 = PropeDotProductAttention( head_dim=self.head_dim, patches_x=self.width // (self.patch_size * 2), patches_y=self.height // (self.patch_size * 2), image_width=self.width, image_height=self.height, num_register_tokens=self.num_register_tokens, ) self.attention3 = PropeDotProductAttention( head_dim=self.head_dim, patches_x=self.width // (self.patch_size * 4), patches_y=self.height // (self.patch_size * 4), image_width=self.width, image_height=self.height, num_register_tokens=self.num_register_tokens, ) self.merge_block1 = nn.Conv2d( self.dim1, self.dim2, kernel_size=2, stride=2, padding=0, bias=True, groups=self.dim1, ) self.resize_block1 = nn.Linear(self.dim1, self.dim2) self.merge_block2 = nn.Conv2d( self.dim2, self.dim3, kernel_size=2, stride=2, padding=0, bias=True, groups=self.dim2, ) self.resize_block2 = nn.Linear(self.dim2, self.dim3) self.dpt_head = DPTHead( dim_in=[self.dim1, self.dim2, self.dim3], features=self.dim3, out_channels=[self.dim1, self.dim2, self.dim3], ) if self.mvp_weights_path != "": checkpoint = torch.load(self.mvp_weights_path, map_location="cpu", weights_only=True) state_dict = checkpoint["ema"] if "ema" in checkpoint else checkpoint result = self.load_state_dict(state_dict, strict=False) print(f"Loaded MVP appearance weights from {self.mvp_weights_path}") def _build_geometry_modules(self): self.pose_regressor = DepthAnything3(model_name=self.da_model_name) if self.da_weights_path != "": state_dict = load_file(self.da_weights_path) self.pose_regressor.load_state_dict(state_dict, strict=False) print(f"Loaded DA3 ({self.da_model_name}) pose regressor weights from {self.da_weights_path}") self.pose_regressor.model.backbone.pretrained.grad_ckpt_every = self.grad_ckpt_every self.pose_regressor.prune_layers() def _build_transformer_stage(self, dim: int, nlayer: int) -> nn.ModuleList: return nn.ModuleList([ TransformerBlock(dim, False, self.head_dim, self.inter_multi, self.qk_norm) for _ in range(nlayer) ]) @staticmethod def _create_patch_tokenizer( in_channels: int, patch_size: int, d_model: int, ) -> nn.Sequential: """Patch embedding: (B, V, C, H, W) -> (B, V*L, d_model).""" return nn.Sequential( Rearrange( "b v c (hh ph) (ww pw) -> b (v hh ww) (ph pw c)", ph=patch_size, pw=patch_size, ), nn.Linear(in_channels * (patch_size ** 2), d_model, bias=False), nn.LayerNorm(d_model, bias=False), ) def train(self, mode: bool = True): """Keeps the loss modules permanently in eval mode.""" super().train(mode) self.appearance_expert.training = mode if not self.inference_mode: self.loss_computer.eval() def _build_raymap_input( self, input_data_dict: dict, i_fxfycxcy: torch.Tensor, i_c2w: torch.Tensor, ): """Construct plucker ray-map images: [ray_origin, ray_dir, originĂ—dir, image].""" h, w = input_data_dict["image"].shape[-2:] with torch.autocast(device_type="cuda", enabled=False): ray_o, ray_d = compute_plucmap(i_fxfycxcy, i_c2w, h, w) o_cross_d = torch.cross(ray_o, ray_d, dim=2) i_normalized_image = input_data_dict["image"] * 2.0 - 1.0 i_raymap_images = torch.concat([ray_o, ray_d, o_cross_d, i_normalized_image], dim=2) Ks = fxfycxcy_to_K(i_fxfycxcy) i_w2c = invert_SE3(i_c2w) return i_raymap_images, Ks, i_w2c def _render_and_compute_loss( self, gaussians: GaussianField, cameras: CameraBundle, target_images: torch.Tensor, ) -> tuple[torch.Tensor, dict]: """Render Gaussians to all target views and compute photometric + pose losses.""" h, w = target_images.shape[-2:] with torch.autocast(device_type="cuda", enabled=False): renderings = GaussianRenderer.apply( gaussians.xyz, gaussians.feature, gaussians.scale, gaussians.rotation, gaussians.opacity_precompute, cameras.pred_t_c2w, cameras.pred_t_fxfycxcy, w, h, self.sh_degree, self.near_plane, self.far_plane, ) renderings = renderings.permute(0, 1, 4, 2, 3).contiguous() # (B, V, 3, H, W) cam_info = { "pred_fxfycxcy": torch.cat([cameras.pred_i_fxfycxcy, cameras.pred_t_fxfycxcy], dim=1), "pred_c2w": torch.cat([cameras.pred_i_c2w, cameras.pred_t_c2w], dim=1), "gt_fxfycxcy": torch.cat([cameras.gt_i_fxfycxcy, cameras.gt_t_fxfycxcy], dim=1), "gt_c2w": torch.cat([cameras.gt_i_c2w, cameras.gt_t_c2w], dim=1), } loss_metrics = self.loss_computer(renderings, target_images, cam_info) return renderings, loss_metrics def _render_target_views( self, gaussians: GaussianField, t_c2w: torch.Tensor, t_fxfycxcy: torch.Tensor, w: int, h: int, ) -> torch.Tensor: """Render each target view sequentially without gradient tracking.""" t = t_c2w.shape[1] xyz = gaussians.xyz[0] feature = gaussians.feature[0] scale = gaussians.scale[0] rotation = gaussians.rotation[0] opacity = gaussians.opacity[0] renderings = [] with torch.no_grad(), torch.autocast(device_type="cuda", enabled=False): for i in range(t): dir = xyz - t_c2w[0, i:i+1, :3, 3][None, ...] opacity_i = _spherical_harmonics( self.opacity_degree, dir, opacity[None, ...], )[0] rendering = GaussianRenderer.render( xyz, feature, scale, rotation, opacity_i, t_c2w[0, i], t_fxfycxcy[0, i], w, h, self.sh_degree, self.near_plane, self.far_plane, ) renderings.append(rendering) renderings = torch.cat(renderings, dim=0)[None, ...] return renderings.permute(0, 1, 4, 2, 3).contiguous() # (1, T, 3, H, W) def _training_forward( self, gaussians: GaussianField, cameras: CameraBundle, target_data_dict: dict, input_data_dict: dict, ) -> edict: """Render and compute training losses; returns edict(input, target, loss_metrics, render).""" renderings, loss_metrics = self._render_and_compute_loss( gaussians, cameras, target_data_dict["image"], ) return edict( input=input_data_dict, target=target_data_dict, loss_metrics=loss_metrics, render=renderings, ) def _inference_forward( self, gaussians: GaussianField, cameras: CameraBundle, target_data_dict: dict, input_data_dict: dict, ) -> edict: """Render target views (after EPA if enabled); returns edict(input, target, render, gaussians, cameras).""" h, w = target_data_dict["image"].shape[-2:] if self.use_pose_optimization: prev_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(True) renderings, _ = self.pose_optimization( gaussians=gaussians, t_c2w=cameras.pred_t_c2w, t_fxfycxcy=cameras.pred_t_fxfycxcy, w=w, h=h, target_images=target_data_dict["image"], ) torch.set_grad_enabled(prev_grad_state) else: renderings = self._render_target_views( gaussians, cameras.pred_t_c2w, cameras.pred_t_fxfycxcy, w, h, ) # gaussians/cameras ride along for scene export; Gaussians live in the normalized camera frame. return edict( input=input_data_dict, target=target_data_dict, render=renderings, gaussians=gaussians, cameras=cameras, ) def forward(self, input_data_dict: dict, target_data_dict: dict) -> edict: """Predict cameras -> build ray maps -> predict Gaussians -> render (mode-dependent edict).""" cameras = self.geometry_expert.predict_cameras(input_data_dict, target_data_dict) raymap_images, Ks, i_w2c = self._build_raymap_input( input_data_dict, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, ) gaussians = self.appearance_expert.predict_gaussians( raymap_images, i_w2c, Ks, cameras.pred_i_fxfycxcy, cameras.pred_i_c2w, cameras.pred_t_c2w, ) if not self.inference_mode: return self._training_forward(gaussians, cameras, target_data_dict, input_data_dict) return self._inference_forward(gaussians, cameras, target_data_dict, input_data_dict) def pose_optimization( self, gaussians: GaussianField, t_c2w: torch.Tensor, t_fxfycxcy: torch.Tensor, w: int, h: int, target_images: torch.Tensor, ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]: """EPA (evaluation-time pose alignment): ``epa_steps`` Adam steps on the target quaternions + translations against an MSE loss, Gaussians frozen. Takes t_c2w (1, T, 4, 4), t_fxfycxcy (1, T, 4), target_images (1, T, 3, H, W); returns (renderings (1, T, 3, H, W), (c2w, fxfycxcy)).""" num_target_views = t_c2w.shape[1] quats = mat_to_quat(t_c2w[..., :3, :3]).clone().detach().requires_grad_(True) trans = t_c2w[..., :3, 3].clone().detach().requires_grad_(True) t_fxfycxcy = t_fxfycxcy.clone().detach() xyz = gaussians.xyz[0].detach() feature = gaussians.feature[0].detach() scale = gaussians.scale[0].detach() rotation = gaussians.rotation[0].detach() opacity = gaussians.opacity[0].detach() optimizer = torch.optim.Adam([quats, trans], lr=1e-4) for _ in range(self.epa_steps): R = quat_to_mat(quats / quats.norm(dim=-1, keepdim=True)) t_c2w_new = torch.zeros_like(t_c2w) t_c2w_new[..., :3, :3] = R t_c2w_new[..., :3, 3] = trans t_c2w_new[..., 3, 3] = 1.0 optimizer.zero_grad() renderings = [] with torch.autocast(device_type="cuda", enabled=False): for i in range(num_target_views): dir = xyz - t_c2w_new[0, i:i+1, :3, 3][None, ...] opacity_i = _spherical_harmonics( self.opacity_degree, dir, opacity[None, ...], )[0] rendering = GaussianRenderer.render( xyz, feature, scale, rotation, opacity_i, t_c2w_new[0, i], t_fxfycxcy[0, i], w, h, self.sh_degree, self.near_plane, self.far_plane, ) renderings.append(rendering) renderings = torch.cat(renderings, dim=0)[None, ...] loss = F.mse_loss(renderings.permute(0, 1, 4, 2, 3).contiguous(), target_images) loss.backward() optimizer.step() refined_c2w = t_c2w_new.detach() refined_fxfycxcy = t_fxfycxcy.detach() return renderings.permute(0, 1, 4, 2, 3).contiguous(), (refined_c2w, refined_fxfycxcy) def _expand_gaussian_head(self, state_dict: dict) -> dict: """Convert a pretrained ``gaussian_decoder`` head to this model's layout: mean-pool a denser patch grid to this sub-grid, replace the xyz rows by depth (mean of xyz) + zero image_offset rows, and zero-pad SH0 to SH1. Converted checkpoints pass through.""" key = "gaussian_decoder.1.weight" w = state_dict.get(key, None) if w is None: return state_dict # Pixel xyz and depth + image_offset both use three rows; the marker tells them apart. image_offset = "_image_offset_version" in state_dict if image_offset and w.shape == self.gaussian_decoder[1].weight.shape: return state_dict rows = w.shape[0] # Accept the target color layout or an SH0 head when upgrading to SH1. color_dims = (self.color_dim, 3) if self.color_dim == 12 else (self.color_dim,) for color_dim_old in color_dims: d_old = 3 + color_dim_old + 3 + 4 + self.opacity_dim p_old = rows // d_old side_old = int(round(p_old ** 0.5)) if (rows % d_old == 0 and side_old > 0 and side_old ** 2 == p_old and side_old % self.gaussian_side == 0): break else: raise ValueError( "Expected a pixel-aligned MVP checkpoint or a marked image-offset " "checkpoint with matching SH degree or SH0 for SH1 conversion; " "legacy world-space offsets require explicit migration." ) k = side_old // self.gaussian_side # Rows are (ph, pw, d) over the pretrained grid; pool each k x k sub-block (k == 1: no-op). w = rearrange( w, "(ah kh aw kw d) D -> ah aw (kh kw) d D", ah=self.gaussian_side, kh=k, aw=self.gaussian_side, kw=k, d=d_old, ).mean(dim=2) position_w, color_w, rest_w = torch.split( w, [3, color_dim_old, 3 + 4 + self.opacity_dim], dim=-2, ) if not image_offset: position_w = torch.cat( [position_w.mean(dim=-2, keepdim=True), torch.zeros_like(position_w[..., :2, :])], dim=-2, ) if color_dim_old != self.color_dim: # Forward stores coefficients per RGB channel: R[SH], G[SH], B[SH]. color_w = rearrange(color_w, "ah aw (c d) D -> ah aw c d D", c=3) color_w = torch.cat( [color_w, color_w.new_zeros(*color_w.shape[:-2], (self.color_dim - color_dim_old) // 3, color_w.shape[-1])], dim=-2, ) color_w = rearrange(color_w, "ah aw c d D -> ah aw (c d) D") w = torch.cat([position_w, color_w, rest_w], dim=-2) # Copy, keeping the _metadata that nn.Module.load_state_dict reads. converted = OrderedDict(state_dict) converted._metadata = getattr(state_dict, "_metadata", OrderedDict()) state_dict = converted state_dict[key] = rearrange(w, "ah aw d D -> (ah aw d) D") state_dict["_image_offset_version"] = self._image_offset_version.clone() print(f"Converted pretrained {key}: {p_old} -> {self.gaussians_per_patch} " f"gaussians/patch, depth + image_offset layout, " f"{color_dim_old} -> {self.color_dim} color channels") return state_dict def load_state_dict(self, state_dict, strict=True, assign=False): """Convert pixel-aligned checkpoints on the fly, then load normally.""" return super().load_state_dict( self._expand_gaussian_head(state_dict), strict=strict, assign=assign, ) @torch.no_grad() def load_ckpt(self, load_path: str) -> int | None: """Load the ``"ema"`` state dict from a .pt file or the last .pt in a directory (strict=False: DA3 weights are loaded separately). Returns 0, or None on failure.""" if os.path.isdir(load_path): ckpt_names = sorted(f for f in os.listdir(load_path) if f.endswith(".pt")) ckpt_path = os.path.join(load_path, ckpt_names[-1]) else: ckpt_path = load_path try: checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=True) except: traceback.print_exc() print(f"Failed to load {ckpt_path}") return None result = self.load_state_dict(checkpoint["ema"], strict=False) print(f"Loaded 2Xplat checkpoint in {ckpt_path}") return 0