Files
hand-motion-pipeline/third_party/FoundationPose/bundlesdf/nerf_helpers.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

476 lines
17 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 torch,pdb,os,sys
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
code_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(f'{code_dir}/../')
from Utils import *
from pytorch3d.transforms import so3_log_map,so3_exp_map,se3_exp_map
img2mse = lambda x, y : torch.mean((x - y) ** 2)
img2mae = lambda x, y: (torch.abs(x - y)).mean()
mse2psnr = lambda x : -10. * torch.log(x) / torch.log(torch.Tensor([10.]))
to8b = lambda x : (255*np.clip(x,0,1)).astype(np.uint8)
class FeatureArray(nn.Module):
"""
Per-frame corrective latent code.
"""
def __init__(self, num_frames, num_channels):
super().__init__()
self.num_frames = num_frames
self.num_channels = num_channels
self.data = nn.parameter.Parameter(torch.normal(0,1,size=[num_frames, num_channels]).float(), requires_grad=True)
self.register_parameter('data',self.data)
def __call__(self, ids):
return self.data[ids]
class PoseArray(nn.Module):
def __init__(self, num_frames,max_trans,max_rot):
super().__init__()
self.num_frames = num_frames
self.max_trans = max_trans
self.max_rot = max_rot
self.data = nn.parameter.Parameter(torch.zeros([num_frames, 6]).float(), requires_grad=True)
self.register_parameter('data',self.data)
def get_matrices(self,ids):
if not torch.is_tensor(ids):
ids = torch.tensor(ids).long()
theta = torch.tanh(self.data)
trans = theta[:,:3] * self.max_trans
rot = theta[:,3:6] * self.max_rot/180.0*np.pi
Ts_data = se3_exp_map(torch.cat((trans,rot),dim=-1)).permute(0,2,1)
Ts = torch.eye(4, device=self.data.device).reshape(1,4,4).repeat(len(ids),1,1)
mask = ids!=0
Ts[mask] = Ts_data[ids[mask]]
return Ts
class SHEncoder(nn.Module):
'''Spherical encoding
'''
def __init__(self, input_dim=3, degree=4):
super().__init__()
self.input_dim = input_dim
self.degree = degree
assert self.input_dim == 3
assert self.degree >= 1 and self.degree <= 5
self.out_dim = degree ** 2
self.C0 = 0.28209479177387814
self.C1 = 0.4886025119029199
self.C2 = [
1.0925484305920792,
-1.0925484305920792,
0.31539156525252005,
-1.0925484305920792,
0.5462742152960396
]
self.C3 = [
-0.5900435899266435,
2.890611442640554,
-0.4570457994644658,
0.3731763325901154,
-0.4570457994644658,
1.445305721320277,
-0.5900435899266435
]
self.C4 = [
2.5033429417967046,
-1.7701307697799304,
0.9461746957575601,
-0.6690465435572892,
0.10578554691520431,
-0.6690465435572892,
0.47308734787878004,
-1.7701307697799304,
0.6258357354491761
]
def forward(self, input, **kwargs):
result = torch.empty((*input.shape[:-1], self.out_dim), dtype=input.dtype, device=input.device)
x, y, z = input.unbind(-1)
result[..., 0] = self.C0
if self.degree > 1:
result[..., 1] = -self.C1 * y
result[..., 2] = self.C1 * z
result[..., 3] = -self.C1 * x
if self.degree > 2:
xx, yy, zz = x * x, y * y, z * z
xy, yz, xz = x * y, y * z, x * z
result[..., 4] = self.C2[0] * xy
result[..., 5] = self.C2[1] * yz
result[..., 6] = self.C2[2] * (2.0 * zz - xx - yy)
#result[..., 6] = self.C2[2] * (3.0 * zz - 1) # xx + yy + zz == 1, but this will lead to different backward gradients, interesting...
result[..., 7] = self.C2[3] * xz
result[..., 8] = self.C2[4] * (xx - yy)
if self.degree > 3:
result[..., 9] = self.C3[0] * y * (3 * xx - yy)
result[..., 10] = self.C3[1] * xy * z
result[..., 11] = self.C3[2] * y * (4 * zz - xx - yy)
result[..., 12] = self.C3[3] * z * (2 * zz - 3 * xx - 3 * yy)
result[..., 13] = self.C3[4] * x * (4 * zz - xx - yy)
result[..., 14] = self.C3[5] * z * (xx - yy)
result[..., 15] = self.C3[6] * x * (xx - 3 * yy)
if self.degree > 4:
result[..., 16] = self.C4[0] * xy * (xx - yy)
result[..., 17] = self.C4[1] * yz * (3 * xx - yy)
result[..., 18] = self.C4[2] * xy * (7 * zz - 1)
result[..., 19] = self.C4[3] * yz * (7 * zz - 3)
result[..., 20] = self.C4[4] * (zz * (35 * zz - 30) + 3)
result[..., 21] = self.C4[5] * xz * (7 * zz - 3)
result[..., 22] = self.C4[6] * (xx - yy) * (7 * zz - 1)
result[..., 23] = self.C4[7] * xz * (xx - 3 * yy)
result[..., 24] = self.C4[8] * (xx * (xx - 3 * yy) - yy * (3 * xx - yy))
return result
class Embedder(nn.Module):
def __init__(self, **kwargs):
super().__init__()
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 forward(self, inputs):
return torch.cat([fn(inputs) for fn in self.embed_fns], -1)
def get_embedder(multires, cfg, i=0, octree_m=None):
if i == -1:
return nn.Identity(), 3
elif i==0:
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],
}
embed = Embedder(**embed_kwargs)
out_dim = embed.out_dim
elif i==1:
from mycuda.torch_ngp_grid_encoder.grid import GridEncoder
embed = GridEncoder(input_dim=3, n_levels=cfg['num_levels'], log2_hashmap_size=cfg['log2_hashmap_size'], desired_resolution=cfg['finest_res'], base_resolution=cfg['base_res'], level_dim=cfg['feature_grid_dim'])
print(embed)
out_dim = embed.out_dim
elif i==2:
embed = SHEncoder(degree=cfg['multires_views'])
out_dim = embed.out_dim
return embed, out_dim
def mesh_to_real_world(mesh,pose_offset,translation,sc_factor):
'''
@pose_offset: optimized delta pose of the first frame. Usually it's identity
'''
mesh.vertices = mesh.vertices/sc_factor - np.array(translation).reshape(1,3)
mesh.apply_transform(pose_offset)
return mesh
def get_optimized_poses_in_real_world(poses_normalized, pose_array, sc_factor, translation):
'''
@poses_normalized: np array, cam_in_ob (opengl convention), normalized to [-1,1] and centered
@pose_array: PoseArray, delta poses
Return:
cam_in_ob, real-world unit, opencv convention
'''
original_poses = poses_normalized.copy()
original_poses[:, :3, 3] /= sc_factor # To true world scale
original_poses[:, :3, 3] -= translation
# Apply pose transformation
tf = pose_array.get_matrices(np.arange(len(poses_normalized))).reshape(-1,4,4).data.cpu().numpy()
optimized_poses = tf@poses_normalized
optimized_poses = np.array(optimized_poses).astype(np.float32)
optimized_poses[:, :3, 3] /= sc_factor
optimized_poses[:, :3, 3] -= translation
original_init_ob_in_cam = optimized_poses[0].copy()
offset = np.linalg.inv(original_init_ob_in_cam)@original_poses[0]
for i in range(len(optimized_poses)):
new_ob_in_cam = optimized_poses[i]@offset
optimized_poses[i] = new_ob_in_cam
optimized_poses[i] = optimized_poses[i]@glcam_in_cvcam
return optimized_poses,offset
def preprocess_data(rgbs,depths,masks,normal_maps,poses,sc_factor,translation):
'''
@rgbs: np array (N,H,W,3)
@depths: (N,H,W)
@masks: (N,H,W)
@normal_maps: (N,H,W,3)
@poses: (N,4,4)
'''
depths[depths<0.001] = BAD_DEPTH
if masks is not None:
rgbs[masks==0] = BAD_COLOR
depths[masks==0] = BAD_DEPTH
if normal_maps is not None:
normal_maps[...,[1,2]] *= -1 # To OpenGL
normal_maps[masks==0] = 0
masks = masks[...,None]
rgbs = (rgbs / 255.0).astype(np.float32)
depths *= sc_factor
depths = depths[...,None]
poses[:, :3, 3] += translation
poses[:, :3, 3] *= sc_factor
return rgbs,depths,masks,normal_maps,poses
class NeRFSmall(nn.Module):
def __init__(self,num_layers=3,hidden_dim=64,geo_feat_dim=15,num_layers_color=4,hidden_dim_color=64,input_ch=3, input_ch_views=3):
super(NeRFSmall, self).__init__()
self.input_ch = input_ch
self.input_ch_views = input_ch_views
# sigma network
self.num_layers = num_layers
self.hidden_dim = hidden_dim
self.geo_feat_dim = geo_feat_dim
sigma_net = []
for l in range(num_layers):
if l == 0:
in_dim = self.input_ch
else:
in_dim = hidden_dim
if l == num_layers - 1:
out_dim = 1 + self.geo_feat_dim # 1 sigma + 15 SH features for color
else:
out_dim = hidden_dim
sigma_net.append(nn.Linear(in_dim, out_dim, bias=True))
if l!=num_layers-1:
sigma_net.append(nn.ReLU(inplace=True))
self.sigma_net = nn.Sequential(*sigma_net)
torch.nn.init.constant_(self.sigma_net[-1].bias, 0.1) # Encourage last layer predict positive SDF
# color network
self.num_layers_color = num_layers_color
self.hidden_dim_color = hidden_dim_color
color_net = []
for l in range(num_layers_color):
if l == 0:
in_dim = self.input_ch_views + self.geo_feat_dim
else:
in_dim = hidden_dim
if l == num_layers_color - 1:
out_dim = 3 # 3 rgb
else:
out_dim = hidden_dim
color_net.append(nn.Linear(in_dim, out_dim, bias=True))
if l!=num_layers_color-1:
color_net.append(nn.ReLU(inplace=True))
self.color_net = nn.Sequential(*color_net)
def forward_sdf(self,x):
'''
@x: embedded positions
'''
h = self.sigma_net(x)
sigma, geo_feat = h[..., 0], h[..., 1:]
return sigma
def forward(self, x):
x = x.float()
input_pts, input_views = torch.split(x, [self.input_ch, self.input_ch_views], dim=-1)
# sigma
h = input_pts
h = self.sigma_net(h)
sigma, geo_feat = h[..., 0], h[..., 1:]
# color
h = torch.cat([input_views, geo_feat], dim=-1)
color = self.color_net(h)
outputs = torch.cat([color, sigma.unsqueeze(dim=-1)], -1)
return outputs
def sample_pdf(bins, weights, N_samples, det=False):
weights = weights + 1e-5 # prevent nans
pdf = weights / torch.sum(weights, -1, keepdim=True)
cdf = torch.cumsum(pdf, -1)
cdf = torch.cat([torch.zeros_like(cdf[...,:1]), cdf], -1) # (batch, len(bins))
if det:
u = torch.linspace(0., 1., steps=N_samples)
u = u.expand(list(cdf.shape[:-1]) + [N_samples])
else:
u = torch.rand(list(cdf.shape[:-1]) + [N_samples])
u = u.contiguous()
inds = torch.searchsorted(cdf, u, right=True)
below = torch.max(torch.zeros_like(inds-1), inds-1)
above = torch.min((cdf.shape[-1]-1) * torch.ones_like(inds), inds)
inds_g = torch.stack([below, above], -1) # (batch, N_samples, 2)
matched_shape = [inds_g.shape[0], inds_g.shape[1], cdf.shape[-1]]
cdf_g = torch.gather(cdf.unsqueeze(1).expand(matched_shape), 2, inds_g)
bins_g = torch.gather(bins.unsqueeze(1).expand(matched_shape), 2, inds_g)
denom = (cdf_g[...,1]-cdf_g[...,0])
denom = torch.where(denom<1e-5, torch.ones_like(denom), denom)
t = (u-cdf_g[...,0])/denom
samples = bins_g[...,0] + t * (bins_g[...,1]-bins_g[...,0])
return samples
def get_camera_rays_np(H, W, K):
"""Get ray origins, directions from a pinhole camera."""
i, j = np.meshgrid(np.arange(W, dtype=np.float32),
np.arange(H, dtype=np.float32), indexing='xy')
dirs = np.stack([(i - K[0,2])/K[0,0], -(j - K[1,2])/K[1,1], -np.ones_like(i)], axis=-1)
return dirs
def get_masks(z_vals, target_d, truncation, cfg, dir_norm=None):
valid_depth_mask = (target_d>=cfg['near']*cfg['sc_factor']) & (target_d<=cfg['far']*cfg['sc_factor'])
front_mask = (z_vals < target_d - truncation)
back_mask = (z_vals > target_d + truncation*cfg['neg_trunc_ratio'])
sdf_mask = (1.0 - front_mask.float()) * (1.0 - back_mask.float()) * valid_depth_mask
num_fs_samples = front_mask.sum()
num_sdf_samples = sdf_mask.sum()
num_samples = num_sdf_samples + num_fs_samples
fs_weight = 0.5
sdf_weight = 1.0 - fs_weight
return front_mask.bool(), sdf_mask.bool(), fs_weight, sdf_weight
def get_sdf_loss(z_vals, target_d, predicted_sdf, truncation, cfg, return_mask=False, sample_weights=None, rays_d=None):
dir_norm = rays_d.norm(dim=-1,keepdim=True)
front_mask, sdf_mask, fs_weight, sdf_weight = get_masks(z_vals, target_d, truncation, cfg, dir_norm=dir_norm)
front_mask = front_mask.bool()
mask = (target_d>cfg['far']*cfg['sc_factor']) & (predicted_sdf<cfg['fs_sdf'])
fs_loss = torch.mean(((predicted_sdf-cfg['fs_sdf']) * mask)**2 * sample_weights) * fs_weight
mask = front_mask & (target_d<=cfg['far']*cfg['sc_factor']) & (predicted_sdf<1)
empty_loss = torch.mean(torch.abs(predicted_sdf-1) * mask * sample_weights)
sdf_loss = torch.mean(((z_vals + predicted_sdf * truncation) * sdf_mask - target_d * sdf_mask)**2 * sample_weights) * sdf_weight
if return_mask:
return fs_loss,sdf_loss,empty_loss, front_mask,sdf_mask
return fs_loss, sdf_loss, empty_loss
def ray_box_intersection_batch(origins, dirs, bounds):
'''
@origins: (N,3) origin and directions. In the same coordinate frame as the bounding box
@bounds: (2,3) xyz_min and max
'''
if not torch.is_tensor(origins):
origins = torch.tensor(origins)
dirs = torch.tensor(dirs)
if not torch.is_tensor(bounds):
bounds = torch.tensor(bounds)
dirs = dirs/(torch.norm(dirs,dim=-1,keepdim=True)+1e-10)
inv_dirs = 1/dirs
bounds = bounds[None].expand(len(dirs),-1,-1) #(N,2,3)
sign = torch.zeros((len(dirs),3)).long().to(dirs.device) #(N,3)
sign[:,0] = (inv_dirs[:,0] < 0)
sign[:,1] = (inv_dirs[:,1] < 0)
sign[:,2] = (inv_dirs[:,2] < 0)
tmin = (torch.gather(bounds[...,0],dim=1,index=sign[:,0].reshape(-1,1)).reshape(-1) - origins[:,0]) * inv_dirs[:,0] #(N)
tmin[tmin<0] = 0
tmax = (torch.gather(bounds[...,0],dim=1,index=1-sign[:,0].reshape(-1,1)).reshape(-1) - origins[:,0]) * inv_dirs[:,0]
tymin = (torch.gather(bounds[...,1],dim=1,index=sign[:,1].reshape(-1,1)).reshape(-1) - origins[:,1]) * inv_dirs[:,1]
tymin[tymin<0] = 0
tymax = (torch.gather(bounds[...,1],dim=1,index=1-sign[:,1].reshape(-1,1)).reshape(-1) - origins[:,1]) * inv_dirs[:,1]
ishit = torch.ones(len(dirs)).bool().to(dirs.device)
ishit[(tmin > tymax) | (tymin > tmax)] = 0
tmin[tymin>tmin] = tymin[tymin>tmin]
tmax[tymax<tmax] = tymax[tymax<tmax]
tzmin = (torch.gather(bounds[...,2],dim=1,index=sign[:,2].reshape(-1,1)).reshape(-1) - origins[:,2]) * inv_dirs[:,2]
tzmin[tzmin<0] = 0
tzmax = (torch.gather(bounds[...,2],dim=1,index=1-sign[:,2].reshape(-1,1)).reshape(-1) - origins[:,2]) * inv_dirs[:,2]
ishit[(tmin > tzmax) | (tzmin > tmax)] = 0
tmin[tzmin>tmin] = tzmin[tzmin>tmin] #(N)
tmax[tzmax<tmax] = tzmax[tzmax<tmax]
tmin[ishit==0] = -1
tmax[ishit==0] = -1
return tmin, tmax