Add Deep Marching Tetrahedra (DMTet)

This commit is contained in:
MrForExample
2024-01-12 21:43:44 +01:00
parent 01b0a25138
commit 49fed463a2
24 changed files with 1773 additions and 119 deletions
+8
View File
@@ -0,0 +1,8 @@
import os
import numpy as np
import torch
grid_res = 128
p = os.path.join(os.path.dirname(__file__), f'../data/tets/{grid_res}_tets.npz')
print(p)
+148
View File
@@ -0,0 +1,148 @@
import os
import math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import nvdiffrast.torch as dr
from ..mesh_processer.mesh import safe_normalize
def scale_img_nhwc(x, size, mag='bilinear', min='bilinear'):
assert (x.shape[1] >= size[0] and x.shape[2] >= size[1]) or (x.shape[1] < size[0] and x.shape[2] < size[1]), "Trying to magnify image in one dimension and minify in the other"
y = x.permute(0, 3, 1, 2) # NHWC -> NCHW
if x.shape[1] > size[0] and x.shape[2] > size[1]: # Minification, previous size was bigger
y = torch.nn.functional.interpolate(y, size, mode=min)
else: # Magnification
if mag == 'bilinear' or mag == 'bicubic':
y = torch.nn.functional.interpolate(y, size, mode=mag, align_corners=True)
else:
y = torch.nn.functional.interpolate(y, size, mode=mag)
return y.permute(0, 2, 3, 1).contiguous() # NCHW -> NHWC
def scale_img_hwc(x, size, mag='bilinear', min='bilinear'):
return scale_img_nhwc(x[None, ...], size, mag, min)[0]
def scale_img_nhw(x, size, mag='bilinear', min='bilinear'):
return scale_img_nhwc(x[..., None], size, mag, min)[..., 0]
def scale_img_hw(x, size, mag='bilinear', min='bilinear'):
return scale_img_nhwc(x[None, ..., None], size, mag, min)[0, ..., 0]
def trunc_rev_sigmoid(x, eps=1e-6):
x = x.clamp(eps, 1 - eps)
return torch.log(x / (1 - x))
def make_divisible(x, m=8):
return int(math.ceil(x / m) * m)
class Renderer(nn.Module):
def __init__(self, mesh, force_cuda_rast):
super().__init__()
self.mesh = mesh
if force_cuda_rast or os.name != 'nt':
self.glctx = dr.RasterizeCudaContext()
else:
self.glctx = dr.RasterizeGLContext()
# extract trainable parameters
self.v_offsets = nn.Parameter(torch.zeros_like(self.mesh.v), requires_grad=True)
self.raw_albedo = nn.Parameter(trunc_rev_sigmoid(self.mesh.albedo), requires_grad=True)
def get_params(self, texture_lr, train_geo, geom_lr):
params = [
{'params': self.raw_albedo, 'lr': texture_lr},
]
self.train_geo = train_geo
if train_geo:
params.append({'params': self.v_offsets, 'lr': geom_lr})
return params
def update_mesh(self):
self.mesh.v = (self.mesh.v + self.v_offsets).detach()
self.mesh.albedo = torch.sigmoid(self.raw_albedo.detach())
def render(self, pose, proj, h0, w0, ssaa=1, bg_color=1, texture_filter='linear-mipmap-linear'):
# do super-sampling
if ssaa != 1:
h = make_divisible(h0 * ssaa, 8)
w = make_divisible(w0 * ssaa, 8)
else:
h, w = h0, w0
results = {}
# get v
if self.train_geo:
v = self.mesh.v + self.v_offsets # [N, 3]
else:
v = self.mesh.v
pose = torch.from_numpy(pose.astype(np.float32)).to(v.device)
proj = torch.from_numpy(proj.astype(np.float32)).to(v.device)
# get v_clip and render rgb
v_cam = torch.matmul(F.pad(v, pad=(0, 1), mode='constant', value=1.0), torch.inverse(pose).T).float().unsqueeze(0)
v_clip = v_cam @ proj.T
rast, rast_db = dr.rasterize(self.glctx, v_clip, self.mesh.f, (h, w))
alpha = (rast[0, ..., 3:] > 0).float()
depth, _ = dr.interpolate(-v_cam[..., [2]], rast, self.mesh.f) # [1, H, W, 1]
depth = depth.squeeze(0) # [H, W, 1]
texc, texc_db = dr.interpolate(self.mesh.vt.unsqueeze(0).contiguous(), rast, self.mesh.ft, rast_db=rast_db, diff_attrs='all')
albedo = dr.texture(self.raw_albedo.unsqueeze(0), texc, uv_da=texc_db, filter_mode=texture_filter) # [1, H, W, 3]
albedo = torch.sigmoid(albedo)
# get vn and render normal
if self.train_geo:
i0, i1, i2 = self.mesh.f[:, 0].long(), self.mesh.f[:, 1].long(), self.mesh.f[:, 2].long()
v0, v1, v2 = v[i0, :], v[i1, :], v[i2, :]
face_normals = torch.cross(v1 - v0, v2 - v0)
face_normals = safe_normalize(face_normals)
vn = torch.zeros_like(v)
vn.scatter_add_(0, i0[:, None].repeat(1,3), face_normals)
vn.scatter_add_(0, i1[:, None].repeat(1,3), face_normals)
vn.scatter_add_(0, i2[:, None].repeat(1,3), face_normals)
vn = torch.where(torch.sum(vn * vn, -1, keepdim=True) > 1e-20, vn, torch.tensor([0.0, 0.0, 1.0], dtype=torch.float32, device=vn.device))
else:
vn = self.mesh.vn
normal, _ = dr.interpolate(vn.unsqueeze(0).contiguous(), rast, self.mesh.fn)
normal = safe_normalize(normal[0])
# rotated normal (where [0, 0, 1] always faces camera)
rot_normal = normal @ pose[:3, :3]
viewcos = rot_normal[..., [2]]
# antialias
albedo = dr.antialias(albedo, rast, v_clip, self.mesh.f).squeeze(0) # [H, W, 3]
albedo = alpha * albedo + (1 - alpha) * bg_color
# ssaa
if ssaa != 1:
albedo = scale_img_hwc(albedo, (h0, w0))
alpha = scale_img_hwc(alpha, (h0, w0))
depth = scale_img_hwc(depth, (h0, w0))
normal = scale_img_hwc(normal, (h0, w0))
viewcos = scale_img_hwc(viewcos, (h0, w0))
results['image'] = albedo.clamp(0, 1)
results['alpha'] = alpha
results['depth'] = depth
results['normal'] = (normal + 1) / 2
results['viewcos'] = viewcos
return results
+120
View File
@@ -0,0 +1,120 @@
import random
import tqdm
import torch
import torch.nn.functional as F
import numpy as np
from pytorch_msssim import SSIM, MS_SSIM
from .diff_mesh_renderer import Renderer
from ..shared_utils.camera_utils import orbit_camera, OrbitCamera
class DiffTextureBaker:
def __init__(self, mesh, training_iterations, batch_size, texture_learning_rate, train_mesh_geometry, geometry_learning_rate, ms_ssim_loss_weight, force_cuda_rasterize):
self.device = torch.device("cuda")
# prepare main components for optimization
self.renderer = Renderer(mesh, force_cuda_rasterize).to(self.device)
self.optimizer = torch.optim.Adam(self.renderer.get_params(texture_learning_rate, train_mesh_geometry, geometry_learning_rate))
#self.ssim_loss = SSIM(data_range=1, size_average=True, channel=3)
self.ms_ssim_loss = MS_SSIM(data_range=1, size_average=True, channel=3)
self.lambda_ssim = ms_ssim_loss_weight
self.training_iterations = training_iterations
self.batch_size = batch_size
def prepare_img(self, img):
img_new = img.permute(2, 0, 1).unsqueeze(0).to(self.device)
img_new = F.interpolate(img_new, (self.ref_size_H, self.ref_size_W), mode="bilinear", align_corners=False).contiguous()
return img_new
def prepare_training(self, reference_images, reference_masks, reference_orbit_camera_poses, reference_orbit_camera_fovy):
self.ref_imgs_num = len(reference_images)
self.all_ref_cam_poses = reference_orbit_camera_poses
self.ref_cam_fovy = reference_orbit_camera_fovy
self.ref_size_H = reference_images[0].shape[0]
self.ref_size_W = reference_images[0].shape[1]
self.cam = OrbitCamera(self.ref_size_W, self.ref_size_H, fovy=reference_orbit_camera_fovy)
# prepare reference images and masks
ref_imgs_torch_list = []
ref_masks_torch_list = []
for i in range(self.ref_imgs_num):
ref_imgs_torch_list.append(self.prepare_img(reference_images[i]))
ref_masks_torch_list.append(self.prepare_img(reference_masks[i].unsqueeze(2)))
self.ref_imgs_torch = torch.cat(ref_imgs_torch_list, dim=0)
self.ref_masks_torch = torch.cat(ref_masks_torch_list, dim=0)
def training(self):
starter = torch.cuda.Event(enable_timing=True)
ender = torch.cuda.Event(enable_timing=True)
starter.record()
ref_imgs_masked = []
for i in range(self.ref_imgs_num):
ref_imgs_masked.append((self.ref_imgs_torch[i] * self.ref_masks_torch[i]).unsqueeze(0))
ref_imgs_num_minus_1 = self.ref_imgs_num-1
for step in tqdm.trange(self.training_iterations):
### calculate loss between reference and rendered image from known view
loss = 0
masked_rendered_img_batch = []
masked_ref_img_batch = []
for _ in range(self.batch_size):
i = random.randint(0, ref_imgs_num_minus_1)
radius, elevation, azimuth, center_X, center_Y, center_Z = self.all_ref_cam_poses[i]
# render output
orbit_target = np.array([center_X, center_Y, center_Z], dtype=np.float32)
ref_pose = orbit_camera(elevation, azimuth, radius, target=orbit_target)
ref_cam = (ref_pose, self.cam.perspective)
out = self.renderer.render(*ref_cam, self.ref_size_H, self.ref_size_W, ssaa=1) #ssaa = min(2.0, max(0.125, 2 * np.random.random()))
image = out["image"] # [H, W, 3] in [0, 1]
image = image.permute(2, 0, 1).contiguous() # [3, H, W] in [0, 1]
image_masked = (image * self.ref_masks_torch[i]).unsqueeze(0)
masked_rendered_img_batch.append(image_masked)
masked_ref_img_batch.append(ref_imgs_masked[i])
masked_rendered_img_batch_torch = torch.cat(masked_rendered_img_batch, dim=0)
masked_ref_img_batch_torch = torch.cat(masked_ref_img_batch, dim=0)
# rgb loss
loss += (1 - self.lambda_ssim) * F.mse_loss(masked_rendered_img_batch_torch, masked_ref_img_batch_torch)
# D-SSIM loss
# [1, 3, H, W] in [0, 1]
#loss += self.lambda_ssim * (1 - self.ssim_loss(X, Y))
loss += self.lambda_ssim * (1 - self.ms_ssim_loss(masked_ref_img_batch_torch, masked_rendered_img_batch_torch))
# optimize step
loss.backward()
self.optimizer.step()
self.optimizer.zero_grad()
torch.cuda.synchronize()
self.need_update = True
print(f"Step: {step}")
self.renderer.update_mesh()
ender.record()
#t = starter.elapsed_time(ender)
def get_mesh_and_texture(self):
return (self.renderer.mesh, self.renderer.mesh.albedo, )
+271
View File
@@ -0,0 +1,271 @@
import os
import tqdm
import numpy as np
import torch
from pytorch3d.loss import chamfer_distance, mesh_laplacian_smoothing
from pytorch3d.structures.meshes import Meshes
from ..mesh_processer.mesh import Mesh
from ..mesh_processer.mesh_utils import sample_points
from .dmtet_network import Decoder
# Deep Marching Tetrahedrons implementation, adapted from
# https://github.com/NVIDIAGameWorks/kaolin/blob/master/kaolin/ops/conversions/tetmesh.py
class DMTet:
def __init__(self, device='cuda'):
self.device = device
self.triangle_table = torch.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, device=self.device)
self.num_triangles_table = torch.tensor([0,1,1,2,1,2,2,1,1,2,2,1,2,1,1,0], dtype=torch.long, device=self.device)
self.base_tet_edges = torch.tensor([0,1,0,2,0,3,1,2,1,3,2,3], dtype=torch.long, device=self.device)
# Utility functions
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 map_uv(self, faces, face_gidx, max_idx):
N = int(np.ceil(np.sqrt((max_idx+1)//2)))
tex_y, tex_x = torch.meshgrid(
torch.linspace(0, 1 - (1 / N), N, dtype=torch.float32, device="cuda"),
torch.linspace(0, 1 - (1 / N), N, dtype=torch.float32, device="cuda"),
indexing='ij'
)
pad = 0.9 / N
uvs = torch.stack([
tex_x , tex_y,
tex_x + pad, tex_y,
tex_x + pad, tex_y + pad,
tex_x , tex_y + pad
], dim=-1).view(-1, 2)
def _idx(tet_idx, N):
x = tet_idx % N
y = torch.div(tet_idx, N, rounding_mode='trunc')
return y * N + x
tet_idx = _idx(torch.div(face_gidx, 2, rounding_mode='trunc'), N)
tri_idx = face_gidx % 2
uv_idx = torch.stack((
tet_idx * 4, tet_idx * 4 + tri_idx + 1, tet_idx * 4 + tri_idx + 2
), dim = -1). view(-1, 3)
return uvs, uv_idx
# Marching tets implementation
def __call__(self, vertices, tet, sdf, with_uv=False):
r"""Convert discrete signed distance fields encoded on tetrahedral grids to triangle
meshes using marching tetrahedra algorithm as described in `An efficient method of
triangulating equi-valued surfaces by using tetrahedral cells`_. The output surface is differentiable with respect to
input vertex positions and the SDF values. For more details and example usage in learning, see
`Deep Marching Tetrahedra\: a Hybrid Representation for High-Resolution 3D Shape Synthesis`_ NeurIPS 2021.
Args:
vertices (torch.tensor): batched vertices of tetrahedral meshes, of shape
: (num_vertices, 3).
tets (torch.tensor): unbatched tetrahedral mesh topology, of shape
: (num_tetrahedrons, 4).
sdf (torch.tensor): batched SDFs which specify the SDF value of each vertex, of shape
: (num_vertices).
return_tet_idx (optional, bool): if True, return index of tetrahedron
where each face is extracted. Default: False.
Returns:
(list[torch.Tensor], list[torch.LongTensor], (optional) list[torch.LongTensor]):
- the list of vertices for mesh converted from each tetrahedral grid.
- the list of faces for mesh converted from each tetrahedral grid.
- the list of indices that correspond to tetrahedra where faces are extracted.
Example:
>>> vertices = torch.tensor([[[0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [0, 0, 1]]], dtype=torch.float)
>>> tets = torch.tensor([[0, 1, 2, 3]], dtype=torch.long)
>>> sdf = torch.tensor([[-1., -1., 0.5, 0.5]], dtype=torch.float)
>>> verts_list, faces_list, tet_idx_list = marching_tetrahedra(vertices, tets, sdf, True)
>>> verts_list[0]
tensor([[0.0000, 0.6667, 0.0000],
[0.0000, 0.0000, 0.6667],
[0.3333, 0.6667, 0.0000],
[0.3333, 0.0000, 0.6667]])
>>> faces_list[0]
tensor([[3, 0, 1],
[3, 2, 0]])
>>> tet_idx_list[0]
tensor([0, 0])
.. _An efficient method of triangulating equi-valued surfaces by using tetrahedral cells:
https://search.ieice.org/bin/summary.php?id=e74-d_1_214
.. _Deep Marching Tetrahedra\: a Hybrid Representation for High-Resolution 3D Shape Synthesis:
https://arxiv.org/abs/2111.04276
"""
with torch.no_grad():
occ_n = sdf > 0
occ_fx4 = occ_n[tet.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[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="cuda") * -1
mapping[mask_edges] = torch.arange(mask_edges.sum(), dtype=torch.long,device="cuda")
idx_map = mapping[idx_map] # map edges to verts
interp_v = unique_edges[mask_edges]
edges_to_interp = vertices[interp_v.reshape(-1)].reshape(-1,2,3)
edges_to_interp_sdf = sdf[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="cuda"))
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)
if with_uv:
# Get global face index (static, does not depend on topology)
num_tets = tet.shape[0]
tet_gidx = torch.arange(num_tets, dtype=torch.long, device="cuda")[valid_tets]
face_gidx = torch.cat((
tet_gidx[num_triangles == 1]*2,
torch.stack((tet_gidx[num_triangles == 2]*2, tet_gidx[num_triangles == 2]*2 + 1), dim=-1).view(-1)
), dim=0)
uvs, uv_idx = self.map_uv(faces, face_gidx, num_tets*2)
return verts, faces, uvs, uv_idx
else:
return verts, faces
# Geometry interface
class DMTetMesh:
def __init__(self, iterations=5000, chamfer_distance_weight=1, laplacian_weight=0.1, chamfer_faces_sample_scale=1, mesh_scale=1.0, grid_res=128, geom_lr=0.0001, multires=2, mlp_internal_dims=128, mlp_hidden_layer_num=5, device='cuda'):
self.device = torch.device(device)
self.chamfer_distance_weight = chamfer_distance_weight
self.laplacian_weight = laplacian_weight
self.chamfer_faces_sample_scale = chamfer_faces_sample_scale
self.iterations = iterations
self.grid_res = grid_res
self.marching_tets = DMTet(device=self.device)
self.model = Decoder(multires=multires, internal_dims=mlp_internal_dims, hidden=mlp_hidden_layer_num).to(self.device)
self.model.pre_train_sphere()
self.geom_optimizer = torch.optim.Adam(self.model.parameters(), lr=geom_lr)
self.geom_scheduler = torch.optim.lr_scheduler.LambdaLR(self.geom_optimizer, lr_lambda=lambda x: self.lr_schedule(x))
# Load tetrahedral tile, for grid_res==128: verts: torch.Size([277410, 3]), indices: torch.Size([1524684, 4])
# Get pre-generated tetrahedral grid from https://github.com/NVlabs/nvdiffrec/tree/main/data/tets
tets_path = os.path.join(os.path.dirname(__file__), f'../data/tets/{self.grid_res}_tets.npz')
tets = np.load(tets_path)
self.verts = torch.tensor(tets['vertices'], dtype=torch.float32, device=self.device) * mesh_scale
self.indices = torch.tensor(tets['indices'], dtype=torch.long, device=self.device)
self.generate_edges()
def lr_schedule(self, iter):
return max(0.0, 10**(-iter*0.0002)) # Exponential falloff from [1.0, 0.1] over 5k epochs.
def generate_edges(self):
with torch.no_grad():
edges = torch.tensor([0,1,0,2,0,3,1,2,1,3,2,3], dtype = torch.long, device = "cuda")
all_edges = self.indices[:,edges].reshape(-1,2)
all_edges_sorted = torch.sort(all_edges, dim=1)[0]
self.all_edges = torch.unique(all_edges_sorted, dim=0)
def get_mesh(self):
verts, faces = self.pred_verts_faces()
mesh = Mesh(v=verts, f=faces)
mesh.auto_normal()
mesh.auto_uv()
return mesh
def pred_verts_faces(self):
pred = self.model(self.verts) # predict SDF and per-vertex deformation
sdf, deform = self.model.get_sdf_and_deform(pred)
v_deformed = self.verts + torch.tanh(deform) / self.grid_res # constraint deformation to avoid flipping tets
return self.marching_tets(v_deformed, self.indices, sdf)
def training(self, pcd, ref_imgs, ref_masks):
allow_ref_points_loss = pcd is not None
if allow_ref_points_loss:
ref_points_batched = torch.from_numpy(np.asarray(pcd.points)).float().to(self.device).unsqueeze(0)
allow_imgs_loss = ref_imgs is not None
allow_masks_loss = ref_masks is not None
for step in tqdm.trange(self.iterations):
loss = 0
verts, faces = self.pred_verts_faces()
# chamfer distance loss
if allow_ref_points_loss:
num_faces_samples = int(self.chamfer_faces_sample_scale * faces.shape[0])
sampled_points_per_faces = sample_points(verts.unsqueeze(0), faces, num_faces_samples)[0]
loss += self.chamfer_distance_weight * chamfer_distance(sampled_points_per_faces, ref_points_batched)[0].mean()
# laplacian smoothing regularization loss
mesh = Meshes([verts], [faces])
loss += self.laplacian_weight * mesh_laplacian_smoothing(mesh)
# TODO: add images based diffrast loss, i.e (RGB, Alpha, Normal; with L1, SSIM loss)
loss.backward()
self.geom_optimizer.step()
self.geom_scheduler.step()
self.geom_optimizer.zero_grad()
+93
View File
@@ -0,0 +1,93 @@
# This script is modified based on: https://github.com/NVIDIAGameWorks/kaolin/blob/5406915c3a020596384b1a36679ff68afd04d455/examples/tutorial/dmtet_network.py
import torch
from tqdm import tqdm
# MLP + Positional Encoding
class Decoder(torch.nn.Module):
def __init__(self, input_dims = 3, internal_dims = 128, output_dims = 4, hidden = 5, multires = 2):
super().__init__()
self.embed_fn = None
if multires > 0:
embed_fn, input_ch = get_embedder(multires)
self.embed_fn = embed_fn
input_dims = input_ch
net = (torch.nn.Linear(input_dims, internal_dims, bias=False), torch.nn.ReLU())
for i in range(hidden-1):
net = net + (torch.nn.Linear(internal_dims, internal_dims, bias=False), torch.nn.ReLU())
net = net + (torch.nn.Linear(internal_dims, output_dims, bias=False),)
self.net = torch.nn.Sequential(*net)
def forward(self, p):
if self.embed_fn is not None:
p = self.embed_fn(p)
out = self.net(p)
return out
def get_sdf_and_deform(self, out):
return out[:,0], out[:,1:] # sdf, deform
def pre_train_sphere(self, iter=1000):
print ("Initialize SDF to sphere")
loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.Adam(list(self.parameters()), lr=1e-4)
for i in tqdm(range(iter)):
p = torch.rand((1024,3), device='cuda') - 0.5
ref_value = torch.sqrt((p**2).sum(-1)) - 0.3
output = self(p)
loss = loss_fn(output[...,0], ref_value)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Pre-trained MLP", loss.item())
# Positional Encoding from https://github.com/yenchenlin/nerf-pytorch/blob/1f064835d2cca26e4df2d7d130daa39a8cee1795/run_nerf_helpers.py
class Embedder:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.create_embedding_fn()
def create_embedding_fn(self):
embed_fns = []
d = self.kwargs['input_dims']
out_dim = 0
if self.kwargs['include_input']:
embed_fns.append(lambda x : x)
out_dim += d
max_freq = self.kwargs['max_freq_log2']
N_freqs = self.kwargs['num_freqs']
if self.kwargs['log_sampling']:
freq_bands = 2.**torch.linspace(0., max_freq, steps=N_freqs)
else:
freq_bands = torch.linspace(2.**0., 2.**max_freq, steps=N_freqs)
for freq in freq_bands:
for p_fn in self.kwargs['periodic_fns']:
embed_fns.append(lambda x, p_fn=p_fn, freq=freq : p_fn(x * freq))
out_dim += d
self.embed_fns = embed_fns
self.out_dim = out_dim
def embed(self, inputs):
return torch.cat([fn(inputs) for fn in self.embed_fns], -1)
def get_embedder(multires):
embed_kwargs = {
'include_input' : True,
'input_dims' : 3,
'max_freq_log2' : multires-1,
'num_freqs' : multires,
'log_sampling' : True,
'periodic_fns' : [torch.sin, torch.cos],
}
embedder_obj = Embedder(**embed_kwargs)
embed = lambda x, eo=embedder_obj : eo.embed(x)
return embed, embedder_obj.out_dim
+229
View File
@@ -0,0 +1,229 @@
import random
import tqdm
import torch
import torch.nn.functional as F
import numpy as np
from pytorch_msssim import SSIM, MS_SSIM
from .main_3DGS_renderer import Renderer
from ..shared_utils.camera_utils import orbit_camera, OrbitCamera, MiniCam, calculate_fovX, get_projection_matrix
class GSParams:
def __init__(self,
training_iterations=3000,
batch_size=1,
loss_scale=10000,
lambda_ssim=0.2,
lambda_alpha=3,
lambda_offset=0,
lambda_offset_opacity=0,
invert_bg_prob = 0.5,
feature_lr=0.01,
opacity_lr=0.05,
scaling_lr=0.005,
rotation_lr=0.005,
position_lr_init=0.001,
position_lr_final=0.00002,
position_lr_delay_mult=0.02,
position_lr_max_steps=500,
num_pts=5000,
K=3,
percent_dense=0.01,
density_start_iter=100,
density_end_iter=100000,
densification_interval=100,
opacity_reset_interval=700,
densify_grad_threshold=0.01,
sh_degree=0):
# training params
self.training_iterations = training_iterations
self.batch_size = batch_size
self.loss_scale = loss_scale
self.lambda_ssim = lambda_ssim
self.lambda_alpha = lambda_alpha
self.lambda_offset = lambda_offset
self.lambda_offset_opacity = lambda_offset_opacity
self.invert_bg_prob = invert_bg_prob
# learning params
self.feature_lr = feature_lr
self.opacity_lr = opacity_lr
self.scaling_lr = scaling_lr
self.rotation_lr = rotation_lr
self.position_lr_init = position_lr_init
self.position_lr_final = position_lr_final
self.position_lr_delay_mult = position_lr_delay_mult
self.position_lr_max_steps = position_lr_max_steps
# densify and prune params
self.num_pts = num_pts
self.K = K
self.percent_dense = percent_dense
self.density_start_iter=density_start_iter
self.density_end_iter=density_end_iter
self.densification_interval=densification_interval
self.opacity_reset_interval=opacity_reset_interval
self.densify_grad_threshold=densify_grad_threshold
# other gaussian params
self.sh_degree = sh_degree
class GaussianSplatting:
def __init__(self, gs_params=None, init_input=None, device='cuda'):
self.device = torch.device(device)
# prepare renderer for optimization
if gs_params is None:
gs_params = GSParams()
self.renderer = Renderer(sh_degree=gs_params.sh_degree)
self.gaussain_scale_factor = 1
self.renderer.initialize(init_input, num_pts=gs_params.num_pts)
# setup training
self.renderer.gaussians.training_setup(gs_params)
# do not do progressive sh-level
self.renderer.gaussians.active_sh_degree = self.renderer.gaussians.max_sh_degree
self.optimizer = self.renderer.gaussians.optimizer
#self.ssim_loss = SSIM(data_range=1, size_average=True, channel=3)
self.ms_ssim_loss = MS_SSIM(data_range=1, size_average=True, channel=3)
self.gs_params = gs_params
def prepare_img(self, img):
img_new = img.permute(2, 0, 1).unsqueeze(0).to(self.device)
img_new = F.interpolate(img_new, (self.ref_size_H, self.ref_size_W), mode="bilinear", align_corners=False).contiguous()
return img_new
def prepare_training(self, reference_images, reference_masks, reference_orbit_camera_poses, reference_orbit_camera_fovy):
self.ref_imgs_num = len(reference_images)
self.all_ref_cam_poses = reference_orbit_camera_poses
self.ref_cam_fovy = reference_orbit_camera_fovy
self.ref_size_H = reference_images[0].shape[0]
self.ref_size_W = reference_images[0].shape[1]
# default camera settings
self.cam = OrbitCamera(self.ref_size_W, self.ref_size_H, fovy=reference_orbit_camera_fovy)
self.projection_matrix = get_projection_matrix(self.cam.near, self.cam.far, self.cam.fovx, self.cam.fovy).transpose(0, 1).cuda()
# prepare reference images and masks
ref_imgs_torch_list = []
ref_masks_torch_list = []
for i in range(self.ref_imgs_num):
ref_imgs_torch_list.append(self.prepare_img(reference_images[i]))
ref_masks_torch_list.append(self.prepare_img(reference_masks[i].unsqueeze(2)))
self.ref_imgs_torch = torch.cat(ref_imgs_torch_list, dim=0)
self.ref_masks_torch = torch.cat(ref_masks_torch_list, dim=0)
def training(self):
starter = torch.cuda.Event(enable_timing=True)
ender = torch.cuda.Event(enable_timing=True)
starter.record()
ref_imgs_masked = []
for i in range(self.ref_imgs_num):
ref_imgs_masked.append((self.ref_imgs_torch[i] * self.ref_masks_torch[i]).unsqueeze(0))
ref_imgs_num_minus_1 = self.ref_imgs_num-1
for step in tqdm.trange(self.gs_params.training_iterations):
### calculate loss between reference and rendered image from known view
loss = 0
masked_rendered_img_batch = []
masked_ref_img_batch = []
masks_batch = []
ref_masks_batch = []
step_ratio = min(1, step / self.gs_params.training_iterations)
# update lr
self.renderer.gaussians.update_learning_rate(step)
loss = 0
for _ in range(self.gs_params.batch_size):
### calculate loss between reference and rendered image from known view
i = random.randint(0, ref_imgs_num_minus_1)
radius, elevation, azimuth, center_X, center_Y, center_Z = self.all_ref_cam_poses[i]
orbit_target = np.array([center_X, center_Y, center_Z], dtype=np.float32)
ref_pose = orbit_camera(elevation, azimuth, radius, target=orbit_target)
ref_cam = MiniCam(ref_pose, self.ref_size_W, self.ref_size_H, self.cam.fovy, self.cam.fovx, self.cam.near, self.cam.far, self.projection_matrix)
bg_color = torch.tensor([1, 1, 1] if np.random.rand() > self.gs_params.invert_bg_prob else [0, 0, 0], dtype=torch.float32, device=self.device)
out = self.renderer.render(ref_cam, bg_color=bg_color)
image = out["image"] # [3, H, W] in [0, 1]
mask = out["alpha"] # [1, H, W] in [0, 1]
ref_mask = self.ref_masks_torch[i]
image_masked = (image * ref_mask).unsqueeze(0)
masked_rendered_img_batch.append(image_masked)
masked_ref_img_batch.append(ref_imgs_masked[i])
masks_batch.append(mask.unsqueeze(0))
ref_masks_batch.append(ref_mask.unsqueeze(0))
masked_rendered_img_batch_torch = torch.cat(masked_rendered_img_batch, dim=0)
masked_ref_img_batch_torch = torch.cat(masked_ref_img_batch, dim=0)
masks_batch_torch = torch.cat(masks_batch, dim=0)
ref_masks_batch_torch = torch.cat(ref_masks_batch, dim=0)
# rgb loss
loss += (1 - self.gs_params.lambda_ssim) * self.gs_params.loss_scale * step_ratio * F.mse_loss(masked_rendered_img_batch_torch, masked_ref_img_batch_torch)
# alpha loss
loss += self.gs_params.lambda_alpha * self.gs_params.loss_scale * step_ratio * F.mse_loss(masks_batch_torch, ref_masks_batch_torch)
# D-SSIM loss
# [1, 3, H, W] in [0, 1]
#loss += self.lambda_ssim * (1 - self.ssim_loss(X, Y))
loss += self.gs_params.lambda_ssim * self.gs_params.loss_scale * step_ratio * (1 - self.ms_ssim_loss(masked_ref_img_batch_torch, masked_rendered_img_batch_torch))
if self.gs_params.lambda_offset > 0:
# Reference offset loss
offset_norm = self.renderer.gaussians.get_xyz_offset.norm(dim=-1, keepdim=True)
loss += self.gs_params.lambda_offset * self.gs_params.loss_scale * step_ratio * torch.mean(offset_norm)
if self.gs_params.lambda_offset_opacity > 0:
# Alpha penalty loss
loss += self.gs_params.lambda_offset_opacity * self.gs_params.loss_scale * step_ratio * torch.mean(offset_norm.detach() * self.renderer.gaussians.get_opacity)
# optimize step
loss.backward()
self.optimizer.step()
self.optimizer.zero_grad()
# densify and prune
if step >= self.gs_params.density_start_iter and step <= self.gs_params.density_end_iter:
viewspace_point_tensor, visibility_filter, radii = out["viewspace_points"], out["visibility_filter"], out["radii"]
self.renderer.gaussians.max_radii2D[visibility_filter] = torch.max(self.renderer.gaussians.max_radii2D[visibility_filter], radii[visibility_filter])
self.renderer.gaussians.add_densification_stats(viewspace_point_tensor, visibility_filter)
if step % self.gs_params.densification_interval == 0:
#self.renderer.gaussians.densify_and_prune(self.gs_params.densify_grad_threshold, min_opacity=0.01, extent=4, max_screen_size=1)
#self.renderer.gaussians.densify_and_prune_by_compatness(self.gs_params.K, min_opacity=0.01, extent=4, max_screen_size=1)
#self.renderer.gaussians.densify_by_clone_and_split(self.gs_params.densify_grad_threshold, extent=4)
self.renderer.gaussians.densify_by_compatness(self.gs_params.K)
self.renderer.gaussians.prune(min_opacity=0.01, extent=4, max_screen_size=1, max_offset=0.1)
if step % self.gs_params.opacity_reset_interval == 0:
self.renderer.gaussians.reset_opacity()
ender.record()
torch.cuda.synchronize()
#t = starter.elapsed_time(ender)
self.need_update = True
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
import torch
def sdf_reg_loss(sdf, all_edges):
sdf_f1x6x2 = sdf[all_edges.reshape(-1)].reshape(-1,2)
mask = torch.sign(sdf_f1x6x2[...,0]) != torch.sign(sdf_f1x6x2[...,1])
sdf_f1x6x2 = sdf_f1x6x2[mask]
sdf_diff = torch.nn.functional.binary_cross_entropy_with_logits(sdf_f1x6x2[...,0], (sdf_f1x6x2[...,1] > 0).float()) + \
torch.nn.functional.binary_cross_entropy_with_logits(sdf_f1x6x2[...,1], (sdf_f1x6x2[...,0] > 0).float())
return sdf_diff
+165
View File
@@ -0,0 +1,165 @@
# Copyright (c) 2021,22 NVIDIA CORPORATION & AFFILIATES.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
__all__ = ['marching_tetrahedra']
triangle_table = torch.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)
num_triangles_table = torch.tensor([0, 1, 1, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1, 1, 0], dtype=torch.long)
base_tet_edges = torch.tensor([0, 1, 0, 2, 0, 3, 1, 2, 1, 3, 2, 3], dtype=torch.long)
v_id = torch.pow(2, torch.arange(4, dtype=torch.long))
def _sort_edges(edges):
"""sort last dimension of edges of shape (E, 2)"""
with torch.no_grad():
order = (edges[:, 0] > edges[:, 1]).long()
order = order.unsqueeze(dim=1)
a = torch.gather(input=edges, index=order, dim=1)
b = torch.gather(input=edges, index=1 - order, dim=1)
return torch.stack([a, b], -1)
def _unbatched_marching_tetrahedra(vertices, tets, sdf, return_tet_idx):
"""unbatched marching tetrahedra.
Refer to :func:`marching_tetrahedra`.
"""
device = vertices.device
with torch.no_grad():
occ_n = sdf > 0
occ_fx4 = occ_n[tets.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 = tets[valid_tets][:, base_tet_edges.to(device)].reshape(-1, 2)
all_edges = _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=device) * -1
mapping[mask_edges] = torch.arange(mask_edges.sum(), dtype=torch.long, device=device)
idx_map = mapping[idx_map]
interp_v = unique_edges[mask_edges]
edges_to_interp = vertices[interp_v.reshape(-1)].reshape(-1, 2, 3)
edges_to_interp_sdf = sdf[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)
tetindex = (occ_fx4[valid_tets] * v_id.to(device).unsqueeze(0)).sum(-1)
num_triangles = num_triangles_table.to(device)[tetindex]
triangle_table_device = triangle_table.to(device)
# Generate triangle indices
faces = torch.cat((
torch.gather(input=idx_map[num_triangles == 1], dim=1,
index=triangle_table_device[tetindex[num_triangles == 1]][:, :3]).reshape(-1, 3),
torch.gather(input=idx_map[num_triangles == 2], dim=1,
index=triangle_table_device[tetindex[num_triangles == 2]][:, :6]).reshape(-1, 3),
), dim=0)
if return_tet_idx:
tet_idx = torch.arange(tets.shape[0], device=device)[valid_tets]
tet_idx = torch.cat((tet_idx[num_triangles == 1], tet_idx[num_triangles ==
2].unsqueeze(-1).expand(-1, 2).reshape(-1)), dim=0)
return verts, faces, tet_idx
return verts, faces
def marching_tetrahedra(vertices, tets, sdf, return_tet_idx=False):
r"""Convert discrete signed distance fields encoded on tetrahedral grids to triangle
meshes using marching tetrahedra algorithm as described in `An efficient method of
triangulating equi-valued surfaces by using tetrahedral cells`_. The output surface is differentiable with respect to
input vertex positions and the SDF values. For more details and example usage in learning, see
`Deep Marching Tetrahedra\: a Hybrid Representation for High-Resolution 3D Shape Synthesis`_ NeurIPS 2021.
Args:
vertices (torch.tensor): batched vertices of tetrahedral meshes, of shape
:math:`(\text{batch_size}, \text{num_vertices}, 3)`.
tets (torch.tensor): unbatched tetrahedral mesh topology, of shape
:math:`(\text{num_tetrahedrons}, 4)`.
sdf (torch.tensor): batched SDFs which specify the SDF value of each vertex, of shape
:math:`(\text{batch_size}, \text{num_vertices})`.
return_tet_idx (optional, bool): if True, return index of tetrahedron
where each face is extracted. Default: False.
Returns:
(list[torch.Tensor], list[torch.LongTensor], (optional) list[torch.LongTensor]):
- the list of vertices for mesh converted from each tetrahedral grid.
- the list of faces for mesh converted from each tetrahedral grid.
- the list of indices that correspond to tetrahedra where faces are extracted.
Example:
>>> vertices = torch.tensor([[[0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [0, 0, 1]]], dtype=torch.float)
>>> tets = torch.tensor([[0, 1, 2, 3]], dtype=torch.long)
>>> sdf = torch.tensor([[-1., -1., 0.5, 0.5]], dtype=torch.float)
>>> verts_list, faces_list, tet_idx_list = marching_tetrahedra(vertices, tets, sdf, True)
>>> verts_list[0]
tensor([[0.0000, 0.6667, 0.0000],
[0.0000, 0.0000, 0.6667],
[0.3333, 0.6667, 0.0000],
[0.3333, 0.0000, 0.6667]])
>>> faces_list[0]
tensor([[3, 0, 1],
[3, 2, 0]])
>>> tet_idx_list[0]
tensor([0, 0])
.. _An efficient method of triangulating equi-valued surfaces by using tetrahedral cells:
https://search.ieice.org/bin/summary.php?id=e74-d_1_214
.. _Deep Marching Tetrahedra\: a Hybrid Representation for High-Resolution 3D Shape Synthesis:
https://arxiv.org/abs/2111.04276
"""
list_of_outputs = [_unbatched_marching_tetrahedra(vertices[b], tets, sdf[b], return_tet_idx) for b in range(vertices.shape[0])]
return list(zip(*list_of_outputs))