Add StableFast3D

This commit is contained in:
Mr. For Example
2024-08-01 19:38:27 -06:00
parent f524f3d60c
commit d596203483
40 changed files with 19190 additions and 30 deletions
+96
View File
@@ -0,0 +1,96 @@
cond_image_size: 512
isosurface_resolution: 160
radius: 0.87
camera_embedder_cls: StableFast3D.sf3d.models.camera.LinearCameraEmbedder
camera_embedder:
in_channels: 25
out_channels: 768
conditions:
- c2w_cond
- intrinsic_normed_cond
image_tokenizer_cls: StableFast3D.sf3d.models.tokenizers.image.DINOV2SingleImageTokenizer
image_tokenizer:
pretrained_model_name_or_path: "facebook/dinov2-large"
width: 512
height: 512
modulation_cond_dim: 768
tokenizer_cls: StableFast3D.sf3d.models.tokenizers.triplane.TriplaneLearnablePositionalEmbedding
tokenizer:
plane_size: 96
num_channels: 1024
backbone_cls: StableFast3D.sf3d.models.transformers.backbone.TwoStreamInterleaveTransformer
backbone:
num_attention_heads: 16
attention_head_dim: 64
raw_triplane_channels: 1024
triplane_channels: 1024
raw_image_channels: 1024 # DINO features
num_latents: 1792
num_blocks: 4
num_basic_blocks: 3
post_processor_cls: StableFast3D.sf3d.models.network.PixelShuffleUpsampleNetwork
post_processor:
in_channels: 1024
out_channels: 40
scale_factor: 4
conv_layers: 4
decoder_cls: StableFast3D.sf3d.models.network.MaterialMLP
decoder:
in_channels: 120
n_neurons: 64
activation: silu
heads:
- name: density
out_channels: 1
out_bias: -1.0
n_hidden_layers: 2
output_activation: trunc_exp
- name: features
out_channels: 3
n_hidden_layers: 3
output_activation: sigmoid
- name: perturb_normal
out_channels: 3
n_hidden_layers: 3
output_activation: normalize_channel_last
- name: vertex_offset
out_channels: 3
n_hidden_layers: 2
image_estimator_cls: StableFast3D.sf3d.models.image_estimator.clip_based_estimator.ClipBasedHeadEstimator
image_estimator:
distribution: beta
distribution_eval: mode
heads:
- name: roughness
out_channels: 1
n_hidden_layers: 3
output_activation: linear
add_to_decoder_features: true
output_bias: 1.0
shape: [-1, 1, 1]
- name: metallic
out_channels: 1
n_hidden_layers: 3
output_activation: linear
add_to_decoder_features: true
output_bias: 1.0
shape: [-1, 1, 1]
global_estimator_cls: StableFast3D.sf3d.models.global_estimator.multi_head_estimator.MultiHeadEstimator
global_estimator:
triplane_features: 1024
heads:
- name: sg_amplitudes
out_channels: 24
n_hidden_layers: 3
output_activation: softplus
output_bias: 1.0
shape: [-1, 24, 1]
+4
View File
@@ -8,3 +8,7 @@ web{
172.17.0.1,
]
}
huggingface{
token = "" # Your user access token to enable automatic download restricted model card
}
+1 -1
View File
@@ -64,7 +64,7 @@ class CRMSampler:
)
stage1_images = stage1_images[:-1] # remove reference view
return stage1_images # (N, H, W, 3) in [0, 1]
return stage1_images # (N, H, W, 3) in [0, 255]
@classmethod
def stage2_sample(
Binary file not shown.
@@ -0,0 +1,16 @@
# ninja log v5
11 2867 7442488935197392 texture_baker_cuda.cuda.o 950ee83e8ffb6938
6 13908 7442489045337047 texture_baker.o d2552713bf2b5a12
13938 14050 7442489047068090 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
12 15068 7442638778470049 texture_baker.o d2552713bf2b5a12
15097 15194 7442638780013672 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
30 12043 7442638900970605 texture_baker.o d2552713bf2b5a12
12044 12126 7442638901979835 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
35 17156 7442641205223193 texture_baker.o d2552713bf2b5a12
17157 17663 7442641210473291 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
23 13624 7442641347270749 texture_baker.o d2552713bf2b5a12
13625 13704 7442641348223881 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
26 16411 7442645088069002 texture_baker.o d2552713bf2b5a12
16413 16494 7442645089064001 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
24 11717 7442645206723305 texture_baker.o d2552713bf2b5a12
11718 11796 7442645207640080 _slangtorch_texture_baker_44136fa355b3678a.pyd 9b21a5c60eb03f2c
@@ -0,0 +1,33 @@
ninja_required_version = 1.3
cxx = cl
nvcc = C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\bin\nvcc
cflags = -DTORCH_EXTENSION_NAME=_slangtorch_texture_baker_44136fa355b3678a -DTORCH_API_INCLUDE_EXTENSION_H -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\_Projects\stable-fast-3d\sf3d -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\torch\csrc\api\include -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\TH -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\THC "-IC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\include" -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Include -D_GLIBCXX_USE_CXX11_ABI=0 /MD /wd4819 /wd4251 /wd4244 /wd4267 /wd4275 /wd4018 /wd4190 /wd4624 /wd4067 /wd4068 /EHsc /std:c++17 /std:c++17
post_cflags =
cuda_cflags = -Xcudafe --diag_suppress=dll_interface_conflict_dllexport_assumed -Xcudafe --diag_suppress=dll_interface_conflict_none_assumed -Xcudafe --diag_suppress=field_without_dll_interface -Xcudafe --diag_suppress=base_class_has_different_dll_interface -Xcompiler /EHsc -Xcompiler /wd4068 -Xcompiler /wd4067 -Xcompiler /wd4624 -Xcompiler /wd4190 -Xcompiler /wd4018 -Xcompiler /wd4275 -Xcompiler /wd4267 -Xcompiler /wd4244 -Xcompiler /wd4251 -Xcompiler /wd4819 -Xcompiler /MD -DTORCH_EXTENSION_NAME=_slangtorch_texture_baker_44136fa355b3678a -DTORCH_API_INCLUDE_EXTENSION_H -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\_Projects\stable-fast-3d\sf3d -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\torch\csrc\api\include -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\TH -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\include\THC "-IC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\include" -IC:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Include -D_GLIBCXX_USE_CXX11_ABI=0 -D__CUDA_NO_HALF_OPERATORS__ -D__CUDA_NO_HALF_CONVERSIONS__ -D__CUDA_NO_BFLOAT16_CONVERSIONS__ -D__CUDA_NO_HALF2_OPERATORS__ --expt-relaxed-constexpr -gencode=arch=compute_89,code=compute_89 -gencode=arch=compute_89,code=sm_89 -std=c++17 --std=c++17 -U__CUDA_NO_HALF_OPERATORS__ -U__CUDA_NO_HALF_CONVERSIONS__ -U__CUDA_NO_HALF2_OPERATORS__ -U__CUDA_NO_BFLOAT16_CONVERSIONS__ -DSLANG_CUDA_ENABLE_HALF=1
cuda_post_cflags =
cuda_dlink_post_cflags =
ldflags = /DLL c10.lib c10_cuda.lib torch_cpu.lib torch_cuda.lib -INCLUDE:?warp_size@cuda@at@@YAHXZ torch.lib /LIBPATH:C:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\Lib\site-packages\torch\lib torch_python.lib /LIBPATH:C:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\libs "/LIBPATH:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\lib\x64" cudart.lib
rule compile
command = cl /showIncludes $cflags -c $in /Fo$out $post_cflags
deps = msvc
rule cuda_compile
depfile = $out.d
deps = gcc
command = $nvcc --generate-dependencies-with-compile --dependency-output $out.d $cuda_cflags -c $in -o $out $cuda_post_cflags
rule link
command = "C$:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.40.33807\bin\Hostx64\x64/link.exe" $in /nologo $ldflags /out:$out
build texture_baker.o: compile C$:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\_Projects\stable-fast-3d\sf3d\.slangtorch_cache\texture_baker\44136fa355b3678a\texture_baker.cpp
build texture_baker_cuda.cuda.o: cuda_compile C$:\Users\reall\Softwares\Miniconda\envs\AI3D_Exp\_Projects\stable-fast-3d\sf3d\.slangtorch_cache\texture_baker\44136fa355b3678a\texture_baker_cuda.cu
build _slangtorch_texture_baker_44136fa355b3678a.pyd: link texture_baker.o texture_baker_cuda.cuda.o
default _slangtorch_texture_baker_44136fa355b3678a.pyd
@@ -0,0 +1,42 @@
{
"cpp": {
"options": [],
"deps": [
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\_Projects\\stable-fast-3d\\sf3d\\texture_baker.slang",
1722544000.3105776
],
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\Lib\\site-packages\\slangtorch\\bin\\slangc.exe",
1722544345.7507377
],
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\Lib\\site-packages\\slangtorch\\bin\\slang.dll",
1722544345.7507377
]
],
"version": "1.2.3",
"includePaths": []
},
"cuda": {
"options": [],
"deps": [
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\_Projects\\stable-fast-3d\\sf3d\\texture_baker.slang",
1722544000.3105776
],
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\Lib\\site-packages\\slangtorch\\bin\\slangc.exe",
1722544345.7507377
],
[
"C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\Lib\\site-packages\\slangtorch\\bin\\slang.dll",
1722544345.7507377
]
],
"version": "1.2.3",
"includePaths": []
},
"moduleName": "_slangtorch_texture_baker_44136fa355b3678a",
"moduleBinary": "C:\\Users\\reall\\Softwares\\Miniconda\\envs\\AI3D_Exp\\_Projects\\stable-fast-3d\\sf3d\\.slangtorch_cache\\texture_baker\\44136fa355b3678a\\0\\_slangtorch_texture_baker_44136fa355b3678a.pyd"
}
@@ -0,0 +1,610 @@
import math
from typing import Tuple
import torch
import torch.nn.functional as F
from jaxtyping import Float, Integer
from torch import Tensor
from StableFast3D.sf3d.models.utils import dot, triangle_intersection_2d
def _box_assign_vertex_to_cube_face(
vertex_positions: Float[Tensor, "Nv 3"],
vertex_normals: Float[Tensor, "Nv 3"],
triangle_idxs: Integer[Tensor, "Nf 3"],
bbox: Float[Tensor, "2 3"],
) -> Tuple[Float[Tensor, "Nf 3 2"], Integer[Tensor, "Nf 3"]]:
# Test to not have a scaled model to fit the space better
# bbox_min = bbox[:1].mean(-1, keepdim=True)
# bbox_max = bbox[1:].mean(-1, keepdim=True)
# v_pos_normalized = (vertex_positions - bbox_min) / (bbox_max - bbox_min)
# Create a [0, 1] normalized vertex position
v_pos_normalized = (vertex_positions - bbox[:1]) / (bbox[1:] - bbox[:1])
# And to [-1, 1]
v_pos_normalized = 2.0 * v_pos_normalized - 1.0
# Get all vertex positions for each triangle
# Now how do we define to which face the triangle belongs? Mean face pos? Max vertex pos?
v0 = v_pos_normalized[triangle_idxs[:, 0]]
v1 = v_pos_normalized[triangle_idxs[:, 1]]
v2 = v_pos_normalized[triangle_idxs[:, 2]]
tri_stack = torch.stack([v0, v1, v2], dim=1)
vn0 = vertex_normals[triangle_idxs[:, 0]]
vn1 = vertex_normals[triangle_idxs[:, 1]]
vn2 = vertex_normals[triangle_idxs[:, 2]]
tri_stack_nrm = torch.stack([vn0, vn1, vn2], dim=1)
# Just average the normals per face
face_normal = F.normalize(torch.sum(tri_stack_nrm, 1), eps=1e-6, dim=-1)
# Now decide based on the face normal in which box map we project
# abs_x, abs_y, abs_z = tri_stack_nrm.abs().unbind(-1)
abs_x, abs_y, abs_z = tri_stack.abs().unbind(-1)
axis = torch.tensor(
[
[1, 0, 0], # 0
[-1, 0, 0], # 1
[0, 1, 0], # 2
[0, -1, 0], # 3
[0, 0, 1], # 4
[0, 0, -1], # 5
],
device=face_normal.device,
dtype=face_normal.dtype,
)
face_normal_axis = (face_normal[:, None] * axis[None]).sum(-1)
index = face_normal_axis.argmax(-1)
max_axis, uc, vc = (
torch.ones_like(abs_x),
torch.zeros_like(tri_stack[..., :1]),
torch.zeros_like(tri_stack[..., :1]),
)
mask_pos_x = index == 0
max_axis[mask_pos_x] = abs_x[mask_pos_x]
uc[mask_pos_x] = tri_stack[mask_pos_x][..., 1:2]
vc[mask_pos_x] = -tri_stack[mask_pos_x][..., -1:]
mask_neg_x = index == 1
max_axis[mask_neg_x] = abs_x[mask_neg_x]
uc[mask_neg_x] = tri_stack[mask_neg_x][..., 1:2]
vc[mask_neg_x] = -tri_stack[mask_neg_x][..., -1:]
mask_pos_y = index == 2
max_axis[mask_pos_y] = abs_y[mask_pos_y]
uc[mask_pos_y] = tri_stack[mask_pos_y][..., 0:1]
vc[mask_pos_y] = -tri_stack[mask_pos_y][..., -1:]
mask_neg_y = index == 3
max_axis[mask_neg_y] = abs_y[mask_neg_y]
uc[mask_neg_y] = tri_stack[mask_neg_y][..., 0:1]
vc[mask_neg_y] = -tri_stack[mask_neg_y][..., -1:]
mask_pos_z = index == 4
max_axis[mask_pos_z] = abs_z[mask_pos_z]
uc[mask_pos_z] = tri_stack[mask_pos_z][..., 0:1]
vc[mask_pos_z] = tri_stack[mask_pos_z][..., 1:2]
mask_neg_z = index == 5
max_axis[mask_neg_z] = abs_z[mask_neg_z]
uc[mask_neg_z] = tri_stack[mask_neg_z][..., 0:1]
vc[mask_neg_z] = -tri_stack[mask_neg_z][..., 1:2]
# UC from [-1, 1] to [0, 1]
max_dim_div = max_axis.max(dim=0, keepdims=True).values
uc = ((uc[..., 0] / max_dim_div + 1.0) * 0.5).clip(0, 1)
vc = ((vc[..., 0] / max_dim_div + 1.0) * 0.5).clip(0, 1)
uv = torch.stack([uc, vc], dim=-1)
return uv, index
def _assign_faces_uv_to_atlas_index(
vertex_positions: Float[Tensor, "Nv 3"],
triangle_idxs: Integer[Tensor, "Nf 3"],
face_uv: Float[Tensor, "Nf 3 2"],
face_index: Integer[Tensor, "Nf 3"],
) -> Integer[Tensor, "Nf"]: # noqa: F821
triangle_pos = vertex_positions[triangle_idxs]
# We need to do perform 3 overlap checks.
# The first set is placed in the upper two thirds of the UV atlas.
# Conceptually, this is the direct visible surfaces from the each cube side
# The second set is placed in the lower thirds and the left half of the UV atlas.
# This is the first set of occluded surfaces. They will also be saved in the projected fashion
# The third pass finds all non assigned faces. They will be placed in the bottom right half of
# the UV atlas in scattered fashion.
assign_idx = face_index.clone()
for overlap_step in range(3):
overlapping_indicator = torch.zeros_like(assign_idx, dtype=torch.bool)
for i in range(overlap_step * 6, (overlap_step + 1) * 6):
mask = assign_idx == i
if not mask.any():
continue
# Get all elements belonging to the projection face
uv_triangle = face_uv[mask]
cur_triangle_pos = triangle_pos[mask]
# Find the center of the uv coordinates
center_uv = uv_triangle.mean(dim=1, keepdim=True)
# And also the radius of the triangle
uv_triangle_radius = (uv_triangle - center_uv).norm(dim=-1).max(-1).values
potentially_overlapping_mask = (
# Find all close triangles
(center_uv[None, ...] - center_uv[:, None]).norm(dim=-1)
# Do not select the same element by offseting with an large valued identity matrix
+ torch.eye(
uv_triangle.shape[0],
device=uv_triangle.device,
dtype=uv_triangle.dtype,
).unsqueeze(-1)
* 1000
)
# Mark all potentially overlapping triangles to reduce the number of triangle intersection tests
potentially_overlapping_mask = (
potentially_overlapping_mask
<= (uv_triangle_radius.view(-1, 1, 1) * 3.0)
).squeeze(-1)
overlap_coords = torch.stack(torch.where(potentially_overlapping_mask), -1)
# Only unique triangles (A|B and B|A should be the same)
f = torch.min(overlap_coords, dim=-1).values
s = torch.max(overlap_coords, dim=-1).values
overlap_coords = torch.unique(torch.stack([f, s], dim=1), dim=0)
first, second = overlap_coords.unbind(-1)
# Get the triangles
tri_1 = uv_triangle[first]
tri_2 = uv_triangle[second]
# Perform the actual set with the reduced number of potentially overlapping triangles
its = triangle_intersection_2d(tri_1, tri_2, eps=1e-6)
# So we now need to detect which triangles are the occluded ones.
# We always assume the first to be the visible one (the others should move)
# In the previous step we use a lexigraphical sort to get the unique pairs
# In this we use a sort based on the orthographic projection
ax = 0 if i < 2 else 1 if i < 4 else 2
use_max = i % 2 == 1
tri1_c = cur_triangle_pos[first].mean(dim=1)
tri2_c = cur_triangle_pos[second].mean(dim=1)
mark_first = (
(tri1_c[..., ax] > tri2_c[..., ax])
if use_max
else (tri1_c[..., ax] < tri2_c[..., ax])
)
first[mark_first] = second[mark_first]
# Lastly the same index can be tested multiple times.
# If one marks it as overlapping we keep it marked as such.
# We do this by testing if it has been marked at least once.
unique_idx, rev_idx = torch.unique(first, return_inverse=True)
add = torch.zeros_like(unique_idx, dtype=torch.float32)
add.index_add_(0, rev_idx, its.float())
its_mask = add > 0
# And fill it in the overlapping indicator
idx = torch.where(mask)[0][unique_idx]
overlapping_indicator[idx] = its_mask
# Move the index to the overlap regions (shift by 6)
assign_idx[overlapping_indicator] += 6
# We do not care about the correct face placement after the first 2 slices
max_idx = 6 * 2
return assign_idx.clamp(0, max_idx)
def _find_slice_offset_and_scale(
index: Integer[Tensor, "Nf"], # noqa: F821
) -> Tuple[
Float[Tensor, "Nf"], Float[Tensor, "Nf"], Float[Tensor, "Nf"], Float[Tensor, "Nf"] # noqa: F821
]: # noqa: F821
# 6 due to the 6 cube faces
off = 1 / 3
dupl_off = 1 / 6
# Here, we need to decide how to pack the textures in the case of overlap
def x_offset_calc(x, i):
offset_calc = i // 6
# Initial coordinates - just 3x2 grid
if offset_calc == 0:
return off * x
else:
# Smaller 3x2 grid plus eventual shift to right for
# second overlap
return dupl_off * x + min(offset_calc - 1, 1) * 0.5
def y_offset_calc(x, i):
offset_calc = i // 6
# Initial coordinates - just a 3x2 grid
if offset_calc == 0:
return off * x
else:
# Smaller coordinates in the lowest row
return dupl_off * x + off * 2
offset_x = torch.zeros_like(index, dtype=torch.float32)
offset_y = torch.zeros_like(index, dtype=torch.float32)
offset_x_vals = [0, 1, 2, 0, 1, 2]
offset_y_vals = [0, 0, 0, 1, 1, 1]
for i in range(index.max().item() + 1):
mask = index == i
if not mask.any():
continue
offset_x[mask] = x_offset_calc(offset_x_vals[i % 6], i)
offset_y[mask] = y_offset_calc(offset_y_vals[i % 6], i)
div_x = torch.full_like(index, 6 // 2, dtype=torch.float32)
# All overlap elements are saved in half scale
div_x[index >= 6] = 6
div_y = div_x.clone() # Same for y
# Except for the random overlaps
div_x[index >= 12] = 2
# But the random overlaps are saved in a large block in the lower thirds
div_y[index >= 12] = 3
return offset_x, offset_y, div_x, div_y
def rotation_flip_matrix_2d(
rad: float, flip_x: bool = False, flip_y: bool = False
) -> Float[Tensor, "2 2"]:
cos = math.cos(rad)
sin = math.sin(rad)
rot_mat = torch.tensor([[cos, -sin], [sin, cos]], dtype=torch.float32)
flip_mat = torch.tensor(
[
[-1 if flip_x else 1, 0],
[0, -1 if flip_y else 1],
],
dtype=torch.float32,
)
return flip_mat @ rot_mat
def calculate_tangents(
vertex_positions: Float[Tensor, "Nv 3"],
vertex_normals: Float[Tensor, "Nv 3"],
triangle_idxs: Integer[Tensor, "Nf 3"],
face_uv: Float[Tensor, "Nf 3 2"],
) -> Float[Tensor, "Nf 3 4"]: # noqa: F821
vn_idx = [None] * 3
pos = [None] * 3
tex = face_uv.unbind(1)
for i in range(0, 3):
pos[i] = vertex_positions[triangle_idxs[:, i]]
# t_nrm_idx is always the same as t_pos_idx
vn_idx[i] = triangle_idxs[:, i]
tangents = torch.zeros_like(vertex_normals)
tansum = torch.zeros_like(vertex_normals)
# Compute tangent space for each triangle
duv1 = tex[1] - tex[0]
duv2 = tex[2] - tex[0]
dpos1 = pos[1] - pos[0]
dpos2 = pos[2] - pos[0]
tng_nom = dpos1 * duv2[..., 1:2] - dpos2 * duv1[..., 1:2]
denom = duv1[..., 0:1] * duv2[..., 1:2] - duv1[..., 1:2] * duv2[..., 0:1]
# Avoid division by zero for degenerated texture coordinates
denom_safe = denom.clip(1e-6)
tang = tng_nom / denom_safe
# Update all 3 vertices
for i in range(0, 3):
idx = vn_idx[i][:, None].repeat(1, 3)
tangents.scatter_add_(0, idx, tang) # tangents[n_i] = tangents[n_i] + tang
tansum.scatter_add_(
0, idx, torch.ones_like(tang)
) # tansum[n_i] = tansum[n_i] + 1
# Also normalize it. Here we do not normalize the individual triangles first so larger area
# triangles influence the tangent space more
tangents = tangents / tansum
# Normalize and make sure tangent is perpendicular to normal
tangents = F.normalize(tangents, dim=1)
tangents = F.normalize(tangents - dot(tangents, vertex_normals) * vertex_normals)
return tangents
def _rotate_uv_slices_consistent_space(
vertex_positions: Float[Tensor, "Nv 3"],
vertex_normals: Float[Tensor, "Nv 3"],
triangle_idxs: Integer[Tensor, "Nf 3"],
uv: Float[Tensor, "Nf 3 2"],
index: Integer[Tensor, "Nf"], # noqa: F821
):
tangents = calculate_tangents(vertex_positions, vertex_normals, triangle_idxs, uv)
pos_stack = torch.stack(
[
-vertex_positions[..., 1],
vertex_positions[..., 0],
torch.zeros_like(vertex_positions[..., 0]),
],
dim=-1,
)
expected_tangents = F.normalize(
torch.linalg.cross(
vertex_normals, torch.linalg.cross(pos_stack, vertex_normals)
),
-1,
)
actual_tangents = tangents[triangle_idxs]
expected_tangents = expected_tangents[triangle_idxs]
def rotation_matrix_2d(theta):
c, s = torch.cos(theta), torch.sin(theta)
return torch.tensor([[c, -s], [s, c]])
# Now find the rotation
index_mod = index % 6 # Shouldn't happen. Just for safety
for i in range(6):
mask = index_mod == i
if not mask.any():
continue
actual_mean_tangent = actual_tangents[mask].mean(dim=(0, 1))
expected_mean_tangent = expected_tangents[mask].mean(dim=(0, 1))
dot_product = torch.dot(actual_mean_tangent, expected_mean_tangent)
cross_product = (
actual_mean_tangent[0] * expected_mean_tangent[1]
- actual_mean_tangent[1] * expected_mean_tangent[0]
)
angle = torch.atan2(cross_product, dot_product)
rot_matrix = rotation_matrix_2d(angle).to(mask.device)
# Center the uv coordinate to be in the range of -1 to 1 and 0 centered
uv_cur = uv[mask] * 2 - 1 # Center it first
# Rotate it
uv[mask] = torch.einsum("ij,nfj->nfi", rot_matrix, uv_cur)
# Rescale uv[mask] to be within the 0-1 range
uv[mask] = (uv[mask] - uv[mask].min()) / (uv[mask].max() - uv[mask].min())
return uv
def _handle_slice_uvs(
uv: Float[Tensor, "Nf 3 2"],
index: Integer[Tensor, "Nf"], # noqa: F821
island_padding: float,
max_index: int = 6 * 2,
) -> Float[Tensor, "Nf 3 2"]: # noqa: F821
uc, vc = uv.unbind(-1)
# Get the second slice (The first overlap)
index_filter = [index == i for i in range(6, max_index)]
# Normalize them to always fully fill the atlas patch
for i, fi in enumerate(index_filter):
if fi.sum() > 0:
# Scale the slice but only up to a factor of 2
# This keeps the texture resolution with the first slice in line (Half space in UV)
uc[fi] = (uc[fi] - uc[fi].min()) / (uc[fi].max() - uc[fi].min()).clip(0.5)
vc[fi] = (vc[fi] - vc[fi].min()) / (vc[fi].max() - vc[fi].min()).clip(0.5)
uc_padded = (uc * (1 - 2 * island_padding) + island_padding).clip(0, 1)
vc_padded = (vc * (1 - 2 * island_padding) + island_padding).clip(0, 1)
return torch.stack([uc_padded, vc_padded], dim=-1)
def _handle_remaining_uvs(
uv: Float[Tensor, "Nf 3 2"],
index: Integer[Tensor, "Nf"], # noqa: F821
island_padding: float,
) -> Float[Tensor, "Nf 3 2"]:
uc, vc = uv.unbind(-1)
# Get all remaining elements
remaining_filter = index >= 6 * 2
squares_left = remaining_filter.sum()
if squares_left == 0:
return uv
uc = uc[remaining_filter]
vc = vc[remaining_filter]
# Or remaining triangles are distributed in a rectangle
# The rectangle takes 0.5 of the entire uv space in width and 1/3 in height
ratio = 0.5 * (1 / 3) # 1.5
# sqrt(744/(0.5*(1/3)))
mult = math.sqrt(squares_left / ratio)
num_square_width = int(math.ceil(0.5 * mult))
num_square_height = int(math.ceil(squares_left / num_square_width))
width = 1 / num_square_width
height = 1 / num_square_height
# The idea is again to keep the texture resolution consistent with the first slice
# This only occupys half the region in the texture chart but the scaling on the squares
# assumes full coverage.
clip_val = min(width, height) * 1.5
# Now normalize the UVs with taking into account the maximum scaling
uc = (uc - uc.min(dim=1, keepdim=True).values) / (
uc.amax(dim=1, keepdim=True) - uc.amin(dim=1, keepdim=True)
).clip(clip_val)
vc = (vc - vc.min(dim=1, keepdim=True).values) / (
vc.amax(dim=1, keepdim=True) - vc.amin(dim=1, keepdim=True)
).clip(clip_val)
# Add a small padding
uc = (
uc * (1 - island_padding * num_square_width * 0.5)
+ island_padding * num_square_width * 0.25
).clip(0, 1)
vc = (
vc * (1 - island_padding * num_square_height * 0.5)
+ island_padding * num_square_height * 0.25
).clip(0, 1)
uc = uc * width
vc = vc * height
# And calculate offsets for each element
idx = torch.arange(uc.shape[0], device=uc.device, dtype=torch.int32)
x_idx = idx % num_square_width
y_idx = idx // num_square_width
# And move each triangle to its own spot
uc = uc + x_idx[:, None] * width
vc = vc + y_idx[:, None] * height
uc = (uc * (1 - 2 * island_padding * 0.5) + island_padding * 0.5).clip(0, 1)
vc = (vc * (1 - 2 * island_padding * 0.5) + island_padding * 0.5).clip(0, 1)
uv[remaining_filter] = torch.stack([uc, vc], dim=-1)
return uv
def _distribute_individual_uvs_in_atlas(
face_uv: Float[Tensor, "Nf 3 2"],
assigned_faces: Integer[Tensor, "Nf"], # noqa: F821
offset_x: Float[Tensor, "Nf"], # noqa: F821
offset_y: Float[Tensor, "Nf"], # noqa: F821
div_x: Float[Tensor, "Nf"], # noqa: F821
div_y: Float[Tensor, "Nf"], # noqa: F821
island_padding: float,
):
# Place the slice first
placed_uv = _handle_slice_uvs(face_uv, assigned_faces, island_padding)
# Then handle the remaining overlap elements
placed_uv = _handle_remaining_uvs(placed_uv, assigned_faces, island_padding)
uc, vc = placed_uv.unbind(-1)
uc = uc / div_x[:, None] + offset_x[:, None]
vc = vc / div_y[:, None] + offset_y[:, None]
uv = torch.stack([uc, vc], dim=-1).view(-1, 2)
return uv
def _get_unique_face_uv(
uv: Float[Tensor, "Nf 3 2"],
) -> Tuple[Float[Tensor, "Utex 3"], Integer[Tensor, "Nf"]]: # noqa: F821
unique_uv, unique_idx = torch.unique(uv, return_inverse=True, dim=0)
# And add the face to uv index mapping
vtex_idx = unique_idx.view(-1, 3)
return unique_uv, vtex_idx
def _align_mesh_with_main_axis(
vertex_positions: Float[Tensor, "Nv 3"], vertex_normals: Float[Tensor, "Nv 3"]
) -> Tuple[Float[Tensor, "Nv 3"], Float[Tensor, "Nv 3"]]:
# Use pca to find the 2 main axis (third is derived by cross product)
# Set the random seed so it's repeatable
torch.manual_seed(0)
_, _, v = torch.pca_lowrank(vertex_positions, q=2)
main_axis, seconday_axis = v[:, 0], v[:, 1]
main_axis: Float[Tensor, "3"] = F.normalize(main_axis, eps=1e-6, dim=-1)
# Orthogonalize the second axis
seconday_axis: Float[Tensor, "3"] = F.normalize(
seconday_axis - dot(seconday_axis, main_axis) * main_axis, eps=1e-6, dim=-1
)
# Create perpendicular third axis
third_axis: Float[Tensor, "3"] = F.normalize(
torch.cross(main_axis, seconday_axis), dim=-1, eps=1e-6
)
# Check to which canonical axis each aligns
main_axis_max_idx = main_axis.abs().argmax().item()
seconday_axis_max_idx = seconday_axis.abs().argmax().item()
third_axis_max_idx = third_axis.abs().argmax().item()
# Now sort the axes based on the argmax so they align with thecanonoical axes
# If two axes have the same argmax move one of them
all_possible_axis = {0, 1, 2}
cur_index = 1
while len(set([main_axis_max_idx, seconday_axis_max_idx, third_axis_max_idx])) != 3:
# Find missing axis
missing_axis = all_possible_axis - set(
[main_axis_max_idx, seconday_axis_max_idx, third_axis_max_idx]
)
missing_axis = missing_axis.pop()
# Just assign it to third axis as it had the smallest contribution to the
# overall shape
if cur_index == 1:
third_axis_max_idx = missing_axis
elif cur_index == 2:
seconday_axis_max_idx = missing_axis
else:
raise ValueError("Could not find 3 unique axis")
cur_index += 1
if len({main_axis_max_idx, seconday_axis_max_idx, third_axis_max_idx}) != 3:
raise ValueError("Could not find 3 unique axis")
axes = [None] * 3
axes[main_axis_max_idx] = main_axis
axes[seconday_axis_max_idx] = seconday_axis
axes[third_axis_max_idx] = third_axis
# Create rotation matrix from the individual axes
rot_mat = torch.stack(axes, dim=1).T
# Now rotate the vertex positions and vertex normals so the mesh aligns with the main axis
vertex_positions = torch.einsum("ij,nj->ni", rot_mat, vertex_positions)
vertex_normals = torch.einsum("ij,nj->ni", rot_mat, vertex_normals)
return vertex_positions, vertex_normals
def box_projection_uv_unwrap(
vertex_positions: Float[Tensor, "Nv 3"],
vertex_normals: Float[Tensor, "Nv 3"],
triangle_idxs: Integer[Tensor, "Nf 3"],
island_padding: float,
) -> Tuple[Float[Tensor, "Utex 3"], Integer[Tensor, "Nf"]]: # noqa: F821
# Align the mesh with main axis directions first
vertex_positions, vertex_normals = _align_mesh_with_main_axis(
vertex_positions, vertex_normals
)
bbox: Float[Tensor, "2 3"] = torch.stack(
[vertex_positions.min(dim=0).values, vertex_positions.max(dim=0).values], dim=0
)
# First decide in which cube face the triangle is placed
face_uv, face_index = _box_assign_vertex_to_cube_face(
vertex_positions, vertex_normals, triangle_idxs, bbox
)
# Rotate the UV islands in a way that they align with the radial z tangent space
face_uv = _rotate_uv_slices_consistent_space(
vertex_positions, vertex_normals, triangle_idxs, face_uv, face_index
)
# Then find where where the face is placed in the atlas.
# This has to detect potential overlaps
assigned_atlas_index = _assign_faces_uv_to_atlas_index(
vertex_positions, triangle_idxs, face_uv, face_index
)
# Then figure out the final place in the atlas based on the assignment
offset_x, offset_y, div_x, div_y = _find_slice_offset_and_scale(
assigned_atlas_index
)
# Next distribute the faces in the uv atlas
placed_uv = _distribute_individual_uvs_in_atlas(
face_uv, assigned_atlas_index, offset_x, offset_y, div_x, div_y, island_padding
)
# And get the unique per-triangle UV coordinates
return _get_unique_face_uv(placed_uv)
@@ -0,0 +1,32 @@
from dataclasses import dataclass, field
from typing import List
import torch
import torch.nn as nn
from StableFast3D.sf3d.models.utils import BaseModule
class LinearCameraEmbedder(BaseModule):
@dataclass
class Config(BaseModule.Config):
in_channels: int = 25
out_channels: int = 768
conditions: List[str] = field(default_factory=list)
cfg: Config
def configure(self) -> None:
self.linear = nn.Linear(self.cfg.in_channels, self.cfg.out_channels)
def forward(self, **kwargs):
cond_tensors = []
for cond_name in self.cfg.conditions:
assert cond_name in kwargs
cond = kwargs[cond_name]
# cond in shape (B, Nv, ...)
cond_tensors.append(cond.view(*cond.shape[:2], -1))
cond_tensor = torch.cat(cond_tensors, dim=-1)
assert cond_tensor.shape[-1] == self.cfg.in_channels
embedding = self.linear(cond_tensor)
return embedding
@@ -0,0 +1,118 @@
from dataclasses import dataclass, field
from typing import Any, List, Optional
import torch.nn as nn
from jaxtyping import Float
from torch import Tensor
from StableFast3D.sf3d.models.network import get_activation
from StableFast3D.sf3d.models.utils import BaseModule
@dataclass
class HeadSpec:
name: str
out_channels: int
n_hidden_layers: int
output_activation: Optional[str] = None
output_bias: float = 0.0
add_to_decoder_features: bool = False
shape: Optional[list[int]] = None
class MultiHeadEstimator(BaseModule):
@dataclass
class Config(BaseModule.Config):
triplane_features: int = 1024
n_layers: int = 2
hidden_features: int = 512
activation: str = "relu"
pool: str = "max"
# Literal["mean", "max"] = "mean" # noqa: F821
heads: List[HeadSpec] = field(default_factory=lambda: [])
cfg: Config
def configure(self):
layers = []
cur_features = self.cfg.triplane_features * 3
for _ in range(self.cfg.n_layers):
layers.append(
nn.Conv2d(
cur_features,
self.cfg.hidden_features,
kernel_size=3,
padding=0,
stride=2,
)
)
layers.append(self.make_activation(self.cfg.activation))
cur_features = self.cfg.hidden_features
self.layers = nn.Sequential(*layers)
assert len(self.cfg.heads) > 0
heads = {}
for head in self.cfg.heads:
head_layers = []
for i in range(head.n_hidden_layers):
head_layers += [
nn.Linear(
self.cfg.hidden_features,
self.cfg.hidden_features,
),
self.make_activation(self.cfg.activation),
]
head_layers += [
nn.Linear(
self.cfg.hidden_features,
head.out_channels,
),
]
heads[head.name] = nn.Sequential(*head_layers)
self.heads = nn.ModuleDict(heads)
def make_activation(self, activation):
if activation == "relu":
return nn.ReLU(inplace=True)
elif activation == "silu":
return nn.SiLU(inplace=True)
else:
raise NotImplementedError
def forward(
self,
triplane: Float[Tensor, "B 3 F Ht Wt"],
) -> dict[str, Any]:
x = self.layers(
triplane.reshape(
triplane.shape[0], -1, triplane.shape[-2], triplane.shape[-1]
)
)
if self.cfg.pool == "max":
x = x.amax(dim=[-2, -1])
elif self.cfg.pool == "mean":
x = x.mean(dim=[-2, -1])
else:
raise NotImplementedError
out = {
("decoder_" if head.add_to_decoder_features else "")
+ head.name: get_activation(head.output_activation)(
self.heads[head.name](x) + head.output_bias
)
for head in self.cfg.heads
}
for head in self.cfg.heads:
if head.shape:
head_name = (
"decoder_" if head.add_to_decoder_features else ""
) + head.name
out[head_name] = out[head_name].reshape(*head.shape)
return out
@@ -0,0 +1,168 @@
from dataclasses import dataclass, field
from typing import Any, List, Optional
import open_clip
import torch
import torch.nn as nn
from jaxtyping import Float
from torch import Tensor
from torchvision.transforms import Normalize
from StableFast3D.sf3d.models.network import get_activation
from StableFast3D.sf3d.models.utils import BaseModule
@dataclass
class HeadSpec:
name: str
out_channels: int
n_hidden_layers: int
output_activation: Optional[str] = None
output_bias: float = 0.0
add_to_decoder_features: bool = False
shape: Optional[list[int]] = None
class ClipBasedHeadEstimator(BaseModule):
@dataclass
class Config(BaseModule.Config):
model: str = "ViT-B-32"
pretrain: str = "laion2b_s34b_b79k"
distribution: str = "beta"
# ["mean", "mode", "sample", "sample_mean"]
distribution_eval: str = "mode"
activation: str = "relu"
hidden_features: int = 512
heads: List[HeadSpec] = field(default_factory=lambda: [])
cfg: Config
def configure(self):
self.model, _, self.preprocess = open_clip.create_model_and_transforms(
self.cfg.model, pretrained=self.cfg.pretrain
)
self.model.eval()
# Do not add the weights in self.model to the optimizer
for param in self.model.parameters():
param.requires_grad = False
assert len(self.cfg.heads) > 0
heads = {}
for head in self.cfg.heads:
head_layers = []
for i in range(head.n_hidden_layers):
head_layers += [
nn.Linear(
self.cfg.hidden_features,
self.cfg.hidden_features,
),
self.make_activation(self.cfg.activation),
]
head_layers = [nn.Sequential(*head_layers)]
head_layers += [
nn.Sequential(
nn.Linear(
self.cfg.hidden_features,
self.cfg.hidden_features,
),
self.make_activation(self.cfg.activation),
nn.Linear(self.cfg.hidden_features, 1),
)
for _ in range(2)
]
heads[head.name] = nn.ModuleList(head_layers)
self.heads = nn.ModuleDict(heads)
def make_activation(self, activation):
if activation == "relu":
return nn.ReLU(inplace=True)
elif activation == "silu":
return nn.SiLU(inplace=True)
else:
raise NotImplementedError
def forward(
self,
cond_image: Float[Tensor, "B 1 H W 3"],
sample: bool = True,
) -> dict[str, Any]:
# Run the model
# Resize cond_image to 224
cond_image = nn.functional.interpolate(
cond_image.flatten(0, 1).permute(0, 3, 1, 2),
size=(224, 224),
mode="bilinear",
align_corners=False,
)
cond_image = Normalize(
mean=open_clip.constants.OPENAI_DATASET_MEAN,
std=open_clip.constants.OPENAI_DATASET_STD,
)(cond_image)
image_features = self.model.encode_image(cond_image)
# Run the heads
outputs = {}
for head_dict in self.cfg.heads:
head_name = head_dict.name
shared_head, d1_h, d2_h = self.heads[head_name]
shared_features = shared_head(image_features)
d1, d2 = [head(shared_features).squeeze(-1) for head in [d1_h, d2_h]]
if self.cfg.distribution == "normal":
mean = d1
var = d2
if mean.shape[-1] == 1:
outputs[head_name] = torch.distributions.Normal(
mean + head_dict.output_bias,
torch.nn.functional.softplus(var),
)
else:
outputs[head_name] = torch.distributions.MultivariateNormal(
mean + head_dict.output_bias,
torch.nn.functional.softplus(var).diag_embed(),
)
elif self.cfg.distribution == "beta":
outputs[head_name] = torch.distributions.Beta(
torch.nn.functional.softplus(d1 + head_dict.output_bias),
torch.nn.functional.softplus(d2 + head_dict.output_bias),
)
else:
raise NotImplementedError
if sample:
for head_dict in self.cfg.heads:
head_name = head_dict.name
dist = outputs[head_name]
if self.cfg.distribution_eval == "mean":
out = dist.mean
elif self.cfg.distribution_eval == "mode":
out = dist.mode
elif self.cfg.distribution_eval == "sample_mean":
out = dist.sample([10]).mean(-1)
else:
# use rsample if gradient is needed
out = dist.rsample() if self.training else dist.sample()
outputs[head_name] = get_activation(head_dict.output_activation)(out)
outputs[f"{head_name}_dist"] = dist
for head in self.cfg.heads:
if head.shape:
if not sample:
raise ValueError(
"Cannot reshape non-sampled probabilisitic outputs"
)
outputs[head.name] = outputs[head.name].reshape(*head.shape)
if head.add_to_decoder_features:
outputs[f"decoder_{head.name}"] = outputs[head.name]
del outputs[head.name]
return outputs
@@ -0,0 +1,229 @@
from typing import Optional, Tuple
import numpy as np
import torch
import torch.nn as nn
from jaxtyping import Float, Integer
from torch import Tensor
from .mesh import Mesh
class IsosurfaceHelper(nn.Module):
points_range: Tuple[float, float] = (0, 1)
@property
def grid_vertices(self) -> Float[Tensor, "N 3"]:
raise NotImplementedError
@property
def requires_instance_per_batch(self) -> bool:
return False
class MarchingTetrahedraHelper(IsosurfaceHelper):
def __init__(self, resolution: int, tets_path: str):
super().__init__()
self.resolution = resolution
self.tets_path = tets_path
self.triangle_table: Float[Tensor, "..."]
self.register_buffer(
"triangle_table",
torch.as_tensor(
[
[-1, -1, -1, -1, -1, -1],
[1, 0, 2, -1, -1, -1],
[4, 0, 3, -1, -1, -1],
[1, 4, 2, 1, 3, 4],
[3, 1, 5, -1, -1, -1],
[2, 3, 0, 2, 5, 3],
[1, 4, 0, 1, 5, 4],
[4, 2, 5, -1, -1, -1],
[4, 5, 2, -1, -1, -1],
[4, 1, 0, 4, 5, 1],
[3, 2, 0, 3, 5, 2],
[1, 3, 5, -1, -1, -1],
[4, 1, 2, 4, 3, 1],
[3, 0, 4, -1, -1, -1],
[2, 0, 1, -1, -1, -1],
[-1, -1, -1, -1, -1, -1],
],
dtype=torch.long,
),
persistent=False,
)
self.num_triangles_table: Integer[Tensor, "..."]
self.register_buffer(
"num_triangles_table",
torch.as_tensor(
[0, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 0], dtype=torch.long
),
persistent=False,
)
self.base_tet_edges: Integer[Tensor, "..."]
self.register_buffer(
"base_tet_edges",
torch.as_tensor([0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3], dtype=torch.long),
persistent=False,
)
tets = np.load(self.tets_path)
self._grid_vertices: Float[Tensor, "..."]
self.register_buffer(
"_grid_vertices",
torch.from_numpy(tets["vertices"]).float(),
persistent=False,
)
self.indices: Integer[Tensor, "..."]
self.register_buffer(
"indices", torch.from_numpy(tets["indices"]).long(), persistent=False
)
self._all_edges: Optional[Integer[Tensor, "Ne 2"]] = None
center_indices, boundary_indices = self.get_center_boundary_index(
self._grid_vertices
)
self.center_indices: Integer[Tensor, "..."]
self.register_buffer("center_indices", center_indices, persistent=False)
self.boundary_indices: Integer[Tensor, "..."]
self.register_buffer("boundary_indices", boundary_indices, persistent=False)
def get_center_boundary_index(self, verts):
magn = torch.sum(verts**2, dim=-1)
center_idx = torch.argmin(magn)
boundary_neg = verts == verts.max()
boundary_pos = verts == verts.min()
boundary = torch.bitwise_or(boundary_pos, boundary_neg)
boundary = torch.sum(boundary.float(), dim=-1)
boundary_idx = torch.nonzero(boundary)
return center_idx, boundary_idx.squeeze(dim=-1)
def normalize_grid_deformation(
self, grid_vertex_offsets: Float[Tensor, "Nv 3"]
) -> Float[Tensor, "Nv 3"]:
return (
(self.points_range[1] - self.points_range[0])
/ self.resolution # half tet size is approximately 1 / self.resolution
* torch.tanh(grid_vertex_offsets)
) # FIXME: hard-coded activation
@property
def grid_vertices(self) -> Float[Tensor, "Nv 3"]:
return self._grid_vertices
@property
def all_edges(self) -> Integer[Tensor, "Ne 2"]:
if self._all_edges is None:
# compute edges on GPU, or it would be VERY SLOW (basically due to the unique operation)
edges = torch.tensor(
[0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3],
dtype=torch.long,
device=self.indices.device,
)
_all_edges = self.indices[:, edges].reshape(-1, 2)
_all_edges_sorted = torch.sort(_all_edges, dim=1)[0]
_all_edges = torch.unique(_all_edges_sorted, dim=0)
self._all_edges = _all_edges
return self._all_edges
def sort_edges(self, edges_ex2):
with torch.no_grad():
order = (edges_ex2[:, 0] > edges_ex2[:, 1]).long()
order = order.unsqueeze(dim=1)
a = torch.gather(input=edges_ex2, index=order, dim=1)
b = torch.gather(input=edges_ex2, index=1 - order, dim=1)
return torch.stack([a, b], -1)
def _forward(self, pos_nx3, sdf_n, tet_fx4):
with torch.no_grad():
occ_n = sdf_n > 0
occ_fx4 = occ_n[tet_fx4.reshape(-1)].reshape(-1, 4)
occ_sum = torch.sum(occ_fx4, -1)
valid_tets = (occ_sum > 0) & (occ_sum < 4)
occ_sum = occ_sum[valid_tets]
# find all vertices
all_edges = tet_fx4[valid_tets][:, self.base_tet_edges].reshape(-1, 2)
all_edges = self.sort_edges(all_edges)
unique_edges, idx_map = torch.unique(all_edges, dim=0, return_inverse=True)
unique_edges = unique_edges.long()
mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
mapping = (
torch.ones(
(unique_edges.shape[0]), dtype=torch.long, device=pos_nx3.device
)
* -1
)
mapping[mask_edges] = torch.arange(
mask_edges.sum(), dtype=torch.long, device=pos_nx3.device
)
idx_map = mapping[idx_map] # map edges to verts
interp_v = unique_edges[mask_edges]
edges_to_interp = pos_nx3[interp_v.reshape(-1)].reshape(-1, 2, 3)
edges_to_interp_sdf = sdf_n[interp_v.reshape(-1)].reshape(-1, 2, 1)
edges_to_interp_sdf[:, -1] *= -1
denominator = edges_to_interp_sdf.sum(1, keepdim=True)
edges_to_interp_sdf = torch.flip(edges_to_interp_sdf, [1]) / denominator
verts = (edges_to_interp * edges_to_interp_sdf).sum(1)
idx_map = idx_map.reshape(-1, 6)
v_id = torch.pow(2, torch.arange(4, dtype=torch.long, device=pos_nx3.device))
tetindex = (occ_fx4[valid_tets] * v_id.unsqueeze(0)).sum(-1)
num_triangles = self.num_triangles_table[tetindex]
# Generate triangle indices
faces = torch.cat(
(
torch.gather(
input=idx_map[num_triangles == 1],
dim=1,
index=self.triangle_table[tetindex[num_triangles == 1]][:, :3],
).reshape(-1, 3),
torch.gather(
input=idx_map[num_triangles == 2],
dim=1,
index=self.triangle_table[tetindex[num_triangles == 2]][:, :6],
).reshape(-1, 3),
),
dim=0,
)
return verts, faces
def forward(
self,
level: Float[Tensor, "N3 1"],
deformation: Optional[Float[Tensor, "N3 3"]] = None,
) -> Mesh:
if deformation is not None:
grid_vertices = self.grid_vertices + self.normalize_grid_deformation(
deformation
)
else:
grid_vertices = self.grid_vertices
v_pos, t_pos_idx = self._forward(grid_vertices, level, self.indices)
mesh = Mesh(
v_pos=v_pos,
t_pos_idx=t_pos_idx,
# extras
grid_vertices=grid_vertices,
tet_edges=self.all_edges,
grid_level=level,
grid_deformation=deformation,
)
return mesh
@@ -0,0 +1,239 @@
from __future__ import annotations
from typing import Any, Dict, Optional
import gpytoolbox
import numpy as np
import torch
import torch.nn.functional as F
import trimesh
from jaxtyping import Float, Integer
from torch import Tensor
from StableFast3D.sf3d.box_uv_unwrap import box_projection_uv_unwrap
from StableFast3D.sf3d.models.utils import dot
class Mesh:
def __init__(
self, v_pos: Float[Tensor, "Nv 3"], t_pos_idx: Integer[Tensor, "Nf 3"], **kwargs
) -> None:
self.v_pos: Float[Tensor, "Nv 3"] = v_pos
self.t_pos_idx: Integer[Tensor, "Nf 3"] = t_pos_idx
self._v_nrm: Optional[Float[Tensor, "Nv 3"]] = None
self._v_tng: Optional[Float[Tensor, "Nv 3"]] = None
self._v_tex: Optional[Float[Tensor, "Nt 3"]] = None
self._edges: Optional[Integer[Tensor, "Ne 2"]] = None
self.extras: Dict[str, Any] = {}
for k, v in kwargs.items():
self.add_extra(k, v)
def add_extra(self, k, v) -> None:
self.extras[k] = v
@property
def requires_grad(self):
return self.v_pos.requires_grad
@property
def v_nrm(self):
if self._v_nrm is None:
self._v_nrm = self._compute_vertex_normal()
return self._v_nrm
@property
def v_tng(self):
if self._v_tng is None:
self._v_tng = self._compute_vertex_tangent()
return self._v_tng
@property
def v_tex(self):
if self._v_tex is None:
self.unwrap_uv()
return self._v_tex
@property
def edges(self):
if self._edges is None:
self._edges = self._compute_edges()
return self._edges
def _compute_vertex_normal(self):
i0 = self.t_pos_idx[:, 0]
i1 = self.t_pos_idx[:, 1]
i2 = self.t_pos_idx[:, 2]
v0 = self.v_pos[i0, :]
v1 = self.v_pos[i1, :]
v2 = self.v_pos[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0, dim=-1)
# Splat face normals to vertices
v_nrm = torch.zeros_like(self.v_pos)
v_nrm.scatter_add_(0, i0[:, None].repeat(1, 3), face_normals)
v_nrm.scatter_add_(0, i1[:, None].repeat(1, 3), face_normals)
v_nrm.scatter_add_(0, i2[:, None].repeat(1, 3), face_normals)
# Normalize, replace zero (degenerated) normals with some default value
v_nrm = torch.where(
dot(v_nrm, v_nrm) > 1e-20, v_nrm, torch.as_tensor([0.0, 0.0, 1.0]).to(v_nrm)
)
v_nrm = F.normalize(v_nrm, dim=1)
if torch.is_anomaly_enabled():
assert torch.all(torch.isfinite(v_nrm))
return v_nrm
def _compute_vertex_tangent(self):
vn_idx = [None] * 3
pos = [None] * 3
tex = [None] * 3
for i in range(0, 3):
pos[i] = self.v_pos[self.t_pos_idx[:, i]]
tex[i] = self.v_tex[self.t_pos_idx[:, i]]
# t_nrm_idx is always the same as t_pos_idx
vn_idx[i] = self.t_pos_idx[:, i]
tangents = torch.zeros_like(self.v_nrm)
tansum = torch.zeros_like(self.v_nrm)
# Compute tangent space for each triangle
duv1 = tex[1] - tex[0]
duv2 = tex[2] - tex[0]
dpos1 = pos[1] - pos[0]
dpos2 = pos[2] - pos[0]
tng_nom = dpos1 * duv2[..., 1:2] - dpos2 * duv1[..., 1:2]
denom = duv1[..., 0:1] * duv2[..., 1:2] - duv1[..., 1:2] * duv2[..., 0:1]
# Avoid division by zero for degenerated texture coordinates
denom_safe = denom.clip(1e-6)
tang = tng_nom / denom_safe
# Update all 3 vertices
for i in range(0, 3):
idx = vn_idx[i][:, None].repeat(1, 3)
tangents.scatter_add_(0, idx, tang) # tangents[n_i] = tangents[n_i] + tang
tansum.scatter_add_(
0, idx, torch.ones_like(tang)
) # tansum[n_i] = tansum[n_i] + 1
# Also normalize it. Here we do not normalize the individual triangles first so larger area
# triangles influence the tangent space more
tangents = tangents / tansum
# Normalize and make sure tangent is perpendicular to normal
tangents = F.normalize(tangents, dim=1)
tangents = F.normalize(tangents - dot(tangents, self.v_nrm) * self.v_nrm)
if torch.is_anomaly_enabled():
assert torch.all(torch.isfinite(tangents))
return tangents
def quad_remesh(
self,
quad_vertex_count: int = 12_000,
quad_rosy: int = 4,
quad_crease_angle: float = 0.0,
quad_smooth_iter: int = 2,
quad_align_to_boundaries: bool = False,
) -> Mesh:
import pynim
v_pos = self.v_pos.detach().cpu().numpy().astype(np.float32)
t_pos_idx = self.t_pos_idx.detach().cpu().numpy().astype(np.int32)
new_vert, new_faces = pynim.remesh(
v_pos,
t_pos_idx,
quad_vertex_count // 4,
rosy=quad_rosy,
posy=4,
creaseAngle=quad_crease_angle,
align_to_boundaries=quad_align_to_boundaries,
smooth_iter=quad_smooth_iter,
deterministic=True,
)
# Briefly load in trimesh
mesh = trimesh.Trimesh(vertices=new_vert, faces=new_faces)
v_pos = torch.from_numpy(mesh.vertices).to(self.v_pos).contiguous()
t_pos_idx = torch.from_numpy(mesh.faces).to(self.t_pos_idx).contiguous()
# Create new mesh
return Mesh(v_pos, t_pos_idx)
def triangle_remesh(
self,
triangle_average_edge_length_multiplier: float = 1.0,
triangle_remesh_steps: int = 10,
):
edges = self.edges
average_edge_length = (
torch.linalg.norm(self.v_pos[edges[:, 0]] - self.v_pos[edges[:, 1]], dim=1)
.mean()
.item()
)
# Convert to numpy
v_pos = self.v_pos.detach().cpu().numpy().astype(np.float64)
t_pos_idx = self.t_pos_idx.detach().cpu().numpy().astype(np.int32)
# Remesh
v_remesh, f_remesh = gpytoolbox.remesh_botsch(
v_pos,
t_pos_idx,
triangle_remesh_steps,
float(average_edge_length * triangle_average_edge_length_multiplier),
)
# Convert back to torch
v_pos = torch.from_numpy(v_remesh).to(self.v_pos).contiguous()
t_pos_idx = torch.from_numpy(f_remesh).to(self.t_pos_idx).contiguous()
# Create new mesh
return Mesh(v_pos, t_pos_idx)
@torch.no_grad()
def unwrap_uv(
self,
island_padding: float = 0.02,
) -> Mesh:
uv, indices = box_projection_uv_unwrap(
self.v_pos, self.v_nrm, self.t_pos_idx, island_padding
)
# Do store per vertex UVs.
# This means we need to duplicate some vertices at the seams
individual_vertices = self.v_pos[self.t_pos_idx].reshape(-1, 3)
individual_faces = torch.arange(
individual_vertices.shape[0],
device=individual_vertices.device,
dtype=self.t_pos_idx.dtype,
).reshape(-1, 3)
uv_flat = uv[indices].reshape((-1, 2))
# uv_flat[:, 1] = 1 - uv_flat[:, 1]
self.v_pos = individual_vertices
self.t_pos_idx = individual_faces
self._v_tex = uv_flat
self._v_nrm = self._compute_vertex_normal()
self._v_tng = self._compute_vertex_tangent()
def _compute_edges(self):
# Compute edges
edges = torch.cat(
[
self.t_pos_idx[:, [0, 1]],
self.t_pos_idx[:, [1, 2]],
self.t_pos_idx[:, [2, 0]],
],
dim=0,
)
edges = edges.sort()[0]
edges = torch.unique(edges, dim=0)
return edges
@@ -0,0 +1,195 @@
from dataclasses import dataclass, field
from typing import Callable, List, Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from jaxtyping import Float
from torch import Tensor
from torch.autograd import Function
from torch.cuda.amp import custom_bwd, custom_fwd
from StableFast3D.sf3d.models.utils import BaseModule, normalize
class PixelShuffleUpsampleNetwork(BaseModule):
@dataclass
class Config(BaseModule.Config):
in_channels: int = 1024
out_channels: int = 40
scale_factor: int = 4
conv_layers: int = 4
conv_kernel_size: int = 3
cfg: Config
def configure(self) -> None:
layers = []
output_channels = self.cfg.out_channels * self.cfg.scale_factor**2
in_channels = self.cfg.in_channels
for i in range(self.cfg.conv_layers):
cur_out_channels = (
in_channels if i != self.cfg.conv_layers - 1 else output_channels
)
layers.append(
nn.Conv2d(
in_channels,
cur_out_channels,
self.cfg.conv_kernel_size,
padding=(self.cfg.conv_kernel_size - 1) // 2,
)
)
if i != self.cfg.conv_layers - 1:
layers.append(nn.ReLU(inplace=True))
layers.append(nn.PixelShuffle(self.cfg.scale_factor))
self.upsample = nn.Sequential(*layers)
def forward(
self, triplanes: Float[Tensor, "B 3 Ci Hp Wp"]
) -> Float[Tensor, "B 3 Co Hp2 Wp2"]:
return rearrange(
self.upsample(
rearrange(triplanes, "B Np Ci Hp Wp -> (B Np) Ci Hp Wp", Np=3)
),
"(B Np) Co Hp Wp -> B Np Co Hp Wp",
Np=3,
)
class _TruncExp(Function): # pylint: disable=abstract-method
# Implementation from torch-ngp:
# https://github.com/ashawkey/torch-ngp/blob/93b08a0d4ec1cc6e69d85df7f0acdfb99603b628/activation.py
@staticmethod
@custom_fwd(cast_inputs=torch.float32)
def forward(ctx, x): # pylint: disable=arguments-differ
ctx.save_for_backward(x)
return torch.exp(x)
@staticmethod
@custom_bwd
def backward(ctx, g): # pylint: disable=arguments-differ
x = ctx.saved_tensors[0]
return g * torch.exp(torch.clamp(x, max=15))
trunc_exp = _TruncExp.apply
def get_activation(name) -> Callable:
if name is None:
return lambda x: x
name = name.lower()
if name == "none" or name == "linear" or name == "identity":
return lambda x: x
elif name == "lin2srgb":
return lambda x: torch.where(
x > 0.0031308,
torch.pow(torch.clamp(x, min=0.0031308), 1.0 / 2.4) * 1.055 - 0.055,
12.92 * x,
).clamp(0.0, 1.0)
elif name == "exp":
return lambda x: torch.exp(x)
elif name == "shifted_exp":
return lambda x: torch.exp(x - 1.0)
elif name == "trunc_exp":
return trunc_exp
elif name == "shifted_trunc_exp":
return lambda x: trunc_exp(x - 1.0)
elif name == "sigmoid":
return lambda x: torch.sigmoid(x)
elif name == "tanh":
return lambda x: torch.tanh(x)
elif name == "shifted_softplus":
return lambda x: F.softplus(x - 1.0)
elif name == "scale_-11_01":
return lambda x: x * 0.5 + 0.5
elif name == "negative":
return lambda x: -x
elif name == "normalize_channel_last":
return lambda x: normalize(x)
elif name == "normalize_channel_first":
return lambda x: normalize(x, dim=1)
else:
try:
return getattr(F, name)
except AttributeError:
raise ValueError(f"Unknown activation function: {name}")
@dataclass
class HeadSpec:
name: str
out_channels: int
n_hidden_layers: int
output_activation: Optional[str] = None
out_bias: float = 0.0
class MaterialMLP(BaseModule):
@dataclass
class Config(BaseModule.Config):
in_channels: int = 120
n_neurons: int = 64
activation: str = "silu"
heads: List[HeadSpec] = field(default_factory=lambda: [])
cfg: Config
def configure(self) -> None:
assert len(self.cfg.heads) > 0
heads = {}
for head in self.cfg.heads:
head_layers = []
for i in range(head.n_hidden_layers):
head_layers += [
nn.Linear(
self.cfg.in_channels if i == 0 else self.cfg.n_neurons,
self.cfg.n_neurons,
),
self.make_activation(self.cfg.activation),
]
head_layers += [
nn.Linear(
self.cfg.n_neurons,
head.out_channels,
),
]
heads[head.name] = nn.Sequential(*head_layers)
self.heads = nn.ModuleDict(heads)
def make_activation(self, activation):
if activation == "relu":
return nn.ReLU(inplace=True)
elif activation == "silu":
return nn.SiLU(inplace=True)
else:
raise NotImplementedError
def keys(self):
return self.heads.keys()
def forward(
self, x, include: Optional[List] = None, exclude: Optional[List] = None
):
if include is not None and exclude is not None:
raise ValueError("Cannot specify both include and exclude.")
if include is not None:
heads = [h for h in self.cfg.heads if h.name in include]
elif exclude is not None:
heads = [h for h in self.cfg.heads if h.name not in exclude]
else:
heads = self.cfg.heads
out = {
head.name: get_activation(head.output_activation)(
self.heads[head.name](x) + head.out_bias
)
for head in heads
}
return out
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,99 @@
from dataclasses import dataclass
from typing import Optional
import torch
import torch.nn as nn
from einops import rearrange
from jaxtyping import Float
from torch import Tensor
from StableFast3D.sf3d.models.tokenizers.dinov2 import Dinov2Model
from StableFast3D.sf3d.models.transformers.attention import Modulation
from StableFast3D.sf3d.models.utils import BaseModule
class DINOV2SingleImageTokenizer(BaseModule):
@dataclass
class Config(BaseModule.Config):
pretrained_model_name_or_path: str = "facebook/dinov2-large"
width: int = 512
height: int = 512
modulation_cond_dim: int = 768
cfg: Config
def configure(self) -> None:
self.model = Dinov2Model.from_pretrained(self.cfg.pretrained_model_name_or_path)
for p in self.model.parameters():
p.requires_grad_(False)
self.model.eval()
self.model.set_gradient_checkpointing(False)
# add modulation
modulations = []
for layer in self.model.encoder.layer:
norm1_modulation = Modulation(
self.model.config.hidden_size,
self.cfg.modulation_cond_dim,
zero_init=True,
single_layer=True,
)
norm2_modulation = Modulation(
self.model.config.hidden_size,
self.cfg.modulation_cond_dim,
zero_init=True,
single_layer=True,
)
layer.register_ada_norm_modulation(norm1_modulation, norm2_modulation)
modulations += [norm1_modulation, norm2_modulation]
self.modulations = nn.ModuleList(modulations)
self.register_buffer(
"image_mean",
torch.as_tensor([0.485, 0.456, 0.406]).reshape(1, 1, 3, 1, 1),
persistent=False,
)
self.register_buffer(
"image_std",
torch.as_tensor([0.229, 0.224, 0.225]).reshape(1, 1, 3, 1, 1),
persistent=False,
)
def forward(
self,
images: Float[Tensor, "B *N C H W"],
modulation_cond: Optional[Float[Tensor, "B *N Cc"]],
**kwargs,
) -> Float[Tensor, "B *N Ct Nt"]:
model = self.model
packed = False
if images.ndim == 4:
packed = True
images = images.unsqueeze(1)
if modulation_cond is not None:
assert modulation_cond.ndim == 2
modulation_cond = modulation_cond.unsqueeze(1)
batch_size, n_input_views = images.shape[:2]
images = (images - self.image_mean) / self.image_std
out = model(
rearrange(images, "B N C H W -> (B N) C H W"),
modulation_cond=rearrange(modulation_cond, "B N Cc -> (B N) Cc")
if modulation_cond is not None
else None,
)
local_features = out.last_hidden_state
local_features = local_features.permute(0, 2, 1)
local_features = rearrange(
local_features, "(B N) Ct Nt -> B N Ct Nt", B=batch_size
)
if packed:
local_features = local_features.squeeze(1)
return local_features
def detokenize(self, *args, **kwargs):
raise NotImplementedError
@@ -0,0 +1,49 @@
import math
from dataclasses import dataclass
import torch
import torch.nn as nn
from einops import rearrange, repeat
from jaxtyping import Float
from torch import Tensor
from StableFast3D.sf3d.models.utils import BaseModule
class TriplaneLearnablePositionalEmbedding(BaseModule):
@dataclass
class Config(BaseModule.Config):
plane_size: int = 96
num_channels: int = 1024
cfg: Config
def configure(self) -> None:
self.embeddings = nn.Parameter(
torch.randn(
(3, self.cfg.num_channels, self.cfg.plane_size, self.cfg.plane_size),
dtype=torch.float32,
)
* 1
/ math.sqrt(self.cfg.num_channels)
)
def forward(self, batch_size: int) -> Float[Tensor, "B Ct Nt"]:
return rearrange(
repeat(self.embeddings, "Np Ct Hp Wp -> B Np Ct Hp Wp", B=batch_size),
"B Np Ct Hp Wp -> B Ct (Np Hp Wp)",
)
def detokenize(
self, tokens: Float[Tensor, "B Ct Nt"]
) -> Float[Tensor, "B 3 Ct Hp Wp"]:
batch_size, Ct, Nt = tokens.shape
assert Nt == self.cfg.plane_size**2 * 3
assert Ct == self.cfg.num_channels
return rearrange(
tokens,
"B Ct (Np Hp Wp) -> B Np Ct Hp Wp",
Np=3,
Hp=self.cfg.plane_size,
Wp=self.cfg.plane_size,
)
@@ -0,0 +1,31 @@
import torch
import torch.nn as nn
class Modulation(nn.Module):
def __init__(
self,
embedding_dim: int,
condition_dim: int,
zero_init: bool = False,
single_layer: bool = False,
):
super().__init__()
self.silu = nn.SiLU()
if single_layer:
self.linear1 = nn.Identity()
else:
self.linear1 = nn.Linear(condition_dim, condition_dim)
self.linear2 = nn.Linear(condition_dim, embedding_dim * 2)
# Only zero init the last linear layer
if zero_init:
nn.init.zeros_(self.linear2.weight)
nn.init.zeros_(self.linear2.bias)
def forward(self, x: torch.Tensor, condition: torch.Tensor) -> torch.Tensor:
emb = self.linear2(self.silu(self.linear1(condition)))
scale, shift = torch.chunk(emb, 2, dim=1)
x = x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
return x
@@ -0,0 +1,515 @@
from dataclasses import dataclass
from typing import Optional
import torch
import torch.nn.functional as F
from torch import nn
from StableFast3D.sf3d.models.utils import BaseModule
class GEGLU(nn.Module):
r"""
A variant of the gated linear unit activation function from https://arxiv.org/abs/2002.05202.
Parameters:
dim_in (`int`): The number of channels in the input.
dim_out (`int`): The number of channels in the output.
"""
def __init__(self, dim_in: int, dim_out: int):
super().__init__()
self.proj = nn.Linear(dim_in, dim_out * 2)
def gelu(self, gate: torch.Tensor) -> torch.Tensor:
if gate.device.type != "mps":
return F.gelu(gate)
# mps: gelu is not implemented for float16
return F.gelu(gate.to(dtype=torch.float32)).to(dtype=gate.dtype)
def forward(self, hidden_states, scale: float = 1.0):
args = ()
hidden_states, gate = self.proj(hidden_states, *args).chunk(2, dim=-1)
return hidden_states * self.gelu(gate)
class CrossAttention(nn.Module):
def __init__(
self,
dim,
kv_dim=None,
num_heads=16,
qkv_bias=False,
attn_drop=0.0,
proj_drop=0.0,
):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim**-0.5
kv_dim = dim if not kv_dim else kv_dim
self.wq = nn.Linear(dim, dim, bias=qkv_bias)
self.wk = nn.Linear(kv_dim, dim, bias=qkv_bias)
self.wv = nn.Linear(kv_dim, dim, bias=qkv_bias)
self.attn_drop = attn_drop
self.proj = nn.Linear(dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
def forward(self, x_q, x_kv):
B, N_q, C = x_q.shape
B, N_kv, _ = x_kv.shape
# [B, N_q, C] -> [B, N_q, H, C/H]
q = self.wq(x_q).reshape(B, N_q, self.num_heads, C // self.num_heads)
# [B, N_kv, C] -> [B, N_kv, H, C/H]
k = self.wk(x_kv).reshape(B, N_kv, self.num_heads, C // self.num_heads)
v = self.wv(x_kv).reshape(B, N_kv, self.num_heads, C // self.num_heads)
# attention
x = torch.nn.functional.scaled_dot_product_attention(
q.permute(0, 2, 1, 3),
k.permute(0, 2, 1, 3),
v.permute(0, 2, 1, 3),
attn_mask=None,
dropout_p=self.attn_drop,
scale=self.scale,
).permute(0, 2, 1, 3)
# [B, N_q, H, C/H] -> [B, N_q, C]
x = x.reshape(B, N_q, C)
x = self.proj(x)
x = self.proj_drop(x)
return x
class FeedForward(nn.Module):
def __init__(
self,
dim: int,
dim_out: Optional[int] = None,
mult: int = 4,
dropout: float = 0.0,
):
super().__init__()
inner_dim = int(dim * mult)
dim_out = dim_out if dim_out is not None else dim
act_fn = GEGLU(dim, inner_dim)
self.net = nn.ModuleList([])
self.net.append(act_fn)
self.net.append(nn.Dropout(dropout))
self.net.append(nn.Linear(inner_dim, dim_out))
def forward(self, x: torch.Tensor) -> torch.Tensor:
for module in self.net:
x = module(x)
return x
class BasicBlock(nn.Module):
def __init__(
self,
dim: int,
kv_dim: Optional[int] = None,
num_heads: int = 16,
qkv_bias: bool = False,
attn_drop: float = 0.0,
proj_drop: float = 0.0,
ff_drop: float = 0.0,
):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn1 = CrossAttention(
dim,
kv_dim=dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
attn_drop=attn_drop,
proj_drop=proj_drop,
)
self.norm2 = nn.LayerNorm(dim)
self.attn2 = CrossAttention(
dim,
kv_dim=kv_dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
attn_drop=attn_drop,
proj_drop=proj_drop,
)
self.norm3 = nn.LayerNorm(dim)
self.ff = FeedForward(dim, dropout=ff_drop)
def forward(self, z, x):
z_norm = self.norm1(z)
z = z + self.attn1(z_norm, z_norm)
# TODO: do we need to have the second attention when x is None?
z_norm = self.norm2(z)
z = z + self.attn2(z_norm, x if x is not None else z_norm)
z_norm = self.norm3(z)
z = z + self.ff(z_norm)
return z
class SingleStreamTransformer(BaseModule):
@dataclass
class Config(BaseModule.Config):
num_attention_heads: int = 16
attention_head_dim: int = 88
in_channels: Optional[int] = None
out_channels: Optional[int] = None
num_layers: int = 16
dropout: float = 0.0
norm_num_groups: int = 32
cross_attention_dim: Optional[int] = None
attention_bias: bool = False
cfg: Config
def configure(self) -> None:
self.num_attention_heads = self.cfg.num_attention_heads
self.attention_head_dim = self.cfg.attention_head_dim
inner_dim = self.num_attention_heads * self.attention_head_dim
# Define input layers
self.norm = torch.nn.GroupNorm(
num_groups=self.cfg.norm_num_groups,
num_channels=self.cfg.in_channels,
eps=1e-6,
affine=True,
)
self.proj_in = nn.Linear(self.cfg.in_channels, inner_dim)
# Define transformers blocks
self.transformer_blocks = nn.ModuleList(
[
BasicBlock(
inner_dim,
kv_dim=self.cfg.cross_attention_dim,
num_heads=self.num_attention_heads,
qkv_bias=self.cfg.attention_bias,
proj_drop=self.cfg.dropout,
ff_drop=self.cfg.dropout,
)
for d in range(self.cfg.num_layers)
]
)
# 4. Define output layers
self.proj_out = nn.Linear(inner_dim, self.cfg.in_channels)
def forward(self, hidden_states, encoder_hidden_states=None, **kwargs):
residual = hidden_states
hidden_states = self.norm(hidden_states)
hidden_states = hidden_states.permute(0, 2, 1)
hidden_states = self.proj_in(hidden_states)
for block in self.transformer_blocks:
hidden_states = block(hidden_states, encoder_hidden_states)
hidden_states = self.proj_out(hidden_states).permute(0, 2, 1).contiguous()
# TODO: do we really need to add the residual?
hidden_states = hidden_states + residual
return hidden_states
class FuseBlock(nn.Module):
"""
Fuse X in to Z with cross attention
"""
def __init__(
self,
dim_z: int,
dim_x: int,
num_heads: int = 16,
qkv_bias: bool = False,
attn_drop: float = 0.0,
proj_drop: float = 0.0,
ff_drop: float = 0.0,
norm_x_input: bool = True,
):
super().__init__()
self.norm_x_input = norm_x_input
if self.norm_x_input:
self.norm_x = nn.LayerNorm(dim_x)
self.attn = CrossAttention(
dim_z,
kv_dim=dim_x,
num_heads=num_heads,
qkv_bias=qkv_bias,
attn_drop=attn_drop,
proj_drop=proj_drop,
)
self.norm_z1 = nn.LayerNorm(dim_z)
self.norm_z2 = nn.LayerNorm(dim_z)
self.ff = FeedForward(dim_z, dropout=ff_drop)
def forward(self, z, x):
# TODO: do we need to normalize x?
z = z + self.attn(self.norm_z1(z), self.norm_x(x) if self.norm_x_input else x)
z = z + self.ff(self.norm_z2(z))
return z
@torch.no_grad()
def get_triplane_attention_mask(res):
N = 3 * res * res
attn_mask = torch.zeros(3, res, res, 3, res, res)
i, j = torch.meshgrid(torch.arange(res), torch.arange(res))
attn_mask[0, i, j, 1, i, :] = 1.0
attn_mask[0, i, j, 2, j, :] = 1.0
attn_mask[1, i, j, 0, i, :] = 1.0
attn_mask[1, i, j, 2, :, j] = 1.0
attn_mask[2, i, j, 0, :, i] = 1.0
attn_mask[2, i, j, 1, :, j] = 1.0
attn_mask = attn_mask.bool()
attn_bias = torch.empty_like(attn_mask, dtype=torch.float)
attn_bias.masked_fill_(attn_mask, 0.0)
attn_bias.masked_fill_(~attn_mask, float("-inf"))
return attn_bias.reshape(N, N)
class TriplaneAttention(nn.Module):
def __init__(
self,
dim: int,
resolution: int,
num_heads: int = 16,
qkv_bias: bool = False,
attn_drop: float = 0.0,
proj_drop: float = 0.0,
full_attention: bool = False,
):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim**-0.5
self.wq = nn.Linear(dim, dim, bias=qkv_bias)
self.wk = nn.Linear(dim, dim, bias=qkv_bias)
self.wv = nn.Linear(dim, dim, bias=qkv_bias)
self.attn_drop = attn_drop
self.proj = nn.Linear(dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
self.resolution = resolution
self.full_attention = full_attention
self.attn_mask = (
get_triplane_attention_mask(resolution) if not full_attention else None
)
def forward(self, x):
B, N, C = x.shape
# [B, N, C] -> [B, N, H, C/H]
q = self.wq(x).reshape(B, N, self.num_heads, C // self.num_heads)
k = self.wk(x).reshape(B, N, self.num_heads, C // self.num_heads)
v = self.wv(x).reshape(B, N, self.num_heads, C // self.num_heads)
# detokenize the planes
assert N == self.resolution**2 * 3
attn_bias = (
self.attn_mask.to(q)
.unsqueeze(0)
.unsqueeze(0)
.expand(B, self.num_heads, -1, -1)
if not self.full_attention
else None
)
# full attention
x = torch.nn.functional.scaled_dot_product_attention(
q.permute(0, 2, 1, 3),
k.permute(0, 2, 1, 3),
v.permute(0, 2, 1, 3),
attn_mask=attn_bias,
dropout_p=self.attn_drop,
scale=self.scale,
).permute(0, 2, 1, 3)
# [B, N_q, H, C/H] -> [B, N_q, C]
x = x.reshape(B, N, C)
x = self.proj(x)
x = self.proj_drop(x)
return x
class TwoStreamBlock(nn.Module):
def __init__(
self,
dim_latent: int,
dim_input: int,
num_basic_blocks: int = 4,
num_heads: int = 16,
qkv_bias: bool = False,
attn_drop: float = 0.0,
proj_drop: float = 0.0,
ff_drop: float = 0.0,
norm_x_input: bool = True,
dim_cross: Optional[int] = None,
):
super().__init__()
# Define the fuse block that fuse the input into the latent
self.fuse_block_in = FuseBlock(
dim_latent,
dim_input,
num_heads=num_heads,
qkv_bias=qkv_bias,
attn_drop=attn_drop,
proj_drop=proj_drop,
ff_drop=ff_drop,
norm_x_input=norm_x_input,
)
# Define the transformer block that process the latent
self.transformer_block = nn.ModuleList(
[
BasicBlock(
dim_latent,
kv_dim=dim_cross,
num_heads=num_heads,
qkv_bias=qkv_bias,
proj_drop=proj_drop,
ff_drop=ff_drop,
)
for _ in range(num_basic_blocks)
]
)
# Define the fuse block that fuse the latent into the input
self.fuse_block_out = FuseBlock(
dim_input,
dim_latent,
num_heads=num_heads,
qkv_bias=qkv_bias,
attn_drop=attn_drop,
proj_drop=proj_drop,
ff_drop=ff_drop,
norm_x_input=norm_x_input,
)
def forward(self, latent, input, cross_input):
latent = self.fuse_block_in(latent, input)
for block in self.transformer_block:
latent = block(latent, cross_input)
input = self.fuse_block_out(input, latent)
return latent, input
class TwoStreamInterleaveTransformer(BaseModule):
@dataclass
class Config(BaseModule.Config):
num_attention_heads: int = 16
attention_head_dim: int = 64
raw_triplane_channels: int = 1024
triplane_channels: int = 1024
raw_image_channels: int = 1024
num_latents: int = 1792
num_blocks: int = 4
num_basic_blocks: int = 3
dropout: float = 0.0
latent_init_std: float = 0.02
norm_num_groups: int = 32
attention_bias: bool = False
norm_x_input: bool = False
cross_attention_dim: int = 1024
mix_latent: bool = True
cfg: Config
def configure(self) -> None:
self.mix_latent = self.cfg.mix_latent
# Define the dimensions
self.num_attention_heads = self.cfg.num_attention_heads
self.attention_head_dim = self.cfg.attention_head_dim
self.num_latents = self.cfg.num_latents
self.latent_dim = self.num_attention_heads * self.attention_head_dim
# Define input layers
if self.cfg.norm_num_groups > 0:
self.norm_triplane = torch.nn.GroupNorm(
num_groups=self.cfg.norm_num_groups,
num_channels=self.cfg.raw_triplane_channels,
eps=1e-6,
affine=True,
)
else:
self.norm_triplane = nn.LayerNorm(self.cfg.raw_triplane_channels)
self.proj_triplane = nn.Linear(
self.cfg.raw_triplane_channels, self.cfg.triplane_channels
)
if self.mix_latent:
self.norm_image = nn.LayerNorm(self.cfg.raw_image_channels)
self.proj_image = nn.Linear(self.cfg.raw_image_channels, self.latent_dim)
self.norm_latent = nn.LayerNorm(self.latent_dim)
self.proj_latent = nn.Linear(self.latent_dim, self.latent_dim)
# Define the latents
self.latent_init = nn.Parameter(
torch.zeros(1, self.num_latents, self.latent_dim)
)
nn.init.normal_(self.latent_init, std=self.cfg.latent_init_std)
# Define the transformer blocks
self.main_blocks = nn.ModuleList(
[
TwoStreamBlock(
self.latent_dim,
self.cfg.triplane_channels,
num_basic_blocks=self.cfg.num_basic_blocks,
num_heads=self.num_attention_heads,
qkv_bias=self.cfg.attention_bias,
proj_drop=self.cfg.dropout,
ff_drop=self.cfg.dropout,
norm_x_input=self.cfg.norm_x_input,
dim_cross=self.cfg.cross_attention_dim,
)
for _ in range(self.cfg.num_blocks)
]
)
# 4. Define output layers
self.proj_out = nn.Linear(
self.cfg.triplane_channels, self.cfg.raw_triplane_channels
)
def forward(self, hidden_states, encoder_hidden_states, **kwargs):
# hidden_states: [B, triplane_dim, N_triplane] is triplane tokens
# encoder_hidden_states: [B, N_image, image_dim] is the image tokens
if isinstance(self.norm_triplane, nn.GroupNorm):
triplane_tokens = self.norm_triplane(hidden_states)
triplane_tokens = triplane_tokens.permute(
0, 2, 1
) # [B, N_triplane, triplane_dim]
elif isinstance(self.norm_triplane, nn.LayerNorm):
triplane_tokens = self.norm_triplane(hidden_states.permute(0, 2, 1))
else:
raise ValueError("Unknown normalization layer")
triplane_tokens = self.proj_triplane(triplane_tokens)
if self.mix_latent:
image_tokens = self.norm_image(
encoder_hidden_states
) # [B, N_image, image_dim]
image_tokens = self.proj_image(image_tokens)
init_latents = self.latent_init.expand(
hidden_states.shape[0], -1, -1
) # [B, N_latent_init, latent_dim]
init_latents = self.norm_latent(init_latents)
init_latents = self.proj_latent(init_latents)
if self.mix_latent:
latent_tokens = torch.cat(
[image_tokens, init_latents], dim=1
) # [B, N_latent, latent_dim]
else:
latent_tokens = init_latents
# forward the main blocks
for block in self.main_blocks:
latent_tokens, triplane_tokens = block(
latent_tokens, triplane_tokens, encoder_hidden_states
)
# project the triplane tokens back to the original dimension
triplane_tokens = self.proj_out(triplane_tokens).permute(0, 2, 1).contiguous()
triplane_tokens = triplane_tokens + hidden_states
return triplane_tokens
@@ -0,0 +1,292 @@
import dataclasses
import importlib
import math
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple, Union
import numpy as np
import PIL
import torch
import torch.nn as nn
import torch.nn.functional as F
from jaxtyping import Bool, Float, Int, Num
from omegaconf import DictConfig, OmegaConf
from torch import Tensor
class BaseModule(nn.Module):
@dataclass
class Config:
pass
cfg: Config # add this to every subclass of BaseModule to enable static type checking
def __init__(
self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs
) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs) -> None:
raise NotImplementedError
def find_class(cls_string):
module_string = ".".join(cls_string.split(".")[:-1])
cls_name = cls_string.split(".")[-1]
module = importlib.import_module(module_string, package=None)
cls = getattr(module, cls_name)
return cls
def parse_structured(fields: Any, cfg: Optional[Union[dict, DictConfig]] = None) -> Any:
# Check if cfg.keys are in fields
cfg_ = cfg.copy()
keys = list(cfg_.keys())
field_names = {f.name for f in dataclasses.fields(fields)}
for key in keys:
# This is helpful when swapping out modules from CLI
if key not in field_names:
print(f"Ignoring {key} as it's not supported by {fields}")
cfg_.pop(key)
scfg = OmegaConf.merge(OmegaConf.structured(fields), cfg_)
return scfg
EPS_DTYPE = {
torch.float16: 1e-4,
torch.bfloat16: 1e-4,
torch.float32: 1e-7,
torch.float64: 1e-8,
}
def dot(x, y, dim=-1):
return torch.sum(x * y, dim, keepdim=True)
def reflect(x, n):
return x - 2 * dot(x, n) * n
def normalize(x, dim=-1, eps=None):
if eps is None:
eps = EPS_DTYPE[x.dtype]
return F.normalize(x, dim=dim, p=2, eps=eps)
def tri_winding(tri: Float[Tensor, "*B 3 2"]) -> Float[Tensor, "*B 3 3"]:
# One pad for determinant
tri_sq = F.pad(tri, (0, 1), "constant", 1.0)
det_tri = torch.det(tri_sq)
tri_rev = torch.cat(
(tri_sq[..., 0:1, :], tri_sq[..., 2:3, :], tri_sq[..., 1:2, :]), -2
)
tri_sq[det_tri < 0] = tri_rev[det_tri < 0]
return tri_sq
def triangle_intersection_2d(
t1: Float[Tensor, "*B 3 2"],
t2: Float[Tensor, "*B 3 2"],
eps=1e-12,
) -> Float[Tensor, "*B"]: # noqa: F821
"""Returns True if triangles collide, False otherwise"""
def chk_edge(x: Float[Tensor, "*B 3 3"]) -> Bool[Tensor, "*B"]: # noqa: F821
logdetx = torch.logdet(x.double())
if eps is None:
return ~torch.isfinite(logdetx)
return ~(torch.isfinite(logdetx) & (logdetx > math.log(eps)))
t1s = tri_winding(t1)
t2s = tri_winding(t2)
# Assume the triangles do not collide in the begging
ret = torch.zeros(t1.shape[0], dtype=torch.bool, device=t1.device)
for i in range(3):
edge = torch.roll(t1s, i, dims=1)[:, :2, :]
# Check if all points of triangle 2 lay on the external side of edge E.
# If this is the case the triangle do not collide
upd = (
chk_edge(torch.cat((edge, t2s[:, 0:1]), 1))
& chk_edge(torch.cat((edge, t2s[:, 1:2]), 1))
& chk_edge(torch.cat((edge, t2s[:, 2:3]), 1))
)
# Here no collision is still True due to inversion
ret = ret | upd
for i in range(3):
edge = torch.roll(t2s, i, dims=1)[:, :2, :]
upd = (
chk_edge(torch.cat((edge, t1s[:, 0:1]), 1))
& chk_edge(torch.cat((edge, t1s[:, 1:2]), 1))
& chk_edge(torch.cat((edge, t1s[:, 2:3]), 1))
)
# Here no collision is still True due to inversion
ret = ret | upd
return ~ret # Do the inversion
ValidScale = Union[Tuple[float, float], Num[Tensor, "2 D"]]
def scale_tensor(
dat: Num[Tensor, "... D"], inp_scale: ValidScale, tgt_scale: ValidScale
):
if inp_scale is None:
inp_scale = (0, 1)
if tgt_scale is None:
tgt_scale = (0, 1)
if isinstance(tgt_scale, Tensor):
assert dat.shape[-1] == tgt_scale.shape[-1]
dat = (dat - inp_scale[0]) / (inp_scale[1] - inp_scale[0])
dat = dat * (tgt_scale[1] - tgt_scale[0]) + tgt_scale[0]
return dat
def dilate_fill(img, mask, iterations=10):
oldMask = mask.float()
oldImg = img
mask_kernel = torch.ones(
(1, 1, 3, 3),
dtype=oldMask.dtype,
device=oldMask.device,
)
for i in range(iterations):
newMask = torch.nn.functional.max_pool2d(oldMask, 3, 1, 1)
# Fill the extension with mean color of old valid regions
img_unfold = F.unfold(oldImg, (3, 3)).view(1, 3, 3 * 3, -1)
mask_unfold = F.unfold(oldMask, (3, 3)).view(1, 1, 3 * 3, -1)
new_mask_unfold = F.unfold(newMask, (3, 3)).view(1, 1, 3 * 3, -1)
# Average color of the valid region
mean_color = (img_unfold.sum(dim=2) / mask_unfold.sum(dim=2).clip(1)).unsqueeze(
2
)
# Extend it to the new region
fill_color = (mean_color * new_mask_unfold).view(1, 3 * 3 * 3, -1)
mask_conv = F.conv2d(
newMask, mask_kernel, padding=1
) # Get the sum for each kernel patch
newImg = F.fold(
fill_color, (img.shape[-2], img.shape[-1]), (3, 3)
) / mask_conv.clamp(1)
diffMask = newMask - oldMask
oldMask = newMask
oldImg = torch.lerp(oldImg, newImg, diffMask)
return oldImg
def float32_to_uint8_np(
x: Float[np.ndarray, "*B H W C"],
dither: bool = True,
dither_mask: Optional[Float[np.ndarray, "*B H W C"]] = None,
dither_strength: float = 1.0,
) -> Int[np.ndarray, "*B H W C"]:
if dither:
dither = (
dither_strength * np.random.rand(*x[..., :1].shape).astype(np.float32) - 0.5
)
if dither_mask is not None:
dither = dither * dither_mask
return np.clip(np.floor((256.0 * x + dither)), 0, 255).astype(np.uint8)
return np.clip(np.floor((256.0 * x)), 0, 255).astype(torch.uint8)
def convert_data(data):
if data is None:
return None
elif isinstance(data, np.ndarray):
return data
elif isinstance(data, torch.Tensor):
if data.dtype in [torch.float16, torch.bfloat16]:
data = data.float()
return data.detach().cpu().numpy()
elif isinstance(data, list):
return [convert_data(d) for d in data]
elif isinstance(data, dict):
return {k: convert_data(v) for k, v in data.items()}
else:
raise TypeError(
"Data must be in type numpy.ndarray, torch.Tensor, list or dict, getting",
type(data),
)
class ImageProcessor:
def convert_and_resize(
self,
image: Union[PIL.Image.Image, np.ndarray, torch.Tensor],
size: int,
):
if isinstance(image, PIL.Image.Image):
image = torch.from_numpy(np.array(image).astype(np.float32) / 255.0)
elif isinstance(image, np.ndarray):
if image.dtype == np.uint8:
image = torch.from_numpy(image.astype(np.float32) / 255.0)
else:
image = torch.from_numpy(image)
elif isinstance(image, torch.Tensor):
pass
batched = image.ndim == 4
if not batched:
image = image[None, ...]
image = F.interpolate(
image.permute(0, 3, 1, 2),
(size, size),
mode="bilinear",
align_corners=False,
antialias=True,
).permute(0, 2, 3, 1)
if not batched:
image = image[0]
return image
def __call__(
self,
image: Union[
PIL.Image.Image,
np.ndarray,
torch.FloatTensor,
List[PIL.Image.Image],
List[np.ndarray],
List[torch.FloatTensor],
],
size: int,
) -> Any:
if isinstance(image, (np.ndarray, torch.FloatTensor)) and image.ndim == 4:
image = self.convert_and_resize(image, size)
else:
if not isinstance(image, list):
image = [image]
image = [self.convert_and_resize(im, size) for im in image]
image = torch.stack(image, dim=0)
return image
def get_intrinsic_from_fov(fov, H, W, bs=-1):
focal_length = 0.5 * H / np.tan(0.5 * fov)
intrinsic = np.identity(3, dtype=np.float32)
intrinsic[0, 0] = focal_length
intrinsic[1, 1] = focal_length
intrinsic[0, 2] = W / 2.0
intrinsic[1, 2] = H / 2.0
if bs > 0:
intrinsic = intrinsic[None].repeat(bs, axis=0)
return torch.from_numpy(intrinsic)
+479
View File
@@ -0,0 +1,479 @@
import os
from dataclasses import dataclass, field
from typing import Any, List, Literal, Optional, Tuple
import numpy as np
import torch
import torch.nn.functional as F
import trimesh
from einops import rearrange
from huggingface_hub import hf_hub_download
from jaxtyping import Float
from omegaconf import OmegaConf
from PIL import Image
from safetensors.torch import load_model
from torch import Tensor
from StableFast3D.sf3d.models.isosurface import MarchingTetrahedraHelper
from StableFast3D.sf3d.models.mesh import Mesh
from StableFast3D.sf3d.models.utils import (
BaseModule,
ImageProcessor,
convert_data,
dilate_fill,
dot,
find_class,
float32_to_uint8_np,
normalize,
scale_tensor,
)
from StableFast3D.sf3d.utils import create_intrinsic_from_fov_deg, default_cond_c2w
from .texture_baker import TextureBaker
class SF3D(BaseModule):
@dataclass
class Config(BaseModule.Config):
cond_image_size: int
isosurface_resolution: int
isosurface_threshold: float = 10.0
radius: float = 1.0
background_color: list[float] = field(default_factory=lambda: [0.5, 0.5, 0.5])
default_fovy_deg: float = 40.0
default_distance: float = 1.6
camera_embedder_cls: str = ""
camera_embedder: dict = field(default_factory=dict)
image_tokenizer_cls: str = ""
image_tokenizer: dict = field(default_factory=dict)
tokenizer_cls: str = ""
tokenizer: dict = field(default_factory=dict)
backbone_cls: str = ""
backbone: dict = field(default_factory=dict)
post_processor_cls: str = ""
post_processor: dict = field(default_factory=dict)
decoder_cls: str = ""
decoder: dict = field(default_factory=dict)
image_estimator_cls: str = ""
image_estimator: dict = field(default_factory=dict)
global_estimator_cls: str = ""
global_estimator: dict = field(default_factory=dict)
cfg: Config
@classmethod
def from_pretrained(
cls, config_path: str, weight_path: str
):
cfg = OmegaConf.load(config_path)
OmegaConf.resolve(cfg)
model = cls(cfg)
load_model(model, weight_path)
return model
@property
def device(self):
return next(self.parameters()).device
def configure(self):
self.image_tokenizer = find_class(self.cfg.image_tokenizer_cls)(
self.cfg.image_tokenizer
)
self.tokenizer = find_class(self.cfg.tokenizer_cls)(self.cfg.tokenizer)
self.camera_embedder = find_class(self.cfg.camera_embedder_cls)(
self.cfg.camera_embedder
)
self.backbone = find_class(self.cfg.backbone_cls)(self.cfg.backbone)
self.post_processor = find_class(self.cfg.post_processor_cls)(
self.cfg.post_processor
)
self.decoder = find_class(self.cfg.decoder_cls)(self.cfg.decoder)
self.image_estimator = find_class(self.cfg.image_estimator_cls)(
self.cfg.image_estimator
)
self.global_estimator = find_class(self.cfg.global_estimator_cls)(
self.cfg.global_estimator
)
self.bbox: Float[Tensor, "2 3"]
self.register_buffer(
"bbox",
torch.as_tensor(
[
[-self.cfg.radius, -self.cfg.radius, -self.cfg.radius],
[self.cfg.radius, self.cfg.radius, self.cfg.radius],
],
dtype=torch.float32,
),
)
self.isosurface_helper = MarchingTetrahedraHelper(
self.cfg.isosurface_resolution,
os.path.join(
os.path.dirname(__file__),
"..",
"load",
"tets",
f"{self.cfg.isosurface_resolution}_tets.npz",
),
)
self.baker = TextureBaker()
self.image_processor = ImageProcessor()
def triplane_to_meshes(
self, triplanes: Float[Tensor, "B 3 Cp Hp Wp"]
) -> list[Mesh]:
meshes = []
for i in range(triplanes.shape[0]):
triplane = triplanes[i]
grid_vertices = scale_tensor(
self.isosurface_helper.grid_vertices.to(triplanes.device),
self.isosurface_helper.points_range,
self.bbox,
)
values = self.query_triplane(grid_vertices, triplane)
decoded = self.decoder(values, include=["vertex_offset", "density"])
sdf = decoded["density"] - self.cfg.isosurface_threshold
deform = decoded["vertex_offset"].squeeze(0)
mesh: Mesh = self.isosurface_helper(
sdf.view(-1, 1), deform.view(-1, 3) if deform is not None else None
)
mesh.v_pos = scale_tensor(
mesh.v_pos, self.isosurface_helper.points_range, self.bbox
)
meshes.append(mesh)
return meshes
def query_triplane(
self,
positions: Float[Tensor, "*B N 3"],
triplanes: Float[Tensor, "*B 3 Cp Hp Wp"],
) -> Float[Tensor, "*B N F"]:
batched = positions.ndim == 3
if not batched:
# no batch dimension
triplanes = triplanes[None, ...]
positions = positions[None, ...]
assert triplanes.ndim == 5 and positions.ndim == 3
positions = scale_tensor(
positions, (-self.cfg.radius, self.cfg.radius), (-1, 1)
)
indices2D: Float[Tensor, "B 3 N 2"] = torch.stack(
(positions[..., [0, 1]], positions[..., [0, 2]], positions[..., [1, 2]]),
dim=-3,
).to(triplanes.dtype)
out: Float[Tensor, "B3 Cp 1 N"] = F.grid_sample(
rearrange(triplanes, "B Np Cp Hp Wp -> (B Np) Cp Hp Wp", Np=3).float(),
rearrange(indices2D, "B Np N Nd -> (B Np) () N Nd", Np=3).float(),
align_corners=True,
mode="bilinear",
)
out = rearrange(out, "(B Np) Cp () N -> B N (Np Cp)", Np=3)
return out
def get_scene_codes(self, batch) -> Float[Tensor, "B 3 C H W"]:
# if batch[rgb_cond] is only one view, add a view dimension
if len(batch["rgb_cond"].shape) == 4:
batch["rgb_cond"] = batch["rgb_cond"].unsqueeze(1)
batch["mask_cond"] = batch["mask_cond"].unsqueeze(1)
batch["c2w_cond"] = batch["c2w_cond"].unsqueeze(1)
batch["intrinsic_cond"] = batch["intrinsic_cond"].unsqueeze(1)
batch["intrinsic_normed_cond"] = batch["intrinsic_normed_cond"].unsqueeze(1)
batch_size, n_input_views = batch["rgb_cond"].shape[:2]
camera_embeds: Optional[Float[Tensor, "B Nv Cc"]]
camera_embeds = self.camera_embedder(**batch)
input_image_tokens: Float[Tensor, "B Nv Cit Nit"] = self.image_tokenizer(
rearrange(batch["rgb_cond"], "B Nv H W C -> B Nv C H W"),
modulation_cond=camera_embeds,
)
input_image_tokens = rearrange(
input_image_tokens, "B Nv C Nt -> B (Nv Nt) C", Nv=n_input_views
)
tokens: Float[Tensor, "B Ct Nt"] = self.tokenizer(batch_size)
tokens = self.backbone(
tokens,
encoder_hidden_states=input_image_tokens,
modulation_cond=None,
)
direct_codes = self.tokenizer.detokenize(tokens)
scene_codes = self.post_processor(direct_codes)
return scene_codes, direct_codes
def run_image(
self,
image: Image,
bake_resolution: int,
remesh: Literal["none", "triangle", "quad"] = "none",
estimate_illumination: bool = False,
) -> Tuple[trimesh.Trimesh, dict[str, Any]]:
if image.mode != "RGBA":
raise ValueError("Image must be in RGBA mode")
img_cond = (
torch.from_numpy(
np.asarray(
image.resize((self.cfg.cond_image_size, self.cfg.cond_image_size))
).astype(np.float32)
/ 255.0
)
.float()
.clip(0, 1)
.to(self.device)
)
mask_cond = img_cond[:, :, -1:]
rgb_cond = torch.lerp(
torch.tensor(self.cfg.background_color, device=self.device)[None, None, :],
img_cond[:, :, :3],
mask_cond,
)
c2w_cond = default_cond_c2w(self.cfg.default_distance).to(self.device)
intrinsic, intrinsic_normed_cond = create_intrinsic_from_fov_deg(
self.cfg.default_fovy_deg,
self.cfg.cond_image_size,
self.cfg.cond_image_size,
)
batch = {
"rgb_cond": rgb_cond,
"mask_cond": mask_cond,
"c2w_cond": c2w_cond.unsqueeze(0),
"intrinsic_cond": intrinsic.to(self.device).unsqueeze(0),
"intrinsic_normed_cond": intrinsic_normed_cond.to(self.device).unsqueeze(0),
}
meshes, global_dict = self.generate_mesh(
batch, bake_resolution, remesh, estimate_illumination
)
return meshes[0], global_dict
def generate_mesh(
self,
batch,
bake_resolution: int,
remesh: Literal["none", "triangle", "quad"] = "none",
estimate_illumination: bool = False,
) -> Tuple[List[trimesh.Trimesh], dict[str, Any]]:
batch["rgb_cond"] = self.image_processor(
batch["rgb_cond"], self.cfg.cond_image_size
)
batch["mask_cond"] = self.image_processor(
batch["mask_cond"], self.cfg.cond_image_size
)
scene_codes, non_postprocessed_codes = self.get_scene_codes(batch)
global_dict = {}
if self.image_estimator is not None:
global_dict.update(
self.image_estimator(batch["rgb_cond"] * batch["mask_cond"])
)
if self.global_estimator is not None and estimate_illumination:
global_dict.update(self.global_estimator(non_postprocessed_codes))
with torch.no_grad():
with torch.autocast(device_type="cuda", enabled=False):
meshes = self.triplane_to_meshes(scene_codes)
rets = []
for i, mesh in enumerate(meshes):
# Check for empty mesh
if mesh.v_pos.shape[0] == 0:
rets.append(trimesh.Trimesh())
continue
if remesh == "triangle":
mesh = mesh.triangle_remesh()
elif remesh == "quad":
mesh = mesh.quad_remesh()
mesh.unwrap_uv()
# Build textures
rast = self.baker.rasterize(
mesh.v_tex, mesh.t_pos_idx, bake_resolution
)
bake_mask = self.baker.get_mask(rast)
pos_bake = self.baker.interpolate(
mesh.v_pos,
rast,
mesh.t_pos_idx,
mesh.v_tex,
)
gb_pos = pos_bake[bake_mask]
tri_query = self.query_triplane(gb_pos, scene_codes[i])[0]
decoded = self.decoder(
tri_query, exclude=["density", "vertex_offset"]
)
nrm = self.baker.interpolate(
mesh.v_nrm,
rast,
mesh.t_pos_idx,
mesh.v_tex,
)
gb_nrm = F.normalize(nrm[bake_mask], dim=-1)
decoded["normal"] = gb_nrm
# Check if any keys in global_dict start with decoded_
for k, v in global_dict.items():
if k.startswith("decoder_"):
decoded[k.replace("decoder_", "")] = v[i]
mat_out = {
"albedo": decoded["features"],
"roughness": decoded["roughness"],
"metallic": decoded["metallic"],
"normal": normalize(decoded["perturb_normal"]),
"bump": None,
}
for k, v in mat_out.items():
if v is None:
continue
if v.shape[0] == 1:
# Skip and directly add a single value
mat_out[k] = v[0]
else:
f = torch.zeros(
bake_resolution,
bake_resolution,
v.shape[-1],
dtype=v.dtype,
device=v.device,
)
if v.shape == f.shape:
continue
if k == "normal":
# Use un-normalized tangents here so that larger smaller tris
# Don't effect the tangents that much
tng = self.baker.interpolate(
mesh.v_tng,
rast,
mesh.t_pos_idx,
mesh.v_tex,
)
gb_tng = tng[bake_mask]
gb_tng = F.normalize(gb_tng, dim=-1)
gb_btng = F.normalize(
torch.cross(gb_tng, gb_nrm, dim=-1), dim=-1
)
normal = F.normalize(mat_out["normal"], dim=-1)
bump = torch.cat(
# Check if we have to flip some things
(
dot(normal, gb_tng),
dot(normal, gb_btng),
dot(normal, gb_nrm).clip(
0.3, 1
), # Never go below 0.3. This would indicate a flipped (or close to one) normal
),
-1,
)
bump = (bump * 0.5 + 0.5).clamp(0, 1)
f[bake_mask] = bump.view(-1, 3)
mat_out["bump"] = f
else:
f[bake_mask] = v.view(-1, v.shape[-1])
mat_out[k] = f
def uv_padding(arr):
if arr.ndim == 1:
return arr
return (
dilate_fill(
arr.permute(2, 0, 1)[None, ...],
bake_mask.unsqueeze(0).unsqueeze(0),
iterations=bake_resolution // 150,
)
.squeeze(0)
.permute(1, 2, 0)
)
verts_np = convert_data(mesh.v_pos)
faces = convert_data(mesh.t_pos_idx)
uvs = convert_data(mesh.v_tex)
basecolor_tex = Image.fromarray(
float32_to_uint8_np(convert_data(uv_padding(mat_out["albedo"])))
).convert("RGB")
basecolor_tex.format = "JPEG"
metallic = mat_out["metallic"].squeeze().cpu().item()
roughness = mat_out["roughness"].squeeze().cpu().item()
if "bump" in mat_out and mat_out["bump"] is not None:
bump_np = convert_data(uv_padding(mat_out["bump"]))
bump_up = np.ones_like(bump_np)
bump_up[..., :2] = 0.5
bump_up[..., 2:] = 1
bump_tex = Image.fromarray(
float32_to_uint8_np(
bump_np,
dither=True,
# Do not dither if something is perfectly flat
dither_mask=np.all(
bump_np == bump_up, axis=-1, keepdims=True
).astype(np.float32),
)
).convert("RGB")
bump_tex.format = (
"JPEG" # PNG would be better but the assets are larger
)
else:
bump_tex = None
material = trimesh.visual.material.PBRMaterial(
baseColorTexture=basecolor_tex,
roughnessFactor=roughness,
metallicFactor=metallic,
normalTexture=bump_tex,
)
tmesh = trimesh.Trimesh(
vertices=verts_np,
faces=faces,
visual=trimesh.visual.texture.TextureVisuals(
uv=uvs, material=material
),
)
rot = trimesh.transformations.rotation_matrix(
np.radians(-90), [1, 0, 0]
)
tmesh.apply_transform(rot)
tmesh.apply_transform(
trimesh.transformations.rotation_matrix(
np.radians(90), [0, 1, 0]
)
)
tmesh.invert()
rets.append(tmesh)
return rets, global_dict
@@ -0,0 +1,87 @@
import os
import slangtorch
import torch
import torch.nn as nn
from jaxtyping import Bool, Float
from torch import Tensor
class TextureBaker(nn.Module):
def __init__(self):
super().__init__()
self.baker = slangtorch.loadModule(
os.path.join(os.path.dirname(__file__), "texture_baker.slang")
)
def rasterize(
self,
uv: Float[Tensor, "Nv 2"],
face_indices: Float[Tensor, "Nf 3"],
bake_resolution: int,
) -> Float[Tensor, "bake_resolution bake_resolution 4"]:
if not face_indices.is_cuda or not uv.is_cuda:
raise ValueError("All input tensors must be on cuda")
face_indices = face_indices.to(torch.int32)
uv = uv.to(torch.float32)
rast_result = torch.empty(
bake_resolution, bake_resolution, 4, device=uv.device, dtype=torch.float32
)
block_size = 16
grid_size = bake_resolution // block_size
self.baker.bake_uv(uv=uv, indices=face_indices, output=rast_result).launchRaw(
blockSize=(block_size, block_size, 1), gridSize=(grid_size, grid_size, 1)
)
return rast_result
def get_mask(
self, rast: Float[Tensor, "bake_resolution bake_resolution 4"]
) -> Bool[Tensor, "bake_resolution bake_resolution"]:
return rast[..., -1] >= 0
def interpolate(
self,
attr: Float[Tensor, "Nv 3"],
rast: Float[Tensor, "bake_resolution bake_resolution 4"],
face_indices: Float[Tensor, "Nf 3"],
uv: Float[Tensor, "Nv 2"],
) -> Float[Tensor, "bake_resolution bake_resolution 3"]:
# Make sure all input tensors are on torch
if not attr.is_cuda or not face_indices.is_cuda or not rast.is_cuda:
raise ValueError("All input tensors must be on cuda")
attr = attr.to(torch.float32)
face_indices = face_indices.to(torch.int32)
uv = uv.to(torch.float32)
pos_bake = torch.zeros(
rast.shape[0],
rast.shape[1],
3,
device=attr.device,
dtype=attr.dtype,
)
block_size = 16
grid_size = rast.shape[0] // block_size
self.baker.interpolate(
attr=attr, indices=face_indices, rast=rast, output=pos_bake
).launchRaw(
blockSize=(block_size, block_size, 1), gridSize=(grid_size, grid_size, 1)
)
return pos_bake
def forward(
self,
attr: Float[Tensor, "Nv 3"],
uv: Float[Tensor, "Nv 2"],
face_indices: Float[Tensor, "Nf 3"],
bake_resolution: int,
) -> Float[Tensor, "bake_resolution bake_resolution 3"]:
rast = self.rasterize(uv, face_indices, bake_resolution)
return self.interpolate(attr, rast, face_indices, uv)
@@ -0,0 +1,93 @@
// xy: 2D test position
// v1: vertex position 1
// v2: vertex position 2
// v3: vertex position 3
//
bool barycentric_coordinates(float2 xy, float2 v1, float2 v2, float2 v3, out float u, out float v, out float w)
{
// Return true if the point (x,y) is inside the triangle defined by the vertices v1, v2, v3.
// If the point is inside the triangle, the barycentric coordinates are stored in u, v, and w.
float2 v1v2 = v2 - v1;
float2 v1v3 = v3 - v1;
float2 xyv1 = xy - v1;
float d00 = dot(v1v2, v1v2);
float d01 = dot(v1v2, v1v3);
float d11 = dot(v1v3, v1v3);
float d20 = dot(xyv1, v1v2);
float d21 = dot(xyv1, v1v3);
float denom = d00 * d11 - d01 * d01;
v = (d11 * d20 - d01 * d21) / denom;
w = (d00 * d21 - d01 * d20) / denom;
u = 1.0 - v - w;
return (v >= 0.0) && (w >= 0.0) && (v + w <= 1.0);
}
[AutoPyBindCUDA]
[CUDAKernel]
void interpolate(
TensorView<float3> attr,
TensorView<int3> indices,
TensorView<float4> rast,
TensorView<float3> output)
{
// Interpolate the attr into output based on the rast result (barycentric coordinates, + triangle idx)
uint3 dispatch_id = cudaBlockIdx() * cudaBlockDim() + cudaThreadIdx();
if (dispatch_id.x > output.size(0) || dispatch_id.y > output.size(1))
return;
float4 barycentric = rast[dispatch_id.x, dispatch_id.y];
int triangle_idx = int(barycentric.w);
if (triangle_idx < 0) {
output[dispatch_id.x, dispatch_id.y] = float3(0.0, 0.0, 0.0);
return;
}
float3 v1 = attr[indices[triangle_idx].x];
float3 v2 = attr[indices[triangle_idx].y];
float3 v3 = attr[indices[triangle_idx].z];
output[dispatch_id.x, dispatch_id.y] = v1 * barycentric.x + v2 * barycentric.y + v3 * barycentric.z;
}
[AutoPyBindCUDA]
[CUDAKernel]
void bake_uv(
TensorView<float2> uv,
TensorView<int3> indices,
TensorView<float4> output)
{
uint3 dispatch_id = cudaBlockIdx() * cudaBlockDim() + cudaThreadIdx();
if (dispatch_id.y > output.size(0) || dispatch_id.x > output.size(1))
return;
// We index x,y but the orginal coords are HW. So swap them
float2 pixel_coord = float2(dispatch_id.y, dispatch_id.x);
// Normalize to [0, 1]
pixel_coord /= float2(output.size(1), output.size(0));
pixel_coord = clamp(pixel_coord, 0.0, 1.0);
// Flip x-axis
pixel_coord.y = 1 - pixel_coord.y;
for (int i = 0; i < indices.size(0); i++) {
float2 v1 = float2(uv[indices[i].x].x, uv[indices[i].x].y);
float2 v2 = float2(uv[indices[i].y].x, uv[indices[i].y].y);
float2 v3 = float2(uv[indices[i].z].x, uv[indices[i].z].y);
float u, v, w;
bool hit = barycentric_coordinates(pixel_coord, v1, v2, v3, u, v, w);
if (hit){
output[dispatch_id.x, dispatch_id.y] = float4(u, v, w, i);
return;
}
}
output[dispatch_id.x, dispatch_id.y] = float4(0.0, 0.0, 0.0, -1);
}
+91
View File
@@ -0,0 +1,91 @@
from typing import Any
import numpy as np
import rembg
import torch
from PIL import Image
import StableFast3D.sf3d.models.utils as sf3d_utils
def create_intrinsic_from_fov_deg(fov_deg: float, cond_height: int, cond_width: int):
intrinsic = sf3d_utils.get_intrinsic_from_fov(
np.deg2rad(fov_deg),
H=cond_height,
W=cond_width,
)
intrinsic_normed_cond = intrinsic.clone()
intrinsic_normed_cond[..., 0, 2] /= cond_width
intrinsic_normed_cond[..., 1, 2] /= cond_height
intrinsic_normed_cond[..., 0, 0] /= cond_width
intrinsic_normed_cond[..., 1, 1] /= cond_height
return intrinsic, intrinsic_normed_cond
def default_cond_c2w(distance: float):
c2w_cond = torch.as_tensor(
[
[0, 0, 1, distance],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
]
).float()
return c2w_cond
def remove_background(
image: Image,
rembg_session: Any = None,
force: bool = False,
**rembg_kwargs,
) -> Image:
do_remove = True
if image.mode == "RGBA" and image.getextrema()[3][0] < 255:
do_remove = False
do_remove = do_remove or force
if do_remove:
image = rembg.remove(image, session=rembg_session, **rembg_kwargs)
return image
def resize_foreground(
image: Image,
ratio: float,
) -> Image:
image = np.array(image)
assert image.shape[-1] == 4
alpha = np.where(image[..., 3] > 0)
y1, y2, x1, x2 = (
alpha[0].min(),
alpha[0].max(),
alpha[1].min(),
alpha[1].max(),
)
# crop the foreground
fg = image[y1:y2, x1:x2]
# pad to square
size = max(fg.shape[0], fg.shape[1])
ph0, pw0 = (size - fg.shape[0]) // 2, (size - fg.shape[1]) // 2
ph1, pw1 = size - fg.shape[0] - ph0, size - fg.shape[1] - pw0
new_image = np.pad(
fg,
((ph0, ph1), (pw0, pw1), (0, 0)),
mode="constant",
constant_values=((0, 0), (0, 0), (0, 0)),
)
# compute padding according to the ratio
new_size = int(new_image.shape[0] / ratio)
# pad to size, double side
ph0, pw0 = (new_size - size) // 2, (new_size - size) // 2
ph1, pw1 = new_size - size - ph0, new_size - size - pw0
new_image = np.pad(
new_image,
((ph0, ph1), (pw0, pw1), (0, 0)),
mode="constant",
constant_values=((0, 0), (0, 0), (0, 0)),
)
new_image = Image.fromarray(new_image, mode="RGBA")
return new_image
+8 -2
View File
@@ -27,10 +27,15 @@ This is an extensive node suite that enables ComfyUI to process 3D inputs (Mesh
- **Note:** at this moment, you'll still need to install [Visual Studio Build Tools for windows](_Pre_Builds/README.md#build-for-windows) and [install `gcc g++` for Linux](_Pre_Builds/README.md#build-for-linux) in order for `InstantNGP & Convert 3DGS to Mesh with NeRF and Marching_Cubes` nodes to work, since those two nodes used JIT torch cpp extension that builds in runtime, but I plan to replace those nodes soon
## Features:
- For use cases please check out [Example Workflows](./_Example_Workflows/). [**Last update: 09/July/2024**]
- For use cases please check out [Example Workflows](./_Example_Workflows/). [**Last update: 01/August/2024**]
- **Note:** you need to put [Example Inputs Files & Folders](_Example_Workflows/_Example_Inputs_Files/) under ComfyUI Root Directory\ComfyUI\input folder before you can run the example workflow
- [tripoSR-layered-diffusion workflow](https://github.com/C0nsumption/Consume-ComfyUI-Workflows/tree/main/assets/tripo_sr/00) by [@Consumption](https://twitter.com/c0nsumption_)
- **StableFast3D**: [Stability-AI/stable-fast-3d](https://github.com/Stability-AI/stable-fast-3d)
- Single image to 3D Mesh with RGB texture
- *Note: you need to agree to Stability-AI's term of usage before been able to donwload the model weights, if you downloaded model weights manaully, then you need to put it under [Checkpoints/StableFast3D](Checkpoints/StableFast3D), otherwise you can add your huggingface token in [Configs/system.conf](Configs/system.conf)*
- Model weights: https://huggingface.co/stabilityai/stable-fast-3d/tree/main
- **CharacterGen**: [zjp-shadow/CharacterGen](https://github.com/zjp-shadow/CharacterGen)
- Single front view of a character with arbitrary pose
- Can [combine with Unique3D workflow](./_Example_Workflows/CharacterGen/CharacterGen_to_Unique3D.json) for better result
@@ -169,7 +174,8 @@ This is an extensive node suite that enables ComfyUI to process 3D inputs (Mesh
- Since different algorithms likely use different coordinate system, so the ability to re-mapping the axis of coordinate is crucial for passing generated result between differnt nodes.
- **[Customizable system config file](Configs/system.conf)**
- Custom clients IP address
- Custom clients IP address
- Add your huggingface user token
## Roadmap:
- [X] Integrate [CharacterGen](https://github.com/zjp-shadow/CharacterGen)
@@ -0,0 +1,419 @@
{
"last_node_id": 43,
"last_link_id": 69,
"nodes": [
{
"id": 40,
"type": "[Comfy3D] Resize Image Foreground",
"pos": [
305,
244
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 61
},
{
"name": "masks",
"type": "MASK",
"link": 65
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
63
],
"shape": 3,
"slot_index": 0
},
{
"name": "masks",
"type": "MASK",
"links": [
64
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Resize Image Foreground"
},
"widgets_values": [
0.85
]
},
{
"id": 36,
"type": "[Comfy3D] Load SF3D Model",
"pos": [
304,
122
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "sf3d_model",
"type": "SF3D_MODEL",
"links": [
56
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Load SF3D Model"
},
"widgets_values": [
"model.safetensors"
]
},
{
"id": 20,
"type": "InvertMask",
"pos": [
136,
330
],
"size": {
"0": 140,
"1": 26
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 26
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
65
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "InvertMask"
}
},
{
"id": 38,
"type": "[Comfy3D] Preview 3DMesh",
"pos": [
1706,
222
],
"size": [
600,
500
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "mesh_file_path",
"type": "STRING",
"link": 60,
"widget": {
"name": "mesh_file_path"
}
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Preview 3DMesh"
},
"widgets_values": [
"",
null
]
},
{
"id": 37,
"type": "[Comfy3D] StableFast3D",
"pos": [
650,
221
],
"size": {
"0": 315,
"1": 122
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "sf3d_model",
"type": "SF3D_MODEL",
"link": 56
},
{
"name": "reference_image",
"type": "IMAGE",
"link": 63
},
{
"name": "reference_mask",
"type": "MASK",
"link": 64
}
],
"outputs": [
{
"name": "mesh",
"type": "MESH",
"links": [
68
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] StableFast3D"
},
"widgets_values": [
1024,
"None"
]
},
{
"id": 43,
"type": "[Comfy3D] Switch Mesh Axis",
"pos": [
995,
221
],
"size": {
"0": 315,
"1": 154
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": 68
}
],
"outputs": [
{
"name": "switched_mesh",
"type": "MESH",
"links": [
69
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Switch Mesh Axis"
},
"widgets_values": [
"-x",
"+y",
"-z",
false,
1
]
},
{
"id": 39,
"type": "[Comfy3D] Save 3D Mesh",
"pos": [
1345,
222
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": 69
}
],
"outputs": [
{
"name": "save_path",
"type": "STRING",
"links": [
60
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "[Comfy3D] Save 3D Mesh"
},
"widgets_values": [
"SF3D_Test\\mesh.glb"
]
},
{
"id": 1,
"type": "LoadImage",
"pos": [
-379,
245
],
"size": {
"0": 473.3881530761719,
"1": 545.7767944335938
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
61
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [
26
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"PirateCat_0.png",
"image"
]
}
],
"links": [
[
26,
1,
1,
20,
0,
"MASK"
],
[
56,
36,
0,
37,
0,
"SF3D_MODEL"
],
[
60,
39,
0,
38,
0,
"STRING"
],
[
61,
1,
0,
40,
0,
"IMAGE"
],
[
63,
40,
0,
37,
1,
"IMAGE"
],
[
64,
40,
1,
37,
2,
"MASK"
],
[
65,
20,
0,
40,
1,
"MASK"
],
[
68,
37,
0,
43,
0,
"MESH"
],
[
69,
43,
0,
39,
0,
"MESH"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.6830134553650706,
"offset": [
164.94221702032814,
121.40961470392963
]
}
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+6
View File
@@ -40,6 +40,12 @@ sys_conf = ConfigFactory.parse_string(conf_text)
set_web_conf(sys_conf['web'])
# Log into huggingface if given user specificed token
hf_token = sys_conf['huggingface.token']
if isinstance(hf_token, str) and len(hf_token) > 0:
from huggingface_hub import login
login(token=hf_token)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
+177 -21
View File
@@ -61,6 +61,8 @@ from LGM.mvdream.pipeline_mvdream import MVDreamPipeline
from LGM.large_multiview_gaussian_model import LargeMultiviewGaussianModel
from LGM.nerf_marching_cubes_converter import GSConverterNeRFMarchingCubes
from TripoSR.system import TSR
from StableFast3D.sf3d import utils as sf3d_utils
from StableFast3D.sf3d.system import SF3D
from InstantMesh.utils.camera_util import oribt_camera_poses_to_input_cameras
from CRM.model.crm.model import ConvolutionalReconstructionModel
from CRM.model.crm.sampler import CRMSampler
@@ -86,7 +88,7 @@ from craftsman.utils.config import ExperimentConfig as ExperimentConfigCraftsman
from .shared_utils.image_utils import (
prepare_torch_img, torch_imgs_to_pils, troch_image_dilate,
pils_rgba_to_rgb, pil_make_image_grid, pil_split_image, pils_to_torch_imgs,
pils_rgba_to_rgb, pil_make_image_grid, pil_split_image, pils_to_torch_imgs, pils_resize_foreground
)
from .shared_utils.log_utils import cstr
from .shared_utils.common_utils import parse_save_filename, get_list_filenames, resume_or_download_model_from_hf
@@ -391,6 +393,37 @@ class Image_Add_Pure_Color_Background:
images = pils_to_torch_imgs(image_pils, images.device)
return (images,)
class Resize_Image_Foreground:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"masks": ("MASK",),
"foreground_ratio": ("FLOAT", {"default": 0.85, "min": 0.01, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = (
"IMAGE",
"MASK",
)
RETURN_NAMES = (
"images",
"masks",
)
FUNCTION = "resize_img_foreground"
CATEGORY = "Comfy3D/Preprocessor"
def resize_img_foreground(self, images, masks, foreground_ratio):
image_pils = torch_imgs_to_pils(images, masks)
image_pils = pils_resize_foreground(image_pils, foreground_ratio)
images = pils_to_torch_imgs(image_pils, images.device, force_rgb=False)
images, masks = images[:, :, :, 0:-1], images[:, :, :, -1]
return (images, masks,)
class Make_Image_Grid:
@classmethod
def INPUT_TYPES(cls):
@@ -531,7 +564,7 @@ class Fast_Clean_Mesh:
"apply_smooth": ("BOOLEAN", {"default": True},),
"smooth_step": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff}),
"apply_sub_divide": ("BOOLEAN", {"default": True},),
"sub_divide_threshold": ("FLOAT", {"default": 0.25, 'step': 0.001}),
"sub_divide_threshold": ("FLOAT", {"default": 0.25, "step": 0.001}),
},
}
@@ -675,24 +708,24 @@ class Stack_Orbit_Camera_Poses:
def INPUT_TYPES(cls):
return {
"required": {
"orbit_radius_start": ("FLOAT", {"default": 1.75, 'step': 0.0001}),
"orbit_radius_stop": ("FLOAT", {"default": 1.75, 'step': 0.0001}),
"orbit_radius_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}),
"elevation_start": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}),
"elevation_stop": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}),
"elevation_step": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}),
"azimuth_start": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}),
"azimuth_stop": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}),
"azimuth_step": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}),
"orbit_center_X_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_X_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_X_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}),
"orbit_center_Y_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_Y_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_Y_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}),
"orbit_center_Z_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_Z_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}),
"orbit_center_Z_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}),
"orbit_radius_start": ("FLOAT", {"default": 1.75, "step": 0.0001}),
"orbit_radius_stop": ("FLOAT", {"default": 1.75, "step": 0.0001}),
"orbit_radius_step": ("FLOAT", {"default": 0.1, "step": 0.0001}),
"elevation_start": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, "step": 0.0001}),
"elevation_stop": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, "step": 0.0001}),
"elevation_step": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, "step": 0.0001}),
"azimuth_start": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, "step": 0.0001}),
"azimuth_stop": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, "step": 0.0001}),
"azimuth_step": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, "step": 0.0001}),
"orbit_center_X_start": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_X_stop": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_X_step": ("FLOAT", {"default": 0.1, "step": 0.0001}),
"orbit_center_Y_start": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_Y_stop": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_Y_step": ("FLOAT", {"default": 0.1, "step": 0.0001}),
"orbit_center_Z_start": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_Z_stop": ("FLOAT", {"default": 0.0, "step": 0.0001}),
"orbit_center_Z_step": ("FLOAT", {"default": 0.1, "step": 0.0001}),
},
}
@@ -1600,7 +1633,7 @@ class MVDream_Model:
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"mv_guidance_scale": ("FLOAT", {"default": 5.0, "min": 0.0, "step": 0.01}),
"num_inference_steps": ("INT", {"default": 30, "min": 1}),
"elevation": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}),
"elevation": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, "step": 0.0001}),
},
}
@@ -1900,6 +1933,127 @@ class TripoSR:
image = Image.fromarray((image * 255.0).astype(np.uint8))
return image
class Load_SF3D_Model:
checkpoints_dir = "StableFast3D"
default_ckpt_name = "model.safetensors"
default_repo_id = "stabilityai/stable-fast-3d"
config_path = "StableFast3D_config.yaml"
@classmethod
def INPUT_TYPES(cls):
cls.checkpoints_dir_abs = os.path.join(CKPT_ROOT_PATH, cls.checkpoints_dir)
all_models_names = get_list_filenames(cls.checkpoints_dir_abs, SUPPORTED_CHECKPOINTS_EXTENSIONS)
if cls.default_ckpt_name not in all_models_names:
all_models_names += [cls.default_ckpt_name]
cls.config_path_abs = os.path.join(CONFIG_ROOT_PATH, cls.config_path)
return {
"required": {
"model_name": (all_models_names, ),
},
}
RETURN_TYPES = (
"SF3D_MODEL",
)
RETURN_NAMES = (
"sf3d_model",
)
FUNCTION = "load_SF3D"
CATEGORY = "Comfy3D/Import|Export"
def load_SF3D(self, model_name):
ckpt_path = resume_or_download_model_from_hf(self.checkpoints_dir_abs, self.default_repo_id, model_name, self.__class__.__name__)
sf3d_model = SF3D.from_pretrained(
config_path=self.config_path_abs,
weight_path=ckpt_path
)
sf3d_model.eval()
sf3d_model.to(DEVICE)
cstr(f"[{self.__class__.__name__}] loaded model ckpt from {ckpt_path}").msg.print()
return (sf3d_model, )
class StableFast3D:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"sf3d_model": ("SF3D_MODEL", ),
"reference_image": ("IMAGE",),
"reference_mask": ("MASK",),
"texture_resolution": ("INT", {"default": 1024, "min": 128, "max": 8192}),
"remesh_option": (["None", "Triangle"], ),
}
}
RETURN_TYPES = (
"MESH",
)
RETURN_NAMES = (
"mesh",
)
FUNCTION = "run_SF3D"
CATEGORY = "Comfy3D/Algorithm"
@torch.no_grad()
def run_SF3D(self, sf3d_model, reference_image, reference_mask, texture_resolution, remesh_option):
single_image = torch_imgs_to_pils(reference_image, reference_mask)[0]
with torch.autocast(device_type=DEVICE_STR, dtype=WEIGHT_DTYPE):
model_batch = self.create_batch(single_image)
model_batch = {k: v.cuda() for k, v in model_batch.items()}
trimesh_mesh, _ = sf3d_model.generate_mesh(
model_batch, texture_resolution, remesh_option
)
mesh = Mesh.load_trimesh(given_mesh=trimesh_mesh[0])
return (mesh,)
# Default model are trained on images with this background
def create_batch(self, input_image: Image):
COND_WIDTH = 512
COND_HEIGHT = 512
COND_DISTANCE = 1.6
COND_FOVY_DEG = 40
BACKGROUND_COLOR = [0.5, 0.5, 0.5]
# Cached. Doesn't change
c2w_cond = sf3d_utils.default_cond_c2w(COND_DISTANCE)
intrinsic, intrinsic_normed_cond = sf3d_utils.create_intrinsic_from_fov_deg(
COND_FOVY_DEG, COND_HEIGHT, COND_WIDTH
)
img_cond = (
torch.from_numpy(
np.asarray(input_image.resize((COND_WIDTH, COND_HEIGHT))).astype(np.float32)
/ 255.0
)
.float()
.clip(0, 1)
)
mask_cond = img_cond[:, :, -1:]
rgb_cond = torch.lerp(
torch.tensor(BACKGROUND_COLOR)[None, None, :], img_cond[:, :, :3], mask_cond
)
batch_elem = {
"rgb_cond": rgb_cond,
"mask_cond": mask_cond,
"c2w_cond": c2w_cond.unsqueeze(0),
"intrinsic_cond": intrinsic.unsqueeze(0),
"intrinsic_normed_cond": intrinsic_normed_cond.unsqueeze(0),
}
# Add batch dim
batched = {k: v.unsqueeze(0) for k, v in batch_elem.items()}
return batched
class Load_CRM_MVDiffusion_Model:
checkpoints_dir = "CRM"
default_ckpt_name = ["pixel-diffusion.pth", "ccm-diffusion.pth"]
@@ -3115,6 +3269,8 @@ class Craftsman_Shape_Diffusion_Model:
mesh.auto_uv()
return (mesh,)
ORBITPOSE_PRESET = ["Custom", "CRM(6)", "Zero123Plus(6)", "Wonder3D(6)", "Era3D(6)", "MVDream(4)", "Unique3D(4)", "CharacterGen(4)"]
+4 -2
View File
@@ -14,7 +14,7 @@ kornia
opencv-python
pillow
nerfacc>=0.5.3
PyMCubes
slangtorch
# for use ML models
diffusers>=0.26.1
@@ -33,12 +33,14 @@ imageio
imageio-ffmpeg
matplotlib
# for dmtet and mesh import & export
# for process, import & export mesh
trimesh
plyfile
pygltflib
xatlas
pymeshlab
gpytoolbox
PyMCubes
# configs & extra
torchtyping
+51 -4
View File
@@ -55,18 +55,18 @@ def troch_image_dilate(img):
img = (img.clip(0, 255) / 255).astype(np.float32)
return torch.from_numpy(img)
def pils_to_torch_imgs(pils: Union[Image.Image, List[Image.Image]], device="cuda"):
def pils_to_torch_imgs(pils: Union[Image.Image, List[Image.Image]], device="cuda", force_rgb=True):
if isinstance(pils, Image.Image):
pils = [pils]
images = []
for pil in pils:
if pil.mode == "RGBA":
if pil.mode == "RGBA" and force_rgb:
pil = pil.convert('RGB')
images.append(TF.to_tensor(pil).permute(1, 2, 0))
images = torch.stack(images, dim=0)
images = torch.stack(images, dim=0).to(device)
return images
@@ -148,4 +148,51 @@ def pils_erode_masks(mask_list):
alpha = (alpha * 255).astype(np.uint8)
out_mask_list.append(Image.fromarray(alpha[:, :, None]))
return out_mask_list
return out_mask_list
def pils_resize_foreground(
pils: Union[Image.Image, List[Image.Image]],
ratio: float,
) -> List[Image.Image]:
if isinstance(pils, Image.Image):
pils = [pils]
new_pils = []
for image in pils:
image = np.array(image)
assert image.shape[-1] == 4
alpha = np.where(image[..., 3] > 0)
y1, y2, x1, x2 = (
alpha[0].min(),
alpha[0].max(),
alpha[1].min(),
alpha[1].max(),
)
# crop the foreground
fg = image[y1:y2, x1:x2]
# pad to square
size = max(fg.shape[0], fg.shape[1])
ph0, pw0 = (size - fg.shape[0]) // 2, (size - fg.shape[1]) // 2
ph1, pw1 = size - fg.shape[0] - ph0, size - fg.shape[1] - pw0
new_image = np.pad(
fg,
((ph0, ph1), (pw0, pw1), (0, 0)),
mode="constant",
constant_values=((0, 0), (0, 0), (0, 0)),
)
# compute padding according to the ratio
new_size = int(new_image.shape[0] / ratio)
# pad to size, double side
ph0, pw0 = (new_size - size) // 2, (new_size - size) // 2
ph1, pw1 = new_size - size - ph0, new_size - size - pw0
new_image = np.pad(
new_image,
((ph0, ph1), (pw0, pw1), (0, 0)),
mode="constant",
constant_values=((0, 0), (0, 0), (0, 0)),
)
new_image = Image.fromarray(new_image, mode="RGBA")
new_pils.append(new_image)
return new_pils