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

1023 lines
35 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, time,torch,pickle,trimesh,itertools,pdb,zipfile,datetime,imageio,gzip,logging,joblib,importlib,uuid,signal,multiprocessing,psutil,subprocess,tarfile,scipy,argparse
from pytorch3d.transforms import so3_log_map,so3_exp_map,se3_exp_map,se3_log_map,matrix_to_axis_angle,matrix_to_euler_angles,euler_angles_to_matrix, rotation_6d_to_matrix
from pytorch3d.renderer import FoVPerspectiveCameras, PerspectiveCameras, look_at_view_transform, look_at_rotation, RasterizationSettings, MeshRenderer, MeshRasterizer, BlendParams, SoftSilhouetteShader, HardPhongShader, PointLights, TexturesVertex
from pytorch3d.renderer.mesh.rasterize_meshes import barycentric_coordinates
from pytorch3d.renderer.mesh.shader import SoftDepthShader, HardFlatShader
from pytorch3d.renderer.mesh.textures import Textures
from pytorch3d.structures import Meshes
from scipy.interpolate import griddata
import nvdiffrast.torch as dr
import torch.nn.functional as F
import torchvision
import torch.nn as nn
from functools import partial
import pandas as pd
import open3d as o3d
from uuid import uuid4
import cv2
from PIL import Image
import numpy as np
from collections import defaultdict
import multiprocessing as mp
import matplotlib.pyplot as plt
import math,glob,re,copy
from transformations import *
from scipy.spatial import cKDTree
from collections import OrderedDict
import ruamel.yaml
yaml = ruamel.yaml.YAML()
code_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(code_dir)
try:
import kornia
except Exception:
kornia = None
try:
_mycpp_build = os.path.join(code_dir, 'mycpp', 'build')
if os.path.isdir(_mycpp_build):
sys.path.insert(0, _mycpp_build)
import mycpp
except Exception:
mycpp = None
try:
from bundlesdf.mycuda import common
except:
common = None
try:
import warp as wp
wp.init()
except:
wp = None
enable_timer = 0
def NestDict():
return defaultdict(NestDict)
to8b = lambda x : (255*np.clip(x,0,1)).astype(np.uint8)
BAD_DEPTH = 99
BAD_COLOR = 0
glcam_in_cvcam = np.array([[1,0,0,0],
[0,-1,0,0],
[0,0,-1,0],
[0,0,0,1]]).astype(float)
COLOR_MAP=np.array([[0, 0, 0], #Ignore
[128,0,0], #Background
[0,128,0], #Wall
[128,128,0], #Floor
[0,0,128], #Ceiling
[128,0,128], #Table
[0,128,128], #Chair
[128,128,128], #Window
[64,0,0], #Door
[192,0,0], #Monitor
[64, 128, 0], # 11th
[192, 0, 128],
[64, 128, 128],
[192, 128, 128],
[0, 64, 0],
[128, 64, 0],
[0, 192, 0],
[128, 192, 0],
])
def set_logging_format(level=logging.INFO):
importlib.reload(logging)
FORMAT = '[%(funcName)s()] %(message)s'
logging.basicConfig(level=level, format=FORMAT)
set_logging_format()
def make_mesh_tensors(mesh, device='cuda', max_tex_size=None):
mesh_tensors = {}
if isinstance(mesh.visual, trimesh.visual.texture.TextureVisuals):
img = np.array(mesh.visual.material.image.convert('RGB'))
img = img[...,:3]
if max_tex_size is not None:
max_size = max(img.shape[0], img.shape[1])
if max_size>max_tex_size:
scale = 1/max_size * max_tex_size
img = cv2.resize(img, fx=scale, fy=scale, dsize=None)
mesh_tensors['tex'] = torch.as_tensor(img, device=device, dtype=torch.float)[None]/255.0
mesh_tensors['uv_idx'] = torch.as_tensor(mesh.faces, device=device, dtype=torch.int)
uv = torch.as_tensor(mesh.visual.uv, device=device, dtype=torch.float)
uv[:,1] = 1 - uv[:,1]
mesh_tensors['uv'] = uv
else:
if mesh.visual.vertex_colors is None:
logging.info(f"WARN: mesh doesn't have vertex_colors, assigning a pure color")
mesh.visual.vertex_colors = np.tile(np.array([128,128,128]).reshape(1,3), (len(mesh.vertices), 1))
mesh_tensors['vertex_color'] = torch.as_tensor(mesh.visual.vertex_colors[...,:3], device=device, dtype=torch.float)/255.0
mesh_tensors.update({
'pos': torch.tensor(mesh.vertices, device=device, dtype=torch.float),
'faces': torch.tensor(mesh.faces, device=device, dtype=torch.int),
'vnormals': torch.tensor(mesh.vertex_normals, device=device, dtype=torch.float),
})
return mesh_tensors
def nvdiffrast_render(K=None, H=None, W=None, ob_in_cams=None, glctx=None, context='cuda', get_normal=False, mesh_tensors=None, mesh=None, projection_mat=None, bbox2d=None, output_size=None, use_light=False, light_color=None, light_dir=np.array([0,0,1]), light_pos=np.array([0,0,0]), w_ambient=0.8, w_diffuse=0.5, extra={}):
'''Just plain rendering, not support any gradient
@K: (3,3) np array
@ob_in_cams: (N,4,4) torch tensor, openCV camera
@projection_mat: np array (4,4)
@output_size: (height, width)
@bbox2d: (N,4) (umin,vmin,umax,vmax) if only roi need to render.
@light_dir: in cam space
@light_pos: in cam space
'''
if glctx is None:
if context == 'gl':
glctx = dr.RasterizeGLContext()
elif context=='cuda':
glctx = dr.RasterizeCudaContext()
else:
raise NotImplementedError
logging.info("created context")
if mesh_tensors is None:
mesh_tensors = make_mesh_tensors(mesh)
pos = mesh_tensors['pos']
vnormals = mesh_tensors['vnormals']
pos_idx = mesh_tensors['faces']
has_tex = 'tex' in mesh_tensors
ob_in_glcams = torch.tensor(glcam_in_cvcam, device='cuda', dtype=torch.float)[None]@ob_in_cams
if projection_mat is None:
projection_mat = projection_matrix_from_intrinsics(K, height=H, width=W, znear=0.001, zfar=100)
projection_mat = torch.as_tensor(projection_mat.reshape(-1,4,4), device='cuda', dtype=torch.float)
mtx = projection_mat@ob_in_glcams
if output_size is None:
output_size = np.asarray([H,W])
pts_cam = transform_pts(pos, ob_in_cams)
pos_homo = to_homo_torch(pos)
pos_clip = (mtx[:,None]@pos_homo[None,...,None])[...,0]
if bbox2d is not None:
l = bbox2d[:,0]
t = H-bbox2d[:,1]
r = bbox2d[:,2]
b = H-bbox2d[:,3]
tf = torch.eye(4, dtype=torch.float, device='cuda').reshape(1,4,4).expand(len(ob_in_cams),4,4).contiguous()
tf[:,0,0] = W/(r-l)
tf[:,1,1] = H/(t-b)
tf[:,3,0] = (W-r-l)/(r-l)
tf[:,3,1] = (H-t-b)/(t-b)
pos_clip = pos_clip@tf
rast_out, _ = dr.rasterize(glctx, pos_clip, pos_idx, resolution=np.asarray(output_size))
xyz_map, _ = dr.interpolate(pts_cam, rast_out, pos_idx)
depth = xyz_map[...,2]
if has_tex:
texc, _ = dr.interpolate(mesh_tensors['uv'], rast_out, mesh_tensors['uv_idx'])
color = dr.texture(mesh_tensors['tex'], texc, filter_mode='linear')
else:
color, _ = dr.interpolate(mesh_tensors['vertex_color'], rast_out, pos_idx)
if use_light:
get_normal = True
if get_normal:
vnormals_cam = transform_dirs(vnormals, ob_in_cams)
normal_map, _ = dr.interpolate(vnormals_cam, rast_out, pos_idx)
normal_map = F.normalize(normal_map, dim=-1)
normal_map = torch.flip(normal_map, dims=[1])
else:
normal_map = None
if use_light:
if light_dir is not None:
light_dir_neg = -torch.as_tensor(light_dir, dtype=torch.float, device='cuda')
else:
light_dir_neg = torch.as_tensor(light_pos, dtype=torch.float, device='cuda').reshape(1,1,3) - pts_cam
diffuse_intensity = (F.normalize(vnormals_cam, dim=-1) * F.normalize(light_dir_neg, dim=-1)).sum(dim=-1).clip(0, 1)[...,None]
diffuse_intensity_map, _ = dr.interpolate(diffuse_intensity, rast_out, pos_idx) # (N_pose, H, W, 1)
if light_color is None:
light_color = color
else:
light_color = torch.as_tensor(light_color, device='cuda', dtype=torch.float)
color = color*w_ambient + diffuse_intensity_map*light_color*w_diffuse
color = color.clip(0,1)
color = color * torch.clamp(rast_out[..., -1:], 0, 1) # Mask out background using alpha
color = torch.flip(color, dims=[1]) # Flip Y coordinates
depth = torch.flip(depth, dims=[1])
extra['xyz_map'] = torch.flip(xyz_map, dims=[1])
return color, depth, normal_map
def set_seed(random_seed):
import torch,random
np.random.seed(random_seed)
random.seed(random_seed)
torch.manual_seed(random_seed)
torch.cuda.manual_seed_all(random_seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def add_err(pred,gt,model_pts,symetry_tfs=np.eye(4)[None]):
"""
Average Distance of Model Points for objects with no indistinguishable views
- by Hinterstoisser et al. (ACCV 2012).
"""
pred_pts = transform_pts(model_pts, pred)
gt_pts = transform_pts(model_pts, gt)
e = np.linalg.norm(pred_pts - gt_pts, axis=-1).mean()
return e
def adds_err(pred,gt,model_pts):
"""
@pred: 4x4 mat
@gt:
@model: (N,3)
"""
pred_pts = transform_pts(model_pts, pred)
gt_pts = transform_pts(model_pts, gt)
nn_index = cKDTree(pred_pts)
nn_dists, _ = nn_index.query(gt_pts, k=1, workers=-1)
e = nn_dists.mean()
return e
def compute_auc_sklearn(errs, max_val=0.1, step=0.001):
from sklearn import metrics
errs = np.sort(np.array(errs))
X = np.arange(0, max_val+step, step)
Y = np.ones(len(X))
for i,x in enumerate(X):
y = (errs<=x).sum()/len(errs)
Y[i] = y
if y>=1:
break
auc = metrics.auc(X, Y) / (max_val*1)
return auc
def normalizeRotation(pose):
'''Assume no shear case
'''
new_pose = pose.copy()
scales = np.linalg.norm(pose[:3,:3],axis=0)
new_pose[:3,:3] /= scales.reshape(1,3)
return new_pose
def toOpen3dCloud(points,colors=None,normals=None):
cloud = o3d.geometry.PointCloud()
cloud.points = o3d.utility.Vector3dVector(points.astype(np.float64))
if colors is not None:
if colors.max()>1:
colors = colors/255.0
cloud.colors = o3d.utility.Vector3dVector(colors.astype(np.float64))
if normals is not None:
cloud.normals = o3d.utility.Vector3dVector(normals.astype(np.float64))
return cloud
def make_grid_image(imgs, nrow, padding=5, pad_value=255):
'''
@imgs: (B,H,W,C) np array
@nrow: num of images per row
'''
grid = torchvision.utils.make_grid(torch.as_tensor(np.asarray(imgs)).permute(0,3,1,2), nrow=nrow, padding=padding, pad_value=pad_value)
grid = grid.permute(1,2,0).contiguous().data.cpu().numpy().astype(np.uint8)
return grid
if wp is not None:
@wp.kernel(enable_backward=False)
def bilateral_filter_depth_kernel(depth:wp.array(dtype=float, ndim=2), out:wp.array(dtype=float, ndim=2), radius:int, zfar:float, sigmaD:float, sigmaR:float):
h,w = wp.tid()
H = depth.shape[0]
W = depth.shape[1]
if w>=W or h>=H:
return
out[h,w] = 0.0
mean_depth = float(0.0)
num_valid = int(0)
for u in range(w-radius, w+radius+1):
if u<0 or u>=W:
continue
for v in range(h-radius, h+radius+1):
if v<0 or v>=H:
continue
cur_depth = depth[v,u]
if cur_depth>=0.001 and cur_depth<zfar:
num_valid += 1
mean_depth += cur_depth
if num_valid==0:
return
mean_depth /= float(num_valid)
depthCenter = depth[h,w]
sum_weight = float(0.0)
sum = float(0.0)
for u in range(w-radius, w+radius+1):
if u<0 or u>=W:
continue
for v in range(h-radius, h+radius+1):
if v<0 or v>=H:
continue
cur_depth = depth[v,u]
if cur_depth>=0.001 and cur_depth<zfar and abs(cur_depth-mean_depth)<0.01:
weight = wp.exp( -float((u-w)*(u-w) + (h-v)*(h-v)) / (2.0*sigmaD*sigmaD) - (depthCenter-cur_depth)*(depthCenter-cur_depth)/(2.0*sigmaR*sigmaR) )
sum_weight += weight
sum += weight*cur_depth
if sum_weight>0 and num_valid>0:
out[h,w] = sum/sum_weight
def bilateral_filter_depth(depth, radius=2, zfar=100, sigmaD=2, sigmaR=100000, device='cuda'):
if isinstance(depth, np.ndarray):
depth_wp = wp.array(depth, dtype=float, device=device)
else:
depth_wp = wp.from_torch(depth)
out_wp = wp.zeros(depth.shape, dtype=float, device=device)
wp.launch(kernel=bilateral_filter_depth_kernel, device=device, dim=[depth.shape[0], depth.shape[1]], inputs=[depth_wp, out_wp, radius, zfar, sigmaD, sigmaR])
depth_out = wp.to_torch(out_wp)
if isinstance(depth, np.ndarray):
depth_out = depth_out.data.cpu().numpy()
return depth_out
@wp.kernel(enable_backward=False)
def erode_depth_kernel(depth:wp.array(dtype=float, ndim=2), out:wp.array(dtype=float, ndim=2), radius:int, depth_diff_thres:float, ratio_thres:float, zfar:float):
h,w = wp.tid()
H = depth.shape[0]
W = depth.shape[1]
if w>=W or h>=H:
return
d_ori = depth[h,w]
if d_ori<0.001 or d_ori>=zfar:
out[h,w] = 0.0
bad_cnt = float(0)
total = float(0)
for u in range(w-radius, w+radius+1):
if u<0 or u>=W:
continue
for v in range(h-radius, h+radius+1):
if v<0 or v>=H:
continue
cur_depth = depth[v,u]
total += 1.0
if cur_depth<0.001 or cur_depth>=zfar or abs(cur_depth-d_ori)>depth_diff_thres:
bad_cnt += 1.0
if bad_cnt/total>ratio_thres:
out[h,w] = 0.0
else:
out[h,w] = d_ori
def erode_depth(depth, radius=2, depth_diff_thres=0.001, ratio_thres=0.8, zfar=100, device='cuda'):
depth_wp = wp.from_torch(torch.as_tensor(depth, dtype=torch.float, device=device))
out_wp = wp.zeros(depth.shape, dtype=float, device=device)
wp.launch(kernel=erode_depth_kernel, device=device, dim=[depth.shape[0], depth.shape[1]], inputs=[depth_wp, out_wp, radius, depth_diff_thres, ratio_thres, zfar],)
depth_out = wp.to_torch(out_wp)
if isinstance(depth, np.ndarray):
depth_out = depth_out.data.cpu().numpy()
return depth_out
def depth2xyzmap(depth, K, uvs=None):
invalid_mask = (depth<0.001)
H,W = depth.shape[:2]
if uvs is None:
vs,us = np.meshgrid(np.arange(0,H),np.arange(0,W), sparse=False, indexing='ij')
vs = vs.reshape(-1)
us = us.reshape(-1)
else:
uvs = uvs.round().astype(int)
us = uvs[:,0]
vs = uvs[:,1]
zs = depth[vs,us]
xs = (us-K[0,2])*zs/K[0,0]
ys = (vs-K[1,2])*zs/K[1,1]
pts = np.stack((xs.reshape(-1),ys.reshape(-1),zs.reshape(-1)), 1) #(N,3)
xyz_map = np.zeros((H,W,3), dtype=np.float32)
xyz_map[vs,us] = pts
xyz_map[invalid_mask] = 0
return xyz_map
def depth2xyzmap_batch(depths, Ks, zfar):
'''
@depths: torch tensor (B,H,W)
@Ks: torch tensor (B,3,3)
'''
bs = depths.shape[0]
invalid_mask = (depths<0.001) | (depths>zfar)
H,W = depths.shape[-2:]
vs,us = torch.meshgrid(torch.arange(0,H),torch.arange(0,W), indexing='ij')
vs = vs.reshape(-1).float().cuda()[None].expand(bs,-1)
us = us.reshape(-1).float().cuda()[None].expand(bs,-1)
zs = depths.reshape(bs,-1)
Ks = Ks[:,None].expand(bs,zs.shape[-1],3,3)
xs = (us-Ks[...,0,2])*zs/Ks[...,0,0] #(B,N)
ys = (vs-Ks[...,1,2])*zs/Ks[...,1,1]
pts = torch.stack([xs,ys,zs], dim=-1) #(B,N,3)
xyz_maps = pts.reshape(bs,H,W,3)
xyz_maps[invalid_mask] = 0
return xyz_maps
def rle_to_mask(rle: dict) -> np.ndarray:
"""Compute a binary mask from an uncompressed RLE."""
h, w = rle["size"]
mask = np.empty(h * w, dtype=bool)
idx = 0
parity = False
for count in rle["counts"]:
mask[idx : idx + count] = parity
idx += count
parity ^= True
mask = mask.reshape(w, h)
return mask.transpose() # Put in C order
def depth_to_vis(depth, zmin=None, zmax=None, mode='rgb', inverse=True):
if zmin is None:
zmin = depth.min()
if zmax is None:
zmax = depth.max()
if inverse:
invalid = depth<0.001
vis = zmin/(depth+1e-8)
vis[invalid] = 0
else:
depth = depth.clip(zmin, zmax)
invalid = (depth==zmin) | (depth==zmax)
vis = (depth-zmin)/(zmax-zmin)
vis[invalid] = 1
if mode=='gray':
vis = (vis*255).clip(0, 255).astype(np.uint8)
elif mode=='rgb':
vis = cv2.applyColorMap((vis*255).astype(np.uint8), cv2.COLORMAP_JET)[...,::-1]
else:
raise RuntimeError
return vis
def sample_views_icosphere(n_views, subdivisions=None, radius=1):
if subdivisions is not None:
mesh = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius)
else:
subdivision = 1
while 1:
mesh = trimesh.creation.icosphere(subdivisions=subdivision, radius=radius)
if mesh.vertices.shape[0]>=n_views:
break
subdivision += 1
cam_in_obs = np.tile(np.eye(4)[None], (len(mesh.vertices),1,1))
cam_in_obs[:,:3,3] = mesh.vertices
up = np.array([0,0,1])
z_axis = -cam_in_obs[:,:3,3] #(N,3)
z_axis /= np.linalg.norm(z_axis, axis=-1).reshape(-1,1)
x_axis = np.cross(up.reshape(1,3), z_axis)
invalid = (x_axis==0).all(axis=-1)
x_axis[invalid] = [1,0,0]
x_axis /= np.linalg.norm(x_axis, axis=-1).reshape(-1,1)
y_axis = np.cross(z_axis, x_axis)
y_axis /= np.linalg.norm(y_axis, axis=-1).reshape(-1,1)
cam_in_obs[:,:3,0] = x_axis
cam_in_obs[:,:3,1] = y_axis
cam_in_obs[:,:3,2] = z_axis
return cam_in_obs
def to_homo(pts):
'''
@pts: (N,3 or 2) will homogeneliaze the last dimension
'''
assert len(pts.shape)==2, f'pts.shape: {pts.shape}'
homo = np.concatenate((pts, np.ones((pts.shape[0],1))),axis=-1)
return homo
def to_homo_torch(pts):
'''
@pts: shape can be (...,N,3 or 2) or (N,3) will homogeneliaze the last dimension
'''
ones = torch.ones((*pts.shape[:-1],1), dtype=torch.float, device=pts.device)
homo = torch.cat((pts, ones),dim=-1)
return homo
def transform_pts(pts,tf):
"""Transform 2d or 3d points
@pts: (...,N_pts,3)
@tf: (...,4,4)
"""
if len(tf.shape)>=3 and tf.shape[-3]!=pts.shape[-2]:
tf = tf[...,None,:,:]
return (tf[...,:-1,:-1]@pts[...,None] + tf[...,:-1,-1:])[...,0]
def transform_dirs(dirs,tf):
"""
@dirs: (...,3)
@tf: (...,4,4)
"""
if len(tf.shape)>=3 and tf.shape[-3]!=dirs.shape[-2]:
tf = tf[...,None,:,:]
return (tf[...,:3,:3]@dirs[...,None])[...,0]
def random_direction():
'''https://stackoverflow.com/questions/33976911/generate-a-random-sample-of-points-distributed-on-the-surface-of-a-unit-sphere
'''
vec = np.random.randn(3).reshape(3)
vec /= np.linalg.norm(vec)
return vec
def compute_mesh_diameter(model_pts=None, mesh=None, n_sample=1000):
from sklearn.decomposition import TruncatedSVD
if mesh is not None:
u, s, vh = scipy.linalg.svd(mesh.vertices, full_matrices=False)
pts = u@s
diameter = np.linalg.norm(pts.max(axis=0)-pts.min(axis=0))
return float(diameter)
if n_sample is None:
pts = model_pts
else:
ids = np.random.choice(len(model_pts), size=min(n_sample, len(model_pts)), replace=False)
pts = model_pts[ids]
dists = np.linalg.norm(pts[None]-pts[:,None], axis=-1)
diameter = dists.max()
return diameter
def compute_crop_window_tf_batch(pts=None, H=None, W=None, poses=None, K=None, crop_ratio=1.2, out_size=None, rgb=None, uvs=None, method='min_box', mesh_diameter=None):
'''Project the points and find the cropping transform
@pts: (N,3)
@poses: (B,4,4) tensor
@min_box: min_box/min_circle
@scale: scale to apply to the tightly enclosing roi
'''
def compute_tf_batch(left, right, top, bottom):
B = len(left)
left = left.round()
right = right.round()
top = top.round()
bottom = bottom.round()
tf = torch.eye(3)[None].expand(B,-1,-1).contiguous()
tf[:,0,2] = -left
tf[:,1,2] = -top
new_tf = torch.eye(3)[None].expand(B,-1,-1).contiguous()
new_tf[:,0,0] = out_size[0]/(right-left)
new_tf[:,1,1] = out_size[1]/(bottom-top)
tf = new_tf@tf
return tf
B = len(poses)
torch.set_default_tensor_type('torch.cuda.FloatTensor')
if method=='box_3d':
radius = mesh_diameter*crop_ratio/2
offsets = torch.tensor([0,0,0,
radius,0,0,
-radius,0,0,
0,radius,0,
0,-radius,0]).reshape(-1,3)
pts = poses[:,:3,3].reshape(-1,1,3)+offsets.reshape(1,-1,3)
K = torch.as_tensor(K)
projected = (K@pts.reshape(-1,3).T).T
uvs = projected[:,:2]/projected[:,2:3]
uvs = uvs.reshape(B, -1, 2)
center = uvs[:,0] #(B,2)
radius = torch.abs(uvs-center.reshape(-1,1,2)).reshape(B,-1).max(axis=-1)[0].reshape(-1) #(B)
left = center[:,0]-radius
right = center[:,0]+radius
top = center[:,1]-radius
bottom = center[:,1]+radius
tfs = compute_tf_batch(left, right, top, bottom)
return tfs
else:
raise RuntimeError
return tf
def cv_draw_text(img,text,uv_top_left,color=(255, 255, 255),fontScale=0.5,thickness=1,fontFace=cv2.FONT_HERSHEY_SIMPLEX,outline_color=None,line_spacing=1.5):
H,W = img.shape[:2]
uv_top_left = np.array(uv_top_left, dtype=float)
assert uv_top_left.shape == (2,)
for line in text.splitlines():
(w, h), _ = cv2.getTextSize(text=line,fontFace=fontFace,fontScale=fontScale,thickness=thickness,)
uv_bottom_left_i = uv_top_left + [0, h]
############# Ensure inside image
while uv_bottom_left_i[0]<0:
uv_bottom_left_i[0] += 1
while uv_bottom_left_i[0]+w>=W:
uv_bottom_left_i[0] -= 1
while uv_bottom_left_i[1]>=H:
uv_bottom_left_i[1] -= 1
while uv_bottom_left_i[1]-h<0:
uv_bottom_left_i[1] += 1
org = tuple(uv_bottom_left_i.astype(int))
if outline_color is not None:
cv2.putText(img,text=line,org=org,fontFace=fontFace,fontScale=fontScale,color=outline_color,thickness=thickness,lineType=cv2.LINE_AA,)
cv2.putText(img,text=line,org=org,fontFace=fontFace,fontScale=fontScale,color=color,thickness=thickness,lineType=cv2.LINE_AA,)
uv_top_left[1] = uv_bottom_left_i[1]-h+h*line_spacing
return img
def trimesh_add_pure_colored_texture(mesh, color=np.array([255,255,255]), resolution=5):
tex_img = np.tile(color.reshape(1,1,3), (resolution, resolution, 1)).astype(np.uint8)
mesh = mesh.unwrap()
mesh.visual = trimesh.visual.texture.TextureVisuals(uv=mesh.visual.uv,image=Image.fromarray(tex_img))
return mesh
def project_3d_to_2d(pt,K,ob_in_cam):
pt = pt.reshape(4,1)
projected = K @ ((ob_in_cam@pt)[:3,:])
projected = projected.reshape(-1)
projected = projected/projected[2]
return projected.reshape(-1)[:2].round().astype(int)
def draw_xyz_axis(color, ob_in_cam, scale=0.1, K=np.eye(3), thickness=3, transparency=0,is_input_rgb=False):
'''
@color: BGR
'''
if is_input_rgb:
color = cv2.cvtColor(color,cv2.COLOR_RGB2BGR)
xx = np.array([1,0,0,1]).astype(float)
yy = np.array([0,1,0,1]).astype(float)
zz = np.array([0,0,1,1]).astype(float)
xx[:3] = xx[:3]*scale
yy[:3] = yy[:3]*scale
zz[:3] = zz[:3]*scale
origin = tuple(project_3d_to_2d(np.array([0,0,0,1]), K, ob_in_cam))
xx = tuple(project_3d_to_2d(xx, K, ob_in_cam))
yy = tuple(project_3d_to_2d(yy, K, ob_in_cam))
zz = tuple(project_3d_to_2d(zz, K, ob_in_cam))
line_type = cv2.LINE_AA
arrow_len = 0
tmp = color.copy()
tmp1 = tmp.copy()
tmp1 = cv2.arrowedLine(tmp1, origin, xx, color=(0,0,255), thickness=thickness,line_type=line_type, tipLength=arrow_len)
mask = np.linalg.norm(tmp1-tmp, axis=-1)>0
tmp[mask] = tmp[mask]*transparency + tmp1[mask]*(1-transparency)
tmp1 = tmp.copy()
tmp1 = cv2.arrowedLine(tmp1, origin, yy, color=(0,255,0), thickness=thickness,line_type=line_type, tipLength=arrow_len)
mask = np.linalg.norm(tmp1-tmp, axis=-1)>0
tmp[mask] = tmp[mask]*transparency + tmp1[mask]*(1-transparency)
tmp1 = tmp.copy()
tmp1 = cv2.arrowedLine(tmp1, origin, zz, color=(255,0,0), thickness=thickness,line_type=line_type, tipLength=arrow_len)
mask = np.linalg.norm(tmp1-tmp, axis=-1)>0
tmp[mask] = tmp[mask]*transparency + tmp1[mask]*(1-transparency)
tmp = tmp.astype(np.uint8)
if is_input_rgb:
tmp = cv2.cvtColor(tmp,cv2.COLOR_BGR2RGB)
return tmp
def draw_posed_3d_box(K, img, ob_in_cam, bbox, line_color=(0,255,0), linewidth=2):
'''Revised from 6pack dataset/inference_dataset_nocs.py::projection
@bbox: (2,3) min/max
@line_color: RGB
'''
min_xyz = bbox.min(axis=0)
xmin, ymin, zmin = min_xyz
max_xyz = bbox.max(axis=0)
xmax, ymax, zmax = max_xyz
def draw_line3d(start,end,img):
pts = np.stack((start,end),axis=0).reshape(-1,3)
pts = (ob_in_cam@to_homo(pts).T).T[:,:3] #(2,3)
projected = (K@pts.T).T
uv = np.round(projected[:,:2]/projected[:,2].reshape(-1,1)).astype(int) #(2,2)
img = cv2.line(img, uv[0].tolist(), uv[1].tolist(), color=line_color, thickness=linewidth, lineType=cv2.LINE_AA)
return img
for y in [ymin,ymax]:
for z in [zmin,zmax]:
start = np.array([xmin,y,z])
end = start+np.array([xmax-xmin,0,0])
img = draw_line3d(start,end,img)
for x in [xmin,xmax]:
for z in [zmin,zmax]:
start = np.array([x,ymin,z])
end = start+np.array([0,ymax-ymin,0])
img = draw_line3d(start,end,img)
for x in [xmin,xmax]:
for y in [ymin,ymax]:
start = np.array([x,y,zmin])
end = start+np.array([0,0,zmax-zmin])
img = draw_line3d(start,end,img)
return img
def projection_matrix_from_intrinsics(K, height, width, znear, zfar, window_coords='y_down'):
"""Conversion of Hartley-Zisserman intrinsic matrix to OpenGL proj. matrix.
Ref:
1) https://strawlab.org/2011/11/05/augmented-reality-with-OpenGL
2) https://github.com/strawlab/opengl-hz/blob/master/src/calib_test_utils.py
:param K: 3x3 ndarray with the intrinsic camera matrix.
:param x0 The X coordinate of the camera image origin (typically 0).
:param y0: The Y coordinate of the camera image origin (typically 0).
:param w: Image width.
:param h: Image height.
:param nc: Near clipping plane.
:param fc: Far clipping plane.
:param window_coords: 'y_up' or 'y_down'.
:return: 4x4 ndarray with the OpenGL projection matrix.
"""
x0 = 0
y0 = 0
w = width
h = height
nc = znear
fc = zfar
depth = float(fc - nc)
q = -(fc + nc) / depth
qn = -2 * (fc * nc) / depth
# Draw our images upside down, so that all the pixel-based coordinate
# systems are the same.
if window_coords == 'y_up':
proj = np.array([
[2 * K[0, 0] / w, -2 * K[0, 1] / w, (-2 * K[0, 2] + w + 2 * x0) / w, 0],
[0, -2 * K[1, 1] / h, (-2 * K[1, 2] + h + 2 * y0) / h, 0],
[0, 0, q, qn], # Sets near and far planes (glPerspective).
[0, 0, -1, 0]
])
# Draw the images upright and modify the projection matrix so that OpenGL
# will generate window coords that compensate for the flipped image coords.
elif window_coords == 'y_down':
proj = np.array([
[2 * K[0, 0] / w, -2 * K[0, 1] / w, (-2 * K[0, 2] + w + 2 * x0) / w, 0],
[0, 2 * K[1, 1] / h, (2 * K[1, 2] - h + 2 * y0) / h, 0],
[0, 0, q, qn], # Sets near and far planes (glPerspective).
[0, 0, -1, 0]
])
else:
raise NotImplementedError
return proj
def symmetry_tfs_from_info(info, rot_angle_discrete=5):
symmetry_tfs = [np.eye(4)]
if 'symmetries_discrete' in info:
tfs = np.array(info['symmetries_discrete']).reshape(-1,4,4)
tfs[...,:3,3] *= 0.001
symmetry_tfs = [np.eye(4)]
symmetry_tfs += list(tfs)
if 'symmetries_continuous' in info:
axis = np.array(info['symmetries_continuous'][0]['axis']).reshape(3)
offset = info['symmetries_continuous'][0]['offset']
rxs = [0]
rys = [0]
rzs = [0]
if axis[0]>0:
rxs = np.arange(0,360,rot_angle_discrete)/180.0*np.pi
elif axis[1]>0:
rys = np.arange(0,360,rot_angle_discrete)/180.0*np.pi
elif axis[2]>0:
rzs = np.arange(0,360,rot_angle_discrete)/180.0*np.pi
for rx in rxs:
for ry in rys:
for rz in rzs:
tf = euler_matrix(rx, ry, rz)
tf[:3,3] = offset
symmetry_tfs.append(tf)
if len(symmetry_tfs)==0:
symmetry_tfs = [np.eye(4)]
symmetry_tfs = np.array(symmetry_tfs)
return symmetry_tfs
def pose_to_egocentric_delta_pose(A_in_cam, B_in_cam):
'''Used for Pose Refinement. Given the object's two poses in camera, convert them to relative poses in camera's egocentric view
@A_in_cam: (B,4,4) torch tensor
'''
trans_delta = B_in_cam[:,:3,3] - A_in_cam[:,:3,3]
rot_mat_delta = B_in_cam[:,:3,:3]@A_in_cam[:,:3,:3].permute(0,2,1)
return trans_delta, rot_mat_delta
def egocentric_delta_pose_to_pose(A_in_cam, trans_delta, rot_mat_delta):
'''Used for Pose Refinement. Given the object's two poses in camera, convert them to relative poses in camera's egocentric view
@A_in_cam: (B,4,4) torch tensor
'''
B_in_cam = torch.eye(4, dtype=torch.float, device=A_in_cam.device)[None].expand(len(A_in_cam),-1,-1).contiguous()
B_in_cam[:,:3,3] = A_in_cam[:,:3,3]+trans_delta
B_in_cam[:,:3,:3] = rot_mat_delta@A_in_cam[:,:3,:3]
return B_in_cam
def sdg_load_bounding_box(file_path: str):
"""Load bounding boxes.
Args:
file_path: Path of the bounding box.
Returns:
A dictionary of the bounding boxes.
"""
bbox_dict = {}
bbox_array = np.load(file_path)
for id, x_min, y_min, x_max, y_max, occlusion_ratio in zip(
bbox_array["semanticId"],
bbox_array["x_min"],
bbox_array["y_min"],
bbox_array["x_max"],
bbox_array["y_max"],
bbox_array["occlusionRatio"],
):
bbox_dict[id] = {
"x_min": x_min,
"y_min": y_min,
"x_max": x_max,
"y_max": y_max,
"occlusion_ratio": occlusion_ratio,
}
return bbox_dict
def texture_map_interpolation(tex_image_numpy):
all_channels = []
mask = np.all(tex_image_numpy == 0, axis=2)
x = np.arange(0, tex_image_numpy.shape[1])
y = np.arange(0, tex_image_numpy.shape[0])
xx, yy = np.meshgrid(x, y)
for each_channel in range(tex_image_numpy.shape[2]):
curr_channel = tex_image_numpy[:,:,each_channel]
x1 = xx[~mask]
y1 = yy[~mask]
newarr = curr_channel[~mask]
GD1 = griddata((x1, y1), newarr.ravel(), (xx, yy), method='nearest')
all_channels.append(GD1[:,:,np.newaxis].round().astype(np.uint8))
final_image = np.concatenate(all_channels, axis =-1)
return final_image
class OctreeManager:
def __init__(self,pts=None,max_level=None,octree=None):
import kaolin
if octree is None:
pts_quantized = kaolin.ops.spc.quantize_points(pts.contiguous(), level=max_level)
self.octree = kaolin.ops.spc.unbatched_points_to_octree(pts_quantized, max_level, sorted=False)
else:
self.octree = octree
lengths = torch.tensor([len(self.octree)], dtype=torch.int32).cpu()
self.max_level, self.pyramids, self.exsum = kaolin.ops.spc.scan_octrees(self.octree,lengths)
self.finest_vox_size = 2.0/(2**self.max_level)
self.n_level = self.max_level+1
self.vox_point_all_levels = kaolin.ops.spc.generate_points(self.octree, self.pyramids, self.exsum)
self.point_hierarchy_dual, self.pyramid_dual = kaolin.ops.spc.unbatched_make_dual(self.vox_point_all_levels, self.pyramids[0])
self.trinkets, self.pointers_to_parent = kaolin.ops.spc.unbatched_make_trinkets(self.vox_point_all_levels, self.pyramids[0], self.point_hierarchy_dual, self.pyramid_dual)
self.n_vox = len(self.vox_point_all_levels)
self.n_corners = len(self.point_hierarchy_dual)
for level in range(self.n_level):
vox_pts = self.get_level_quantized_points(level)
corner_pts = self.get_level_corner_quantized_points(level)
logging.info(f'level:{level}, vox_pts:{vox_pts.shape}, corner_pts:{corner_pts.shape}')
def get_level_corner_quantized_points(self,level):
start = self.pyramid_dual[...,1,level]
num = self.pyramid_dual[...,0,level]
return self.point_hierarchy_dual[start:start+num]
def get_level_quantized_points(self,level):
start = self.pyramids[...,1,level]
num = self.pyramids[...,0,level]
return self.vox_point_all_levels[start:start+num]
def get_center_ids(self,x,level):
'''Get ids with 0 starting from current level's first point
'''
import kaolin
pidx = kaolin.ops.spc.unbatched_query(self.octree, self.exsum, x.float(), level, with_parents=False)
return pidx
def get_vox_size_at_level(self, level):
return 2.0/(2**level)
def draw(self,level, method='point'):
import kaolin
logging.info(f"level:{level}")
vox_size = self.get_vox_size_at_level(level)
if method=='point':
corner_coords = self.get_level_corner_quantized_points(level)
pts = corner_coords*vox_size - 1
mesh = trimesh.points.PointCloud(pts.data.cpu().numpy().reshape(-1,3))
return mesh
def ray_trace(self,rays_o,rays_d,level,debug=False):
"""Octree is in normalized [-1,1] world coordinate frame
'rays_o': ray origin in normalized world coordinate system
'rays_d': (N,3) unit length ray direction in normalized world coordinate system
'octree': spc
@voxel_size: in the scale of [-1,1] space
Return:
ray_depths_in_out: traveling times, NOT the Z value; invalid will be zeros
"""
from mycuda import common
import kaolin
ray_index, rays_pid, depth_in_out = kaolin.render.spc.unbatched_raytrace(self.octree,self.vox_point_all_levels,self.pyramids[0],self.exsum,rays_o,rays_d,level=level,return_depth=True,with_exit=True)
if ray_index.size()[0] == 0:
pdb.set_trace()
print("[WARNING] batch has 0 intersections!!")
ray_depths_in_out = torch.zeros((rays_o.shape[0],1,2))
rays_pid = -torch.ones_like(rays_o[:, :1])
rays_near = torch.zeros_like(rays_o[:, :1])
rays_far = torch.zeros_like(rays_o[:, :1])
return rays_near, rays_far, rays_pid, ray_depths_in_out
intersected_ray_ids,counts = torch.unique_consecutive(ray_index,return_counts=True)
max_intersections = counts.max().item()
start_poss = torch.cat([torch.tensor([0], device=counts.device),torch.cumsum(counts[:-1],dim=0)],dim=0)
ray_depths_in_out = common.postprocessOctreeRayTracing(ray_index.long().contiguous(),depth_in_out.contiguous(),intersected_ray_ids.long().contiguous(),start_poss.long().contiguous(), max_intersections, rays_o.shape[0])
rays_far = ray_depths_in_out[:,:,1].max(dim=-1)[0].reshape(-1,1)
rays_near = ray_depths_in_out[:,0,0].reshape(-1,1)
return rays_near, rays_far, rays_pid, ray_depths_in_out
def make_yaml_dumpable(D):
if isinstance(D, np.ndarray):
return D.tolist()
for d in D:
if isinstance(D[d], dict) or isinstance(D[d], OrderedDict) or isinstance(D[d], defaultdict):
D[d] = dict(D[d])
D[d] = make_yaml_dumpable(D[d])
continue
if isinstance(D[d], np.ndarray):
D[d] = D[d].tolist()
continue
if np.issubdtype(type(D[d]), int):
D[d] = int(D[d])
continue
if np.issubdtype(type(D[d]), float):
D[d] = float(D[d])
continue
if np.issubdtype(type(D[d]), str):
D[d] = str(D[d])
continue
if isinstance(D[d], list):
for i in range(len(D[d])):
D[d][i] = make_yaml_dumpable(D[d][i])
continue
return dict(D)