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,49 @@
|
|
|
1
|
+
"""AnySplat (Jiang, Mao, Xu, Lin, Xu, Gong, Dai, Wang, Lin, Zhou, Lin; ACM TOG /
|
|
2
|
+
SIGGRAPH Asia 2025), vendored from the official code at
|
|
3
|
+
https://github.com/InternRobotics/AnySplat (5f5e208).
|
|
4
|
+
|
|
5
|
+
Non-commercial: this directory holds MIT, CC BY-NC 4.0 (VGGT, as vendored by
|
|
6
|
+
AnySplat), CC BY-NC-SA 4.0 (Naver) and Apache-2.0 code, and the released weights
|
|
7
|
+
build on VGGT-1B; see LICENSE in this directory.
|
|
8
|
+
|
|
9
|
+
`model_type` "anysplat". Imported on first use: `import ffgs` does not import it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from ...registry import ModelSpec, register_model
|
|
17
|
+
from .evaluation import predict_target_cameras, split_llffhold, target_cameras
|
|
18
|
+
from .modeling import AnySplat
|
|
19
|
+
from .processing import AnySplatProcessor, AnySplatProcessorConfig
|
|
20
|
+
|
|
21
|
+
MODEL_TYPE = "anysplat"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def convert_state_dict(checkpoint: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
"""The official checkpoint (`lhjiang/anysplat` model.safetensors) -> `AnySplat`:
|
|
26
|
+
the parameter names are the upstream ones, so it is used as it is."""
|
|
27
|
+
return dict(checkpoint)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SPEC = register_model(
|
|
31
|
+
ModelSpec(
|
|
32
|
+
model_type=MODEL_TYPE,
|
|
33
|
+
model_cls=AnySplat,
|
|
34
|
+
processor_cls=AnySplatProcessor,
|
|
35
|
+
convert_state_dict=convert_state_dict,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"MODEL_TYPE",
|
|
41
|
+
"SPEC",
|
|
42
|
+
"AnySplat",
|
|
43
|
+
"AnySplatProcessor",
|
|
44
|
+
"AnySplatProcessorConfig",
|
|
45
|
+
"convert_state_dict",
|
|
46
|
+
"predict_target_cameras",
|
|
47
|
+
"split_llffhold",
|
|
48
|
+
"target_cameras",
|
|
49
|
+
]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lihan Jiang and Yucheng Mao
|
|
2
|
+
# SPDX-License-Identifier: MIT (see the AnySplat section of LICENSE, this directory)
|
|
3
|
+
#
|
|
4
|
+
# Modified by the libgs contributors, 2026, from src/model/encoder/anysplat.py and
|
|
5
|
+
# src/model/encoder/encoder.py of https://github.com/InternRobotics/AnySplat
|
|
6
|
+
# (5f5e208): inference-only subset of `EncoderAnySplat` (pred_head_type="depth",
|
|
7
|
+
# pose_free=True, the released configuration).
|
|
8
|
+
# - The VGGT modules are built directly from the architecture arguments instead of
|
|
9
|
+
# `VGGT.from_pretrained("facebook/VGGT-1B")` (whose weights the AnySplat checkpoint
|
|
10
|
+
# replaces anyway); the point / track heads it also built are not. The Gaussian
|
|
11
|
+
# head's `dim_in` is `2 * embed_dim` (upstream: the literal 2048, VGGT-1B's).
|
|
12
|
+
# - torch_scatter's `scatter_max` / `scatter_add` replaced by the same reductions in
|
|
13
|
+
# torch (`scatter_reduce_("amax")`; `scatter_add_` on a broadcast index, which is
|
|
14
|
+
# what torch_scatter's `scatter_add` runs).
|
|
15
|
+
# - The aggregated tokens are made float32 before the float32 heads: a no-op on
|
|
16
|
+
# CUDA, where the bfloat16 autocast already returns them in float32, and what lets
|
|
17
|
+
# the model run on other devices, where that autocast is inactive.
|
|
18
|
+
# - `infos["num_gaussians"]` records each sample's Gaussian count: for batches,
|
|
19
|
+
# upstream pads the samples to the largest count (`pad_tensor_list`).
|
|
20
|
+
# - Removed: distillation, parameter freezing, `normalize_pts3d` /
|
|
21
|
+
# `align_pts_all_with_pts3d`, the point-head branch, visualization dumps, the data
|
|
22
|
+
# shim, the `print`s and the unconditional `torch.cuda.empty_cache()`; einops /
|
|
23
|
+
# jaxtyping no longer needed. `EncoderOutput` keeps the fields used here.
|
|
24
|
+
|
|
25
|
+
"""AnySplat's encoder: VGGT aggregator, camera and depth heads, a DPT head for the
|
|
26
|
+
Gaussian parameters, and voxel fusion of the per-pixel Gaussians."""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import torch
|
|
34
|
+
from torch import Tensor, nn
|
|
35
|
+
|
|
36
|
+
from .gaussian_adapter import GaussianAdapterCfg, Gaussians, UnifiedGaussianAdapter
|
|
37
|
+
from .gs_head import VGGT_DPT_GS_Head
|
|
38
|
+
from .vggt.heads.camera_head import CameraHead
|
|
39
|
+
from .vggt.heads.dpt_head import DPTHead
|
|
40
|
+
from .vggt.models.aggregator import Aggregator
|
|
41
|
+
from .vggt.utils.geometry import batchify_unproject_depth_map_to_point_map
|
|
42
|
+
from .vggt.utils.pose_enc import pose_encoding_to_extri_intri
|
|
43
|
+
|
|
44
|
+
inf = float("inf")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OpacityMappingCfg:
|
|
49
|
+
initial: float
|
|
50
|
+
final: float
|
|
51
|
+
warm_up: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class GSHeadParams:
|
|
56
|
+
dec_depth: int = 23
|
|
57
|
+
patch_size: tuple[int, int] = (14, 14)
|
|
58
|
+
enc_embed_dim: int = 2048
|
|
59
|
+
dec_embed_dim: int = 2048
|
|
60
|
+
feature_dim: int = 256
|
|
61
|
+
depth_mode = ("exp", -inf, inf)
|
|
62
|
+
conf_mode = True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EncoderAnySplatCfg:
|
|
67
|
+
"""The inference fields of upstream's `EncoderAnySplatCfg` (same names and
|
|
68
|
+
defaults), and the VGGT architecture (ffgs: upstream fixes it to VGGT-1B)."""
|
|
69
|
+
|
|
70
|
+
voxel_size: float
|
|
71
|
+
gaussian_adapter: GaussianAdapterCfg
|
|
72
|
+
opacity_mapping: OpacityMappingCfg
|
|
73
|
+
gs_prune: bool = False
|
|
74
|
+
opacity_threshold: float = 0.001
|
|
75
|
+
gs_keep_ratio: float = 1.0
|
|
76
|
+
render_conf: bool = False
|
|
77
|
+
opacity_conf: bool = False
|
|
78
|
+
conf_threshold: float = 0.1
|
|
79
|
+
intermediate_layer_idx: list[int] | None = None
|
|
80
|
+
voxelize: bool = False
|
|
81
|
+
# VGGT-1B: `VGGT(img_size=518, patch_size=14, embed_dim=1024)`; extra keyword
|
|
82
|
+
# arguments of each module (all empty for the released model).
|
|
83
|
+
img_size: int = 518
|
|
84
|
+
patch_size: int = 14
|
|
85
|
+
embed_dim: int = 1024
|
|
86
|
+
aggregator: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
camera_head: dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
depth_head: dict[str, Any] = field(default_factory=dict)
|
|
89
|
+
gs_head: dict[str, Any] = field(default_factory=dict)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class EncoderOutput:
|
|
94
|
+
gaussians: Gaussians
|
|
95
|
+
pred_pose_enc_list: list[Tensor] | None
|
|
96
|
+
pred_context_pose: dict | None
|
|
97
|
+
depth_dict: dict | None
|
|
98
|
+
infos: dict | None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class EncoderAnySplat(nn.Module):
|
|
102
|
+
def __init__(self, cfg: EncoderAnySplatCfg) -> None:
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.cfg = cfg
|
|
105
|
+
dim_in = 2 * cfg.embed_dim
|
|
106
|
+
self.aggregator = Aggregator(
|
|
107
|
+
img_size=cfg.img_size,
|
|
108
|
+
patch_size=cfg.patch_size,
|
|
109
|
+
embed_dim=cfg.embed_dim,
|
|
110
|
+
**cfg.aggregator,
|
|
111
|
+
).to(torch.bfloat16)
|
|
112
|
+
self.camera_head = CameraHead(dim_in=dim_in, **cfg.camera_head)
|
|
113
|
+
self.depth_head = DPTHead(
|
|
114
|
+
dim_in=dim_in,
|
|
115
|
+
output_dim=2,
|
|
116
|
+
activation="exp",
|
|
117
|
+
conf_activation="expp1",
|
|
118
|
+
**cfg.depth_head,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.gaussian_adapter = UnifiedGaussianAdapter(cfg.gaussian_adapter)
|
|
122
|
+
|
|
123
|
+
self.raw_gs_dim = 1 + self.gaussian_adapter.d_in # 1 for opacity
|
|
124
|
+
self.voxel_size = cfg.voxel_size
|
|
125
|
+
# fake backbone for head parameters
|
|
126
|
+
head_params = GSHeadParams()
|
|
127
|
+
self.gaussian_param_head = VGGT_DPT_GS_Head(
|
|
128
|
+
dim_in=dim_in,
|
|
129
|
+
patch_size=head_params.patch_size,
|
|
130
|
+
output_dim=self.raw_gs_dim + 1,
|
|
131
|
+
activation="norm_exp",
|
|
132
|
+
conf_activation="expp1",
|
|
133
|
+
features=head_params.feature_dim,
|
|
134
|
+
**cfg.gs_head,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def map_pdf_to_opacity(
|
|
138
|
+
self,
|
|
139
|
+
pdf: Tensor,
|
|
140
|
+
global_step: int,
|
|
141
|
+
) -> Tensor:
|
|
142
|
+
# https://www.desmos.com/calculator/opvwti3ba9
|
|
143
|
+
|
|
144
|
+
# Figure out the exponent.
|
|
145
|
+
cfg = self.cfg.opacity_mapping
|
|
146
|
+
x = cfg.initial + min(global_step / cfg.warm_up, 1) * (cfg.final - cfg.initial)
|
|
147
|
+
exponent = 2**x
|
|
148
|
+
|
|
149
|
+
# Map the probability density to an opacity.
|
|
150
|
+
return 0.5 * (1 - (1 - pdf) ** exponent + pdf ** (1 / exponent))
|
|
151
|
+
|
|
152
|
+
def pad_tensor_list(self, tensor_list, pad_shape, value=0.0):
|
|
153
|
+
padded = []
|
|
154
|
+
for t in tensor_list:
|
|
155
|
+
pad_len = pad_shape[0] - t.shape[0]
|
|
156
|
+
if pad_len > 0:
|
|
157
|
+
padding = torch.full(
|
|
158
|
+
(pad_len, *t.shape[1:]), value, device=t.device, dtype=t.dtype
|
|
159
|
+
)
|
|
160
|
+
t = torch.cat([t, padding], dim=0)
|
|
161
|
+
padded.append(t)
|
|
162
|
+
return torch.stack(padded)
|
|
163
|
+
|
|
164
|
+
def voxelizaton_with_fusion(self, img_feat, pts3d, voxel_size, conf=None):
|
|
165
|
+
# img_feat: B*V, C, H, W
|
|
166
|
+
# pts3d: B*V, 3, H, W
|
|
167
|
+
V, C, H, W = img_feat.shape
|
|
168
|
+
pts3d_flatten = pts3d.permute(0, 2, 3, 1).flatten(0, 2)
|
|
169
|
+
|
|
170
|
+
voxel_indices = (pts3d_flatten / voxel_size).round().int() # [B*V*N, 3]
|
|
171
|
+
unique_voxels, inverse_indices, counts = torch.unique(
|
|
172
|
+
voxel_indices, dim=0, return_inverse=True, return_counts=True
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Flatten confidence scores and features
|
|
176
|
+
conf_flat = conf.flatten() # [B*V*N]
|
|
177
|
+
anchor_feats_flat = img_feat.permute(0, 2, 3, 1).flatten(0, 2) # [B*V*N, ...]
|
|
178
|
+
|
|
179
|
+
# Compute softmax weights per voxel
|
|
180
|
+
conf_voxel_max = _scatter_max(conf_flat, inverse_indices, len(unique_voxels))
|
|
181
|
+
conf_exp = torch.exp(conf_flat - conf_voxel_max[inverse_indices])
|
|
182
|
+
voxel_weights = _scatter_add(
|
|
183
|
+
conf_exp, inverse_indices, len(unique_voxels)
|
|
184
|
+
) # [num_unique_voxels]
|
|
185
|
+
weights = (conf_exp / (voxel_weights[inverse_indices] + 1e-6)).unsqueeze(
|
|
186
|
+
-1
|
|
187
|
+
) # [B*V*N, 1]
|
|
188
|
+
|
|
189
|
+
# Compute weighted average of positions and features
|
|
190
|
+
weighted_pts = pts3d_flatten * weights
|
|
191
|
+
weighted_feats = anchor_feats_flat.squeeze(1) * weights
|
|
192
|
+
|
|
193
|
+
# Aggregate per voxel
|
|
194
|
+
voxel_pts = _scatter_add(
|
|
195
|
+
weighted_pts, inverse_indices, len(unique_voxels)
|
|
196
|
+
) # [num_unique_voxels, 3]
|
|
197
|
+
voxel_feats = _scatter_add(
|
|
198
|
+
weighted_feats, inverse_indices, len(unique_voxels)
|
|
199
|
+
) # [num_unique_voxels, feat_dim]
|
|
200
|
+
|
|
201
|
+
return voxel_pts, voxel_feats
|
|
202
|
+
|
|
203
|
+
def forward(
|
|
204
|
+
self,
|
|
205
|
+
image: torch.Tensor,
|
|
206
|
+
global_step: int = 0,
|
|
207
|
+
) -> EncoderOutput:
|
|
208
|
+
device = image.device
|
|
209
|
+
b, v, _, h, w = image.shape
|
|
210
|
+
|
|
211
|
+
with torch.amp.autocast("cuda", enabled=True, dtype=torch.bfloat16):
|
|
212
|
+
aggregated_tokens_list, patch_start_idx = self.aggregator(
|
|
213
|
+
image.to(torch.bfloat16),
|
|
214
|
+
intermediate_layer_idx=self.cfg.intermediate_layer_idx,
|
|
215
|
+
)
|
|
216
|
+
aggregated_tokens_list = [t.float() for t in aggregated_tokens_list]
|
|
217
|
+
|
|
218
|
+
with torch.amp.autocast("cuda", enabled=False):
|
|
219
|
+
pred_pose_enc_list = self.camera_head(aggregated_tokens_list)
|
|
220
|
+
last_pred_pose_enc = pred_pose_enc_list[-1]
|
|
221
|
+
extrinsic, intrinsic = pose_encoding_to_extri_intri(
|
|
222
|
+
last_pred_pose_enc, image.shape[-2:]
|
|
223
|
+
) # only for debug
|
|
224
|
+
|
|
225
|
+
depth_map, depth_conf = self.depth_head(
|
|
226
|
+
aggregated_tokens_list,
|
|
227
|
+
images=image,
|
|
228
|
+
patch_start_idx=patch_start_idx,
|
|
229
|
+
)
|
|
230
|
+
pts_all = batchify_unproject_depth_map_to_point_map(
|
|
231
|
+
depth_map, extrinsic, intrinsic
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if self.cfg.render_conf:
|
|
235
|
+
conf_valid = torch.quantile(
|
|
236
|
+
depth_conf.flatten(0, 1), self.cfg.conf_threshold
|
|
237
|
+
)
|
|
238
|
+
conf_valid_mask = depth_conf > conf_valid
|
|
239
|
+
else:
|
|
240
|
+
conf_valid_mask = torch.ones_like(depth_conf, dtype=torch.bool)
|
|
241
|
+
|
|
242
|
+
# dpt style gs_head input format
|
|
243
|
+
out = self.gaussian_param_head(
|
|
244
|
+
aggregated_tokens_list,
|
|
245
|
+
pts_all.flatten(0, 1).permute(0, 3, 1, 2),
|
|
246
|
+
image,
|
|
247
|
+
patch_start_idx=patch_start_idx,
|
|
248
|
+
image_size=(h, w),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
del aggregated_tokens_list, patch_start_idx
|
|
252
|
+
|
|
253
|
+
pts_flat = pts_all.flatten(2, 3)
|
|
254
|
+
scene_scale = pts_flat.norm(dim=-1).mean().clip(min=1e-8)
|
|
255
|
+
|
|
256
|
+
anchor_feats, conf = out[:, :, : self.raw_gs_dim], out[:, :, self.raw_gs_dim]
|
|
257
|
+
|
|
258
|
+
neural_feats_list, neural_pts_list = [], []
|
|
259
|
+
if self.cfg.voxelize:
|
|
260
|
+
for b_i in range(b):
|
|
261
|
+
neural_pts, neural_feats = self.voxelizaton_with_fusion(
|
|
262
|
+
anchor_feats[b_i],
|
|
263
|
+
pts_all[b_i].permute(0, 3, 1, 2).contiguous(),
|
|
264
|
+
self.voxel_size,
|
|
265
|
+
conf=conf[b_i],
|
|
266
|
+
)
|
|
267
|
+
neural_feats_list.append(neural_feats)
|
|
268
|
+
neural_pts_list.append(neural_pts)
|
|
269
|
+
else:
|
|
270
|
+
for b_i in range(b):
|
|
271
|
+
neural_feats_list.append(
|
|
272
|
+
anchor_feats[b_i].permute(0, 2, 3, 1)[conf_valid_mask[b_i]]
|
|
273
|
+
)
|
|
274
|
+
neural_pts_list.append(pts_all[b_i][conf_valid_mask[b_i]])
|
|
275
|
+
|
|
276
|
+
# ffgs: the per-sample Gaussian counts, before padding to the batch maximum.
|
|
277
|
+
num_gaussians = [f.shape[0] for f in neural_feats_list]
|
|
278
|
+
max_voxels = max(f.shape[0] for f in neural_feats_list)
|
|
279
|
+
neural_feats = self.pad_tensor_list(
|
|
280
|
+
neural_feats_list, (max_voxels,), value=-1e10
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
neural_pts = self.pad_tensor_list(
|
|
284
|
+
neural_pts_list, (max_voxels,), -1e4
|
|
285
|
+
) # -1 == invalid voxel
|
|
286
|
+
|
|
287
|
+
depths = neural_pts[..., -1].unsqueeze(-1)
|
|
288
|
+
densities = neural_feats[..., 0].sigmoid()
|
|
289
|
+
|
|
290
|
+
assert len(densities.shape) == 2, "the shape of densities should be (B, N)"
|
|
291
|
+
assert neural_pts.shape[1] > 1, "the number of voxels should be greater than 1"
|
|
292
|
+
|
|
293
|
+
opacity = self.map_pdf_to_opacity(densities, global_step).squeeze(-1)
|
|
294
|
+
if self.cfg.opacity_conf:
|
|
295
|
+
shift = torch.quantile(depth_conf, self.cfg.conf_threshold)
|
|
296
|
+
opacity = opacity * torch.sigmoid(depth_conf - shift)[
|
|
297
|
+
conf_valid_mask
|
|
298
|
+
].unsqueeze(
|
|
299
|
+
0
|
|
300
|
+
) # little bit hacky
|
|
301
|
+
|
|
302
|
+
# GS Prune, but only works when bs = 1
|
|
303
|
+
# if want to support bs > 1, need to random prune gaussians based on the rank of opacity like LongLRM
|
|
304
|
+
# Note: we not prune gaussians here, but we will try it in the future
|
|
305
|
+
if self.cfg.gs_prune and b == 1:
|
|
306
|
+
opacity_threshold = self.cfg.opacity_threshold
|
|
307
|
+
gaussian_usage = opacity > opacity_threshold # (B, N)
|
|
308
|
+
|
|
309
|
+
if (gaussian_usage.sum() / gaussian_usage.numel()) > self.cfg.gs_keep_ratio:
|
|
310
|
+
# rank by opacity
|
|
311
|
+
num_keep = int(gaussian_usage.shape[1] * self.cfg.gs_keep_ratio)
|
|
312
|
+
idx_sort = opacity.argsort(dim=1, descending=True)
|
|
313
|
+
keep_idx = idx_sort[:, :num_keep]
|
|
314
|
+
gaussian_usage = torch.zeros_like(gaussian_usage, dtype=torch.bool)
|
|
315
|
+
gaussian_usage.scatter_(1, keep_idx, True)
|
|
316
|
+
|
|
317
|
+
neural_pts = neural_pts[gaussian_usage].view(b, -1, 3).contiguous()
|
|
318
|
+
depths = depths[gaussian_usage].view(b, -1, 1).contiguous()
|
|
319
|
+
neural_feats = (
|
|
320
|
+
neural_feats[gaussian_usage].view(b, -1, self.raw_gs_dim).contiguous()
|
|
321
|
+
)
|
|
322
|
+
opacity = opacity[gaussian_usage].view(b, -1).contiguous()
|
|
323
|
+
|
|
324
|
+
gaussians = self.gaussian_adapter.forward(
|
|
325
|
+
neural_pts,
|
|
326
|
+
depths,
|
|
327
|
+
opacity,
|
|
328
|
+
neural_feats[..., 1:].squeeze(2),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
infos = {}
|
|
332
|
+
infos["scene_scale"] = scene_scale
|
|
333
|
+
infos["voxelize_ratio"] = densities.shape[1] / (h * w * v)
|
|
334
|
+
if self.cfg.gs_prune and b == 1:
|
|
335
|
+
num_gaussians = [neural_pts.shape[1]]
|
|
336
|
+
infos["num_gaussians"] = torch.tensor(num_gaussians, device=device)
|
|
337
|
+
|
|
338
|
+
extrinsic_padding = (
|
|
339
|
+
torch.tensor([0, 0, 0, 1], device=device, dtype=extrinsic.dtype)
|
|
340
|
+
.view(1, 1, 1, 4)
|
|
341
|
+
.repeat(b, v, 1, 1)
|
|
342
|
+
)
|
|
343
|
+
intrinsic = intrinsic.clone() # Create a new tensor
|
|
344
|
+
intrinsic = torch.stack(
|
|
345
|
+
[intrinsic[:, :, 0] / w, intrinsic[:, :, 1] / h, intrinsic[:, :, 2]], dim=2
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return EncoderOutput(
|
|
349
|
+
gaussians=gaussians,
|
|
350
|
+
pred_pose_enc_list=pred_pose_enc_list,
|
|
351
|
+
pred_context_pose=dict(
|
|
352
|
+
extrinsic=torch.cat([extrinsic, extrinsic_padding], dim=2).inverse(),
|
|
353
|
+
intrinsic=intrinsic,
|
|
354
|
+
),
|
|
355
|
+
depth_dict=dict(depth=depth_map, conf_valid_mask=conf_valid_mask),
|
|
356
|
+
infos=infos,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _scatter_max(src: Tensor, index: Tensor, size: int) -> Tensor:
|
|
361
|
+
"""torch_scatter's `scatter_max(src, index, dim=0)[0]` for 1-D `src` whose
|
|
362
|
+
`index` hits every one of `size` slots (the maximum is exact in any order)."""
|
|
363
|
+
out = src.new_empty(size)
|
|
364
|
+
return out.scatter_reduce_(0, index, src, "amax", include_self=False)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _scatter_add(src: Tensor, index: Tensor, size: int) -> Tensor:
|
|
368
|
+
"""torch_scatter's `scatter_add(src, index, dim=0)`: its implementation, a
|
|
369
|
+
`scatter_add_` into zeros along an index broadcast to `src`."""
|
|
370
|
+
index = index.view(-1, *([1] * (src.dim() - 1))).expand_as(src)
|
|
371
|
+
out = src.new_zeros(size, *src.shape[1:])
|
|
372
|
+
return out.scatter_add_(0, index, src)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lihan Jiang and Yucheng Mao
|
|
2
|
+
# SPDX-License-Identifier: MIT (see the AnySplat section of LICENSE, this directory)
|
|
3
|
+
#
|
|
4
|
+
# Modified by the libgs contributors, 2026, from src/eval_nvs.py of
|
|
5
|
+
# https://github.com/InternRobotics/AnySplat (5f5e208): the target-camera prediction
|
|
6
|
+
# of its `evaluate` as functions; the scale factor is taken per sample (upstream
|
|
7
|
+
# evaluates one scene at a time, where the two agree).
|
|
8
|
+
|
|
9
|
+
"""The official novel-view-synthesis protocol of AnySplat (`src/eval_nvs.py`).
|
|
10
|
+
|
|
11
|
+
AnySplat predicts its cameras; the target views of an evaluation are not posed in
|
|
12
|
+
its frame. Upstream places them there by running the aggregator and camera head
|
|
13
|
+
once more, on the context and target images together, and rescaling the predicted
|
|
14
|
+
target translations by the ratio of the mean context translations of the two
|
|
15
|
+
predictions:
|
|
16
|
+
|
|
17
|
+
>>> out = pipe(context) # context views without c2w: out.frame is None
|
|
18
|
+
>>> cameras = target_cameras(pipe, out, context, targets)
|
|
19
|
+
>>> images = pipe.render(out, cameras)["images"]
|
|
20
|
+
|
|
21
|
+
Upstream holds out every `llffhold`-th image (`idx % 8 == 0`) as a target and uses
|
|
22
|
+
the others as context (`split_llffhold`).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
from ...types import Cameras, Views
|
|
30
|
+
from .modeling import AnySplat
|
|
31
|
+
from .vggt.utils.pose_enc import pose_encoding_to_extri_intri
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def split_llffhold(num_views: int, llffhold: int = 8) -> tuple[list[int], list[int]]:
|
|
35
|
+
"""(context, target) view indices: targets are the indices divisible by
|
|
36
|
+
`llffhold`, as `eval_nvs.py` splits a scene."""
|
|
37
|
+
context = [i for i in range(num_views) if i % llffhold != 0]
|
|
38
|
+
target = [i for i in range(num_views) if i % llffhold == 0]
|
|
39
|
+
return context, target
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def target_cameras(pipe, output, context: Views, targets: Views) -> Cameras:
|
|
43
|
+
"""Cameras of `targets` in the model frame of `output` (= `pipe(context)`, run
|
|
44
|
+
without poses), by the official protocol. Render them with `pipe.render`."""
|
|
45
|
+
if output.frame is not None or output.space != "model":
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"the official protocol places targets in the model's own frame: run "
|
|
48
|
+
"the pipeline on context views without c2w"
|
|
49
|
+
)
|
|
50
|
+
processor, device = pipe.processor, pipe.device
|
|
51
|
+
context_images = processor.preprocess(context, device).model_input
|
|
52
|
+
target_images = processor.preprocess(targets, device).model_input
|
|
53
|
+
return predict_target_cameras(
|
|
54
|
+
pipe.model, context_images, target_images, output.cameras.c2w
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@torch.no_grad()
|
|
59
|
+
def predict_target_cameras(
|
|
60
|
+
model: AnySplat,
|
|
61
|
+
context_images: torch.Tensor,
|
|
62
|
+
target_images: torch.Tensor,
|
|
63
|
+
context_c2w: torch.Tensor,
|
|
64
|
+
) -> Cameras:
|
|
65
|
+
"""`eval_nvs.py`'s target cameras: model inputs [B, V, 3, H, W] of the context
|
|
66
|
+
and target views, and the context c2w [B, V, 4, 4] the encoder predicted for
|
|
67
|
+
the context views alone (`pred_context_pose["extrinsic"]`)."""
|
|
68
|
+
encoder = model.encoder
|
|
69
|
+
b, num_context = context_images.shape[:2]
|
|
70
|
+
images = torch.cat((context_images, target_images), dim=1).to(torch.bfloat16)
|
|
71
|
+
h, w = images.shape[-2:]
|
|
72
|
+
with torch.amp.autocast("cuda", enabled=False):
|
|
73
|
+
tokens, _ = encoder.aggregator(
|
|
74
|
+
images, intermediate_layer_idx=encoder.cfg.intermediate_layer_idx
|
|
75
|
+
)
|
|
76
|
+
tokens = [token.float() for token in tokens]
|
|
77
|
+
pose_enc = encoder.camera_head(tokens)[-1]
|
|
78
|
+
extrinsic, intrinsic = pose_encoding_to_extri_intri(pose_enc, (h, w))
|
|
79
|
+
|
|
80
|
+
padding = torch.tensor([0, 0, 0, 1], device=extrinsic.device, dtype=extrinsic.dtype)
|
|
81
|
+
padding = padding.view(1, 1, 1, 4).repeat(b, images.shape[1], 1, 1)
|
|
82
|
+
c2w = torch.cat([extrinsic, padding], dim=2).inverse()
|
|
83
|
+
intrinsic = intrinsic.clone()
|
|
84
|
+
intrinsic[:, :, 0] = intrinsic[:, :, 0] / w
|
|
85
|
+
intrinsic[:, :, 1] = intrinsic[:, :, 1] / h
|
|
86
|
+
|
|
87
|
+
# Upstream: the mean over all context translation components (signed).
|
|
88
|
+
scale = context_c2w[:, :, :3, 3].flatten(1).mean(1) / c2w[
|
|
89
|
+
:, :num_context, :3, 3
|
|
90
|
+
].flatten(1).mean(1)
|
|
91
|
+
target_c2w = c2w[:, num_context:].clone()
|
|
92
|
+
target_c2w[..., :3, 3] = target_c2w[..., :3, 3] * scale[:, None, None]
|
|
93
|
+
return Cameras(
|
|
94
|
+
target_c2w.float(),
|
|
95
|
+
intrinsic[:, num_context:].float(),
|
|
96
|
+
(h, w),
|
|
97
|
+
normalized_intrinsics=True,
|
|
98
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lihan Jiang and Yucheng Mao
|
|
2
|
+
# SPDX-License-Identifier: MIT (see the AnySplat section of LICENSE, this directory)
|
|
3
|
+
#
|
|
4
|
+
# Modified by the libgs contributors, 2026, from
|
|
5
|
+
# src/model/encoder/common/gaussian_adapter.py and src/model/types.py of
|
|
6
|
+
# https://github.com/InternRobotics/AnySplat (5f5e208): inference-only subset. Only
|
|
7
|
+
# `UnifiedGaussianAdapter` (the adapter of `pose_free=True`) and what it uses from
|
|
8
|
+
# its base class are kept; the covariances (`build_covariance`) are not built, as
|
|
9
|
+
# ffgs renders from scales and quaternions; the unused `intrinsics` / `coordinates`
|
|
10
|
+
# arguments of `forward` dropped; einops' rearrange replaced by the equivalent
|
|
11
|
+
# `unflatten`, jaxtyping annotations by plain `Tensor`.
|
|
12
|
+
|
|
13
|
+
"""Raw Gaussian features -> Gaussian parameters (AnySplat's pose-free adapter)."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn.functional as F
|
|
21
|
+
from torch import Tensor, nn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class GaussianAdapterCfg:
|
|
26
|
+
gaussian_scale_min: float
|
|
27
|
+
gaussian_scale_max: float
|
|
28
|
+
sh_degree: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Gaussians:
|
|
33
|
+
"""Upstream's Gaussians, without covariances. Quaternions are xyzw (scipy
|
|
34
|
+
order); harmonics are [batch, gaussian, 3, d_sh]."""
|
|
35
|
+
|
|
36
|
+
means: Tensor # [batch, gaussian, 3]
|
|
37
|
+
harmonics: Tensor # [batch, gaussian, 3, d_sh]
|
|
38
|
+
opacities: Tensor # [batch, gaussian]
|
|
39
|
+
scales: Tensor # [batch, gaussian, 3]
|
|
40
|
+
rotations: Tensor # [batch, gaussian, 4], xyzw
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GaussianAdapter(nn.Module):
|
|
44
|
+
cfg: GaussianAdapterCfg
|
|
45
|
+
|
|
46
|
+
def __init__(self, cfg: GaussianAdapterCfg):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.cfg = cfg
|
|
49
|
+
|
|
50
|
+
# Create a mask for the spherical harmonics coefficients. This ensures that at
|
|
51
|
+
# initialization, the coefficients are biased towards having a large DC
|
|
52
|
+
# component and small view-dependent components.
|
|
53
|
+
self.register_buffer(
|
|
54
|
+
"sh_mask",
|
|
55
|
+
torch.ones((self.d_sh,), dtype=torch.float32),
|
|
56
|
+
persistent=False,
|
|
57
|
+
)
|
|
58
|
+
for degree in range(1, self.cfg.sh_degree + 1):
|
|
59
|
+
self.sh_mask[degree**2 : (degree + 1) ** 2] = 0.1 * 0.25**degree
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def d_sh(self) -> int:
|
|
63
|
+
return (self.cfg.sh_degree + 1) ** 2
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def d_in(self) -> int:
|
|
67
|
+
return 7 + 3 * self.d_sh
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UnifiedGaussianAdapter(GaussianAdapter):
|
|
71
|
+
def forward(
|
|
72
|
+
self,
|
|
73
|
+
means: Tensor,
|
|
74
|
+
depths: Tensor,
|
|
75
|
+
opacities: Tensor,
|
|
76
|
+
raw_gaussians: Tensor,
|
|
77
|
+
eps: float = 1e-8,
|
|
78
|
+
) -> Gaussians:
|
|
79
|
+
scales, rotations, sh = raw_gaussians.split((3, 4, 3 * self.d_sh), dim=-1)
|
|
80
|
+
|
|
81
|
+
scales = 0.001 * F.softplus(scales)
|
|
82
|
+
scales = scales.clamp_max(0.3)
|
|
83
|
+
|
|
84
|
+
# Normalize the quaternion features to yield a valid quaternion.
|
|
85
|
+
rotations = rotations / (rotations.norm(dim=-1, keepdim=True) + eps)
|
|
86
|
+
|
|
87
|
+
sh = sh.unflatten(-1, (3, self.d_sh))
|
|
88
|
+
sh = sh.broadcast_to((*opacities.shape, 3, self.d_sh)) * self.sh_mask
|
|
89
|
+
|
|
90
|
+
return Gaussians(
|
|
91
|
+
means=means.float(),
|
|
92
|
+
harmonics=sh.float(),
|
|
93
|
+
opacities=opacities.float(),
|
|
94
|
+
scales=scales.float(),
|
|
95
|
+
rotations=rotations.float(),
|
|
96
|
+
)
|