Files
hand-motion-pipeline/third_party/Dyn-HaMR/dyn-hamr/optim/losses.py
T

1457 lines
55 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import math
import numpy as np
import cv2
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import (
MixtureSameFamily,
Categorical,
Normal,
MultivariateNormal,
)
# from body_model import SMPL_JOINTS, SMPL_PARENTS
from geometry.rotation import rotation_matrix_to_angle_axis
from geometry import camera as cam_util
from optim.bio_loss import BMCLoss
from util.logger import Logger
from typing import List, Tuple, NewType
Tensor = NewType('Tensor', torch.Tensor)
CONTACT_HEIGHT_THRESH = 0.08
openpose_indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
gt_indices = openpose_indices
def solid_angles(
points: Tensor,
triangles: Tensor,
thresh: float = 1e-8
) -> Tensor:
''' Compute solid angle between the input points and triangles
Follows the method described in:
The Solid Angle of a Plane Triangle
A. VAN OOSTEROM AND J. STRACKEE
IEEE TRANSACTIONS ON BIOMEDICAL ENGINEERING,
VOL. BME-30, NO. 2, FEBRUARY 1983
Parameters
-----------
points: BxQx3
Tensor of input query points
triangles: BxFx3x3
Target triangles
thresh: float
float threshold
Returns
-------
solid_angles: BxQxF
A tensor containing the solid angle between all query points
and input triangles
'''
# Center the triangles on the query points. Size should be BxQxFx3x3
centered_tris = triangles[:, None] - points[:, :, None, None]
# BxQxFx3
norms = torch.norm(centered_tris, dim=-1)
# Should be BxQxFx3
cross_prod = torch.cross(
centered_tris[:, :, :, 1], centered_tris[:, :, :, 2], dim=-1)
# Should be BxQxF
numerator = (centered_tris[:, :, :, 0] * cross_prod).sum(dim=-1)
del cross_prod
dot01 = (centered_tris[:, :, :, 0] * centered_tris[:, :, :, 1]).sum(dim=-1)
dot12 = (centered_tris[:, :, :, 1] * centered_tris[:, :, :, 2]).sum(dim=-1)
dot02 = (centered_tris[:, :, :, 0] * centered_tris[:, :, :, 2]).sum(dim=-1)
del centered_tris
denominator = (
norms.prod(dim=-1) +
dot01 * norms[:, :, :, 2] +
dot02 * norms[:, :, :, 1] +
dot12 * norms[:, :, :, 0]
)
del dot01, dot12, dot02, norms
# Should be BxQ
solid_angle = torch.atan2(numerator, denominator)
del numerator, denominator
torch.cuda.empty_cache()
return 2 * solid_angle
def winding_numbers(
points: Tensor,
triangles: Tensor,
thresh: float = 1e-8
) -> Tensor:
''' Uses winding_numbers to compute inside/outside
Robust inside-outside segmentation using generalized winding numbers
Alec Jacobson,
Ladislav Kavan,
Olga Sorkine-Hornung
Fast Winding Numbers for Soups and Clouds SIGGRAPH 2018
Gavin Barill
NEIL G. Dickson
Ryan Schmidt
David I.W. Levin
and Alec Jacobson
Parameters
-----------
points: BxQx3
Tensor of input query points
triangles: BxFx3x3
Target triangles
thresh: float
float threshold
Returns
-------
winding_numbers: BxQ
A tensor containing the Generalized winding numbers
'''
# The generalized winding number is the sum of solid angles of the point
# with respect to all triangles.
return 1 / (4 * math.pi) * solid_angles(
points, triangles, thresh=thresh).sum(dim=-1)
def pcl_pcl_pairwise_distance(
x: torch.Tensor,
y: torch.Tensor,
use_cuda: bool = True,
squared: bool = False
):
"""
Calculate the pairse distance between two point clouds.
"""
bs, num_points_x, points_dim = x.size()
_, num_points_y, _ = y.size()
dtype = torch.cuda.LongTensor if \
use_cuda else torch.LongTensor
xx = torch.bmm(x, x.transpose(2, 1))
yy = torch.bmm(y, y.transpose(2, 1))
zz = torch.bmm(x, y.transpose(2, 1))
diag_ind_x = torch.arange(0, num_points_x).type(dtype)
diag_ind_y = torch.arange(0, num_points_y).type(dtype)
rx = (
xx[:, diag_ind_x, diag_ind_x]
.unsqueeze(1)
.expand_as(zz.transpose(2, 1))
)
ry = yy[:, diag_ind_y, diag_ind_y].unsqueeze(1).expand_as(zz)
P = rx.transpose(2, 1) + ry - 2 * zz
if not squared:
P = torch.clamp(P, min=0.0) # make sure we dont get nans
P = torch.sqrt(P)
return P
def get_keypoints_rectangle(keypoints: np.array, threshold: float) -> Tuple[float, float, float]:
"""
Compute rectangle enclosing keypoints above the threshold.
Args:
keypoints (np.array): Keypoint array of shape (N, 3).
threshold (float): Confidence visualization threshold.
Returns:
Tuple[float, float, float]: Rectangle width, height and area.
"""
# print(keypoints.shape)
valid_ind = keypoints[:, -1] > threshold
if valid_ind.sum() > 0:
valid_keypoints = keypoints[valid_ind][:, :-1]
max_x = valid_keypoints[:,0].max()
max_y = valid_keypoints[:,1].max()
min_x = valid_keypoints[:,0].min()
min_y = valid_keypoints[:,1].min()
width = max_x - min_x
height = max_y - min_y
area = width * height
return width, height, area
else:
return 0,0,0
def render_keypoints(img: np.array,
keypoints: np.array,
pairs: List,
colors: List,
thickness_circle_ratio: float,
thickness_line_ratio_wrt_circle: float,
pose_scales: List,
threshold: float = 0.1,
alpha: float = 1.0) -> np.array:
"""
Render keypoints on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
keypoints (np.array): Keypoint array of shape (N, 3).
pairs (List): List of keypoint pairs per limb.
colors: (List): List of colors per keypoint.
thickness_circle_ratio (float): Circle thickness ratio.
thickness_line_ratio_wrt_circle (float): Line thickness ratio wrt the circle.
pose_scales (List): List of pose scales.
threshold (float): Only visualize keypoints with confidence above the threshold.
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
img_orig = img.copy()
width, height = img.shape[1], img.shape[2]
area = width * height
lineType = 8
shift = 0
numberColors = len(colors)
thresholdRectangle = 0.1
person_width, person_height, person_area = get_keypoints_rectangle(keypoints, thresholdRectangle)
if person_area > 0:
ratioAreas = min(1, max(person_width / width, person_height / height))
thicknessRatio = np.maximum(np.round(math.sqrt(area) * thickness_circle_ratio * ratioAreas), 2)
thicknessCircle = np.maximum(1, thicknessRatio if ratioAreas > 0.05 else -np.ones_like(thicknessRatio))
thicknessLine = np.maximum(1, np.round(thicknessRatio * thickness_line_ratio_wrt_circle))
radius = thicknessRatio / 2
img = np.ascontiguousarray(img.copy())
for i, pair in enumerate(pairs):
index1, index2 = pair
if keypoints[index1, -1] > threshold and keypoints[index2, -1] > threshold:
thicknessLineScaled = int(round(min(thicknessLine[index1], thicknessLine[index2]) * pose_scales[0]))
colorIndex = index2
color = colors[colorIndex % numberColors]
keypoint1 = keypoints[index1, :-1].astype(np.int32)
keypoint2 = keypoints[index2, :-1].astype(np.int32)
cv2.line(img, tuple(keypoint1.tolist()), tuple(keypoint2.tolist()), tuple(color.tolist()), thicknessLineScaled, lineType, shift)
for part in range(len(keypoints)):
faceIndex = part
if keypoints[faceIndex, -1] > threshold:
radiusScaled = int(round(radius[faceIndex] * pose_scales[0]))
thicknessCircleScaled = int(round(thicknessCircle[faceIndex] * pose_scales[0]))
colorIndex = part
color = colors[colorIndex % numberColors]
center = keypoints[faceIndex, :-1].astype(np.int32)
cv2.circle(img, tuple(center.tolist()), radiusScaled, tuple(color.tolist()), thicknessCircleScaled, lineType, shift)
return img
def render_hand_keypoints(img, right_hand_keypoints, threshold=0.1, use_confidence=False, map_fn=lambda x: np.ones_like(x), alpha=1.0):
if use_confidence and map_fn is not None:
#thicknessCircleRatioLeft = 1./50 * map_fn(left_hand_keypoints[:, -1])
thicknessCircleRatioRight = 1./50 * map_fn(right_hand_keypoints[:, -1])
else:
#thicknessCircleRatioLeft = 1./50 * np.ones(left_hand_keypoints.shape[0])
thicknessCircleRatioRight = 1./50 * np.ones(right_hand_keypoints.shape[0])
thicknessLineRatioWRTCircle = 0.75
pairs = [0,1, 1,2, 2,3, 3,4, 0,5, 5,6, 6,7, 7,8, 0,9, 9,10, 10,11, 11,12, 0,13, 13,14, 14,15, 15,16, 0,17, 17,18, 18,19, 19,20]
pairs = np.array(pairs).reshape(-1,2)
colors = [100., 100., 100.,
100., 0., 0.,
150., 0., 0.,
200., 0., 0.,
255., 0., 0.,
100., 100., 0.,
150., 150., 0.,
200., 200., 0.,
255., 255., 0.,
0., 100., 50.,
0., 150., 75.,
0., 200., 100.,
0., 255., 125.,
0., 50., 100.,
0., 75., 150.,
0., 100., 200.,
0., 125., 255.,
100., 0., 100.,
150., 0., 150.,
200., 0., 200.,
255., 0., 255.]
colors = np.array(colors).reshape(-1,3)
#colors = np.zeros_like(colors)
poseScales = [1]
#img = render_keypoints(img, left_hand_keypoints, pairs, colors, thicknessCircleRatioLeft, thicknessLineRatioWRTCircle, poseScales, threshold, alpha=alpha)
img = render_keypoints(img, right_hand_keypoints, pairs, colors, thicknessCircleRatioRight, thicknessLineRatioWRTCircle, poseScales, threshold, alpha=alpha)
#img = render_keypoints(img, right_hand_keypoints, pairs, colors, thickness_circle_ratio, thickness_line_ratio_wrt_circle, pose_scales, 0.1)
return img
def render_body_keypoints(img: np.array,
body_keypoints: np.array) -> np.array:
"""
Render OpenPose body keypoints on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
body_keypoints (np.array): Keypoint array of shape (N, 3); 3 <====> (x, y, confidence).
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
thickness_circle_ratio = 1./75. * np.ones(body_keypoints.shape[0])
thickness_line_ratio_wrt_circle = 0.75
pairs = []
pairs = [1,8,1,2,1,5,2,3,3,4,5,6,6,7,8,9,9,10,10,11,8,12,12,13,13,14,1,0,0,15,15,17,0,16,16,18,14,19,19,20,14,21,11,22,22,23,11,24]
pairs = np.array(pairs).reshape(-1,2)
colors = [255., 0., 85.,
255., 0., 0.,
255., 85., 0.,
255., 170., 0.,
255., 255., 0.,
170., 255., 0.,
85., 255., 0.,
0., 255., 0.,
255., 0., 0.,
0., 255., 85.,
0., 255., 170.,
0., 255., 255.,
0., 170., 255.,
0., 85., 255.,
0., 0., 255.,
255., 0., 170.,
170., 0., 255.,
255., 0., 255.,
85., 0., 255.,
0., 0., 255.,
0., 0., 255.,
0., 0., 255.,
0., 255., 255.,
0., 255., 255.,
0., 255., 255.]
colors = np.array(colors).reshape(-1,3)
pose_scales = [1]
return render_keypoints(img, body_keypoints, pairs, colors, thickness_circle_ratio, thickness_line_ratio_wrt_circle, pose_scales, 0.1)
def render_openpose(img: np.array,
hand_keypoints: np.array) -> np.array:
"""
Render keypoints in the OpenPose format on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
body_keypoints (np.array): Keypoint array of shape (N, 3); 3 <====> (x, y, confidence).
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
#img = render_body_keypoints(img, body_keypoints)
img = render_hand_keypoints(img, hand_keypoints)
return img
###################################################
class StageLoss(nn.Module):
def __init__(self, loss_weights, **kwargs):
super().__init__()
self.cur_optim_step = 0
self.set_loss_weights(loss_weights)
print(kwargs)
self.setup_losses(loss_weights, **kwargs)
def setup_losses(self, *args, **kwargs):
raise NotImplementedError
def set_loss_weights(self, loss_weights):
self.loss_weights = loss_weights
Logger.log("Stage loss weights set to:")
Logger.log(self.loss_weights)
class RootLoss(StageLoss):
def setup_losses(
self,
loss_weights,
ignore_op_joints=None,
joints2d_sigma=100,
use_chamfer=False,
robust_loss="none",
robust_tuning_const=4.6851,
faces=None
):
self.joints2d_loss = Joints2DLoss(ignore_op_joints, joints2d_sigma)
self.points3d_loss = Points3DLoss(use_chamfer, robust_loss, robust_tuning_const)
self.inter_penetration_loss = GeneralContactLoss(faces)
self.bio_loss = BMCLoss(lambda_bl=1, lambda_rb=1, lambda_a=1)
def forward(self, observed_data, pred_data, valid_mask=None):
"""
For fitting just global root trans/orientation.
Only computes joint/point/vert losses, i.e. no priors.
"""
stats_dict = dict()
loss = 0.0
# Joints in 3D space
if (
"joints3d" in observed_data
and "joints3d" in pred_data
and self.loss_weights["joints3d"] > 0.0
):
cur_loss = joints3d_loss(
observed_data["joints3d"], pred_data["joints3d"], valid_mask
)
loss += self.loss_weights["joints3d"] * cur_loss
stats_dict["joints3d"] = cur_loss
if (
"joints3d" in pred_data
and self.loss_weights["bio"] > 0.0
):
# print('biobiobiobiobiobiobiobiobiobiobiobio')
cur_loss, _ = self.bio_loss.compute_loss(
pred_data["joints3d"], valid_mask
)
loss += self.loss_weights["bio"] * cur_loss[0]
stats_dict["bio"] = cur_loss[0]
# Select vertices in 3D space
if (
"verts3d" in observed_data
and "verts3d" in pred_data
and self.loss_weights["verts3d"] > 0.0
):
# raise ValueError
cur_loss = verts3d_loss(
observed_data["verts3d"], pred_data["verts3d"], valid_mask
)
loss += self.loss_weights["verts3d"] * cur_loss
stats_dict["verts3d"] = cur_loss
# All vertices to non-corresponding observed points in 3D space
if (
"points3d" in observed_data
and "points3d" in pred_data
and self.loss_weights["points3d"] > 0.0
):
# raise ValueError
cur_loss = self.points3d_loss(
observed_data["points3d"], pred_data["points3d"]
)
loss += self.loss_weights["points3d"] * cur_loss
stats_dict["points3d"] = cur_loss
# 2D re-projection loss
if (
"joints2d" in observed_data
and "joints3d_op" in pred_data
and "cameras" in pred_data
and self.loss_weights["joints2d"] > 0.0
):
# print(4444444444444444444)
joints2d = cam_util.reproject(
pred_data["joints3d_op"], *pred_data["cameras"]
)
cur_loss = self.joints2d_loss(
observed_data["joints2d"], joints2d, valid_mask
)
loss += self.loss_weights["joints2d"] * cur_loss
stats_dict["joints2d"] = cur_loss
# smooth 3d joint motion
if self.loss_weights["joints3d_smooth"] > 0.0:
# print(55555555555555555555)
cur_loss = joints3d_smooth_loss(pred_data["joints3d"], valid_mask)
loss += self.loss_weights["joints3d_smooth"] * cur_loss
stats_dict["joints3d_smooth"] = cur_loss
# If we're optimizing cameras, camera reprojection loss
if "bg2d_err" in pred_data and self.loss_weights["bg2d"] > 0.0:
raise ValueError
cur_loss = pred_data["bg2d_err"]
loss += self.loss_weights["bg2d"] * cur_loss
stats_dict["bg2d_err"] = cur_loss
# camera smoothness
if "cam_R" in pred_data and self.loss_weights["cam_R_smooth"] > 0.0:
raise ValueError
cam_R = pred_data["cam_R"] # (T, 3, 3)
cur_loss = rotation_smoothness_loss(cam_R[1:], cam_R[:-1])
loss += self.loss_weights["cam_R_smooth"] * cur_loss
stats_dict["cam_R_smooth"] = cur_loss
if "cam_t" in pred_data and self.loss_weights["cam_t_smooth"] > 0.0:
raise ValueError
cam_t = pred_data["cam_t"] # (T, 3, 3)
cur_loss = translation_smoothness_loss(cam_t[1:], cam_t[:-1])
loss += self.loss_weights["cam_t_smooth"] * cur_loss
stats_dict["cam_t_smooth"] = cur_loss
# Depth constraint: keep hand in front of camera
if (
"joints3d" in pred_data
and "cameras" in pred_data
and self.loss_weights.get("depth_constraint", 0.0) > 0.0
):
# Extract cam_R and cam_t from cameras tuple
# cameras = (cam_R, cam_t, cam_f, cam_center)
cam_R, cam_t, cam_f, cam_center = pred_data["cameras"]
cur_loss = depth_constraint_loss(
pred_data["joints3d"],
cam_R,
cam_t,
min_depth=0.0,
max_depth=999
)
loss += self.loss_weights["depth_constraint"] * cur_loss
stats_dict["depth_constraint"] = cur_loss
print(stats_dict)
return loss, stats_dict
def rotation_smoothness_loss(R1, R2):
R12 = torch.einsum("...ij,...jk->...ik", R2, R1.transpose(-1, -2))
aa12 = rotation_matrix_to_angle_axis(R12)
return torch.sum(aa12**2)
def translation_smoothness_loss(t1, t2):
return torch.sum((t2 - t1) ** 2)
def depth_constraint_loss(joints3d, cam_R, cam_t, min_depth=0.0, max_depth=999):
"""
Penalize joints that are behind the camera or too far away.
Ensures hand stays within reasonable depth range in camera space.
:param joints3d (B, T, J, 3) or (T, J, 3) joints in world space
:param cam_R (B, T, 3, 3) or (T, 3, 3) world-to-camera rotation
:param cam_t (B, T, 3) or (T, 3) world-to-camera translation
:param min_depth minimum allowed Z in camera space (default 0.1m)
:param max_depth maximum allowed Z in camera space (default 5.0m)
"""
# Handle both (B, T, J, 3) and (T, J, 3) shapes
if joints3d.ndim == 3:
# (T, J, 3) -> add batch dimension
joints3d = joints3d.unsqueeze(0) # (1, T, J, 3)
if cam_R.ndim == 3:
# (T, 3, 3) -> add batch dimension
cam_R = cam_R.unsqueeze(0) # (1, T, 3, 3)
if cam_t.ndim == 2:
# (T, 3) -> add batch dimension
cam_t = cam_t.unsqueeze(0) # (1, T, 3)
# Transform joints to camera space
B, T, J, _ = joints3d.shape
joints_cam = torch.einsum("btij,btjk->btik", cam_R, joints3d.transpose(-1, -2)).transpose(-1, -2)
joints_cam = joints_cam + cam_t[..., None, :] # (B, T, J, 3)
# Get Z coordinate (depth)
depth = joints_cam[..., 2] # (B, T, J)
# Penalize negative depth (behind camera) heavily
behind_camera_loss = torch.sum(torch.relu(-depth) ** 2)
# Penalize depth outside reasonable range
too_close_loss = torch.sum(torch.relu(min_depth - depth) ** 2)
too_far_loss = torch.sum(torch.relu(depth - max_depth) ** 2)
return behind_camera_loss * 100.0 + too_close_loss + too_far_loss
def camera_smoothness_loss(R1, t1, R2, t2):
"""
:param R1, t1 (N, 3, 3), (N, 3)
:param R2, t2 (N, 3, 3), (N, 3)
"""
R12, t12 = cam_util.compose_cameras(R2, t2, *cam_util.invert_camera(R1, t1))
aa12 = rotation_matrix_to_angle_axis(R12)
return torch.sum(aa12**2) + torch.sum(t12**2)
"""
Losses are cumulative
SMPLLoss setup is same as RootLoss
"""
class SMPLLoss(RootLoss):
def forward(self, observed_data, pred_data, nsteps, valid_mask=None):
"""
For fitting full shape and pose of SMPL.
nsteps used to scale single-step losses
"""
loss, stats_dict = super().forward(
observed_data, pred_data, valid_mask=valid_mask
)
# prior to keep latent pose likely
print(f"DEBUG pose_prior check: 'latent_pose' in pred_data = {'latent_pose' in pred_data}, loss_weight = {self.loss_weights.get('pose_prior', 'NOT_FOUND')}")
if "latent_pose" in pred_data:
print(f" latent_pose shape: {pred_data['latent_pose'].shape}")
if "init_latent_pose" in observed_data:
print(f" init_latent_pose shape: {observed_data['init_latent_pose'].shape}")
if "latent_pose" in pred_data and self.loss_weights["pose_prior"] > 0.0:
# Use initial latent pose from HaMeR as the prior target
latent_pose_init = observed_data["init_latent_pose"]
cur_loss = pose_prior_loss(pred_data["latent_pose"], latent_pose_init, valid_mask)
print("pose_prior: ", cur_loss, pred_data["latent_pose"][0],latent_pose_init[0])
loss += self.loss_weights["pose_prior"] * cur_loss
stats_dict["pose_prior"] = cur_loss
else:
raise ValueError
# prior to keep PCA shape likely
if "betas" in pred_data and self.loss_weights["shape_prior"] > 0.0:
cur_loss = shape_prior_loss(pred_data["betas"])
loss += self.loss_weights["shape_prior"] * nsteps * cur_loss
stats_dict["shape_prior"] = cur_loss
# inter-penetration loss
if 'verts3d' in pred_data and self.loss_weights["penetration"] > 0.0:
print('???????????????????????????????? inter-penetration loss')
# print(pred_data["verts3d"].shape, pred_data['is_right'].shape)
order = torch.sum(pred_data['is_right'], dim=-1)//pred_data['is_right'].shape[1]
if len(order) > 1:
cur_loss = 0
if order[0] == 0:
l_verts = pred_data["verts3d"][0]
r_verts = pred_data["verts3d"][1]
else:
l_verts = pred_data["verts3d"][1]
r_verts = pred_data["verts3d"][0]
for idx in range(len(l_verts)):
cur_loss += self.inter_penetration_loss(v1=l_verts[idx:idx+1], v2=r_verts[idx:idx+1])
loss += self.loss_weights["penetration"] * cur_loss
stats_dict["penetration"] = cur_loss
print(stats_dict)
# if 'penetration' in stats_dict.keys():
# if stats_dict['penetration'] == 0:
# print(pred_data)
# print('***************')
# print(observed_data)
# raise ValueError
return loss, stats_dict
# class MotionLoss(SMPLLoss):
# def setup_losses(
# self,
# loss_weights,
# init_motion_prior=None,
# **kwargs,
# ):
# super().setup_losses(loss_weights, **kwargs)
# if loss_weights["init_motion_prior"] > 0.0:
# self.init_motion_prior_loss = GMMPriorLoss(init_motion_prior)
# def forward(
# self,
# observed_data,
# pred_data,
# cam_pred_data,
# nsteps,
# valid_mask=None,
# init_motion_scale=1.0,
# ):
# """
# For fitting full shape and pose of SMPL with motion prior.
# pred_data is data pertinent to the canonical prior coordinate frame
# cam_pred_data is for the camera coordinate frame
# loss rather than standard normal if given.
# """
# cam_pred_data["latent_pose"] = pred_data["latent_pose"]
# loss, stats_dict = super().forward(
# observed_data, cam_pred_data, nsteps, valid_mask=valid_mask
# )
# # prior to keep latent motion likely
# if "latent_motion" in pred_data and self.loss_weights["motion_prior"] > 0.0:
# # NOTE: latent is NOT synchronized in time,
# # the mask is NOT relevant
# # Generate the async mask to properly mask motion prior loss
# # Helps to calibrate the range of 'good' loss values
# B, T, _ = pred_data["latent_motion"].shape
# device = pred_data["latent_motion"].device
# async_mask = (
# torch.arange(T, device=device)[None].expand(B, -1)
# < valid_mask.sum(dim=1)[:, None]
# )
# cur_loss = motion_prior_loss(
# pred_data["latent_motion"],
# cond_prior=pred_data.get("cond_prior", None),
# mask=async_mask,
# )
# loss += self.loss_weights["motion_prior"] * cur_loss
# stats_dict["motion_prior"] = cur_loss
# # prior to keep initial state likely
# have_init_prior_info = (
# "joints3d_init" in pred_data
# and "joints_vel" in pred_data
# and "trans_vel" in pred_data
# and "root_orient_vel" in pred_data
# )
# if have_init_prior_info and self.loss_weights["init_motion_prior"] > 0.0:
# cur_loss = self.init_motion_prior_loss(
# pred_data["joints3d_init"],
# pred_data["joints_vel"],
# pred_data["trans_vel"],
# pred_data["root_orient_vel"],
# )
# loss += (
# self.loss_weights["init_motion_prior"] * init_motion_scale * cur_loss
# ) # must scale since doesn't scale with more steps
# stats_dict["init_motion_prior"] = cur_loss
# # make sure joints consistent between SMPL and direct motion prior output
# if (
# "joints3d_rollout" in pred_data
# and "joints3d" in pred_data
# and self.loss_weights["joint_consistency"] > 0.0
# ):
# cur_loss = joint_consistency_loss(
# pred_data["joints3d"],
# pred_data["joints3d_rollout"],
# valid_mask,
# )
# loss += self.loss_weights["joint_consistency"] * cur_loss
# stats_dict["joint_consistency"] = cur_loss
# # make sure bone lengths between frames of direct motion prior output are consistent
# if "joints3d_rollout" in pred_data and self.loss_weights["bone_length"] > 0.0:
# cur_loss = bone_length_loss(pred_data["joints3d_rollout"], valid_mask)
# loss += self.loss_weights["bone_length"] * cur_loss
# stats_dict["bone_length"] = cur_loss
# # make sure rolled out joints match observations too
# if (
# "joints3d" in observed_data
# and "joints3d_rollout" in pred_data
# and self.loss_weights["joints3d_rollout"] > 0.0
# ):
# raise ValueError
# # cur_loss = joints3d_loss(
# # observed_data["joints3d"], pred_data["joints3d_rollout"], valid_mask
# # )
# # loss += self.loss_weights["joints3d_rollout"] * cur_loss
# # stats_dict["joints3d_rollout"] = cur_loss
# # velocity 0 during contacts
# if (
# self.loss_weights["contact_vel"] > 0.0
# and "contacts_conf" in pred_data
# and "joints3d" in pred_data
# ):
# raise ValueError
# # cur_loss = contact_vel_loss(
# # pred_data["contacts_conf"], pred_data["joints3d"], valid_mask
# # )
# # loss += self.loss_weights["contact_vel"] * cur_loss
# # stats_dict["contact_vel"] = cur_loss
# # contacting joints are near the floor
# if (
# self.loss_weights["contact_height"] > 0.0
# and "contacts_conf" in pred_data
# and "joints3d" in pred_data
# ):
# raise ValueError
# # cur_loss = contact_height_loss(
# # pred_data["contacts_conf"], pred_data["joints3d"], valid_mask
# # )
# # loss += self.loss_weights["contact_height"] * cur_loss
# # stats_dict["contact_height"] = cur_loss
# # floor is close to the initialization
# if (
# self.loss_weights["floor_reg"] > 0.0
# and "floor_plane" in pred_data
# and "floor_plane" in observed_data
# ):
# raise ValueError
# # cur_loss = floor_reg_loss(
# # pred_data["floor_plane"], observed_data["floor_plane"]
# # )
# # loss += self.loss_weights["floor_reg"] * nsteps * cur_loss
# # stats_dict["floor_reg"] = cur_loss
# return loss, stats_dict
def joints3d_loss(joints3d_obs, joints3d_pred, mask=None):
"""
:param joints3d_obs (B, T, J, 3)
:param joints3d_pred (B, T, J, 3)
:param mask (optional) (B, T)
"""
B, T, *dims = joints3d_obs.shape
vis_mask = get_visible_mask(joints3d_obs)
if mask is not None:
vis_mask = vis_mask & mask.reshape(B, T, *(1,) * len(dims)).bool()
loss = (joints3d_obs[vis_mask] - joints3d_pred[vis_mask]) ** 2
loss = 0.5 * torch.sum(loss)
return loss
def verts3d_loss(verts3d_obs, verts3d_pred, mask=None):
"""
:param verts3d_obs (B, T, V, 3)
:param verts3d_pred (B, T, V, 3)
:param mask (optional) (B, T)
"""
B, T, *dims = verts3d_obs.shape
vis_mask = get_visible_mask(verts3d_obs)
if mask is not None:
assert mask.shape == (B, T)
vis_mask = vis_mask & mask.reshape(B, T, *(1,) * len(dims)).bool()
loss = (verts3d_obs[vis_mask] - verts3d_pred[vis_mask]) ** 2
loss = 0.5 * torch.sum(loss)
return loss
def get_visible_mask(obs_data):
"""
Given observed data gets the mask of visible data (that actually contributes to the loss).
"""
return torch.logical_not(torch.isinf(obs_data))
class Joints2DLoss(nn.Module):
def __init__(self, ignore_op_joints=None, joints2d_sigma=100, normalize_by_scale=True):
super().__init__()
self.ignore_op_joints = ignore_op_joints
self.joints2d_sigma = joints2d_sigma
self.normalize_by_scale = normalize_by_scale
def forward(self, joints2d_obs, joints2d_pred, mask=None):
"""
:param joints2d_obs (B, T, 25, 3)
:param joints2d_pred (B, T, 22, 2)
:param mask (optional) (B, T)
"""
# Safety check: detect NaN in predictions before computing loss
if torch.isnan(joints2d_pred).any() or torch.isinf(joints2d_pred).any():
print(f"ERROR: joints2d_pred contains NaN or Inf!")
print(f" NaN count: {torch.isnan(joints2d_pred).sum().item()}")
print(f" This means MANO model produced invalid output")
return torch.tensor(1e8, device=joints2d_pred.device, requires_grad=True)
if mask is not None:
mask = mask.bool()
joints2d_obs = joints2d_obs[mask] # (N, 25, 3)
joints2d_pred = joints2d_pred[mask] # (N, 22, 2)
joints2d_obs_conf = joints2d_obs[..., 2:3]
if self.ignore_op_joints is not None:
# set confidence to 0 so not weighted
joints2d_obs_conf[..., self.ignore_op_joints, :] = 0.0
# Compute error
error = joints2d_pred - joints2d_obs[..., :2] # (N, 22, 2)
# Normalize by hand scale to make loss resolution-independent
if self.normalize_by_scale:
# Estimate hand scale from observed keypoints spread
valid_obs = joints2d_obs[:, :, :2] # (N, 25, 2)
# Compute bbox size of observed keypoints for each hand
kp_min = valid_obs.min(dim=1, keepdim=True)[0] # (N, 1, 2)
kp_max = valid_obs.max(dim=1, keepdim=True)[0] # (N, 1, 2)
hand_scale = torch.sqrt(((kp_max - kp_min) ** 2).sum(dim=-1, keepdim=True)) # (N, 1, 1)
# Safety check: detect invalid/degenerate cases
if torch.isnan(hand_scale).any() or torch.isinf(hand_scale).any() or (hand_scale < 5.0).any():
print(f"ERROR: Invalid hand_scale! min={hand_scale.min():.2f}, max={hand_scale.max():.2f}")
print(f" This indicates MANO produced degenerate output (collapsed joints)")
print(f" kp_min: {kp_min[0, 0]}, kp_max: {kp_max[0, 0]}")
# Return a large constant loss to signal the optimizer this is a bad configuration
return torch.tensor(1e6, device=hand_scale.device, requires_grad=True)
# Clamp to reasonable range (5-1000 pixels)
hand_scale = torch.clamp(hand_scale, min=5.0, max=1000.0)
# Normalize error by hand scale AND scale up to reasonable magnitude
error = error / hand_scale * 100.0 # Now error is in normalized units (0-100 scale)
# Use FIXED sigma in normalized space (not divided by hand_scale!)
normalized_sigma = self.joints2d_sigma # Already in normalized units
else:
normalized_sigma = self.joints2d_sigma
# weight errors by detection confidence
robust_sqr_dist = gmof(error, normalized_sigma)
reproj_err = (joints2d_obs_conf**2) * robust_sqr_dist
loss = torch.mean(reproj_err)
return loss
# class Joints2DLoss(nn.Module):
# def __init__(self, ignore_op_joints=None, joints2d_sigma=100):
# super().__init__()
# self.ignore_op_joints = ignore_op_joints
# self.joints2d_sigma = joints2d_sigma
# def forward(self, joints2d_obs, joints2d_pred, mask=None):
# """
# :param joints2d_obs (B, T, 25, 3)
# :param joints2d_pred (B, T, 22, 2)
# :param mask (optional) (B, T)
# """
# if mask is not None:
# mask = mask.bool()
# joints2d_obs = joints2d_obs[mask] # (N, 25, 3)
# joints2d_pred = joints2d_pred[mask] # (N, 22, 2)
# joints2d_obs_conf = joints2d_obs[..., 2:3]
# if self.ignore_op_joints is not None:
# # set confidence to 0 so not weighted
# joints2d_obs_conf[..., self.ignore_op_joints, :] = 0.0
# # Compute per-joint pixel errors
# error = joints2d_pred - joints2d_obs[..., :2] # (N, J, 2)
# pixel_error = torch.sqrt((error**2).sum(dim=-1, keepdim=True)) # (N, J, 1)
# # Use Smooth L1 (Huber) loss: L2 for small errors, L1 for large errors
# # This prevents gradient explosion while keeping strong gradients for normal errors
# # Beta=1000 means: errors <1000px get L2 (strong gradients), errors >1000px get L1 (bounded)
# beta = 1000.0 # Transition point: L2 below, L1 above
# smooth_l1 = torch.nn.functional.smooth_l1_loss(
# joints2d_pred,
# joints2d_obs[..., :2],
# reduction='none',
# beta=beta
# ) # (N, J, 2)
# sqr_dist = smooth_l1
# # DEBUG
# print(f'Pixel errors: mean={pixel_error.mean():.1f}, max={pixel_error.max():.1f}, median={pixel_error.median():.1f}')
# num_large = (pixel_error > beta).sum().item()
# print(f'L2 loss for {pixel_error.numel() - num_large}/{pixel_error.numel()} joints, L1 for {num_large} large >{beta}px')
# print(f'Loss stats: mean={sqr_dist.mean():.2f}, max={sqr_dist.max():.2f}')
# # Weight by confidence and compute loss
# reproj_err = (joints2d_obs_conf**2) * sqr_dist
# reproj_err = torch.clamp(reproj_err, max=1000)
# loss = torch.mean(reproj_err)
# return loss
class Points3DLoss(nn.Module):
def __init__(
self,
use_chamfer=False,
robust_loss="bisquare",
robust_tuning_const=4.6851,
):
super().__init__()
if not use_chamfer:
self.active = False
return
self.active = True
robust_choices = ["none", "bisquare", "gm"]
if robust_loss not in robust_choices:
Logger.log(
"Not a valid robust loss: %s. Please use %s"
% (robust_loss, str(robust_choices))
)
exit()
from utils.chamfer_distance import ChamferDistance
self.chamfer_dist = ChamferDistance()
self.robust_loss = robust_loss
self.robust_tuning_const = robust_tuning_const
def forward(self, points3d_obs, points3d_pred):
if not self.active:
return torch.tensor(0.0, dtype=torch.float32, device=points3d_obs.device)
# one-way chamfer
B, T, N_obs, _ = points3d_obs.size()
N_pred = points3d_pred.size(2)
points3d_obs = points3d_obs.reshape((B * T, -1, 3))
points3d_pred = points3d_pred.reshape((B * T, -1, 3))
obs2pred_sqr_dist, pred2obs_sqr_dist = self.chamfer_dist(
points3d_obs, points3d_pred
)
obs2pred_sqr_dist = obs2pred_sqr_dist.reshape((B, T * N_obs))
pred2obs_sqr_dist = pred2obs_sqr_dist.reshape((B, T * N_pred))
weighted_obs2pred_sqr_dist, w = apply_robust_weighting(
obs2pred_sqr_dist.sqrt(),
robust_loss_type=self.robust_loss,
robust_tuning_const=self.robust_tuning_const,
)
loss = torch.sum(weighted_obs2pred_sqr_dist)
loss = 0.5 * loss
return loss
######################
# losses
######################
class GeneralContactLoss(nn.Module):
def __init__(
self,
faces,
region_aggregation_type: str = 'sum',
squared_dist: bool = False,
model_type: str = 'mano',
body_model_utils_folder: str = 'body_model_utils',
**kwargs
):
super().__init__()
"""
Compute intersection and contact between two meshes and resolves.
"""
self.region_aggregation_type = region_aggregation_type
self.squared = squared_dist
self.criterion = self.init_loss()
# create extra vertex and faces to close back of the mouth to maske
# the mano mesh watertight.
self.model_type = model_type
if self.model_type == 'mano':
# add faces that make the hand mesh watertight
faces_new = torch.tensor([[92, 38, 234],
[234, 38, 239],
[38, 122, 239],
[239, 122, 279],
[122, 118, 279],
[279, 118, 215],
[118, 117, 215],
[215, 117, 214],
[117, 119, 214],
[214, 119, 121],
[119, 120, 121],
[121, 120, 78],
[120, 108, 78],
[78, 108, 79]])
r_faces = torch.cat([faces, faces_new.to(faces.device)], dim=0)
l_faces = r_faces[:,[0,2,1]].clone()
self.register_buffer('l_faces', l_faces)
self.register_buffer('r_faces', r_faces)
# low resolution mesh
# inner_mouth_verts_path = f'{body_model_utils_folder}/lowres_{model_type}.pkl'
# self.low_res_mesh = pickle.load(open(inner_mouth_verts_path, 'rb'))
# def close_mouth(self, v):
# mv = torch.mean(v[:,self.vert_ids_wt,:], 1, keepdim=True)
# v = torch.cat((v, mv), 1)
# return v
def to_lowres(self, v, is_right):
v = v
if is_right == 0:
t = v[:,self.l_faces,:]
elif is_right == 1:
t = v[:,self.r_faces,:]
else:
raise ValueError
return v, t
def init_loss(self):
def loss_func(v1, v2, factor=100, wn_batch=True):
"""
Compute loss between region r1 on meshes v1 and
region r2 on mesh v2.
"""
nn = 1000
loss = torch.tensor(0.0, device=v1.device)
if wn_batch:
# close mouth for self-intersection test
v1l, t1l = self.to_lowres(v1, 0)
v2l, t2l = self.to_lowres(v2, 1)
# compute intersection between v1 and v2
# print(v1.shape, t1l.shape)
# exit()
interior_v1 = winding_numbers(v1, t2l).ge(0.99)
interior_v2 = winding_numbers(v2, t1l).ge(0.99)
# visu results
# if True:
# import trimesh
# mesh = trimesh.Trimesh(
# vertices=v2l[0].detach().cpu().numpy(),
# faces=self.l_faces.cpu().numpy().astype(np.int32),
# )
# col = 255 * np.ones((778, 4))
# inside_idx = torch.where(interior_v2[0])[0].detach().cpu().numpy()
# col[inside_idx] = [0, 255, 0, 255]
# mesh.visual.vertex_colors = col
# _ = mesh.export('/vol/bitbucket/zy3023/code/hand/slam-hand//interior_v2_lowres.obj')
# vmc = v1 # self.close_mouth(v1)
# mesh = trimesh.Trimesh(vertices=vmc[0].detach().cpu().numpy(), faces=self.l_faces.cpu().numpy())
# col = 255 * np.ones((778, 4))
# inside_idx = torch.where(interior_v1[0])[0].detach().cpu().numpy()
# col[inside_idx] = [255, 0, 0, 255]
# mesh.visual.vertex_colors = col
# _ = mesh.export('/vol/bitbucket/zy3023/code/hand/slam-hand//interior_v1_highres.obj')
# # Other direction
# mesh = trimesh.Trimesh(
# vertices=v1l[0].detach().cpu().numpy(),
# faces=self.r_faces.cpu().numpy().astype(np.int32),
# )
# col = 255 * np.ones((778, 4))
# inside_idx = torch.where(interior_v1[0])[0].detach().cpu().numpy()
# col[inside_idx] = [0, 255, 0, 255]
# mesh.visual.vertex_colors = col
# _ = mesh.export('/vol/bitbucket/zy3023/code/hand/slam-hand//interior_v1_lowres.obj')
# vmc = v2 # self.close_mouth(v2)
# mesh = trimesh.Trimesh(vertices=vmc[0].detach().cpu().numpy(), faces=self.r_faces.cpu().numpy())
# col = 255 * np.ones((778, 4))
# inside_idx = torch.where(interior_v2[0])[0].detach().cpu().numpy()
# col[inside_idx] = [255, 0, 0, 255]
# mesh.visual.vertex_colors = col
# _ = mesh.export('/vol/bitbucket/zy3023/code/hand/slam-hand//interior_v2_highres.obj')
# exit()
batch_losses = []
for bidx in range(v1.shape[0]):
if not wn_batch:
# close mouth for self-intersection test
v1l, t1l = self.to_lowres(v1[[bidx]], nn)
v2l, t2l = self.to_lowres(v2[[bidx]], nn)
# compute intersection between v1 and v2
curr_interior_v1 = winding_numbers(v1[[bidx]], t2l.detach()).ge(0.99)[0]
curr_interior_v2 = winding_numbers(v2[[bidx]], t1l.detach()).ge(0.99)[0]
crit_v1, crit_v2 = torch.any(curr_interior_v1), torch.any(curr_interior_v2)
else:
curr_interior_v1 = interior_v1[bidx]
curr_interior_v2 = interior_v2[bidx]
crit_v1, crit_v2 = torch.any(interior_v1[bidx]), torch.any(interior_v2[bidx])
bloss = torch.tensor(0.0, device=v1.device)
if crit_v1 and crit_v2:
# find vertices that are close to each other between v1 and v2
#squared_dist = pcl_pcl_pairwise_distance(
# v1[:,interior_v1[bidx],:], v2[:, interior_v2[bidx], :], squared=self.squared
#)
squared_dist_v1v2 = pcl_pcl_pairwise_distance(
v1[[[bidx]],curr_interior_v1,:], v2[[bidx]], squared=self.squared)
squared_dist_v2v1 = pcl_pcl_pairwise_distance(
v2[[[bidx]], curr_interior_v2, :], v1[[bidx]], squared=self.squared)
v1_to_v2 = (squared_dist_v1v2[0].min(1)[0] * factor)**2
#v1_to_v2 = 10.0 * (torch.tanh(v1_to_v2 / 10.0)**2)
bloss += v1_to_v2.sum()
v2_to_v1 = (squared_dist_v2v1[0].min(1)[0] * factor)**2
#v2_to_v1 = 10.0 * (torch.tanh(v2_to_v1 / 10.0)**2)
bloss += v2_to_v1.sum()
batch_losses.append(bloss)
# compute loss
if len(batch_losses) > 0:
loss = sum(batch_losses) / len(batch_losses)
#if loss > 1.0:
# import ipdb; ipdb.set_trace()
return loss
return loss_func
def forward(self, **args):
losses = self.criterion(**args)
return losses
def pose_prior_loss(latent_pose_pred, latent_pose_init=None, mask=None):
"""
:param latent_pose_pred (B, T, D) - optimized latent pose
:param latent_pose_init (optional) (B, T, D) - initial HaMeR prediction
:param mask (optional) (B, T)
"""
if latent_pose_init is not None:
# Penalize deviation from original HaMeR prediction
loss = (latent_pose_pred - latent_pose_init)**2
else:
# Fallback: prior is isotropic gaussian so take L2 distance from 0
loss = latent_pose_pred**2
if mask is not None:
loss = loss[mask.bool()]
loss = torch.sum(loss)
return loss
def shape_prior_loss(betas_pred):
# prior is isotropic gaussian so take L2 distance from 0
loss = betas_pred**2
loss = torch.sum(loss)
return loss
def joints3d_smooth_loss(joints3d_pred, mask=None, normalize_by_scale=True):
"""
:param joints3d_pred (B, T, J, 3)
:param mask (optional) (B, T)
:param normalize_by_scale: If True, normalize by hand scale for size-invariance
"""
# Safety check: detect NaN in predictions before computing loss
if torch.isnan(joints3d_pred).any() or torch.isinf(joints3d_pred).any():
print(f"ERROR: joints3d_pred contains NaN or Inf in smooth loss!")
print(f" NaN count: {torch.isnan(joints3d_pred).sum().item()}")
return torch.tensor(1e8, device=joints3d_pred.device, requires_grad=True)
B, T, *dims = joints3d_pred.shape
# Normalize by hand scale FIRST to make loss size-invariant
if normalize_by_scale:
# Estimate hand scale from 3D joint positions at each frame
# Use bbox of joints as scale reference
joints_min = joints3d_pred.min(dim=2, keepdim=True)[0] # (B, T, 1, 3)
joints_max = joints3d_pred.max(dim=2, keepdim=True)[0] # (B, T, 1, 3)
hand_scale = torch.sqrt(((joints_max - joints_min) ** 2).sum(dim=-1, keepdim=True)) # (B, T, 1, 1)
# Safety check: detect invalid/degenerate cases
if torch.isnan(hand_scale).any() or torch.isinf(hand_scale).any() or (hand_scale < 0.001).any():
print(f"ERROR: Invalid hand_scale in joints3d_smooth! min={hand_scale.min():.4f}, max={hand_scale.max():.4f}")
print(f" This indicates MANO produced degenerate 3D output")
# Return a large constant loss
return torch.tensor(1e6, device=hand_scale.device, requires_grad=True)
# Clamp to reasonable range (0.001m - 1m for hand size)
hand_scale = torch.clamp(hand_scale, min=0.001, max=1.0)
# Normalize joints FIRST by their own scale at each frame (same as joints2d)
joints3d_pred_normalized = joints3d_pred / hand_scale # (B, T, J, 3) - same scale factor as joints2d
else:
joints3d_pred_normalized = joints3d_pred
# Now compute delta on normalized joints
delta = joints3d_pred_normalized[:, 1:, :, :] - joints3d_pred_normalized[:, :-1, :, :] # (B, T-1, J, 3)
loss = delta ** 2
if mask is not None:
mask = mask.bool()
mask = mask[:, 1:] & mask[:, :-1]
loss = loss[mask]
loss = torch.mean(loss, dim=0)
loss = 0.5 * torch.sum(loss)
return loss
# def motion_prior_loss(latent_motion_pred, cond_prior=None, mask=None):
# """
# :param latent_motion_pred (B, T, D)
# :param cond_prior (optional) (B, T, D, 2) stacked mean and var of post distribution
# :param mask (optional) (B, T)
# """
# if mask is not None:
# latent_motion_pred = latent_motion_pred[mask.bool()]
# if cond_prior is None:
# # assume standard normal
# loss = latent_motion_pred**2
# else:
# pm, pv = cond_prior[..., 0], cond_prior[..., 1]
# if mask is not None:
# mask = mask.bool()
# pm, pv = pm[mask], pv[mask]
# loss = -log_normal(latent_motion_pred, pm, pv)
# return torch.sum(loss)
# class GMMPriorLoss(nn.Module):
# def __init__(self, init_motion_prior=None):
# super().__init__()
# if init_motion_prior is None:
# self.active = False
# return
# # build pytorch GMM
# self.active = True
# self.init_motion_prior = dict()
# gmm = build_gmm(*init_motion_prior["gmm"])
# self.init_motion_prior["gmm"] = gmm
# def forward(self, joints, joints_vel, trans_vel, root_orient_vel):
# if not self.active:
# return torch.tensor(0.0, dtype=torch.float32, device=joints.device)
# # create input
# B = joints.size(0)
# joints = joints.reshape((B, -1))
# joints_vel = joints_vel.reshape((B, -1))
# trans_vel = trans_vel.reshape((B, -1))
# root_orient_vel = root_orient_vel.reshape((B, -1))
# init_state = torch.cat([joints, joints_vel, trans_vel, root_orient_vel], dim=-1)
# loss = -self.init_motion_prior["gmm"].log_prob(init_state)
# loss = torch.sum(loss)
# return loss
# def build_gmm(gmm_weights, gmm_means, gmm_covs):
# mix = Categorical(gmm_weights)
# comp = MultivariateNormal(gmm_means, covariance_matrix=gmm_covs)
# gmm_distrib = MixtureSameFamily(mix, comp)
# return gmm_distrib
# def joint_consistency_loss(smpl_joints3d, rollout_joints3d, mask=None):
# """
# :param smpl_joints3d (B, T, J, 3)
# :param rollout_joints3d (B, T, J, 3)
# :param mask (optional) (B, T)
# """
# loss = (smpl_joints3d - rollout_joints3d) ** 2
# if mask is not None:
# loss = loss[mask.bool()]
# loss = 0.5 * torch.sum(loss)
# return loss
# def bone_length_loss(rollout_joints3d, mask=None):
# bones = rollout_joints3d[:, :, 1:]
# parents = rollout_joints3d[:, :, SMPL_PARENTS[1:]]
# bone_lengths = torch.norm(bones - parents, dim=-1)
# loss = bone_lengths[:, 1:] - bone_lengths[:, :-1]
# if mask is not None:
# mask = mask.bool()
# mask = mask[:, 1:] & mask[:, :-1]
# loss = loss[mask]
# loss = 0.5 * torch.sum(loss**2)
# return loss
def kl_normal(qm, qv, pm, pv):
"""
Computes the elem-wise KL divergence between two normal distributions KL(q || p) and
sum over the last dimension
​
Args:
qm: tensor: (batch, dim): q mean
qv: tensor: (batch, dim): q variance
pm: tensor: (batch, dim): p mean
pv: tensor: (batch, dim): p variance
​
Return:
kl: tensor: (batch,): kl between each sample
"""
element_wise = 0.5 * (
torch.log(pv) - torch.log(qv) + qv / pv + (qm - pm).pow(2) / pv - 1
)
kl = element_wise.sum(-1)
return kl
def log_normal(x, m, v):
"""
Computes the elem-wise log probability of a Gaussian and then sum over the
last dim. Basically we're assuming all dims are batch dims except for the
last dim. Args:
x: tensor: (batch_1, batch_2, ..., batch_k, dim): Observation
m: tensor: (batch_1, batch_2, ..., batch_k, dim): Mean
v: tensor: (batch_1, batch_2, ..., batch_k, dim): Variance Return:
log_prob: tensor: (batch_1, batch_2, ..., batch_k): log probability of
each sample. Note that the summation dimension is not kept
"""
log_prob = (
-torch.log(torch.sqrt(v))
- math.log(math.sqrt(2 * math.pi))
- ((x - m) ** 2 / (2 * v))
)
log_prob = torch.sum(log_prob, dim=-1)
return log_prob
def apply_robust_weighting(
res, robust_loss_type="bisquare", robust_tuning_const=4.6851
):
"""
Returns robustly weighted squared residuals.
- res : torch.Tensor (B x N), take the MAD over each batch dimension independently.
"""
robust_choices = ["none", "bisquare"]
if robust_loss_type not in robust_choices:
print(
"Not a valid robust loss: %s. Please use %s"
% (robust_loss_type, str(robust_choices))
)
w = None
detach_res = (
res.clone().detach()
) # don't want gradients flowing through the weights to avoid degeneracy
if robust_loss_type == "none":
w = torch.ones_like(detach_res)
elif robust_loss_type == "bisquare":
w = bisquare_robust_weights(detach_res, tune_const=robust_tuning_const)
# apply weights to squared residuals
weighted_sqr_res = w * (res**2)
return weighted_sqr_res, w
def robust_std(res):
"""
Compute robust estimate of standarad deviation using median absolute deviation (MAD)
of the given residuals independently over each batch dimension.
- res : (B x N)
Returns:
- std : B x 1
"""
B = res.size(0)
med = torch.median(res, dim=-1)[0].reshape((B, 1))
abs_dev = torch.abs(res - med)
MAD = torch.median(abs_dev, dim=-1)[0].reshape((B, 1))
std = MAD / 0.67449
return std
def bisquare_robust_weights(res, tune_const=4.6851):
"""
Bisquare (Tukey) loss.
See https://www.mathworks.com/help/curvefit/least-squares-fitting.html
- residuals
"""
# print(res.size())
norm_res = res / (robust_std(res) * tune_const)
# NOTE: this should use absolute value, it's ok right now since only used for 3d point cloud residuals
# which are guaranteed positive, but generally this won't work)
outlier_mask = norm_res >= 1.0
# print(torch.sum(outlier_mask))
# print('Outlier frac: %f' % (float(torch.sum(outlier_mask)) / res.size(1)))
w = (1.0 - norm_res**2) ** 2
w[outlier_mask] = 0.0
return w
def gmof(res, sigma):
"""
Geman-McClure error function
- residual
- sigma scaling factor
"""
x_squared = res**2
sigma_squared = sigma**2
return (sigma_squared * x_squared) / (sigma_squared + x_squared)