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.
- ffgs/__init__.py +39 -0
- ffgs/dynamic.py +239 -0
- ffgs/geometry.py +418 -0
- ffgs/image.py +204 -0
- ffgs/models/__init__.py +2 -0
- ffgs/models/anysplat/LICENSE +1120 -0
- ffgs/models/anysplat/__init__.py +49 -0
- ffgs/models/anysplat/encoder.py +372 -0
- ffgs/models/anysplat/evaluation.py +98 -0
- ffgs/models/anysplat/gaussian_adapter.py +96 -0
- ffgs/models/anysplat/gs_head.py +166 -0
- ffgs/models/anysplat/modeling.py +95 -0
- ffgs/models/anysplat/processing.py +249 -0
- ffgs/models/anysplat/vggt/__init__.py +0 -0
- ffgs/models/anysplat/vggt/heads/__init__.py +0 -0
- ffgs/models/anysplat/vggt/heads/camera_head.py +181 -0
- ffgs/models/anysplat/vggt/heads/dpt_head.py +612 -0
- ffgs/models/anysplat/vggt/heads/head_act.py +134 -0
- ffgs/models/anysplat/vggt/heads/utils.py +126 -0
- ffgs/models/anysplat/vggt/layers/__init__.py +28 -0
- ffgs/models/anysplat/vggt/layers/attention.py +92 -0
- ffgs/models/anysplat/vggt/layers/block.py +157 -0
- ffgs/models/anysplat/vggt/layers/drop_path.py +42 -0
- ffgs/models/anysplat/vggt/layers/layer_scale.py +32 -0
- ffgs/models/anysplat/vggt/layers/mlp.py +45 -0
- ffgs/models/anysplat/vggt/layers/patch_embed.py +106 -0
- ffgs/models/anysplat/vggt/layers/rope.py +211 -0
- ffgs/models/anysplat/vggt/layers/swiglu_ffn.py +77 -0
- ffgs/models/anysplat/vggt/layers/vision_transformer.py +463 -0
- ffgs/models/anysplat/vggt/models/__init__.py +0 -0
- ffgs/models/anysplat/vggt/models/aggregator.py +398 -0
- ffgs/models/anysplat/vggt/utils/__init__.py +0 -0
- ffgs/models/anysplat/vggt/utils/geometry.py +230 -0
- ffgs/models/anysplat/vggt/utils/pose_enc.py +144 -0
- ffgs/models/anysplat/vggt/utils/rotation.py +148 -0
- ffgs/models/tokengs/__init__.py +43 -0
- ffgs/models/tokengs/activations.py +135 -0
- ffgs/models/tokengs/attention.py +189 -0
- ffgs/models/tokengs/enc_dec.py +323 -0
- ffgs/models/tokengs/modeling.py +173 -0
- ffgs/models/tokengs/processing.py +339 -0
- ffgs/pipeline.py +341 -0
- ffgs/processor.py +236 -0
- ffgs/registry.py +132 -0
- ffgs/render.py +111 -0
- ffgs/types.py +291 -0
- ffgs/zoo/__init__.py +234 -0
- ffgs/zoo/anysplat/default.json +34 -0
- ffgs/zoo/tokengs/dl3dv-2v.json +29 -0
- ffgs/zoo/tokengs/dl3dv-4v.json +29 -0
- ffgs/zoo/tokengs/dl3dv-6v.json +29 -0
- ffgs/zoo/tokengs/dl3dv-base.json +29 -0
- ffgs/zoo/tokengs/dl3dv-latent-2v-lpips.json +48 -0
- ffgs/zoo/tokengs/dl3dv-latent-2v-ssim.json +48 -0
- ffgs/zoo/tokengs/dl3dv-latent-4v-lpips.json +48 -0
- ffgs/zoo/tokengs/dl3dv-latent-4v-ssim.json +48 -0
- ffgs/zoo/tokengs/dl3dv-latent-6v-lpips.json +48 -0
- ffgs/zoo/tokengs/dl3dv-latent-6v-ssim.json +48 -0
- ffgs-0.1.0.dev0.dist-info/METADATA +320 -0
- ffgs-0.1.0.dev0.dist-info/RECORD +63 -0
- ffgs-0.1.0.dev0.dist-info/WHEEL +4 -0
- ffgs-0.1.0.dev0.dist-info/licenses/LICENSE +237 -0
- ffgs-0.1.0.dev0.dist-info/licenses/src/ffgs/models/anysplat/LICENSE +1120 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
#
|
|
7
|
+
# Modified by the libgs contributors, 2026, from
|
|
8
|
+
# src/model/encoder/vggt/models/aggregator.py of
|
|
9
|
+
# https://github.com/InternRobotics/AnySplat (5f5e208): imports made package-relative,
|
|
10
|
+
# formatted with black, unused imports removed.
|
|
11
|
+
# "The license found in the LICENSE file" above is the one VGGT's code
|
|
12
|
+
# had when AnySplat vendored it (2025-06-30): CC BY-NC 4.0, non-commercial
|
|
13
|
+
# (facebookresearch/vggt c6bf698); see LICENSE (this directory).
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
import torch.nn as nn
|
|
20
|
+
|
|
21
|
+
from ..layers import PatchEmbed
|
|
22
|
+
from ..layers.block import Block
|
|
23
|
+
from ..layers.rope import PositionGetter, RotaryPositionEmbedding2D
|
|
24
|
+
from ..layers.vision_transformer import vit_base, vit_giant2, vit_large, vit_small
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_RESNET_MEAN = [0.485, 0.456, 0.406]
|
|
29
|
+
_RESNET_STD = [0.229, 0.224, 0.225]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Aggregator(nn.Module):
|
|
33
|
+
"""
|
|
34
|
+
The Aggregator applies alternating-attention over input frames,
|
|
35
|
+
as described in VGGT: Visual Geometry Grounded Transformer.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
img_size (int): Image size in pixels.
|
|
40
|
+
patch_size (int): Size of each patch for PatchEmbed.
|
|
41
|
+
embed_dim (int): Dimension of the token embeddings.
|
|
42
|
+
depth (int): Number of blocks.
|
|
43
|
+
num_heads (int): Number of attention heads.
|
|
44
|
+
mlp_ratio (float): Ratio of MLP hidden dim to embedding dim.
|
|
45
|
+
num_register_tokens (int): Number of register tokens.
|
|
46
|
+
block_fn (nn.Module): The block type used for attention (Block by default).
|
|
47
|
+
qkv_bias (bool): Whether to include bias in QKV projections.
|
|
48
|
+
proj_bias (bool): Whether to include bias in the output projection.
|
|
49
|
+
ffn_bias (bool): Whether to include bias in MLP layers.
|
|
50
|
+
patch_embed (str): Type of patch embed. e.g., "conv" or "dinov2_vitl14_reg".
|
|
51
|
+
aa_order (list[str]): The order of alternating attention, e.g. ["frame", "global"].
|
|
52
|
+
aa_block_size (int): How many blocks to group under each attention type before switching. If not necessary, set to 1.
|
|
53
|
+
qk_norm (bool): Whether to apply QK normalization.
|
|
54
|
+
rope_freq (int): Base frequency for rotary embedding. -1 to disable.
|
|
55
|
+
init_values (float): Init scale for layer scale.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
img_size=518,
|
|
61
|
+
patch_size=14,
|
|
62
|
+
embed_dim=1024,
|
|
63
|
+
depth=24,
|
|
64
|
+
num_heads=16,
|
|
65
|
+
mlp_ratio=4.0,
|
|
66
|
+
num_register_tokens=4,
|
|
67
|
+
block_fn=Block,
|
|
68
|
+
qkv_bias=True,
|
|
69
|
+
proj_bias=True,
|
|
70
|
+
ffn_bias=True,
|
|
71
|
+
patch_embed="dinov2_vitl14_reg",
|
|
72
|
+
aa_order=["frame", "global"],
|
|
73
|
+
aa_block_size=1,
|
|
74
|
+
qk_norm=True,
|
|
75
|
+
rope_freq=100,
|
|
76
|
+
init_values=0.01,
|
|
77
|
+
):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.use_checkpoint = True
|
|
80
|
+
self.__build_patch_embed__(
|
|
81
|
+
patch_embed, img_size, patch_size, num_register_tokens, embed_dim=embed_dim
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Initialize rotary position embedding if frequency > 0
|
|
85
|
+
self.rope = (
|
|
86
|
+
RotaryPositionEmbedding2D(frequency=rope_freq) if rope_freq > 0 else None
|
|
87
|
+
)
|
|
88
|
+
self.position_getter = PositionGetter() if self.rope is not None else None
|
|
89
|
+
|
|
90
|
+
self.frame_blocks = nn.ModuleList(
|
|
91
|
+
[
|
|
92
|
+
block_fn(
|
|
93
|
+
dim=embed_dim,
|
|
94
|
+
num_heads=num_heads,
|
|
95
|
+
mlp_ratio=mlp_ratio,
|
|
96
|
+
qkv_bias=qkv_bias,
|
|
97
|
+
proj_bias=proj_bias,
|
|
98
|
+
ffn_bias=ffn_bias,
|
|
99
|
+
init_values=init_values,
|
|
100
|
+
qk_norm=qk_norm,
|
|
101
|
+
rope=self.rope,
|
|
102
|
+
)
|
|
103
|
+
for _ in range(depth)
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.global_blocks = nn.ModuleList(
|
|
108
|
+
[
|
|
109
|
+
block_fn(
|
|
110
|
+
dim=embed_dim,
|
|
111
|
+
num_heads=num_heads,
|
|
112
|
+
mlp_ratio=mlp_ratio,
|
|
113
|
+
qkv_bias=qkv_bias,
|
|
114
|
+
proj_bias=proj_bias,
|
|
115
|
+
ffn_bias=ffn_bias,
|
|
116
|
+
init_values=init_values,
|
|
117
|
+
qk_norm=qk_norm,
|
|
118
|
+
rope=self.rope,
|
|
119
|
+
)
|
|
120
|
+
for _ in range(depth)
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.depth = depth
|
|
125
|
+
self.aa_order = aa_order
|
|
126
|
+
self.patch_size = patch_size
|
|
127
|
+
self.aa_block_size = aa_block_size
|
|
128
|
+
|
|
129
|
+
# Validate that depth is divisible by aa_block_size
|
|
130
|
+
if self.depth % self.aa_block_size != 0:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"depth ({depth}) must be divisible by aa_block_size ({aa_block_size})"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.aa_block_num = self.depth // self.aa_block_size
|
|
136
|
+
|
|
137
|
+
# Note: We have two camera tokens, one for the first frame and one for the rest
|
|
138
|
+
# The same applies for register tokens
|
|
139
|
+
self.camera_token = nn.Parameter(torch.randn(1, 2, 1, embed_dim))
|
|
140
|
+
self.register_token = nn.Parameter(
|
|
141
|
+
torch.randn(1, 2, num_register_tokens, embed_dim)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# The patch tokens start after the camera and register tokens
|
|
145
|
+
self.patch_start_idx = 1 + num_register_tokens
|
|
146
|
+
|
|
147
|
+
# Initialize parameters with small values
|
|
148
|
+
nn.init.normal_(self.camera_token, std=1e-6)
|
|
149
|
+
nn.init.normal_(self.register_token, std=1e-6)
|
|
150
|
+
|
|
151
|
+
# Register normalization constants as buffers
|
|
152
|
+
for name, value in (
|
|
153
|
+
("_resnet_mean", _RESNET_MEAN),
|
|
154
|
+
("_resnet_std", _RESNET_STD),
|
|
155
|
+
):
|
|
156
|
+
self.register_buffer(
|
|
157
|
+
name,
|
|
158
|
+
torch.FloatTensor(value).view(1, 1, 3, 1, 1),
|
|
159
|
+
persistent=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def __build_patch_embed__(
|
|
163
|
+
self,
|
|
164
|
+
patch_embed,
|
|
165
|
+
img_size,
|
|
166
|
+
patch_size,
|
|
167
|
+
num_register_tokens,
|
|
168
|
+
interpolate_antialias=True,
|
|
169
|
+
interpolate_offset=0.0,
|
|
170
|
+
block_chunks=0,
|
|
171
|
+
init_values=1.0,
|
|
172
|
+
embed_dim=1024,
|
|
173
|
+
):
|
|
174
|
+
"""
|
|
175
|
+
Build the patch embed layer. If 'conv', we use a
|
|
176
|
+
simple PatchEmbed conv layer. Otherwise, we use a vision transformer.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
if "conv" in patch_embed:
|
|
180
|
+
self.patch_embed = PatchEmbed(
|
|
181
|
+
img_size=img_size,
|
|
182
|
+
patch_size=patch_size,
|
|
183
|
+
in_chans=3,
|
|
184
|
+
embed_dim=embed_dim,
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
vit_models = {
|
|
188
|
+
"dinov2_vitl14_reg": vit_large,
|
|
189
|
+
"dinov2_vitb14_reg": vit_base,
|
|
190
|
+
"dinov2_vits14_reg": vit_small,
|
|
191
|
+
"dinov2_vitg2_reg": vit_giant2,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
self.patch_embed = vit_models[patch_embed](
|
|
195
|
+
img_size=img_size,
|
|
196
|
+
patch_size=patch_size,
|
|
197
|
+
num_register_tokens=num_register_tokens,
|
|
198
|
+
interpolate_antialias=interpolate_antialias,
|
|
199
|
+
interpolate_offset=interpolate_offset,
|
|
200
|
+
block_chunks=block_chunks,
|
|
201
|
+
init_values=init_values,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Disable gradient updates for mask token
|
|
205
|
+
if hasattr(self.patch_embed, "mask_token"):
|
|
206
|
+
self.patch_embed.mask_token.requires_grad_(False)
|
|
207
|
+
|
|
208
|
+
def forward(
|
|
209
|
+
self, images: torch.Tensor, intermediate_layer_idx: Optional[List[int]] = None
|
|
210
|
+
) -> Tuple[List[torch.Tensor], int]:
|
|
211
|
+
"""
|
|
212
|
+
Args:
|
|
213
|
+
images (torch.Tensor): Input images with shape [B, S, 3, H, W], in range [0, 1].
|
|
214
|
+
B: batch size, S: sequence length, 3: RGB channels, H: height, W: width
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
(list[torch.Tensor], int):
|
|
218
|
+
The list of outputs from the attention blocks,
|
|
219
|
+
and the patch_start_idx indicating where patch tokens begin.
|
|
220
|
+
"""
|
|
221
|
+
B, S, C_in, H, W = images.shape
|
|
222
|
+
|
|
223
|
+
if C_in != 3:
|
|
224
|
+
raise ValueError(f"Expected 3 input channels, got {C_in}")
|
|
225
|
+
|
|
226
|
+
# Normalize images and reshape for patch embed
|
|
227
|
+
images = (images - self._resnet_mean) / self._resnet_std
|
|
228
|
+
|
|
229
|
+
# Reshape to [B*S, C, H, W] for patch embedding
|
|
230
|
+
images = images.view(B * S, C_in, H, W)
|
|
231
|
+
patch_tokens = self.patch_embed(images)
|
|
232
|
+
|
|
233
|
+
if isinstance(patch_tokens, dict):
|
|
234
|
+
patch_tokens = patch_tokens["x_norm_patchtokens"]
|
|
235
|
+
|
|
236
|
+
_, P, C = patch_tokens.shape
|
|
237
|
+
|
|
238
|
+
# Expand camera and register tokens to match batch size and sequence length
|
|
239
|
+
camera_token = slice_expand_and_flatten(self.camera_token, B, S)
|
|
240
|
+
register_token = slice_expand_and_flatten(self.register_token, B, S)
|
|
241
|
+
|
|
242
|
+
# Concatenate special tokens with patch tokens
|
|
243
|
+
tokens = torch.cat([camera_token, register_token, patch_tokens], dim=1)
|
|
244
|
+
|
|
245
|
+
pos = None
|
|
246
|
+
if self.rope is not None:
|
|
247
|
+
pos = self.position_getter(
|
|
248
|
+
B * S, H // self.patch_size, W // self.patch_size, device=images.device
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if self.patch_start_idx > 0:
|
|
252
|
+
# do not use position embedding for special tokens (camera and register tokens)
|
|
253
|
+
# so set pos to 0 for the special tokens
|
|
254
|
+
pos = pos + 1
|
|
255
|
+
pos_special = (
|
|
256
|
+
torch.zeros(B * S, self.patch_start_idx, 2)
|
|
257
|
+
.to(images.device)
|
|
258
|
+
.to(pos.dtype)
|
|
259
|
+
)
|
|
260
|
+
pos = torch.cat([pos_special, pos], dim=1)
|
|
261
|
+
|
|
262
|
+
# update P because we added special tokens
|
|
263
|
+
_, P, C = tokens.shape
|
|
264
|
+
|
|
265
|
+
frame_idx = 0
|
|
266
|
+
global_idx = 0
|
|
267
|
+
output_list = []
|
|
268
|
+
layer_idx = 0
|
|
269
|
+
|
|
270
|
+
# Convert intermediate_layer_idx to a set for O(1) lookup
|
|
271
|
+
if intermediate_layer_idx is not None:
|
|
272
|
+
required_layers = set(intermediate_layer_idx)
|
|
273
|
+
# Always include the last layer for camera_head
|
|
274
|
+
required_layers.add(self.depth - 1)
|
|
275
|
+
|
|
276
|
+
for _ in range(self.aa_block_num):
|
|
277
|
+
for attn_type in self.aa_order:
|
|
278
|
+
if attn_type == "frame":
|
|
279
|
+
tokens, frame_idx, frame_intermediates = (
|
|
280
|
+
self._process_frame_attention(
|
|
281
|
+
tokens, B, S, P, C, frame_idx, pos=pos
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
elif attn_type == "global":
|
|
285
|
+
tokens, global_idx, global_intermediates = (
|
|
286
|
+
self._process_global_attention(
|
|
287
|
+
tokens, B, S, P, C, global_idx, pos=pos
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
raise ValueError(f"Unknown attention type: {attn_type}")
|
|
292
|
+
|
|
293
|
+
if intermediate_layer_idx is not None:
|
|
294
|
+
for i in range(len(frame_intermediates)):
|
|
295
|
+
current_layer = layer_idx + i
|
|
296
|
+
if current_layer in required_layers:
|
|
297
|
+
# concat frame and global intermediates, [B x S x P x 2C]
|
|
298
|
+
concat_inter = torch.cat(
|
|
299
|
+
[frame_intermediates[i], global_intermediates[i]], dim=-1
|
|
300
|
+
)
|
|
301
|
+
output_list.append(concat_inter)
|
|
302
|
+
layer_idx += self.aa_block_size
|
|
303
|
+
|
|
304
|
+
else:
|
|
305
|
+
for i in range(len(frame_intermediates)):
|
|
306
|
+
# concat frame and global intermediates, [B x S x P x 2C]
|
|
307
|
+
concat_inter = torch.cat(
|
|
308
|
+
[frame_intermediates[i], global_intermediates[i]], dim=-1
|
|
309
|
+
)
|
|
310
|
+
output_list.append(concat_inter)
|
|
311
|
+
|
|
312
|
+
del concat_inter
|
|
313
|
+
del frame_intermediates
|
|
314
|
+
del global_intermediates
|
|
315
|
+
return output_list, self.patch_start_idx
|
|
316
|
+
|
|
317
|
+
def _process_frame_attention(self, tokens, B, S, P, C, frame_idx, pos=None):
|
|
318
|
+
"""
|
|
319
|
+
Process frame attention blocks. We keep tokens in shape (B*S, P, C).
|
|
320
|
+
"""
|
|
321
|
+
# If needed, reshape tokens or positions:
|
|
322
|
+
if tokens.shape != (B * S, P, C):
|
|
323
|
+
tokens = tokens.view(B, S, P, C).view(B * S, P, C)
|
|
324
|
+
|
|
325
|
+
if pos is not None and pos.shape != (B * S, P, 2):
|
|
326
|
+
pos = pos.view(B, S, P, 2).view(B * S, P, 2)
|
|
327
|
+
|
|
328
|
+
intermediates = []
|
|
329
|
+
|
|
330
|
+
# by default, self.aa_block_size=1, which processes one block at a time
|
|
331
|
+
for _ in range(self.aa_block_size):
|
|
332
|
+
if self.use_checkpoint:
|
|
333
|
+
tokens = torch.utils.checkpoint.checkpoint(
|
|
334
|
+
self.frame_blocks[frame_idx],
|
|
335
|
+
tokens,
|
|
336
|
+
pos,
|
|
337
|
+
use_reentrant=False,
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
tokens = self.frame_blocks[frame_idx](tokens, pos=pos)
|
|
341
|
+
frame_idx += 1
|
|
342
|
+
intermediates.append(tokens.view(B, S, P, C))
|
|
343
|
+
|
|
344
|
+
return tokens, frame_idx, intermediates
|
|
345
|
+
|
|
346
|
+
def _process_global_attention(self, tokens, B, S, P, C, global_idx, pos=None):
|
|
347
|
+
"""
|
|
348
|
+
Process global attention blocks. We keep tokens in shape (B, S*P, C).
|
|
349
|
+
"""
|
|
350
|
+
if tokens.shape != (B, S * P, C):
|
|
351
|
+
tokens = tokens.view(B, S, P, C).view(B, S * P, C)
|
|
352
|
+
|
|
353
|
+
if pos is not None and pos.shape != (B, S * P, 2):
|
|
354
|
+
pos = pos.view(B, S, P, 2).view(B, S * P, 2)
|
|
355
|
+
|
|
356
|
+
intermediates = []
|
|
357
|
+
|
|
358
|
+
# by default, self.aa_block_size=1, which processes one block at a time
|
|
359
|
+
for _ in range(self.aa_block_size):
|
|
360
|
+
if self.use_checkpoint:
|
|
361
|
+
tokens = torch.utils.checkpoint.checkpoint(
|
|
362
|
+
self.global_blocks[global_idx],
|
|
363
|
+
tokens,
|
|
364
|
+
pos,
|
|
365
|
+
use_reentrant=False,
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
tokens = self.global_blocks[global_idx](tokens, pos=pos)
|
|
369
|
+
global_idx += 1
|
|
370
|
+
intermediates.append(tokens.view(B, S, P, C))
|
|
371
|
+
|
|
372
|
+
return tokens, global_idx, intermediates
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def slice_expand_and_flatten(token_tensor, B, S):
|
|
376
|
+
"""
|
|
377
|
+
Processes specialized tokens with shape (1, 2, X, C) for multi-frame processing:
|
|
378
|
+
1) Uses the first position (index=0) for the first frame only
|
|
379
|
+
2) Uses the second position (index=1) for all remaining frames (S-1 frames)
|
|
380
|
+
3) Expands both to match batch size B
|
|
381
|
+
4) Concatenates to form (B, S, X, C) where each sequence has 1 first-position token
|
|
382
|
+
followed by (S-1) second-position tokens
|
|
383
|
+
5) Flattens to (B*S, X, C) for processing
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
torch.Tensor: Processed tokens with shape (B*S, X, C)
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
# Slice out the "query" tokens => shape (1, 1, ...)
|
|
390
|
+
query = token_tensor[:, 0:1, ...].expand(B, 1, *token_tensor.shape[2:])
|
|
391
|
+
# Slice out the "other" tokens => shape (1, S-1, ...)
|
|
392
|
+
others = token_tensor[:, 1:, ...].expand(B, S - 1, *token_tensor.shape[2:])
|
|
393
|
+
# Concatenate => shape (B, S, ...)
|
|
394
|
+
combined = torch.cat([query, others], dim=1)
|
|
395
|
+
|
|
396
|
+
# Finally flatten => shape (B*S, ...)
|
|
397
|
+
combined = combined.view(B * S, *combined.shape[2:])
|
|
398
|
+
return combined
|
|
File without changes
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
#
|
|
7
|
+
# Modified by the libgs contributors, 2026, from
|
|
8
|
+
# src/model/encoder/vggt/utils/geometry.py of https://github.com/InternRobotics/AnySplat
|
|
9
|
+
# (5f5e208): formatted with black, unused imports removed.
|
|
10
|
+
# "The license found in the LICENSE file" above is the one VGGT's code
|
|
11
|
+
# had when AnySplat vendored it (2025-06-30): CC BY-NC 4.0, non-commercial
|
|
12
|
+
# (facebookresearch/vggt c6bf698); see LICENSE (this directory).
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def batchify_unproject_depth_map_to_point_map(
|
|
18
|
+
depth_map: torch.Tensor, extrinsics_cam: torch.Tensor, intrinsics_cam: torch.Tensor
|
|
19
|
+
) -> torch.Tensor:
|
|
20
|
+
"""
|
|
21
|
+
Unproject a batch of depth maps to 3D world coordinates.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
depth_map (torch.Tensor): Batch of depth maps of shape (B, V, H, W, 1) or (B, V, H, W)
|
|
25
|
+
extrinsics_cam (torch.Tensor): Batch of camera extrinsic matrices of shape (B, V, 3, 4)
|
|
26
|
+
intrinsics_cam (torch.Tensor): Batch of camera intrinsic matrices of shape (B, V, 3, 3)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
torch.Tensor: Batch of 3D world coordinates of shape (S, H, W, 3)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Handle both (S, H, W, 1) and (S, H, W) cases
|
|
33
|
+
if depth_map.dim() == 5:
|
|
34
|
+
depth_map = depth_map.squeeze(-1) # (S, H, W)
|
|
35
|
+
|
|
36
|
+
# Generate batched camera coordinates
|
|
37
|
+
H, W = depth_map.shape[2:]
|
|
38
|
+
batch_size, num_views = depth_map.shape[0], depth_map.shape[1]
|
|
39
|
+
|
|
40
|
+
# Intrinsic parameters (S, 3, 3)
|
|
41
|
+
intrinsics_cam, extrinsics_cam, depth_map = (
|
|
42
|
+
intrinsics_cam.flatten(0, 1),
|
|
43
|
+
extrinsics_cam.flatten(0, 1),
|
|
44
|
+
depth_map.flatten(0, 1),
|
|
45
|
+
)
|
|
46
|
+
fu = intrinsics_cam[:, 0, 0] # (S,)
|
|
47
|
+
fv = intrinsics_cam[:, 1, 1] # (S,)
|
|
48
|
+
cu = intrinsics_cam[:, 0, 2] # (S,)
|
|
49
|
+
cv = intrinsics_cam[:, 1, 2] # (S,)
|
|
50
|
+
|
|
51
|
+
# Generate grid of pixel coordinates
|
|
52
|
+
u = torch.arange(W, device=depth_map.device)[None, None, :].expand(
|
|
53
|
+
batch_size * num_views, H, W
|
|
54
|
+
) # (S, H, W)
|
|
55
|
+
v = torch.arange(H, device=depth_map.device)[None, :, None].expand(
|
|
56
|
+
batch_size * num_views, H, W
|
|
57
|
+
) # (S, H, W)
|
|
58
|
+
|
|
59
|
+
# Unproject to camera coordinates (S, H, W, 3)
|
|
60
|
+
x_cam = (u - cu[:, None, None]) * depth_map / fu[:, None, None]
|
|
61
|
+
y_cam = (v - cv[:, None, None]) * depth_map / fv[:, None, None]
|
|
62
|
+
z_cam = depth_map
|
|
63
|
+
|
|
64
|
+
cam_coords = torch.stack((x_cam, y_cam, z_cam), dim=-1) # (S, H, W, 3)
|
|
65
|
+
|
|
66
|
+
# Transform to world coordinates
|
|
67
|
+
cam_to_world = closed_form_inverse_se3(extrinsics_cam) # (S, 4, 4)
|
|
68
|
+
|
|
69
|
+
# homo transformation
|
|
70
|
+
homo_pts = torch.cat(
|
|
71
|
+
(cam_coords, torch.ones_like(cam_coords[..., :1])), dim=-1
|
|
72
|
+
).flatten(1, 2)
|
|
73
|
+
world_coords = (
|
|
74
|
+
torch.bmm(cam_to_world, homo_pts.transpose(1, 2))
|
|
75
|
+
.transpose(1, 2)[:, :, :3]
|
|
76
|
+
.view(batch_size * num_views, H, W, 3)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return world_coords.view(batch_size, num_views, H, W, 3)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def unproject_depth_map_to_point_map(
|
|
83
|
+
depth_map: torch.Tensor, extrinsics_cam: torch.Tensor, intrinsics_cam: torch.Tensor
|
|
84
|
+
) -> torch.Tensor:
|
|
85
|
+
"""
|
|
86
|
+
Unproject a batch of depth maps to 3D world coordinates.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
depth_map (torch.Tensor): Batch of depth maps of shape (S, H, W, 1) or (S, H, W)
|
|
90
|
+
extrinsics_cam (torch.Tensor): Batch of camera extrinsic matrices of shape (S, 3, 4)
|
|
91
|
+
intrinsics_cam (torch.Tensor): Batch of camera intrinsic matrices of shape (S, 3, 3)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
torch.Tensor: Batch of 3D world coordinates of shape (S, H, W, 3)
|
|
95
|
+
"""
|
|
96
|
+
world_points_list = []
|
|
97
|
+
for frame_idx in range(depth_map.shape[0]):
|
|
98
|
+
cur_world_points, _, _ = depth_to_world_coords_points(
|
|
99
|
+
depth_map[frame_idx].squeeze(-1),
|
|
100
|
+
extrinsics_cam[frame_idx],
|
|
101
|
+
intrinsics_cam[frame_idx],
|
|
102
|
+
)
|
|
103
|
+
world_points_list.append(cur_world_points)
|
|
104
|
+
world_points_array = torch.stack(world_points_list, dim=0)
|
|
105
|
+
|
|
106
|
+
return world_points_array
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def depth_to_world_coords_points(
|
|
110
|
+
depth_map: torch.Tensor,
|
|
111
|
+
extrinsic: torch.Tensor,
|
|
112
|
+
intrinsic: torch.Tensor,
|
|
113
|
+
eps=1e-8,
|
|
114
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
115
|
+
"""
|
|
116
|
+
Convert a depth map to world coordinates.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
depth_map (torch.Tensor): Depth map of shape (H, W).
|
|
120
|
+
intrinsic (torch.Tensor): Camera intrinsic matrix of shape (3, 3).
|
|
121
|
+
extrinsic (torch.Tensor): Camera extrinsic matrix of shape (3, 4). OpenCV camera coordinate convention, cam from world.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
tuple[torch.Tensor, torch.Tensor]: World coordinates (H, W, 3) and valid depth mask (H, W).
|
|
125
|
+
"""
|
|
126
|
+
if depth_map is None:
|
|
127
|
+
return None, None, None
|
|
128
|
+
|
|
129
|
+
# Valid depth mask
|
|
130
|
+
point_mask = depth_map > eps
|
|
131
|
+
|
|
132
|
+
# Convert depth map to camera coordinates
|
|
133
|
+
cam_coords_points = depth_to_cam_coords_points(depth_map, intrinsic)
|
|
134
|
+
|
|
135
|
+
# Multiply with the inverse of extrinsic matrix to transform to world coordinates
|
|
136
|
+
# extrinsic_inv is 4x4 (note closed_form_inverse_OpenCV is batched, the output is (N, 4, 4))
|
|
137
|
+
cam_to_world_extrinsic = closed_form_inverse_se3(extrinsic[None])[0]
|
|
138
|
+
|
|
139
|
+
R_cam_to_world = cam_to_world_extrinsic[:3, :3]
|
|
140
|
+
t_cam_to_world = cam_to_world_extrinsic[:3, 3]
|
|
141
|
+
|
|
142
|
+
# Apply the rotation and translation to the camera coordinates
|
|
143
|
+
world_coords_points = (
|
|
144
|
+
torch.matmul(cam_coords_points, R_cam_to_world.T) + t_cam_to_world
|
|
145
|
+
) # HxWx3, 3x3 -> HxWx3
|
|
146
|
+
|
|
147
|
+
return world_coords_points, cam_coords_points, point_mask
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def depth_to_cam_coords_points(
|
|
151
|
+
depth_map: torch.Tensor, intrinsic: torch.Tensor
|
|
152
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
153
|
+
"""
|
|
154
|
+
Convert a depth map to camera coordinates.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
depth_map (torch.Tensor): Depth map of shape (H, W).
|
|
158
|
+
intrinsic (torch.Tensor): Camera intrinsic matrix of shape (3, 3).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
tuple[torch.Tensor, torch.Tensor]: Camera coordinates (H, W, 3)
|
|
162
|
+
"""
|
|
163
|
+
H, W = depth_map.shape
|
|
164
|
+
assert intrinsic.shape == (3, 3), "Intrinsic matrix must be 3x3"
|
|
165
|
+
assert (
|
|
166
|
+
intrinsic[0, 1] == 0 and intrinsic[1, 0] == 0
|
|
167
|
+
), "Intrinsic matrix must have zero skew"
|
|
168
|
+
|
|
169
|
+
# Intrinsic parameters
|
|
170
|
+
fu, fv = intrinsic[0, 0], intrinsic[1, 1]
|
|
171
|
+
cu, cv = intrinsic[0, 2], intrinsic[1, 2]
|
|
172
|
+
|
|
173
|
+
# Generate grid of pixel coordinates
|
|
174
|
+
u, v = torch.meshgrid(
|
|
175
|
+
torch.arange(W, device=depth_map.device),
|
|
176
|
+
torch.arange(H, device=depth_map.device),
|
|
177
|
+
indexing="xy",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Unproject to camera coordinates
|
|
181
|
+
x_cam = (u - cu) * depth_map / fu
|
|
182
|
+
y_cam = (v - cv) * depth_map / fv
|
|
183
|
+
z_cam = depth_map
|
|
184
|
+
|
|
185
|
+
# Stack to form camera coordinates
|
|
186
|
+
cam_coords = torch.stack((x_cam, y_cam, z_cam), dim=-1).to(dtype=torch.float32)
|
|
187
|
+
|
|
188
|
+
return cam_coords
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def closed_form_inverse_se3(se3, R=None, T=None):
|
|
192
|
+
"""
|
|
193
|
+
Compute the inverse of each 4x4 (or 3x4) SE3 matrix in a batch.
|
|
194
|
+
|
|
195
|
+
If `R` and `T` are provided, they must correspond to the rotation and translation
|
|
196
|
+
components of `se3`. Otherwise, they will be extracted from `se3`.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
se3: Nx4x4 or Nx3x4 array or tensor of SE3 matrices.
|
|
200
|
+
R (optional): Nx3x3 array or tensor of rotation matrices.
|
|
201
|
+
T (optional): Nx3x1 array or tensor of translation vectors.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Inverted SE3 matrices with the same type and device as `se3`.
|
|
205
|
+
|
|
206
|
+
Shapes:
|
|
207
|
+
se3: (N, 4, 4)
|
|
208
|
+
R: (N, 3, 3)
|
|
209
|
+
T: (N, 3, 1)
|
|
210
|
+
"""
|
|
211
|
+
# Validate shapes
|
|
212
|
+
if se3.shape[-2:] != (4, 4) and se3.shape[-2:] != (3, 4):
|
|
213
|
+
raise ValueError(f"se3 must be of shape (N,4,4), got {se3.shape}.")
|
|
214
|
+
|
|
215
|
+
# Extract R and T if not provided
|
|
216
|
+
if R is None:
|
|
217
|
+
R = se3[:, :3, :3] # (N,3,3)
|
|
218
|
+
if T is None:
|
|
219
|
+
T = se3[:, :3, 3:] # (N,3,1)
|
|
220
|
+
|
|
221
|
+
# Transpose R
|
|
222
|
+
R_transposed = R.transpose(1, 2) # (N,3,3)
|
|
223
|
+
top_right = -torch.bmm(R_transposed, T) # (N,3,1)
|
|
224
|
+
inverted_matrix = torch.eye(4, 4, device=R.device)[None].repeat(len(R), 1, 1)
|
|
225
|
+
inverted_matrix = inverted_matrix.to(R.dtype)
|
|
226
|
+
|
|
227
|
+
inverted_matrix[:, :3, :3] = R_transposed
|
|
228
|
+
inverted_matrix[:, :3, 3:] = top_right
|
|
229
|
+
|
|
230
|
+
return inverted_matrix
|