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,166 @@
1
+ # Copyright (C) 2024-present Naver Corporation. All rights reserved.
2
+ # Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
3
+ #
4
+ # --------------------------------------------------------
5
+ # dpt head implementation for DUST3R
6
+ # Downstream heads assume inputs of size B x N x C (where N is the number of tokens) ;
7
+ # or if it takes as input the output at every layer, the attribute return_all_layers should be set to True
8
+ # the forward function also takes as input a dictionnary img_info with key "height" and "width"
9
+ # for PixelwiseTask, the output will be of dimension B x num_channels x H x W
10
+ # --------------------------------------------------------
11
+ #
12
+ # Modified by the libgs contributors, 2026, from
13
+ # src/model/encoder/heads/vggt_dpt_gs_head.py of
14
+ # https://github.com/InternRobotics/AnySplat (5f5e208): only `VGGT_DPT_GS_Head` kept
15
+ # (`PixelwiseTaskWithDPT` / `create_gs_dpt_head` and the commented-out signature
16
+ # removed, with the imports only they used: einops, dpt_block, head_modules (xFormers),
17
+ # postprocess); `DPTHead` imported package-relative; formatted with black.
18
+ # CC BY-NC-SA 4.0 (non-commercial): see LICENSE (this directory).
19
+
20
+ from typing import List
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ import torch.nn.functional as F
25
+
26
+ from .vggt.heads.dpt_head import DPTHead
27
+
28
+
29
+ class VGGT_DPT_GS_Head(DPTHead):
30
+ def __init__(
31
+ self,
32
+ dim_in: int,
33
+ patch_size: int = 14,
34
+ output_dim: int = 83,
35
+ activation: str = "inv_log",
36
+ conf_activation: str = "expp1",
37
+ features: int = 256,
38
+ out_channels: List[int] = [256, 512, 1024, 1024],
39
+ intermediate_layer_idx: List[int] = [4, 11, 17, 23],
40
+ pos_embed: bool = True,
41
+ feature_only: bool = False,
42
+ down_ratio: int = 1,
43
+ ):
44
+ super().__init__(
45
+ dim_in,
46
+ patch_size,
47
+ output_dim,
48
+ activation,
49
+ conf_activation,
50
+ features,
51
+ out_channels,
52
+ intermediate_layer_idx,
53
+ pos_embed,
54
+ feature_only,
55
+ down_ratio,
56
+ )
57
+
58
+ head_features_1 = 128
59
+ head_features_2 = (
60
+ 128 if output_dim > 50 else 32
61
+ ) # sh=0, head_features_2 = 32; sh=4, head_features_2 = 128
62
+ self.input_merger = nn.Sequential(
63
+ nn.Conv2d(3, head_features_2, 7, 1, 3),
64
+ nn.ReLU(),
65
+ )
66
+
67
+ self.scratch.output_conv2 = nn.Sequential(
68
+ nn.Conv2d(
69
+ head_features_1, head_features_2, kernel_size=3, stride=1, padding=1
70
+ ),
71
+ nn.ReLU(inplace=True),
72
+ nn.Conv2d(head_features_2, output_dim, kernel_size=1, stride=1, padding=0),
73
+ )
74
+
75
+ def forward(
76
+ self,
77
+ encoder_tokens: List[torch.Tensor],
78
+ depths,
79
+ imgs,
80
+ patch_start_idx: int = 5,
81
+ image_size=None,
82
+ conf=None,
83
+ frames_chunk_size: int = 8,
84
+ ):
85
+ # H, W = input_info['image_size']
86
+ B, S, _, H, W = imgs.shape
87
+ image_size = self.image_size if image_size is None else image_size
88
+
89
+ # If frames_chunk_size is not specified or greater than S, process all frames at once
90
+ if frames_chunk_size is None or frames_chunk_size >= S:
91
+ return self._forward_impl(encoder_tokens, imgs, patch_start_idx)
92
+
93
+ # Otherwise, process frames in chunks to manage memory usage
94
+ assert frames_chunk_size > 0
95
+
96
+ # Process frames in batches
97
+ all_preds = []
98
+
99
+ for frames_start_idx in range(0, S, frames_chunk_size):
100
+ frames_end_idx = min(frames_start_idx + frames_chunk_size, S)
101
+
102
+ # Process batch of frames
103
+ chunk_output = self._forward_impl(
104
+ encoder_tokens, imgs, patch_start_idx, frames_start_idx, frames_end_idx
105
+ )
106
+ all_preds.append(chunk_output)
107
+
108
+ # Concatenate results along the sequence dimension
109
+ return torch.cat(all_preds, dim=1)
110
+
111
+ def _forward_impl(
112
+ self,
113
+ encoder_tokens: List[torch.Tensor],
114
+ imgs,
115
+ patch_start_idx: int = 5,
116
+ frames_start_idx: int = None,
117
+ frames_end_idx: int = None,
118
+ ):
119
+
120
+ if frames_start_idx is not None and frames_end_idx is not None:
121
+ imgs = imgs[:, frames_start_idx:frames_end_idx]
122
+
123
+ B, S, _, H, W = imgs.shape
124
+
125
+ patch_h, patch_w = H // self.patch_size[0], W // self.patch_size[1]
126
+
127
+ out = []
128
+ dpt_idx = 0
129
+ for layer_idx in self.intermediate_layer_idx:
130
+ # x = encoder_tokens[layer_idx][:, :, patch_start_idx:]
131
+ if len(encoder_tokens) > 10:
132
+ x = encoder_tokens[layer_idx][:, :, patch_start_idx:]
133
+ else:
134
+ list_idx = self.intermediate_layer_idx.index(layer_idx)
135
+ x = encoder_tokens[list_idx][:, :, patch_start_idx:]
136
+
137
+ # Select frames if processing a chunk
138
+ if frames_start_idx is not None and frames_end_idx is not None:
139
+ x = x[:, frames_start_idx:frames_end_idx].contiguous()
140
+
141
+ x = x.view(B * S, -1, x.shape[-1])
142
+
143
+ x = self.norm(x)
144
+
145
+ x = x.permute(0, 2, 1).reshape((x.shape[0], x.shape[-1], patch_h, patch_w))
146
+
147
+ x = self.projects[dpt_idx](x)
148
+ if self.pos_embed:
149
+ x = self._apply_pos_embed(x, W, H)
150
+ x = self.resize_layers[dpt_idx](x)
151
+
152
+ out.append(x)
153
+ dpt_idx += 1
154
+
155
+ # Fuse features from multiple layers.
156
+ out = self.scratch_forward(out)
157
+ direct_img_feat = self.input_merger(imgs.flatten(0, 1))
158
+ out = F.interpolate(out, size=(H, W), mode="bilinear", align_corners=True)
159
+ out = out + direct_img_feat
160
+
161
+ if self.pos_embed:
162
+ out = self._apply_pos_embed(out, W, H)
163
+
164
+ out = self.scratch.output_conv2(out)
165
+ out = out.view(B, S, *out.shape[1:])
166
+ return out
@@ -0,0 +1,95 @@
1
+ """The AnySplat network: images -> Gaussians and the input cameras, in its frame."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from huggingface_hub import PyTorchModelHubMixin
10
+
11
+ from .encoder import (
12
+ EncoderAnySplat,
13
+ EncoderAnySplatCfg,
14
+ EncoderOutput,
15
+ OpacityMappingCfg,
16
+ )
17
+ from .gaussian_adapter import GaussianAdapterCfg
18
+
19
+
20
+ class AnySplat(nn.Module, PyTorchModelHubMixin):
21
+ """
22
+ AnySplat (Jiang et al., TOG 2025): VGGT-based feed-forward Gaussians from
23
+ unposed images, with the cameras it predicts for them.
24
+
25
+ Input (from `AnySplatProcessor`): images [B, V, 3, H, W] in [0, 1], H and W
26
+ multiples of the patch size. Output: upstream's `EncoderOutput` -- Gaussians
27
+ (xyzw quaternions, SH of `sh_degree`) and `pred_context_pose` (c2w, normalised
28
+ K) in the frame of the first predicted camera, at the model's own scale.
29
+
30
+ The arguments are the inference fields of upstream's `encoder_cfg` (defaults:
31
+ the released `lhjiang/anysplat` config) and the VGGT architecture (defaults:
32
+ VGGT-1B). The aggregator is kept in bfloat16, as upstream; the parameter names
33
+ are upstream's (`encoder.`...), so its checkpoint loads as it is.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ voxel_size: float = 0.002,
39
+ voxelize: bool = True,
40
+ sh_degree: int = 4,
41
+ gaussian_scale_min: float = 0.5,
42
+ gaussian_scale_max: float = 15.0,
43
+ opacity_mapping: dict[str, float] | None = None,
44
+ intermediate_layer_idx: list[int] | None = (4, 11, 17, 23),
45
+ gs_prune: bool = False,
46
+ opacity_threshold: float = 0.001,
47
+ gs_keep_ratio: float = 1.0,
48
+ render_conf: bool = False,
49
+ opacity_conf: bool = False,
50
+ conf_threshold: float = 0.1,
51
+ img_size: int = 518,
52
+ patch_size: int = 14,
53
+ embed_dim: int = 1024,
54
+ aggregator: dict[str, Any] | None = None,
55
+ camera_head: dict[str, Any] | None = None,
56
+ depth_head: dict[str, Any] | None = None,
57
+ gs_head: dict[str, Any] | None = None,
58
+ ):
59
+ super().__init__()
60
+ mapping = {"initial": 0.0, "final": 0.0, "warm_up": 1}
61
+ cfg = EncoderAnySplatCfg(
62
+ voxel_size=voxel_size,
63
+ voxelize=voxelize,
64
+ gaussian_adapter=GaussianAdapterCfg(
65
+ gaussian_scale_min=gaussian_scale_min,
66
+ gaussian_scale_max=gaussian_scale_max,
67
+ sh_degree=sh_degree,
68
+ ),
69
+ opacity_mapping=OpacityMappingCfg(**(opacity_mapping or mapping)),
70
+ intermediate_layer_idx=(
71
+ None if intermediate_layer_idx is None else list(intermediate_layer_idx)
72
+ ),
73
+ gs_prune=gs_prune,
74
+ opacity_threshold=opacity_threshold,
75
+ gs_keep_ratio=gs_keep_ratio,
76
+ render_conf=render_conf,
77
+ opacity_conf=opacity_conf,
78
+ conf_threshold=conf_threshold,
79
+ img_size=img_size,
80
+ patch_size=patch_size,
81
+ embed_dim=embed_dim,
82
+ aggregator=dict(aggregator or {}),
83
+ camera_head=dict(camera_head or {}),
84
+ depth_head=dict(depth_head or {}),
85
+ gs_head=dict(gs_head or {}),
86
+ )
87
+ self.encoder = EncoderAnySplat(cfg)
88
+
89
+ @property
90
+ def sh_degree(self) -> int:
91
+ return self.encoder.cfg.gaussian_adapter.sh_degree
92
+
93
+ def forward(self, images: torch.Tensor) -> EncoderOutput:
94
+ # Upstream's `AnySplat.inference`: the encoder at global step 0.
95
+ return self.encoder(images, global_step=0)
@@ -0,0 +1,249 @@
1
+ """AnySplat pre / post-processing: the author's inference data path.
2
+
3
+ Per sample, as `src/utils/image.py:process_image` and the callers in
4
+ `src/eval_nvs.py` / `inference.py` of https://github.com/InternRobotics/AnySplat
5
+ do:
6
+
7
+ 1. the image is read as 8 bit (inputs are quantised to 8 bit here), resized with
8
+ PIL's bicubic filter so that it covers `image_shape`, the covering side by the
9
+ upstream arithmetic (`int`, not `round`), then centre-cropped to `image_shape`
10
+ ([448, 448] for the released model);
11
+ 2. `ToTensor() * 2 - 1`, then `(x + 1) * 0.5`: the [0, 1] images the model takes,
12
+ with the float32 rounding of that round trip.
13
+
14
+ The model needs no cameras: it predicts them (`predicted_cameras`), in the frame of
15
+ its first input camera at its own scale. With poses, the pipeline fits that frame
16
+ to them (`ModelFrame.fit`). Gaussians come out with SH of degree 4.
17
+
18
+ Rendering uses upstream's settings (its `DecoderSplattingCUDA`): near plane 1e-10
19
+ and gsplat's default far plane, in model units, `radius_clip=0.1`, classic
20
+ rasterisation, white background, colours clamped to [0, 1].
21
+
22
+ `crop_mode="pad"` / `"none"` replace step 1's crop with the ffgs rules; the
23
+ `resize_mode` `"bicubic"` (the default) is step 1's PIL kernel.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+ from typing import Any
30
+
31
+ import numpy as np
32
+ import torch
33
+ from PIL import Image
34
+
35
+ from ...geometry import ModelFrame
36
+ from ...image import CROP_MODES, RESIZE_MODES, center_crop, fit_images, rescale
37
+ from ...processor import ImageFitConfig, Prepared, Processor
38
+ from ...types import Cameras, Gaussians, Views
39
+ from .encoder import EncoderOutput
40
+
41
+ # The ffgs kernels, and upstream's (PIL bicubic on 8-bit images).
42
+ ANYSPLAT_RESIZE_MODES = ("bicubic", *RESIZE_MODES)
43
+ RASTERIZE_MODES = ("classic", "antialiased")
44
+ PATCH_SIZE = 14
45
+
46
+
47
+ @dataclass
48
+ class AnySplatProcessorConfig(ImageFitConfig):
49
+ # [h, w], multiples of the 14-pixel patches; upstream: 448 x 448.
50
+ image_shape: list[int] = field(default_factory=lambda: [448, 448])
51
+ resize_mode: str = "bicubic"
52
+ # Render settings of the upstream renderer; planes in model units.
53
+ znear: float = 1e-10
54
+ zfar: float = 1e10
55
+ background: list[float] = field(default_factory=lambda: [1.0, 1.0, 1.0])
56
+ radius_clip: float = 0.1
57
+ rasterize_mode: str = "classic"
58
+ clamp: bool = True
59
+
60
+ def __post_init__(self) -> None:
61
+ # ImageFitConfig's checks, with upstream's kernel among the resize modes.
62
+ self.image_shape = [int(x) for x in self.image_shape]
63
+ if len(self.image_shape) != 2:
64
+ raise ValueError(f"image_shape must be [h, w], got {self.image_shape}")
65
+ if any(x <= 0 or x % PATCH_SIZE for x in self.image_shape):
66
+ raise ValueError(
67
+ f"image_shape must be positive multiples of {PATCH_SIZE}, got "
68
+ f"{self.image_shape}"
69
+ )
70
+ if self.resize_mode not in ANYSPLAT_RESIZE_MODES:
71
+ raise ValueError(f"resize_mode must be one of {ANYSPLAT_RESIZE_MODES}")
72
+ if self.crop_mode not in CROP_MODES:
73
+ raise ValueError(f"crop_mode must be one of {CROP_MODES}")
74
+ if self.rasterize_mode not in RASTERIZE_MODES:
75
+ raise ValueError(f"rasterize_mode must be one of {RASTERIZE_MODES}")
76
+
77
+
78
+ class AnySplatProcessor(Processor):
79
+ config_cls = AnySplatProcessorConfig
80
+
81
+ def preprocess(self, views: Views, device: torch.device) -> Prepared:
82
+ images = self.fit_views(views).images
83
+ # process_image's `ToTensor() * 2 - 1`, undone by its callers.
84
+ images = (images * 2.0 - 1.0 + 1) * 0.5
85
+ # No frame yet: it is fitted to the predicted cameras (`output_frame`).
86
+ return Prepared(images.to(device), None)
87
+
88
+ def postprocess(self, model_output: EncoderOutput) -> Gaussians:
89
+ g = model_output.gaussians
90
+ d_sh = g.harmonics.shape[-1]
91
+ gaussians = Gaussians(
92
+ means=g.means.float(),
93
+ scales=g.scales.float(),
94
+ quats=g.rotations[..., [3, 0, 1, 2]].float(), # xyzw -> wxyz
95
+ opacities=g.opacities.float(),
96
+ colors=g.harmonics.transpose(-1, -2).float(), # [B, N, d_sh, 3]
97
+ sh_degree=round(d_sh**0.5) - 1,
98
+ )
99
+ counts = (model_output.infos or {}).get("num_gaussians")
100
+ return gaussians if counts is None else neutralize_padding(gaussians, counts)
101
+
102
+ def predicted_cameras(self, model_output: EncoderOutput) -> Cameras:
103
+ pose = model_output.pred_context_pose
104
+ h, w = model_output.depth_dict["depth"].shape[2:4]
105
+ return Cameras(
106
+ pose["extrinsic"].float(),
107
+ pose["intrinsic"].float(),
108
+ (h, w),
109
+ normalized_intrinsics=True,
110
+ )
111
+
112
+ def render_planes(self, frame: ModelFrame | None) -> tuple[float, float]:
113
+ cfg = self.config
114
+ if frame is None:
115
+ return cfg.znear, cfg.zfar
116
+ return cfg.znear / frame.scale, cfg.zfar / frame.scale
117
+
118
+ @property
119
+ def background(self) -> tuple[float, float, float]:
120
+ return tuple(self.config.background)
121
+
122
+ @property
123
+ def render_settings(self) -> dict[str, Any]:
124
+ cfg = self.config
125
+ return {
126
+ "radius_clip": cfg.radius_clip,
127
+ "rasterize_mode": cfg.rasterize_mode,
128
+ "clamp": cfg.clamp,
129
+ }
130
+
131
+ # --- fitting inputs ---------------------------------------------------------
132
+
133
+ def fit_views(self, views: Views, image_shape: list[int] | None = None) -> Views:
134
+ """`process_image` for `crop_mode="crop"`, the ffgs rules otherwise; the
135
+ images are quantised to 8 bit first (upstream reads 8-bit files)."""
136
+ cfg = self.config
137
+ shape = tuple(image_shape or cfg.image_shape)
138
+ scaled = upstream_scaled_shape(views.image_shape, shape)
139
+ k = None if views.intrinsics is None else views.normalized_k
140
+ images, ks = [], []
141
+ for i, img in enumerate(views.images):
142
+ ki = torch.eye(3).expand(len(img), 3, 3) if k is None else k[i]
143
+ img = quantize(img)
144
+ if cfg.crop_mode == "crop":
145
+ img, ki = center_crop(self._resize(img, scaled), ki, shape)
146
+ else:
147
+ img, ki = fit_images(
148
+ img,
149
+ ki,
150
+ shape,
151
+ crop_mode=cfg.crop_mode,
152
+ resize=self._resize,
153
+ pad_value=cfg.pad_value,
154
+ )
155
+ images.append(img)
156
+ ks.append(ki)
157
+ return Views(
158
+ torch.stack(images),
159
+ None if k is None else torch.stack(ks),
160
+ views.c2w,
161
+ normalized_intrinsics=True,
162
+ )
163
+
164
+ def fit_cameras(
165
+ self, cameras: Cameras, image_shape: list[int] | None = None
166
+ ) -> Cameras:
167
+ cfg = self.config
168
+ if cfg.crop_mode != "crop":
169
+ return super().fit_cameras(cameras, image_shape)
170
+ shape = tuple(image_shape or cfg.image_shape)
171
+ h, w = upstream_scaled_shape(cameras.image_shape, shape)
172
+ k = cameras.normalized_k.clone()
173
+ k[..., 0, 0] *= w / shape[1]
174
+ k[..., 1, 1] *= h / shape[0]
175
+ return Cameras(cameras.c2w, k, shape, normalized_intrinsics=True)
176
+
177
+ def _resize(self, images: torch.Tensor, shape: tuple[int, int]) -> torch.Tensor:
178
+ if self.resize is not None:
179
+ return self.resize(images, shape)
180
+ if self.config.resize_mode == "bicubic":
181
+ return pil_bicubic(images, shape)
182
+ return rescale(images, shape, self.config.resize_mode)
183
+
184
+
185
+ def upstream_scaled_shape(
186
+ image_shape: tuple[int, int], shape: tuple[int, int]
187
+ ) -> tuple[int, int]:
188
+ """The size `process_image` resizes an `image_shape` image to before cropping
189
+ it to `shape`: the covering side exactly, the other by `int` (truncation)."""
190
+ h, w = image_shape
191
+ h_out, w_out = shape
192
+ if h_out / h > w_out / w: # upstream (square shape): width > height
193
+ return h_out, int(w * (h_out / h))
194
+ return int(h * (w_out / w)), w_out
195
+
196
+
197
+ def quantize(images: torch.Tensor) -> torch.Tensor:
198
+ """[0, 1] images -> the nearest 8-bit values, as `ToTensor` gives them."""
199
+ return (images * 255).round().clamp(0, 255).to(torch.uint8).float() / 255
200
+
201
+
202
+ def pil_bicubic(images: torch.Tensor, shape: tuple[int, int]) -> torch.Tensor:
203
+ """[..., 3, h, w] -> [..., 3, *shape]: PIL's bicubic resize (`Image.resize`'s
204
+ default) of the 8-bit images, then `ToTensor`'s / 255."""
205
+ *batch, c, h, w = images.shape
206
+ device, dtype = images.device, images.dtype
207
+ h_out, w_out = shape
208
+ arrays = (images.reshape(-1, c, h, w) * 255).round().clamp(0, 255)
209
+ arrays = arrays.to(torch.uint8).permute(0, 2, 3, 1).contiguous().cpu().numpy()
210
+ resized = [
211
+ np.asarray(Image.fromarray(a).resize((w_out, h_out), Image.BICUBIC))
212
+ for a in arrays
213
+ ]
214
+ out = torch.from_numpy(np.stack(resized)).permute(0, 3, 1, 2).float() / 255
215
+ return out.to(device=device, dtype=dtype).reshape(*batch, c, h_out, w_out)
216
+
217
+
218
+ def neutralize_padding(gaussians: Gaussians, counts: torch.Tensor) -> Gaussians:
219
+ """Make the padding of a batch inert: Gaussians past each sample's count.
220
+
221
+ Upstream pads the samples of a batch to the largest Gaussian count with
222
+ placeholder values (means -1e4, scales 0, colours -1e10, opacity 0) that a
223
+ ply cannot hold (log 0). Each placeholder becomes a transparent copy of its
224
+ sample's first Gaussian: opacity 0, colour 0, same mean, scale and rotation.
225
+ It renders nothing, stays within the sample's extent and is left out of
226
+ plys. Samples without padding (any batch of one) are returned unchanged.
227
+ """
228
+ n = gaussians.num_gaussians
229
+ counts = counts.to(gaussians.means.device)
230
+ if bool((counts >= n).all()):
231
+ return gaussians
232
+ valid = torch.arange(n, device=counts.device)[None] < counts[:, None] # [B, N]
233
+
234
+ def first(x: torch.Tensor) -> torch.Tensor:
235
+ mask = valid.view(*valid.shape, *([1] * (x.dim() - 2)))
236
+ return torch.where(mask, x, x[:, :1])
237
+
238
+ return Gaussians(
239
+ means=first(gaussians.means),
240
+ scales=first(gaussians.scales),
241
+ quats=first(gaussians.quats),
242
+ opacities=torch.where(valid, gaussians.opacities, 0.0),
243
+ colors=torch.where(
244
+ valid.view(*valid.shape, *([1] * (gaussians.colors.dim() - 2))),
245
+ gaussians.colors,
246
+ 0.0,
247
+ ),
248
+ sh_degree=gaussians.sh_degree,
249
+ )
File without changes
File without changes
@@ -0,0 +1,181 @@
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/heads/camera_head.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
+
16
+ import torch
17
+ import torch.nn as nn
18
+
19
+ from ..layers import Mlp
20
+ from ..layers.block import Block
21
+ from .head_act import activate_pose
22
+
23
+
24
+ class CameraHead(nn.Module):
25
+ """
26
+ CameraHead predicts camera parameters from token representations using iterative refinement.
27
+
28
+ It applies a series of transformer blocks (the "trunk") to dedicated camera tokens.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ dim_in: int = 2048,
34
+ trunk_depth: int = 4,
35
+ pose_encoding_type: str = "absT_quaR_FoV",
36
+ num_heads: int = 16,
37
+ mlp_ratio: int = 4,
38
+ init_values: float = 0.01,
39
+ trans_act: str = "linear",
40
+ quat_act: str = "linear",
41
+ fl_act: str = "relu", # Field of view activations: ensures FOV values are positive.
42
+ ):
43
+ super().__init__()
44
+
45
+ if pose_encoding_type == "absT_quaR_FoV":
46
+ self.target_dim = 9
47
+ else:
48
+ raise ValueError(f"Unsupported camera encoding type: {pose_encoding_type}")
49
+
50
+ self.trans_act = trans_act
51
+ self.quat_act = quat_act
52
+ self.fl_act = fl_act
53
+ self.trunk_depth = trunk_depth
54
+
55
+ # Build the trunk using a sequence of transformer blocks.
56
+ self.trunk = nn.Sequential(
57
+ *[
58
+ Block(
59
+ dim=dim_in,
60
+ num_heads=num_heads,
61
+ mlp_ratio=mlp_ratio,
62
+ init_values=init_values,
63
+ )
64
+ for _ in range(trunk_depth)
65
+ ]
66
+ )
67
+
68
+ # Normalizations for camera token and trunk output.
69
+ self.token_norm = nn.LayerNorm(dim_in)
70
+ self.trunk_norm = nn.LayerNorm(dim_in)
71
+
72
+ # Learnable empty camera pose token.
73
+ self.empty_pose_tokens = nn.Parameter(torch.zeros(1, 1, self.target_dim))
74
+ self.embed_pose = nn.Linear(self.target_dim, dim_in)
75
+
76
+ # Module for producing modulation parameters: shift, scale, and a gate.
77
+ self.poseLN_modulation = nn.Sequential(
78
+ nn.SiLU(), nn.Linear(dim_in, 3 * dim_in, bias=True)
79
+ )
80
+
81
+ # Adaptive layer normalization without affine parameters.
82
+ self.adaln_norm = nn.LayerNorm(dim_in, elementwise_affine=False, eps=1e-6)
83
+ self.pose_branch = Mlp(
84
+ in_features=dim_in,
85
+ hidden_features=dim_in // 2,
86
+ out_features=self.target_dim,
87
+ drop=0,
88
+ )
89
+
90
+ def forward(self, aggregated_tokens_list: list, num_iterations: int = 4) -> list:
91
+ """
92
+ Forward pass to predict camera parameters.
93
+
94
+ Args:
95
+ aggregated_tokens_list (list): List of token tensors from the network;
96
+ the last tensor is used for prediction.
97
+ num_iterations (int, optional): Number of iterative refinement steps. Defaults to 4.
98
+
99
+ Returns:
100
+ list: A list of predicted camera encodings (post-activation) from each iteration.
101
+ """
102
+ # Use tokens from the last block for camera prediction.
103
+ tokens = aggregated_tokens_list[-1]
104
+
105
+ # Extract the camera tokens
106
+ pose_tokens = tokens[:, :, 0]
107
+
108
+ pose_tokens = self.token_norm(pose_tokens)
109
+
110
+ pred_pose_enc_list = self.trunk_fn(pose_tokens, num_iterations)
111
+ return pred_pose_enc_list
112
+
113
+ def trunk_fn(self, pose_tokens: torch.Tensor, num_iterations: int) -> list:
114
+ """
115
+ Iteratively refine camera pose predictions.
116
+
117
+ Args:
118
+ pose_tokens (torch.Tensor): Normalized camera tokens with shape [B, 1, C].
119
+ num_iterations (int): Number of refinement iterations.
120
+
121
+ Returns:
122
+ list: List of activated camera encodings from each iteration.
123
+ """
124
+ B, S, C = pose_tokens.shape # S is expected to be 1.
125
+ pred_pose_enc = None
126
+ pred_pose_enc_list = []
127
+
128
+ for _ in range(num_iterations):
129
+ # Use a learned empty pose for the first iteration.
130
+ if pred_pose_enc is None:
131
+ module_input = self.embed_pose(self.empty_pose_tokens.expand(B, S, -1))
132
+ else:
133
+ # Detach the previous prediction to avoid backprop through time.
134
+ pred_pose_enc = pred_pose_enc.detach()
135
+ module_input = self.embed_pose(pred_pose_enc)
136
+
137
+ # Generate modulation parameters and split them into shift, scale, and gate components.
138
+ shift_msa, scale_msa, gate_msa = self.poseLN_modulation(module_input).chunk(
139
+ 3, dim=-1
140
+ )
141
+
142
+ # Adaptive layer normalization and modulation.
143
+ pose_tokens_modulated = gate_msa * modulate(
144
+ self.adaln_norm(pose_tokens), shift_msa, scale_msa
145
+ )
146
+ pose_tokens_modulated = pose_tokens_modulated + pose_tokens
147
+
148
+ pose_tokens_modulated = torch.utils.checkpoint.checkpoint(
149
+ self.trunk,
150
+ pose_tokens_modulated,
151
+ use_reentrant=False,
152
+ )
153
+
154
+ # Compute the delta update for the pose encoding.
155
+ pred_pose_enc_delta = self.pose_branch(
156
+ self.trunk_norm(pose_tokens_modulated)
157
+ )
158
+
159
+ if pred_pose_enc is None:
160
+ pred_pose_enc = pred_pose_enc_delta
161
+ else:
162
+ pred_pose_enc = pred_pose_enc + pred_pose_enc_delta
163
+
164
+ # Apply final activation functions for translation, quaternion, and field-of-view.
165
+ activated_pose = activate_pose(
166
+ pred_pose_enc,
167
+ trans_act=self.trans_act,
168
+ quat_act=self.quat_act,
169
+ fl_act=self.fl_act,
170
+ )
171
+ pred_pose_enc_list.append(activated_pose)
172
+
173
+ return pred_pose_enc_list
174
+
175
+
176
+ def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
177
+ """
178
+ Modulate the input tensor using scaling and shifting parameters.
179
+ """
180
+ # modified from https://github.com/facebookresearch/DiT/blob/796c29e532f47bba17c5b9c5eb39b9354b8b7c64/models.py#L19
181
+ return x * (1 + scale) + shift