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,144 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
#
|
|
7
|
+
# Modified by the libgs contributors, 2026, from
|
|
8
|
+
# src/model/encoder/vggt/utils/pose_enc.py of https://github.com/InternRobotics/AnySplat
|
|
9
|
+
# (5f5e208): formatted with black, unused imports removed.
|
|
10
|
+
# "The license found in the LICENSE file" above is the one VGGT's code
|
|
11
|
+
# had when AnySplat vendored it (2025-06-30): CC BY-NC 4.0, non-commercial
|
|
12
|
+
# (facebookresearch/vggt c6bf698); see LICENSE (this directory).
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
from .rotation import mat_to_quat, quat_to_mat
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extri_intri_to_pose_encoding(
|
|
20
|
+
extrinsics,
|
|
21
|
+
intrinsics,
|
|
22
|
+
image_size_hw=None, # e.g., (256, 512)
|
|
23
|
+
pose_encoding_type="absT_quaR_FoV",
|
|
24
|
+
):
|
|
25
|
+
"""Convert camera extrinsics and intrinsics to a compact pose encoding.
|
|
26
|
+
|
|
27
|
+
This function transforms camera parameters into a unified pose encoding format,
|
|
28
|
+
which can be used for various downstream tasks like pose prediction or representation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
extrinsics (torch.Tensor): Camera extrinsic parameters with shape BxSx3x4,
|
|
32
|
+
where B is batch size and S is sequence length.
|
|
33
|
+
In OpenCV coordinate system (x-right, y-down, z-forward), representing camera from world transformation.
|
|
34
|
+
The format is [R|t] where R is a 3x3 rotation matrix and t is a 3x1 translation vector.
|
|
35
|
+
intrinsics (torch.Tensor): Camera intrinsic parameters with shape BxSx3x3.
|
|
36
|
+
Defined in pixels, with format:
|
|
37
|
+
[[fx, 0, cx],
|
|
38
|
+
[0, fy, cy],
|
|
39
|
+
[0, 0, 1]]
|
|
40
|
+
where fx, fy are focal lengths and (cx, cy) is the principal point
|
|
41
|
+
image_size_hw (tuple): Tuple of (height, width) of the image in pixels.
|
|
42
|
+
Required for computing field of view values. For example: (256, 512).
|
|
43
|
+
pose_encoding_type (str): Type of pose encoding to use. Currently only
|
|
44
|
+
supports "absT_quaR_FoV" (absolute translation, quaternion rotation, field of view).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
torch.Tensor: Encoded camera pose parameters with shape BxSx9.
|
|
48
|
+
For "absT_quaR_FoV" type, the 9 dimensions are:
|
|
49
|
+
- [:3] = absolute translation vector T (3D)
|
|
50
|
+
- [3:7] = rotation as quaternion quat (4D)
|
|
51
|
+
- [7:] = field of view (2D)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# extrinsics: BxSx3x4
|
|
55
|
+
# intrinsics: BxSx3x3
|
|
56
|
+
|
|
57
|
+
if pose_encoding_type == "absT_quaR_FoV":
|
|
58
|
+
R = extrinsics[:, :, :3, :3] # BxSx3x3
|
|
59
|
+
T = extrinsics[:, :, :3, 3] # BxSx3
|
|
60
|
+
|
|
61
|
+
quat = mat_to_quat(R)
|
|
62
|
+
# Note the order of h and w here
|
|
63
|
+
H, W = image_size_hw
|
|
64
|
+
fov_h = 2 * torch.atan((H / 2) / intrinsics[..., 1, 1])
|
|
65
|
+
fov_w = 2 * torch.atan((W / 2) / intrinsics[..., 0, 0])
|
|
66
|
+
pose_encoding = torch.cat(
|
|
67
|
+
[T, quat, fov_h[..., None], fov_w[..., None]], dim=-1
|
|
68
|
+
).float()
|
|
69
|
+
else:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
return pose_encoding
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def pose_encoding_to_extri_intri(
|
|
76
|
+
pose_encoding,
|
|
77
|
+
image_size_hw=None, # e.g., (256, 512)
|
|
78
|
+
pose_encoding_type="absT_quaR_FoV",
|
|
79
|
+
build_intrinsics=True,
|
|
80
|
+
):
|
|
81
|
+
"""Convert a pose encoding back to camera extrinsics and intrinsics.
|
|
82
|
+
|
|
83
|
+
This function performs the inverse operation of extri_intri_to_pose_encoding,
|
|
84
|
+
reconstructing the full camera parameters from the compact encoding.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
pose_encoding (torch.Tensor): Encoded camera pose parameters with shape BxSx9,
|
|
88
|
+
where B is batch size and S is sequence length.
|
|
89
|
+
For "absT_quaR_FoV" type, the 9 dimensions are:
|
|
90
|
+
- [:3] = absolute translation vector T (3D)
|
|
91
|
+
- [3:7] = rotation as quaternion quat (4D)
|
|
92
|
+
- [7:] = field of view (2D)
|
|
93
|
+
image_size_hw (tuple): Tuple of (height, width) of the image in pixels.
|
|
94
|
+
Required for reconstructing intrinsics from field of view values.
|
|
95
|
+
For example: (256, 512).
|
|
96
|
+
pose_encoding_type (str): Type of pose encoding used. Currently only
|
|
97
|
+
supports "absT_quaR_FoV" (absolute translation, quaternion rotation, field of view).
|
|
98
|
+
build_intrinsics (bool): Whether to reconstruct the intrinsics matrix.
|
|
99
|
+
If False, only extrinsics are returned and intrinsics will be None.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
tuple: (extrinsics, intrinsics)
|
|
103
|
+
- extrinsics (torch.Tensor): Camera extrinsic parameters with shape BxSx3x4.
|
|
104
|
+
In OpenCV coordinate system (x-right, y-down, z-forward), representing camera from world
|
|
105
|
+
transformation. The format is [R|t] where R is a 3x3 rotation matrix and t is
|
|
106
|
+
a 3x1 translation vector.
|
|
107
|
+
- intrinsics (torch.Tensor or None): Camera intrinsic parameters with shape BxSx3x3,
|
|
108
|
+
or None if build_intrinsics is False. Defined in pixels, with format:
|
|
109
|
+
[[fx, 0, cx],
|
|
110
|
+
[0, fy, cy],
|
|
111
|
+
[0, 0, 1]]
|
|
112
|
+
where fx, fy are focal lengths and (cx, cy) is the principal point,
|
|
113
|
+
assumed to be at the center of the image (W/2, H/2).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
intrinsics = None
|
|
117
|
+
|
|
118
|
+
if pose_encoding_type == "absT_quaR_FoV":
|
|
119
|
+
T = pose_encoding[..., :3]
|
|
120
|
+
quat = pose_encoding[..., 3:7]
|
|
121
|
+
fov_h = pose_encoding[..., 7]
|
|
122
|
+
fov_w = pose_encoding[..., 8]
|
|
123
|
+
|
|
124
|
+
R = quat_to_mat(quat)
|
|
125
|
+
extrinsics = torch.cat([R, T[..., None]], dim=-1)
|
|
126
|
+
|
|
127
|
+
if build_intrinsics:
|
|
128
|
+
H, W = image_size_hw
|
|
129
|
+
fy = (H / 2.0) / (torch.tan(fov_h / 2.0) + 1e-3)
|
|
130
|
+
fx = (W / 2.0) / (torch.tan(fov_w / 2.0) + 1e-3)
|
|
131
|
+
intrinsics = torch.zeros(
|
|
132
|
+
pose_encoding.shape[:2] + (3, 3),
|
|
133
|
+
device=pose_encoding.device,
|
|
134
|
+
dtype=pose_encoding.dtype,
|
|
135
|
+
)
|
|
136
|
+
intrinsics[..., 0, 0] = fx
|
|
137
|
+
intrinsics[..., 1, 1] = fy
|
|
138
|
+
intrinsics[..., 0, 2] = W / 2
|
|
139
|
+
intrinsics[..., 1, 2] = H / 2
|
|
140
|
+
intrinsics[..., 2, 2] = 1.0 # Set the homogeneous coordinate to 1
|
|
141
|
+
else:
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
|
|
144
|
+
return extrinsics, intrinsics
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
#
|
|
7
|
+
# Modified by the libgs contributors, 2026, from
|
|
8
|
+
# src/model/encoder/vggt/utils/rotation.py of https://github.com/InternRobotics/AnySplat
|
|
9
|
+
# (5f5e208): formatted with black, unused imports removed.
|
|
10
|
+
# "The license found in the LICENSE file" above is the one VGGT's code
|
|
11
|
+
# had when AnySplat vendored it (2025-06-30): CC BY-NC 4.0, non-commercial
|
|
12
|
+
# (facebookresearch/vggt c6bf698); see LICENSE (this directory).
|
|
13
|
+
|
|
14
|
+
# Modified from PyTorch3D, https://github.com/facebookresearch/pytorch3d
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn.functional as F
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def quat_to_mat(quaternions: torch.Tensor) -> torch.Tensor:
|
|
21
|
+
"""
|
|
22
|
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
|
23
|
+
|
|
24
|
+
Convert rotations given as quaternions to rotation matrices.
|
|
25
|
+
Args:
|
|
26
|
+
quaternions: quaternions with real part last,
|
|
27
|
+
as tensor of shape (..., 4).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Rotation matrices as tensor of shape (..., 3, 3).
|
|
31
|
+
"""
|
|
32
|
+
i, j, k, r = torch.unbind(quaternions, -1)
|
|
33
|
+
# pyre-fixme[58]: `/` is not supported for operand types `float` and `Tensor`.
|
|
34
|
+
two_s = 2.0 / (quaternions * quaternions).sum(-1)
|
|
35
|
+
|
|
36
|
+
o = torch.stack(
|
|
37
|
+
(
|
|
38
|
+
1 - two_s * (j * j + k * k),
|
|
39
|
+
two_s * (i * j - k * r),
|
|
40
|
+
two_s * (i * k + j * r),
|
|
41
|
+
two_s * (i * j + k * r),
|
|
42
|
+
1 - two_s * (i * i + k * k),
|
|
43
|
+
two_s * (j * k - i * r),
|
|
44
|
+
two_s * (i * k - j * r),
|
|
45
|
+
two_s * (j * k + i * r),
|
|
46
|
+
1 - two_s * (i * i + j * j),
|
|
47
|
+
),
|
|
48
|
+
-1,
|
|
49
|
+
)
|
|
50
|
+
return o.reshape(quaternions.shape[:-1] + (3, 3))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def mat_to_quat(matrix: torch.Tensor) -> torch.Tensor:
|
|
54
|
+
"""
|
|
55
|
+
Convert rotations given as rotation matrices to quaternions.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
matrix: Rotation matrices as tensor of shape (..., 3, 3).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
quaternions with real part last, as tensor of shape (..., 4).
|
|
62
|
+
Quaternion Order: XYZW or say ijkr, scalar-last
|
|
63
|
+
"""
|
|
64
|
+
if matrix.size(-1) != 3 or matrix.size(-2) != 3:
|
|
65
|
+
raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
|
|
66
|
+
|
|
67
|
+
batch_dim = matrix.shape[:-2]
|
|
68
|
+
m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
|
|
69
|
+
matrix.reshape(batch_dim + (9,)), dim=-1
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
q_abs = _sqrt_positive_part(
|
|
73
|
+
torch.stack(
|
|
74
|
+
[
|
|
75
|
+
1.0 + m00 + m11 + m22,
|
|
76
|
+
1.0 + m00 - m11 - m22,
|
|
77
|
+
1.0 - m00 + m11 - m22,
|
|
78
|
+
1.0 - m00 - m11 + m22,
|
|
79
|
+
],
|
|
80
|
+
dim=-1,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# we produce the desired quaternion multiplied by each of r, i, j, k
|
|
85
|
+
quat_by_rijk = torch.stack(
|
|
86
|
+
[
|
|
87
|
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
|
88
|
+
# `int`.
|
|
89
|
+
torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1),
|
|
90
|
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
|
91
|
+
# `int`.
|
|
92
|
+
torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1),
|
|
93
|
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
|
94
|
+
# `int`.
|
|
95
|
+
torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1),
|
|
96
|
+
# pyre-fixme[58]: `**` is not supported for operand types `Tensor` and
|
|
97
|
+
# `int`.
|
|
98
|
+
torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1),
|
|
99
|
+
],
|
|
100
|
+
dim=-2,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# We floor here at 0.1 but the exact level is not important; if q_abs is small,
|
|
104
|
+
# the candidate won't be picked.
|
|
105
|
+
flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
|
|
106
|
+
quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
|
|
107
|
+
|
|
108
|
+
# if not for numerical problems, quat_candidates[i] should be same (up to a sign),
|
|
109
|
+
# forall i; we pick the best-conditioned one (with the largest denominator)
|
|
110
|
+
out = quat_candidates[
|
|
111
|
+
F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :
|
|
112
|
+
].reshape(batch_dim + (4,))
|
|
113
|
+
|
|
114
|
+
# Convert from rijk to ijkr
|
|
115
|
+
out = out[..., [1, 2, 3, 0]]
|
|
116
|
+
|
|
117
|
+
out = standardize_quaternion(out)
|
|
118
|
+
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
"""
|
|
124
|
+
Returns torch.sqrt(torch.max(0, x))
|
|
125
|
+
but with a zero subgradient where x is 0.
|
|
126
|
+
"""
|
|
127
|
+
ret = torch.zeros_like(x)
|
|
128
|
+
positive_mask = x > 0
|
|
129
|
+
if torch.is_grad_enabled():
|
|
130
|
+
ret[positive_mask] = torch.sqrt(x[positive_mask])
|
|
131
|
+
else:
|
|
132
|
+
ret = torch.where(positive_mask, torch.sqrt(x), ret)
|
|
133
|
+
return ret
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def standardize_quaternion(quaternions: torch.Tensor) -> torch.Tensor:
|
|
137
|
+
"""
|
|
138
|
+
Convert a unit quaternion to a standard form: one in which the real
|
|
139
|
+
part is non negative.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
quaternions: Quaternions with real part last,
|
|
143
|
+
as tensor of shape (..., 4).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Standardized quaternions as tensor of shape (..., 4).
|
|
147
|
+
"""
|
|
148
|
+
return torch.where(quaternions[..., 3:4] < 0, -quaternions, quaternions)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""TokenGS (Ren, Tyszkiewicz, Huang, Gojcic; CVPR 2026), vendored from the official
|
|
2
|
+
Apache-2.0 code at https://github.com/nv-tlabs/TokenGS (b16269c).
|
|
3
|
+
|
|
4
|
+
`model_type` "tokengs". Imported on first use: `import ffgs` does not import it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ...registry import ModelSpec, register_model
|
|
12
|
+
from .modeling import TokenGS
|
|
13
|
+
from .processing import TokenGSProcessor, TokenGSProcessorConfig
|
|
14
|
+
|
|
15
|
+
MODEL_TYPE = "tokengs"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convert_state_dict(checkpoint: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""An official checkpoint (the upstream module's state dict) -> `TokenGS`.
|
|
20
|
+
|
|
21
|
+
The parameter names are the upstream ones; only the LPIPS training loss that
|
|
22
|
+
upstream checkpoints may carry is dropped, as the upstream evaluation does.
|
|
23
|
+
"""
|
|
24
|
+
return {k: v for k, v in checkpoint.items() if "lpips_loss" not in k}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SPEC = register_model(
|
|
28
|
+
ModelSpec(
|
|
29
|
+
model_type=MODEL_TYPE,
|
|
30
|
+
model_cls=TokenGS,
|
|
31
|
+
processor_cls=TokenGSProcessor,
|
|
32
|
+
convert_state_dict=convert_state_dict,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"MODEL_TYPE",
|
|
38
|
+
"SPEC",
|
|
39
|
+
"TokenGS",
|
|
40
|
+
"TokenGSProcessor",
|
|
41
|
+
"TokenGSProcessorConfig",
|
|
42
|
+
"convert_state_dict",
|
|
43
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Modified by the libgs contributors, 2026, from tokengs/models/activations.py of
|
|
17
|
+
# https://github.com/nv-tlabs/TokenGS (b16269c): the clip head only, taking a
|
|
18
|
+
# `TokenGSConfig`; einops replaced by the equivalent reshape / permute; the unused
|
|
19
|
+
# ray arguments and weight-decay flags removed. Parameter names are unchanged.
|
|
20
|
+
|
|
21
|
+
"""Gaussian tokens -> activated Gaussian parameters."""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn as nn
|
|
29
|
+
import torch.nn.functional as F
|
|
30
|
+
from torch.nn.init import trunc_normal_
|
|
31
|
+
|
|
32
|
+
from .enc_dec import TokenGSConfig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClipActivationHead(nn.Module):
|
|
36
|
+
"""
|
|
37
|
+
Clip-based activation head that uses exponential activations with hard
|
|
38
|
+
clipping. Uses a single deconv (linear) layer for all Gaussian components.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, opt: TokenGSConfig):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.opt = opt
|
|
44
|
+
|
|
45
|
+
self.num_gaussians_per_token = self.opt.dec_patch_size**2
|
|
46
|
+
|
|
47
|
+
scale_cap = self.opt.gaussian_scale_cap
|
|
48
|
+
self.scale_shift = 1 - math.log(scale_cap)
|
|
49
|
+
self.scale_cap = scale_cap
|
|
50
|
+
|
|
51
|
+
self.output_dims = 3 + 1 + 3 + 4 + 3 # x, y, z, opacity, scale, rotation, rgb
|
|
52
|
+
|
|
53
|
+
# Single linear layer (deconv) for all components
|
|
54
|
+
self.deconv = nn.Linear(
|
|
55
|
+
self.opt.enc_embed_dim,
|
|
56
|
+
self.output_dims * self.opt.dec_patch_size * self.opt.dec_patch_size,
|
|
57
|
+
bias=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self._init_weights(self.deconv)
|
|
61
|
+
if self.opt.clip_head_z_init is not None:
|
|
62
|
+
bias_val = math.log(1.0 + self.opt.clip_head_z_init)
|
|
63
|
+
with torch.no_grad():
|
|
64
|
+
self.deconv.bias.view(
|
|
65
|
+
self.opt.dec_patch_size * self.opt.dec_patch_size,
|
|
66
|
+
self.output_dims,
|
|
67
|
+
)[:, 2].fill_(bias_val)
|
|
68
|
+
|
|
69
|
+
def _init_weights(self, m):
|
|
70
|
+
"""Initialize deconv weights"""
|
|
71
|
+
if isinstance(m, nn.Linear):
|
|
72
|
+
trunc_normal_(m.weight, std=self.opt.clip_head_readout_std)
|
|
73
|
+
if m.bias is not None:
|
|
74
|
+
m.bias.data.zero_()
|
|
75
|
+
|
|
76
|
+
def pos_act(self, x: torch.Tensor) -> torch.Tensor:
|
|
77
|
+
"""Position activation without scaling factor (for xyz prediction)"""
|
|
78
|
+
pos = torch.sign(x) * (torch.expm1(torch.abs(x))) # inverse log transform
|
|
79
|
+
return pos
|
|
80
|
+
|
|
81
|
+
def scale_act(self, x: torch.Tensor) -> torch.Tensor:
|
|
82
|
+
"""Scale activation with exponential and hard clipping"""
|
|
83
|
+
x = x - self.scale_shift
|
|
84
|
+
return torch.minimum(
|
|
85
|
+
torch.exp(x),
|
|
86
|
+
torch.tensor(self.scale_cap, device=x.device, dtype=x.dtype),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def opacity_act(self, x: torch.Tensor) -> torch.Tensor:
|
|
90
|
+
"""Opacity activation with hard clipping"""
|
|
91
|
+
return torch.sigmoid(x - self.opt.opacity_bias)
|
|
92
|
+
|
|
93
|
+
def rot_act(self, x: torch.Tensor) -> torch.Tensor:
|
|
94
|
+
"""Rotation normalization"""
|
|
95
|
+
return F.normalize(x, dim=-1)
|
|
96
|
+
|
|
97
|
+
def rgb_act(self, x: torch.Tensor) -> torch.Tensor:
|
|
98
|
+
"""RGB activation (always tanh for clip-based head)"""
|
|
99
|
+
return 0.5 * torch.tanh(x) + 0.5
|
|
100
|
+
|
|
101
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
102
|
+
"""
|
|
103
|
+
Convert tokens to Gaussians using a single deconv layer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
x: Input tokens [B, N, C]
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Gaussians [B, N * num_gaussians_per_token, 14]: xyz, opacity, scale,
|
|
110
|
+
rotation (wxyz), rgb
|
|
111
|
+
"""
|
|
112
|
+
B = x.shape[0]
|
|
113
|
+
|
|
114
|
+
x = self.deconv(x) # [B, N, output_dims * P * P]
|
|
115
|
+
# "b n (p c) -> b c (n p)"
|
|
116
|
+
n = x.shape[1]
|
|
117
|
+
p = self.opt.dec_patch_size**2
|
|
118
|
+
x = x.reshape(B, n, p, -1).permute(0, 3, 1, 2).reshape(B, -1, n * p)
|
|
119
|
+
|
|
120
|
+
x = x.reshape(B, self.output_dims, -1) # B, output_dims, N * P * P
|
|
121
|
+
x = x.permute(0, 2, 1).contiguous() # B, N * P * P, output_dims
|
|
122
|
+
|
|
123
|
+
pos, rgb, scaling, rotation, opacity = x.split([3, 3, 3, 4, 1], dim=-1)
|
|
124
|
+
pos = self.pos_act(pos)
|
|
125
|
+
|
|
126
|
+
opacity = self.opacity_act(opacity)
|
|
127
|
+
scale = self.scale_act(scaling)
|
|
128
|
+
rotation = self.rot_act(rotation)
|
|
129
|
+
rgbs = self.rgb_act(rgb)
|
|
130
|
+
|
|
131
|
+
gaussians = torch.cat(
|
|
132
|
+
[pos, opacity, scale, rotation, rgbs], dim=-1
|
|
133
|
+
) # [B, N, 14]
|
|
134
|
+
|
|
135
|
+
return gaussians
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Modified by the libgs contributors, 2026, from tokengs/models/attention.py of
|
|
17
|
+
# https://github.com/nv-tlabs/TokenGS (b16269c): inference-only subset. Removed
|
|
18
|
+
# FlexAttention (and the global torch._dynamo settings made on import), RoPE and
|
|
19
|
+
# stochastic depth; parameter names are unchanged.
|
|
20
|
+
|
|
21
|
+
"""Transformer building blocks of TokenGS."""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn.functional as F
|
|
29
|
+
from torch import Tensor, nn
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Mlp(nn.Module):
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
in_features: int,
|
|
36
|
+
hidden_features: int | None = None,
|
|
37
|
+
out_features: int | None = None,
|
|
38
|
+
act_layer: Callable[..., nn.Module] = nn.GELU,
|
|
39
|
+
bias: bool = True,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__()
|
|
42
|
+
out_features = out_features or in_features
|
|
43
|
+
hidden_features = hidden_features or in_features
|
|
44
|
+
self.fc1 = nn.Linear(in_features, hidden_features, bias=bias)
|
|
45
|
+
self.act = act_layer()
|
|
46
|
+
self.fc2 = nn.Linear(hidden_features, out_features, bias=bias)
|
|
47
|
+
|
|
48
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
49
|
+
x = self.fc1(x)
|
|
50
|
+
x = self.act(x)
|
|
51
|
+
x = self.fc2(x)
|
|
52
|
+
return x
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Attention(nn.Module):
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
dim: int,
|
|
59
|
+
num_heads: int = 8,
|
|
60
|
+
qkv_bias: bool = True,
|
|
61
|
+
proj_bias: bool = True,
|
|
62
|
+
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
|
|
63
|
+
qk_norm: bool = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__()
|
|
66
|
+
assert dim % num_heads == 0, "dim should be divisible by num_heads"
|
|
67
|
+
self.num_heads = num_heads
|
|
68
|
+
self.head_dim = dim // num_heads
|
|
69
|
+
|
|
70
|
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
|
71
|
+
self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
|
72
|
+
self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
|
|
73
|
+
self.proj = nn.Linear(dim, dim, bias=proj_bias)
|
|
74
|
+
|
|
75
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
76
|
+
B, N, C = x.shape
|
|
77
|
+
qkv = (
|
|
78
|
+
self.qkv(x)
|
|
79
|
+
.reshape(B, N, 3, self.num_heads, self.head_dim)
|
|
80
|
+
.permute(2, 0, 3, 1, 4)
|
|
81
|
+
)
|
|
82
|
+
q, k, v = qkv.unbind(0)
|
|
83
|
+
q, k = self.q_norm(q), self.k_norm(k)
|
|
84
|
+
|
|
85
|
+
x = F.scaled_dot_product_attention(q, k, v)
|
|
86
|
+
|
|
87
|
+
x = x.transpose(1, 2).reshape(B, N, C)
|
|
88
|
+
x = self.proj(x)
|
|
89
|
+
return x
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LayerScale(nn.Module):
|
|
93
|
+
def __init__(self, dim: int, init_values: float | Tensor = 1e-5) -> None:
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.gamma = nn.Parameter(init_values * torch.ones(dim))
|
|
96
|
+
|
|
97
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
98
|
+
return x * self.gamma
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Block(nn.Module):
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
dim: int,
|
|
105
|
+
num_heads: int,
|
|
106
|
+
mlp_ratio: float = 4.0,
|
|
107
|
+
qkv_bias: bool = True,
|
|
108
|
+
proj_bias: bool = True,
|
|
109
|
+
ffn_bias: bool = True,
|
|
110
|
+
init_values: float | Tensor | None = None,
|
|
111
|
+
act_layer: Callable[..., nn.Module] = nn.GELU,
|
|
112
|
+
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
|
|
113
|
+
qk_norm: bool = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
super().__init__()
|
|
116
|
+
|
|
117
|
+
self.norm1 = norm_layer(dim)
|
|
118
|
+
self.attn = Attention(
|
|
119
|
+
dim,
|
|
120
|
+
num_heads=num_heads,
|
|
121
|
+
qkv_bias=qkv_bias,
|
|
122
|
+
proj_bias=proj_bias,
|
|
123
|
+
qk_norm=qk_norm,
|
|
124
|
+
)
|
|
125
|
+
self.ls1 = (
|
|
126
|
+
LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.norm2 = norm_layer(dim)
|
|
130
|
+
self.mlp = Mlp(
|
|
131
|
+
in_features=dim,
|
|
132
|
+
hidden_features=int(dim * mlp_ratio),
|
|
133
|
+
act_layer=act_layer,
|
|
134
|
+
bias=ffn_bias,
|
|
135
|
+
)
|
|
136
|
+
self.ls2 = (
|
|
137
|
+
LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
141
|
+
x = x + self.ls1(self.attn(self.norm1(x)))
|
|
142
|
+
x = x + self.ls2(self.mlp(self.norm2(x)))
|
|
143
|
+
return x
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def make_2tuple(x: int | tuple[int, int]) -> tuple[int, int]:
|
|
147
|
+
if isinstance(x, tuple):
|
|
148
|
+
assert len(x) == 2
|
|
149
|
+
return x
|
|
150
|
+
assert isinstance(x, int)
|
|
151
|
+
return (x, x)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class PatchEmbed(nn.Module):
|
|
155
|
+
"""2D image to patch embedding: (B, C, H, W) -> (B, N, D)."""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
patch_size: int | tuple[int, int] = 16,
|
|
160
|
+
in_chans: int = 3,
|
|
161
|
+
embed_dim: int = 768,
|
|
162
|
+
norm_layer: Callable[..., nn.Module] | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
super().__init__()
|
|
165
|
+
patch_HW = make_2tuple(patch_size)
|
|
166
|
+
self.patch_size = patch_HW
|
|
167
|
+
self.in_chans = in_chans
|
|
168
|
+
self.embed_dim = embed_dim
|
|
169
|
+
|
|
170
|
+
self.proj = nn.Conv2d(
|
|
171
|
+
in_chans, embed_dim, kernel_size=patch_HW, stride=patch_HW
|
|
172
|
+
)
|
|
173
|
+
self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
|
|
174
|
+
|
|
175
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
176
|
+
_, _, H, W = x.shape
|
|
177
|
+
patch_H, patch_W = self.patch_size
|
|
178
|
+
|
|
179
|
+
assert (
|
|
180
|
+
H % patch_H == 0
|
|
181
|
+
), f"Input image height {H} is not a multiple of patch height {patch_H}"
|
|
182
|
+
assert (
|
|
183
|
+
W % patch_W == 0
|
|
184
|
+
), f"Input image width {W} is not a multiple of patch width: {patch_W}"
|
|
185
|
+
|
|
186
|
+
x = self.proj(x)
|
|
187
|
+
x = x.flatten(2).transpose(1, 2)
|
|
188
|
+
x = self.norm(x)
|
|
189
|
+
return x
|