ffgs 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. ffgs/__init__.py +39 -0
  2. ffgs/dynamic.py +239 -0
  3. ffgs/geometry.py +418 -0
  4. ffgs/image.py +204 -0
  5. ffgs/models/__init__.py +2 -0
  6. ffgs/models/anysplat/LICENSE +1120 -0
  7. ffgs/models/anysplat/__init__.py +49 -0
  8. ffgs/models/anysplat/encoder.py +372 -0
  9. ffgs/models/anysplat/evaluation.py +98 -0
  10. ffgs/models/anysplat/gaussian_adapter.py +96 -0
  11. ffgs/models/anysplat/gs_head.py +166 -0
  12. ffgs/models/anysplat/modeling.py +95 -0
  13. ffgs/models/anysplat/processing.py +249 -0
  14. ffgs/models/anysplat/vggt/__init__.py +0 -0
  15. ffgs/models/anysplat/vggt/heads/__init__.py +0 -0
  16. ffgs/models/anysplat/vggt/heads/camera_head.py +181 -0
  17. ffgs/models/anysplat/vggt/heads/dpt_head.py +612 -0
  18. ffgs/models/anysplat/vggt/heads/head_act.py +134 -0
  19. ffgs/models/anysplat/vggt/heads/utils.py +126 -0
  20. ffgs/models/anysplat/vggt/layers/__init__.py +28 -0
  21. ffgs/models/anysplat/vggt/layers/attention.py +92 -0
  22. ffgs/models/anysplat/vggt/layers/block.py +157 -0
  23. ffgs/models/anysplat/vggt/layers/drop_path.py +42 -0
  24. ffgs/models/anysplat/vggt/layers/layer_scale.py +32 -0
  25. ffgs/models/anysplat/vggt/layers/mlp.py +45 -0
  26. ffgs/models/anysplat/vggt/layers/patch_embed.py +106 -0
  27. ffgs/models/anysplat/vggt/layers/rope.py +211 -0
  28. ffgs/models/anysplat/vggt/layers/swiglu_ffn.py +77 -0
  29. ffgs/models/anysplat/vggt/layers/vision_transformer.py +463 -0
  30. ffgs/models/anysplat/vggt/models/__init__.py +0 -0
  31. ffgs/models/anysplat/vggt/models/aggregator.py +398 -0
  32. ffgs/models/anysplat/vggt/utils/__init__.py +0 -0
  33. ffgs/models/anysplat/vggt/utils/geometry.py +230 -0
  34. ffgs/models/anysplat/vggt/utils/pose_enc.py +144 -0
  35. ffgs/models/anysplat/vggt/utils/rotation.py +148 -0
  36. ffgs/models/tokengs/__init__.py +43 -0
  37. ffgs/models/tokengs/activations.py +135 -0
  38. ffgs/models/tokengs/attention.py +189 -0
  39. ffgs/models/tokengs/enc_dec.py +323 -0
  40. ffgs/models/tokengs/modeling.py +173 -0
  41. ffgs/models/tokengs/processing.py +339 -0
  42. ffgs/pipeline.py +341 -0
  43. ffgs/processor.py +236 -0
  44. ffgs/registry.py +132 -0
  45. ffgs/render.py +111 -0
  46. ffgs/types.py +291 -0
  47. ffgs/zoo/__init__.py +234 -0
  48. ffgs/zoo/anysplat/default.json +34 -0
  49. ffgs/zoo/tokengs/dl3dv-2v.json +29 -0
  50. ffgs/zoo/tokengs/dl3dv-4v.json +29 -0
  51. ffgs/zoo/tokengs/dl3dv-6v.json +29 -0
  52. ffgs/zoo/tokengs/dl3dv-base.json +29 -0
  53. ffgs/zoo/tokengs/dl3dv-latent-2v-lpips.json +48 -0
  54. ffgs/zoo/tokengs/dl3dv-latent-2v-ssim.json +48 -0
  55. ffgs/zoo/tokengs/dl3dv-latent-4v-lpips.json +48 -0
  56. ffgs/zoo/tokengs/dl3dv-latent-4v-ssim.json +48 -0
  57. ffgs/zoo/tokengs/dl3dv-latent-6v-lpips.json +48 -0
  58. ffgs/zoo/tokengs/dl3dv-latent-6v-ssim.json +48 -0
  59. ffgs-0.1.0.dev0.dist-info/METADATA +320 -0
  60. ffgs-0.1.0.dev0.dist-info/RECORD +63 -0
  61. ffgs-0.1.0.dev0.dist-info/WHEEL +4 -0
  62. ffgs-0.1.0.dev0.dist-info/licenses/LICENSE +237 -0
  63. ffgs-0.1.0.dev0.dist-info/licenses/src/ffgs/models/anysplat/LICENSE +1120 -0
@@ -0,0 +1,323 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Modified by the libgs contributors, 2026, from tokengs/models/enc_dec.py of
17
+ # https://github.com/nv-tlabs/TokenGS (b16269c): inference-only subset. Takes a
18
+ # `TokenGSConfig` instead of the training `Options`; einops replaced by the
19
+ # equivalent reshape / permute; dynamic Gaussian tokens (block-causal
20
+ # FlexAttention) removed. Parameter names are unchanged.
21
+
22
+ """TokenGS encoder-decoder backbone: ViT encoder, optional latent bottleneck, and a
23
+ decoder whose Gaussian tokens cross-attend to the encoder output."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+ import torch
30
+ import torch.nn as nn
31
+ import torch.nn.functional as F
32
+
33
+ from .attention import Attention, Block, LayerScale, Mlp
34
+
35
+
36
+ @dataclass
37
+ class TokenGSConfig:
38
+ """Architecture of a TokenGS checkpoint (the model part of the upstream
39
+ `Options`, same names and defaults)."""
40
+
41
+ patch_size: int = 8
42
+ dec_patch_size: int | None = None
43
+ enc_depth: int = 3
44
+ dec_depth: int = 12
45
+ enc_embed_dim: int = 1024
46
+ enc_num_heads: int = 16
47
+ mlp_ratio: float = 4
48
+ clip_head_readout_std: float = 0.002
49
+ clip_head_z_init: float | None = None
50
+ dec_init_values: float | None = None
51
+ gaussian_scale_cap: float = 0.075
52
+ opacity_bias: float = 2.0
53
+ gaussian_z_offset: float = 1.0
54
+ num_gs_tokens: int = 1024
55
+ token_dim: int = 1024
56
+ gs_token_std: float = 1e-2
57
+ use_multiscale_encoder: bool = False
58
+ multiscale_encoder_layers: tuple[int, ...] = (5, 7, 9, 11)
59
+ use_latent_bottleneck: bool = False
60
+ num_latents: int = 4096
61
+ latent_cross_attn_depth: int = 12
62
+
63
+ def __post_init__(self) -> None:
64
+ if self.dec_patch_size is None:
65
+ self.dec_patch_size = self.patch_size
66
+ self.multiscale_encoder_layers = tuple(self.multiscale_encoder_layers)
67
+
68
+
69
+ def _split_kv(x: torch.Tensor, num_heads: int) -> tuple[torch.Tensor, torch.Tensor]:
70
+ """ "b n (kv h c) -> kv b h n c", unbound into (k, v)."""
71
+ b, n, _ = x.shape
72
+ x = x.reshape(b, n, 2, num_heads, -1).permute(2, 0, 3, 1, 4)
73
+ return x[0], x[1]
74
+
75
+
76
+ class DecoderBlock(nn.Module):
77
+ """
78
+ Decoder block for encoder-decoder architecture.
79
+ Works like a transformer decoder layer except the keys/values are provided by
80
+ the encoder (already normalized).
81
+ """
82
+
83
+ class SelfAttnBlock(nn.Module):
84
+ def __init__(self, dim: int, num_heads: int, qkv_bias: bool, qk_norm: bool):
85
+ super().__init__()
86
+ self.norm = nn.LayerNorm(dim)
87
+ self.gs_self_attn = Attention(
88
+ dim, num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm
89
+ )
90
+
91
+ def forward(self, gs_tokens: torch.Tensor) -> torch.Tensor:
92
+ queries_normed = self.norm(gs_tokens)
93
+ return self.gs_self_attn(queries_normed)
94
+
95
+ class CrossAttnBlock(nn.Module):
96
+ def __init__(self, dim: int, num_heads: int, qkv_bias: bool, q_norm: bool):
97
+ super().__init__()
98
+ self.num_heads = num_heads
99
+ self.gs_token_norm = nn.LayerNorm(dim)
100
+ self.q_norm = nn.LayerNorm(dim // num_heads) if q_norm else nn.Identity()
101
+ self.q_proj = nn.Linear(dim, dim, bias=qkv_bias)
102
+ self.out_proj = nn.Linear(dim, dim)
103
+
104
+ def forward(
105
+ self, gs_tokens: torch.Tensor, keys: torch.Tensor, values: torch.Tensor
106
+ ) -> torch.Tensor:
107
+ gs_tokens_normed = self.gs_token_norm(gs_tokens)
108
+ q = self.q_proj(gs_tokens_normed)
109
+ b, n, _ = q.shape
110
+ # "b n (h d) -> b h n d"
111
+ q = q.reshape(b, n, self.num_heads, -1).permute(0, 2, 1, 3)
112
+ q = self.q_norm(q)
113
+
114
+ cross_attn_output = F.scaled_dot_product_attention(q, keys, values)
115
+ # "b h n d -> b n (h d)"
116
+ cross_attn_output = cross_attn_output.permute(0, 2, 1, 3).reshape(b, n, -1)
117
+ return self.out_proj(cross_attn_output)
118
+
119
+ class MlpBlock(nn.Module):
120
+ def __init__(self, dim: int, mlp_ratio: float, ffn_bias: bool):
121
+ super().__init__()
122
+ self.norm = nn.LayerNorm(dim)
123
+ self.mlp = Mlp(dim, int(dim * mlp_ratio), dim, bias=ffn_bias)
124
+
125
+ def forward(self, gs_tokens: torch.Tensor) -> torch.Tensor:
126
+ return self.mlp(self.norm(gs_tokens))
127
+
128
+ def __init__(
129
+ self,
130
+ dim: int,
131
+ num_heads: int,
132
+ mlp_ratio: float,
133
+ qkv_bias: bool,
134
+ ffn_bias: bool,
135
+ qk_norm: bool,
136
+ init_values: float | None = None,
137
+ ):
138
+ super().__init__()
139
+
140
+ def make_scale() -> nn.Module:
141
+ return (
142
+ LayerScale(dim, init_values=init_values)
143
+ if init_values
144
+ else nn.Identity()
145
+ )
146
+
147
+ self.gs_self_attn = DecoderBlock.SelfAttnBlock(
148
+ dim, num_heads, qkv_bias, qk_norm=qk_norm
149
+ )
150
+ self.gs_self_attn_scale = make_scale()
151
+
152
+ self.gs_cross_attn = DecoderBlock.CrossAttnBlock(
153
+ dim, num_heads, qkv_bias, q_norm=qk_norm
154
+ )
155
+ self.gs_cross_attn_scale = make_scale()
156
+
157
+ self.mlp = DecoderBlock.MlpBlock(dim, mlp_ratio, ffn_bias)
158
+ self.mlp_scale = make_scale()
159
+
160
+ def forward(
161
+ self, gs_tokens: torch.Tensor, keys: torch.Tensor, values: torch.Tensor
162
+ ) -> torch.Tensor:
163
+ gs_tokens = gs_tokens + self.gs_cross_attn_scale(
164
+ self.gs_cross_attn(gs_tokens, keys, values)
165
+ )
166
+ gs_tokens = gs_tokens + self.gs_self_attn_scale(self.gs_self_attn(gs_tokens))
167
+ gs_tokens = gs_tokens + self.mlp_scale(self.mlp(gs_tokens))
168
+
169
+ return gs_tokens
170
+
171
+
172
+ class EncDecBackbone(nn.Module):
173
+ """
174
+ An encoder-decoder backbone for the EncDec architecture.
175
+
176
+ Encoder: a stack of ViT blocks which produce a latent representation. This is
177
+ followed by a key-value projection to produce a key and value for the decoder.
178
+
179
+ Decoder: a stack of transformer decoder layers which attend from GS tokens to
180
+ the encoder output and among themselves.
181
+ """
182
+
183
+ def __init__(self, opt: TokenGSConfig):
184
+ super().__init__()
185
+
186
+ self.opt = opt
187
+
188
+ self.encoder = nn.Sequential(
189
+ *[
190
+ Block(
191
+ self.opt.enc_embed_dim,
192
+ self.opt.enc_num_heads,
193
+ self.opt.mlp_ratio,
194
+ qkv_bias=True,
195
+ proj_bias=True,
196
+ ffn_bias=True,
197
+ init_values=0.01,
198
+ qk_norm=True,
199
+ )
200
+ for _ in range(self.opt.enc_depth)
201
+ ]
202
+ )
203
+
204
+ self.use_multiscale = self.opt.use_multiscale_encoder
205
+ self.use_latent_bottleneck = self.opt.use_latent_bottleneck
206
+
207
+ if self.use_multiscale:
208
+ self.multiscale_layers = self.opt.multiscale_encoder_layers
209
+ self.multiscale_norms = nn.ModuleList(
210
+ [nn.LayerNorm(self.opt.enc_embed_dim) for _ in self.multiscale_layers]
211
+ )
212
+ encoder_feature_dim = self.opt.enc_embed_dim * len(self.multiscale_layers)
213
+ else:
214
+ self.encoder_norm = nn.LayerNorm(self.opt.enc_embed_dim)
215
+ encoder_feature_dim = self.opt.enc_embed_dim
216
+
217
+ if not self.use_latent_bottleneck:
218
+ self.kv_proj = nn.Linear(
219
+ encoder_feature_dim, self.opt.enc_embed_dim * 2, bias=True
220
+ )
221
+
222
+ self.k_proj_norm = nn.LayerNorm(
223
+ self.opt.enc_embed_dim // self.opt.enc_num_heads
224
+ )
225
+
226
+ if self.use_latent_bottleneck:
227
+ self.latents = nn.Parameter(
228
+ torch.randn(self.opt.num_latents, self.opt.enc_embed_dim) * 0.02
229
+ )
230
+
231
+ self.latent_feature_kv_proj = nn.Linear(
232
+ encoder_feature_dim, self.opt.enc_embed_dim * 2, bias=True
233
+ )
234
+ self.latent_feature_k_norm = nn.LayerNorm(
235
+ self.opt.enc_embed_dim // self.opt.enc_num_heads
236
+ )
237
+ self.latent_blocks = nn.ModuleList(
238
+ [
239
+ DecoderBlock(
240
+ self.opt.enc_embed_dim,
241
+ self.opt.enc_num_heads,
242
+ self.opt.mlp_ratio,
243
+ qkv_bias=True,
244
+ ffn_bias=True,
245
+ init_values=0.01,
246
+ qk_norm=True,
247
+ )
248
+ for _ in range(self.opt.latent_cross_attn_depth)
249
+ ]
250
+ )
251
+ self.latent_kv_proj = nn.Linear(
252
+ self.opt.enc_embed_dim, self.opt.enc_embed_dim * 2, bias=True
253
+ )
254
+
255
+ self.decoder_blocks = nn.ModuleList(
256
+ [
257
+ DecoderBlock(
258
+ self.opt.enc_embed_dim,
259
+ self.opt.enc_num_heads,
260
+ self.opt.mlp_ratio,
261
+ qkv_bias=True,
262
+ ffn_bias=True,
263
+ init_values=(
264
+ self.opt.dec_init_values
265
+ if self.opt.dec_init_values is not None
266
+ else 5e-3 * self.opt.gs_token_std
267
+ ),
268
+ qk_norm=True,
269
+ )
270
+ for _ in range(self.opt.dec_depth)
271
+ ]
272
+ )
273
+
274
+ def _encode_features(self, image_features: torch.Tensor) -> torch.Tensor:
275
+ if self.use_multiscale:
276
+ x = image_features
277
+ features = []
278
+ for i, block in enumerate(self.encoder):
279
+ x = block(x)
280
+ if i in self.multiscale_layers:
281
+ features.append(self.multiscale_norms[len(features)](x))
282
+ return torch.cat(features, dim=-1)
283
+ image_features = self.encoder(image_features)
284
+ return self.encoder_norm(image_features)
285
+
286
+ def _features_to_kv(
287
+ self, image_features: torch.Tensor
288
+ ) -> tuple[torch.Tensor, torch.Tensor]:
289
+ image_feature_keys, image_feature_values = _split_kv(
290
+ self.kv_proj(image_features), self.opt.enc_num_heads
291
+ )
292
+ image_feature_keys = self.k_proj_norm(image_feature_keys)
293
+ return image_feature_keys, image_feature_values
294
+
295
+ def _features_to_scene_latents(self, image_features: torch.Tensor) -> torch.Tensor:
296
+ feat_keys, feat_values = _split_kv(
297
+ self.latent_feature_kv_proj(image_features), self.opt.enc_num_heads
298
+ )
299
+ feat_keys = self.latent_feature_k_norm(feat_keys)
300
+
301
+ latents = self.latents.unsqueeze(0).expand(image_features.shape[0], -1, -1)
302
+ for block in self.latent_blocks:
303
+ latents = block(gs_tokens=latents, keys=feat_keys, values=feat_values)
304
+
305
+ return latents
306
+
307
+ def _latents_to_kv(
308
+ self, latents: torch.Tensor
309
+ ) -> tuple[torch.Tensor, torch.Tensor]:
310
+ latent_keys, latent_values = _split_kv(
311
+ self.latent_kv_proj(latents), self.opt.enc_num_heads
312
+ )
313
+ latent_keys = self.k_proj_norm(latent_keys)
314
+ return latent_keys, latent_values
315
+
316
+ def _encode_to_kv(
317
+ self, image_features: torch.Tensor
318
+ ) -> tuple[torch.Tensor, torch.Tensor]:
319
+ image_features = self._encode_features(image_features)
320
+ if self.use_latent_bottleneck:
321
+ latents = self._features_to_scene_latents(image_features)
322
+ return self._latents_to_kv(latents)
323
+ return self._features_to_kv(image_features)
@@ -0,0 +1,173 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Modified by the libgs contributors, 2026, from tokengs/models/tokengs.py of
17
+ # https://github.com/nv-tlabs/TokenGS (b16269c): the feed-forward inference path
18
+ # (`forward_reconstruction`) as a `PyTorchModelHubMixin` module built from keyword
19
+ # arguments. Removed the renderer, losses, LPIPS, test-time tuning, and time /
20
+ # dynamic-token conditioning; einops replaced by the equivalent reshape / permute.
21
+ # Parameter names are unchanged, so upstream checkpoints load as they are.
22
+
23
+ """The TokenGS network: input views -> Gaussians in the model frame."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import fields
28
+ from functools import partial
29
+
30
+ import torch
31
+ import torch.nn as nn
32
+ from huggingface_hub import PyTorchModelHubMixin
33
+
34
+ from .activations import ClipActivationHead
35
+ from .attention import PatchEmbed
36
+ from .enc_dec import EncDecBackbone, TokenGSConfig
37
+
38
+ _CONFIG_FIELDS = frozenset(f.name for f in fields(TokenGSConfig))
39
+
40
+
41
+ class TokenGS(nn.Module, PyTorchModelHubMixin):
42
+ """
43
+ TokenGS model with encoder-decoder architecture: learnable Gaussian tokens
44
+ cross-attend to the encoded input views and decode to 3D Gaussians.
45
+
46
+ Input (from `TokenGSProcessor`): {"images": [B, V, 3, H, W] ImageNet-normalised
47
+ RGB, "plucker": [B, V, 6, H, W]}. Output: Gaussians [B, N, 14] in the model
48
+ frame -- xyz, opacity, scale, rotation (wxyz), rgb -- with
49
+ N = num_gs_tokens * dec_patch_size ** 2. The arguments are those of
50
+ `TokenGSConfig`.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ patch_size: int = 8,
56
+ dec_patch_size: int | None = None,
57
+ enc_depth: int = 3,
58
+ dec_depth: int = 12,
59
+ enc_embed_dim: int = 1024,
60
+ enc_num_heads: int = 16,
61
+ mlp_ratio: float = 4,
62
+ clip_head_readout_std: float = 0.002,
63
+ clip_head_z_init: float | None = None,
64
+ dec_init_values: float | None = None,
65
+ gaussian_scale_cap: float = 0.075,
66
+ opacity_bias: float = 2.0,
67
+ gaussian_z_offset: float = 1.0,
68
+ num_gs_tokens: int = 1024,
69
+ token_dim: int = 1024,
70
+ gs_token_std: float = 1e-2,
71
+ use_multiscale_encoder: bool = False,
72
+ multiscale_encoder_layers: tuple[int, ...] | list[int] = (5, 7, 9, 11),
73
+ use_latent_bottleneck: bool = False,
74
+ num_latents: int = 4096,
75
+ latent_cross_attn_depth: int = 12,
76
+ ):
77
+ arguments = {k: v for k, v in locals().items() if k in _CONFIG_FIELDS}
78
+ super().__init__()
79
+ self.opt = TokenGSConfig(**arguments)
80
+
81
+ norm_layer_factory = partial(nn.LayerNorm, bias=True)
82
+
83
+ self.patch_embed = PatchEmbed(
84
+ patch_size=self.opt.patch_size,
85
+ in_chans=3,
86
+ embed_dim=self.opt.enc_embed_dim,
87
+ norm_layer=norm_layer_factory,
88
+ )
89
+
90
+ self.patch_plucker_embed = PatchEmbed(
91
+ patch_size=self.opt.patch_size,
92
+ in_chans=6,
93
+ embed_dim=self.opt.enc_embed_dim,
94
+ norm_layer=norm_layer_factory,
95
+ )
96
+
97
+ # Encoder-decoder architecture
98
+ self.enc_dec_backbone = EncDecBackbone(self.opt)
99
+
100
+ # Activation head (always clip)
101
+ self.activation_head = ClipActivationHead(self.opt)
102
+
103
+ # Learnable GS tokens
104
+ self.gs_tokens = nn.Parameter(
105
+ self.opt.gs_token_std
106
+ * torch.randn(self.opt.num_gs_tokens, self.opt.token_dim)
107
+ )
108
+
109
+ def _embed_encoder_input(
110
+ self, images_rgb: torch.Tensor, plucker: torch.Tensor
111
+ ) -> torch.Tensor:
112
+ B, V, _, H, W = images_rgb.shape
113
+ height = int(H // self.opt.patch_size)
114
+ width = int(W // self.opt.patch_size)
115
+
116
+ assert (
117
+ height * self.opt.patch_size == H
118
+ ), f"H={H} must be divisible by patch_size={self.opt.patch_size}"
119
+ assert (
120
+ width * self.opt.patch_size == W
121
+ ), f"W={W} must be divisible by patch_size={self.opt.patch_size}"
122
+
123
+ # Reshape for tokenization
124
+ images_rgb_reshaped = images_rgb.reshape(B * V, 3, H, W)
125
+ plucker_reshaped = plucker.reshape(B * V, 6, H, W)
126
+
127
+ # Embed RGB patches
128
+ x = self.patch_embed(images_rgb_reshaped)
129
+ # Add Plucker embeddings
130
+ x_plucker_emb = self.patch_plucker_embed(plucker_reshaped)
131
+ x = x + x_plucker_emb # B*V, N, C
132
+
133
+ # "(b v) n c -> b (v n) c": one sequence per batch
134
+ return x.reshape(B, V * x.shape[1], x.shape[2])
135
+
136
+ def forward_encoder(
137
+ self, images_rgb: torch.Tensor, plucker: torch.Tensor
138
+ ) -> tuple[torch.Tensor, torch.Tensor]:
139
+ """Encode input views into keys and values for the decoder's
140
+ cross-attention, each [B, heads, tokens, head_dim]."""
141
+ x = self._embed_encoder_input(images_rgb, plucker)
142
+ return self.enc_dec_backbone._encode_to_kv(x)
143
+
144
+ def get_gs_tokens(self, batch_size: int) -> torch.Tensor:
145
+ """Initial GS tokens [B, num_gs_tokens, C]."""
146
+ return self.gs_tokens.unsqueeze(0).expand(batch_size, -1, -1).clone()
147
+
148
+ def forward_decoder(
149
+ self,
150
+ keys: torch.Tensor,
151
+ values: torch.Tensor,
152
+ gs_tokens: torch.Tensor | None = None,
153
+ ) -> torch.Tensor:
154
+ """Keys / values -> Gaussians [B, N, 14]."""
155
+ if gs_tokens is None:
156
+ gs_tokens = self.get_gs_tokens(batch_size=keys.shape[0])
157
+
158
+ for layer in self.enc_dec_backbone.decoder_blocks:
159
+ gs_tokens = layer(gs_tokens=gs_tokens, keys=keys, values=values)
160
+
161
+ # Convert to Gaussians
162
+ gaussians = self.activation_head(gs_tokens)
163
+
164
+ # give gaussians an offset to the z axis so it is visible when initialized
165
+ gaussians[..., 2] = gaussians[..., 2] + self.opt.gaussian_z_offset
166
+
167
+ return gaussians
168
+
169
+ def forward(self, model_input: dict[str, torch.Tensor]) -> torch.Tensor:
170
+ keys, values = self.forward_encoder(
171
+ model_input["images"], model_input["plucker"]
172
+ )
173
+ return self.forward_decoder(keys, values)