HMR2.0 / hmr2 /models /hmr2.py
brjathu
Adding HF files
29a229f
raw
history blame
17 kB
import torch
import pytorch_lightning as pl
from typing import Any, Dict, Mapping, Tuple
from yacs.config import CfgNode
from ..utils import SkeletonRenderer, MeshRenderer
from ..utils.geometry import aa_to_rotmat, perspective_projection
from .backbones import create_backbone
from .heads import build_smpl_head
from .discriminator import Discriminator
from .losses import Keypoint3DLoss, Keypoint2DLoss, ParameterLoss
from . import SMPL
class HMR2(pl.LightningModule):
def __init__(self, cfg: CfgNode, init_renderer: bool = True):
"""
Setup HMR2 model
Args:
cfg (CfgNode): Config file as a yacs CfgNode
"""
super().__init__()
# Save hyperparameters
self.save_hyperparameters(logger=False, ignore=['init_renderer'])
self.cfg = cfg
# Create backbone feature extractor
self.backbone = create_backbone(cfg)
# Create SMPL head
self.smpl_head = build_smpl_head(cfg)
# Create discriminator
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
self.discriminator = Discriminator()
# Define loss functions
self.keypoint_3d_loss = Keypoint3DLoss(loss_type='l1')
self.keypoint_2d_loss = Keypoint2DLoss(loss_type='l1')
self.smpl_parameter_loss = ParameterLoss()
# Instantiate SMPL model
smpl_cfg = {k.lower(): v for k,v in dict(cfg.SMPL).items()}
self.smpl = SMPL(**smpl_cfg)
# Buffer that shows whetheer we need to initialize ActNorm layers
self.register_buffer('initialized', torch.tensor(False))
# Setup renderer for visualization
if init_renderer:
self.renderer = SkeletonRenderer(self.cfg)
self.mesh_renderer = MeshRenderer(self.cfg, faces=self.smpl.faces)
else:
self.renderer = None
self.mesh_renderer = None
# Disable automatic optimization since we use adversarial training
self.automatic_optimization = False
def get_parameters(self):
all_params = list(self.smpl_head.parameters())
all_params += list(self.backbone.parameters())
return all_params
def configure_optimizers(self) -> Tuple[torch.optim.Optimizer, torch.optim.Optimizer]:
"""
Setup model and distriminator Optimizers
Returns:
Tuple[torch.optim.Optimizer, torch.optim.Optimizer]: Model and discriminator optimizers
"""
param_groups = [{'params': filter(lambda p: p.requires_grad, self.get_parameters()), 'lr': self.cfg.TRAIN.LR}]
optimizer = torch.optim.AdamW(params=param_groups,
# lr=self.cfg.TRAIN.LR,
weight_decay=self.cfg.TRAIN.WEIGHT_DECAY)
optimizer_disc = torch.optim.AdamW(params=self.discriminator.parameters(),
lr=self.cfg.TRAIN.LR,
weight_decay=self.cfg.TRAIN.WEIGHT_DECAY)
return optimizer, optimizer_disc
def forward_step(self, batch: Dict, train: bool = False) -> Dict:
"""
Run a forward step of the network
Args:
batch (Dict): Dictionary containing batch data
train (bool): Flag indicating whether it is training or validation mode
Returns:
Dict: Dictionary containing the regression output
"""
# Use RGB image as input
x = batch['img']
batch_size = x.shape[0]
# Compute conditioning features using the backbone
# if using ViT backbone, we need to use a different aspect ratio
conditioning_feats = self.backbone(x[:,:,:,32:-32])
pred_smpl_params, pred_cam, _ = self.smpl_head(conditioning_feats)
# Store useful regression outputs to the output dict
output = {}
output['pred_cam'] = pred_cam
output['pred_smpl_params'] = {k: v.clone() for k,v in pred_smpl_params.items()}
# Compute camera translation
device = pred_smpl_params['body_pose'].device
dtype = pred_smpl_params['body_pose'].dtype
focal_length = self.cfg.EXTRA.FOCAL_LENGTH * torch.ones(batch_size, 2, device=device, dtype=dtype)
pred_cam_t = torch.stack([pred_cam[:, 1],
pred_cam[:, 2],
2*focal_length[:, 0]/(self.cfg.MODEL.IMAGE_SIZE * pred_cam[:, 0] +1e-9)],dim=-1)
output['pred_cam_t'] = pred_cam_t
output['focal_length'] = focal_length
# Compute model vertices, joints and the projected joints
pred_smpl_params['global_orient'] = pred_smpl_params['global_orient'].reshape(batch_size, -1, 3, 3)
pred_smpl_params['body_pose'] = pred_smpl_params['body_pose'].reshape(batch_size, -1, 3, 3)
pred_smpl_params['betas'] = pred_smpl_params['betas'].reshape(batch_size, -1)
smpl_output = self.smpl(**{k: v.float() for k,v in pred_smpl_params.items()}, pose2rot=False)
pred_keypoints_3d = smpl_output.joints
pred_vertices = smpl_output.vertices
output['pred_keypoints_3d'] = pred_keypoints_3d.reshape(batch_size, -1, 3)
output['pred_vertices'] = pred_vertices.reshape(batch_size, -1, 3)
pred_cam_t = pred_cam_t.reshape(-1, 3)
focal_length = focal_length.reshape(-1, 2)
pred_keypoints_2d = perspective_projection(pred_keypoints_3d,
translation=pred_cam_t,
focal_length=focal_length / self.cfg.MODEL.IMAGE_SIZE)
output['pred_keypoints_2d'] = pred_keypoints_2d.reshape(batch_size, -1, 2)
return output
def compute_loss(self, batch: Dict, output: Dict, train: bool = True) -> torch.Tensor:
"""
Compute losses given the input batch and the regression output
Args:
batch (Dict): Dictionary containing batch data
output (Dict): Dictionary containing the regression output
train (bool): Flag indicating whether it is training or validation mode
Returns:
torch.Tensor : Total loss for current batch
"""
pred_smpl_params = output['pred_smpl_params']
pred_keypoints_2d = output['pred_keypoints_2d']
pred_keypoints_3d = output['pred_keypoints_3d']
batch_size = pred_smpl_params['body_pose'].shape[0]
device = pred_smpl_params['body_pose'].device
dtype = pred_smpl_params['body_pose'].dtype
# Get annotations
gt_keypoints_2d = batch['keypoints_2d']
gt_keypoints_3d = batch['keypoints_3d']
gt_smpl_params = batch['smpl_params']
has_smpl_params = batch['has_smpl_params']
is_axis_angle = batch['smpl_params_is_axis_angle']
# Compute 3D keypoint loss
loss_keypoints_2d = self.keypoint_2d_loss(pred_keypoints_2d, gt_keypoints_2d)
loss_keypoints_3d = self.keypoint_3d_loss(pred_keypoints_3d, gt_keypoints_3d, pelvis_id=25+14)
# Compute loss on SMPL parameters
loss_smpl_params = {}
for k, pred in pred_smpl_params.items():
gt = gt_smpl_params[k].view(batch_size, -1)
if is_axis_angle[k].all():
gt = aa_to_rotmat(gt.reshape(-1, 3)).view(batch_size, -1, 3, 3)
has_gt = has_smpl_params[k]
loss_smpl_params[k] = self.smpl_parameter_loss(pred.reshape(batch_size, -1), gt.reshape(batch_size, -1), has_gt)
# # Filter out images with corresponding SMPL parameter annotations
# smpl_params = {k: v.clone() for k,v in gt_smpl_params.items()}
# smpl_params['body_pose'] = aa_to_rotmat(smpl_params['body_pose'].reshape(-1, 3)).reshape(batch_size, -1, 3, 3)[:, :, :, :2].permute(0, 1, 3, 2).reshape(batch_size, -1)
# smpl_params['global_orient'] = aa_to_rotmat(smpl_params['global_orient'].reshape(-1, 3)).reshape(batch_size, -1, 3, 3)[:, :, :, :2].permute(0, 1, 3, 2).reshape(batch_size, -1)
# smpl_params['betas'] = smpl_params['betas']
# has_smpl_params = (batch['has_smpl_params']['body_pose'] > 0)
# smpl_params = {k: v[has_smpl_params] for k, v in smpl_params.items()}
loss = self.cfg.LOSS_WEIGHTS['KEYPOINTS_3D'] * loss_keypoints_3d+\
self.cfg.LOSS_WEIGHTS['KEYPOINTS_2D'] * loss_keypoints_2d+\
sum([loss_smpl_params[k] * self.cfg.LOSS_WEIGHTS[k.upper()] for k in loss_smpl_params])
losses = dict(loss=loss.detach(),
loss_keypoints_2d=loss_keypoints_2d.detach(),
loss_keypoints_3d=loss_keypoints_3d.detach())
for k, v in loss_smpl_params.items():
losses['loss_' + k] = v.detach()
output['losses'] = losses
return loss
# Tensoroboard logging should run from first rank only
@pl.utilities.rank_zero.rank_zero_only
def tensorboard_logging(self, batch: Dict, output: Dict, step_count: int, train: bool = True, write_to_summary_writer: bool = True) -> None:
"""
Log results to Tensorboard
Args:
batch (Dict): Dictionary containing batch data
output (Dict): Dictionary containing the regression output
step_count (int): Global training step count
train (bool): Flag indicating whether it is training or validation mode
"""
mode = 'train' if train else 'val'
batch_size = batch['keypoints_2d'].shape[0]
images = batch['img']
images = images * torch.tensor([0.229, 0.224, 0.225], device=images.device).reshape(1,3,1,1)
images = images + torch.tensor([0.485, 0.456, 0.406], device=images.device).reshape(1,3,1,1)
#images = 255*images.permute(0, 2, 3, 1).cpu().numpy()
pred_keypoints_3d = output['pred_keypoints_3d'].detach().reshape(batch_size, -1, 3)
pred_vertices = output['pred_vertices'].detach().reshape(batch_size, -1, 3)
focal_length = output['focal_length'].detach().reshape(batch_size, 2)
gt_keypoints_3d = batch['keypoints_3d']
gt_keypoints_2d = batch['keypoints_2d']
losses = output['losses']
pred_cam_t = output['pred_cam_t'].detach().reshape(batch_size, 3)
pred_keypoints_2d = output['pred_keypoints_2d'].detach().reshape(batch_size, -1, 2)
if write_to_summary_writer:
summary_writer = self.logger.experiment
for loss_name, val in losses.items():
summary_writer.add_scalar(mode +'/' + loss_name, val.detach().item(), step_count)
num_images = min(batch_size, self.cfg.EXTRA.NUM_LOG_IMAGES)
gt_keypoints_3d = batch['keypoints_3d']
pred_keypoints_3d = output['pred_keypoints_3d'].detach().reshape(batch_size, -1, 3)
# We render the skeletons instead of the full mesh because rendering a lot of meshes will make the training slow.
#predictions = self.renderer(pred_keypoints_3d[:num_images],
# gt_keypoints_3d[:num_images],
# 2 * gt_keypoints_2d[:num_images],
# images=images[:num_images],
# camera_translation=pred_cam_t[:num_images])
predictions = self.mesh_renderer.visualize_tensorboard(pred_vertices[:num_images].cpu().numpy(),
pred_cam_t[:num_images].cpu().numpy(),
images[:num_images].cpu().numpy(),
pred_keypoints_2d[:num_images].cpu().numpy(),
gt_keypoints_2d[:num_images].cpu().numpy(),
focal_length=focal_length[:num_images].cpu().numpy())
if write_to_summary_writer:
summary_writer.add_image('%s/predictions' % mode, predictions, step_count)
return predictions
def forward(self, batch: Dict) -> Dict:
"""
Run a forward step of the network in val mode
Args:
batch (Dict): Dictionary containing batch data
Returns:
Dict: Dictionary containing the regression output
"""
return self.forward_step(batch, train=False)
def training_step_discriminator(self, batch: Dict,
body_pose: torch.Tensor,
betas: torch.Tensor,
optimizer: torch.optim.Optimizer) -> torch.Tensor:
"""
Run a discriminator training step
Args:
batch (Dict): Dictionary containing mocap batch data
body_pose (torch.Tensor): Regressed body pose from current step
betas (torch.Tensor): Regressed betas from current step
optimizer (torch.optim.Optimizer): Discriminator optimizer
Returns:
torch.Tensor: Discriminator loss
"""
batch_size = body_pose.shape[0]
gt_body_pose = batch['body_pose']
gt_betas = batch['betas']
gt_rotmat = aa_to_rotmat(gt_body_pose.view(-1,3)).view(batch_size, -1, 3, 3)
disc_fake_out = self.discriminator(body_pose.detach(), betas.detach())
loss_fake = ((disc_fake_out - 0.0) ** 2).sum() / batch_size
disc_real_out = self.discriminator(gt_rotmat, gt_betas)
loss_real = ((disc_real_out - 1.0) ** 2).sum() / batch_size
loss_disc = loss_fake + loss_real
loss = self.cfg.LOSS_WEIGHTS.ADVERSARIAL * loss_disc
optimizer.zero_grad()
self.manual_backward(loss)
optimizer.step()
return loss_disc.detach()
def training_step(self, joint_batch: Dict, batch_idx: int) -> Dict:
"""
Run a full training step
Args:
joint_batch (Dict): Dictionary containing image and mocap batch data
batch_idx (int): Unused.
batch_idx (torch.Tensor): Unused.
Returns:
Dict: Dictionary containing regression output.
"""
batch = joint_batch['img']
mocap_batch = joint_batch['mocap']
optimizer = self.optimizers(use_pl_optimizer=True)
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
optimizer, optimizer_disc = optimizer
# Update learning rates
self.update_learning_rates(batch_idx)
batch_size = batch['img'].shape[0]
output = self.forward_step(batch, train=True)
pred_smpl_params = output['pred_smpl_params']
if self.cfg.get('UPDATE_GT_SPIN', False):
self.update_batch_gt_spin(batch, output)
loss = self.compute_loss(batch, output, train=True)
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
disc_out = self.discriminator(pred_smpl_params['body_pose'].reshape(batch_size, -1), pred_smpl_params['betas'].reshape(batch_size, -1))
loss_adv = ((disc_out - 1.0) ** 2).sum() / batch_size
loss = loss + self.cfg.LOSS_WEIGHTS.ADVERSARIAL * loss_adv
# Error if Nan
if torch.isnan(loss):
raise ValueError('Loss is NaN')
optimizer.zero_grad()
self.manual_backward(loss)
# Clip gradient
if self.cfg.TRAIN.get('GRAD_CLIP_VAL', 0) > 0:
gn = torch.nn.utils.clip_grad_norm_(self.get_parameters(), self.cfg.TRAIN.GRAD_CLIP_VAL, error_if_nonfinite=True)
self.log('train/grad_norm', gn, on_step=True, on_epoch=True, prog_bar=True, logger=True)
optimizer.step()
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
loss_disc = self.training_step_discriminator(mocap_batch, pred_smpl_params['body_pose'].reshape(batch_size, -1), pred_smpl_params['betas'].reshape(batch_size, -1), optimizer_disc)
output['losses']['loss_gen'] = loss_adv
output['losses']['loss_disc'] = loss_disc
if self.global_step > 0 and self.global_step % self.cfg.GENERAL.LOG_STEPS == 0:
self.tensorboard_logging(batch, output, self.global_step, train=True)
self.log('train/loss', output['losses']['loss'], on_step=True, on_epoch=True, prog_bar=True, logger=False)
return output
def validation_step(self, batch: Dict, batch_idx: int, dataloader_idx=0) -> Dict:
"""
Run a validation step and log to Tensorboard
Args:
batch (Dict): Dictionary containing batch data
batch_idx (int): Unused.
Returns:
Dict: Dictionary containing regression output.
"""
# batch_size = batch['img'].shape[0]
output = self.forward_step(batch, train=False)
pred_smpl_params = output['pred_smpl_params']
loss = self.compute_loss(batch, output, train=False)
output['loss'] = loss
self.tensorboard_logging(batch, output, self.global_step, train=False)
return output