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,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