| """Scene point cloud encoder. |
| |
| Pipeline: |
| (B, N=8192, ·) per-point MLP → (B, N, d_model) |
| self-attn × n_full (full resolution — every point sees all others) |
| cross-attn × n_pool_cross (n_pool learned slots attend to all N features) |
| self-attn × n_pool (pooled resolution) |
| → (B, n_pool, d_model) for cross-attention in the denoiser. |
| |
| Per-point features include an RGB→16-D MLP branch (v10+): COLMAP points use the |
| COLMAP-stored colour, depth points inherit RGB from their nearest COLMAP |
| neighbour (raw images are not released), and camera tokens get the (0, 0, 0) |
| sentinel. Provenance is already in `type_ids` so no extra has-RGB flag is used. |
| |
| The Perceiver-style cross-attention pool replaces FPS / random subsampling. |
| Every output slot aggregates from the full 8k-point cloud — no points discarded. |
| Cost: O(n_pool × N) per head per cross-attn layer, similar to one full self-attn layer. |
| |
| Full-resolution self-attention uses F.scaled_dot_product_attention (flash backend). |
| |
| Per-slot xyz anchors are picked per-scene by farthest-point sampling restricted |
| to the priority-Gestalt subset of the input cloud. This replaces the previous |
| zero-init learned shared anchor — every scene token now lives at a real, |
| structurally-relevant 3D position in *that* scene, giving the denoiser cross- |
| attention a meaningful spatial signal. |
| """ |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from typing import Tuple |
| from .pos_enc import fourier_3d |
|
|
| POS_ENC_DIM = 48 |
|
|
| |
| |
| |
| _TIER1_GESTALT_IDS_T = (1, 2, 3, 6, 4, 5, 12) |
| _TIER2_GESTALT_IDS_T = (8, 9, 10, 11, 18, 27) |
| _ADE_HOUSE_FOREGROUND_IDS_T = (1, 2, 9, 15, 26, 43, 49, 54) |
|
|
|
|
| @torch.no_grad() |
| def masked_fps(xyz: torch.Tensor, mask: torch.Tensor, n_pool: int) -> torch.Tensor: |
| """Batched farthest-point sampling, preferring `mask=True` points first. |
| |
| Greedy FPS with the priority-aware twist: at every step, only points that |
| are either (a) marked priority, or (b) in a batch that has already |
| exhausted its priority points, are eligible. Distances for non-priority |
| points are tracked throughout, so by the time they become eligible they |
| already hold the correct min-distance to the previously selected set — |
| no second pass. |
| |
| Args: |
| xyz: (B, N, 3) point coordinates. |
| mask: (B, N) bool — True for priority candidates. |
| n_pool: number of indices to select per batch. |
| Returns: |
| (B, n_pool) int64 indices into `xyz`. |
| """ |
| B, N, _ = xyz.shape |
| device = xyz.device |
|
|
| INF = 1e10 |
| NEG = -1e10 |
|
|
| |
| |
| dist = torch.full(xyz.shape[:-1], INF, device=device, dtype=xyz.dtype) |
| indices = torch.zeros(B, n_pool, dtype=torch.long, device=device) |
| batch_idx = torch.arange(B, device=device) |
| n_priority = mask.sum(dim=1) |
|
|
| for k in range(n_pool): |
| priority_remaining = (n_priority > k).unsqueeze(1) |
| eligible = mask | (~priority_remaining) |
|
|
| score = torch.where(eligible, dist, torch.full_like(dist, NEG)) |
| if k == 0: |
| |
| |
| score = score + torch.rand_like(score) * 1e-3 |
| farthest = score.argmax(dim=1) |
| indices[:, k] = farthest |
|
|
| last_xyz = xyz[batch_idx, farthest] |
| new_dist = (xyz - last_xyz[:, None, :]).norm(dim=-1) |
| dist = torch.minimum(dist, new_dist) |
| dist[batch_idx, farthest] = NEG |
|
|
| return indices |
|
|
| class _MHA(nn.Module): |
| """Multi-head self-attention via F.scaled_dot_product_attention (flash backend).""" |
|
|
| def __init__(self, d_model: int, n_heads: int): |
| super().__init__() |
| assert d_model % n_heads == 0 |
| self.n_heads = n_heads |
| self.d_head = d_model // n_heads |
| self.q_proj = nn.Linear(d_model, d_model) |
| self.k_proj = nn.Linear(d_model, d_model) |
| self.v_proj = nn.Linear(d_model, d_model) |
| self.out_proj = nn.Linear(d_model, d_model) |
|
|
| def _shape(self, x: torch.Tensor) -> torch.Tensor: |
| B, L, _ = x.shape |
| return x.view(B, L, self.n_heads, self.d_head).transpose(1, 2) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| q = self._shape(self.q_proj(x)) |
| k = self._shape(self.k_proj(x)) |
| v = self._shape(self.v_proj(x)) |
| out = F.scaled_dot_product_attention(q, k, v) |
| B, H, L, D = out.shape |
| out = out.transpose(1, 2).contiguous().view(B, L, H * D) |
| return self.out_proj(out) |
|
|
|
|
| class _SelfAttnBlock(nn.Module): |
| def __init__(self, d_model: int, n_heads: int, d_ff: int): |
| super().__init__() |
| self.norm1 = nn.LayerNorm(d_model) |
| self.norm2 = nn.LayerNorm(d_model) |
| self.attn = _MHA(d_model, n_heads) |
| self.ff = nn.Sequential( |
| nn.Linear(d_model, d_ff), |
| nn.GELU(), |
| nn.Linear(d_ff, d_model), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| x = x + self.attn(self.norm1(x)) |
| x = x + self.ff(self.norm2(x)) |
| return x |
|
|
|
|
| class _CrossAttnBlock(nn.Module): |
| """Single cross-attention layer: query slots attend to scene tokens. |
| |
| Queries (the learned slots) are normalised before attention; key/value |
| (scene features) are normalised independently. A feed-forward follows. |
| """ |
|
|
| def __init__(self, d_model: int, n_heads: int, d_ff: int): |
| super().__init__() |
| self.norm_q = nn.LayerNorm(d_model) |
| self.norm_kv = nn.LayerNorm(d_model) |
| self.norm_ff = nn.LayerNorm(d_model) |
| self.attn = _MHA(d_model, n_heads) |
| self.ff = nn.Sequential( |
| nn.Linear(d_model, d_ff), |
| nn.GELU(), |
| nn.Linear(d_ff, d_model), |
| ) |
|
|
| def forward(self, q: torch.Tensor, kv: torch.Tensor) -> torch.Tensor: |
| |
| |
| |
| nq = self.norm_q(q) |
| nkv = self.norm_kv(kv) |
| B, Lq, _ = nq.shape |
| B, Lkv, _ = nkv.shape |
|
|
| def _shape(x, L): |
| return x.view(B, L, self.attn.n_heads, self.attn.d_head).transpose(1, 2) |
|
|
| qh = _shape(self.attn.q_proj(nq), Lq) |
| kh = _shape(self.attn.k_proj(nkv), Lkv) |
| vh = _shape(self.attn.v_proj(nkv), Lkv) |
| out = F.scaled_dot_product_attention(qh, kh, vh) |
| out = out.transpose(1, 2).contiguous().view(B, Lq, -1) |
| q = q + self.attn.out_proj(out) |
| q = q + self.ff(self.norm_ff(q)) |
| return q |
|
|
|
|
| class SceneEncoder(nn.Module): |
| """Per-point MLP → full self-attn → cross-attn pool → pooled self-attn. |
| |
| Pooling uses n_pool learned query slots (Perceiver-style) that cross-attend |
| to all N scene features — no points are discarded. |
| """ |
|
|
| N_TYPES = 3 |
| N_GESTALT = 29 |
| N_ADE20K = 151 |
|
|
| RGB_DIM = 16 |
|
|
| def __init__( |
| self, |
| d_model: int = 256, |
| n_heads: int = 8, |
| d_ff: int = 1024, |
| n_full_layers: int = 2, |
| n_pool: int = 1024, |
| n_pool_cross_layers: int = 1, |
| n_pool_layers: int = 2, |
| use_rgb: bool = True, |
| predict_query_xyz: bool = False, |
| n_query: int = 64, |
| ): |
| super().__init__() |
| self.d_model = d_model |
| self.n_pool = n_pool |
| self.use_rgb = use_rgb |
| self.predict_query_xyz = bool(predict_query_xyz) |
| self.n_query = int(n_query) |
|
|
| self.type_emb = nn.Embedding(self.N_TYPES, 16) |
| |
| |
| |
| |
| self.gestalt_emb = nn.Embedding(self.N_GESTALT, 12) |
| self.ade_emb = nn.Embedding(self.N_ADE20K, 8) |
|
|
| |
| |
| |
| if self.use_rgb: |
| self.rgb_mlp = nn.Sequential( |
| nn.Linear(3, self.RGB_DIM), |
| nn.GELU(), |
| nn.Linear(self.RGB_DIM, self.RGB_DIM), |
| ) |
|
|
| |
| in_dim = 3 + POS_ENC_DIM + 16 + (12 + 12) + 8 + 3 |
| if self.use_rgb: |
| in_dim += self.RGB_DIM |
| self.point_mlp = nn.Sequential( |
| nn.Linear(in_dim, d_model), |
| nn.LayerNorm(d_model), |
| nn.GELU(), |
| nn.Linear(d_model, d_model), |
| nn.LayerNorm(d_model), |
| ) |
|
|
| self.full_blocks = nn.ModuleList( |
| [_SelfAttnBlock(d_model, n_heads, d_ff) for _ in range(n_full_layers)] |
| ) |
|
|
| |
| |
| |
| |
| self.pool_queries = nn.Parameter(torch.randn(1, n_pool, d_model) * 0.02) |
| self.anchor_pos_proj = nn.Linear(POS_ENC_DIM, d_model) |
|
|
| |
| tier1 = torch.tensor(_TIER1_GESTALT_IDS_T, dtype=torch.long) |
| tier2 = torch.tensor(_TIER2_GESTALT_IDS_T, dtype=torch.long) |
| house_ade = torch.tensor(_ADE_HOUSE_FOREGROUND_IDS_T, dtype=torch.long) |
| self.register_buffer("tier1_ids", tier1, persistent=False) |
| self.register_buffer("tier2_ids", tier2, persistent=False) |
| self.register_buffer("house_ade_ids", house_ade, persistent=False) |
|
|
| self.pool_cross_blocks = nn.ModuleList( |
| [_CrossAttnBlock(d_model, n_heads, d_ff) for _ in range(n_pool_cross_layers)] |
| ) |
| self.pool_blocks = nn.ModuleList( |
| [_SelfAttnBlock(d_model, n_heads, d_ff) for _ in range(n_pool_layers)] |
| ) |
| self.out_norm = nn.LayerNorm(d_model) |
|
|
| |
| |
| |
| |
| |
| if self.predict_query_xyz: |
| self.query_embed = nn.Parameter( |
| torch.randn(1, self.n_query, d_model) * 0.02 |
| ) |
| self.query_cross = _CrossAttnBlock(d_model, n_heads, d_ff) |
| self.query_self = _SelfAttnBlock(d_model, n_heads, d_ff) |
| self.query_norm = nn.LayerNorm(d_model) |
| self.query_xyz_head = nn.Linear(d_model, 3) |
| nn.init.zeros_(self.query_xyz_head.weight) |
| nn.init.zeros_(self.query_xyz_head.bias) |
|
|
| def forward( |
| self, |
| xyz: torch.Tensor, |
| type_ids: torch.Tensor, |
| gestalt_ids: torch.Tensor, |
| ade_ids: torch.Tensor, |
| gestalt_id2: torch.Tensor = None, |
| gestalt_w1: torch.Tensor = None, |
| scene_geom_conf: torch.Tensor = None, |
| scene_sem_conf: torch.Tensor = None, |
| scene_rgb: torch.Tensor = None, |
| ) -> Tuple[torch.Tensor, ...]: |
| |
| |
| |
| |
| |
| |
| B, N, _ = xyz.shape |
|
|
| |
| pos = fourier_3d(xyz, POS_ENC_DIM) |
| t_e = self.type_emb(type_ids) |
|
|
| |
| |
| |
| if gestalt_id2 is None: |
| gestalt_id2 = torch.full_like(gestalt_ids, -1) |
| g1_safe = gestalt_ids.masked_fill(gestalt_ids < 0, self.N_GESTALT - 1) |
| g2_safe = gestalt_id2.masked_fill(gestalt_id2 < 0, self.N_GESTALT - 1) |
| g_e1 = self.gestalt_emb(g1_safe) |
| g_e2 = self.gestalt_emb(g2_safe) |
| gestalt_feat = torch.cat([g_e1, g_e2], dim=-1) |
|
|
| a_e = self.ade_emb(ade_ids) |
|
|
| if gestalt_w1 is None: |
| gestalt_w1 = torch.ones(B, N, device=xyz.device, dtype=xyz.dtype) |
| if scene_geom_conf is None: |
| scene_geom_conf = torch.ones(B, N, device=xyz.device, dtype=xyz.dtype) |
| if scene_sem_conf is None: |
| scene_sem_conf = torch.ones(B, N, device=xyz.device, dtype=xyz.dtype) |
| scalars = torch.stack([ |
| scene_geom_conf.to(xyz.dtype), |
| scene_sem_conf.to(xyz.dtype), |
| gestalt_w1.to(xyz.dtype).clamp(0.0, 1.0), |
| ], dim=-1) |
|
|
| feats = [xyz, pos, t_e, gestalt_feat, a_e, scalars] |
| if self.use_rgb: |
| if scene_rgb is None: |
| rgb_in = torch.zeros(B, N, 3, device=xyz.device, dtype=xyz.dtype) |
| else: |
| |
| rgb_in = scene_rgb.to(xyz.dtype) / 127.5 - 1.0 |
| feats.append(self.rgb_mlp(rgb_in)) |
|
|
| x = self.point_mlp(torch.cat(feats, dim=-1)) |
|
|
| |
| for block in self.full_blocks: |
| x = block(x) |
|
|
| |
| |
| |
| tier1_mask = (gestalt_ids[..., None] == self.tier1_ids).any(dim=-1) |
| tier2_mask = (gestalt_ids[..., None] == self.tier2_ids).any(dim=-1) |
| house_mask = (ade_ids[..., None] == self.house_ade_ids).any(dim=-1) |
| priority_mask = tier1_mask | (tier2_mask & house_mask) |
| anchor_idx = masked_fps(xyz, priority_mask, self.n_pool) |
| xyz_pooled = torch.gather( |
| xyz, 1, anchor_idx.unsqueeze(-1).expand(-1, -1, 3) |
| ) |
|
|
| |
| |
| |
| |
| |
| q = self.pool_queries.expand(B, -1, -1) |
| q = q + self.anchor_pos_proj(fourier_3d(xyz_pooled, POS_ENC_DIM)) |
| for block in self.pool_cross_blocks: |
| q = block(q, x) |
|
|
| |
| for block in self.pool_blocks: |
| q = block(q) |
|
|
| scene_feats = self.out_norm(q) |
|
|
| if not self.predict_query_xyz: |
| return scene_feats, xyz_pooled |
|
|
| |
| |
| |
| qv = self.query_embed.expand(B, -1, -1) |
| qv = self.query_cross(qv, scene_feats) |
| qv = self.query_self(qv) |
| qv = self.query_norm(qv) |
| query_xyz = self.query_xyz_head(qv) |
|
|
| return scene_feats, xyz_pooled, query_xyz |
|
|