Files
hand-motion-pipeline/third_party/FoundationPose/bundlesdf/nerf_runner.py
T
liyang ae28d55f81 Update to 2026-09-17 pipeline snapshot; add weights, L20 assets and recording via Git LFS
Source: RGB-D -> Dyn-HaMR -> L20 retargeting -> FoundationPose -> reference repair -> SPIDER,
documented in docs/PIPELINE_LATEST.md and docs/SETUP_AND_WEIGHTS.md. Adds FoundationPose and
nvdiffrast upstream snapshots, requirements/pipeline_venv.txt and the FoundationPose weight
manifest/downloader.

Assets (Git LFS): weights/ (WiLoR detector, HandFlow denoiser, UniDepth-L), FoundationPose
checkpoints, HaMeR checkpoint, Dyn-HaMR HMP model and BMC constraints, L20 URDF/meshes, the
20260915_171525 D405 recording and the two box CADs. MANO models are not redistributed
(third_party/hamer/_DATA/data/mano/README.txt). Environments, caches and run outputs excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:43:37 +08:00

1233 lines
53 KiB
Python

# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto. Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
import os, sys,copy,cv2,itertools,uuid,joblib,uuid
import shutil
from datetime import datetime
import numpy as np
import matplotlib.pyplot as plt
import imageio,trimesh
import json
import pdb
import random
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
import pickle
import matplotlib.pyplot as plt
code_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(f'{code_dir}/../')
sys.path.append(f'{code_dir}')
from nerf_helpers import *
from Utils import *
def batchify(fn, chunk):
"""Constructs a version of 'fn' that applies to smaller batches.
"""
if chunk is None:
return fn
def ret(inputs):
return torch.cat([fn(inputs[i:i+chunk]) for i in range(0, inputs.shape[0], chunk)], 0)
return ret
def compute_near_far_and_filter_rays(cam_in_world,rays,cfg):
'''
@cam_in_world: (4,4) in normalized space
@rays: (...,D) in camera
Return:
(-1,D+2) with near far
'''
D = rays.shape[-1]
rays = rays.reshape(-1,D)
dirs_unit = rays[:,:3]/np.linalg.norm(rays[:,:3],axis=-1).reshape(-1,1)
dirs = (cam_in_world[:3,:3]@rays[:,:3].T).T
origins = (cam_in_world@to_homo(np.zeros(dirs.shape)).T).T[:,:3]
bounds = np.array(cfg['bounding_box']).reshape(2,3)
tmin,tmax = ray_box_intersection_batch(origins,dirs,bounds)
tmin = tmin.data.cpu().numpy()
tmax = tmax.data.cpu().numpy()
ishit = tmin>=0
near = (dirs_unit*tmin.reshape(-1,1))[:,2]
far = (dirs_unit*tmax.reshape(-1,1))[:,2]
good_rays = rays[ishit]
near = near[ishit]
far = far[ishit]
near = np.abs(near)
far = np.abs(far)
good_rays = np.concatenate((good_rays,near.reshape(-1,1),far.reshape(-1,1)), axis=-1) #(N,8+2)
return good_rays
@torch.no_grad()
def sample_rays_uniform(N_samples,near,far,lindisp=False,perturb=True):
'''
@near: (N_ray,1)
'''
N_ray = near.shape[0]
t_vals = torch.linspace(0., 1., steps=N_samples, device=near.device).reshape(1,-1)
if not lindisp:
z_vals = near * (1.-t_vals) + far * (t_vals)
else:
z_vals = 1./(1./near * (1.-t_vals) + 1./far * (t_vals)) #(N_ray,N_sample)
if perturb > 0.:
mids = .5 * (z_vals[...,1:] + z_vals[...,:-1])
upper = torch.cat([mids, z_vals[...,-1:]], -1)
lower = torch.cat([z_vals[...,:1], mids], -1)
t_rand = torch.rand(z_vals.shape, device=far.device)
z_vals = lower + (upper - lower) * t_rand
z_vals = torch.clip(z_vals,near,far)
return z_vals.reshape(N_ray,N_samples)
class DataLoader:
def __init__(self,rays,batch_size):
self.rays = rays
self.batch_size = batch_size
self.pos = 0
self.ids = torch.randperm(len(self.rays))
def __next__(self):
if self.pos+self.batch_size<len(self.ids):
self.batch_ray_ids = self.ids[self.pos:self.pos+self.batch_size]
out = self.rays[self.batch_ray_ids]
self.pos += self.batch_size
return out.cuda()
self.ids = torch.randperm(len(self.rays))
self.pos = self.batch_size
self.batch_ray_ids = self.ids[:self.batch_size]
return self.rays[self.batch_ray_ids].cuda()
class NerfRunner:
def __init__(self,cfg,images,depths,masks,normal_maps,poses,K,_run=None,occ_masks=None,build_octree_pcd=None):
set_seed(0)
self.cfg = cfg
self.cfg['tv_loss_weight'] = eval(str(self.cfg['tv_loss_weight']))
self._run = _run
self.images = images
self.depths = depths
self.masks = masks
self.poses = poses
self.normal_maps = normal_maps
self.occ_masks = occ_masks
self.K = K.copy()
self.mesh = None
self.train_pose = False
self.N_iters = self.cfg['n_step']+1
self.build_octree_pts = np.asarray(build_octree_pcd.points).copy() # Make it pickable
down_scale_ratio = cfg['down_scale_ratio']
self.down_scale = np.ones((2),dtype=np.float32)
if down_scale_ratio!=1:
H,W = images[0].shape[:2]
down_scale_ratio = int(down_scale_ratio)
self.images = images[:, ::down_scale_ratio, ::down_scale_ratio]
self.depths = depths[:, ::down_scale_ratio, ::down_scale_ratio]
self.masks = masks[:, ::down_scale_ratio, ::down_scale_ratio]
if normal_maps is not None:
self.normal_maps = normal_maps[:, ::down_scale_ratio, ::down_scale_ratio]
if occ_masks is not None:
self.occ_masks = occ_masks[:, ::down_scale_ratio, ::down_scale_ratio]
self.H, self.W = self.images.shape[1:3]
self.cfg['dilate_mask_size'] = int(self.cfg['dilate_mask_size']//down_scale_ratio)
self.K[0] *= float(self.W)/W
self.K[1] *= float(self.H)/H
self.down_scale = np.array([float(self.W)/W, float(self.H)/H])
self.H, self.W = self.images[0].shape[:2]
self.octree_m = None
if self.cfg['use_octree']:
self.build_octree()
self.create_nerf()
self.create_optimizer()
self.amp_scaler = torch.cuda.amp.GradScaler(enabled=self.cfg['amp'])
self.global_step = 0
print("sc_factor",self.cfg['sc_factor'])
print("translation",self.cfg['translation'])
self.c2w_array = torch.tensor(poses).float().cuda()
self.best_models = None
self.best_loss = np.inf
rays_ = []
for i_mask in range(len(self.masks)):
rays = self.make_frame_rays(i_mask)
rays_.append(rays)
rays = np.concatenate(rays_, axis=0)
if self.cfg['denoise_depth_use_octree_cloud']:
logging.info("denoise cloud")
mask = (rays[:,self.ray_mask_slice]>0) & (rays[:,self.ray_depth_slice]<=self.cfg['far']*self.cfg['sc_factor'])
rays_dir = rays[mask][:,self.ray_dir_slice]
rays_depth = rays[mask][:,self.ray_depth_slice]
pts3d = rays_dir*rays_depth.reshape(-1,1)
frame_ids = rays[mask][:,self.ray_frame_id_slice].astype(int)
pts3d_w = (self.poses[frame_ids]@to_homo(pts3d)[...,None])[:,:3,0]
logging.info(f"Denoising rays based on octree cloud")
kdtree = cKDTree(self.build_octree_pts)
dists,indices = kdtree.query(pts3d_w,k=1,workers=-1)
bad_mask = dists>0.02*self.cfg['sc_factor']
bad_ids = np.arange(len(rays))[mask][bad_mask]
rays[bad_ids,self.ray_depth_slice] = BAD_DEPTH*self.cfg['sc_factor']
rays[bad_ids, self.ray_type_slice] = 1
rays = rays[rays[:,self.ray_type_slice]==0]
logging.info(f"bad_mask#={bad_mask.sum()}")
rays = torch.tensor(rays, dtype=torch.float).cuda()
self.rays = rays
print("rays",rays.shape)
self.data_loader = DataLoader(rays=self.rays, batch_size=self.cfg['N_rand'])
def create_nerf(self,device=torch.device("cuda")):
"""Instantiate NeRF's MLP model.
"""
models = {}
embed_fn, input_ch = get_embedder(self.cfg['multires'], self.cfg, i=self.cfg['i_embed'], octree_m=self.octree_m)
embed_fn = embed_fn.to(device)
models['embed_fn'] = embed_fn
input_ch_views = 0
embeddirs_fn = None
if self.cfg['use_viewdirs']:
embeddirs_fn, input_ch_views = get_embedder(self.cfg['multires_views'], self.cfg, i=self.cfg['i_embed_views'], octree_m=self.octree_m)
models['embeddirs_fn'] = embeddirs_fn
output_ch = 4
skips = [4]
model = NeRFSmall(num_layers=2,hidden_dim=64,geo_feat_dim=15,num_layers_color=3,hidden_dim_color=64,input_ch=input_ch, input_ch_views=input_ch_views+self.cfg['frame_features']).to(device)
model = model.to(device)
models['model'] = model
model_fine = None
if self.cfg['N_importance'] > 0:
if not self.cfg['share_coarse_fine']:
model_fine = NeRFSmall(num_layers=2,hidden_dim=64,geo_feat_dim=15,num_layers_color=3,hidden_dim_color=64,input_ch=input_ch, input_ch_views=input_ch_views).to(device)
models['model_fine'] = model_fine
# Create feature array
num_training_frames = len(self.images)
feature_array = None
if self.cfg['frame_features'] > 0:
feature_array = FeatureArray(num_training_frames, self.cfg['frame_features']).to(device)
models['feature_array'] = feature_array
# Create pose array
pose_array = None
if self.cfg['optimize_poses']:
pose_array = PoseArray(num_training_frames,max_trans=self.cfg['max_trans']*self.cfg['sc_factor'],max_rot=self.cfg['max_rot']).to(device)
models['pose_array'] = pose_array
self.models = models
def make_frame_rays(self,frame_id):
mask = self.masks[frame_id,...,0].copy()
rays = get_camera_rays_np(self.H, self.W, self.K) # [self.H, self.W, 3] We create rays frame-by-frame to save memory
rays = np.concatenate([rays, self.images[frame_id]], -1) # [H, W, 6]
rays = np.concatenate([rays, self.depths[frame_id]], -1) # [H, W, 7]
rays = np.concatenate([rays, self.masks[frame_id]>0], -1) # [H, W, 8]
if self.normal_maps is not None:
rays = np.concatenate([rays, self.normal_maps[frame_id]], -1) # [H, W, 11]
rays = np.concatenate([rays, frame_id*np.ones(self.depths[frame_id].shape)], -1) # [H, W, 12]
ray_types = np.zeros((self.H,self.W,1)) # 0 is good; 1 is invalid depth (uncertain)
invalid_depth = ((self.depths[frame_id,...,0]<self.cfg['near']*self.cfg['sc_factor']) | (self.depths[frame_id,...,0]>self.cfg['far']*self.cfg['sc_factor'])) & (mask>0)
ray_types[invalid_depth] = 1
rays = np.concatenate((rays,ray_types), axis=-1)
self.ray_dir_slice = [0,1,2]
self.ray_rgb_slice = [3,4,5]
self.ray_depth_slice = 6
self.ray_mask_slice = 7
if self.normal_maps is not None:
self.ray_normal_slice = [8,9,10]
self.ray_frame_id_slice = 11
self.ray_type_slice = 12
else:
self.ray_frame_id_slice = 8
self.ray_type_slice = 9
n = rays.shape[-1]
########## Option2: dilate
down_scale_ratio = int(self.cfg['down_scale_ratio'])
if frame_id==0: #!NOTE first frame ob mask is assumed perfect
kernel = np.ones((100, 100), np.uint8)
mask = cv2.dilate(mask.astype(np.uint8), kernel, iterations=1)
if self.occ_masks is not None:
mask[self.occ_masks[frame_id]>0] = 0
else:
dilate = 60//down_scale_ratio
kernel = np.ones((dilate, dilate), np.uint8)
mask = cv2.dilate(mask.astype(np.uint8), kernel, iterations=1)
if self.occ_masks is not None:
mask[self.occ_masks[frame_id]>0] = 0
if self.cfg['rays_valid_depth_only']:
mask[invalid_depth] = 0
vs,us = np.where(mask>0)
cur_rays = rays[vs,us].reshape(-1,n)
cur_rays = cur_rays[cur_rays[:,self.ray_type_slice]==0]
cur_rays = compute_near_far_and_filter_rays(self.poses[frame_id],cur_rays,self.cfg)
if self.normal_maps is not None:
self.ray_near_slice = 13
self.ray_far_slice = 14
else:
self.ray_near_slice = 10
self.ray_far_slice = 11
if self.cfg['use_octree']:
rays_o_world = (self.poses[frame_id]@to_homo(np.zeros((len(cur_rays),3))).T).T[:,:3]
rays_o_world = torch.from_numpy(rays_o_world).cuda().float()
rays_unit_d_cam = cur_rays[:,:3]/np.linalg.norm(cur_rays[:,:3],axis=-1).reshape(-1,1)
rays_d_world = (self.poses[frame_id][:3,:3]@rays_unit_d_cam.T).T
rays_d_world = torch.from_numpy(rays_d_world).cuda().float()
vox_size = self.cfg['octree_raytracing_voxel_size']*self.cfg['sc_factor']
level = int(np.floor(np.log2(2.0/vox_size)))
near,far,_,ray_depths_in_out = self.octree_m.ray_trace(rays_o_world,rays_d_world,level=level)
near = near.cpu().numpy()
valid = (near>0).reshape(-1)
cur_rays = cur_rays[valid]
return cur_rays
def build_octree(self):
if self.cfg['save_octree_clouds']:
dir = f"{self.cfg['save_dir']}/build_octree_cloud.ply"
pcd = toOpen3dCloud(self.build_octree_pts)
o3d.io.write_point_cloud(dir,pcd)
if self._run is not None:
self._run.add_artifact(dir)
pts = torch.tensor(self.build_octree_pts).cuda().float() # Must be within [-1,1]
octree_smallest_voxel_size = self.cfg['octree_smallest_voxel_size']*self.cfg['sc_factor']
finest_n_voxels = 2.0/octree_smallest_voxel_size
max_level = int(np.round(np.log2(finest_n_voxels)))
octree_smallest_voxel_size = 2.0/(2**max_level)
#################### Dilate
dilate_radius = int(np.round(self.cfg['octree_dilate_size']/octree_smallest_voxel_size))
dilate_radius = max(1, dilate_radius)
logging.info(f"Octree voxel dilate_radius:{dilate_radius}")
shifts = []
for dx in [-1,0,1]:
for dy in [-1,0,1]:
for dz in [-1,0,1]:
shifts.append([dx,dy,dz])
shifts = torch.tensor(shifts).cuda().long() # (27,3)
coords = torch.floor((pts+1)/octree_smallest_voxel_size).long() #(N,3)
dilated_coords = coords.detach().clone()
for iter in range(dilate_radius):
dilated_coords = (dilated_coords[None].expand(shifts.shape[0],-1,-1) + shifts[:,None]).reshape(-1,3)
dilated_coords = torch.unique(dilated_coords,dim=0)
pts = (dilated_coords+0.5) * octree_smallest_voxel_size - 1
pts = torch.clip(pts,-1,1)
if self.cfg['save_octree_clouds']:
pcd = toOpen3dCloud(pts.data.cpu().numpy())
dir = f"{self.cfg['save_dir']}/build_octree_cloud_dilated.ply"
o3d.io.write_point_cloud(dir,pcd)
if self._run is not None:
self._run.add_artifact(dir)
####################
assert pts.min()>=-1 and pts.max()<=1
self.octree_m = OctreeManager(pts, max_level)
if self.cfg['save_octree_clouds']:
dir = f"{self.cfg['save_dir']}/octree_boxes_max_level.ply"
m = self.octree_m.draw(level=max_level,method='point')
m.export(dir)
if self._run is not None:
self._run.add_artifact(dir)
vox_size = self.cfg['octree_raytracing_voxel_size']*self.cfg['sc_factor']
level = int(np.round(np.log2(2.0/vox_size)))
if self.cfg['save_octree_clouds']:
dir = f"{self.cfg['save_dir']}/octree_boxes_ray_tracing_level.ply"
m = self.octree_m.draw(level=level,method='point')
m.export(dir)
if self._run is not None:
self._run.add_artifact(dir)
def create_optimizer(self):
params = []
for k in self.models:
if self.models[k] is not None and k!='pose_array':
params += list(self.models[k].parameters())
param_groups = [{'name':'basic', 'params':params, 'lr':self.cfg['lrate']}]
if self.models['pose_array'] is not None:
param_groups.append({'name':'pose_array', 'params':self.models['pose_array'].parameters(), 'lr':self.cfg['lrate_pose']})
self.optimizer = torch.optim.Adam(param_groups, betas=(0.9, 0.999),weight_decay=0,eps=1e-15)
self.param_groups_init = copy.deepcopy(self.optimizer.param_groups)
def save_weights(self,out_file,models):
data = {
'global_step': self.global_step,
'model': models['model'].state_dict(),
'optimizer': self.optimizer.state_dict(),
}
if 'model_fine' in models and models['model_fine'] is not None:
data['model_fine'] = models['model_fine'].state_dict()
if models['embed_fn'] is not None:
data['embed_fn'] = models['embed_fn'].state_dict()
if models['embeddirs_fn'] is not None:
data['embeddirs_fn'] = models['embeddirs_fn'].state_dict()
if self.cfg['optimize_poses']>0:
data['pose_array'] = models['pose_array'].state_dict()
if self.cfg['frame_features'] > 0:
data['feature_array'] = models['feature_array'].state_dict()
if self.octree_m is not None:
data['octree'] = self.octree_m.octree
dir = out_file
torch.save(data,dir)
print('Saved checkpoints at', dir)
if self._run is not None:
self._run.add_artifact(dir)
dir1 = copy.deepcopy(dir)
dir = f'{os.path.dirname(out_file)}/model_latest.pth'
if dir1!=dir:
os.system(f'cp {dir1} {dir}')
if self._run is not None:
self._run.add_artifact(dir)
def schedule_lr(self):
for i,param_group in enumerate(self.optimizer.param_groups):
init_lr = self.param_groups_init[i]['lr']
new_lrate = init_lr * (self.cfg['decay_rate'] ** (float(self.global_step) / self.N_iters))
param_group['lr'] = new_lrate
def render_images(self,img_i,cur_rays=None):
if cur_rays is None:
frame_ids = self.rays[:, self.ray_frame_id_slice].cuda()
cur_rays = self.rays[frame_ids==img_i].cuda()
gt_depth = cur_rays[:,self.ray_depth_slice]
gt_rgb = cur_rays[:,self.ray_rgb_slice].cpu()
ray_type = cur_rays[:,self.ray_type_slice].data.cpu().numpy()
ori_chunk = self.cfg['chunk']
self.cfg['chunk'] = copy.deepcopy(self.cfg['N_rand'])
with torch.no_grad():
rgb, extras = self.render(rays=cur_rays, lindisp=False,perturb=False,raw_noise_std=0, depth=gt_depth)
self.cfg['chunk'] = ori_chunk
sdf = extras['raw'][...,-1]
z_vals = extras['z_vals']
signs = sdf[:, 1:] * sdf[:, :-1]
empty_rays = (signs>0).all(dim=-1)
mask = signs<0
inds = torch.argmax(mask.float(), axis=1)
inds = inds[..., None]
depth = torch.gather(z_vals,dim=1,index=inds)
depth[empty_rays] = self.cfg['far']*self.cfg['sc_factor']
depth = depth[..., None].data.cpu().numpy()
rgb = rgb.data.cpu().numpy()
rgb_full = np.zeros((self.H,self.W,3),dtype=float)
depth_full = np.zeros((self.H,self.W),dtype=float)
ray_mask_full = np.zeros((self.H,self.W,3),dtype=np.uint8)
X = cur_rays[:,self.ray_dir_slice].data.cpu().numpy()
X[:,[1,2]] = -X[:,[1,2]]
projected = (self.K@X.T).T
uvs = projected/projected[:,2].reshape(-1,1)
uvs = uvs.round().astype(int)
uvs_good = uvs[ray_type==0]
ray_mask_full[uvs_good[:,1],uvs_good[:,0]] = [255,0,0]
uvs_uncertain = uvs[ray_type==1]
ray_mask_full[uvs_uncertain[:,1],uvs_uncertain[:,0]] = [0,255,0]
rgb_full[uvs[:,1],uvs[:,0]] = rgb.reshape(-1,3)
depth_full[uvs[:,1],uvs[:,0]] = depth.reshape(-1)
gt_rgb_full = np.zeros((self.H,self.W,3),dtype=float)
gt_rgb_full[uvs[:,1],uvs[:,0]] = gt_rgb.reshape(-1,3).data.cpu().numpy()
gt_depth_full = np.zeros((self.H,self.W),dtype=float)
gt_depth_full[uvs[:,1],uvs[:,0]] = gt_depth.reshape(-1).data.cpu().numpy()
return rgb_full, depth_full, ray_mask_full, gt_rgb_full, gt_depth_full, extras
def get_gradients(self):
if self.models['pose_array'] is not None:
max_pose_grad = torch.abs(self.models['pose_array'].data.grad).max()
max_embed_grad = 0
for embed in self.models['embed_fn'].embeddings:
max_embed_grad = max(max_embed_grad,torch.abs(embed.weight.grad).max())
if self.models['feature_array'] is not None:
max_feature_grad = torch.abs(self.models['feature_array'].data.grad).max()
return max_pose_grad, max_embed_grad, max_feature_grad
def get_truncation(self):
'''Annearl truncation over training
'''
if self.cfg['trunc_decay_type']=='linear':
truncation = self.cfg['trunc_start'] - (self.cfg['trunc_start']-self.cfg['trunc']) * float(self.global_step)/self.cfg['n_step']
elif self.cfg['trunc_decay_type']=='exp':
lamb = np.log(self.cfg['trunc']/self.cfg['trunc_start']) / (self.cfg['n_step']/4)
truncation = self.cfg['trunc_start']*np.exp(self.global_step*lamb)
truncation = max(truncation,self.cfg['trunc'])
else:
truncation = self.cfg['trunc']
truncation *= self.cfg['sc_factor']
return truncation
def train_loop(self,batch):
target_s = batch[:, self.ray_rgb_slice] # Color (N,3)
target_d = batch[:, self.ray_depth_slice] # Normalized scale (N)
target_mask = batch[:,self.ray_mask_slice].bool().reshape(-1)
frame_ids = batch[:,self.ray_frame_id_slice]
rgb, extras = self.render(rays=batch, depth=target_d,lindisp=False,perturb=True,raw_noise_std=self.cfg['raw_noise_std'], get_normals=False)
valid_samples = extras['valid_samples'] #(N_ray,N_samples)
z_vals = extras['z_vals'] # [N_rand, N_samples + N_importance]
sdf = extras['raw'][..., -1]
N_rays,N_samples = sdf.shape[:2]
valid_rays = (valid_samples>0).any(dim=-1).bool().reshape(N_rays) & (batch[:,self.ray_type_slice]==0)
ray_type = batch[:,self.ray_type_slice].reshape(-1)
ray_weights = torch.ones((N_rays), device=rgb.device, dtype=torch.float32)
ray_weights[(frame_ids==0).view(-1)] = self.cfg['first_frame_weight']
ray_weights = ray_weights*valid_rays.view(-1)
sample_weights = ray_weights.view(N_rays,1).expand(-1,N_samples) * valid_samples
img_loss = (((rgb-target_s)**2 * ray_weights.view(-1,1))).mean()
rgb_loss = self.cfg['rgb_weight'] * img_loss
loss = rgb_loss
rgb0_loss = torch.tensor(0)
if 'rgb0' in extras:
img_loss0 = (((extras['rgb0']-target_s)**2 * ray_weights.view(-1,1))).mean()
rgb0_loss = img_loss0*self.cfg['rgb_weight']
loss += rgb0_loss
depth_loss = torch.tensor(0)
depth_loss0 = torch.tensor(0)
if self.cfg['depth_weight']>0:
signs = sdf[:, 1:] * sdf[:, :-1]
mask = signs<0
inds = torch.argmax(mask.float(), axis=1)
inds = inds[..., None]
z_min = torch.gather(z_vals,dim=1,index=inds)
weights = ray_weights * (depth<=self.cfg['far']*self.cfg['sc_factor']) * (mask.any(dim=-1))
depth_loss = ((z_min*weights-depth.view(-1,1)*weights)**2).mean() * self.cfg['depth_weight']
loss = loss+depth_loss
truncation = self.get_truncation()
sample_weights[ray_type==1] = 0
fs_loss, sdf_loss,empty_loss, front_mask,sdf_mask = get_sdf_loss(z_vals, target_d.reshape(-1,1).expand(-1,N_samples), sdf, truncation, self.cfg,return_mask=True, sample_weights=sample_weights, rays_d=batch[:,self.ray_dir_slice])
fs_loss = fs_loss*self.cfg['fs_weight']
empty_loss = empty_loss*self.cfg['empty_weight']
sdf_loss = sdf_loss*self.cfg['trunc_weight']
loss = loss + fs_loss + sdf_loss + empty_loss
fs_rgb_loss = torch.tensor(0)
if self.cfg['fs_rgb_weight']>0:
fs_rgb_loss = ((((torch.sigmoid(extras['raw'][...,:3])-1)*front_mask[...,None])**2) * sample_weights[...,None]).mean()
loss += fs_rgb_loss*self.cfg['fs_rgb_weight']
eikonal_loss = torch.tensor(0)
if self.cfg['eikonal_weight']>0:
nerf_normals = extras['normals']
eikonal_loss = ((torch.norm(nerf_normals[sdf<1], dim=-1)-1)**2).mean() * self.cfg['eikonal_weight']
loss += eikonal_loss
point_cloud_loss = torch.tensor(0)
point_cloud_normal_loss = torch.tensor(0)
reg_features = torch.tensor(0)
if self.models['feature_array'] is not None:
reg_features = self.cfg['feature_reg_weight'] * (self.models['feature_array'].data**2).mean()
loss += reg_features
if self.models['pose_array'] is not None:
pose_array = self.models['pose_array']
pose_reg = self.cfg['pose_reg_weight']*pose_array.data[1:].norm()
loss += pose_reg
variation_loss = torch.tensor(0)
self.optimizer.zero_grad()
self.amp_scaler.scale(loss).backward()
self.amp_scaler.step(self.optimizer)
self.amp_scaler.update()
if self.global_step%10==0 and self.global_step>0:
self.schedule_lr()
if self.global_step%self.cfg['i_weights']==0 and self.global_step>0:
self.save_weights(out_file=os.path.join(self.cfg['save_dir'], f'model_latest.pth'), models=self.models)
if self.global_step % self.cfg['i_img'] == 0 and self.global_step>0:
ids = torch.unique(self.rays[:, self.ray_frame_id_slice]).data.cpu().numpy().astype(int).tolist()
ids.sort()
last = ids[-1]
ids = ids[::max(1,len(ids)//5)]
if last not in ids:
ids.append(last)
canvas = []
for frame_idx in ids:
rgb, depth, ray_mask, gt_rgb, gt_depth, _ = self.render_images(frame_idx)
mask_vis = (rgb*255*0.2 + ray_mask*0.8).astype(np.uint8)
mask_vis = np.clip(mask_vis,0,255)
rgb = np.concatenate((rgb,gt_rgb),axis=1)
far = self.cfg['far']*self.cfg['sc_factor']
gt_depth = np.clip(gt_depth, self.cfg['near']*self.cfg['sc_factor'], far)
depth_vis = np.concatenate((to8b(depth / far), to8b(gt_depth / far)), axis=1)
depth_vis = np.tile(depth_vis[...,None],(1,1,3))
row = np.concatenate((to8b(rgb),depth_vis,mask_vis),axis=1)
canvas.append(row)
canvas = np.concatenate(canvas,axis=0).astype(np.uint8)
dir = f"{self.cfg['save_dir']}/image_step_{self.global_step:07d}.png"
imageio.imwrite(dir,canvas)
if self._run is not None:
self._run.add_artifact(dir)
if self.global_step%self.cfg['i_print']==0:
msg = f"Iter: {self.global_step}, valid_samples: {valid_samples.sum()}/{torch.numel(valid_samples)}, valid_rays: {valid_rays.sum()}/{torch.numel(valid_rays)}, "
metrics = {
'loss':loss.item(),
'rgb_loss':rgb_loss.item(),
'rgb0_loss':rgb0_loss.item(),
'fs_rgb_loss': fs_rgb_loss.item(),
'depth_loss':depth_loss.item(),
'depth_loss0':depth_loss0.item(),
'fs_loss':fs_loss.item(),
'point_cloud_loss': point_cloud_loss.item(),
'point_cloud_normal_loss':point_cloud_normal_loss.item(),
'sdf_loss':sdf_loss.item(),
'eikonal_loss': eikonal_loss.item(),
"variation_loss": variation_loss.item(),
'truncation(meter)': self.get_truncation()/self.cfg['sc_factor'],
}
if self.models['pose_array'] is not None:
metrics['pose_reg'] = pose_reg.item()
if 'feature_array' in self.models:
metrics['reg_features'] = reg_features.item()
for k in metrics.keys():
msg += f"{k}: {metrics[k]:.7f}, "
msg += "\n"
logging.info(msg)
if self._run is not None:
for k in metrics.keys():
self._run.log_scalar(k,metrics[k],self.global_step)
if self.global_step % self.cfg['i_mesh'] == 0 and self.global_step > 0:
with torch.no_grad():
model = self.models['model_fine'] if self.models['model_fine'] is not None else self.models['model']
mesh = self.extract_mesh(isolevel=0, voxel_size=self.cfg['mesh_resolution'])
self.mesh = copy.deepcopy(mesh)
if mesh is not None:
dir = os.path.join(self.cfg['save_dir'], f'step_{self.global_step:07d}_mesh_normalized_space.obj')
mesh.export(dir)
if self._run is not None:
self._run.add_artifact(dir)
dir = os.path.join(self.cfg['save_dir'], f'step_{self.global_step:07d}_mesh_real_world.obj')
if self.models['pose_array'] is not None:
_,offset = get_optimized_poses_in_real_world(self.poses,self.models['pose_array'],translation=self.cfg['translation'],sc_factor=self.cfg['sc_factor'])
else:
offset = np.eye(4)
mesh = mesh_to_real_world(mesh,offset,translation=self.cfg['translation'],sc_factor=self.cfg['sc_factor'])
mesh.export(dir)
if self._run is not None:
self._run.add_artifact(dir)
if self.global_step % self.cfg['i_pose'] == 0 and self.global_step > 0:
if self.models['pose_array'] is not None:
optimized_poses,offset = get_optimized_poses_in_real_world(self.poses,self.models['pose_array'],translation=self.cfg['translation'],sc_factor=self.cfg['sc_factor'])
else:
optimized_poses = self.poses
dir = os.path.join(self.cfg['save_dir'], f'step_{self.global_step:07d}_optimized_poses.txt')
np.savetxt(dir,optimized_poses.reshape(-1,4))
if self._run is not None:
self._run.add_artifact(dir)
def train(self):
set_seed(0)
for iter in range(self.N_iters):
if iter%(self.N_iters//10)==0:
logging.info(f'train progress {iter}/{self.N_iters}')
batch = next(self.data_loader)
self.train_loop(batch.cuda())
self.global_step += 1
@torch.no_grad()
def sample_rays_uniform_occupied_voxels(self,rays_d,depths_in_out,lindisp=False,perturb=False, depths=None, N_samples=None):
'''We first connect the discontinuous boxes for each ray and treat it as uniform sample, then we disconnect into correct boxes
@rays_d: (N_ray,3)
@depths_in_out: Padded tensor each has (N_ray,N_intersect,2) tensor, the time travel of each ray
'''
N_rays = rays_d.shape[0]
N_intersect = depths_in_out.shape[1]
dirs = rays_d/rays_d.norm(dim=-1,keepdim=True)
########### Convert the time to Z
z_in_out = depths_in_out.cuda()*torch.abs(dirs[...,2]).reshape(N_rays,1,1).cuda()
if depths is not None:
depths = depths.reshape(-1,1)
trunc = self.get_truncation()
valid = (depths>=self.cfg['near']*self.cfg['sc_factor']) & (depths<=self.cfg['far']*self.cfg['sc_factor']).expand(-1,N_intersect)
valid = valid & (z_in_out>0).all(dim=-1) #(N_ray, N_intersect)
z_in_out[valid] = torch.clip(z_in_out[valid],
min=torch.zeros_like(z_in_out[valid]),
max=torch.ones_like(z_in_out[valid])*(depths.reshape(-1,1,1).expand(-1,N_intersect,2)[valid]+trunc))
depths_lens = z_in_out[:,:,1]-z_in_out[:,:,0] #(N_ray,N_intersect)
z_vals_continous = sample_rays_uniform(N_samples,torch.zeros((N_rays,1),device=z_in_out.device).reshape(-1,1),depths_lens.sum(dim=-1).reshape(-1,1),lindisp=lindisp,perturb=perturb) #(N_ray,N_sample)
############# Option2 mycuda extension
N_samples = z_vals_continous.shape[1]
z_vals = torch.zeros((N_rays,N_samples), dtype=torch.float, device=rays_d.device)
z_vals = common.sampleRaysUniformOccupiedVoxels(z_in_out.contiguous(),z_vals_continous.contiguous(), z_vals)
z_vals = z_vals.float().to(rays_d.device) #(N_ray,N_sample)
return z_vals,z_vals_continous
def render_rays(self,ray_batch,retraw=True,lindisp=False,perturb=False,raw_noise_std=0.,depth=None, get_normals=False, tf=None):
"""Volumetric rendering.
Args:
ray_batch: array of shape [batch_size, ...]. All information necessary
for sampling along a ray, including: ray origin, ray direction, min
dist, max dist, and unit-magnitude viewing direction, frame_ids.
model: function. Model for predicting RGB and density at each point
in space.
N_samples: int. Number of different times to sample along each ray.
retraw: bool. If True, include model's raw, unprocessed predictions.
lindisp: bool. If True, sample linearly in inverse depth rather than in depth.
perturb: float, 0 or 1. If non-zero, each ray is sampled at stratified
random points in time.
N_importance: int. Number of additional times to sample along each ray.
These samples are only passed to model_fine.
model_fine: "fine" network with same spec as model.
raw_noise_std: ...
verbose: bool. If True, print more debugging info.
@depth: depth (from depth image) values of the ray (N_ray,1)
@tf: (N_ray,4,4) glcam in ob, normalized space
Returns:
rgb_map: [num_rays, 3]. Estimated RGB color of a ray. Comes from fine model.
disp_map: [num_rays]. Disparity map. 1 / depth.
acc_map: [num_rays]. Accumulated opacity along each ray. Comes from fine model.
raw: [num_rays, num_samples, 4]. Raw predictions from model.
rgb0: See rgb_map. Output for coarse model.
disp0: See disp_map. Output for coarse model.
acc0: See acc_map. Output for coarse model.
z_std: [num_rays]. Standard deviation of distances along ray for each
sample.
"""
N_rays = ray_batch.shape[0]
rays_d = ray_batch[:,self.ray_dir_slice]
rays_o = torch.zeros_like(rays_d)
viewdirs = rays_d/rays_d.norm(dim=-1,keepdim=True)
frame_ids = ray_batch[:,self.ray_frame_id_slice].long()
tf = self.c2w_array[frame_ids]
if self.models['pose_array'] is not None:
tf = self.models['pose_array'].get_matrices(frame_ids)@tf
rays_o_w = transform_pts(rays_o,tf)
viewdirs_w = (tf[:,:3,:3]@viewdirs[:,None].permute(0,2,1))[:,:3,0]
voxel_size = self.cfg['octree_raytracing_voxel_size']*self.cfg['sc_factor']
level = int(np.floor(np.log2(2.0/voxel_size)))
near,far,_,depths_in_out = self.octree_m.ray_trace(rays_o_w,viewdirs_w,level=level,debug=0)
z_vals,_ = self.sample_rays_uniform_occupied_voxels(rays_d=viewdirs,depths_in_out=depths_in_out,lindisp=lindisp,perturb=perturb, depths=depth, N_samples=self.cfg['N_samples'])
if self.cfg['N_samples_around_depth']>0 and depth is not None: #!NOTE only fine when depths are all valid
valid_depth_mask = (depth>=self.cfg['near']*self.cfg['sc_factor']) & (depth<=self.cfg['far']*self.cfg['sc_factor'])
valid_depth_mask = valid_depth_mask.reshape(-1)
trunc = self.get_truncation()
near_depth = depth[valid_depth_mask]-trunc
far_depth = depth[valid_depth_mask]+trunc*self.cfg['neg_trunc_ratio']
z_vals_around_depth = torch.zeros((N_rays,self.cfg['N_samples_around_depth']), device=ray_batch.device).float()
# if torch.sum(inside_mask)>0:
z_vals_around_depth[valid_depth_mask] = sample_rays_uniform(self.cfg['N_samples_around_depth'],near_depth.reshape(-1,1),far_depth.reshape(-1,1),lindisp=lindisp,perturb=perturb)
invalid_depth_mask = valid_depth_mask==0
if invalid_depth_mask.any() and self.cfg['use_octree']:
z_vals_invalid,_ = self.sample_rays_uniform_occupied_voxels(rays_d=viewdirs[invalid_depth_mask],depths_in_out=depths_in_out[invalid_depth_mask],lindisp=lindisp,perturb=perturb, depths=None, N_samples=self.cfg['N_samples_around_depth'])
z_vals_around_depth[invalid_depth_mask] = z_vals_invalid
else:
z_vals_around_depth[invalid_depth_mask] = sample_rays_uniform(self.cfg['N_samples_around_depth'],near[invalid_depth_mask].reshape(-1,1),far[invalid_depth_mask].reshape(-1,1),lindisp=lindisp,perturb=perturb)
z_vals = torch.cat((z_vals,z_vals_around_depth), dim=-1)
valid_samples = torch.ones(z_vals.shape, dtype=torch.bool, device=ray_batch.device) # During pose update if ray out of box, it becomes invalid
pts = rays_o[...,None,:] + rays_d[...,None,:] * z_vals[...,:,None] # [N_rays, N_samples, 3]
deformation = None
raw,normals,valid_samples = self.run_network(pts, viewdirs, frame_ids, tf=tf, valid_samples=valid_samples, get_normals=get_normals) # [N_rays, N_samples, 4]
rgb_map, weights = self.raw2outputs(raw, z_vals, rays_d, raw_noise_std=raw_noise_std, valid_samples=valid_samples, depth=depth)
if self.cfg['N_importance'] > 0:
rgb_map_0 = rgb_map
for iter in range(self.cfg['N_importance_iter']):
z_vals_mid = .5 * (z_vals[...,1:] + z_vals[...,:-1])
z_samples = sample_pdf(z_vals_mid, weights[...,1:-1], self.cfg['N_importance'], det=(perturb==0.))
z_samples = z_samples.detach()
valid_samples_importance = torch.ones(z_samples.shape, dtype=torch.bool).to(z_vals.device)
valid_samples_importance[torch.all(valid_samples==0, dim=-1).reshape(-1)] = 0
if self.models['model_fine'] is not None and self.models['model_fine']!=self.models['model']:
z_vals, _ = torch.sort(torch.cat([z_vals, z_samples], -1), -1)
pts = rays_o[...,None,:] + rays_d[...,None,:] * z_vals[...,:,None] # [N_rays, N_samples + self.cfg['N_importance'], 3]
raw, normals,valid_samples = self.run_network(pts, viewdirs, frame_ids, tf=tf, valid_samples=valid_samples, get_normals=False)
else:
pts = rays_o[..., None, :] + rays_d[..., None, :] * z_samples[..., :, None] # [N_rays, N_samples + self.cfg['N_importance'], 3]
raw_fine,valid_samples_importance = self.run_network(pts, viewdirs, frame_ids, tf=tf, valid_samples=valid_samples_importance, get_normals=False)
z_vals = torch.cat([z_vals, z_samples], -1) #(N_ray, N_sample)
indices = torch.argsort(z_vals, dim=-1)
z_vals = torch.gather(z_vals,dim=1,index=indices)
raw = torch.gather(torch.cat([raw, raw_fine], dim=1), dim=1, index=indices[...,None].expand(-1,-1,raw.shape[-1]))
valid_samples = torch.cat((valid_samples,valid_samples_importance), dim=-1)
rgb_map, weights = self.raw2outputs(raw, z_vals, rays_d, raw_noise_std=raw_noise_std,valid_samples=valid_samples)
ret = {'rgb_map' : rgb_map, 'valid_samples':valid_samples, 'weights':weights, 'z_vals':z_vals}
if retraw:
ret['raw'] = raw
if normals is not None:
ret['normals'] = normals
if deformation is not None:
ret['deformation'] = deformation
if self.cfg['N_importance'] > 0:
ret['rgb0'] = rgb_map_0
return ret
def raw2outputs(self, raw, z_vals, rays_d, raw_noise_std=0, valid_samples=None, depth=None):
"""Transforms model's predictions to semantically meaningful values.
Args:
raw: [num_rays, num_samples along ray, 4]. Prediction from model.
z_vals: [num_rays, num_samples along ray]. Integration time.
rays_d: [num_rays, 3]. Direction of each ray.
Returns:
rgb_map: [num_rays, 3]. Estimated RGB color of a ray.
disp_map: [num_rays]. Disparity map. Inverse of depth map.
acc_map: [num_rays]. Sum of weights along each ray.
weights: [num_rays, num_samples]. Weights assigned to each sampled color.
depth_map: [num_rays]. Estimated distance to object.
"""
truncation = self.get_truncation()
if depth is not None:
depth = depth.view(-1,1)
if valid_samples is None:
valid_samples = torch.ones(z_vals.shape, dtype=torch.bool).to(z_vals.device)
def sdf2weights(sdf):
sdf_from_depth = (depth.view(-1,1)-z_vals)/truncation
weights = torch.sigmoid(sdf_from_depth*self.cfg['sdf_lambda']) * torch.sigmoid(-sdf_from_depth*self.cfg['sdf_lambda']) # This not work well
invalid = (depth>self.cfg['far']*self.cfg['sc_factor']).reshape(-1)
mask = (z_vals-depth<=truncation*self.cfg['neg_trunc_ratio']) & (z_vals-depth>=-truncation)
weights[~invalid] = weights[~invalid] * mask[~invalid]
weights[invalid] = 0
return weights / (weights.sum(dim=-1,keepdim=True) + 1e-10)
rgb = torch.sigmoid(raw[...,:3]) # [N_rays, N_samples, 3]
weights = sdf2weights(raw[..., 3])
weights[valid_samples==0] = 0
rgb_map = torch.sum(weights[...,None] * rgb, -2) # [N_rays, 3]
return rgb_map, weights
def render(self, rays, depth=None,lindisp=False,perturb=False,raw_noise_std=0.0, get_normals=False):
"""Render rays
Args:
H: int. Height of image in pixels.
W: int. Width of image in pixels.
K: float. Focal length of pinhole camera.
chunk: int. Maximum number of rays to process simultaneously. Used to
control maximum memory usage. Does not affect final results.
rays: array of shape [batch_size, 6]. Ray origin and direction for
each example in batch.
ndc: bool. If True, represent ray origin, direction in NDC coordinates. Only true for llff data
near: float or array of shape [batch_size]. Nearest distance for a ray.
far: float or array of shape [batch_size]. Farthest distance for a ray.
@depth: depth values (N_ray,1)
Returns:
rgb_map: [batch_size, 3]. Predicted RGB values for rays.
disp_map: [batch_size]. Disparity map. Inverse of depth.
acc_map: [batch_size]. Accumulated opacity (alpha) along a ray.
extras: dict with everything.
"""
all_ret = self.batchify_rays(rays,depth=depth,lindisp=lindisp,perturb=perturb,raw_noise_std=raw_noise_std, get_normals=get_normals)
k_extract = ['rgb_map']
ret_list = [all_ret[k] for k in k_extract]
ret_dict = {k : all_ret[k] for k in all_ret if k not in k_extract}
return ret_list + [ret_dict]
def batchify_rays(self,rays_flat, depth=None,lindisp=False,perturb=False,raw_noise_std=0.0, get_normals=False):
"""Render rays in smaller minibatches to avoid OOM.
"""
all_ret = {}
chunk = self.cfg['chunk']
for i in range(0, rays_flat.shape[0], chunk):
if depth is not None:
cur_depth = depth[i:i+chunk]
else:
cur_depth = None
ret = self.render_rays(rays_flat[i:i+chunk],depth=cur_depth,lindisp=lindisp,perturb=perturb,raw_noise_std=raw_noise_std, get_normals=get_normals)
for k in ret:
if ret[k] is None:
continue
if k not in all_ret:
all_ret[k] = []
all_ret[k].append(ret[k])
all_ret = {k : torch.cat(all_ret[k], 0) for k in all_ret}
return all_ret
def run_network(self, inputs, viewdirs, frame_ids=None, tf=None, latent_code=None, valid_samples=None, get_normals=False):
"""Prepares inputs and applies network 'fn'.
@inputs: (N_ray,N_sample,3) sampled points on rays in GL camera's frame
@viewdirs: (N_ray,3) unit length vector in camera frame, z-axis backward
@frame_ids: (N_ray)
@tf: (N_ray,4,4)
@latent_code: (N_ray, D)
"""
N_ray,N_sample = inputs.shape[:2]
if valid_samples is None:
valid_samples = torch.ones((N_ray,N_sample), dtype=torch.bool, device=inputs.device)
inputs_flat = torch.reshape(inputs, [-1, inputs.shape[-1]])
tf_flat = tf[:,None].expand(-1,N_sample,-1,-1).reshape(-1,4,4)
inputs_flat = transform_pts(inputs_flat, tf_flat)
valid_samples = valid_samples.bool() & (torch.abs(inputs_flat)<=1).all(dim=-1).view(N_ray,N_sample).bool()
embedded = torch.zeros((inputs_flat.shape[0],self.models['embed_fn'].out_dim), device=inputs_flat.device)
if valid_samples is None:
valid_samples = torch.ones((N_ray,N_sample), dtype=torch.bool, device=inputs_flat.device)
if get_normals:
if inputs_flat.requires_grad==False:
inputs_flat.requires_grad = True
with torch.cuda.amp.autocast(enabled=self.cfg['amp']):
if self.cfg['i_embed'] in [3]:
embedded[valid_samples.reshape(-1)], valid_samples_embed = self.models['embed_fn'](inputs_flat[valid_samples.reshape(-1)])
valid_samples = valid_samples.reshape(-1)
prev_valid_ids = valid_samples.nonzero().reshape(-1)
bad_ids = prev_valid_ids[valid_samples_embed==0]
new_valid_ids = torch.ones((N_ray*N_sample),device=inputs.device).bool()
new_valid_ids[bad_ids] = 0
valid_samples = valid_samples & new_valid_ids
valid_samples = valid_samples.reshape(N_ray,N_sample).bool()
else:
embedded[valid_samples.reshape(-1)] = self.models['embed_fn'](inputs_flat[valid_samples.reshape(-1)]).to(embedded.dtype)
embedded = embedded.float()
# Add latent code
if self.models['feature_array'] is not None:
if latent_code is None:
frame_features = self.models['feature_array'](frame_ids)
D = frame_features.shape[-1]
frame_features = frame_features[:,None].expand(-1,N_sample,-1).reshape(-1,D)
else:
D = latent_code.shape[-1]
frame_features = latent_code[:,None].expand(N_ray,N_sample,latent_code.shape[-1]).reshape(-1,D)
embedded = torch.cat([embedded, frame_features], -1)
# Add view directions
if self.models['embeddirs_fn'] is not None:
input_dirs = (tf[..., :3, :3]@viewdirs[...,None])[...,0] #(N_ray,3)
embedded_dirs = self.models['embeddirs_fn'](input_dirs)
tmp = embedded_dirs.shape[1:]
embedded_dirs_flat = embedded_dirs[:,None].expand(-1,N_sample,*tmp).reshape(-1,*tmp)
embedded = torch.cat([embedded, embedded_dirs_flat], -1)
outputs_flat = []
with torch.cuda.amp.autocast(enabled=self.cfg['amp']):
chunk = self.cfg['netchunk']
for i in range(0,embedded.shape[0],chunk):
out = self.models['model'](embedded[i:i+chunk])
outputs_flat.append(out)
outputs_flat = torch.cat(outputs_flat, dim=0).float()
outputs = torch.reshape(outputs_flat, list(inputs.shape[:-1]) + [outputs_flat.shape[-1]]).float()
normals = None
if get_normals:
sdf = outputs[...,-1]
d_output = torch.zeros(sdf.shape, device=sdf.device)
normals = torch.autograd.grad(outputs=sdf,inputs=inputs_flat,grad_outputs=d_output,create_graph=False,retain_graph=True,only_inputs=True,allow_unused=True)[0]
normals = normals.reshape(N_ray,N_sample,3)
return outputs,normals,valid_samples
def run_network_density(self, inputs, get_normals=False):
"""Directly query the network w/o pose transformations or deformations (inputs are already in normalized [-1,1]); Particularly used for mesh extraction
@inputs: (N,3) sampled points on rays in GL camera's frame
"""
inputs_flat = torch.reshape(inputs, [-1, inputs.shape[-1]])
inputs_flat = torch.clip(inputs_flat,-1,1)
valid_samples = torch.ones((len(inputs_flat)),device=inputs.device).bool()
if not inputs_flat.requires_grad:
inputs_flat.requires_grad = True
with torch.cuda.amp.autocast(enabled=self.cfg['amp']):
if self.cfg['i_embed'] in [3]:
embedded, valid_samples_embed = self.models['embed_fn'](inputs_flat)
valid_samples = valid_samples.reshape(-1)
prev_valid_ids = valid_samples.nonzero().reshape(-1)
bad_ids = prev_valid_ids[valid_samples_embed==0]
new_valid_ids = torch.ones((len(inputs_flat)),device=inputs.device).bool()
new_valid_ids[bad_ids] = 0
valid_samples = valid_samples & new_valid_ids
else:
embedded = self.models['embed_fn'](inputs_flat)
embedded = embedded.float()
input_ch = embedded.shape[-1]
outputs_flat = []
with torch.cuda.amp.autocast(enabled=self.cfg['amp']):
chunk = self.cfg['netchunk']
for i in range(0,embedded.shape[0],chunk):
alpha = self.models['model'].forward_sdf(embedded[i:i+chunk]) #(N,1)
outputs_flat.append(alpha.reshape(-1,1))
outputs_flat = torch.cat(outputs_flat,dim=0).float()
outputs = torch.reshape(outputs_flat, list(inputs.shape[:-1]) + [outputs_flat.shape[-1]])
if get_normals:
d_output = torch.ones_like(outputs, requires_grad=False, device=outputs.device)
normal = torch.autograd.grad(outputs=outputs,inputs=inputs_flat,grad_outputs=d_output,create_graph=False,retain_graph=True,only_inputs=True,allow_unused=True)[0]
outputs = torch.cat((outputs, normal), dim=-1)
return outputs,valid_samples
@torch.no_grad()
def extract_mesh(self, level=None, voxel_size=0.003, isolevel=0.0, return_sigma=False):
voxel_size *= self.cfg['sc_factor'] # in "network space"
bounds = np.array(self.cfg['bounding_box']).reshape(2,3)
x_min, x_max = bounds[0,0], bounds[1,0]
y_min, y_max = bounds[0,1], bounds[1,1]
z_min, z_max = bounds[0,2], bounds[1,2]
tx = np.arange(x_min+0.5*voxel_size, x_max, voxel_size)
ty = np.arange(y_min+0.5*voxel_size, y_max, voxel_size)
tz = np.arange(z_min+0.5*voxel_size, z_max, voxel_size)
N = len(tx)
query_pts = torch.tensor(np.stack(np.meshgrid(tx, ty, tz, indexing='ij'), -1).astype(np.float32).reshape(-1,3)).float().cuda()
if self.octree_m is not None:
vox_size = self.cfg['octree_raytracing_voxel_size']*self.cfg['sc_factor']
level = int(np.floor(np.log2(2.0/vox_size)))
center_ids = self.octree_m.get_center_ids(query_pts, level)
valid = center_ids>=0
else:
valid = torch.ones(len(query_pts), dtype=bool).cuda()
logging.info(f'query_pts:{query_pts.shape}, valid:{valid.sum()}')
flat = query_pts[valid]
sigma = []
chunk = self.cfg['netchunk']
for i in range(0,flat.shape[0],chunk):
inputs = flat[i:i+chunk]
with torch.no_grad():
outputs,valid_samples = self.run_network_density(inputs=inputs)
sigma.append(outputs)
sigma = torch.cat(sigma, dim=0)
sigma_ = torch.ones((N**3)).float().cuda()
sigma_[valid] = sigma.reshape(-1)
sigma = sigma_.reshape(N,N,N).data.cpu().numpy()
logging.info('Running Marching Cubes')
from skimage import measure
try:
vertices, triangles, normals, values = measure.marching_cubes(sigma, isolevel)
except Exception as e:
logging.info(f"ERROR Marching Cubes {e}")
return None
logging.info(f'done V:{vertices.shape}, F:{triangles.shape}')
voxel_size_ndc = np.array([tx[-1] - tx[0], ty[-1] - ty[0], tz[-1] - tz[0]]) / np.array([[tx.shape[0] - 1, ty.shape[0] - 1, tz.shape[0] - 1]])
offset = np.array([tx[0], ty[0], tz[0]])
vertices[:, :3] = voxel_size_ndc.reshape(1,3) * vertices[:, :3] + offset.reshape(1,3)
mesh = trimesh.Trimesh(vertices, triangles, process=False)
if return_sigma:
return mesh,sigma,query_pts
return mesh
def mesh_texture_from_train_images(self, mesh, rgbs_raw, tex_res=1024):
'''
@rgbs_raw: raw complete image that was trained on, no black holes
@mesh: in normalized space
'''
assert len(self.images)==len(rgbs_raw)
frame_ids = torch.arange(len(self.images)).long().cuda()
tf = self.c2w_array[frame_ids]
if self.models['pose_array'] is not None:
tf = self.models['pose_array'].get_matrices(frame_ids)@tf
tf = tf.data.cpu().numpy()
from offscreen_renderer import ModelRendererOffscreen
tex_image = torch.zeros((tex_res,tex_res,3)).cuda().float()
weight_tex_image = torch.zeros(tex_image.shape[:-1]).cuda().float()
mesh.merge_vertices()
mesh.remove_duplicate_faces()
mesh = mesh.unwrap()
H,W = tex_image.shape[:2]
uvs_tex = (mesh.visual.uv*np.array([W-1,H-1]).reshape(1,2)) #(n_V,2)
renderer = ModelRendererOffscreen(cam_K=self.K, H=self.H, W=self.W, zfar=self.cfg['far']*self.cfg['sc_factor'])
vertices_cuda = torch.from_numpy(mesh.vertices).float().cuda()
faces_cuda = torch.from_numpy(mesh.faces).long().cuda()
face_vertices = torch.zeros((len(faces_cuda),3,3))
for i in range(3):
face_vertices[:,i] = vertices_cuda[faces_cuda[:,i]]
all_tri_list= {key: [] for key in range(mesh.triangles.shape[0])}
for i in range(len(rgbs_raw)):
cvcam_in_ob = tf[i]@np.linalg.inv(glcam_in_cvcam)
_, render_depth = renderer.render(mesh=mesh, ob_in_cvcam=np.linalg.inv(cvcam_in_ob))
xyz_map = depth2xyzmap(render_depth, self.K)
mask = self.masks[i].reshape(self.H,self.W).astype(bool)
valid = (render_depth.reshape(self.H,self.W)>=0.1*self.cfg['sc_factor']) & (mask)
pts = xyz_map[valid].reshape(-1,3)
pts = transform_pts(pts, cvcam_in_ob)
ray_colors = rgbs_raw[i][valid].reshape(-1,3)
locations, distance, index_tri = trimesh.proximity.closest_point(mesh, pts)
normals = mesh.face_normals[index_tri]
for ind_tri, each_tri in enumerate(index_tri):
rays_o = np.zeros(3)
pts = transform_pts(rays_o, cvcam_in_ob) #transform to world space
rays_d = locations[ind_tri]-pts
rays_d /= np.linalg.norm(rays_d)
dot_product = np.dot(-rays_d, normals[ind_tri])
angle_radians = np.arccos(dot_product)
angle_degrees = np.degrees(angle_radians)
all_tri_list[each_tri].extend([[i,angle_degrees]])
_CHOOSE_TOP_N = 4
all_triangles_dict={}
for k,v in all_tri_list.items():
if(v):
v.sort(key=lambda x: x[1])
tep = [i[0] for i in v[:_CHOOSE_TOP_N]]
all_triangles_dict[k]=set(list(tep))
all_tri_visited= {key: 0 for key in range(mesh.triangles.shape[0])}
logging.info(f"Texture: Texture map computation")
for i in range(len(rgbs_raw)):
print(f'project train_images {i}/{len(rgbs_raw)}')
cvcam_in_ob = tf[i]@np.linalg.inv(glcam_in_cvcam)
_, render_depth = renderer.render(mesh=mesh, ob_in_cvcam=np.linalg.inv(cvcam_in_ob))
xyz_map = depth2xyzmap(render_depth, self.K)
mask = self.masks[i].reshape(self.H,self.W).astype(bool)
valid = (render_depth.reshape(self.H,self.W)>=0.1*self.cfg['sc_factor']) & (mask)
pts = xyz_map[valid].reshape(-1,3)
pts = transform_pts(pts, cvcam_in_ob)
ray_colors = rgbs_raw[i][valid].reshape(-1,3)
locations, distance, index_tri = trimesh.proximity.closest_point(mesh, pts)
normals = mesh.face_normals[index_tri]
rays_o = np.zeros((len(normals),3))
rays_o = transform_pts(rays_o,cvcam_in_ob)
rays_d = locations-rays_o
rays_d /= np.linalg.norm(rays_d,axis=-1).reshape(-1,1)
dots = (normals*(-rays_d)).sum(axis=-1)
ray_weights = np.ones((len(rays_o)))
bool_weights=torch.zeros(len(locations), dtype=torch.bool, device='cuda')
count = 0
for jj, trtind__ in enumerate(index_tri):
if(i in all_triangles_dict[trtind__] ):
bool_weights[jj]=1
all_tri_visited[trtind__]=1
count +=1
uvs = torch.zeros((len(locations),2)).cuda().float()
common.rayColorToTextureImageCUDA(torch.from_numpy(mesh.faces).cuda().long(), torch.from_numpy(mesh.vertices).cuda().float(), torch.from_numpy(locations).cuda().float(), torch.from_numpy(index_tri).cuda().long(), torch.from_numpy(uvs_tex).cuda().float(), uvs)
uvs = torch.round(uvs).long()
uvs_flat = uvs[:,1]*(W-1) + uvs[:,0]
uvs_flat_unique, inverse_ids, cnts = torch.unique(uvs_flat, return_counts=True, return_inverse=True)
perm = torch.arange(inverse_ids.size(0)).cuda()
inverse_ids, perm = inverse_ids.flip([0]), perm.flip([0])
unique_ids = inverse_ids.new_empty(uvs_flat_unique.size(0)).scatter_(0, inverse_ids, perm)
uvs_unique = torch.stack((uvs_flat_unique%(W-1), uvs_flat_unique//(W-1)), dim=-1).reshape(-1,2)
cur_weights= bool_weights[unique_ids].cuda().float()
tex_image[uvs_unique[:,1],uvs_unique[:,0]] += torch.from_numpy(ray_colors).cuda().float()[unique_ids]*cur_weights.reshape(-1,1)
weight_tex_image[uvs_unique[:,1], uvs_unique[:,0]] += cur_weights
tex_image = tex_image/weight_tex_image[...,None]
tex_image = tex_image.data.cpu().numpy()
tex_image = np.clip(tex_image,0,255).astype(np.uint8)
tex_image = tex_image[::-1].copy()
new_texture = texture_map_interpolation(tex_image)
mesh.visual = trimesh.visual.texture.TextureVisuals(uv=mesh.visual.uv,image=Image.fromarray(new_texture))
return mesh