import os import glob import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from body_model import OP_IGNORE_JOINTS from geometry.mesh import save_mesh_scenes, vertices_to_trimesh from util.logger import Logger, log_cur_stats from util.tensor import move_to, detach_all from vis.output import prep_result_vis, animate_scene from .losses import RootLoss, SMPLLoss #, MotionLoss from .output import save_camera_json import time LINE_SEARCH = "strong_wolfe" LIGHT_BLUE=(0.65098039, 0.74117647, 0.85882353) """ Optimization happens in multiple stages Stage 1: fit root orients and trans of each frame indendpently Stage 2: fit SMPL poses and betas of each frame independently Stage 3: fit poses and roots in same global coordinate frame """ class StageOptimizer(object): def __init__( self, name, model, param_names, lr=1.0, lbfgs_max_iter=20, save_every=10, vis_every=-1, save_meshes=True, max_chunk_steps=10, **kwargs, ): Logger.log(f"INITIALIZING OPTIMIZER {name} for {param_names}") self.name = name self.model = model self.set_opt_vars(param_names) self.optim = torch.optim.LBFGS( self.opt_params, max_iter=lbfgs_max_iter, lr=lr, line_search_fn=LINE_SEARCH ) # LBFGS computes losses multiple times per iteration, # save a dict mapping iteration to list of stats_dicts self.loss_dicts = {} self.save_every = save_every self.vis_every = vis_every self.save_meshes = save_meshes self.max_chunk_steps = max_chunk_steps self.cur_step = 0 self.add_chunk = 0 self.cur_loss = 0 self.prev_loss = np.inf self.last_updated = 0 self.reached_max = False self.reached_max_iter = -1 def set_opt_vars(self, param_names): Logger.log("Set param names:") Logger.log(param_names) self.param_names = param_names self.model.params.set_require_grads(self.param_names) self.opt_params = [ getattr(self.model.params, name) for name in self.param_names ] def forward_pass(self, obs_data): raise NotImplementedError def load_checkpoint(self, out_dir, device=None): if device is None: device = torch.device("cpu") param_path = os.path.join(out_dir, f"{self.name}_params.pth") if os.path.isfile(param_path): param_dict = torch.load(param_path, map_location=device) self.model.params.load_dict(param_dict) Logger.log(f"Params loaded from {param_path}") optim_path = os.path.join(out_dir, f"{self.name}_optim.pth") if os.path.isfile(optim_path): optim_dict = torch.load(optim_path) self.optim.load_state_dict(optim_dict["optim"]) self.cur_step = optim_dict["cur_step"] Logger.log(f"Optimizer loaded from {optim_path} at iter {self.cur_step}") def save_checkpoint(self, out_dir): param_path = os.path.join(out_dir, f"{self.name}_params.pth") param_dict = self.model.params.get_dict() if "world_scale" in param_dict: print("WORLD_SCALE", param_dict["world_scale"].detach().cpu()) if "floor_plane" in param_dict: print("FLOOR PLANE", param_dict["floor_plane"].detach().cpu()) if "cam_f" in param_dict: print("CAM_F", param_dict["cam_f"].detach().cpu()) torch.save(param_dict, param_path) Logger.log(f"Model saved at {param_path}") optim_path = os.path.join(out_dir, f"{self.name}_optim.pth") torch.save( {"optim": self.optim.state_dict(), "cur_step": self.cur_step}, optim_path, ) Logger.log(f"Optimizer saved at {optim_path}") def save_results(self, out_dir, seq_name): """ pred dict will be a dictionary of trajectories. each trajectory will have params and lists of trimesh sequences """ os.makedirs(out_dir, exist_ok=True) with torch.no_grad(): print('go into get_optim_result from optimizer.py save_results') pred_dict = self.model.get_optim_result() pred_dict = move_to(detach_all(pred_dict), "cpu") i = self.cur_step for name, results in pred_dict.items(): print('save_results, name: ', name) # save parameters of trajectory out_path = f"{out_dir}/{seq_name}_{i:06d}_{name}_results.npz" Logger.log(f"saving params to {out_path}") np.savez(out_path, **results) print('cam_R in save_results and its .npz', results['cam_R']) # also save the cameras # print('save the cameras in ptimizer.py save_results') # print(self.model.params) # with torch.no_grad(): # cam_R, cam_t = self.model.params.get_extrinsics() # intrins = self.model.params.get_intrinsics() # print('cam_R in 0000.json:', cam_R) # save_camera_json( # f"{out_dir}/{seq_name}_cameras_{self.cur_step:06d}.json", # cam_R.detach().cpu(), # cam_t.detach().cpu(), # intrins.detach().cpu(), # ) # plot losses self.plot_losses(out_dir) # def save_meshes_all(self, res_dir, obs_data, seq_name, num_steps=-1): # # check which results are saved # seq_name = obs_data["seq_name"][0] # res_pre = f"{res_dir}/{seq_name}_opt_{self.cur_step:06d}" # with torch.no_grad(): # print('go into get_optim_result from optimizer.py vis_result') # pred_dict = self.model.get_optim_result(num_steps=num_steps) # res_dict = detach_all(pred_dict["world"]) # scene_dict = move_to( # prep_result_vis( # res_dict, # obs_data["vis_mask"], # obs_data["track_id"], # self.model.body_model, # temporal_smooth=True # ), # "cpu", # ) # # save the meshes # os.makedirs(res_dir, exist_ok=True) # i = self.cur_step # scene_dir = f"{res_dir}/{seq_name}_{i:06d}_meshes" # os.makedirs(scene_dir, exist_ok=True) # verts, joints, colors, l_faces, r_faces, is_right, bounds = scene_dict["geometry"] # T = len(verts) # print(f"{T} mesh frames") # times = list(range(0, T, 1)) # for t in times: # if len(is_right[t]) > 1: # assert (is_right[t].cpu().numpy().tolist() == [0,1]) # # l_meshes = make_batch_mesh(verts[t][0][None], l_faces[t], colors[t][0][None]) # # r_meshes = make_batch_mesh(verts[t][1][None], r_faces[t], colors[t][1][None]) # # assert len(l_meshes) == 1 # # assert len(r_meshes) == 1 # tmesh = vertices_to_trimesh(verts[t][0].detach().cpu().numpy(), l_faces[t].detach().cpu().numpy(), LIGHT_BLUE, is_right=0) # tmesh.export(os.path.join(scene_dir, f'{str(t).zfill(6)}_0.obj')) # tmesh = vertices_to_trimesh(verts[t][1].detach().cpu().numpy(), r_faces[t].detach().cpu().numpy(), LIGHT_BLUE, is_right=1) # tmesh.export(os.path.join(scene_dir, f'{str(t).zfill(6)}_1.obj')) # else: # assert len(is_right[t]) == 1 # if is_right[t] == 0: # # meshes = make_batch_mesh(verts[t][0][None], l_faces[t], colors[t][0][None]) # tmesh = vertices_to_trimesh(verts[t][0].detach().cpu().numpy(), l_faces[t].detach().cpu().numpy(), LIGHT_BLUE, is_right=0) # tmesh.export(os.path.join(scene_dir, f'{str(t).zfill(6)}_0.obj')) # elif is_right[t] == 1: # # meshes = make_batch_mesh(verts[t][0][None], r_faces[t], colors[t][0][None]) # tmesh = vertices_to_trimesh(verts[t][0].detach().cpu().numpy(), r_faces[t].detach().cpu().numpy(), LIGHT_BLUE, is_right=1) # tmesh.export(os.path.join(scene_dir, f'{str(t).zfill(6)}_1.obj')) def vis_result(self, res_dir, obs_data, vis=None, num_steps=-1): if vis is None or self.vis_every < 0: return # check which results are saved seq_name = obs_data["seq_name"][0] res_pre = f"{res_dir}/{seq_name}_opt_{self.cur_step:06d}" with torch.no_grad(): print('go into get_optim_result from optimizer.py vis_result') pred_dict = self.model.get_optim_result(num_steps=num_steps) res_dict = detach_all(pred_dict["world"]) scene_dict = move_to( prep_result_vis( res_dict, obs_data["vis_mask"], obs_data["track_id"], self.model.body_model, ), "cpu", ) animate_scene(vis, scene_dict, res_pre, render_views=["src_cam", "above"]) def log_losses(self, stats_dict): stats_dict = move_to(detach_all(stats_dict), "cpu") log_cur_stats( stats_dict, iter=self.cur_step, to_stdout=(self.cur_step % self.save_every == 0), ) for loss_name, loss_val in stats_dict.items(): loss_dict = self.loss_dicts.get(loss_name, {}) loss_series = loss_dict.get(self.cur_step, []) loss_series.append(loss_val) loss_dict[self.cur_step] = loss_series self.loss_dicts[loss_name] = loss_dict def record_current_losses(self, writer): """ record the mean of current step's loss values in tensorboard """ if len(self.loss_dicts) < 1: return for loss_name, loss_dict in self.loss_dicts.items(): loss_mean = np.mean(loss_dict[self.cur_step]) writer.add_scalar(f"{self.name}/{loss_name}", loss_mean, self.cur_step) def plot_losses(self, res_dir): """ plot a box plot for each BFGS iteration """ if len(self.loss_dicts) < 1: return for loss_name, loss_dict in self.loss_dicts.items(): # times (list len T) # loss vals (list len T of loss value lists) times, loss_vals = zip(*loss_dict.items()) plt.figure() plt.boxplot(loss_vals, labels=times, showfliers=False) plt.savefig(f"{res_dir}/{loss_name}.png") def run(self, obs_data, num_iters, out_dir, vis=None, writer=None): self.cur_step = 0 self.loss.cur_step = 0 res_dir = os.path.join(out_dir, self.name) os.makedirs(res_dir, exist_ok=True) seq_name = obs_data["seq_name"][0] print("SEQ NAME", seq_name, "num_iters", num_iters) # try to load from checkpoint if exists device = obs_data["joints2d"].device self.load_checkpoint(out_dir, device=device) if self.cur_step >= num_iters: Logger.log(f"Current optimizer {self}") Logger.log(f"Checkpoint at {self.cur_step} >= {num_iters}, skipping") return Logger.log(f"OPTIMIZING {self.name} FOR {num_iters} ITERATIONS") # time.sleep(5) # save initial results and vis print('go into save_results in optimizers.py run()') self.save_results(res_dir, seq_name) for i in range(self.cur_step, num_iters): Logger.log("ITER: %d" % (i)) if (i + 1) % self.save_every == 0: # save before self.save_checkpoint(out_dir) self.save_results(res_dir, seq_name) else: self.save_checkpoint(out_dir) if (i + 1) % self.vis_every == 0: # render self.vis_result(res_dir, obs_data, vis) self.cur_step = i self.loss.cur_step = i #print("ITER: %d" % (i)) self.optim_step(obs_data, i, writer) # early termination in case of nans if np.isnan(self.cur_loss): # we need to backtrack self.load_checkpoint(out_dir, device=device) print(self.cur_loss, self.kkk, self.ppp) raise ValueError # termination for the last chunk if self.reached_max and self.reached_max_iter < 0: self.reached_max_iter = i - 1 if self.reached_max and i - self.reached_max_iter >= self.max_chunk_steps: break # termination for middle chunks loss_change = self.prev_loss - self.cur_loss if self.last_updated == i - 1 and loss_change == 0: break if ( (self.cur_loss < 0 and loss_change < 100) or (i - self.last_updated >= self.max_chunk_steps) or (loss_change < 20 and i - self.last_updated > 5) ): self.add_chunk = self.add_chunk + 1 self.last_updated = i self.prev_loss = self.cur_loss # final save and vis step self.cur_step = num_iters self.save_checkpoint(out_dir) self.save_results(res_dir, seq_name) self.vis_result(res_dir, obs_data, vis) # self.save_meshes_all(res_dir, obs_data, seq_name) def optim_step(self, obs_data, i, writer=None): def closure(): self.optim.zero_grad() #print('******************************', i) loss, stats_dict, preds = self.forward_pass(obs_data) #print('******************************', i) stats_dict["total"] = loss self.log_losses(move_to(detach_all(stats_dict), "cpu")) self.cur_loss = stats_dict["total"].detach().cpu().item() self.kkk = stats_dict self.ppp = preds loss_keys = stats_dict.keys() if "motion_prior" in loss_keys and "pose_prior" in loss_keys: self.cur_loss = ( self.cur_loss - 0.04 * stats_dict["pose_prior"].detach().cpu().item() ) print("optimizer print", loss, loss.shape, stats_dict, loss.requires_grad) loss.backward() # print('end of loss backward') return loss #print('!!!!!!!!!!!!!!!!!!!!!!!!!!', i) self.optim.step(closure) #print('!!!!!!!!!!!!!!!!!!!!!!!!!!', i) if writer is not None: self.record_current_losses(writer) class RootOptimizer(StageOptimizer): name = "root_fit" stage = 0 def __init__( self, model, all_loss_weights, use_chamfer=False, robust_loss_type="none", robust_tuning_const=4.6851, joints2d_sigma=100, **kwargs, ): param_names = ["trans", "root_orient"] super().__init__(self.name, model, param_names, **kwargs) self.loss = RootLoss( all_loss_weights[self.stage], ignore_op_joints=OP_IGNORE_JOINTS, joints2d_sigma=joints2d_sigma, use_chamfer=use_chamfer, robust_loss=robust_loss_type, robust_tuning_const=robust_tuning_const, faces=model.body_model.faces_tensor ) def forward_pass(self, obs_data): """ Takes in observed data, predicts the smpl parameters and returns loss """ # raise ValueError # Use current params to go through SMPL and get joints3d, verts3d, points3d pred_data = self.model.pred_params_mano(obs_data["is_right"]) pred_data["cameras"] = self.model.params.get_cameras() # compute data losses only vis_mask = obs_data["vis_mask"] >= 0 # print('entering loss from RootOptimizer') loss, stats_dict = self.loss(obs_data, pred_data, vis_mask) return loss, stats_dict, pred_data class SmoothOptimizer(StageOptimizer): name = "smooth_fit" stage = 1 def __init__( self, model, all_loss_weights, use_chamfer=False, robust_loss_type="none", robust_tuning_const=4.6851, joints2d_sigma=100, **kwargs, ): param_names = ["trans", "root_orient", "betas", "latent_pose"] if model.opt_scale: param_names += ["world_scale"] if model.opt_cams: param_names += ["cam_f", "delta_cam_R"] super().__init__(self.name, model, param_names, **kwargs) self.loss = SMPLLoss( all_loss_weights[self.stage], ignore_op_joints=OP_IGNORE_JOINTS, joints2d_sigma=joints2d_sigma, use_chamfer=use_chamfer, robust_loss=robust_loss_type, robust_tuning_const=robust_tuning_const, faces=model.body_model.faces_tensor ) def forward_pass(self, obs_data): """ Takes in observed data, predicts the smpl parameters and returns loss """ # raise ValueError # Use current params to go through SMPL and get joints3d, verts3d, points3d pred_data = self.model.pred_params_mano(obs_data["is_right"]) pred_data["cameras"] = self.model.params.get_cameras() pred_data.update(self.model.params.get_vars()) # camera predictions # pts_ij, target, weight = self.model.params.get_reprojected_points() # pred_data["bg2d_err"] = ( # (weight * (pts_ij - target) ** 2).sum(dim=(-1, -2)).mean() # ) pred_data["cam_R"], pred_data["cam_t"] = self.model.params.get_extrinsics() # compute data losses only vis_mask = obs_data["vis_mask"] >= 0 loss, stats_dict = self.loss(obs_data, pred_data, self.model.seq_len, vis_mask) return loss, stats_dict, pred_data # class MotionOptimizer(StageOptimizer): # name = "motion_fit" # stage = 2 # def __init__(self, model, all_loss_weights, opt_cams=False, **kwargs): # self.opt_cams = opt_cams and model.opt_cams # param_names = [ # "trans", # "root_orient", # "latent_pose", # "trans_vel", # "root_orient_vel", # "joints_vel", # "betas", # "latent_motion", # "floor_plane", # ] # if model.opt_scale: # param_names += ["world_scale"] # if self.opt_cams: # Logger.log(f"{self.name} OPTIMIZING CAMERAS") # param_names += ["delta_cam_R", "cam_f"] # super().__init__(self.name, model, param_names, **kwargs) # self.set_loss(model, all_loss_weights[self.stage], **kwargs) # def set_loss( # self, # model, # loss_weights, # use_chamfer=False, # robust_loss_type="none", # robust_tuning_const=4.6851, # joints2d_sigma=100, # **kwargs, # ): # self.loss = MotionLoss( # loss_weights, # init_motion_prior=model.init_motion_prior, # ignore_op_joints=OP_IGNORE_JOINTS, # joints2d_sigma=joints2d_sigma, # use_chamfer=use_chamfer, # robust_loss=robust_loss_type, # robust_tuning_const=robust_tuning_const, # ) # def get_motion_scale(self): # return 1.0 # def forward_pass(self, obs_data, num_steps=-1): # p = self.model.params # param_names = [ # "betas", # "joints_vel", # "trans_vel", # "root_orient_vel", # ] # param_dict = p.get_vars(param_names) # # param_dict["floor_plane"] = p.floor_plane[p.floor_idcs] # preds, world_preds = self.model.rollout_mano_steps(num_steps) # preds.update(param_dict) # world_preds.update(param_dict) # # track mask is the length of the sequence that contains num_steps of each track # track_mask = world_preds.get("track_mask", None) # T = track_mask.shape[1] if track_mask is not None else num_steps # world_preds["cameras"] = p.get_cameras(np.arange(T)) # if self.opt_cams: # cam_R, cam_t = p.get_extrinsics() # world_preds["cam_R"], world_preds["cam_t"] = cam_R[:T], cam_t[:T] # obs_data = slice_dict(obs_data, 0, T) # motion_scale = self.get_motion_scale() # loss, stats_dict = self.loss( # obs_data, # preds, # world_preds, # self.model.seq_len, # valid_mask=track_mask, # init_motion_scale=motion_scale, # ) # return loss, stats_dict, (preds, world_preds) # def slice_dict(d, start, end): # out = d.copy() # for k, v in d.items(): # if not isinstance(v, torch.Tensor) or v.ndim < 3 or v.shape[1] < end: # continue # out[k] = v[:, start:end] # return out # class MotionOptimizerChunks(MotionOptimizer): # """ # Incrementally optimize sequence in chunks (with all variables) # :param chunk_size (int) number of frames per chunk # :param init_steps (int) number of optimization steps to spend on first chunk # :param chunk_steps (int) number of opt steps to spend on subsequent chunks # """ # name = "motion_chunks" # stage = 2 # def __init__(self, *args, chunk_size=20, init_steps=20, chunk_steps=10, **kwargs): # self.chunk_size = chunk_size # self.init_steps = init_steps # self.chunk_steps = chunk_steps # super().__init__(*args, **kwargs) # @property # def num_iters(self): # num_chunks = int(np.ceil(self.model.seq_len / self.chunk_size)) + 1 # return self.init_steps + self.chunk_steps * num_chunks # @property # def end_idx(self): # if self.cur_step < self.init_steps and self.add_chunk == 0: # return self.chunk_size # chunk_idx = max( # (self.cur_step - self.init_steps) // self.chunk_steps + 1, self.add_chunk # ) # if int(np.ceil(self.model.seq_len / self.chunk_size)) == self.add_chunk: # self.reached_max = True # return min(self.chunk_size * (chunk_idx + 1), self.model.seq_len) # @property # def start_idx(self): # return 0 # def get_motion_scale(self): # num_frames = self.end_idx - self.start_idx # return max(1.0, float(self.model.seq_len) / num_frames) # def forward_pass(self, obs_data): # return super().forward_pass(obs_data, num_steps=self.end_idx) # def vis_result(self, res_dir, obs_data, vis=None, **kwargs): # print("start, end", self.start_idx, self.end_idx) # return super().vis_result(res_dir, obs_data, vis=vis, num_steps=self.end_idx) # class MotionOptimizerFreeze(MotionOptimizer): # """ # Only optimizing a subset of parameters # """ # name = "motion_freeze" # stage = 2 # def __init__(self, model, all_loss_weights, no_contacts=True, **kwargs): # loss_weights = all_loss_weights[self.stage].copy() # if no_contacts: # loss_weights["contact_height"] = 0.0 # loss_weights["contact_vel"] = 0.0 # param_names = ["latent_motion", "betas", "trans", "floor_plane"] # if model.optim_scale: # param_names += ["world_scale"] # StageOptimizer.__init__(self, self.name, model, param_names, **kwargs) # self.set_loss(model, loss_weights, **kwargs)