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