From cd865154b5d390187877e294d1f762857514da02 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Sat, 10 May 2025 08:36:51 +0000 Subject: [PATCH] Create Prometheus-generated README file --- .kno/chunk_review.txt | 17251 ++++++++++++++++ .../data_level0.bin | Bin 0 -> 3212000 bytes .../header.bin | Bin 0 -> 100 bytes .../length.bin | Bin 0 -> 4000 bytes .../link_lists.bin | 0 .../chroma.sqlite3 | Bin 0 -> 6443008 bytes SECURITY_AUDIT_Prometheus-beta.md | 112 + 7 files changed, 17363 insertions(+) create mode 100644 .kno/chunk_review.txt create mode 100644 .kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/data_level0.bin create mode 100644 .kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/header.bin create mode 100644 .kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/length.bin create mode 100644 .kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/link_lists.bin create mode 100644 .kno/embedding_SBERTEmbedding_1746865869449_9651d09/chroma.sqlite3 create mode 100644 SECURITY_AUDIT_Prometheus-beta.md diff --git a/.kno/chunk_review.txt b/.kno/chunk_review.txt new file mode 100644 index 0000000..2729941 --- /dev/null +++ b/.kno/chunk_review.txt @@ -0,0 +1,17251 @@ + +=== File: requirements.txt === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/requirements.txt:1-14 +fvcore>=0.1.1.post20200716 +loguru>=0.5.1 +matplotlib>=3.3.1 +numpy>=1.19.1 +open3d>=0.10.0.0 +opencv-python>=3.4.3 +Pillow>=7.2.0 +pyrender>=0.1.43 +smplx>=0.1.21 +threadpoolctl>=2.1.0 +torch>=1.6.0 +torchvision>=0.7.0+cu101 +tqdm>=4.48.2 +trimesh>=3.8.1 + +=== File: inference.py === + +-- Chunk 1 -- +// inference.py:57-94 + weak_persp_to_blender( + targets, + camera_scale, + camera_transl, + H, W, + sensor_width=36, + focal_length=5000): + ''' Converts weak-perspective camera to a perspective camera + ''' + if torch.is_tensor(camera_scale): + camera_scale = camera_scale.detach().cpu().numpy() + if torch.is_tensor(camera_transl): + camera_transl = camera_transl.detach().cpu().numpy() + + output = defaultdict(lambda: []) + for ii, target in enumerate(targets): + orig_bbox_size = target.get_field('orig_bbox_size') + bbox_center = target.get_field('orig_center') + z = 2 * focal_length / (camera_scale[ii] * orig_bbox_size) + + transl = [ + camera_transl[ii, 0].item(), camera_transl[ii, 1].item(), + z.item()] + shift_x = - (bbox_center[0] / W - 0.5) + shift_y = (bbox_center[1] - 0.5 * H) / W + focal_length_in_mm = focal_length / W * sensor_width + output['shift_x'].append(shift_x) + output['shift_y'].append(shift_y) + output['transl'].append(transl) + output['focal_length_in_mm'].append(focal_length_in_mm) + output['focal_length_in_px'].append(focal_length) + output['center'].append(bbox_center) + output['sensor_width'].append(sensor_width) + for key in output: + output[key] = np.stack(output[key], axis=0) + return output + + + +-- Chunk 2 -- +// inference.py:95-107 + undo_img_normalization(image, mean, std, add_alpha=True): + if torch.is_tensor(image): + image = image.detach().cpu().numpy().squeeze() + + out_img = (image * std[np.newaxis, :, np.newaxis, np.newaxis] + + mean[np.newaxis, :, np.newaxis, np.newaxis]) + if add_alpha: + out_img = np.pad( + out_img, [[0, 0], [0, 1], [0, 0], [0, 0]], + mode='constant', constant_values=1.0) + return out_img + + + +-- Chunk 3 -- +// inference.py:109-258 + main( + exp_cfg, + show=False, + demo_output_folder='demo_output', + pause=-1, + focal_length=5000, sensor_width=36, + save_vis=True, + save_params=False, + save_mesh=False, + degrees=[], +): + + device = torch.device('cuda') + if not torch.cuda.is_available(): + logger.error('CUDA is not available!') + sys.exit(3) + + logger.remove() + logger.add(lambda x: tqdm.write(x, end=''), + level=exp_cfg.logger_level.upper(), + colorize=True) + + demo_output_folder = osp.expanduser(osp.expandvars(demo_output_folder)) + logger.info(f'Saving results to: {demo_output_folder}') + os.makedirs(demo_output_folder, exist_ok=True) + + model = SMPLXNet(exp_cfg) + try: + model = model.to(device=device) + except RuntimeError: + # Re-submit in case of a device error + sys.exit(3) + + checkpoint_folder = osp.join( + exp_cfg.output_folder, exp_cfg.checkpoint_folder) + checkpointer = Checkpointer(model, save_dir=checkpoint_folder, + pretrained=exp_cfg.pretrained) + + arguments = {'iteration': 0, 'epoch_number': 0} + extra_checkpoint_data = checkpointer.load_checkpoint() + for key in arguments: + if key in extra_checkpoint_data: + arguments[key] = extra_checkpoint_data[key] + + model = model.eval() + + means = np.array(exp_cfg.datasets.body.transforms.mean) + std = np.array(exp_cfg.datasets.body.transforms.std) + + render = save_vis or show + body_crop_size = exp_cfg.get('datasets', {}).get('body', {}).get( + 'transforms').get('crop_size', 256) + if render: + hd_renderer = HDRenderer(img_size=body_crop_size) + + dataloaders = make_all_data_loaders(exp_cfg, split='test') + + body_dloader = dataloaders['body'][0] + + total_time = 0 + cnt = 0 + for bidx, batch in enumerate(tqdm(body_dloader, dynamic_ncols=True)): + + full_imgs_list, body_imgs, body_targets = batch + if full_imgs_list is None: + continue + + full_imgs = to_image_list(full_imgs_list) + body_imgs = body_imgs.to(device=device) + body_targets = [target.to(device) for target in body_targets] + full_imgs = full_imgs.to(device=device) + + torch.cuda.synchronize() + start = time.perf_counter() + model_output = model(body_imgs, body_targets, full_imgs=full_imgs, + device=device) + torch.cuda.synchronize() + elapsed = time.perf_counter() - start + cnt += 1 + total_time += elapsed + + hd_imgs = full_imgs.images.detach().cpu().numpy().squeeze() + body_imgs = body_imgs.detach().cpu().numpy() + body_output = model_output.get('body') + + _, _, H, W = full_imgs.shape + # logger.info(f'{H}, {W}') + # H, W, _ = hd_imgs.shape + if render: + hd_imgs = np.transpose(undo_img_normalization(hd_imgs, means, std), + [0, 2, 3, 1]) + hd_imgs = np.clip(hd_imgs, 0, 1.0) + right_hand_crops = body_output.get('right_hand_crops') + left_hand_crops = torch.flip( + body_output.get('left_hand_crops'), dims=[-1]) + head_crops = body_output.get('head_crops') + bg_imgs = undo_img_normalization(body_imgs, means, std) + + right_hand_crops = undo_img_normalization( + right_hand_crops, means, std) + left_hand_crops = undo_img_normalization( + left_hand_crops, means, std) + head_crops = undo_img_normalization(head_crops, means, std) + + body_output = model_output.get('body', {}) + num_stages = body_output.get('num_stages', 3) + stage_n_out = body_output.get(f'stage_{num_stages - 1:02d}', {}) + model_vertices = stage_n_out.get('vertices', None) + + if stage_n_out is not None: + model_vertices = stage_n_out.get('vertices', None) + + faces = stage_n_out['faces'] + if model_vertices is not None: + model_vertices = model_vertices.detach().cpu().numpy() + camera_parameters = body_output.get('camera_parameters', {}) + camera_scale = camera_parameters['scale'].detach() + camera_transl = camera_parameters['translation'].detach() + + out_img = OrderedDict() + + final_model_vertices = None + stage_n_out = model_output.get('body', {}).get('final', {}) + if stage_n_out is not None: + final_model_vertices = stage_n_out.get('vertices', None) + + if final_model_vertices is not None: + final_model_vertices = final_model_vertices.detach().cpu().numpy() + camera_parameters = model_output.get('body', {}).get( + 'camera_parameters', {}) + camera_scale = camera_parameters['scale'].detach() + camera_transl = camera_parameters['translation'].detach() + + hd_params = weak_persp_to_blender( + body_targets, + camera_scale=camera_scale, + camera_transl=camera_transl, + H=H, W=W, + sensor_width=sensor_width, + focal_length=focal_length, + ) + + if save_vis: + bg_hd_imgs = np.transpose(hd_imgs, [0, 3, 1, 2]) + out_img['hd_imgs'] = bg_hd_imgs + if render: + # Render the initial predictions on the original image resolution + hd_orig_overlays = hd_renderer( + model_vertices, faces, + focal_length=hd_params['focal_length_in_px'], + +-- Chunk 4 -- +// inference.py:259-373 + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + ) + out_img['hd_orig_overlay'] = hd_orig_overlays + + # Render the overlays of the final prediction + if render: + hd_overlays = hd_renderer( + final_model_vertices, + faces, + focal_length=hd_params['focal_length_in_px'], + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + body_color=[0.4, 0.4, 0.7] + ) + out_img['hd_overlay'] = hd_overlays + + for deg in degrees: + hd_overlays = hd_renderer( + final_model_vertices, faces, + focal_length=hd_params['focal_length_in_px'], + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + render_bg=False, + body_color=[0.4, 0.4, 0.7], + deg=deg, + ) + out_img[f'hd_rendering_{deg:03.0f}'] = hd_overlays + + if save_vis: + for key in out_img.keys(): + out_img[key] = np.clip( + np.transpose( + out_img[key], [0, 2, 3, 1]) * 255, 0, 255).astype( + np.uint8) + + for idx in tqdm(range(len(body_targets)), 'Saving ...'): + fname = body_targets[idx].get_field('fname') + curr_out_path = osp.join(demo_output_folder, fname) + os.makedirs(curr_out_path, exist_ok=True) + + if save_vis: + for name, curr_img in out_img.items(): + pil_img.fromarray(curr_img[idx]).save( + osp.join(curr_out_path, f'{name}.png')) + + if save_mesh: + # Store the mesh predicted by the body-crop network + naive_mesh = o3d.geometry.TriangleMesh() + naive_mesh.vertices = Vec3d( + model_vertices[idx] + hd_params['transl'][idx]) + naive_mesh.triangles = Vec3i(faces) + mesh_fname = osp.join(curr_out_path, f'body_{fname}.ply') + o3d.io.write_triangle_mesh(mesh_fname, naive_mesh) + + # Store the final mesh + expose_mesh = o3d.geometry.TriangleMesh() + expose_mesh.vertices = Vec3d( + final_model_vertices[idx] + hd_params['transl'][idx]) + expose_mesh.triangles = Vec3i(faces) + mesh_fname = osp.join(curr_out_path, f'{fname}.ply') + o3d.io.write_triangle_mesh(mesh_fname, expose_mesh) + + if save_params: + params_fname = osp.join(curr_out_path, f'{fname}_params.npz') + out_params = dict(fname=fname) + for key, val in stage_n_out.items(): + if torch.is_tensor(val): + val = val.detach().cpu().numpy()[idx] + out_params[key] = val + for key, val in hd_params.items(): + if torch.is_tensor(val): + val = val.detach().cpu().numpy() + if np.isscalar(val[idx]): + out_params[key] = val[idx].item() + else: + out_params[key] = val[idx] + np.savez_compressed(params_fname, **out_params) + + if show: + nrows = 1 + ncols = 4 + len(degrees) + fig, axes = plt.subplots( + ncols=ncols, nrows=nrows, num=0, + gridspec_kw={'wspace': 0, 'hspace': 0}) + axes = axes.reshape(nrows, ncols) + for ax in axes.flatten(): + ax.clear() + ax.set_axis_off() + + axes[0, 0].imshow(hd_imgs[idx]) + axes[0, 1].imshow(out_img['rgb'][idx]) + axes[0, 2].imshow(out_img['hd_orig_overlay'][idx]) + axes[0, 3].imshow(out_img['hd_overlay'][idx]) + start = 4 + for deg in degrees: + axes[0, start].imshow( + out_img[f'hd_rendering_{deg:03.0f}'][idx]) + start += 1 + + plt.draw() + if pause > 0: + plt.pause(pause) + else: + plt.show() + + logger.info(f'Average inference time: {total_time / cnt}') + + + +=== File: demo.py === + +-- Chunk 1 -- +// demo.py:64-74 + collate_fn(batch): + output_dict = dict() + + for d in batch: + for key, val in d.items(): + if key not in output_dict: + output_dict[key] = [] + output_dict[key].append(val) + return output_dict + + + +-- Chunk 2 -- +// demo.py:75-159 + preprocess_images( + image_folder: str, + exp_cfg, + num_workers: int = 8, batch_size: int = 1, + min_score: float = 0.5, + scale_factor: float = 1.2, + device: Optional[torch.device] = None +) -> dutils.DataLoader: + + if device is None: + device = torch.device('cuda') + if not torch.cuda.is_available(): + logger.error('CUDA is not available!') + sys.exit(3) + + rcnn_model = keypointrcnn_resnet50_fpn(pretrained=True) + rcnn_model.eval() + rcnn_model = rcnn_model.to(device=device) + + transform = Compose( + [ToTensor(), ] + ) + + # Load the images + dataset = ImageFolder(image_folder, transforms=transform) + rcnn_dloader = dutils.DataLoader( + dataset, batch_size=batch_size, num_workers=num_workers, + collate_fn=collate_fn + ) + + out_dir = osp.expandvars('$HOME/Dropbox/boxes') + os.makedirs(out_dir, exist_ok=True) + + img_paths = [] + bboxes = [] + for bidx, batch in enumerate( + tqdm(rcnn_dloader, desc='Processing with R-CNN')): + batch['images'] = [x.to(device=device) for x in batch['images']] + + output = rcnn_model(batch['images']) + for ii, x in enumerate(output): + img = np.transpose( + batch['images'][ii].detach().cpu().numpy(), [1, 2, 0]) + img = (img * 255).astype(np.uint8) + + img_path = batch['paths'][ii] + _, fname = osp.split(img_path) + fname, _ = osp.splitext(fname) + + # out_path = osp.join(out_dir, f'{fname}_{ii:03d}.jpg') + for n, bbox in enumerate(output[ii]['boxes']): + bbox = bbox.detach().cpu().numpy() + if output[ii]['scores'][n].item() < min_score: + continue + img_paths.append(img_path) + bboxes.append(bbox) + + # cv2.rectangle(img, tuple(bbox[:2]), tuple(bbox[2:]), + # (255, 0, 0)) + # cv2.imwrite(out_path, img[:, :, ::-1]) + + dataset_cfg = exp_cfg.get('datasets', {}) + body_dsets_cfg = dataset_cfg.get('body', {}) + + body_transfs_cfg = body_dsets_cfg.get('transforms', {}) + transforms = build_transforms(body_transfs_cfg, is_train=False) + batch_size = body_dsets_cfg.get('batch_size', 64) + + expose_dset = ImageFolderWithBoxes( + img_paths, bboxes, scale_factor=scale_factor, transforms=transforms) + + expose_collate = functools.partial( + collate_batch, use_shared_memory=num_workers > 0, + return_full_imgs=True) + expose_dloader = dutils.DataLoader( + expose_dset, + batch_size=batch_size, + num_workers=num_workers, + collate_fn=expose_collate, + drop_last=False, + pin_memory=True, + ) + return expose_dloader + + + +-- Chunk 3 -- +// demo.py:160-197 + weak_persp_to_blender( + targets, + camera_scale, + camera_transl, + H, W, + sensor_width=36, + focal_length=5000): + ''' Converts weak-perspective camera to a perspective camera + ''' + if torch.is_tensor(camera_scale): + camera_scale = camera_scale.detach().cpu().numpy() + if torch.is_tensor(camera_transl): + camera_transl = camera_transl.detach().cpu().numpy() + + output = defaultdict(lambda: []) + for ii, target in enumerate(targets): + orig_bbox_size = target.get_field('orig_bbox_size') + bbox_center = target.get_field('orig_center') + z = 2 * focal_length / (camera_scale[ii] * orig_bbox_size) + + transl = [ + camera_transl[ii, 0].item(), camera_transl[ii, 1].item(), + z.item()] + shift_x = - (bbox_center[0] / W - 0.5) + shift_y = (bbox_center[1] - 0.5 * H) / W + focal_length_in_mm = focal_length / W * sensor_width + output['shift_x'].append(shift_x) + output['shift_y'].append(shift_y) + output['transl'].append(transl) + output['focal_length_in_mm'].append(focal_length_in_mm) + output['focal_length_in_px'].append(focal_length) + output['center'].append(bbox_center) + output['sensor_width'].append(sensor_width) + for key in output: + output[key] = np.stack(output[key], axis=0) + return output + + + +-- Chunk 4 -- +// demo.py:198-210 + undo_img_normalization(image, mean, std, add_alpha=True): + if torch.is_tensor(image): + image = image.detach().cpu().numpy().squeeze() + + out_img = (image * std[np.newaxis, :, np.newaxis, np.newaxis] + + mean[np.newaxis, :, np.newaxis, np.newaxis]) + if add_alpha: + out_img = np.pad( + out_img, [[0, 0], [0, 1], [0, 0], [0, 0]], + mode='constant', constant_values=1.0) + return out_img + + + +-- Chunk 5 -- +// demo.py:212-361 + main( + image_folder: str, + exp_cfg, + show: bool = False, + demo_output_folder: str = 'demo_output', + pause: float = -1, + focal_length: float = 5000, + rcnn_batch: int = 1, + sensor_width: float = 36, + save_vis: bool = True, + save_params: bool = False, + save_mesh: bool = False, + degrees: Optional[List[float]] = [], +) -> None: + + device = torch.device('cuda') + if not torch.cuda.is_available(): + logger.error('CUDA is not available!') + sys.exit(3) + + logger.remove() + logger.add(lambda x: tqdm.write(x, end=''), + level=exp_cfg.logger_level.upper(), + colorize=True) + + expose_dloader = preprocess_images( + image_folder, exp_cfg, batch_size=rcnn_batch, device=device) + + demo_output_folder = osp.expanduser(osp.expandvars(demo_output_folder)) + logger.info(f'Saving results to: {demo_output_folder}') + os.makedirs(demo_output_folder, exist_ok=True) + + model = SMPLXNet(exp_cfg) + try: + model = model.to(device=device) + except RuntimeError: + # Re-submit in case of a device error + sys.exit(3) + + output_folder = exp_cfg.output_folder + checkpoint_folder = osp.join(output_folder, exp_cfg.checkpoint_folder) + checkpointer = Checkpointer( + model, save_dir=checkpoint_folder, pretrained=exp_cfg.pretrained) + + arguments = {'iteration': 0, 'epoch_number': 0} + extra_checkpoint_data = checkpointer.load_checkpoint() + for key in arguments: + if key in extra_checkpoint_data: + arguments[key] = extra_checkpoint_data[key] + + model = model.eval() + + means = np.array(exp_cfg.datasets.body.transforms.mean) + std = np.array(exp_cfg.datasets.body.transforms.std) + + render = save_vis or show + body_crop_size = exp_cfg.get('datasets', {}).get('body', {}).get( + 'transforms').get('crop_size', 256) + if render: + hd_renderer = HDRenderer(img_size=body_crop_size) + + total_time = 0 + cnt = 0 + for bidx, batch in enumerate(tqdm(expose_dloader, dynamic_ncols=True)): + + full_imgs_list, body_imgs, body_targets = batch + if full_imgs_list is None: + continue + + full_imgs = to_image_list(full_imgs_list) + body_imgs = body_imgs.to(device=device) + body_targets = [target.to(device) for target in body_targets] + full_imgs = full_imgs.to(device=device) + + torch.cuda.synchronize() + start = time.perf_counter() + model_output = model(body_imgs, body_targets, full_imgs=full_imgs, + device=device) + torch.cuda.synchronize() + elapsed = time.perf_counter() - start + cnt += 1 + total_time += elapsed + + hd_imgs = full_imgs.images.detach().cpu().numpy().squeeze() + body_imgs = body_imgs.detach().cpu().numpy() + body_output = model_output.get('body') + + _, _, H, W = full_imgs.shape + # logger.info(f'{H}, {W}') + # H, W, _ = hd_imgs.shape + if render: + hd_imgs = np.transpose(undo_img_normalization(hd_imgs, means, std), + [0, 2, 3, 1]) + hd_imgs = np.clip(hd_imgs, 0, 1.0) + right_hand_crops = body_output.get('right_hand_crops') + left_hand_crops = torch.flip( + body_output.get('left_hand_crops'), dims=[-1]) + head_crops = body_output.get('head_crops') + bg_imgs = undo_img_normalization(body_imgs, means, std) + + right_hand_crops = undo_img_normalization( + right_hand_crops, means, std) + left_hand_crops = undo_img_normalization( + left_hand_crops, means, std) + head_crops = undo_img_normalization(head_crops, means, std) + + body_output = model_output.get('body', {}) + num_stages = body_output.get('num_stages', 3) + stage_n_out = body_output.get(f'stage_{num_stages - 1:02d}', {}) + model_vertices = stage_n_out.get('vertices', None) + + if stage_n_out is not None: + model_vertices = stage_n_out.get('vertices', None) + + faces = stage_n_out['faces'] + if model_vertices is not None: + model_vertices = model_vertices.detach().cpu().numpy() + camera_parameters = body_output.get('camera_parameters', {}) + camera_scale = camera_parameters['scale'].detach() + camera_transl = camera_parameters['translation'].detach() + + out_img = OrderedDict() + + final_model_vertices = None + stage_n_out = model_output.get('body', {}).get('final', {}) + if stage_n_out is not None: + final_model_vertices = stage_n_out.get('vertices', None) + + if final_model_vertices is not None: + final_model_vertices = final_model_vertices.detach().cpu().numpy() + camera_parameters = model_output.get('body', {}).get( + 'camera_parameters', {}) + camera_scale = camera_parameters['scale'].detach() + camera_transl = camera_parameters['translation'].detach() + + hd_params = weak_persp_to_blender( + body_targets, + camera_scale=camera_scale, + camera_transl=camera_transl, + H=H, W=W, + sensor_width=sensor_width, + focal_length=focal_length, + ) + + if save_vis: + bg_hd_imgs = np.transpose(hd_imgs, [0, 3, 1, 2]) + out_img['hd_imgs'] = bg_hd_imgs + if render: + # Render the initial predictions on the original image resolution + hd_orig_overlays = hd_renderer( + +-- Chunk 6 -- +// demo.py:362-478 + model_vertices, faces, + focal_length=hd_params['focal_length_in_px'], + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + ) + out_img['hd_orig_overlay'] = hd_orig_overlays + + # Render the overlays of the final prediction + if render: + hd_overlays = hd_renderer( + final_model_vertices, + faces, + focal_length=hd_params['focal_length_in_px'], + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + body_color=[0.4, 0.4, 0.7] + ) + out_img['hd_overlay'] = hd_overlays + + for deg in degrees: + hd_overlays = hd_renderer( + final_model_vertices, faces, + focal_length=hd_params['focal_length_in_px'], + camera_translation=hd_params['transl'], + camera_center=hd_params['center'], + bg_imgs=bg_hd_imgs, + return_with_alpha=True, + render_bg=False, + body_color=[0.4, 0.4, 0.7], + deg=deg, + ) + out_img[f'hd_rendering_{deg:03.0f}'] = hd_overlays + + if save_vis: + for key in out_img.keys(): + out_img[key] = np.clip( + np.transpose( + out_img[key], [0, 2, 3, 1]) * 255, 0, 255).astype( + np.uint8) + + for idx in tqdm(range(len(body_targets)), 'Saving ...'): + fname = body_targets[idx].get_field('fname') + curr_out_path = osp.join(demo_output_folder, fname) + os.makedirs(curr_out_path, exist_ok=True) + + if save_vis: + for name, curr_img in out_img.items(): + pil_img.fromarray(curr_img[idx]).save( + osp.join(curr_out_path, f'{name}.png')) + + if save_mesh: + # Store the mesh predicted by the body-crop network + naive_mesh = o3d.geometry.TriangleMesh() + naive_mesh.vertices = Vec3d( + model_vertices[idx] + hd_params['transl'][idx]) + naive_mesh.triangles = Vec3i(faces) + mesh_fname = osp.join(curr_out_path, f'body_{fname}.ply') + o3d.io.write_triangle_mesh(mesh_fname, naive_mesh) + + # Store the final mesh + expose_mesh = o3d.geometry.TriangleMesh() + expose_mesh.vertices = Vec3d( + final_model_vertices[idx] + hd_params['transl'][idx]) + expose_mesh.triangles = Vec3i(faces) + mesh_fname = osp.join(curr_out_path, f'{fname}.ply') + o3d.io.write_triangle_mesh(mesh_fname, expose_mesh) + + if save_params: + params_fname = osp.join(curr_out_path, f'{fname}_params.npz') + out_params = dict(fname=fname) + for key, val in stage_n_out.items(): + if torch.is_tensor(val): + val = val.detach().cpu().numpy()[idx] + out_params[key] = val + for key, val in hd_params.items(): + if torch.is_tensor(val): + val = val.detach().cpu().numpy() + if np.isscalar(val[idx]): + out_params[key] = val[idx].item() + else: + out_params[key] = val[idx] + np.savez_compressed(params_fname, **out_params) + + if show: + nrows = 1 + ncols = 4 + len(degrees) + fig, axes = plt.subplots( + ncols=ncols, nrows=nrows, num=0, + gridspec_kw={'wspace': 0, 'hspace': 0}) + axes = axes.reshape(nrows, ncols) + for ax in axes.flatten(): + ax.clear() + ax.set_axis_off() + + axes[0, 0].imshow(hd_imgs[idx]) + axes[0, 1].imshow(out_img['rgb'][idx]) + axes[0, 2].imshow(out_img['hd_orig_overlay'][idx]) + axes[0, 3].imshow(out_img['hd_overlay'][idx]) + start = 4 + for deg in degrees: + axes[0, start].imshow( + out_img[f'hd_rendering_{deg:03.0f}'][idx]) + start += 1 + + plt.draw() + if pause > 0: + plt.pause(pause) + else: + plt.show() + + logger.info(f'Average inference time: {total_time / cnt}') + + + +=== File: README.md === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/README.md:1-150 +## ExPose: Monocular Expressive Body Regression through Body-Driven Attention + +[![report](https://img.shields.io/badge/arxiv-report-red)](https://arxiv.org/abs/2008.09062) + +[[Project Page](https://expose.is.tue.mpg.de/)] +[[Paper](https://ps.is.tuebingen.mpg.de/uploads_file/attachment/attachment/620/0983.pdf)] +[[Supp. Mat.](https://ps.is.tuebingen.mpg.de/uploads_file/attachment/attachment/621/0983-supp_no_header_compressed.pdf)] + +![SMPL-X Examples](./images/expose.png) + +| Short Video | Long Video | +| --- | --- | +| [![ShortVideo](https://img.youtube.com/vi/a-sVItuoPek/0.jpg)](https://www.youtube.com/watch?v=a-sVItuoPek) | [![LongVideo](https://img.youtube.com/vi/lNTmHLYTiB8/0.jpg)](https://www.youtube.com/watch?v=lNTmHLYTiB8) | + +## Table of Contents + * [License](#license) + * [Description](#description) + * [Dependencies](#dependencies) + * [Preparing the data](#preparing-the-data) + * [Demo](#demo) + * [Inference](#inference) + * [Citation](#citation) + * [Acknowledgments](#acknowledgments) + * [Contact](#contact) + + +## License + +Software Copyright License for non-commercial scientific research purposes. +Please read carefully the following [terms and conditions](LICENSE) and any accompanying +documentation before you download and/or use the ExPose data, model and +software, (the "Data & Software"), including 3D meshes, images, videos, +textures, software, scripts, and animations. By downloading and/or using the +Data & Software (including downloading, cloning, installing, and any other use +of the corresponding github repository), you acknowledge that you have read +these [terms and conditions](LICENSE), understand them, and agree to be bound by them. If +you do not agree with these [terms and conditions](LICENSE), you must not download and/or +use the Data & Software. Any infringement of the terms of this agreement will +automatically terminate your rights under this [License](LICENSE). + +## Description + +**EX**pressive **PO**se and **S**hape r**E**gression (ExPose) is a method +that estimates 3D body pose and shape, hand articulation and facial expression +of a person from a single RGB image. For more details, please see our ECCV paper +[Monocular Expressive Body Regression through Body-Driven Attention](https://expose.is.tue.mpg.de/). +This repository contains: +* A PyTorch demo to run ExPose on images. +* An inference script for the supported datasets. + +## Installation + +To install the necessary dependencies run the following command: +```shell + pip install -r requirements.txt +``` +The code has been tested with two configurations: a) with Python 3.7, CUDA 10.1, CuDNN 7.5 and PyTorch 1.5 on Ubuntu 18.04, and b) with Python 3.6, CUDA 10.2 and PyTorch 1.6 on Ubuntu 18.04. + + +### Preparing the data + +First, you should head to the [project website](https://expose.is.tue.mpg.de/) and create an account. +If you want to stay informed, please opt-in for email communication +and we will reach out with any updates on the project. +Once you have your account, login and head to the download section +to get the pre-trained **ExPose** model. +Create a folder named *data* and extract the downloaded zip there. +You should now have a folder with the following structure: +```bash +data +├── checkpoints +├── all_means.pkl +├── conf.yaml +├── shape_mean.npy +├── SMPLX_to_J14.pkl +``` +For more information on the data, please read the [data documentation](doc/data.md). +If you don't already have an account on the [SMPL-X website](https://smpl-x.is.tue.mpg.de/), +please register to be able to download the model. Afterward, extract the SMPL-X model +zip inside the data folder you created above. +```bash +data +├── models +│   ├── smplx +``` +You are now ready to run the demo and inference scripts. + +### Demo + +We provide a script to run **ExPose** directly on images. +To get you started, we provide a sample folder, taken from [pexels](https://pexels.com), +which can be processed with the the following command: +```shell + python demo.py --image-folder samples \ + --exp-cfg data/conf.yaml \ + --show=False \ + --output-folder OUTPUT_FOLDER \ + --save-params [True/False] \ + --save-vis [True/False] \ + --save-mesh [True/False] +``` +The script will use a *Keypoint R-CNN* from *torchvision* to detect people in +the images and then produce a SMPL-X prediction for each using **ExPose**. +You should see the following output for the sample image: + +| ![Sample](samples/man-in-red-crew-neck-sweatshirt-photography-941693.png) | ![HD Overlay](images/hd_overlay.png) | +| --- | --- | + +### Inference + +The [inference](inference.py) script can be used to run inference on one of the supported +datasets. For example, if you have a folder with images and OpenPose keypoints +with the following structure: +```bash +folder +├── images +│   ├── img0001.jpg +│   └── img0002.jpg +│   └── img0002.jpg +├── keypoints +│   ├── img0001_keypoints.json +│   └── img0002_keypoints.json +│   └── img0002_keypoints.json +``` +Then you can use the following command to run ExPose for each person: +```shell +python inference.py --exp-cfg data/conf.yaml \ + --datasets openpose \ + --exp-opts datasets.body.batch_size B datasets.body.openpose.data_folder folder \ + --show=[True/False] \ + --output-folder OUTPUT_FOLDER \ + --save-params [True/False] \ + --save-vis [True/False] \ + --save-mesh [True/False] +``` +You can select if you want to save the estimated parameters, meshes, and renderings by +setting the corresponding flags. + +## Citation + +If you find this Model & Software useful in your research we would kindly ask you to cite: + +```bibtex +@inproceedings{ExPose:2020, + title= {Monocular Expressive Body Regression through Body-Driven Attention}, + author= {Choutas, Vasileios and Pavlakos, Georgios and Bolkart, Timo and Tzionas, Dimitrios and Black, Michael J.}, + booktitle = {European Conference on Computer Vision (ECCV)}, + year = {2020}, + url = {https://expose.is.tue.mpg.de} +} + +-- Chunk 2 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/README.md:151-173 +``` +```bibtex +@inproceedings{SMPL-X:2019, + title = {Expressive Body Capture: 3D Hands, Face, and Body from a Single Image}, + author = {Pavlakos, Georgios and Choutas, Vasileios and Ghorbani, Nima and Bolkart, Timo and Osman, Ahmed A. A. and Tzionas, Dimitrios and Black, Michael J.}, + booktitle = {Proceedings IEEE Conf. on Computer Vision and Pattern Recognition (CVPR)}, + year = {2019} +} +``` + +## Acknowledgments + +We thank Haiwen Feng for the FLAME fits, +Nikos Kolotouros, Muhammed Kocabas and Nikos Athanasiou for helpful discussions, +Sai Kumar Dwivedi and Lea Muller for proofreading, +Mason Landry and Valerie Callaghan for video voiceovers. + +## Contact +The code of this repository was implemented by [Vassilis Choutas](mailto:vassilis.choutas@tuebingen.mpg.de). + +For questions, please contact [expose@tue.mpg.de](mailto:expose@tue.mpg.de). + +For commercial licensing (and all related questions for business applications), please contact [ps-licensing@tue.mpg.de](mailto:ps-licensing@tue.mpg.de). + +=== File: doc/data.md === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/doc/data.md:1-32 +## ExPose Model - Documentation +For suggestions on improving documentation, please contact [expose@tue.mpg.de](mailto:expose@tue.mpg.de). + +Once you download and extract the zip with the pre-trained model you should have the following files: +* all_means.pkl : The mean pose parameters, which are used as the initial point for the iterative regression, in different pose representations ( axis-angle, PCA for the hands only, etc). +* shape_mean.npy: The mean shape parameters used to initialize the iterative regressor. +* SMPLX_to_J14.pkl: A linear regressor that computes the 14 LSP-like joints used to compute the mean per-joint point error (MPJPE). +* conf.yaml: Contains all the arguments needed to run ExPose. +* checkpoints: The pre-trained checkpoint. +* ExPose Dataset - Documentation + +### Curated fits +Downloading and extracting the curated fits zip should give you the following +two files: +* train.npz + * img_fns: The name of the image to read. + * betas: A Nx10 numpy array with the shape coefficients of each instance. + * expression: A Nx10 numpy array with the expression coefficients of each instance. + * keypoints2D: The OpenPose keypoints used to generate the fits. + * pose: A numpy array that contains the estimated SMPL-X pose vector in axis-angle format. +* val.npz + * img_fns: The name of the image to read. + * betas: A Nx10 numpy array with the shape coefficients of each instance. + * expression: A Nx10 numpy array with the expression coefficients of each instance. + * keypoints2D: The OpenPose keypoints used to generate the fits. + * pose: A numpy array that contains the estimated SMPL-X pose vector in axis-angle format. + * vertices: A numpy array that contains the estimated SMPL-X vertices. + * joints: The 14 LSP-like joints used to compute the mean per-joint point error metric. + +### SPIN in SMPL-X + +The data format is exactly the same as the one in SPIN, see the [original page](https://github.com/nkolot/SPIN#final-fits) for more details. + +=== File: expose/evaluation.py === + +-- Chunk 1 -- +// evaluation.py:52-57 + make_filter(name): + def filter(record): + return record['extra'].get('key_name') == name + return filter + + + +-- Chunk 2 -- +// evaluation.py:58-207 +ss Evaluator(object): + def __init__(self, exp_cfg, rank=0, distributed=False): + super(Evaluator, self).__init__() + self.rank = rank + self.distributed = distributed + + self.alpha_blend = exp_cfg.get('alpha_blend', 0.7) + j14_regressor_path = exp_cfg.j14_regressor_path + with open(j14_regressor_path, 'rb') as f: + self.J14_regressor = pickle.load(f, encoding='latin1') + part_map_path = osp.expandvars(exp_cfg.part_map) + with open(part_map_path, 'rb') as f: + data = pickle.load(f) + self.num2part = data['num2part'] + self.segm = data['segm'] + + smplx_valid_verts_fn = osp.expandvars( + exp_cfg.get('smplx_valid_verts_fn', '')) + self.use_body_verts = osp.exists(smplx_valid_verts_fn) + if self.use_body_verts: + self.use_hands_for_shape = exp_cfg.get( + 'use_hands_for_shape', False) + verts_data = np.load(smplx_valid_verts_fn) + if self.use_hands_for_shape: + # First column should be SMPL vertices + self.smplx_valid_verts = verts_data['mapping'][:, 1] + else: + self.smplx_valid_verts = verts_data['no_hands_mapping'][:, 1] + self.smplx_valid_verts = np.asarray( + self.smplx_valid_verts, dtype=np.int64) + + body_vertex_ids_path = osp.expandvars( + exp_cfg.get('body_vertex_ids_path', '')) + body_vertex_ids = None + if osp.exists(body_vertex_ids_path): + body_vertex_ids = np.load(body_vertex_ids_path).astype(np.int32) + self.body_vertex_ids = body_vertex_ids + + face_vertex_ids_path = osp.expandvars( + exp_cfg.get('face_vertex_ids_path', '')) + face_vertex_ids = None + if osp.exists(face_vertex_ids_path): + face_vertex_ids = np.load(face_vertex_ids_path).astype(np.int32) + self.face_vertex_ids = face_vertex_ids + + hand_vertex_ids_path = osp.expandvars( + exp_cfg.get('hand_vertex_ids_path', '')) + left_hand_vertex_ids, right_hand_vertex_ids = None, None + if osp.exists(hand_vertex_ids_path): + with open(hand_vertex_ids_path, 'rb') as f: + vertex_idxs_data = pickle.load(f, encoding='latin1') + left_hand_vertex_ids = vertex_idxs_data['left_hand'] + right_hand_vertex_ids = vertex_idxs_data['right_hand'] + + self.left_hand_vertex_ids = left_hand_vertex_ids + self.right_hand_vertex_ids = right_hand_vertex_ids + + self.imgs_per_row = exp_cfg.get('imgs_per_row', 2) + + self.save_part_v2v = exp_cfg.save_part_v2v + + self.exp_cfg = exp_cfg.clone() + self.output_folder = osp.expandvars(exp_cfg.output_folder) + + self.summary_folder = osp.join(self.output_folder, + exp_cfg.summary_folder) + os.makedirs(self.summary_folder, exist_ok=True) + self.summary_steps = exp_cfg.summary_steps + + self.results_folder = osp.join(self.output_folder, + exp_cfg.results_folder) + os.makedirs(self.results_folder, exist_ok=True) + self.loggers = defaultdict(lambda: None) + + self.body_degrees = exp_cfg.get('degrees', {}).get( + 'body', [90, 180, 270]) + self.hand_degrees = exp_cfg.get('degrees', {}).get( + 'hand', [90, 180, 270]) + self.head_degrees = exp_cfg.get('degrees', {}).get( + 'head', [90, 180, 270]) + + self.body_alignments = {'procrustes': ProcrustesAlignmentMPJPE(), + 'pelvis': PelvisAlignmentMPJPE() + } + hand_fscores_thresh = exp_cfg.get('fscores_thresh', {}).get( + 'hand', [5.0 / 1000, 15.0 / 1000]) + self.hand_fscores_thresh = hand_fscores_thresh + + self.hand_alignments = { + 'procrustes': ProcrustesAlignmentMPJPE( + fscore_thresholds=hand_fscores_thresh), + } + head_fscores_thresh = exp_cfg.get('fscores_thresh', {}).get( + 'head', [5.0 / 1000, 15.0 / 1000]) + self.head_fscores_thresh = head_fscores_thresh + self.head_alignments = { + 'procrustes': ProcrustesAlignmentMPJPE( + fscore_thresholds=head_fscores_thresh)} + + self.plot_conf_thresh = exp_cfg.plot_conf_thresh + + idxs_dict = get_part_idxs() + self.body_idxs = idxs_dict['body'] + self.hand_idxs = idxs_dict['hand'] + self.left_hand_idxs = idxs_dict['left_hand'] + self.right_hand_idxs = idxs_dict['right_hand'] + self.flame_idxs = idxs_dict['flame'] + + self.means = np.array(self.exp_cfg.datasets.body.transforms.mean) + self.std = np.array(self.exp_cfg.datasets.body.transforms.std) + + body_crop_size = exp_cfg.get('datasets', {}).get('body', {}).get( + 'crop_size', 256) + self.body_renderer = OverlayRenderer(img_size=body_crop_size) + + hand_crop_size = exp_cfg.get('datasets', {}).get('hand', {}).get( + 'crop_size', 256) + self.hand_renderer = OverlayRenderer(img_size=hand_crop_size) + + head_crop_size = exp_cfg.get('datasets', {}).get('head', {}).get( + 'crop_size', 256) + self.head_renderer = OverlayRenderer(img_size=head_crop_size) + + self.render_gt_meshes = exp_cfg.get('render_gt_meshes', True) + if self.render_gt_meshes: + self.gt_body_renderer = GTRenderer(img_size=body_crop_size) + self.gt_hand_renderer = GTRenderer(img_size=hand_crop_size) + self.gt_head_renderer = GTRenderer(img_size=head_crop_size) + + @torch.no_grad() + def __enter__(self): + self.filewriter = SummaryWriter(self.summary_folder, max_queue=1) + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.filewriter.close() + + def create_summaries(self, step, dset_name, images, targets, + model_output, camera_parameters, + renderer=None, gt_renderer=None, + degrees=None, prefix=''): + if not hasattr(self, 'filewriter'): + return + if degrees is None: + degrees = [] + + crop_size = images.shape[-1] + + imgs = (images * self.std[np.newaxis, :, np.newaxis, np.newaxis] + + self.means[np.newaxis, :, np.newaxis, np.newaxis]) + +-- Chunk 3 -- +// evaluation.py:208-357 + summary_imgs = OrderedDict() + summary_imgs['rgb'] = imgs + + gt_keyp_imgs = [] + for img_idx in range(imgs.shape[0]): + input_img = np.ascontiguousarray( + np.transpose(imgs[img_idx], [1, 2, 0])) + gt_keyp2d = targets[img_idx].smplx_keypoints.detach( + ).cpu().numpy() + gt_conf = targets[img_idx].conf.detach().cpu().numpy() + + gt_keyp2d[:, 0] = ( + gt_keyp2d[:, 0] * 0.5 + 0.5) * crop_size + gt_keyp2d[:, 1] = ( + gt_keyp2d[:, 1] * 0.5 + 0.5) * crop_size + + gt_keyp_img = create_skel_img( + input_img, gt_keyp2d, + targets[img_idx].CONNECTIONS, + gt_conf > 0, + names=KEYPOINT_NAMES) + + gt_keyp_img = np.transpose(gt_keyp_img, [2, 0, 1]) + gt_keyp_imgs.append(gt_keyp_img) + gt_keyp_imgs = np.stack(gt_keyp_imgs) + + # Add the ground-truth keypoints + summary_imgs['gt_keypoints'] = gt_keyp_imgs + + proj_joints = model_output.get('proj_joints', None) + if proj_joints is not None: + proj_points = model_output[ + 'proj_joints'].detach().cpu().numpy() + proj_points = (proj_points * 0.5 + 0.5) * crop_size + + reproj_joints_imgs = [] + for img_idx in range(imgs.shape[0]): + gt_conf = targets[img_idx].conf.detach().cpu().numpy() + + input_img = np.ascontiguousarray( + np.transpose(imgs[img_idx], [1, 2, 0])) + + reproj_joints_img = create_skel_img( + input_img, + proj_points[img_idx], + targets[img_idx].CONNECTIONS, + valid=gt_conf > 0, names=KEYPOINT_NAMES) + + reproj_joints_img = np.transpose( + reproj_joints_img, [2, 0, 1]) + reproj_joints_imgs.append(reproj_joints_img) + + # Add the the projected keypoints + reproj_joints_imgs = np.stack(reproj_joints_imgs) + summary_imgs['proj_joints'] = reproj_joints_imgs + + render_gt_meshes = (self.render_gt_meshes and + any([t.has_field('vertices') for t in targets])) + if render_gt_meshes: + gt_mesh_imgs = [] + faces = model_output['faces'] + for bidx, t in enumerate(targets): + if not (t.has_field('vertices') and t.has_field('intrinsics')): + gt_mesh_imgs.append(np.zeros_like(imgs[bidx])) + continue + + curr_gt_vertices = t.get_field( + 'vertices').vertices.detach().cpu().numpy().squeeze() + intrinsics = t.get_field('intrinsics') + + mesh_img = gt_renderer( + curr_gt_vertices[np.newaxis], faces=faces, + intrinsics=intrinsics[np.newaxis], + bg_imgs=imgs[[bidx]]) + gt_mesh_imgs.append(mesh_img.squeeze()) + + gt_mesh_imgs = np.stack(gt_mesh_imgs) + B, C, H, W = gt_mesh_imgs.shape + row_pad = (crop_size - H) // 2 + gt_mesh_imgs = np.pad( + gt_mesh_imgs, + [[0, 0], [0, 0], [row_pad, row_pad], [row_pad, row_pad]]) + summary_imgs['gt_meshes'] = gt_mesh_imgs + + vertices = model_output.get('vertices', None) + if vertices is not None: + body_imgs = [] + + camera_scale = camera_parameters.scale.detach() + camera_transl = camera_parameters.translation.detach() + + vertices = vertices.detach().cpu().numpy() + faces = model_output['faces'] + body_imgs = renderer( + vertices, faces, + camera_scale, camera_transl, + bg_imgs=imgs, + return_with_alpha=False, + ) + # Add the rendered meshes + summary_imgs['overlay'] = body_imgs.copy() + + for deg in degrees: + body_imgs = renderer( + vertices, faces, + camera_scale, camera_transl, + deg=deg, + return_with_alpha=False, + ) + summary_imgs[f'{deg:03d}'] = body_imgs.copy() + + summary_imgs = np.concatenate( + list(summary_imgs.values()), axis=3) + img_grid = make_grid( + torch.from_numpy(summary_imgs), nrow=self.imgs_per_row) + img_tab_name = (f'{dset_name}/{prefix}/Images' if len(prefix) > 0 else + f'{dset_name}/Images') + self.filewriter.add_image(img_tab_name, img_grid, step) + return + + def build_metric_logger(self, name): + output_fn = osp.join( + self.results_folder, name + '.log') + if self.loggers[name] is None: + logger.add(output_fn, filter=make_filter(name)) + self.loggers[name] = logger.bind(key_name=name) + + def compute_mpjpe(self, model_joints, targets, + alignments, + gt_joint_idxs=None, + joint_idxs=None): + gt_keyps = [target.get_field( + 'keypoints3d'). smplx_keypoints.detach().cpu().numpy() + for target in targets + if target.has_field('keypoints3d')] + gt_conf = [target.get_field('keypoints3d').conf.detach().cpu().numpy() + for target in targets + if target.has_field('keypoints3d')] + idxs = [idx + for idx, target in enumerate(targets) + if target.has_field('keypoints3d')] + if len(gt_keyps) < 1: + out_array = { + key: np.zeros(model_joints.shape[:2], dtype=model_joints.dtype) + for key in alignments + } + return {'error': defaultdict(lambda: 0.0), + 'valid': 0, 'array': out_array} + if model_joints is None: + return {'error': defaultdict(lambda: 0.0), + +-- Chunk 4 -- +// evaluation.py:358-507 + 'valid': 0, 'array': out_array} + + if torch.is_tensor(model_joints): + model_joints = model_joints.detach().cpu().numpy() + if joint_idxs is None: + joint_idxs = np.arange(0, model_joints.shape[1]) + + gt_keyps = np.asarray(gt_keyps) + gt_conf = np.asarray(gt_conf) + if gt_joint_idxs is not None: + gt_keyps = gt_keyps[:, gt_joint_idxs] + gt_conf = gt_conf[:, gt_joint_idxs] + if joint_idxs is not None: + model_joints = model_joints[:, joint_idxs] + num_valid_joints = (gt_conf > 0).sum() + idxs = np.asarray(idxs) + + mpjpe_err = {} + for alignment_name, alignment in alignments.items(): + mpjpe_err[alignment_name] = [] + for bidx in range(gt_keyps.shape[0]): + align_out = alignment( + model_joints[bidx, :], + gt_keyps[bidx, :]) + mpjpe_err[alignment_name].append( + align_out['point']) + mpjpe_err[alignment_name] = np.stack(mpjpe_err[alignment_name]) + + return { + 'valid': num_valid_joints, + 'array': mpjpe_err + } + + def compute_v2v(self, model_vertices, targets, alignments, vids=None): + if model_vertices is None: + return {'valid': 0, + 'fscore': {}, + 'point': {}} + + gt_vertices = [target.get_field('vertices'). + vertices.detach().cpu().numpy() + for target in targets + if target.has_field('vertices')] + if len(gt_vertices) < 1: + out_array = { + key: np.zeros( + model_vertices.shape[:2], dtype=np.float32) + for key in alignments + } + return {'fscore': {}, + 'valid': 0, 'point': out_array} + gt_vertices = np.array(gt_vertices) + if torch.is_tensor(model_vertices): + model_vertices = model_vertices.detach().cpu().numpy() + + if vids is not None: + gt_vertices = gt_vertices[:, vids] + model_vertices = model_vertices[:, vids] + + v2v_err = {} + fscores = {} + for alignment_name, alignment in alignments.items(): + v2v_err[alignment_name] = [] + fscores[alignment_name] = defaultdict(lambda: []) + + for bidx in range(gt_vertices.shape[0]): + align_out = alignment( + model_vertices[bidx], gt_vertices[bidx]) + v2v_err[alignment_name].append(align_out['point']) + for thresh, val in align_out['fscore'].items(): + fscores[alignment_name][thresh].append( + val['fscore'].copy()) + + v2v_err[alignment_name] = np.stack(v2v_err[alignment_name]) + for thresh in fscores[alignment_name]: + fscores[alignment_name][thresh] = np.stack( + fscores[alignment_name][thresh]) + # logger.info(f'{alignment_name}: {v2v_err[alignment_name].shape}') + + return {'point': v2v_err, 'fscore': fscores} + + def run_head_eval(self, dataloaders, model, step, alignments=None, + device=None): + head_model = model.get_head_model() + if alignments is None: + alignments = {'procrustes': ProcrustesAlignmentMPJPE(), + 'root': RootAlignmentMPJPE()} + if device is None: + device = torch.device('cpu') + + for dataloader in dataloaders: + dset = dataloader.dataset + dset_name = dset.name() + dset_metrics = dset.metrics + + compute_v2v = 'v2v' in dset_metrics + if compute_v2v: + v2v_err = {key: [] for key in alignments} + self.build_metric_logger(f'{dset_name}_v2v') + + fscores = {} + for alignment_name in alignments: + fscores[alignment_name] = {} + for thresh in self.head_fscores_thresh: + fscores[alignment_name][thresh] = [] + self.build_metric_logger( + f'{dset_name}_fscore_{thresh}') + + desc = f'Evaluating dataset: {dset_name}' + for idx, batch in enumerate( + tqdm.tqdm(dataloader, desc=desc, dynamic_ncols=True)): + _, head_imgs, head_targets = batch + + head_imgs = head_imgs.to(device=device) + head_targets = [t.to(device=device) for t in head_targets] + + model_output = head_model(head_imgs=head_imgs, + num_head_imgs=len(head_imgs)) + + head_vertices = model_output.get('vertices') + + out_params = {} + for key, val in model_output.items(): + if not torch.is_tensor(val): + continue + out_params[key] = val.detach().cpu().numpy() + + if compute_v2v: + v2v_output = self.compute_v2v( + head_vertices, head_targets, alignments) + for alignment_name, val in v2v_output['point'].items(): + v2v_err[alignment_name].append(val.copy()) + + for alignment_name, val in v2v_output['fscore'].items(): + for thresh, fscore_val in val.items(): + fscores[alignment_name][thresh].append( + fscore_val) + if idx == 0: + camera_parameters = model_output.get('camera_parameters') + self.create_summaries( + step, dset_name, + head_imgs.detach().cpu().numpy(), + head_targets, + model_output, + camera_parameters=camera_parameters, + degrees=self.head_degrees, + renderer=self.head_renderer, + gt_renderer=self.gt_head_renderer, + prefix='Head', + ) + +-- Chunk 5 -- +// evaluation.py:508-657 + + if compute_v2v: + for key, val in v2v_err.items(): + val = np.concatenate(val, axis=0) + # Divide by the number of items in the dataset and the + # number of vertices + metric_value = val.mean() * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/Head_V2V' + # summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + + for alignment_name, val in fscores.items(): + for thresh, fscore_arr in val.items(): + fscore_arr = np.concatenate(fscore_arr) + if len(fscore_arr) < 1: + continue + metric_value = np.asarray(fscore_arr).mean() + logger.info( + '[{:06d}] {}: F-Score@{:.1f}/{}: {:.3f} ', + step, dset_name, thresh * 1000, + alignment_name, metric_value) + + summary_name = (f'{dset_name}/F@{thresh * 1000:.1f}/' + f'{alignment_name}') + self.filewriter.add_scalar( + summary_name, metric_value, step) + return + + def run_hand_eval(self, dataloaders, model, step, alignments=None, + device=None): + hand_model = model.get_hand_model() + if alignments is None: + alignments = {'procrustes': ProcrustesAlignmentMPJPE(), + 'root': RootAlignmentMPJPE()} + if device is None: + device = torch.device('cpu') + + for dataloader in dataloaders: + dset = dataloader.dataset + dset_name = dset.name() + dset_metrics = dset.metrics + + compute_mpjpe = 'mpjpe' in dset_metrics + if compute_mpjpe: + hand_valid = 0 + mpjpe_err = { + alignment_name: [] for alignment_name in alignments} + self.build_metric_logger(f'{dset_name}_mpjpe') + self.build_metric_logger(f'{dset_name}_hand_mpjpe') + + compute_v2v = 'v2v' in dset_metrics + if compute_v2v: + v2v_err = {key: [] for key in alignments} + self.build_metric_logger(f'{dset_name}_v2v') + + fscores = {} + for alignment_name in alignments: + fscores[alignment_name] = {} + for thresh in self.hand_fscores_thresh: + fscores[alignment_name][thresh] = [] + self.build_metric_logger( + f'{dset_name}_fscore_{thresh}') + + desc = f'Evaluating dataset: {dset_name}' + for idx, batch in enumerate( + tqdm.tqdm(dataloader, desc=desc, dynamic_ncols=True)): + _, hand_imgs, hand_targets = batch + + hand_imgs = hand_imgs.to(device=device) + hand_targets = [t.to(device=device) for t in hand_targets] + + model_output = hand_model(hand_imgs=hand_imgs, + num_hand_imgs=len(hand_imgs)) + + hand_vertices = model_output.get('vertices') + hand_joints = model_output.get('joints') + + out_params = {} + for key, val in model_output.items(): + if not torch.is_tensor(val): + continue + out_params[key] = val.detach().cpu().numpy() + + if compute_mpjpe: + hand_mpjpe_out = self.compute_mpjpe( + hand_joints, hand_targets, + gt_joint_idxs=self.right_hand_idxs, + alignments=alignments) + hand_valid += hand_mpjpe_out['valid'].sum() + + for alignment_name, val in hand_mpjpe_out['array'].items(): + if len(val) < 1: + continue + mpjpe_err[alignment_name].append(val) + + if compute_v2v: + v2v_output = self.compute_v2v( + hand_vertices, hand_targets, alignments) + for alignment_name, val in v2v_output['point'].items(): + v2v_err[alignment_name].append(val) + + for alignment_name, val in v2v_output['fscore'].items(): + for thresh, fscore_val in val.items(): + fscores[alignment_name][thresh].append(fscore_val) + if idx == 0: + camera_parameters = model_output.get('camera_parameters') + self.create_summaries( + step, dset_name, + hand_imgs.detach().cpu().numpy(), + hand_targets, + model_output, + camera_parameters=camera_parameters, + degrees=self.hand_degrees, + renderer=self.hand_renderer, + gt_renderer=self.gt_hand_renderer, + prefix='Hand', + ) + + # Compute hand Mean per Joint Point Error (MPJPE) + if compute_mpjpe: + for key, val in mpjpe_err.items(): + val = np.concatenate(val) + metric_value = val.sum() / hand_valid * 1000 + alignment_name = key.title() + + # Store the Procrustes aligned MPJPE + self.loggers[f'{dset_name}_mpjpe'].info( + '[{:06d}] {}: {} 3D Hand Keypoint error: {:.4f} mm', + step, + dset_name, + alignment_name, + metric_value) + + metric_name = f'{dset_name}/{alignment_name}/Hand' + self.filewriter.add_scalar(metric_name, metric_value, step) + + if compute_v2v: + for key, val in v2v_err.items(): + val = np.concatenate(val, axis=0) + # Divide by the number of items in the dataset and the + # number of vertices + metric_value = val.mean() * 1000 + alignment_name = key.title() + + +-- Chunk 6 -- +// evaluation.py:658-807 + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/Hand_V2V' + # summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + + for alignment_name, val in fscores.items(): + for thresh, fscore_arr in val.items(): + metric_value = np.concatenate( + fscore_arr, axis=0).mean() + summary_name = (f'{dset_name}/F@{thresh * 1000:.1f}/' + f'{alignment_name}') + self.filewriter.add_scalar( + summary_name, metric_value, step) + return + + def run_body_eval(self, dataloaders, model, step, alignments=None, + device=None): + if alignments is None: + alignments = {'procrustes': ProcrustesAlignmentMPJPE(), + # 'root': RootAlignmentMPJPE(), + } + if device is None: + device = torch.device('cpu') + + for dataloader in dataloaders: + dset = dataloader.dataset + + dset_name = dset.name() + dset_metrics = dset.metrics + + compute_body_mpjpe = 'body_mpjpe' in dset_metrics + if compute_body_mpjpe: + body_valid = 0 + body_mpjpe_err = { + alignment_name: [] for alignment_name in alignments} + self.build_metric_logger(f'{dset_name}_body_mpjpe') + + compute_hand_mpjpe = 'hand_mpjpe' in dset_metrics + if compute_hand_mpjpe: + left_hand_valid = 0 + left_hand_mpjpe_err = { + alignment_name: [] for alignment_name in alignments} + + right_hand_valid = 0 + right_hand_mpjpe_err = { + alignment_name: [] for alignment_name in alignments} + self.build_metric_logger(f'{dset_name}_left_hand_mpjpe') + self.build_metric_logger(f'{dset_name}_right_hand_mpjpe') + + compute_head_mpjpe = 'head_mpjpe' in dset_metrics + if compute_head_mpjpe: + head_valid = 0 + head_mpjpe_err = { + alignment_name: [] for alignment_name in alignments} + self.build_metric_logger(f'{dset_name}_head_mpjpe') + + compute_mpjpe14 = 'mpjpe14' in dset_metrics + if compute_mpjpe14: + mpjpe14_err = { + alignment_name: [] for alignment_name in alignments} + self.build_metric_logger(f'{dset_name}_mpjpe14') + + compute_v2v = 'v2v' in dset_metrics + if compute_v2v: + # num_verts = len(self.segm) + v2v_err = {key: [] for key in alignments} + self.build_metric_logger(f'{dset_name}_v2v') + + body_v2v_err = {key: [] for key in alignments} + left_hand_v2v_err = {key: [] for key in alignments} + right_hand_v2v_err = {key: [] for key in alignments} + face_v2v_err = {key: [] for key in alignments} + + if not any([compute_mpjpe14, compute_body_mpjpe, compute_v2v]): + continue + + desc = f'Evaluating dataset: {dset_name}' + + for idx, batch in enumerate( + tqdm.tqdm(dataloader, desc=desc, dynamic_ncols=True)): + + full_imgs_list, body_imgs, body_targets = batch + full_imgs = to_image_list(full_imgs_list) + + hand_imgs, hand_targets = None, None + head_imgs, head_targets = None, None + + if full_imgs is not None: + full_imgs = full_imgs.to(device=device) + body_imgs = body_imgs.to(device=device) + body_targets = [target.to(device) for target in body_targets] + + model_output = model( + body_imgs, body_targets, + hand_imgs=hand_imgs, hand_targets=hand_targets, + head_imgs=head_imgs, head_targets=head_targets, + full_imgs=full_imgs, + device=device) + + body_vertices = None + body_output = model_output.get('body') + body_stage_n_out = body_output.get('final', {}) + if body_stage_n_out is not None: + body_vertices = body_stage_n_out.get('vertices', None) + body_joints = body_stage_n_out.get('joints', None) + if body_vertices is None: + num_stages = body_output.get('num_stages', 1) + body_stage_n_out = body_output.get( + f'stage_{num_stages - 1:02d}', {}) + if body_stage_n_out is not None: + body_vertices = body_stage_n_out.get('vertices', None) + body_joints = body_stage_n_out.get('joints', None) + + out_params = {} + for key, val in body_stage_n_out.items(): + if not torch.is_tensor(val): + continue + out_params[key] = val.detach().cpu().numpy() + + if compute_body_mpjpe: + body_mpjpe_out = self.compute_mpjpe( + body_joints, body_targets, + gt_joint_idxs=self.body_idxs, + joint_idxs=self.body_idxs, + alignments=alignments) + body_valid += body_mpjpe_out['valid'] + + computed_errors = body_mpjpe_out['array'] + for alignment_name, val in computed_errors.items(): + logger.info( + f'{alignment_name}: ' + f'{val.shape}') + if alignment_name == 'pelvis': + continue + body_mpjpe_err[alignment_name].append( + val) + + if compute_head_mpjpe: + head_mpjpe_out = self.compute_mpjpe( + body_joints, head_targets, + gt_joint_idxs=self.head_idxs, + joint_idxs=self.head_idxs, + alignments=alignments) + head_valid += head_mpjpe_out['valid'] + + computed_errors = head_mpjpe_out['array'] + for alignment_name, val in computed_errors.items(): + +-- Chunk 7 -- +// evaluation.py:808-957 + if alignment_name == 'pelvis': + continue + head_mpjpe_err[alignment_name].append(val) + + if compute_hand_mpjpe: + left_hand_mpjpe_out = self.compute_mpjpe( + body_joints, body_targets, + gt_joint_idxs=self.left_hand_idxs, + joint_idxs=self.left_hand_idxs, + alignments=alignments) + left_hand_valid += left_hand_mpjpe_out['valid'] + + computed_errors = left_hand_mpjpe_out['array'] + for alignment_name, val in computed_errors.items(): + if alignment_name == 'pelvis': + continue + left_hand_mpjpe_err[alignment_name].append(val) + + right_hand_mpjpe_out = self.compute_mpjpe( + body_joints, body_targets, + gt_joint_idxs=self.right_hand_idxs, + joint_idxs=self.right_hand_idxs, + alignments=alignments) + right_hand_valid += right_hand_mpjpe_out['valid'] + + computed_errors = right_hand_mpjpe_out['array'] + for alignment_name, val in computed_errors.items(): + if alignment_name == 'pelvis': + continue + right_hand_mpjpe_err[alignment_name].append(val) + + if compute_v2v: + v2v_output = self.compute_v2v( + body_vertices, body_targets, alignments) + for alignment_name, val in v2v_output['point'].items(): + if alignment_name == 'pelvis': + continue + v2v_err[alignment_name].append(val) + + if self.body_vertex_ids is not None: + body_v2v_output = self.compute_v2v( + body_vertices, body_targets, + alignments, vids=self.body_vertex_ids + ) + for alignment_name, val in body_v2v_output['point'].items(): + if alignment_name == 'pelvis': + continue + body_v2v_err[alignment_name].append(val) + if self.left_hand_vertex_ids is not None: + left_hand_v2v_output = self.compute_v2v( + body_vertices, body_targets, + alignments, vids=self.left_hand_vertex_ids + ) + iterator = left_hand_v2v_output['point'].items() + for alignment_name, val in iterator: + if alignment_name == 'pelvis': + continue + left_hand_v2v_err[alignment_name].append(val) + if self.right_hand_vertex_ids is not None: + right_hand_v2v_output = self.compute_v2v( + body_vertices, body_targets, + alignments, vids=self.right_hand_vertex_ids + ) + iterator = right_hand_v2v_output['point'].items() + for alignment_name, val in iterator: + if alignment_name == 'pelvis': + continue + right_hand_v2v_err[alignment_name].append(val) + if self.face_vertex_ids is not None: + face_v2v_output = self.compute_v2v( + body_vertices, body_targets, + alignments, vids=self.face_vertex_ids + ) + for alignment_name, val in face_v2v_output['point'].items(): + if alignment_name == 'pelvis': + continue + face_v2v_err[alignment_name].append(val) + + if compute_mpjpe14 and body_vertices is not None: + gt_joints14 = [target.get_field('joints14'). + joints.detach().cpu().numpy() + for target in body_targets + if target.has_field('joints14')] + if len(gt_joints14) > 0: + gt_joints14 = np.asarray(gt_joints14) + if torch.is_tensor(body_vertices): + body_vertices = body_vertices.detach( + ).cpu().numpy() + + pred_joints = np.einsum( + 'jv,bvm->bjm', self.J14_regressor, body_vertices) + for alignment_name, alignment in alignments.items(): + for bidx in range(gt_joints14.shape[0]): + mpjpe14_err[alignment_name].append( + alignment( + pred_joints[bidx], + gt_joints14[bidx])['point']) + + if idx == 0: + camera_parameters = body_output.get('camera_parameters') + self.create_summaries( + step, dset_name, + body_imgs.detach().cpu().numpy(), + body_targets, + body_stage_n_out, + camera_parameters=camera_parameters, + renderer=self.body_renderer, + gt_renderer=self.gt_body_renderer, + degrees=self.body_degrees, + ) + + # Compute Body Mean per Joint point error + if compute_body_mpjpe: + for key, val in body_mpjpe_err.items(): + val = np.concatenate(val) + logger.info(f'{key}: {val.shape}') + # Compute the mean over the dataset and convert to + # millimeters + logger.info(f'body valid: {body_valid}') + metric_value = val.sum() / body_valid * 1000 + alignment_name = key.title() + + # Store the Procrustes aligned MPJPE + self.loggers[f'{dset_name}_body_mpjpe'].info( + '[{:06d}] {}: {} 3D Keypoint error: {:.4f} mm', + step, dset_name, + alignment_name, + metric_value) + + metric_name = f'{dset_name}/{alignment_name}/MPJPE' + self.filewriter.add_scalar( + metric_name, metric_value, step) + + # Compute Hand Mean per Joint point error + if compute_hand_mpjpe: + for key, val in left_hand_mpjpe_err.items(): + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + # Compute the mean over the dataset and convert to + # millimeters + metric_value = val.sum() / left_hand_valid * 1000 + alignment_name = key.title() + # Store the Procrustes aligned MPJPE + # self.loggers[f'{dset_name}_hand_mpjpe'].info( + logger.info( + '[{:06d}] {}: {} 3D Left Hand Keypoint error: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + metric_name = f'{dset_name}/{alignment_name}/LeftHand' + self.filewriter.add_scalar( + +-- Chunk 8 -- +// evaluation.py:958-1107 + metric_name, metric_value, step) + for key, val in right_hand_mpjpe_err.items(): + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + # Compute the mean over the dataset and convert to + # millimeters + metric_value = val.sum() / right_hand_valid * 1000 + alignment_name = key.title() + # Store the Procrustes aligned MPJPE + # self.loggers[f'{dset_name}_hand_mpjpe'].info( + logger.info( + '[{:06d}] {}: {} 3D Right Hand Keypoint error: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + metric_name = f'{dset_name}/{alignment_name}/RightHand' + self.filewriter.add_scalar( + metric_name, metric_value, step) + + # Compute Head Mean per Joint point error + if compute_head_mpjpe: + for key, val in head_mpjpe_err.items(): + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = val.sum() / head_valid * 1000 + alignment_name = key.title() + + # Store the Procrustes aligned MPJPE + self.loggers[f'{dset_name}_head_mpjpe'].info( + '[{:06d}] {}: {} 3D Head Keypoint error: {:.4f} mm', + step, + dset_name, + alignment_name, + metric_value) + + metric_name = f'{dset_name}/{alignment_name}/Head' + self.filewriter.add_scalar(metric_name, metric_value, step) + + # Compute Mean per Joint point error + if compute_mpjpe14: + for key, val in mpjpe14_err.items(): + if len(val) < 1: + continue + val = np.asarray(val) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + # Store the Procrustes aligned MPJPE + self.loggers[f'{dset_name}_mpjpe14'].info( + '[{:06d}] {}: {} MPJPE: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/MPJPE' + self.filewriter.add_scalar(metric_name, metric_value, step) + + if compute_v2v: + summary_dict = {} + for key, val in v2v_err.items(): + # Divide by the number of items in the dataset and the + # number of vertices + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/V2V' + summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + for key, val in body_v2v_err.items(): + # Divide by the number of items in the dataset and the + # number of vertices + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Body Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/BodyV2V' + summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + for key, val in left_hand_v2v_err.items(): + # Divide by the number of items in the dataset and the + # number of vertices + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Left Hand Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/LeftHandV2V' + summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + + for key, val in right_hand_v2v_err.items(): + # Divide by the number of items in the dataset and the + # number of vertices + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Right Hand Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/RightHandV2V' + summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + + for key, val in face_v2v_err.items(): + # Divide by the number of items in the dataset and the + # number of vertices + if len(val) < 1: + continue + val = np.concatenate(val, axis=0) + metric_value = np.mean(val) * 1000 + alignment_name = key.title() + + self.loggers[f'{dset_name}_v2v'].info( + '[{:06d}] {}: Face Vertex-To-Vertex/{}: {:.4f} mm', + step, dset_name, alignment_name, metric_value) + + metric_name = f'{dset_name}/{alignment_name}/FaceV2V' + summary_dict[metric_name] = val + self.filewriter.add_scalar(metric_name, metric_value, step) + + return + + @torch.no_grad() + def run(self, model, dataloaders, exp_cfg, device, step=0): + if self.rank > 0: + return + model.eval() + assert not (model.training), 'Model is in training mode!' + + body_dloader = dataloaders.get('body', None) + +-- Chunk 9 -- +// evaluation.py:1108-1130 + hand_dloader = dataloaders.get('hand', None) + head_dloader = dataloaders.get('head', None) + + if self.distributed: + eval_model = deepcopy(model.module) + else: + eval_model = deepcopy(model) + + eval_model.eval() + assert not (eval_model.training), 'Model is in training mode!' + if body_dloader is not None: + self.run_body_eval(body_dloader, eval_model, + alignments=self.body_alignments, + step=step, device=device) + if hand_dloader is not None: + self.run_hand_eval(hand_dloader, eval_model, + alignments=self.hand_alignments, + step=step, + device=device) + if head_dloader is not None: + self.run_head_eval(head_dloader, eval_model, + alignments=self.head_alignments, + step=step, device=device) + +=== File: expose/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/__init__.py:1-15 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +=== File: expose/utils/plot_utils.py === + +-- Chunk 1 -- +// plot_utils.py:92-95 + blend_images(img1, img2, alpha=0.7): + return img1 * alpha + (1 - alpha) * img2 + + + +-- Chunk 2 -- +// plot_utils.py:96-121 + target_to_part_mask_img(target, num_parts=14, cmap_name='tab20'): + cmap = mpl_cm.get_cmap(name='tab20') + norm = mpl_colors.Normalize(0, num_parts + 1) + + full_mask = np.full(tuple(target.size), num_parts + 1, + dtype=np.float32) + + for part_idx in range(num_parts): + if not target.has_field(f'part_mask{part_idx}'): + continue + + masks = target.get_field(f'part_mask{part_idx}') + masks = masks.get_mask_tensor() + masks = masks.detach().cpu().numpy().astype(np.float32) + + full_mask[masks > 0] = part_idx + # color = np.asarray(cmap(norm(part_idx)))[:3].reshape(1, 1, 3) + # if colored_mask is None: + # colored_mask = np.zeros(masks.shape + (3,), dtype=masks.dtype) + # colored_mask += masks[:, :, np.newaxis] * color + colored_mask = cmap(norm(full_mask))[:, :, :3] + colored_mask = np.clip(colored_mask, 0.0, 1.0) + + return colored_mask + + + +-- Chunk 3 -- +// plot_utils.py:122-189 + create_skel_img(img, keypoints, connections, valid=None, + names=None, + color_left=[0.9, 0.0, 0.0], + color_right=[0.0, 0.0, 0.9], + color_else=[1.0, 1.0, 1.0], + marker_size=2, linewidth=2, draw_skel=True, + draw_text=True, + ): + kp_mask = np.copy(img) + if valid is None: + valid = np.ones([keypoints.shape[0]]) + + for idx, pair in enumerate(connections): + if pair[0] > len(valid) or pair[1] > len(valid): + continue + if not valid[pair[0]] or not valid[pair[1]]: + continue + + curr_line_width = linewidth + if pair[1] >= 22: + curr_marker_size = int(0.1 * marker_size) + # curr_line_width = 1 + else: + curr_marker_size = marker_size + + if names is not None: + curr_name = names[pair[1]] + + if any([finger_name in curr_name for finger_name in FINGER_NAMES]): + if 'left' in curr_name: + color = HAND_COLORS[LEFT_FINGER.index(curr_name)] + else: + color = HAND_COLORS[RIGHT_FINGER.index(curr_name)] + elif 'left' in curr_name: + color = color_left + elif 'right' in curr_name: + color = color_right + else: + color = color_else + else: + color = color_else + + if pair[1] >= keypoints.shape[0] or pair[0] >= keypoints.shape[0]: + continue + center = tuple(keypoints[pair[1], :].astype(np.int32).tolist()) + + cv2.circle(kp_mask, center, curr_marker_size, color) + + if draw_skel: + if not valid[pair[0]] and not valid[pair[1]]: + continue + start_pt = tuple(keypoints[pair[0], :2].astype(np.int32).tolist()) + end_pt = tuple(keypoints[pair[1], :2].astype(np.int32).tolist()) + cv2.line(kp_mask, start_pt, end_pt, + color, thickness=curr_line_width, + lineType=cv2.LINE_AA) + + if pair[1] <= 22 and draw_text: + cv2.putText(kp_mask, f'{pair[1]}', + center, cv2.FONT_HERSHEY_PLAIN, fontScale=1.0, + color=[0.0, 0.0, 0.0], thickness=4) + cv2.putText(kp_mask, f'{pair[1]}', + center, cv2.FONT_HERSHEY_PLAIN, fontScale=1.0, + color=color, thickness=2) + + return kp_mask + + + +-- Chunk 4 -- +// plot_utils.py:190-199 + create_bbox_img(img, bounding_box, color=(0.0, 0.0, 0.0), + linewidth=2): + bbox_img = img.copy() + xmin, ymin, xmax, ymax = bounding_box.reshape(4) + + cv2.rectangle(bbox_img, (xmin, ymin), (xmax, ymax), + color, thickness=linewidth) + return bbox_img + + + +-- Chunk 5 -- +// plot_utils.py:200-216 + create_dp_img(img, dp_points, cmap='viridis', marker_size=4): + ''' Creates a Dense Pose visualization + ''' + dp_img = np.copy(img) + + cm = mpl_cm.get_cmap(name=cmap) + + num_points = dp_points.shape[0] + colors = cm(np.linspace(0, 1, num_points))[:, :3] + for idx in range(num_points): + center = tuple(dp_points[idx, :].astype(np.int32).tolist()) + cv2.circle(dp_img, center, marker_size, + colors[idx], -1) + + return dp_img + + + +-- Chunk 6 -- +// plot_utils.py:217-257 +ss OpenCVCamera(pyrender.Camera): + PIXEL_CENTER_OFFSET = 0.5 + + def __init__(self, + focal_length=1000, + znear=pyrender.camera.DEFAULT_Z_NEAR, + zfar=None, + name=None): + super(OpenCVCamera, self).__init__( + znear=znear, + zfar=zfar, + name=name, + ) + self.focal_length = focal_length + + def get_projection_matrix(self, width=None, height=None): + cx = 0.5 * width + cy = 0.5 * height + + right = (width - (cx + self.PIXEL_CENTER_OFFSET)) * ( + self.znear / self.focal_length) + left = -(cx + self.PIXEL_CENTER_OFFSET) * (self.znear / + self.focal_length) + top = -(height - (cy + self.PIXEL_CENTER_OFFSET)) * ( + self.znear / self.focal_length) + bottom = (cy + self.PIXEL_CENTER_OFFSET) * ( + self.znear / self.focal_length) + + P = np.zeros([4, 4]) + + P[0][0] = 2 * self.znear / (right - left) + P[1, 1] = -2 * self.znear / (top - bottom) + P[0, 2] = (right + left) / (right - left) + P[1, 2] = (top + bottom) / (top - bottom) + P[2, 2] = - (self.zfar + self.znear) / (self.zfar - self.znear) + P[3, 2] = -1.0 + P[2][3] = (2 * self.zfar * self.znear) / (self.znear - self.zfar) + + return P + + + +-- Chunk 7 -- +// plot_utils.py:258-356 +ss Renderer(object): + def __init__(self, near=0.1, far=200, width=224, height=224, + bg_color=(0.0, 0.0, 0.0, 0.0), ambient_light=None, + use_raymond_lighting=True, + light_color=None, light_intensity=3.0): + if light_color is None: + light_color = np.ones(3) + + self.near = near + self.far = far + + self.renderer = pyrender.OffscreenRenderer(viewport_width=width, + viewport_height=height, + point_size=1.0) + + if ambient_light is None: + ambient_light = (0.1, 0.1, 0.1) + + self.scene = pyrender.Scene(bg_color=bg_color, + ambient_light=ambient_light) + + pc = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, + aspectRatio=float(width) / height) + camera_pose = np.eye(4) + camera_pose[:3, 3] = np.array([0, 0, 2]) + self.scene.add(pc, pose=camera_pose) + + if use_raymond_lighting: + light_nodes = self._create_raymond_lights() + for node in light_nodes: + self.scene.add_node(node) + + def _create_raymond_lights(self): + thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0]) + phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0]) + + nodes = [] + + for phi, theta in zip(phis, thetas): + xp = np.sin(theta) * np.cos(phi) + yp = np.sin(theta) * np.sin(phi) + zp = np.cos(theta) + + z = np.array([xp, yp, zp]) + z = z / np.linalg.norm(z) + x = np.array([-z[1], z[0], 0.0]) + if np.linalg.norm(x) == 0: + x = np.array([1.0, 0.0, 0.0]) + x = x / np.linalg.norm(x) + y = np.cross(z, x) + + matrix = np.eye(4) + matrix[:3, :3] = np.c_[x, y, z] + nodes.append( + pyrender.Node( + light=pyrender.DirectionalLight(color=np.ones(3), + intensity=1.0), + matrix=matrix + )) + + return nodes + + def __call__(self, vertices, faces, img=None, + img_size=224, + body_color=(1.0, 1.0, 1.0, 1.0), + **kwargs): + + centered_verts = vertices - np.mean(vertices, axis=0, keepdims=True) + meshes = self.create_mesh(centered_verts, faces, + vertex_color=body_color) + + for node in self.scene.get_nodes(): + if node.name == 'mesh': + self.scene.remove_node(node) + for mesh in meshes: + self.scene.add(mesh, name='mesh') + + color, _ = self.renderer.render(self.scene) + + return color.astype(np.uint8) + + def create_mesh(self, vertices, faces, + vertex_color=(0.9, 0.9, 0.7, 1.0)): + + tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces) + rot = trimesh.transformations.rotation_matrix(np.radians(180), + [1, 0, 0]) + tri_mesh.apply_transform(rot) + + meshes = [] + + material = pyrender.MetallicRoughnessMaterial( + metallicFactor=0.0, + baseColorFactor=vertex_color) + mesh = pyrender.Mesh.from_trimesh(tri_mesh, material=material) + meshes.append(mesh) + return meshes + + + +-- Chunk 8 -- +// plot_utils.py:357-384 +ss WeakPerspectiveCamera(pyrender.Camera): + PIXEL_CENTER_OFFSET = 0.5 + + def __init__(self, + scale, + translation, + znear=pyrender.camera.DEFAULT_Z_NEAR, + zfar=pyrender.camera.DEFAULT_Z_FAR, + name=None): + super(WeakPerspectiveCamera, self).__init__( + znear=znear, + zfar=zfar, + name=name, + ) + self.scale = scale + self.translation = translation + + def get_projection_matrix(self, width=None, height=None): + P = np.eye(4) + P[0, 0] = self.scale + P[1, 1] = self.scale + P[0, 3] = self.translation[0] * self.scale + P[1, 3] = -self.translation[1] * self.scale + P[2, 2] = -1 + + return P + + + +-- Chunk 9 -- +// plot_utils.py:385-412 +ss WeakPerspectiveCameraNonSquare(pyrender.Camera): + PIXEL_CENTER_OFFSET = 0.5 + + def __init__(self, + scale, + translation, + znear=pyrender.camera.DEFAULT_Z_NEAR, + zfar=pyrender.camera.DEFAULT_Z_FAR, + name=None): + super(WeakPerspectiveCameraNonSquare, self).__init__( + znear=znear, + zfar=zfar, + name=name, + ) + self.scale = scale + self.translation = translation + + def get_projection_matrix(self, width=None, height=None): + P = np.eye(4) + P[0, 0] = self.scale[0] + P[1, 1] = self.scale[1] + P[0, 3] = self.translation[0] * self.scale[0] + P[1, 3] = -self.translation[1] * self.scale[1] + P[2, 2] = -1 + + return P + + + +-- Chunk 10 -- +// plot_utils.py:413-506 +ss AbstractRenderer(object): + def __init__(self, faces=None, img_size=224, use_raymond_lighting=True): + super(AbstractRenderer, self).__init__() + + self.img_size = img_size + self.renderer = pyrender.OffscreenRenderer( + viewport_width=img_size, + viewport_height=img_size, + point_size=1.0) + self.mat_constructor = pyrender.MetallicRoughnessMaterial + self.mesh_constructor = trimesh.Trimesh + self.trimesh_to_pymesh = pyrender.Mesh.from_trimesh + self.transf = trimesh.transformations.rotation_matrix + + self.scene = pyrender.Scene(bg_color=[0.0, 0.0, 0.0, 0.0], + ambient_light=(0.0, 0.0, 0.0)) + if use_raymond_lighting: + light_nodes = self._create_raymond_lights() + for node in light_nodes: + self.scene.add_node(node) + + def _create_raymond_lights(self): + thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0]) + phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0]) + + nodes = [] + + for phi, theta in zip(phis, thetas): + xp = np.sin(theta) * np.cos(phi) + yp = np.sin(theta) * np.sin(phi) + zp = np.cos(theta) + + z = np.array([xp, yp, zp]) + z = z / np.linalg.norm(z) + x = np.array([-z[1], z[0], 0.0]) + if np.linalg.norm(x) == 0: + x = np.array([1.0, 0.0, 0.0]) + x = x / np.linalg.norm(x) + y = np.cross(z, x) + + matrix = np.eye(4) + matrix[:3, :3] = np.c_[x, y, z] + nodes.append( + pyrender.Node( + light=pyrender.DirectionalLight(color=np.ones(3), + intensity=1.0), + matrix=matrix + )) + + return nodes + + def is_active(self): + return self.viewer.is_active + + def close_viewer(self): + if self.viewer.is_active: + self.viewer.close_external() + + def create_mesh(self, vertices, faces, color=(0.3, 0.3, 0.3, 1.0), + wireframe=False, deg=0): + + material = self.mat_constructor( + metallicFactor=0.0, + alphaMode='BLEND', + baseColorFactor=color) + + mesh = self.mesh_constructor(vertices, faces, process=False) + + curr_vertices = vertices.copy() + mesh = self.mesh_constructor( + curr_vertices, faces, process=False) + if deg != 0: + rot = self.transf( + np.radians(deg), [0, 1, 0], + point=np.mean(curr_vertices, axis=0)) + mesh.apply_transform(rot) + + rot = self.transf(np.radians(180), [1, 0, 0]) + mesh.apply_transform(rot) + + return self.trimesh_to_pymesh(mesh, material=material) + + def update_mesh(self, vertices, faces, body_color=(1.0, 1.0, 1.0, 1.0), + deg=0): + for node in self.scene.get_nodes(): + if node.name == 'body_mesh': + self.scene.remove_node(node) + break + + body_mesh = self.create_mesh( + vertices, faces, color=body_color, deg=deg) + self.scene.add(body_mesh, name='body_mesh') + + + +-- Chunk 11 -- +// plot_utils.py:507-573 +ss SMPLifyXRenderer(AbstractRenderer): + def __init__(self, faces=None, img_size=224): + super(SMPLifyXRenderer, self).__init__(faces=faces, img_size=img_size) + + def update_camera(self, translation, rotation=None, focal_length=5000, + camera_center=None): + for node in self.scene.get_nodes(): + if node.name == 'camera': + self.scene.remove_node(node) + if rotation is None: + rotation = np.eye(3, dtype=translation.dtype) + if camera_center is None: + camera_center = np.array( + [self.img_size, self.img_size], dtype=translation.dtype) * 0.5 + + camera_transl = translation.copy() + camera_transl[0] *= -1.0 + pc = pyrender.camera.IntrinsicsCamera( + fx=focal_length, fy=focal_length, + cx=camera_center[0], cy=camera_center[1]) + camera_pose = np.eye(4) + camera_pose[:3, :3] = rotation + camera_pose[:3, 3] = camera_transl + self.scene.add(pc, pose=camera_pose, name='camera') + + @torch.no_grad() + def __call__(self, vertices, faces, + camera_translation, bg_imgs=None, + body_color=(1.0, 1.0, 1.0), + upd_color=None, + **kwargs): + if upd_color is None: + upd_color = {} + + if torch.is_tensor(vertices): + vertices = vertices.detach().cpu().numpy() + if torch.is_tensor(camera_translation): + camera_translation = camera_translation.cpu().numpy() + batch_size = vertices.shape[0] + + output_imgs = [] + for bidx in range(batch_size): + self.update_camera(camera_translation[bidx]) + + curr_col = upd_color.get(bidx, None) + if curr_col is None: + curr_col = body_color + self.update_mesh(vertices[bidx], faces, body_color=curr_col) + + flags = (pyrender.RenderFlags.RGBA | + pyrender.RenderFlags.SKIP_CULL_FACES) + color, depth = self.renderer.render(self.scene, flags=flags) + + color = np.transpose(color, [2, 0, 1]).astype(np.float32) / 255.0 + color = np.clip(color, 0, 1) + + if bg_imgs is None: + output_imgs.append(color[:-1]) + else: + valid_mask = (color[3] > 0)[np.newaxis] + + output_img = (color[:-1] * valid_mask + + (1 - valid_mask) * bg_imgs[bidx]) + output_imgs.append(np.clip(output_img, 0, 1)) + return np.stack(output_imgs, axis=0) + + + +-- Chunk 12 -- +// plot_utils.py:574-654 +ss OverlayRenderer(AbstractRenderer): + def __init__(self, faces=None, img_size=224, tex_size=1): + super(OverlayRenderer, self).__init__(faces=faces, img_size=img_size) + + def update_camera(self, scale, translation): + for node in self.scene.get_nodes(): + if node.name == 'camera': + self.scene.remove_node(node) + + pc = WeakPerspectiveCamera(scale, translation, + znear=1e-5, + zfar=1000) + camera_pose = np.eye(4) + self.scene.add(pc, pose=camera_pose, name='camera') + + @torch.no_grad() + def __call__(self, vertices, faces, + camera_scale, camera_translation, bg_imgs=None, + deg=0, + return_with_alpha=False, + body_color=None, + **kwargs): + + if torch.is_tensor(vertices): + vertices = vertices.detach().cpu().numpy() + if torch.is_tensor(camera_scale): + camera_scale = camera_scale.detach().cpu().numpy() + if torch.is_tensor(camera_translation): + camera_translation = camera_translation.detach().cpu().numpy() + batch_size = vertices.shape[0] + + output_imgs = [] + for bidx in range(batch_size): + if body_color is None: + body_color = COLORS['N'] + + if bg_imgs is not None: + _, H, W = bg_imgs[bidx].shape + # Update the renderer's viewport + self.renderer.viewport_height = H + self.renderer.viewport_width = W + + self.update_camera(camera_scale[bidx], camera_translation[bidx]) + self.update_mesh(vertices[bidx], faces, body_color=body_color, + deg=deg) + + flags = (pyrender.RenderFlags.RGBA | + pyrender.RenderFlags.SKIP_CULL_FACES) + color, depth = self.renderer.render(self.scene, flags=flags) + color = np.transpose(color, [2, 0, 1]).astype(np.float32) / 255.0 + color = np.clip(color, 0, 1) + + if bg_imgs is None: + if return_with_alpha: + output_imgs.append(color) + else: + output_imgs.append(color[:-1]) + else: + if return_with_alpha: + valid_mask = (color[3] > 0)[np.newaxis] + + if bg_imgs[bidx].shape[0] < 4: + curr_bg_img = np.concatenate( + [bg_imgs[bidx], + np.ones_like(bg_imgs[bidx, [0], :, :]) + ], axis=0) + else: + curr_bg_img = bg_imgs[bidx] + + output_img = (color * valid_mask + + (1 - valid_mask) * curr_bg_img) + output_imgs.append(np.clip(output_img, 0, 1)) + else: + valid_mask = (color[3] > 0)[np.newaxis] + + output_img = (color[:-1] * valid_mask + + (1 - valid_mask) * bg_imgs[bidx]) + output_imgs.append(np.clip(output_img, 0, 1)) + return np.stack(output_imgs, axis=0) + + + +-- Chunk 13 -- +// plot_utils.py:655-732 +ss GTRenderer(AbstractRenderer): + def __init__(self, faces=None, img_size=224): + super(GTRenderer, self).__init__(faces=faces, img_size=img_size) + + def update_camera(self, intrinsics): + for node in self.scene.get_nodes(): + if node.name == 'camera': + self.scene.remove_node(node) + pc = pyrender.IntrinsicsCamera( + fx=intrinsics[0, 0], + fy=intrinsics[1, 1], + cx=intrinsics[0, 2], + cy=intrinsics[1, 2], + zfar=1000) + camera_pose = np.eye(4) + self.scene.add(pc, pose=camera_pose, name='camera') + + @torch.no_grad() + def __call__(self, vertices, faces, + intrinsics, bg_imgs=None, deg=0, + return_with_alpha=False, + **kwargs): + ''' Returns a B3xHxW batch of mesh overlays + ''' + + if torch.is_tensor(vertices): + vertices = vertices.detach().cpu().numpy() + if torch.is_tensor(intrinsics): + intrinsics = intrinsics.detach().cpu().numpy() + batch_size = vertices.shape[0] + + body_color = COLORS['GT'] + output_imgs = [] + for bidx in range(batch_size): + if bg_imgs is not None: + _, H, W = bg_imgs[bidx].shape + # Update the renderer's viewport + self.renderer.viewport_height = H + self.renderer.viewport_width = W + self.update_camera(intrinsics[bidx]) + self.update_mesh(vertices[bidx], faces, body_color=body_color, + deg=deg) + + flags = (pyrender.RenderFlags.RGBA | + pyrender.RenderFlags.SKIP_CULL_FACES) + color, depth = self.renderer.render(self.scene, flags=flags) + color = np.transpose(color, [2, 0, 1]).astype(np.float32) / 255.0 + color = np.clip(color, 0, 1) + + if bg_imgs is None: + if return_with_alpha: + output_imgs.append(color) + else: + output_imgs.append(color[:-1]) + else: + if return_with_alpha: + valid_mask = (color[3] > 0)[np.newaxis] + + if bg_imgs[bidx].shape[0] < 4: + curr_bg_img = np.concatenate( + [bg_imgs[bidx], + np.ones_like(bg_imgs[bidx, [0], :, :]) + ], axis=0) + else: + curr_bg_img = bg_imgs[bidx] + + output_img = (color * valid_mask + + (1 - valid_mask) * curr_bg_img) + output_imgs.append(np.clip(output_img, 0, 1)) + else: + valid_mask = (color[3] > 0)[np.newaxis] + + output_img = (color[:-1] * valid_mask + + (1 - valid_mask) * bg_imgs[bidx]) + output_imgs.append(np.clip(output_img, 0, 1)) + return np.stack(output_imgs, axis=0) + + + +-- Chunk 14 -- +// plot_utils.py:733-855 +ss HDRenderer(OverlayRenderer): + def __init__(self, **kwargs): + super(HDRenderer, self).__init__(**kwargs) + + def update_camera(self, focal_length, translation, center): + for node in self.scene.get_nodes(): + if node.name == 'camera': + self.scene.remove_node(node) + + pc = pyrender.IntrinsicsCamera( + fx=focal_length, + fy=focal_length, + cx=center[0], + cy=center[1], + ) + camera_pose = np.eye(4) + camera_pose[:3, 3] = translation.copy() + camera_pose[0, 3] *= (-1) + self.scene.add(pc, pose=camera_pose, name='camera') + + @torch.no_grad() + def __call__(self, + vertices: Tensor, + faces: Union[Tensor, Array], + focal_length: Union[Tensor, Array], + camera_translation: Union[Tensor, Array], + camera_center: Union[Tensor, Array], + bg_imgs: Array, + render_bg: bool = True, + deg: float = 0, + return_with_alpha: bool = False, + body_color: List[float] = None, + **kwargs): + ''' + Parameters + ---------- + vertices: BxVx3, torch.Tensor + The torch Tensor that contains the current vertices to be drawn + faces: Fx3, np.array + The faces of the meshes to be drawn. Right now only support a + batch of meshes with the same topology + focal_length: B, torch.Tensor + The focal length used by the perspective camera + camera_translation: Bx3, torch.Tensor + The translation of the camera estimated by the network + camera_center: Bx2, torch.Tensor + The center of the camera in pixels + bg_imgs: np.ndarray + Optional background images used for overlays + render_bg: bool, optional + Render on top of the background image + deg: float, optional + Degrees to rotate the mesh around itself. Used to render the + same mesh from multiple viewpoints. Defaults to 0 degrees + return_with_alpha: bool, optional + Whether to return the rendered image with an alpha channel. + Default value is False. + body_color: list, optional + The color used to render the image. + ''' + if torch.is_tensor(vertices): + vertices = vertices.detach().cpu().numpy() + if torch.is_tensor(faces): + faces = faces.detach().cpu().numpy() + if torch.is_tensor(focal_length): + focal_length = focal_length.detach().cpu().numpy() + if torch.is_tensor(camera_translation): + camera_translation = camera_translation.detach().cpu().numpy() + if torch.is_tensor(camera_center): + camera_center = camera_center.detach().cpu().numpy() + batch_size = vertices.shape[0] + + output_imgs = [] + for bidx in range(batch_size): + if body_color is None: + body_color = COLORS['N'] + + _, H, W = bg_imgs[bidx].shape + # Update the renderer's viewport + self.renderer.viewport_height = H + self.renderer.viewport_width = W + + self.update_camera( + focal_length=focal_length[bidx], + translation=camera_translation[bidx], + center=camera_center[bidx], + ) + self.update_mesh( + vertices[bidx], faces, body_color=body_color, deg=deg) + + flags = (pyrender.RenderFlags.RGBA | + pyrender.RenderFlags.SKIP_CULL_FACES) + color, depth = self.renderer.render(self.scene, flags=flags) + color = np.transpose(color, [2, 0, 1]).astype(np.float32) / 255.0 + color = np.clip(color, 0, 1) + + if render_bg: + if return_with_alpha: + valid_mask = (color[3] > 0)[np.newaxis] + + if bg_imgs[bidx].shape[0] < 4: + curr_bg_img = np.concatenate( + [bg_imgs[bidx], + np.ones_like(bg_imgs[bidx, [0], :, :]) + ], axis=0) + else: + curr_bg_img = bg_imgs[bidx] + + output_img = (color * valid_mask + + (1 - valid_mask) * curr_bg_img) + output_imgs.append(np.clip(output_img, 0, 1)) + else: + valid_mask = (color[3] > 0)[np.newaxis] + + output_img = (color[:-1] * valid_mask + + (1 - valid_mask) * bg_imgs[bidx]) + output_imgs.append(np.clip(output_img, 0, 1)) + else: + if return_with_alpha: + output_imgs.append(color) + else: + output_imgs.append(color[:-1]) + return np.stack(output_imgs, axis=0) + +=== File: expose/utils/typing_utils.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/utils/typing_utils.py:1-27 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from typing import NewType, List, Union +import numpy as np +import torch + +__all__ = [ + 'Tensor', + 'Array', +] + +Tensor = NewType('Tensor', torch.Tensor) +Array = NewType('Array', np.ndarray) + +=== File: expose/utils/img_utils.py === + +-- Chunk 1 -- +// img_utils.py:28-33 + read_img(img_fn: str, dtype=np.float32) -> Array: + img = cv2.cvtColor(cv2.imread(img_fn), cv2.COLOR_BGR2RGB) + if dtype == np.float32: + if img.dtype == np.uint8: + img = img.astype(dtype) / 255.0 + return img + +=== File: expose/utils/data_structs.py === + +-- Chunk 1 -- +// data_structs.py:18-25 +ss Struct(object): + def __init__(self, **kwargs): + self.keys = list(kwargs.keys()) + for key, val in kwargs.items(): + setattr(self, key, val) + + def keys(self): + return self.keys + +=== File: expose/utils/metrics.py === + +-- Chunk 1 -- +// metrics.py:26-36 +ss NoAligment(object): + def __init__(self): + super(NoAligment, self).__init__() + + def __repr__(self): + return 'NoAlignment' + + def __call__(self, S1, S2): + return S1 + + + +-- Chunk 2 -- +// metrics.py:37-94 +ss ProcrustesAlignment(object): + def __init__(self): + super(ProcrustesAlignment, self).__init__() + + def __repr__(self): + return 'ProcrustesAlignment' + + def __call__(self, S1, S2): + ''' + Computes a similarity transform (sR, t) that takes + a set of 3D points S1 (3 x N) closest to a set of 3D points S2, + where R is an 3x3 rotation matrix, t 3x1 translation, s scale. + i.e. solves the orthogonal Procrustes problem. + ''' + transposed = False + if S1.shape[0] != 3 and S1.shape[0] != 2: + S1 = S1.T + S2 = S2.T + transposed = True + assert(S2.shape[1] == S1.shape[1]) + + # 1. Remove mean. + mu1 = S1.mean(axis=1, keepdims=True) + mu2 = S2.mean(axis=1, keepdims=True) + X1 = S1 - mu1 + X2 = S2 - mu2 + + # 2. Compute variance of X1 used for scale. + var1 = np.sum(X1**2) + + # 3. The outer product of X1 and X2. + K = X1.dot(X2.T) + + # 4. Solution that Maximizes trace(R'K) is R=U*V', where U, V are + # singular vectors of K. + U, s, Vh = np.linalg.svd(K) + V = Vh.T + # Construct Z that fixes the orientation of R to get det(R)=1. + Z = np.eye(U.shape[0]) + Z[-1, -1] *= np.sign(np.linalg.det(U.dot(V.T))) + # Construct R. + R = V.dot(Z.dot(U.T)) + + # 5. Recover scale. + scale = np.trace(R.dot(K)) / var1 + + # 6. Recover translation. + t = mu2 - scale * (R.dot(mu1)) + + # 7. Error: + S1_hat = scale * R.dot(S1) + t + + if transposed: + S1_hat = S1_hat.T + + return S1_hat + + + +-- Chunk 3 -- +// metrics.py:95-124 +ss ProcrustesAlignmentMPJPE(ProcrustesAlignment): + def __init__(self, fscore_thresholds=None): + super(ProcrustesAlignmentMPJPE, self).__init__() + self.fscore_thresholds = fscore_thresholds + + def __repr__(self): + msg = [super(ProcrustesAlignment).__repr__()] + if self.fscore_thresholds is not None: + msg.append( + 'F-Score thresholds: ' + + f'(mm), '.join(map(lambda x: f'{x * 1000}', + self.fscore_thresholds)) + ) + return '\n'.join(msg) + + def __call__(self, est_points, gt_points): + aligned_est_points = super(ProcrustesAlignmentMPJPE, self).__call__( + est_points, gt_points) + + fscore = {} + if self.fscore_thresholds is not None: + for thresh in self.fscore_thresholds: + fscore[thresh] = point_fscore( + aligned_est_points, gt_points, thresh) + return { + 'point': mpjpe(aligned_est_points, gt_points), + 'fscore': fscore + } + + + +-- Chunk 4 -- +// metrics.py:125-170 +ss ScaleAlignment(object): + def __init__(self): + super(ScaleAlignment, self).__init__() + + def __repr__(self): + return 'ScaleAlignment' + + def __call__(self, S1, S2): + ''' + Computes a similarity transform (sR, t) that takes + a set of 3D points S1 (3 x N) closest to a set of 3D points S2, + where R is an 3x3 rotation matrix, t 3x1 translation, s scale. + i.e. solves the orthogonal Procrutes problem. + ''' + transposed = False + if S1.shape[0] != 3 and S1.shape[0] != 2: + S1 = S1.T + S2 = S2.T + transposed = True + assert(S2.shape[1] == S1.shape[1]) + + # 1. Remove mean. + mu1 = S1.mean(axis=1, keepdims=True) + mu2 = S2.mean(axis=1, keepdims=True) + X1 = S1 - mu1 + X2 = S2 - mu2 + + # 2. Compute variance of X1 used for scale. + var1 = np.sum(X1**2) + var2 = np.sum(X2**2) + + # 5. Recover scale. + scale = np.sqrt(var2 / var1) + + # 6. Recover translation. + t = mu2 - scale * (mu1) + + # 7. Error: + S1_hat = scale * S1 + t + + if transposed: + S1_hat = S1_hat.T + + return S1_hat + + + +-- Chunk 5 -- +// metrics.py:171-198 +ss RootAlignmentMPJPE(object): + def __init__(self, root=0, fscore_thresholds=None): + super(RootAlignmentMPJPE, self).__init__() + self.root = root + self.fscore_thresholds = fscore_thresholds + + def align_by_root(self, joints): + root_joint = joints[self.root, :] + return {'joints': joints - root_joint, 'root': root_joint} + + def __call__(self, gt, est): + gt_out = self.align_by_root(gt) + est_out = self.align_by_root(est) + + aligned_gt_joints = gt_out['joints'] + aligned_est_joints = est_out['joints'] + fscore = {} + if self.fscore_thresholds is not None: + for thresh in self.fscore_thresholds: + fscore[thresh] = point_fscore( + aligned_est_joints, aligned_gt_joints, thresh) + + return { + 'point': mpjpe(aligned_est_joints, aligned_gt_joints), + 'fscore': fscore + } + + + +-- Chunk 6 -- +// metrics.py:199-219 +ss PelvisAlignment(object): + def __init__(self, hips_idxs=None): + super(PelvisAlignment, self).__init__() + if hips_idxs is None: + hips_idxs = [2, 3] + self.hips_idxs = hips_idxs + + def align_by_pelvis(self, joints): + pelvis = joints[self.hips_idxs, :].mean(axis=0, keepdims=True) + return {'joints': joints - pelvis, 'pelvis': pelvis} + + def __call__(self, gt, est): + gt_out = self.align_by_pelvis(gt) + est_out = self.align_by_pelvis(est) + + aligned_gt_joints = gt_out['joints'] + aligned_est_joints = est_out['joints'] + + return aligned_gt_joints, aligned_est_joints + + + +-- Chunk 7 -- +// metrics.py:220-249 +ss PelvisAlignmentMPJPE(PelvisAlignment): + def __init__(self, fscore_thresholds=None): + super(PelvisAlignmentMPJPE, self).__init__() + self.fscore_thresholds = fscore_thresholds + + def __repr__(self): + msg = [super(PelvisAlignmentMPJPE).__repr__()] + if self.fscore_thresholds is not None: + msg.append( + 'F-Score thresholds: ' + + f'(mm), '.join(map(lambda x: f'{x * 1000}', + self.fscore_thresholds)) + ) + return '\n'.join(msg) + + def __call__(self, est_points, gt_points): + aligned_gt_points, aligned_est_points = super( + PelvisAlignmentMPJPE, self).__call__(gt_points, est_points) + + fscore = {} + if self.fscore_thresholds is not None: + for thresh in self.fscore_thresholds: + fscore[thresh] = point_fscore( + aligned_est_points, gt_points, thresh) + return { + 'point': mpjpe(aligned_est_points, aligned_gt_points), + 'fscore': fscore + } + + + +-- Chunk 8 -- +// metrics.py:250-267 + mpjpe(input_joints, target_joints): + ''' Calculate mean per-joint point error + + Parameters + ---------- + input_joints: numpy.array, Jx3 + The joints predicted by the model + target_joints: numpy.array, Jx3 + The ground truth joints + Returns + ------- + numpy.array, BxJ + The per joint point error for each element in the batch + ''' + + return np.sqrt(np.power(input_joints - target_joints, 2).sum(axis=-1)) + + + +-- Chunk 9 -- +// metrics.py:268-271 + vertex_to_vertex_error(input_vertices, target_vertices): + return np.sqrt(np.power(input_vertices - target_vertices, 2).sum(axis=-1)) + + + +-- Chunk 10 -- +// metrics.py:272-298 + point_fscore( + pred: torch.Tensor, + gt: torch.Tensor, + thresh: float) -> Dict[str, float]: + if torch.is_tensor(pred): + pred = pred.detach().cpu().numpy() + if torch.is_tensor(gt): + gt = gt.detach().cpu().numpy() + + pred_pcl = np2o3d_pcl(pred) + gt_pcl = np2o3d_pcl(gt) + + gt_to_pred = np.asarray(gt_pcl.compute_point_cloud_distance(pred_pcl)) + pred_to_gt = np.asarray(pred_pcl.compute_point_cloud_distance(gt_pcl)) + + recall = (pred_to_gt < thresh).sum() / len(pred_to_gt) + precision = (gt_to_pred < thresh).sum() / len(gt_to_pred) + if recall + precision > 0.0: + fscore = 2 * recall * precision / (recall + precision) + else: + fscore = 0.0 + + return { + 'fscore': fscore, + 'precision': precision, + 'recall': recall, + } + +=== File: expose/utils/transf_utils.py === + +-- Chunk 1 -- +// transf_utils.py:28-61 + get_transform( + center: Array, scale: float, + res: Tuple[int], + rot: float = 0 +) -> Array: + """ + General image processing functions + """ + # Generate transformation matrix + h = 200 * scale + t = np.zeros((3, 3), dtype=np.float32) + t[0, 0] = float(res[1]) / h + t[1, 1] = float(res[0]) / h + t[0, 2] = res[1] * (-float(center[0]) / h + .5) + t[1, 2] = res[0] * (-float(center[1]) / h + .5) + t[2, 2] = 1 + if not rot == 0: + rot = -rot # To match direction of rotation from cropping + rot_mat = np.zeros((3, 3), dtype=np.float32) + rot_rad = rot * np.pi / 180 + sn, cs = np.sin(rot_rad), np.cos(rot_rad) + rot_mat[0, :2] = [cs, -sn] + rot_mat[1, :2] = [sn, cs] + rot_mat[2, 2] = 1 + # Need to rotate around center + t_mat = np.eye(3) + t_mat[0, 2] = -res[1] / 2 + t_mat[1, 2] = -res[0] / 2 + t_inv = t_mat.copy() + t_inv[:2, 2] *= -1 + t = np.dot(t_inv, np.dot(rot_mat, np.dot(t_mat, t))) + return t.astype(np.float32) + + + +-- Chunk 2 -- +// transf_utils.py:64-73 + transform(pt, center, scale, res, invert=0, rot=0): + # Transform pixel location to different reference + t = get_transform(center, scale, res, rot=rot) + if invert: + t = np.linalg.inv(t) + new_pt = np.array([pt[0] - 1, pt[1] - 1, 1.], dtype=np.float32).T + new_pt = np.dot(t, new_pt) + return new_pt[:2].astype(int) + 1 + + + +-- Chunk 3 -- +// transf_utils.py:74-119 + crop(img, center, scale, res, rot=0, dtype=np.float32): + # Upper left point + ul = np.array(transform([1, 1], center, scale, res, invert=1)) - 1 + # Bottom right point + br = np.array(transform([res[0] + 1, res[1] + 1], + center, scale, res, invert=1)) - 1 + # size of cropped image + # crop_shape = [br[1] - ul[1], br[0] - ul[0]] + # Padding so that when rotated proper amount of context is included + pad = int(np.linalg.norm(br - ul) / 2 - float(br[1] - ul[1]) / 2) + + if not rot == 0: + ul -= pad + br += pad + + new_shape = [br[1] - ul[1], br[0] - ul[0]] + if len(img.shape) > 2: + new_shape += [img.shape[2]] + new_shape = list(map(int, new_shape)) + new_img = np.zeros(new_shape, dtype=img.dtype) + + # Range to fill new array + new_x = max(0, -ul[0]), min(br[0], len(img[0])) - ul[0] + new_y = max(0, -ul[1]), min(br[1], len(img)) - ul[1] + + # Range to sample from original image + old_x = max(0, ul[0]), min(len(img[0]), br[0]) + old_y = max(0, ul[1]), min(len(img), br[1]) + # Range to sample from original image + new_img[new_y[0]:new_y[1], new_x[0]:new_x[1] + ] = img[old_y[0]:old_y[1], old_x[0]:old_x[1]] + + # pixel_scale = 1.0 if new_img.max() > 1.0 else 255 + # resample = pil_img.BILINEAR + if not rot == 0: + new_H, new_W, _ = new_img.shape + + rotn_center = (new_W / 2.0, new_H / 2.0) + M = cv2.getRotationMatrix2D(rotn_center, rot, 1.0).astype(np.float32) + + new_img = cv2.warpAffine(new_img, M, tuple(new_shape[:2]), + cv2.INTER_LINEAR_EXACT) + new_img = new_img[pad:new_H - pad, pad:new_W - pad] + + output = cv2.resize(new_img, tuple(res), interpolation=cv2.INTER_LINEAR) + return output.astype(np.float32) + +=== File: expose/utils/checkpointer.py === + +-- Chunk 1 -- +// checkpointer.py:27-150 +ss Checkpointer(object): + def __init__(self, model, optimizer=None, scheduler=None, + adv_optimizer=None, + pretrained='', + distributed=False, + rank=0, + save_dir='/tmp/exp'): + self.rank = rank + self.distributed = distributed + + self.model = model + self.optimizer = optimizer + self.scheduler = scheduler + self.adv_optimizer = adv_optimizer + + self.save_dir = save_dir + if self.rank == 0: + logger.info(f'Creating directory {self.save_dir}') + os.makedirs(self.save_dir, exist_ok=True) + self.pretrained = pretrained + + def save_checkpoint(self, name, **kwargs): + if self.rank > 0: + return + ckpt_data = {} + ckpt_data['model'] = self.model.state_dict() + + if self.optimizer is not None: + logger.info('Adding optimizer state ...') + ckpt_data['optimizer'] = self.optimizer.state_dict() + if self.scheduler is not None: + logger.info('Adding scheduler state ...') + ckpt_data['scheduler'] = self.scheduler.state_dict() + if self.adv_optimizer is not None: + logger.info('Adding discriminator optimizer state ...') + ckpt_data['adv_optimizer'] = self.adv_optimizer.state_dict() + + ckpt_data.update(kwargs) + + curr_ckpt_fn = osp.join(self.save_dir, name) + logger.info('Saving checkpoint to {}'.format(curr_ckpt_fn)) + torch.save(ckpt_data, curr_ckpt_fn) + with open(osp.join(self.save_dir, 'latest_checkpoint'), 'w') as f: + f.write(curr_ckpt_fn) + ckpt_data.clear() + + def load_checkpoint(self): + save_fn = osp.join(self.save_dir, 'latest_checkpoint') + + load_pretrained = False + if not osp.exists(save_fn): + # If no previous checkpoint exists, load from the pretrained model + if len(self.pretrained) > 1: + self.pretrained = osp.expandvars(self.pretrained) + load_pretrained = True + save_fn = osp.join( + self.pretrained, 'checkpoints', 'latest_checkpoint') + # If neither the pretrained model exists nor there is a previous + # checkpoint then initialize from scratch + if not osp.exists(save_fn): + logger.warning(f'No checkpoint found in {self.save_dir}!') + return {} + + logger.info('Load pretrained: {}', load_pretrained) + with open(save_fn, 'r') as f: + latest_ckpt_fn = f.read().strip() + logger.warning(f'Loading checkpoint from {latest_ckpt_fn}!') + + if self.distributed: + map_location = torch.device(f'cuda:{self.rank}') + else: + map_location = torch.device('cpu') + ckpt_data = torch.load(latest_ckpt_fn, map_location=map_location) + + if load_pretrained: + if 'face_idxs' in ckpt_data['model']: + del ckpt_data['model']['face_idxs'] + if 'smplx.smplx_loss.body_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.smplx_loss.body_idxs'] + if 'smplx.smplx_loss.hand_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.smplx_loss.hand_idxs'] + if 'smplx.smplx_loss.face_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.smplx_loss.face_idxs'] + if 'smplx.smplx_loss.left_hand_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.smplx_loss.left_hand_idxs'] + if 'smplx.smplx_loss.right_hand_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.smplx_loss.right_hand_idxs'] + if 'smplx.head_idxs' in ckpt_data['model']: + del ckpt_data['model']['smplx.head_idxs'] + + missing, unexpected = self.model.load_state_dict( + # ckpt_data['model'], strict=not load_pretrained) + ckpt_data['model'], strict=False) + if len(missing) > 0: + logger.warning( + f'The following keys were not found: {missing}') + if len(unexpected): + logger.warning( + f'The following keys were not expected: {unexpected}') + + if self.optimizer is not None and 'optimizer' in ckpt_data: + if not load_pretrained: + logger.warning('Loading optimizer data from: {}'.format( + self.save_dir)) + self.optimizer.load_state_dict(ckpt_data['optimizer']) + + if self.scheduler is not None and 'scheduler' in ckpt_data: + if not load_pretrained: + logger.warning('Loading scheduler data from: {}'.format( + self.save_dir)) + self.scheduler.load_state_dict(ckpt_data['scheduler']) + if self.adv_optimizer is not None and 'adv_optimizer' in ckpt_data: + if not load_pretrained: + logger.warning( + 'Loading discriminator optim data from: {}'.format( + self.save_dir)) + self.adv_optimizer.load_state_dict( + ckpt_data['adv_optimizer']) + + if load_pretrained: + ckpt_data['iteration'] = 0 + ckpt_data['epoch_number'] = 0 + + return ckpt_data + +=== File: expose/utils/torch_utils.py === + +-- Chunk 1 -- +// torch_utils.py:23-26 + no_reduction(arg): + return arg + + + +-- Chunk 2 -- +// torch_utils.py:27-37 + to_tensor( + tensor: Union[Tensor, Array], + device=None, + dtype=torch.float32 +) -> Tensor: + if torch.is_tensor(tensor): + return tensor + else: + return torch.tensor(tensor, dtype=dtype, device=device) + + + +-- Chunk 3 -- +// torch_utils.py:38-49 + get_reduction_method(reduction='mean'): + if reduction == 'mean': + reduction = torch.mean + elif reduction == 'sum': + reduction = torch.sum + elif reduction == 'none': + reduction = no_reduction + else: + raise ValueError('Unknown reduction type: {}'.format(reduction)) + return reduction + + + +-- Chunk 4 -- +// torch_utils.py:50-56 + tensor_to_numpy(tensor: Tensor, default=None) -> Array: + if tensor is None: + return default + else: + return tensor.detach().cpu().numpy() + + + +-- Chunk 5 -- +// torch_utils.py:57-63 + rot_mat_to_euler(rot_mats: Tensor) -> Tensor: + # Calculates rotation matrix to euler angles + # Careful for extreme cases of eular angles like [0.0, pi, 0.0] + + sy = torch.sqrt(rot_mats[:, 0, 0] * rot_mats[:, 0, 0] + + rot_mats[:, 1, 0] * rot_mats[:, 1, 0]) + return torch.atan2(-rot_mats[:, 2, 0], sy) + +=== File: expose/utils/__init__.py === + +-- Chunk 1 -- +// __init__.py:18-19 + nand(x: bool, y: bool) -> bool: + return not (x and y) + +=== File: expose/utils/timer.py === + +-- Chunk 1 -- +// timer.py:24-42 +ss Timer(object): + def __init__(self, name='', sync=False): + super(Timer, self).__init__() + self.elapsed = [] + self.name = name + self.sync = sync + + def __enter__(self): + if self.sync: + torch.cuda.synchronize() + self.start = time.perf_counter() + + def __exit__(self, type, value, traceback): + if self.sync: + torch.cuda.synchronize() + elapsed = time.perf_counter() - self.start + self.elapsed.append(elapsed) + logger.info( + f'[{self.name}]: {elapsed:.3f}, {np.mean(self.elapsed):.3f}') + +=== File: expose/utils/np_utils.py === + +-- Chunk 1 -- +// np_utils.py:21-24 + rel_change(prev_val, curr_val): + return (prev_val - curr_val) / max([np.abs(prev_val), np.abs(curr_val), 1]) + + + +-- Chunk 2 -- +// np_utils.py:25-28 + max_grad_change(grad_arr): + return grad_arr.abs().max() + + + +-- Chunk 3 -- +// np_utils.py:29-34 + to_np(array, dtype=np.float32): + if 'scipy.sparse' in str(type(array)): + array = array.todense() + return np.array(array, dtype=dtype) + + + +-- Chunk 4 -- +// np_utils.py:35-39 + np2o3d_pcl(x: np.ndarray) -> o3d.geometry.PointCloud: + pcl = o3d.geometry.PointCloud() + pcl.points = o3d.utility.Vector3dVector(x) + + return pcl + +=== File: expose/utils/cfg_utils.py === + +-- Chunk 1 -- +// cfg_utils.py:20-27 + cfg_to_dict(cfg_node): + if type(cfg_node) in BUILTINS: + return cfg_node + else: + curr_dict = dict(cfg_node) + for key, val in curr_dict.items(): + curr_dict[key] = cfg_to_dict(val) + return curr_dict + +=== File: expose/utils/rotation_utils.py === + +-- Chunk 1 -- +// rotation_utils.py:20-54 + batch_rodrigues(rot_vecs, epsilon=1e-8): + ''' Calculates the rotation matrices for a batch of rotation vectors + Parameters + ---------- + rot_vecs: torch.tensor Nx3 + array of N axis-angle vectors + Returns + ------- + R: torch.tensor Nx3x3 + The rotation matrices for the given axis-angle parameters + ''' + + batch_size = rot_vecs.shape[0] + device = rot_vecs.device + dtype = rot_vecs.dtype + + angle = torch.norm(rot_vecs + epsilon, dim=1, keepdim=True, p=2) + rot_dir = rot_vecs / angle + + cos = torch.unsqueeze(torch.cos(angle), dim=1) + sin = torch.unsqueeze(torch.sin(angle), dim=1) + + # Bx1 arrays + rx, ry, rz = torch.split(rot_dir, 1, dim=1) + K = torch.zeros((batch_size, 3, 3), dtype=dtype, device=device) + + zeros = torch.zeros((batch_size, 1), dtype=dtype, device=device) + K = torch.cat([zeros, -rz, ry, rz, zeros, -rx, -ry, rx, zeros], dim=1) \ + .view((batch_size, 3, 3)) + + ident = torch.eye(3, dtype=dtype, device=device).unsqueeze(dim=0) + rot_mat = ident + sin * K + (1 - cos) * torch.bmm(K, K) + return rot_mat + + + +-- Chunk 2 -- +// rotation_utils.py:55-98 + batch_rot2aa(Rs, epsilon=1e-7): + """ + Rs is B x 3 x 3 + void cMathUtil::RotMatToAxisAngle(const tMatrix& mat, tVector& out_axis, + double& out_theta) + { + double c = 0.5 * (mat(0, 0) + mat(1, 1) + mat(2, 2) - 1); + c = cMathUtil::Clamp(c, -1.0, 1.0); + + out_theta = std::acos(c); + + if (std::abs(out_theta) < 0.00001) + { + out_axis = tVector(0, 0, 1, 0); + } + else + { + double m21 = mat(2, 1) - mat(1, 2); + double m02 = mat(0, 2) - mat(2, 0); + double m10 = mat(1, 0) - mat(0, 1); + double denom = std::sqrt(m21 * m21 + m02 * m02 + m10 * m10); + out_axis[0] = m21 / denom; + out_axis[1] = m02 / denom; + out_axis[2] = m10 / denom; + out_axis[3] = 0; + } + } + """ + + cos = 0.5 * (torch.einsum('bii->b', [Rs]) - 1) + cos = torch.clamp(cos, -1 + epsilon, 1 - epsilon) + + theta = torch.acos(cos) + + m21 = Rs[:, 2, 1] - Rs[:, 1, 2] + m02 = Rs[:, 0, 2] - Rs[:, 2, 0] + m10 = Rs[:, 1, 0] - Rs[:, 0, 1] + denom = torch.sqrt(m21 * m21 + m02 * m02 + m10 * m10 + epsilon) + + axis0 = torch.where(torch.abs(theta) < 0.00001, m21, m21 / denom) + axis1 = torch.where(torch.abs(theta) < 0.00001, m02, m02 / denom) + axis2 = torch.where(torch.abs(theta) < 0.00001, m10, m10 / denom) + + return theta.unsqueeze(1) * torch.stack([axis0, axis1, axis2], 1) + +=== File: expose/losses/robustifiers.py === + +-- Chunk 1 -- +// robustifiers.py:29-37 + build_robustifier(robustifier_type: str = None, **kwargs) -> nn.Module: + if robustifier_type is None or robustifier_type == 'none': + return None + elif robustifier_type == 'gmof': + return GMOF(**kwargs) + else: + raise ValueError(f'Unknown robustifier: {robustifier_type}') + + + +-- Chunk 2 -- +// robustifiers.py:38-48 +ss GMOF(nn.Module): + def __init__(self, rho: float = 100, **kwargs) -> None: + super(GMOF, self).__init__() + self.rho = rho + + def extra_repr(self): + return f'Rho = {self.rho}' + + def forward(self, residual): + squared_residual = residual.pow(2) + return torch.div(squared_residual, squared_residual + self.rho ** 2) + +=== File: expose/losses/priors.py === + +-- Chunk 1 -- +// priors.py:44-66 + build_prior(prior_type, rho=100, reduction='mean', size_average=True, + **kwargs): + logger.debug('Building prior: {}', prior_type) + if prior_type == 'l2': + return L2Prior(reduction=reduction, **kwargs) + elif prior_type == 'l1': + return L1Prior(reduction=reduction, **kwargs) + elif prior_type == 'identity': + return IdentityPrior(reduction=reduction, **kwargs) + elif prior_type == 'mean': + return MeanPrior(reduction=reduction, **kwargs) + elif prior_type == 'penalty': + return PenaltyPrior(reduction=reduction, **kwargs) + elif prior_type == 'barrier': + return BarrierPrior(reduction=reduction, **kwargs) + elif prior_type == 'threshold': + return ThresholdPrior(reduction=reduction, **kwargs) + elif prior_type == 'gmm': + return GMMPrior(reduction=reduction, **kwargs) + else: + raise ValueError('Unknown prior type: {}'.format(prior_type)) + + + +-- Chunk 2 -- +// priors.py:67-83 +ss MeanPrior(nn.Module): + def __init__(self, mean=None, reduction='mean', **kwargs): + super(MeanPrior, self).__init__() + assert mean is not None, 'Request MeanPrior, but mean was not given!' + if type(mean) is not torch.Tensor: + mean = torch.tensor(mean) + self.register_buffer('mean', mean.view(1, *list(mean.shape))) + self.reduction_str = reduction + self.reduction = get_reduction_method(reduction) + + def extra_repr(self): + return f'Mean: {self.mean.shape}' + + def forward(self, module_input, *args, **kwargs): + return (module_input - self.mean).pow(2).sum() / module_input.shape[0] + + + +-- Chunk 3 -- +// priors.py:84-101 +ss IdentityPrior(nn.Module): + def __init__(self, reduction='mean', **kwargs): + ''' Penalizes inputs to be close to identity matrix + ''' + super(IdentityPrior, self).__init__() + self.reduction_str = reduction + self.reduction = get_reduction_method(reduction) + + self.register_buffer( + 'identity', torch.eye(3, dtype=torch.float32).unsqueeze(dim=0)) + + def forward(self, module_input, *args, **kwargs): + x = module_input.view(-1, 3, 3) + batch_size = module_input.shape[0] + + return (x - self.identity).pow(2).sum() / batch_size + + + +-- Chunk 4 -- +// priors.py:102-136 +ss ThresholdPrior(nn.Module): + def __init__(self, reduction='mean', margin=1, norm='l2', epsilon=1e-7, + **kwargs): + super(ThresholdPrior, self).__init__() + self.reduction_str = reduction + self.reduction = get_reduction_method(reduction) + self.margin = margin + assert norm in ['l1', 'l2'], 'Norm variable must me l1 or l2' + self.norm = norm + self.epsilon = epsilon + + def extra_repr(self): + msg = 'Reduction: {}\n'.format(self.reduction_str) + msg += 'Margin: {}\n'.format(self.margin) + msg += 'Norm: {}'.format(self.norm) + return msg + + def forward(self, module_input, *args, **kwargs): + batch_size = module_input.shape[0] + + abs_values = module_input.abs() + mask = abs_values.gt(self.margin) + + invalid_values = torch.masked_select(module_input, mask) + + if self.norm == 'l1': + return invalid_values.abs().sum() / ( + mask.to(dtype=module_input.dtype).sum() + self.epsilon + ) + elif self.norm == 'l2': + return invalid_values.pow(2).sum() / ( + mask.to(dtype=module_input.dtype).sum() + self.epsilon + ) + + + +-- Chunk 5 -- +// priors.py:137-195 +ss PenaltyPrior(nn.Module): + def __init__(self, reduction='mean', margin=1, norm='l2', epsilon=1e-7, + use_vector=True, + **kwargs): + ''' Soft constraint to prevent parameters for leaving feasible set + + Implements a penalty constraint that encourages the parameters to + stay in the feasible set of solutions. Assumes that the initial + estimate is already in this set + ''' + super(PenaltyPrior, self).__init__() + self.reduction_str = reduction + self.reduction = get_reduction_method(reduction) + self.margin = margin + assert norm in ['l1', 'l2'], 'Norm variable must me l1 or l2' + self.norm = norm + self.epsilon = epsilon + self.use_vector = use_vector + + def extra_repr(self): + msg = 'Reduction: {}\n'.format(self.reduction_str) + msg += 'Margin: {}\n'.format(self.margin) + msg += 'Norm: {}'.format(self.norm) + return msg + + def forward(self, module_input, *args, **kwargs): + batch_size = module_input.shape[0] + if self.use_vector: + + if self.norm == 'l1': + param_norm = module_input.abs().view( + batch_size, -1).sum(dim=-1) + margin = self.margin + elif self.norm == 'l2': + param_norm = module_input.pow(2).view( + batch_size, -1).sum(dim=-1) + margin = self.margin ** 2 + + thresholded_vals = F.relu(param_norm - margin) + non_zeros = ( + thresholded_vals.gt(0).to(torch.float32).sum() + self.epsilon) + return (thresholded_vals.sum() / non_zeros) + else: + upper_margin = F.relu(module_input - self.margin) + lower_margin = F.relu(-(module_input + self.margin)) + with torch.no_grad(): + upper_non_zeros = ( + upper_margin.gt(0).to(torch.float32).sum() + self.epsilon) + lower_non_zeros = ( + lower_margin.gt(0).to(torch.float32).sum() + self.epsilon) + + if self.norm == 'l1': + return (upper_margin.abs().sum() / upper_non_zeros + + lower_margin.abs().sum() / lower_non_zeros) + elif self.norm == 'l2': + return (upper_margin.pow(2).sum() / upper_non_zeros + + lower_margin.pow(2).sum() / lower_non_zeros) + + + +-- Chunk 6 -- +// priors.py:196-236 +ss BarrierPrior(nn.Module): + def __init__(self, reduction='mean', margin=1, barrier='log', + epsilon=1e-7, symmetric=True, **kwargs): + ''' Soft constraint that pushes parameters away from the border + + Implements a barrier constraint that encourages the parameters to + stay away from the border of the feasible set. Assumes that the initial + estimate is already in this set + ''' + super(BarrierPrior, self).__init__() + self.reduction_str = reduction + self.reduction = get_reduction_method(reduction) + assert barrier in ['log', 'inv'], 'Norm variable must me inv or log' + self.barrier = barrier + self.epsilon = epsilon + self.symmetric = symmetric + self.register_buffer('margin', torch.tensor(margin)) + + def extra_repr(self): + msg = 'Reduction: {}\n'.format(self.reduction_str) + msg += 'Margin: {}\n'.format(self.margin) + msg += 'Barrier: {}'.format(self.barrier) + msg += 'Symmetric: {}'.format(self.symmetric) + return msg + + def forward(self, module_input, *args, **kwargs): + if self.barrier == 'log': + loss = -torch.log(self.margin) - torch.log( + -(module_input - self.margin) + self.epsilon).mean() + if self.symmetric: + loss += -torch.log(self.margin) - torch.log( + (module_input + self.margin) + self.epsilon).mean() + elif self.barrier == 'inv': + loss = - 1 / (module_input - self.margin + self.epsilon).mean() + if self.symmetric: + loss += 1 / (module_input + self.margin) + # Compensate for the minimum to make it zero + loss -= 1 + return loss + + + +-- Chunk 7 -- +// priors.py:237-245 +ss L1Prior(nn.Module): + def __init__(self, dtype=torch.float32, reduction='mean', **kwargs): + super(L1Prior, self).__init__() + self.reduction = get_reduction_method(reduction) + + def forward(self, module_input, *args): + return self.reduction(module_input.abs().sum(dim=-1)) + + + +-- Chunk 8 -- +// priors.py:246-254 +ss L2Prior(nn.Module): + def __init__(self, dtype=torch.float32, reduction='mean', **kwargs): + super(L2Prior, self).__init__() + self.reduction = get_reduction_method(reduction) + + def forward(self, module_input, *args): + return self.reduction(module_input.pow(2)) + + + +-- Chunk 9 -- +// priors.py:255-375 +ss GMMPrior(nn.Module): + + def __init__(self, path, + num_gaussians=6, dtype=torch.float32, epsilon=1e-16, + reduction='mean', + use_max=False, + **kwargs): + super(GMMPrior, self).__init__() + + logger.debug('Loading GMMPrior from {}', path) + if dtype == torch.float32: + np_dtype = np.float32 + elif dtype == torch.float64: + np_dtype = np.float64 + else: + raise ValueError( + 'Unknown float type {}.format(exiting)!'.format(dtype)) + + self.num_gaussians = num_gaussians + self.epsilon = epsilon + self.reduction = get_reduction_method(reduction) + self.use_max = use_max + self.dtype = dtype + + path = osp.expanduser(osp.expandvars(path)) + with open(path, 'rb') as f: + gmm = pickle.load(f, encoding='latin1') + + if type(gmm) == dict: + means = gmm['means'] + covs = gmm['covars'] + weights = gmm['weights'] + elif 'sklearn.mixture.gmm.GMM' in str(type(gmm)): + means = gmm.means_ + covs = gmm.covars_ + weights = gmm.weights_ + else: + msg = 'Unknown type for the prior: {}, exiting!'.format(type(gmm)) + raise ValueError(msg) + + self.register_buffer('means', torch.tensor(means, dtype=dtype)) + self.register_buffer('covs', torch.tensor(covs, dtype=dtype)) + + precisions = [np.linalg.inv(cov) for cov in covs] + precisions = np.stack(precisions) + + self.register_buffer('precisions', + torch.tensor(precisions, dtype=dtype)) + + nll_weights = np.asarray(gmm['weights']) + nll_weights = torch.tensor(nll_weights, dtype=dtype).unsqueeze(dim=0) + + nll_weights = torch.log(nll_weights) + self.register_buffer('nll_weights', nll_weights) + + weights = torch.tensor(gmm['weights'], dtype=dtype).unsqueeze(dim=0) + self.register_buffer('weights', weights) + + self.register_buffer('pi_term', + torch.log(torch.tensor(2 * np.pi, dtype=dtype))) + + cov_dets = [np.log(np.linalg.det(covs[idx])) + for idx in range(covs.shape[0])] + + self.register_buffer('cov_dets', + torch.tensor(cov_dets, dtype=dtype)) + + # The dimensionality of the random variable + self.random_var_dim = self.means.shape[1] + + def extra_repr(self): + msg = [] + msg.append(f'Mean: {self.means.shape}') + msg.append(f'Covariance: {self.covs.shape}') + return '\n'.join(msg) + + def get_mean(self): + ''' Returns the mean of the mixture ''' + mean_pose = torch.matmul(self.weights, self.means) + return mean_pose + + def max_log_likelihood(self, pose, *args): + diff_from_mean = pose.unsqueeze(dim=1) - self.means + + prec_diff_prod = torch.einsum('mij,bmj->bmi', + [self.precisions, diff_from_mean]) + diff_prec_quadratic = (prec_diff_prod * diff_from_mean).sum(dim=-1) + + curr_loglikelihood = -0.5 * (diff_prec_quadratic + + self.cov_dets + + self.random_var_dim * self.pi_term) + curr_loglikelihood += (-self.nll_weights) + # curr_loglikelihood = 0.5 * diff_prec_quadratic - \ + # torch.log(self.nll_weights) + + min_likelihood, _ = torch.min(curr_loglikelihood, dim=1) + return self.reduction(min_likelihood) + + def logsumexp_likelihood(self, pose, *args, **kwargs): + diff_from_mean = pose.unsqueeze(dim=1) - self.means + + prec_diff_prod = torch.einsum('mij,bmj->bmi', + [self.precisions, diff_from_mean]) + diff_prec_quadratic = (prec_diff_prod * diff_from_mean).sum(dim=-1) + + exponent = (self.nll_weights - + 0.5 * self.random_var_dim * self.pi_term - + 0.5 * self.cov_dets - + 0.5 * diff_prec_quadratic) + logsumexp = -torch.logsumexp(exponent, dim=-1) + + return self.reduction(logsumexp) + + def forward(self, pose, *args): + if len(pose.shape) == 4: + raise NotImplementedError + + if self.use_max: + return self.max_log_likelihood(pose, *args) + else: + return self.logsumexp_likelihood(pose, *args) + +=== File: expose/losses/losses.py === + +-- Chunk 1 -- +// losses.py:49-54 + GMof(residual, rho=1): + squared_res = residual ** 2 + dist = torch.div(squared_res, squared_res + rho ** 2) + return rho ** 2 * dist + + + +-- Chunk 2 -- +// losses.py:55-86 + build_loss(type='l2', rho=100, reduction='mean', size_average=True, + ignore_index=-100, + **kwargs) -> nn.Module: + logger.debug(f'Building loss: {type}') + if type == 'gmof': + return GMofLoss(rho=rho, reduction=reduction, **kwargs) + elif type == 'keypoints': + return KeypointLoss(reduction=reduction, **kwargs) + elif type == 'l2': + return WeightedMSELoss(reduction=reduction, **kwargs) + elif type == 'weighted-l1': + return WeightedL1Loss( + reduction=reduction, size_average=size_average, **kwargs) + elif type == 'keypoint-edge': + return KeypointEdgeLoss(reduction=reduction, **kwargs) + elif type == 'vertex-edge': + return VertexEdgeLoss(reduction=reduction, **kwargs) + elif type == 'bce': + return nn.BCELoss() + elif type == 'bce-logits': + return nn.BCEWithLogitsLoss() + elif type == 'cross-entropy': + return nn.CrossEntropyLoss( + reduction=reduction, ignore_index=ignore_index) + elif type == 'l1': + return nn.L1Loss() + elif type == 'rotation': + return RotationDistance(reduction=reduction, **kwargs) + else: + raise ValueError(f'Unknown loss type: {type}') + + + +-- Chunk 3 -- +// losses.py:87-106 +ss SmoothL1LossModule(nn.Module): + def __init__(self, size_average=True, beta=1. / 9): + super(SmoothL1LossModule, self).__init__() + self.size_average = size_average + self.beta = beta + + def extra_repr(self): + return 'beta={}, size_average={}'.format(self.beta, + self.size_average) + + def forward(self, input, target): + n = torch.abs(input - target) + cond = n < self.beta + loss = torch.where(cond, 0.5 * n ** 2 / self.beta, + n - 0.5 * self.beta) + if self.size_average: + return loss.mean() + return loss.sum() + + + +-- Chunk 4 -- +// losses.py:107-146 +ss KeypointLoss(nn.Module): + def __init__(self, norm_type='l1', binarize=True, + robustifier=None, epsilon=1e-6, + **kwargs): + super(KeypointLoss, self).__init__() + self.norm_type = norm_type + assert self.norm_type in ['l1', 'l2'], 'Keypoint loss must be L1, L2' + self.binarize = binarize + self.robustifier = build_robustifier( + robustifier_type=robustifier, **kwargs) + self.epsilon = epsilon + + def extra_repr(self): + return 'Norm type: {}'.format(self.norm_type.title()) + + def forward(self, input, target, weights=None, epsilon=1e-9): + assert weights is not None + keyp_dim = input.shape[-1] + + if self.binarize: + weights = weights.gt(0).to(dtype=input.dtype) + + raw_diff = input - target + # Should be B + # Should contain the number of visible keypoints per batch item + # visibility = (weights.sum(dim=-1) * keyp_dim).view(-1, 1, 1) + + if self.robustifier is not None: + diff = self.robustifier(raw_diff) + else: + if self.norm_type == 'l1': + diff = raw_diff.abs() + elif self.norm_type == 'l2': + diff = raw_diff.pow(2) + weighted_diff = diff * weights.unsqueeze(dim=-1) + + return torch.sum(weighted_diff) / weighted_diff.shape[0] + # return torch.sum(weighted_diff) / (torch.sum(visibility) + epsilon) + + + +-- Chunk 5 -- +// losses.py:147-162 +ss WeightedL1Loss(nn.Module): + def __init__(self, reduction='mean', **kwargs): + super(WeightedL1Loss, self).__init__() + self.reduce_str = reduction + self.reduce = get_reduction_method(reduction) + + def forward(self, input, target, weights=None): + diff = input - target + if weights is None: + return diff.abs().sum() / diff.shape[0] + else: + diff = input - target + weighted_diff = weights.unsqueeze(dim=-1) * diff.abs() + return weighted_diff.sum() / diff.shape[0] + + + +-- Chunk 6 -- +// losses.py:163-177 +ss WeightedMSELoss(nn.Module): + def __init__(self, reduction='mean', **kwargs): + super(WeightedMSELoss, self).__init__() + self.reduce_str = reduction + self.reduce = get_reduction_method(reduction) + + def forward(self, input, target, weights=None): + diff = input - target + if weights is None: + return diff.pow(2).sum() / diff.shape[0] + else: + return ( + weights.unsqueeze(dim=-1) * diff.pow(2)).sum() / diff.shape[0] + + + +-- Chunk 7 -- +// losses.py:178-200 +ss GMofLoss(nn.Module): + + def __init__(self, rho=100, reduction='mean', **kwargs): + super(GMofLoss, self).__init__() + self.rho = rho + self.reduction = get_reduction_method(reduction) + self.reduction_str = reduction + + def extra_repr(self): + return 'rho={}, reduction={}'.format(self.rho, + self.reduction_str) + + def forward(self, module_input, target, weights=None): + batch_size = module_input.shape[0] + squared_residual = (module_input - target).pow(2) + dist = torch.div(squared_residual, squared_residual + self.rho ** 2) + output = self.rho ** 2 * dist + if weights is not None: + output *= weights.view(batch_size, -1, 1).pow(2) + + return self.reduction(output) + + + +-- Chunk 8 -- +// losses.py:201-238 +ss RotationDistance(nn.Module): + def __init__(self, reduction='mean', epsilon=1e-7, + robustifier='none', + **kwargs): + super(RotationDistance, self).__init__() + self.reduction = get_reduction_method(reduction) + self.reduction_str = reduction + self.epsilon = epsilon + self.robustifier = build_robustifier( + robustifier_type=robustifier, epsilon=epsilon, **kwargs) + + def extra_repr(self) -> str: + msg = [] + msg.append(f'Reduction: {self.reduction_str}') + msg.append(f'Epsilon: {self.epsilon}') + return '\n'.join(msg) + + def forward(self, module_input, target, weights=None): + tr = torch.einsum( + 'bij,bij->b', + [module_input.view(-1, 3, 3), + target.view(-1, 3, 3)]) + + theta = (tr - 1) * 0.5 + loss = torch.acos( + torch.clamp(theta, -1 + self.epsilon, 1 - self.epsilon)) + if self.robustifier is not None: + loss = self.robustifier(loss) + if weights is not None: + loss = loss.view( + module_input.shape[0], -1) * weights.view( + module_input.shape[0], -1) + return loss.sum() / ( + weights.gt(0).to(loss.dtype).sum() + self.epsilon) + else: + return loss.sum() / module_input.shape[0] + + + +-- Chunk 9 -- +// losses.py:239-310 +ss VertexEdgeLoss(nn.Module): + def __init__(self, norm_type='l2', + gt_edge_path='', + est_edge_path='', + robustifier=None, + edge_thresh=0.0, epsilon=1e-8, **kwargs): + super(VertexEdgeLoss, self).__init__() + + assert norm_type in ['l1', 'l2'], 'Norm type must be [l1, l2]' + self.norm_type = norm_type + self.epsilon = epsilon + self.robustifier = build_robustifier( + robustifier_type=robustifier, **kwargs) + + gt_edge_path = osp.expandvars(gt_edge_path) + est_edge_path = osp.expandvars(est_edge_path) + self.has_connections = osp.exists(gt_edge_path) and osp.exists( + est_edge_path) + if self.has_connections: + gt_edges = np.load(gt_edge_path) + est_edges = np.load(est_edge_path) + + self.register_buffer( + 'gt_connections', torch.tensor(gt_edges, dtype=torch.long)) + self.register_buffer( + 'est_connections', torch.tensor(est_edges, dtype=torch.long)) + + def extra_repr(self): + msg = [ + f'Norm type: {self.norm_type}', + ] + if self.has_connections: + msg.append( + f'GT Connections shape: {self.gt_connections.shape}' + ) + msg.append( + f'Est Connections shape: {self.est_connections.shape}' + ) + return '\n'.join(msg) + + def compute_edges(self, points, connections): + start = torch.index_select( + points, 1, connections[:, 0]) + end = torch.index_select(points, 1, connections[:, 1]) + return start - end + + def forward(self, gt_vertices, est_vertices, weights=None): + if not self.has_connections: + return 0.0 + + # Compute the edges for the ground truth keypoints and the model keypoints + # Remove the confidence from the ground truth keypoints + gt_edges = self.compute_edges( + gt_vertices, connections=self.gt_connections) + est_edges = self.compute_edges( + est_vertices, connections=self.est_connections) + + raw_edge_diff = (gt_edges - est_edges) + + batch_size = gt_vertices.shape[0] + if self.robustifier is not None: + raise NotImplementedError + else: + if self.norm_type == 'l2': + return (raw_edge_diff.pow(2).sum(dim=-1)).sum() / batch_size + elif self.norm_type == 'l1': + return (raw_edge_diff.pow(2).sum(dim=-1)).sum() / batch_size + else: + raise NotImplementedError( + f'Loss type not implemented: {self.loss_type}') + + + +-- Chunk 10 -- +// losses.py:311-379 +ss KeypointEdgeLoss(nn.Module): + def __init__(self, norm_type='l2', connections=None, + robustifier=None, + edge_thresh=0.0, epsilon=1e-8, **kwargs): + super(KeypointEdgeLoss, self).__init__() + if connections is not None: + connections = torch.tensor(connections).reshape(-1, 2) + self.register_buffer('connections', connections) + else: + self.connections = None + self.edge_thresh = edge_thresh + + assert norm_type in ['l1', 'l2'], 'Norm type must be [l1, l2]' + self.norm_type = norm_type + self.epsilon = epsilon + self.robustifier = build_robustifier( + robustifier_type=robustifier, **kwargs) + + def extra_repr(self): + msg = [ + f'Edge threshold: {self.edge_thresh}', + f'Norm type: {self.norm_type}', + f'Connections shape: {self.connections.shape}' + ] + return '\n'.join(msg) + + def compute_edges(self, keypoints): + start = torch.index_select( + keypoints, 1, self.connections[:, 0]) + end = torch.index_select(keypoints, 1, self.connections[:, 1]) + return start - end + + def forward(self, gt_keypoints, model_keypoints, weights=None): + if self.connections is None or len(self.connections) < 1: + return 0.0 + + # Compute the edges for the ground truth keypoints and the model keypoints + # Remove the confidence from the ground truth keypoints + gt_edges = self.compute_edges(gt_keypoints) + model_edges = self.compute_edges(model_keypoints) + + # Compute the confidence of the edge as the harmonic mean of the + # confidences + # Weights: BxC + if weights is not None: + weight_start_pt = torch.index_select( + weights, 1, self.connections[:, 0]) + weight_end_pt = torch.index_select( + weights, 1, self.connections[:, 1]) + edge_weight = 2.0 * weight_start_pt * weight_end_pt / ( + weight_start_pt + weight_end_pt + self.epsilon) + edge_weight[torch.isnan(edge_weight)] = 0 + else: + edge_weight = torch.ones_like(gt_edges[:, :, 0]) + + # num_visible = edge_weight.gt( + # self.edge_thresh).to(dtype=gt_edges.dtype).sum() + + raw_edge_diff = (gt_edges - model_edges) + + if self.robustifier is not None: + raise NotImplementedError + else: + if self.norm_type == 'l2': + return (raw_edge_diff.pow(2).sum(dim=-1) * + edge_weight).sum() / gt_keypoints.shape[0] + else: + raise NotImplementedError( + f'Loss type not implemented: {self.loss_type}') + +=== File: expose/losses/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/losses/__init__.py:1-18 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from .priors import * +from .losses import * + +=== File: expose/losses/utils.py === + +-- Chunk 1 -- +// utils.py:21-29 + get_reduction_method(reduction='mean'): + if reduction == 'mean': + return torch.mean + elif reduction == 'sum': + return torch.sum + elif reduction == 'none': + return lambda x: x + else: + raise ValueError('Unknown reduction method: {}'.format(reduction)) + +=== File: expose/optimizers/build.py === + +-- Chunk 1 -- +// build.py:26-51 + build_optimizer( + model: nn.Module, + optim_cfg: Dict, + exclude: str = '', +) -> optim.Optimizer: + params = [] + + for key, value in model.named_parameters(): + if not value.requires_grad: + continue + lr = optim_cfg.lr + weight_decay = optim_cfg.weight_decay + if "bias" in key: + lr = optim_cfg.lr * optim_cfg.bias_lr_factor + weight_decay = optim_cfg.weight_decay_bias + + if len(exclude) > 0 and exclude in key: + continue + params += [{"params": [value], "lr": lr, "weight_decay": weight_decay}] + + lr = optim_cfg.lr + + optimizer = get_optimizer(params, optim_cfg) + return optimizer + + + +-- Chunk 2 -- +// build.py:52-69 + get_optimizer(params, optim_cfg): + lr = optim_cfg.lr + optimizer_type = optim_cfg.type + logger.debug('Building optimizer: {}', optimizer_type.upper()) + if optimizer_type == 'sgd': + optimizer = optim.SGD(params, lr, + **optim_cfg.sgd) + elif optimizer_type == 'adam': + optimizer = optim.Adam(params, lr, **optim_cfg.adam) + elif optimizer_type == 'rmsprop': + optimizer = optim.RMSprop(params, lr, **optim_cfg.rmsprop) + elif optimizer_type == 'lbfgs': + optimizer = optim.LBFGS(params, **optim_cfg.get('lbfgs', {})) + else: + raise ValueError(f'Unknown optimizer type: {optimizer_type}') + return optimizer + + + +-- Chunk 3 -- +// build.py:70-91 + build_scheduler( + optimizer: optim.Optimizer, + sched_cfg: Dict +) -> optim.lr_scheduler._LRScheduler: + scheduler_type = sched_cfg.type + if scheduler_type == 'none': + return None + elif scheduler_type == 'step-lr': + step_size = sched_cfg.step_size + gamma = sched_cfg.gamma + logger.info('Building scheduler: StepLR(step_size={}, gamma={})', + step_size, gamma) + return scheduler.StepLR(optimizer, step_size, gamma) + elif scheduler_type == 'multi-step-lr': + gamma = sched_cfg.gamma + milestones = sched_cfg.milestones + logger.info('Building scheduler: MultiStepLR(milestone={}, gamma={})', + milestones, gamma) + return scheduler.MultiStepLR( + optimizer, milestones=milestones, gamma=gamma) + else: + raise ValueError(f'Unknown scheduler type: {scheduler_type}') + +=== File: expose/optimizers/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/optimizers/__init__.py:1-19 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from .build import build_optimizer +from .build import build_scheduler +from .build import get_optimizer + +=== File: expose/config/loss_defaults.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/loss_defaults.py:1-150 +from copy import deepcopy +# from yacs.config import CfgNode as CN +from fvcore.common.config import CfgNode as CN + +_C = CN() + + +_C.stages_to_penalize = [-1] +_C.stages_to_regularize = [-1] + +_C.body_joints_2d = CN() +_C.body_joints_2d.type = 'keypoints' +_C.body_joints_2d.robustifier = 'none' +_C.body_joints_2d.norm_type = 'l1' +_C.body_joints_2d.rho = 100.0 +_C.body_joints_2d.beta = 5.0 / 100 * 2 +_C.body_joints_2d.size_average = True +_C.body_joints_2d.weight = 1.0 +_C.body_joints_2d.enable = 0 + +_C.hand_joints_2d = CN() +_C.hand_joints_2d.type = 'keypoints' +_C.hand_joints_2d.norm_type = 'l1' +_C.hand_joints_2d.robustifier = 'none' +_C.hand_joints_2d.rho = 100.0 +_C.hand_joints_2d.beta = 5.0 / 100 * 2 +_C.hand_joints_2d.size_average = True +_C.hand_joints_2d.weight = 1.0 +_C.hand_joints_2d.enable = 0 + +_C.face_joints_2d = CN() +_C.face_joints_2d.type = 'keypoints' +_C.face_joints_2d.norm_type = 'l1' +_C.face_joints_2d.robustifier = 'none' +_C.face_joints_2d.rho = 100.0 +_C.face_joints_2d.beta = 5.0 / 100 * 2 +_C.face_joints_2d.size_average = True +_C.face_joints_2d.weight = 1.0 +_C.face_joints_2d.enable = 0 + + +_C.head_crop_keypoints = CN() +_C.head_crop_keypoints.type = 'keypoints' +_C.head_crop_keypoints.norm_type = 'l1' +_C.head_crop_keypoints.robustifier = 'none' +_C.head_crop_keypoints.rho = 100.0 +_C.head_crop_keypoints.beta = 5.0 / 100 * 2 +_C.head_crop_keypoints.size_average = True +_C.head_crop_keypoints.weight = 0.0 +_C.head_crop_keypoints.enable = 0 + +_C.left_hand_crop_keypoints = CN() +_C.left_hand_crop_keypoints.type = 'keypoints' +_C.left_hand_crop_keypoints.norm_type = 'l1' +_C.left_hand_crop_keypoints.robustifier = 'none' +_C.left_hand_crop_keypoints.rho = 100.0 +_C.left_hand_crop_keypoints.beta = 5.0 / 100 * 2 +_C.left_hand_crop_keypoints.size_average = True +_C.left_hand_crop_keypoints.weight = 0.0 +_C.left_hand_crop_keypoints.enable = 0 + +_C.right_hand_crop_keypoints = CN() +_C.right_hand_crop_keypoints.type = 'keypoints' +_C.right_hand_crop_keypoints.norm_type = 'l1' +_C.right_hand_crop_keypoints.robustifier = 'none' +_C.right_hand_crop_keypoints.rho = 100.0 +_C.right_hand_crop_keypoints.beta = 5.0 / 100 * 2 +_C.right_hand_crop_keypoints.size_average = True +_C.right_hand_crop_keypoints.weight = 0.0 +_C.right_hand_crop_keypoints.enable = 0 + +_C.body_edge_2d = CN() +_C.body_edge_2d.norm_type = 'l2' +_C.body_edge_2d.rho = 100.0 +_C.body_edge_2d.beta = 5.0 / 100 * 2 +_C.body_edge_2d.size_average = True +_C.body_edge_2d.weight = 0.0 +_C.body_edge_2d.enable = 0 +_C.body_edge_2d.robustifier = 'none' +_C.body_edge_2d.scale = 1.0 +_C.body_edge_2d.threshold = 1.0 + + +_C.hand_edge_2d = CN() +_C.hand_edge_2d.norm_type = 'l2' +_C.hand_edge_2d.rho = 100.0 +_C.hand_edge_2d.beta = 5.0 / 100 * 2 +_C.hand_edge_2d.size_average = True +_C.hand_edge_2d.weight = 0.0 +_C.hand_edge_2d.enable = 0 +_C.hand_edge_2d.robustifier = 'none' +_C.hand_edge_2d.scale = 1.0 +_C.hand_edge_2d.threshold = 1.0 + + +_C.face_edge_2d = CN() +_C.face_edge_2d.norm_type = 'l2' +_C.face_edge_2d.rho = 100.0 +_C.face_edge_2d.beta = 5.0 / 100 * 2 +_C.face_edge_2d.size_average = True +_C.face_edge_2d.weight = 0.0 +_C.face_edge_2d.enable = 0 +_C.face_edge_2d.robustifier = 'none' +_C.face_edge_2d.scale = 1.0 +_C.face_edge_2d.threshold = 1.0 + +_C.body_joints_3d = CN() +_C.body_joints_3d.type = 'keypoints' +_C.body_joints_3d.norm_type = 'l1' +_C.body_joints_3d.rho = 100.0 +_C.body_joints_3d.beta = 5.0 / 100 * 2 +_C.body_joints_3d.size_average = True +_C.body_joints_3d.weight = 0.0 +_C.body_joints_3d.enable = 0 + + +_C.hand_joints_3d = CN() +_C.hand_joints_3d.type = 'keypoints' +_C.hand_joints_3d.norm_type = 'l1' +_C.hand_joints_3d.rho = 100.0 +_C.hand_joints_3d.beta = 5.0 / 100 * 2 +_C.hand_joints_3d.size_average = True +_C.hand_joints_3d.weight = 0.0 +_C.hand_joints_3d.enable = 500 * 1000 + + +_C.face_joints_3d = CN() +_C.face_joints_3d.type = 'keypoints' +_C.face_joints_3d.norm_type = 'l1' +_C.face_joints_3d.rho = 100.0 +_C.face_joints_3d.beta = 5.0 / 100 * 2 +_C.face_joints_3d.size_average = True +_C.face_joints_3d.weight = 0.0 +_C.face_joints_3d.enable = 500 * 1000 + + +_C.shape = CN() +_C.shape.type = 'l2' +_C.shape.weight = 1.0 +_C.shape.enable = 0 +_C.shape.prior = CN() +_C.shape.prior.type = 'l2' +_C.shape.prior.weight = 0.0 +_C.shape.prior.margin = 1.0 +_C.shape.prior.norm = 'l2' +_C.shape.prior.use_vector = True +_C.shape.prior.barrier = 'log' +_C.shape.prior.epsilon = 1e-7 + +_C.expression = CN() + +-- Chunk 2 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/loss_defaults.py:151-300 +_C.expression.type = 'l2' +_C.expression.weight = 1.0 +_C.expression.enable = 0 +_C.expression.use_conf_weight = False +_C.expression.prior = CN() +_C.expression.prior.type = 'l2' +_C.expression.prior.weight = 0.0 +_C.expression.prior.margin = 1.0 +_C.expression.prior.use_vector = True +_C.expression.prior.norm = 'l2' +_C.expression.prior.barrier = 'log' +_C.expression.prior.epsilon = 1e-7 + +_C.global_orient = CN() +_C.global_orient.type = 'rotation' +_C.global_orient.enable = 0 +_C.global_orient.weight = 1.0 +_C.global_orient.prior = CN() + +_C.body_pose = CN() +_C.body_pose.type = 'rotation' +_C.body_pose.enable = 0 +_C.body_pose.weight = 1.0 +_C.body_pose.prior = CN() +_C.body_pose.prior.type = 'l2' +_C.body_pose.prior.use_max = False +_C.body_pose.prior.weight = 0.0 +_C.body_pose.prior.path = 'data/priors/gmm_08.pkl' +_C.body_pose.prior.num_gaussians = 8 + +_C.left_hand_pose = CN() +_C.left_hand_pose.use_conf_weight = False +_C.left_hand_pose.type = 'rotation' +_C.left_hand_pose.enable = 0 +_C.left_hand_pose.weight = 1.0 +_C.left_hand_pose.prior = CN() +_C.left_hand_pose.prior.type = 'l2' +_C.left_hand_pose.prior.weight = 0.0 +_C.left_hand_pose.prior.num_gaussians = 6 +_C.left_hand_pose.prior.path = 'data/priors/gmm_left_06.pkl' + +_C.right_hand_pose = CN() +_C.right_hand_pose.use_conf_weight = False +_C.right_hand_pose.type = 'rotation' +_C.right_hand_pose.enable = 0 +_C.right_hand_pose.weight = 1.0 +_C.right_hand_pose.prior = CN() +_C.right_hand_pose.prior.type = 'l2' +_C.right_hand_pose.prior.weight = 0.0 +_C.right_hand_pose.prior.num_gaussians = 6 +_C.right_hand_pose.prior.path = 'data/priors/gmm_right_06.pkl' + +_C.jaw_pose = CN() +_C.jaw_pose.type = 'rotation' +_C.jaw_pose.use_conf_weight = False +_C.jaw_pose.enable = 0 +_C.jaw_pose.weight = 1.0 +_C.jaw_pose.prior = CN() +_C.jaw_pose.prior.type = 'l2' +_C.jaw_pose.prior.weight = 0.0 +_C.jaw_pose.prior.reduction = 'mean' + +_C.edge = CN() +_C.edge.weight = 0.0 +_C.edge.type = 'vertex-edge' +_C.edge.norm_type = 'l2' +_C.edge.gt_edge_path = '' +_C.edge.est_edge_path = '' +_C.edge.rho = 100.0 +_C.edge.size_average = True +_C.edge.enable = 0 + +_C.hand = CN() + +_C.hand.joints_2d = CN() +_C.hand.joints_2d.weight = 1.0 +_C.hand.joints_2d.type = 'keypoints' +_C.hand.joints_2d.norm_type = 'l1' +_C.hand.joints_2d.robustifier = 'none' +_C.hand.joints_2d.rho = 100.0 +_C.hand.joints_2d.beta = 5.0 / 100 * 2 +_C.hand.joints_2d.size_average = True +_C.hand.joints_2d.enable = 0 + +_C.hand.vertices = CN() +_C.hand.vertices.weight = 0.0 +_C.hand.vertices.type = 'weighted-l1' +_C.hand.vertices.rho = 100.0 +_C.hand.vertices.beta = 5.0 / 100 * 2 +_C.hand.vertices.size_average = True +_C.hand.vertices.enable = 0 + +_C.hand.edge = CN() +_C.hand.edge.weight = 0.0 +_C.hand.edge.type = 'vertex-edge' +_C.hand.edge.norm_type = 'l2' +_C.hand.edge.gt_edge_path = '' +_C.hand.edge.est_edge_path = '' +_C.hand.edge.rho = 100.0 +_C.hand.edge.size_average = True +_C.hand.edge.enable = 0 + +_C.hand.hand_edge_2d = CN() +_C.hand.hand_edge_2d.weight = 0.0 +_C.hand.hand_edge_2d.norm_type = 'l2' +_C.hand.hand_edge_2d.rho = 100.0 +_C.hand.hand_edge_2d.beta = 5.0 / 100 * 2 +_C.hand.hand_edge_2d.size_average = True +_C.hand.hand_edge_2d.enable = 0 +_C.hand.hand_edge_2d.robustifier = 'none' +_C.hand.hand_edge_2d.scale = 1.0 +_C.hand.hand_edge_2d.threshold = 1.0 + + +_C.hand.joints_3d = CN() +_C.hand.joints_3d.weight = 0.0 +_C.hand.joints_3d.type = 'keypoints' +_C.hand.joints_3d.norm_type = 'l1' +_C.hand.joints_3d.rho = 100.0 +_C.hand.joints_3d.beta = 5.0 / 100 * 2 +_C.hand.joints_3d.size_average = True +_C.hand.joints_3d.enable = 500 * 1000 + + +_C.hand.shape = CN() +_C.hand.shape.type = 'l2' +_C.hand.shape.weight = 0.0 +_C.hand.shape.enable = 0 +_C.hand.shape.prior = CN() +_C.hand.shape.prior.weight = 0.0 +_C.hand.shape.prior.type = 'l2' +_C.hand.shape.prior.margin = 1.0 +_C.hand.shape.prior.norm = 'l2' +_C.hand.shape.prior.use_vector = True +_C.hand.shape.prior.barrier = 'log' +_C.hand.shape.prior.epsilon = 1e-7 + +_C.hand.global_orient = CN() +_C.hand.global_orient.type = 'rotation' +_C.hand.global_orient.enable = 0 +_C.hand.global_orient.weight = 1.0 +_C.hand.global_orient.prior = CN() + +_C.hand.hand_pose = CN() +_C.hand.hand_pose.use_conf_weight = False +_C.hand.hand_pose.type = 'rotation' +_C.hand.hand_pose.enable = 0 +_C.hand.hand_pose.weight = 1.0 +_C.hand.hand_pose.prior = CN() +_C.hand.hand_pose.prior.type = 'l2' + +-- Chunk 3 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/loss_defaults.py:301-397 +_C.hand.hand_pose.prior.weight = 0.0 +_C.hand.hand_pose.prior.num_gaussians = 6 +_C.hand.hand_pose.prior.margin = 1.0 +_C.hand.hand_pose.prior.path = 'data/priors/gmm_left_06.pkl' + +# Losses +_C.head = CN() + +_C.head.joints_2d = CN() +_C.head.joints_2d.type = 'keypoints' +_C.head.joints_2d.norm_type = 'l1' +_C.head.joints_2d.robustifier = 'none' +_C.head.joints_2d.rho = 100.0 +_C.head.joints_2d.beta = 5.0 / 100 * 2 +_C.head.joints_2d.size_average = True +_C.head.joints_2d.weight = 0.0 +_C.head.joints_2d.enable = 0.0 + +_C.head.edge_2d = CN() +_C.head.edge_2d.weight = 0.0 +_C.head.edge_2d.norm_type = 'l2' +_C.head.edge_2d.rho = 100.0 +_C.head.edge_2d.beta = 5.0 / 100 * 2 +_C.head.edge_2d.size_average = True +_C.head.edge_2d.enable = 0 +_C.head.edge_2d.robustifier = 'none' +_C.head.edge_2d.scale = 0.0 +_C.head.edge_2d.threshold = 1.0 + +_C.head.vertices = CN() +_C.head.vertices.weight = 0.0 +_C.head.vertices.type = 'weighted-l1' +_C.head.vertices.rho = 100.0 +_C.head.vertices.beta = 5.0 / 100 * 2 +_C.head.vertices.size_average = True +_C.head.vertices.enable = 0 + +_C.head.edge = CN() +_C.head.edge.weight = 0.0 +_C.head.edge.type = 'vertex-edge' +_C.head.edge.norm_type = 'l2' +_C.head.edge.gt_edge_path = '' +_C.head.edge.est_edge_path = '' +_C.head.edge.rho = 100.0 +_C.head.edge.size_average = True +_C.head.edge.enable = 0 + +_C.head.joints_3d = CN() +_C.head.joints_3d.weight = 0.0 +_C.head.joints_3d.type = 'keypoints' +_C.head.joints_3d.norm_type = 'l1' +_C.head.joints_3d.rho = 100.0 +_C.head.joints_3d.beta = 5.0 / 100 * 2 +_C.head.joints_3d.size_average = True +_C.head.joints_3d.enable = 0.0 + +_C.head.shape = CN() +_C.head.shape.type = 'l2' +_C.head.shape.weight = 1.0 +_C.head.shape.enable = 0 +_C.head.shape.prior = CN() +_C.head.shape.prior.type = 'l2' +_C.head.shape.prior.weight = 0.0 +_C.head.shape.prior.margin = 1.0 +_C.head.shape.prior.norm = 'l2' +_C.head.shape.prior.use_vector = True +_C.head.shape.prior.barrier = 'log' +_C.head.shape.prior.epsilon = 1e-7 + +_C.head.expression = CN() +_C.head.expression.type = 'l2' +_C.head.expression.weight = 1.0 +_C.head.expression.enable = 0 +_C.head.expression.use_conf_weight = False +_C.head.expression.prior = CN() +_C.head.expression.prior.type = 'l2' +_C.head.expression.prior.weight = 0.0 +_C.head.expression.prior.margin = 1.0 +_C.head.expression.prior.use_vector = True +_C.head.expression.prior.norm = 'l2' +_C.head.expression.prior.barrier = 'log' +_C.head.expression.prior.epsilon = 1e-7 + +_C.head.global_orient = CN() +_C.head.global_orient.type = 'rotation' +_C.head.global_orient.enable = 0 +_C.head.global_orient.weight = 1.0 +_C.head.global_orient.prior = CN() + +_C.head.jaw_pose = CN() +_C.head.jaw_pose.type = 'rotation' +_C.head.jaw_pose.use_conf_weight = False +_C.head.jaw_pose.enable = 0 +_C.head.jaw_pose.weight = 1.0 +_C.head.jaw_pose.prior = CN() +_C.head.jaw_pose.prior.type = 'l2' +_C.head.jaw_pose.prior.weight = 0.0 + +=== File: expose/config/defaults.py === + +-- Chunk 1 -- +// defaults.py:12-28 +def create_camera_config(node): + node.camera = CN() + node.camera.type = 'weak-persp' + node.camera.pos_func = 'softplus' + + node.camera.weak_persp = CN() + node.camera.weak_persp.regress_translation = True + node.camera.weak_persp.regress_scale = True + node.camera.weak_persp.regress_scale = True + node.camera.weak_persp.mean_scale = 0.9 + + node.camera.perspective = CN() + node.camera.perspective.regress_translation = False + node.camera.perspective.regress_rotation = False + node.camera.perspective.regress_focal_length = False + node.camera.perspective.focal_length = 5000 + return node.camera + +-- Chunk 2 -- +// defaults.py:31-45 +def create_mlp_config(node, key='mlp'): + if key not in node: + node[key] = CN() + + node[key].layers = (1024, 1024) + node[key].activ_type = 'relu' + node[key].lrelu_slope = 0.2 + node[key].norm_type = 'none' + node[key].num_groups = 32 + node[key].dropout = 0.0 + node[key].init_type = 'xavier' + node[key].gain = 0.01 + node[key].bias_init = 0.0 + + return node[key] + +-- Chunk 3 -- +// defaults.py:48-55 +def create_conv_layers(node, key='layer'): + if key not in node: + node[key] = CN() + + node[key].num_layers = 5 + node[key].num_filters = 2048 + node[key].stride = 1 + return node[key] + +-- Chunk 4 -- +// defaults.py:58-70 +def create_subsample_layer(node, num_layers=3, key='layer', + kernel_size=3, stride=2): + if key not in node: + node[key] = CN() + + node[key].num_filters = (512,) * num_layers + node[key].norm_type = 'bn' + node[key].activ_type = 'relu' + node[key].dim = 2 + node[key].kernel_sizes = [kernel_size] * len(node[key].num_filters) + node[key].strides = [stride] * len(node[key].num_filters) + node[key].padding = 1 + return node[key] + +-- Chunk 5 -- +// defaults.py:73-145 +def create_backbone_cfg(node, backbone_type='resnet50'): + if 'backbone' not in node: + node.backbone = CN() + node.backbone.type = backbone_type + node.backbone.pretrained = True + + node.backbone.resnet = CN() + node.backbone.resnet.replace_stride_with_dilation = (False, False, False) + + node.backbone.fpn = CN() + node.backbone.fpn.pooling_type = 'concat' + node.backbone.fpn.concat = CN() + node.backbone.fpn.concat.use_max = True + node.backbone.fpn.concat.use_avg = True + + node.backbone.hrnet = CN() + node.backbone.hrnet.pretrained_layers = ['*'] + node.backbone.hrnet.pretrained_path = ( + 'data/' + 'network_weights/hrnet/' + 'imagenet/hrnet_w48-8ef0771d.pth' + ) + + node.backbone.hrnet.stage1 = CN() + node.backbone.hrnet.stage1.num_modules = 1 + node.backbone.hrnet.stage1.num_branches = 1 + node.backbone.hrnet.stage1.num_blocks = [4] + node.backbone.hrnet.stage1.num_channels = [64] + node.backbone.hrnet.stage1.block = 'BOTTLENECK' + node.backbone.hrnet.stage1.fuse_method = 'SUM' + + node.backbone.hrnet.stage2 = CN() + node.backbone.hrnet.stage2.num_modules = 1 + node.backbone.hrnet.stage2.num_branches = 2 + node.backbone.hrnet.stage2.num_blocks = [4, 4] + node.backbone.hrnet.stage2.num_channels = [48, 96] + node.backbone.hrnet.stage2.block = 'BASIC' + node.backbone.hrnet.stage2.fuse_method = 'SUM' + + node.backbone.hrnet.stage3 = CN() + node.backbone.hrnet.stage3.num_modules = 4 + node.backbone.hrnet.stage3.num_branches = 3 + node.backbone.hrnet.stage3.num_blocks = [4, 4, 4] + node.backbone.hrnet.stage3.num_channels = [48, 96, 192] + node.backbone.hrnet.stage3.block = 'BASIC' + node.backbone.hrnet.stage3.fuse_method = 'SUM' + + node.backbone.hrnet.stage4 = CN() + node.backbone.hrnet.stage4.num_modules = 3 + node.backbone.hrnet.stage4.num_branches = 4 + node.backbone.hrnet.stage4.num_blocks = [4, 4, 4, 4] + node.backbone.hrnet.stage4.num_channels = [48, 96, 192, 384] + node.backbone.hrnet.stage4.block = 'BASIC' + node.backbone.hrnet.stage4.fuse_method = 'SUM' + + node.backbone.hrnet.stage2.subsample = create_subsample_layer( + node.backbone.hrnet.stage2, key='subsample', num_layers=2) + node.backbone.hrnet.stage2.subsample.num_filters = [96, 192] + node.backbone.hrnet.stage2.subsample.num_filters = [384] + node.backbone.hrnet.stage2.subsample.kernel_sizes = [3] + node.backbone.hrnet.stage2.subsample.strides = [2] + + node.backbone.hrnet.stage3.subsample = create_subsample_layer( + node.backbone.hrnet.stage3, key='subsample', num_layers=1) + node.backbone.hrnet.stage3.subsample.num_filters = [192, 384] + node.backbone.hrnet.stage3.subsample.kernel_sizes = [3, 3] + node.backbone.hrnet.stage3.subsample.strides = [2, 2] + + node.backbone.hrnet.final_conv = create_conv_layers( + node.backbone.hrnet, key='final_conv') + node.backbone.hrnet.final_conv.num_filters = 2048 + + return node.backbone + +-- Chunk 6 -- +// defaults.py:345-349 +def get_cfg_defaults(): + """Get a yacs CfgNode object with default values for my_project.""" + # Return a clone so that the defaults will not be altered + # This is for the "local variable" use pattern + return _C.clone() + +=== File: expose/config/optim_defaults.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/optim_defaults.py:1-38 +from copy import deepcopy +from fvcore.common.config import CfgNode as CN + +_C = CN() + +_C = CN() +_C.type = 'sgd' +_C.num_epochs = 300 +_C.lr = 1e-4 +_C.offsets_decay = 1e-4 + +_C.steps = (30000,) + +_C.sgd = CN() +_C.sgd.momentum = 0.9 +_C.sgd.nesterov = True + +_C.scheduler = CN() +_C.scheduler.type = 'none' +_C.scheduler.gamma = 0.1 +_C.scheduler.milestones = [] +_C.scheduler.step_size = 1000 +_C.scheduler.warmup_factor = 1.0e-1 / 3 +_C.scheduler.warmup_iters = 500 +_C.scheduler.warmup_method = "linear" + +# Adam parameters +_C.adam = CN() +_C.adam.betas = [0.9, 0.999] +_C.adam.eps = 1e-08 +_C.adam.amsgrad = False + +_C.rmsprop = CN() +_C.rmsprop.alpha = 0.99 + +_C.weight_decay = 0.0 +_C.weight_decay_bias = 0.0 +_C.bias_lr_factor = 1.0 + +=== File: expose/config/cmd_parser.py === + +-- Chunk 1 -- +// cmd_parser.py:15-20 +def set_face_contour(node, use_face_contour=False): + for key in node: + if 'use_face_contour' in key: + node[key] = use_face_contour + if isinstance(node[key], CN): + set_face_contour(node[key], use_face_contour=use_face_contour) + +-- Chunk 2 -- +// cmd_parser.py:23-58 +def parse_args(argv=None): + arg_formatter = argparse.ArgumentDefaultsHelpFormatter + + description = 'PyTorch SMPL-X Regressor with Attention' + parser = argparse.ArgumentParser(formatter_class=arg_formatter, + description=description) + + parser.add_argument('--exp-cfg', type=str, dest='exp_cfg', + help='The configuration of the experiment') + parser.add_argument('--exp-opts', default=[], dest='exp_opts', + nargs='*', + help='The configuration of the Detector') + parser.add_argument('--local_rank', default=0, type=int, + help='ranking within the nodes') + parser.add_argument('--num-gpus', dest='num_gpus', + default=1, type=int, + help='Number of gpus') + parser.add_argument('--backend', dest='backend', + default='nccl', type=str, + choices=['nccl', 'gloo'], + help='Backend used for multi-gpu training') + + cmd_args = parser.parse_args() + + cfg.merge_from_file(cmd_args.exp_cfg) + cfg.merge_from_list(cmd_args.exp_opts) + + use_face_contour = cfg.datasets.use_face_contour + set_face_contour(cfg, use_face_contour=use_face_contour) + + cfg.network.use_sync_bn = (cfg.network.use_sync_bn and + cmd_args.num_gpus > 1) + cfg.local_rank = cmd_args.local_rank + cfg.num_gpus = cmd_args.num_gpus + + return cfg + +=== File: expose/config/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/__init__.py:1-2 +from .defaults import _C as cfg +from .cmd_parser import parse_args + +=== File: expose/config/datasets_defaults.py === + +-- Chunk 1 -- +// datasets_defaults.py:8-37 +def build_transform_cfg(node, key='transforms', flip_prob=0.0, + downsample_factor_min=1.0, + downsample_factor_max=1.0, + center_jitter_factor=0.0, + downsample_dist='categorical', + ): + if key not in node: + node[key] = CN() + node[key].flip_prob = flip_prob + node[key].downsample_dist = downsample_dist + node[key].downsample_factor_min = downsample_factor_min + node[key].downsample_factor_max = downsample_factor_max + node[key].downsample_cat_factors = (1.0,) + node[key].center_jitter_factor = center_jitter_factor + node[key].center_jitter_dist = 'normal' + node[key].crop_size = 256 + node[key].scale_factor_min = 1.0 + node[key].scale_factor_max = 1.0 + node[key].scale_factor = 0.0 + node[key].scale_dist = 'uniform' + node[key].noise_scale = 0.0 + node[key].rotation_factor = 0.0 + node[key].mean = [0.485, 0.456, 0.406] + node[key].std = [0.229, 0.224, 0.225] + node[key].brightness = 0.0 + node[key].saturation = 0.0 + node[key].hue = 0.0 + node[key].contrast = 0.0 + + return node[key] + +-- Chunk 2 -- +// datasets_defaults.py:40-46 +def build_num_workers_cfg(node, key='num_workers'): + if key not in node: + node[key] = CN() + node[key].train = 8 + node[key].val = 2 + node[key].test = 2 + return node[key] + +=== File: expose/config/body_model.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/config/body_model.py:1-96 +from fvcore.common.config import CfgNode as CN +# from yacs.config import CfgNode as CN + +_C = CN() + +_C.body_model = CN() + +_C.body_model.j14_regressor_path = '' +_C.body_model.mean_pose_path = '' +_C.body_model.shape_mean_path = 'data/shape_mean.npy' +_C.body_model.type = 'smplx' +_C.body_model.model_folder = 'models' +_C.body_model.use_compressed = True +_C.body_model.gender = 'neutral' +_C.body_model.num_betas = 10 +_C.body_model.num_expression_coeffs = 10 +_C.body_model.use_feet_keypoints = True +_C.body_model.use_face_keypoints = True +_C.body_model.use_face_contour = False + +_C.body_model.global_orient = CN() +# The configuration for the parameterization of the body pose +_C.body_model.global_orient.param_type = 'cont_rot_repr' + +_C.body_model.body_pose = CN() +# The configuration for the parameterization of the body pose +_C.body_model.body_pose.param_type = 'cont_rot_repr' +_C.body_model.body_pose.finetune = False + +_C.body_model.left_hand_pose = CN() +# The configuration for the parameterization of the left hand pose +_C.body_model.left_hand_pose.param_type = 'pca' +_C.body_model.left_hand_pose.num_pca_comps = 12 +_C.body_model.left_hand_pose.flat_hand_mean = False +# The type of prior on the left hand pose + +_C.body_model.right_hand_pose = CN() +# The configuration for the parameterization of the left hand pose +_C.body_model.right_hand_pose.param_type = 'pca' +_C.body_model.right_hand_pose.num_pca_comps = 12 +_C.body_model.right_hand_pose.flat_hand_mean = False + +_C.body_model.jaw_pose = CN() +_C.body_model.jaw_pose.param_type = 'cont_rot_repr' +_C.body_model.jaw_pose.data_fn = 'clusters.pkl' + +####### HAND MODEL ######## + +_C.hand_model = CN() +_C.hand_model.j14_regressor_path = '' +_C.hand_model.mean_pose_path = '' +_C.hand_model.shape_mean_path = 'data/shape_mean.npy' +_C.hand_model.type = 'mano-from-smplx' +_C.hand_model.model_folder = 'models' +_C.hand_model.use_compressed = True +_C.hand_model.gender = 'neutral' +_C.hand_model.num_betas = 10 +_C.hand_model.num_expression_coeffs = 10 +_C.hand_model.use_feet_keypoints = True +_C.hand_model.use_face_keypoints = True + +_C.hand_model.return_hand_vertices_only = True +_C.hand_model.vertex_idxs_path = '' + +_C.hand_model.global_orient = CN() +# The configuration for the parameterization of the body pose +_C.hand_model.global_orient.param_type = 'cont_rot_repr' + +_C.hand_model.hand_pose = CN() +_C.hand_model.hand_pose.param_type = 'pca' +_C.hand_model.hand_pose.num_pca_comps = 12 +_C.hand_model.hand_pose.flat_hand_mean = False + +#### HEAD MODEL ########### +_C.head_model = CN() +_C.head_model.j14_regressor_path = '' +_C.head_model.mean_pose_path = '' +_C.head_model.shape_mean_path = 'data/shape_mean.npy' +_C.head_model.type = 'flame-from-smplx' +_C.head_model.model_folder = 'models' +_C.head_model.use_compressed = True +_C.head_model.gender = 'neutral' +_C.head_model.num_betas = 10 +_C.head_model.num_expression_coeffs = 10 +_C.head_model.use_feet_keypoints = True +_C.head_model.use_face_keypoints = True +_C.head_model.use_face_contour = True +_C.head_model.return_head_vertices_only = True +_C.head_model.vertex_idxs_path = '' + +_C.head_model.global_orient = CN() +# The configuration for the parameterization of the body pose +_C.head_model.global_orient.param_type = 'cont_rot_repr' +# +_C.head_model.jaw_pose = CN() +_C.head_model.jaw_pose.param_type = 'cont_rot_repr' + +=== File: expose/models/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/__init__.py:1-15 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +=== File: expose/models/smplx_net.py === + +-- Chunk 1 -- +// smplx_net.py:30-77 +ss SMPLXNet(nn.Module): + + def __init__(self, exp_cfg): + super(SMPLXNet, self).__init__() + + self.exp_cfg = exp_cfg.clone() + network_cfg = exp_cfg.get('network', {}) + self.net_type = network_cfg.get('type', 'attention') + if self.net_type == 'attention': + self.smplx = build_attention_head(exp_cfg) + else: + raise ValueError(f'Unknown network type: {self.net_type}') + + def toggle_hands_and_face(self, iteration): + pass + + def toggle_losses(self, iteration): + self.smplx.toggle_losses(iteration) + + def get_hand_model(self) -> nn.Module: + return self.smplx.get_hand_model() + + def get_head_model(self) -> nn.Module: + return self.smplx.get_head_model() + + def toggle_param_prediction(self, iteration) -> None: + self.smplx.toggle_param_prediction(iteration) + + def forward(self, images, targets, + hand_imgs=None, hand_targets=None, + head_imgs=None, head_targets=None, + full_imgs=None, + device=None): + + if not self.training: + pass + if device is None: + device = torch.device('cpu') + + losses = {} + + output = self.smplx(images, targets=targets, + hand_imgs=hand_imgs, hand_targets=hand_targets, + head_imgs=head_imgs, head_targets=head_targets, + full_imgs=full_imgs) + + output['losses'] = losses + return output + +=== File: expose/data/build.py === + +-- Chunk 1 -- +// build.py:45-53 + make_data_sampler(dataset, is_train=True, + shuffle=True, is_distributed=False): + if is_train: + sampler = dutils.RandomSampler(dataset) + else: + sampler = dutils.SequentialSampler(dataset) + return sampler + + + +-- Chunk 2 -- +// build.py:54-87 + make_head_dataset(name, dataset_cfg, transforms, + num_betas=10, num_expression_coeffs=10, + **kwargs): + if name == 'ehf': + obj = datasets.EHF + elif name == 'curated_fits': + obj = datasets.CuratedFittings + elif name == 'spinx': + obj = datasets.SPINX + elif name == 'ffhq': + obj = datasets.FFHQ + elif name == 'openpose': + obj = datasets.OpenPose + elif name == 'stirling3d': + obj = datasets.Stirling3D + else: + raise ValueError('Unknown dataset: {}'.format(name)) + + args = dict(**dataset_cfg[name]) + args.update(kwargs) + + vertex_flip_correspondences = osp.expandvars(dataset_cfg.get( + 'vertex_flip_correspondences', '')) + dset_obj = obj(transforms=transforms, + head_only=True, + num_betas=num_betas, + num_expression_coeffs=num_expression_coeffs, + vertex_flip_correspondences=vertex_flip_correspondences, + **args) + + logger.info(f'Created head dataset: {dset_obj.name()}') + return dset_obj + + + +-- Chunk 3 -- +// build.py:88-118 + make_hand_dataset(name, dataset_cfg, transforms, + num_betas=10, num_expression_coeffs=10, + **kwargs): + if name == 'ehf': + obj = datasets.EHF + elif name == 'curated_fits': + obj = datasets.CuratedFittings + elif name == 'spinx': + obj = datasets.SPINX + elif name == 'openpose': + obj = datasets.OpenPose + elif name == 'freihand': + obj = datasets.FreiHand + else: + raise ValueError(f'Unknown dataset: {name}') + + logger.info(f'Building dataset: {name}') + args = dict(**dataset_cfg[name]) + args.update(kwargs) + vertex_flip_correspondences = osp.expandvars(dataset_cfg.get( + 'vertex_flip_correspondences', '')) + + dset_obj = obj(transforms=transforms, num_betas=num_betas, hand_only=True, + num_expression_coeffs=num_expression_coeffs, + vertex_flip_correspondences=vertex_flip_correspondences, + **args) + + logger.info(f'Created dataset: {dset_obj.name()}') + return dset_obj + + + +-- Chunk 4 -- +// build.py:119-155 + make_body_dataset(name, dataset_cfg, transforms, + num_betas=10, + num_expression_coeffs=10, + **kwargs): + if name == 'ehf': + obj = datasets.EHF + elif name == 'curated_fits': + obj = datasets.CuratedFittings + elif name == 'threedpw': + obj = datasets.ThreeDPW + elif name == 'spin': + obj = datasets.SPIN + elif name == 'spinx': + obj = datasets.SPINX + elif name == 'lsp_test': + obj = datasets.LSPTest + elif name == 'openpose': + obj = datasets.OpenPose + elif name == 'tracks': + obj = datasets.OpenPoseTracks + else: + raise ValueError(f'Unknown dataset: {name}') + + args = dict(**dataset_cfg[name]) + args.update(kwargs) + + vertex_flip_correspondences = osp.expandvars(dataset_cfg.get( + 'vertex_flip_correspondences', '')) + dset_obj = obj(transforms=transforms, num_betas=num_betas, + vertex_flip_correspondences=vertex_flip_correspondences, + num_expression_coeffs=num_expression_coeffs, + **args) + + logger.info('Created dataset: {}', dset_obj.name()) + return dset_obj + + + +-- Chunk 5 -- +// build.py:156-182 +ss MemoryPinning(object): + def __init__( + self, + full_img_list: Union[ImageList, List[Tensor]], + images: Tensor, + targets: List[GenericTarget] + ): + super(MemoryPinning, self).__init__() + self.img_list = full_img_list + self.images = images + self.targets = targets + + def pin_memory( + self + ) -> Tuple[Union[ImageList, List[Tensor]], Tensor, List[GenericTarget]]: + if self.img_list is not None: + if isinstance(self.img_list, ImageList): + self.img_list.pin_memory() + elif isinstance(self.img_list, (list, tuple)): + self.img_list = [x.pin_memory() for x in self.img_list] + return ( + self.img_list, + self.images.pin_memory(), + self.targets, + ) + + + +-- Chunk 6 -- +// build.py:183-235 + collate_batch(batch, use_shared_memory=False, return_full_imgs=False, + pin_memory=True): + if return_full_imgs: + images, cropped_images, targets, _ = zip(*batch) + else: + _, cropped_images, targets, _ = zip(*batch) + + out_targets = [] + for t in targets: + if t is None: + continue + if type(t) == list: + out_targets += t + else: + out_targets.append(t) + out_cropped_images = [] + for img in cropped_images: + if img is None: + continue + if len(img.shape) < 4: + img.unsqueeze_(dim=0) + out_cropped_images.append(img.clone()) + + if len(out_cropped_images) < 1: + return None, None, None + + full_img_list = None + if return_full_imgs: + # full_img_list = to_image_list(images) + full_img_list = images + out = None + if use_shared_memory: + numel = sum([x.numel() for x in out_cropped_images if x is not None]) + storage = out_cropped_images[0].storage()._new_shared(numel) + out = out_cropped_images[0].new(storage) + + # if not return_full_imgs: + # del images + # images = None + + batch.clear() + # del targets, batch + if pin_memory: + return MemoryPinning( + full_img_list, + torch.cat(out_cropped_images, 0, out=out), + out_targets + ) + else: + return full_img_list, torch.cat( + out_cropped_images, 0, out=out), out_targets + + + +-- Chunk 7 -- +// build.py:236-243 + make_equal_sampler(datasets, batch_size=32, shuffle=True, ratio_2d=0.5): + batch_sampler = EqualSampler( + datasets, batch_size=batch_size, shuffle=shuffle, ratio_2d=ratio_2d) + out_dsets_lst = [dutils.ConcatDataset(datasets) if len(datasets) > 1 else + datasets[0]] + return batch_sampler, out_dsets_lst + + + +-- Chunk 8 -- +// build.py:244-275 + make_data_loader(dataset, batch_size=32, num_workers=0, + is_train=True, sampler=None, collate_fn=None, + shuffle=True, is_distributed=False, + batch_sampler=None): + if batch_sampler is None: + sampler = make_data_sampler( + dataset, is_train=is_train, + shuffle=shuffle, is_distributed=is_distributed) + + if batch_sampler is None: + assert sampler is not None, ( + 'Batch sampler and sampler can\'t be "None" at the same time') + data_loader = torch.utils.data.DataLoader( + dataset, + batch_size=batch_size, + num_workers=num_workers, + sampler=sampler, + collate_fn=collate_fn, + drop_last=True and is_train, + pin_memory=True, + ) + else: + data_loader = torch.utils.data.DataLoader( + dataset, + num_workers=num_workers, + collate_fn=collate_fn, + batch_sampler=batch_sampler, + pin_memory=True, + ) + return data_loader + + + +-- Chunk 9 -- +// build.py:276-425 + make_all_data_loaders(exp_cfg, split='train', start_iter=0, **kwargs): + is_train = 'train' in split + num_betas = exp_cfg.body_model.num_betas + num_expression_coeffs = exp_cfg.body_model.num_expression_coeffs + + dataset_cfg = exp_cfg.get('datasets', {}) + + body_dsets_cfg = dataset_cfg.get('body', {}) + body_dset_names = body_dsets_cfg.get('splits', {})[split] + + body_transfs_cfg = body_dsets_cfg.get('transforms', {}) + body_transforms = build_transforms(body_transfs_cfg, is_train=is_train) + + hand_dsets_cfg = dataset_cfg.get('hand', {}) + hand_dset_names = hand_dsets_cfg.get('splits', {})[split] + hand_transfs_cfg = hand_dsets_cfg.get('transforms', {}) + hand_transforms = build_transforms(hand_transfs_cfg, is_train=is_train) + + head_dsets_cfg = dataset_cfg.get('head', {}) + head_dset_names = head_dsets_cfg.get('splits', {})[split] + head_transfs_cfg = head_dsets_cfg.get('transforms', {}) + head_transforms = build_transforms(head_transfs_cfg, is_train=is_train) + + body_datasets = [] + for dataset_name in body_dset_names: + dset = make_body_dataset(dataset_name, body_dsets_cfg, + transforms=body_transforms, + num_betas=num_betas, + num_expression_coeffs=num_expression_coeffs, + is_train=is_train, split=split, **kwargs) + body_datasets.append(dset) + + hand_datasets = [] + for dataset_name in hand_dset_names: + dset = make_hand_dataset(dataset_name, hand_dsets_cfg, + transforms=hand_transforms, + num_betas=num_betas, + num_expression_coeffs=num_expression_coeffs, + is_train=is_train, split=split, **kwargs) + hand_datasets.append(dset) + + head_datasets = [] + for dataset_name in head_dset_names: + dset = make_head_dataset(dataset_name, head_dsets_cfg, + transforms=head_transforms, + num_betas=num_betas, + num_expression_coeffs=num_expression_coeffs, + is_train=is_train, split=split, **kwargs) + head_datasets.append(dset) + + use_equal_sampling = exp_cfg.datasets.use_equal_sampling + + # Hard-coded for now + shuffle = is_train + is_distributed = False + + body_batch_size = body_dsets_cfg.get('batch_size', 64) + body_ratio_2d = body_dsets_cfg.get('ratio_2d', 0.5) + + hand_batch_size = hand_dsets_cfg.get('batch_size', 64) + hand_ratio_2d = hand_dsets_cfg.get('ratio_2d', 0.5) + + head_batch_size = head_dsets_cfg.get('batch_size', 64) + head_ratio_2d = head_dsets_cfg.get('ratio_2d', 0.5) + + body_num_workers = body_dsets_cfg.get( + 'num_workers', DEFAULT_NUM_WORKERS).get(split, 0) + logger.info(f'{split.upper()} Body num workers: {body_num_workers}') + + network_cfg = exp_cfg.network + return_full_imgs = (network_cfg.get('apply_hand_network_on_body', True) or + network_cfg.get('apply_head_network_on_body', True)) + logger.info(f'Return full resolution images: {return_full_imgs}') + body_collate_fn = functools.partial( + collate_batch, use_shared_memory=body_num_workers > 0, + return_full_imgs=return_full_imgs) + + hand_num_workers = hand_dsets_cfg.get( + 'num_workers', DEFAULT_NUM_WORKERS).get(split, 0) + hand_collate_fn = functools.partial( + collate_batch, use_shared_memory=hand_num_workers > 0) + # collate_batch, use_shared_memory=False) + + head_num_workers = head_dsets_cfg.get( + 'num_workers', DEFAULT_NUM_WORKERS).get(split, 0) + head_collate_fn = functools.partial( + collate_batch, use_shared_memory=head_num_workers > 0) + # collate_batch, use_shared_memory=False) + + body_batch_sampler, hand_batch_sampler, head_batch_sampler = [None] * 3 + # Equal sampling should only be used during training and only if there + # are multiple datasets + if is_train and use_equal_sampling: + body_batch_sampler, body_datasets = make_equal_sampler( + body_datasets, batch_size=body_batch_size, + shuffle=shuffle, ratio_2d=body_ratio_2d) + if len(hand_datasets) > 0: + hand_batch_sampler, hand_datasets = make_equal_sampler( + hand_datasets, batch_size=hand_batch_size, + shuffle=shuffle, ratio_2d=hand_ratio_2d) + if len(head_datasets) > 0: + head_batch_sampler, head_datasets = make_equal_sampler( + head_datasets, batch_size=head_batch_size, + shuffle=shuffle, ratio_2d=head_ratio_2d) + + body_data_loaders = [] + for body_dataset in body_datasets: + body_data_loaders.append( + make_data_loader(body_dataset, batch_size=body_batch_size, + num_workers=body_num_workers, + is_train=is_train, + batch_sampler=body_batch_sampler, + collate_fn=body_collate_fn, + shuffle=shuffle, is_distributed=is_distributed)) + hand_data_loaders = [] + for hand_dataset in hand_datasets: + hand_data_loaders.append( + make_data_loader(hand_dataset, batch_size=hand_batch_size, + num_workers=hand_num_workers, + is_train=is_train, + batch_sampler=hand_batch_sampler, + collate_fn=hand_collate_fn, + shuffle=shuffle, is_distributed=is_distributed)) + head_data_loaders = [] + for head_dataset in head_datasets: + head_data_loaders.append( + make_data_loader(head_dataset, batch_size=head_batch_size, + num_workers=head_num_workers, + is_train=is_train, + batch_sampler=head_batch_sampler, + collate_fn=head_collate_fn, + shuffle=shuffle, is_distributed=is_distributed)) + + use_adv_training = exp_cfg.use_adv_training + if is_train: + assert len(body_data_loaders) == 1, ( + 'There should be a single body loader,' + f' not {len(body_data_loaders)}') + # assert len(hand_data_loaders) == 1, ( + # 'There should be a single hand loader,' + # f' not {len(hand_data_loaders)}') + # assert len(head_data_loaders) == 1, ( + # 'There should be a single head loader,' + # f' not {len(head_data_loaders)}') + dloaders = { + 'body': body_data_loaders[0], + } + if len(hand_data_loaders) > 0: + dloaders['hand'] = hand_data_loaders[0] + if len(head_data_loaders) > 0: + +-- Chunk 10 -- +// build.py:426-435 + dloaders['head'] = head_data_loaders[0] + if use_adv_training: + raise NotImplementedError + return dloaders + + return { + 'body': body_data_loaders, + 'hand': hand_data_loaders, + 'head': head_data_loaders, + } + +=== File: expose/data/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/data/__init__.py:1-17 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from .build import make_all_data_loaders + +=== File: expose/models/camera/build.py === + +-- Chunk 1 -- +// build.py:11-12 +def build_camera_head(cfg, feat_dim): + return CameraHead(cfg, feat_dim) + +=== File: expose/models/camera/camera_projection.py === + +-- Chunk 1 -- +// camera_projection.py:32-59 +ss CameraParams: + translation: Tensor = None + rotation: Tensor = None + scale: Tensor = None + focal_length: Tensor = None + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def __iter__(self): + return self.keys() + + def keys(self): + keys = [t.name for t in fields(self)] + return iter(keys) + + def values(self): + values = [getattr(self, t.name) for t in fields(self)] + return iter(values) + + def items(self): + data = [(t.name, getattr(self, t.name)) for t in fields(self)] + return iter(data) + + + +-- Chunk 2 -- +// camera_projection.py:60-106 + build_cam_proj(camera_cfg, dtype=torch.float32): + camera_type = camera_cfg.get('type', 'weak-persp') + camera_pos_scale = camera_cfg.get('pos_func') + if camera_pos_scale == 'softplus': + camera_scale_func = F.softplus + elif camera_pos_scale == 'exp': + camera_scale_func = torch.exp + elif camera_pos_scale == 'none' or camera_pos_scale == 'None': + def func(x): + return x + camera_scale_func = func + else: + raise ValueError( + f'Unknown positive scaling function: {camera_pos_scale}') + + if camera_type.lower() == 'persp': + if camera_pos_scale == 'softplus': + mean_flength = np.log(np.exp(DEFAULT_FOCAL_LENGTH) - 1) + elif camera_pos_scale == 'exp': + mean_flength = np.log(DEFAULT_FOCAL_LENGTH) + elif camera_pos_scale == 'none': + mean_flength = DEFAULT_FOCAL_LENGTH + camera = PerspectiveCamera(dtype=dtype) + camera_mean = torch.tensor( + [mean_flength, 0.0, 0.0], dtype=torch.float32) + camera_param_dim = 4 + elif camera_type.lower() == 'weak-persp': + weak_persp_cfg = camera_cfg.get('weak_persp', {}) + mean_scale = weak_persp_cfg.get('mean_scale', 0.9) + if camera_pos_scale == 'softplus': + mean_scale = np.log(np.exp(mean_scale) - 1) + elif camera_pos_scale == 'exp': + mean_scale = np.log(mean_scale) + camera_mean = torch.tensor([mean_scale, 0.0, 0.0], dtype=torch.float32) + camera = WeakPerspectiveCamera(dtype=dtype) + camera_param_dim = 3 + else: + raise ValueError(f'Unknown camera type: {camera_type}') + + return { + 'camera': camera, + 'mean': camera_mean, + 'scale_func': camera_scale_func, + 'dim': camera_param_dim + } + + + +-- Chunk 3 -- +// camera_projection.py:107-194 +ss PerspectiveCamera(nn.Module): + ''' Module that implements a perspective camera + ''' + + FOCAL_LENGTH = DEFAULT_FOCAL_LENGTH + + def __init__(self, dtype=torch.float32, focal_length=None, **kwargs): + super(PerspectiveCamera, self).__init__() + self.dtype = dtype + + if focal_length is None: + focal_length = self.FOCAL_LENGTH + # Make a buffer so that PyTorch does not complain when creating + # the camera matrix + self.register_buffer( + 'focal_length', torch.tensor(focal_length, dtype=dtype)) + + def forward( + self, + points: Tensor, + focal_length: Tensor = None, + translation: Tensor = None, + rotation: Tensor = None, + camera_center: Tensor = None, + **kwargs + ) -> Tensor: + ''' Forward pass for the perspective camera + + Parameters + ---------- + points: torch.tensor, BxNx3 + The tensor that contains the points that will be projected. + If not in homogeneous coordinates, then + focal_length: torch.tensor, BxNx3, optional + The predicted focal length of the camera. If not given, + then the default value of 5000 is assigned + translation: torch.tensor, Bx3, optional + The translation predicted for each element in the batch. If + not given then a zero translation vector is assumed + rotation: torch.tensor, Bx3x3, optional + The rotation predicted for each element in the batch. If + not given then an identity rotation matrix is assumed + camera_center: torch.tensor, Bx2, optional + The center of each image for the projection. If not given, + then a zero vector is used + Returns + ------- + Returns a torch.tensor object with size BxNx2 with the + location of the projected points on the image plane + ''' + + device = points.device + batch_size = points.shape[0] + + if rotation is None: + rotation = torch.eye( + 3, dtype=points.dtype, device=device).unsqueeze(dim=0).expand( + batch_size, -1, -1) + if translation is None: + translation = torch.zeros( + [3], dtype=points.dtype, + device=device).unsqueeze(dim=0).expand(batch_size, -11) + + if camera_center is None: + camera_center = torch.zeros([batch_size, 2], dtype=points.dtype, + device=device) + + with torch.no_grad(): + camera_mat = torch.zeros([batch_size, 2, 2], + dtype=self.dtype, device=points.device) + if focal_length is None: + focal_length = self.focal_length + + camera_mat[:, 0, 0] = focal_length + camera_mat[:, 1, 1] = focal_length + + points_transf = torch.einsum( + 'bji,bmi->bmj', + rotation, points) + translation.unsqueeze(dim=1) + + img_points = torch.div(points_transf[:, :, :2], + points_transf[:, :, 2].unsqueeze(dim=-1)) + img_points = torch.einsum( + 'bmi,bji->bjm', + camera_mat, img_points) + camera_center.reshape(-1, 1, 2) + return img_points + + + +-- Chunk 4 -- +// camera_projection.py:195-231 +ss WeakPerspectiveCamera(nn.Module): + ''' Scaled Orthographic / Weak-Perspective Camera + ''' + + def __init__(self, **kwargs): + super(WeakPerspectiveCamera, self).__init__() + + def forward( + self, + points: Tensor, + scale: Tensor, + translation: Tensor, + **kwargs + ) -> Tensor: + ''' Implements the forward pass for a Scaled Orthographic Camera + + Parameters + ---------- + points: torch.tensor, BxNx3 + The tensor that contains the points that will be projected. + If not in homogeneous coordinates, then + scale: torch.tensor, Bx1 + The predicted scaling parameters + translation: torch.tensor, Bx2 + The translation applied on the image plane to the points + Returns + ------- + projected_points: torch.tensor, BxNx2 + The points projected on the image plane, according to the + given scale and translation + ''' + assert translation.shape[-1] == 2, 'Translation shape must be -1x2' + assert scale.shape[-1] == 1, 'Scale shape must be -1x1' + + projected_points = scale.view(-1, 1, 1) * ( + points[:, :, :2] + translation.view(-1, 1, 2)) + return projected_points + +=== File: expose/models/camera/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/camera/__init__.py:1-4 +from .camera_projection import ( + build_cam_proj, DEFAULT_FOCAL_LENGTH, CameraParams) + + + +=== File: expose/models/backbone/fpn.py === + +-- Chunk 1 -- +// fpn.py:20-31 +class BackboneWithFPN(_BackboneWithFPN): + def __init__(self, *args, **kwargs): + super(BackboneWithFPN, self).__init__(*args, **kwargs) + + def forward(self, x): + body_features = getattr(self, 'body')(x) + + output = getattr(self, 'fpn')(body_features) + + for key in body_features: + output[f'body_{key}'] = body_features[key] + return output + +-- Chunk 2 -- +// fpn.py:34-58 +def resnet_fpn_backbone(backbone_name, pretrained=True, freeze=False): + backbone = resnet.__dict__[backbone_name]( + pretrained=pretrained) + if freeze: + # freeze layers + for name, parameter in backbone.named_parameters(): + if ('layer2' not in name and 'layer3' not in name and + 'layer4' not in name): + parameter.requires_grad_(False) + + return_layers = {'layer1': 'layer1', + 'layer2': 'layer2', + 'layer3': 'layer3', + 'layer4': 'layer4'} + + in_channels_stage2 = backbone.inplanes // 8 + in_channels_list = [ + in_channels_stage2, + in_channels_stage2 * 2, + in_channels_stage2 * 4, + in_channels_stage2 * 8, + ] + out_channels = 256 + return BackboneWithFPN(backbone, return_layers, in_channels_list, + out_channels) + +-- Chunk 3 -- +// fpn.py:61-71 +def build_fpn_backbone(backbone_cfg, + pretrained=True) -> nn.Module: + backbone_type = backbone_cfg.get('type', 'resnet50') + + resnet_type = backbone_type.replace('fpn', '').replace('_', '').replace( + '-', '') + network = resnet_fpn_backbone(resnet_type, pretrained=pretrained) + + fpn_cfg = backbone_cfg.get('fpn', {}) + + return RegressionFPN(network, fpn_cfg) + +-- Chunk 4 -- +// fpn.py:74-98 +class SumAvgPooling(nn.Module): + def __init__(self, pooling_type='avg', **kwargs) -> None: + super(SumAvgPooling, self).__init__() + + if pooling_type == 'avg': + self.pooling = nn.AdaptiveAvgPool2d(1) + elif pooling_type == 'max': + self.pooling = nn.AdaptiveMaxPool2d(1) + else: + raise ValueError(f'Unknown pooling function: {pooling_type}') + + def get_out_feature_dim(self) -> int: + return FPN_FEATURE_DIM + + def forward(self, features: Dict[str, torch.Tensor]) -> torch.Tensor: + + pooled_features = {} + # Pool each feature map + for key in features: + batch_size, feat_dim = features[key].shape[:2] + pooled_features[key] = self.pooling(features[key]).view( + batch_size, feat_dim) + + # Sum the individual features + return sum(pooled_features.values()) + +-- Chunk 5 -- +// fpn.py:101-138 +class ConcatPooling(nn.Module): + def __init__(self, use_max: bool = True, use_avg: bool = True, + **kwargs) -> None: + super(ConcatPooling, self).__init__() + assert use_avg or use_max, 'Either max or avg pooling should be on' + + self.use_avg = use_avg + self.use_max = use_max + if use_avg: + self.avg_pooling = nn.AdaptiveAvgPool2d(1) + if use_max: + self.max_pooling = nn.AdaptiveMaxPool2d(1) + + def extra_repr(self) -> str: + msg = [f'Use average pooling: {self.use_avg}', + f'Use max pooling: {self.use_max}'] + return '\n'.join(msg) + + def get_out_feature_dim(self) -> int: + return 5 * ( + self.use_avg * FPN_FEATURE_DIM + self.use_max * FPN_FEATURE_DIM) + + def forward(self, features: Dict[str, torch.Tensor]) -> torch.Tensor: + pooled_features = [] + for key in features: + batch_size, feat_dim = features[key].shape[:2] + feats = [] + if self.use_avg: + avg_pooled_features = self.avg_pooling(features[key]).view( + batch_size, feat_dim) + feats.append(avg_pooled_features) + if self.use_max: + max_pooled_features = self.max_pooling(features[key]).view( + batch_size, feat_dim) + feats.append(max_pooled_features) + pooled_features.append( + torch.cat(feats, dim=-1)) + return torch.cat(pooled_features, dim=-1) + +-- Chunk 6 -- +// fpn.py:141-161 +class BilinearPooling(nn.Module): + def __init__(self, pooling_type='avg', **kwargs) -> None: + super(BilinearPooling, self).__init__() + raise NotImplementedError + if pooling_type == 'avg': + self.pooling = nn.AdaptiveAvgPool2d(1) + elif pooling_type == 'max': + self.pooling = nn.AdaptiveMaxPool2d(1) + else: + raise ValueError(f'Unknown pooling function: {pooling_type}') + + def forward(self, features: Dict[str, torch.Tensor]) -> torch.Tensor: + pooled_features = {} + # Pool each feature map + for key in features: + batch_size, feat_dim = features[key].shape[:2] + pooled_features[key] = self.pooling(features[key]).view( + batch_size, feat_dim) + # Should be BxNxK + stacked_features = torch.stack(pooled_features.values(), dim=1) + pass + +-- Chunk 7 -- +// fpn.py:165-202 +class RegressionFPN(nn.Module): + + def __init__(self, backbone, fpn_cfg) -> None: + super(RegressionFPN, self).__init__() + self.feat_extractor = backbone + + pooling_type = fpn_cfg.get('pooling_type', 'sum_avg') + self.avg_pooling = nn.AdaptiveAvgPool2d(1) + if pooling_type == 'sum_avg': + sum_avg_cfg = fpn_cfg.get('sum_avg', {}) + self.pooling = SumAvgPooling(**sum_avg_cfg) + elif pooling_type == 'concat': + concat_cfg = fpn_cfg.get('concat', {}) + self.pooling = ConcatPooling(**concat_cfg) + elif pooling_type == 'none': + self.pooling = None + else: + raise ValueError(f'Unknown pooling type {pooling_type}') + + def get_output_dim(self) -> int: + output = { + 'layer1': FPN_FEATURE_DIM, + 'layer2': FPN_FEATURE_DIM, + 'layer3': FPN_FEATURE_DIM, + 'layer4': FPN_FEATURE_DIM, + } + + for key in output: + output[f'{key}_avg_pooling'] = FPN_FEATURE_DIM + return output + + def forward(self, x: torch.Tensor) -> torch.Tensor: + features = self.feat_extractor(x) + + if self.pooling is not None: + pass + features['avg_pooling'] = self.avg_pooling(features['body_layer4']) + return features + +=== File: expose/models/backbone/hrnet.py === + +-- Chunk 1 -- +// hrnet.py:20-28 +def build(cfg, pretrained=True, **kwargs): + hr_net_cfg = cfg.get('hrnet') + model = HighResolutionNet(hr_net_cfg, **kwargs) + + pretrained_path = hr_net_cfg.get('pretrained_path') + if pretrained: + model.load_weights(pretrained_path) + + return model + +-- Chunk 2 -- +// hrnet.py:31-180 +class HighResolutionModule(nn.Module): + def __init__(self, num_branches, blocks, num_blocks, num_inchannels, + num_channels, fuse_method, multi_scale_output=True): + super(HighResolutionModule, self).__init__() + self._check_branches( + num_branches, blocks, num_blocks, num_inchannels, num_channels) + + self.num_inchannels = num_inchannels + self.fuse_method = fuse_method + self.num_branches = num_branches + + self.multi_scale_output = multi_scale_output + + self.branches = self._make_branches( + num_branches, blocks, num_blocks, num_channels) + self.fuse_layers = self._make_fuse_layers() + self.relu = nn.ReLU(True) + + def _check_branches(self, num_branches, blocks, num_blocks, + num_inchannels, num_channels): + if num_branches != len(num_blocks): + error_msg = 'NUM_BRANCHES({}) <> NUM_BLOCKS({})'.format( + num_branches, len(num_blocks)) + logger.error(error_msg) + raise ValueError(error_msg) + + if num_branches != len(num_channels): + error_msg = 'NUM_BRANCHES({}) <> NUM_CHANNELS({})'.format( + num_branches, len(num_channels)) + logger.error(error_msg) + raise ValueError(error_msg) + + if num_branches != len(num_inchannels): + error_msg = 'NUM_BRANCHES({}) <> NUM_INCHANNELS({})'.format( + num_branches, len(num_inchannels)) + logger.error(error_msg) + raise ValueError(error_msg) + + def _make_one_branch(self, branch_index, block, num_blocks, num_channels, + stride=1): + downsample = None + if stride != 1 or \ + self.num_inchannels[branch_index] != num_channels[branch_index] * block.expansion: + downsample = nn.Sequential( + nn.Conv2d( + self.num_inchannels[branch_index], + num_channels[branch_index] * block.expansion, + kernel_size=1, stride=stride, bias=False + ), + nn.BatchNorm2d( + num_channels[branch_index] * block.expansion, + momentum=BN_MOMENTUM + ), + ) + + layers = [] + layers.append( + block( + self.num_inchannels[branch_index], + num_channels[branch_index], + stride, + downsample + ) + ) + self.num_inchannels[branch_index] = \ + num_channels[branch_index] * block.expansion + for i in range(1, num_blocks[branch_index]): + layers.append( + block( + self.num_inchannels[branch_index], + num_channels[branch_index] + ) + ) + + return nn.Sequential(*layers) + + def _make_branches(self, num_branches, block, num_blocks, num_channels): + branches = [] + + for i in range(num_branches): + branches.append( + self._make_one_branch(i, block, num_blocks, num_channels) + ) + + return nn.ModuleList(branches) + + def _make_fuse_layers(self): + if self.num_branches == 1: + return None + + num_branches = self.num_branches + num_inchannels = self.num_inchannels + fuse_layers = [] + for i in range(num_branches if self.multi_scale_output else 1): + fuse_layer = [] + for j in range(num_branches): + if j > i: + fuse_layer.append( + nn.Sequential( + nn.Conv2d( + num_inchannels[j], + num_inchannels[i], + 1, 1, 0, bias=False + ), + nn.BatchNorm2d(num_inchannels[i]), + nn.Upsample(scale_factor=2**(j-i), mode='nearest') + ) + ) + elif j == i: + fuse_layer.append(None) + else: + conv3x3s = [] + for k in range(i-j): + if k == i - j - 1: + num_outchannels_conv3x3 = num_inchannels[i] + conv3x3s.append( + nn.Sequential( + nn.Conv2d( + num_inchannels[j], + num_outchannels_conv3x3, + 3, 2, 1, bias=False + ), + nn.BatchNorm2d(num_outchannels_conv3x3) + ) + ) + else: + num_outchannels_conv3x3 = num_inchannels[j] + conv3x3s.append( + nn.Sequential( + nn.Conv2d( + num_inchannels[j], + num_outchannels_conv3x3, + 3, 2, 1, bias=False + ), + nn.BatchNorm2d(num_outchannels_conv3x3), + nn.ReLU(True) + ) + ) + fuse_layer.append(nn.Sequential(*conv3x3s)) + fuse_layers.append(nn.ModuleList(fuse_layer)) + + return nn.ModuleList(fuse_layers) + + def get_num_inchannels(self): + return self.num_inchannels + + def forward(self, x): + if self.num_branches == 1: + return [self.branches[0](x[0])] + + +-- Chunk 3 -- +// hrnet.py:181-195 + for i in range(self.num_branches): + x[i] = self.branches[i](x[i]) + + x_fuse = [] + + for i in range(len(self.fuse_layers)): + y = x[0] if i == 0 else self.fuse_layers[i][0](x[0]) + for j in range(1, self.num_branches): + if i == j: + y = y + x[j] + else: + y = y + self.fuse_layers[i][j](x[j]) + x_fuse.append(self.relu(y)) + + return x_fuse + +-- Chunk 4 -- +// hrnet.py:204-353 +class HighResolutionNet(nn.Module): + + def __init__(self, cfg, **kwargs): + self.inplanes = 64 + super(HighResolutionNet, self).__init__() + + # stem net + self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(64, momentum=BN_MOMENTUM) + self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(64, momentum=BN_MOMENTUM) + self.relu = nn.ReLU(inplace=True) + + self.stage1_cfg = cfg.get('stage1', {}) + num_channels = self.stage1_cfg['num_channels'][0] + block = blocks_dict[self.stage1_cfg['block']] + num_blocks = self.stage1_cfg['num_blocks'][0] + self.layer1 = self._make_layer(block, num_channels, num_blocks) + stage1_out_channel = block.expansion * num_channels + + self.stage2_cfg = cfg.get('stage2', {}) + num_channels = self.stage2_cfg.get('num_channels', (32, 64)) + block = blocks_dict[self.stage2_cfg.get('block')] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + stage2_num_channels = num_channels + self.transition1 = self._make_transition_layer( + [stage1_out_channel], num_channels) + self.stage2, pre_stage_channels = self._make_stage( + self.stage2_cfg, num_channels) + + self.stage3_cfg = cfg.get('stage3') + num_channels = self.stage3_cfg['num_channels'] + block = blocks_dict[self.stage3_cfg['block']] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + stage3_num_channels = num_channels + self.transition2 = self._make_transition_layer( + pre_stage_channels, num_channels) + self.stage3, pre_stage_channels = self._make_stage( + self.stage3_cfg, num_channels) + + self.stage4_cfg = cfg.get('stage4') + num_channels = self.stage4_cfg['num_channels'] + block = blocks_dict[self.stage4_cfg['block']] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + stage_4_out_channels = num_channels + self.transition3 = self._make_transition_layer( + pre_stage_channels, num_channels) + self.stage4, pre_stage_channels = self._make_stage( + self.stage4_cfg, num_channels, multi_scale_output=False) + self.output_channels_dim = pre_stage_channels + + self.pretrained_layers = cfg['pretrained_layers'] + self.init_weights() + + self.avg_pooling = nn.AdaptiveAvgPool2d(1) + + final_conv_cfg = cfg.get('final_conv') + # self.conv_layers = make_conv_layer(3 * 384, final_conv_cfg) + subsample3_cfg = self.stage3_cfg.get('subsample') + subsample2_cfg = self.stage2_cfg.get('subsample') + + # self.subsample_3, subsample_3_out_dim = make_subsample_layers( + # 96, subsample3_cfg) + # self.subsample_2, subsample_2_out_dim = make_subsample_layers( + # 192, subsample2_cfg) + + # TODO: Replace with parameters + in_dims = (2 ** 2 * stage2_num_channels[-1] + + 2 ** 1 * stage3_num_channels[-1] + + stage_4_out_channels[-1] + ) + self.conv_layers = self._make_conv_layer( + in_channels=in_dims, num_layers=5) + + self.subsample_3 = self._make_subsample_layer( + in_channels=stage2_num_channels[-1], + num_layers=2) + self.subsample_2 = self._make_subsample_layer( + in_channels=stage3_num_channels[-1], + num_layers=1) + # logger.info(self.subsample_3.state_dict().keys()) + + def get_output_dim(self): + base_output = { + f'layer{idx + 1}': val + for idx, val in enumerate(self.output_channels_dim) + } + output = base_output.copy() + for key in base_output: + output[f'{key}_avg_pooling'] = output[key] + output['concat'] = 2048 + return output + + def _make_transition_layer( + self, num_channels_pre_layer, num_channels_cur_layer): + num_branches_cur = len(num_channels_cur_layer) + num_branches_pre = len(num_channels_pre_layer) + + transition_layers = [] + for i in range(num_branches_cur): + if i < num_branches_pre: + if num_channels_cur_layer[i] != num_channels_pre_layer[i]: + transition_layers.append( + nn.Sequential( + nn.Conv2d( + num_channels_pre_layer[i], + num_channels_cur_layer[i], + 3, 1, 1, bias=False + ), + nn.BatchNorm2d(num_channels_cur_layer[i]), + nn.ReLU(inplace=True) + ) + ) + else: + transition_layers.append(None) + else: + conv3x3s = [] + for j in range(i+1-num_branches_pre): + inchannels = num_channels_pre_layer[-1] + outchannels = num_channels_cur_layer[i] \ + if j == i-num_branches_pre else inchannels + conv3x3s.append( + nn.Sequential( + nn.Conv2d( + inchannels, outchannels, 3, 2, 1, bias=False + ), + nn.BatchNorm2d(outchannels), + nn.ReLU(inplace=True) + ) + ) + transition_layers.append(nn.Sequential(*conv3x3s)) + + return nn.ModuleList(transition_layers) + + def _make_layer(self, block, planes, blocks, stride=1): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2d( + self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias=False + ), + +-- Chunk 5 -- +// hrnet.py:354-503 + nn.BatchNorm2d(planes * block.expansion, momentum=BN_MOMENTUM), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes)) + + return nn.Sequential(*layers) + + def _make_conv_layer(self, in_channels=2048, num_layers=3, num_filters=2048, stride=1): + + layers = [] + for i in range(num_layers): + + downsample = nn.Conv2d(in_channels, num_filters, stride=1, + kernel_size=1, bias=False) + layers.append(Bottleneck(in_channels, num_filters // 4, + downsample=downsample)) + in_channels = num_filters + + return nn.Sequential(*layers) + + def _make_subsample_layer(self, in_channels=96, num_layers=3, stride=2): + + layers = [] + for i in range(num_layers): + + layers.append( + nn.Conv2d( + in_channels=in_channels, + out_channels=2*in_channels, + kernel_size=3, + stride=stride, + padding=1)) + in_channels = 2*in_channels + layers.append(nn.BatchNorm2d(in_channels, momentum=BN_MOMENTUM)) + layers.append(nn.ReLU(inplace=True)) + + return nn.Sequential(*layers) + + def _make_stage(self, layer_config, num_inchannels, + multi_scale_output=True, log=False): + num_modules = layer_config['num_modules'] + num_branches = layer_config['num_branches'] + num_blocks = layer_config['num_blocks'] + num_channels = layer_config['num_channels'] + block = blocks_dict[layer_config['block']] + fuse_method = layer_config['fuse_method'] + + modules = [] + for i in range(num_modules): + # multi_scale_output is only used last module + if not multi_scale_output and i == num_modules - 1: + reset_multi_scale_output = False + else: + reset_multi_scale_output = True + + modules.append( + HighResolutionModule( + num_branches, + block, + num_blocks, + num_inchannels, + num_channels, + fuse_method, + reset_multi_scale_output + ) + ) + modules[-1].log = log + num_inchannels = modules[-1].get_num_inchannels() + + return nn.Sequential(*modules), num_inchannels + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu(x) + x = self.layer1(x) + + x_list = [] + for i in range(self.stage2_cfg['num_branches']): + if self.transition1[i] is not None: + x_list.append(self.transition1[i](x)) + else: + x_list.append(x) + y_list = self.stage2(x_list) + + x_list = [] + for i in range(self.stage3_cfg['num_branches']): + if self.transition2[i] is not None: + x_list.append(self.transition2[i](y_list[-1])) + else: + x_list.append(y_list[i]) + y_list = self.stage3(x_list) + + x_list = [] + for i in range(self.stage4_cfg['num_branches']): + if self.transition3[i] is not None: + x_list.append(self.transition3[i](y_list[-1])) + else: + x_list.append(y_list[i]) + + output = {} + for idx, x in enumerate(x_list): + output[f'layer{idx + 1}'] = x + # output[''] + + x3 = self.subsample_3(x_list[1]) + x2 = self.subsample_2(x_list[2]) + x1 = x_list[3] + xf = self.conv_layers(torch.cat([x3, x2, x1], dim=1)) + xf = xf.mean(dim=(2, 3)) + xf = xf.view(xf.size(0), -1) + output['concat'] = xf + # y_list = self.stage4(x_list) + # output['stage4'] = y_list[0] + # output['stage4_avg_pooling'] = self.avg_pooling(y_list[0]).view( + # *y_list[0].shape[:2]) + + # concat_outputs = y_list + x_list + # output['concat'] = torch.cat([ + # self.avg_pooling(tensor).view(*tensor.shape[:2]) + # for tensor in concat_outputs], + # dim=1) + + return output + + def init_weights(self): + logger.info('=> init weights from normal distribution') + for m in self.modules(): + if isinstance(m, nn.Conv2d): + # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + nn.init.normal_(m.weight, std=0.001) + for name, _ in m.named_parameters(): + if name in ['bias']: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.ConvTranspose2d): + nn.init.normal_(m.weight, std=0.001) + for name, _ in m.named_parameters(): + if name in ['bias']: + nn.init.constant_(m.bias, 0) + + +-- Chunk 6 -- +// hrnet.py:504-520 + def load_weights(self, pretrained=''): + pretrained = osp.expandvars(pretrained) + if osp.isfile(pretrained): + pretrained_state_dict = torch.load( + pretrained, map_location=torch.device("cpu")) + logger.info('=> loading pretrained model {}'.format(pretrained)) + + need_init_state_dict = {} + for name, m in pretrained_state_dict.items(): + if (name.split('.')[0] in self.pretrained_layers or + self.pretrained_layers[0] == '*'): + need_init_state_dict[name] = m + missing, unexpected = self.load_state_dict( + need_init_state_dict, strict=False) + else: + logger.warning('=> please download pre-trained models first!') + logger.warning(f'{pretrained} does not exist!') + +=== File: expose/models/backbone/build.py === + +-- Chunk 1 -- +// build.py:8-27 +def build_backbone(backbone_cfg): + backbone_type = backbone_cfg.get('type', 'resnet50') + # use_avgpool = cfg.get('network', {}).get('type') != 'attention' + pretrained = backbone_cfg.pop('pretrained', True) + + if 'fpn' in backbone_type: + backbone = build_fpn_backbone(backbone_cfg, pretrained=pretrained) + return backbone, backbone.get_output_dim() + elif 'hrnet' in backbone_type: + backbone = build_hr_net( + backbone_cfg, pretrained=True) + return backbone, backbone.get_output_dim() + elif 'resnet' in backbone_type: + resnet_cfg = backbone_cfg.get('resnet') + backbone = resnets[backbone_type]( + pretrained=True, **resnet_cfg) + return backbone, backbone.get_output_dim() + else: + msg = 'Unknown backbone type: {}'.format(backbone_type) + raise ValueError(msg) + +=== File: expose/models/backbone/resnet.py === + +-- Chunk 1 -- +// resnet.py:16-102 +class RegressionResNet(ResNet): + + def __init__(self, block, layers, forward_to=4, + num_classes=1000, + use_avgpool=True, + replace_stride_with_dilation=None, + zero_init_residual=False, **kwargs): + super(RegressionResNet, self).__init__( + block, layers, + replace_stride_with_dilation=replace_stride_with_dilation) + self.forward_to = forward_to + msg = 'Forward to must be from 0 to 4' + assert self.forward_to > 0 and self.forward_to <= 4, msg + + self.replace_stride_with_dilation = replace_stride_with_dilation + + self.expansion = block.expansion + self.output_dim = block.expansion * 512 + self.use_avgpool = use_avgpool + if not use_avgpool: + del self.avgpool + del self.fc + + def extra_repr(self): + if self.replace_stride_with_dilation is None: + msg = [ + f'Layer 1: {64 * self.expansion}, H / 4, W / 4', + f'Layer 2: {64 * self.expansion * 2}, H / 8, W / 8', + f'Layer 3: {64 * self.expansion * 4}, H / 16, W / 16', + f'Layer 4: {64 * self.expansion * 8}, H / 32, W / 32' + ] + else: + if not any(self.replace_stride_with_dilation): + msg = [ + f'Layer 1: {64 * self.expansion}, H / 4, W / 4', + f'Layer 2: {64 * self.expansion * 2}, H / 8, W / 8', + f'Layer 3: {64 * self.expansion * 4}, H / 16, W / 16', + f'Layer 4: {64 * self.expansion * 8}, H / 32, W / 32' + ] + else: + layer2 = 4 * 2 ** (not self.replace_stride_with_dilation[0]) + layer3 = (layer2 * + 2 ** (not self.replace_stride_with_dilation[1])) + layer4 = (layer3 * + 2 ** (not self.replace_stride_with_dilation[2])) + msg = [ + f'Layer 1: {64 * self.expansion}, H / 4, W / 4', + f'Layer 2: {64 * self.expansion * 2}, H / {layer2}, ' + f'W / {layer2}', + f'Layer 3: {64 * self.expansion * 4}, H / {layer3}, ' + f'W / {layer3}', + f'Layer 4: {64 * self.expansion * 8}, H / {layer4}, ' + f'W / {layer4}' + ] + + return '\n'.join(msg) + + def get_output_dim(self): + return { + 'layer1': 64 * self.expansion, + 'layer2': 64 * self.expansion * 2, + 'layer3': 64 * self.expansion * 4, + 'layer4': 64 * self.expansion * 8, + 'avg_pooling': 64 * self.expansion * 8, + } + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + output = {'maxpool': x} + + x = self.layer1(x) + output['layer1'] = x + x = self.layer2(x) + output['layer2'] = x + x = self.layer3(x) + output['layer3'] = x + x = self.layer4(x) + output['layer4'] = x + + # Output size: BxC + x = self.avgpool(x).view(x.size(0), -1) + output['avg_pooling'] = x + + return output + +-- Chunk 2 -- +// resnet.py:105-116 +def resnet18(pretrained=False, **kwargs): + """Constructs a ResNet-18 model. + + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = RegressionResNet(BasicBlock, [2, 2, 2, 2], **kwargs) + if pretrained: + logger.info('Loading pretrained ResNet-18') + model.load_state_dict(model_zoo.load_url(model_urls['resnet18']), + strict=False) + return model + +-- Chunk 3 -- +// resnet.py:119-130 +def resnet34(pretrained=False, **kwargs): + """Constructs a ResNet-34 model. + + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = RegressionResNet(BasicBlock, [3, 4, 6, 3], **kwargs) + if pretrained: + logger.info('Loading pretrained ResNet-34') + model.load_state_dict(model_zoo.load_url(model_urls['resnet34']), + strict=False) + return model + +-- Chunk 4 -- +// resnet.py:133-150 +def resnet50(pretrained=False, **kwargs): + """Constructs a ResNet-50 model. + + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = RegressionResNet(Bottleneck, [3, 4, 6, 3], **kwargs) + if pretrained: + logger.info('Loading pretrained ResNet-50') + missing, unexpected = model.load_state_dict( + model_zoo.load_url(model_urls['resnet50']), strict=False) + if len(missing) > 0: + logger.warning( + f'The following keys were not found: {missing}') + if len(unexpected): + logger.warning( + f'The following keys were not expected: {unexpected}') + return model + +-- Chunk 5 -- +// resnet.py:153-164 +def resnet101(pretrained=False, **kwargs): + """Constructs a ResNet-101 model. + + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = RegressionResNet(Bottleneck, [3, 4, 23, 3], **kwargs) + if pretrained: + logger.info('Loading pretrained ResNet-101') + model.load_state_dict(model_zoo.load_url(model_urls['resnet101']), + strict=False) + return model + +-- Chunk 6 -- +// resnet.py:167-178 +def resnet152(pretrained=False, **kwargs): + """Constructs a ResNet-152 model. + + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = RegressionResNet(Bottleneck, [3, 8, 36, 3], **kwargs) + if pretrained: + logger.info('Loading pretrained ResNet-152') + model.load_state_dict(model_zoo.load_url(model_urls['resnet152']), + strict=False) + return model + +=== File: expose/models/backbone/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/backbone/__init__.py:1-2 + +from .build import build_backbone + +=== File: expose/models/backbone/utils.py === + +-- Chunk 1 -- +// utils.py:9-25 +def make_conv_layer(input_dim, cfg): + num_layers = cfg.get('num_layers') + num_filters = cfg.num_filters + + expansion = resnet.Bottleneck.expansion + + layers = [] + for i in range(num_layers): + downsample = nn.Conv2d(input_dim, num_filters, stride=1, + kernel_size=1, bias=False) + + layers.append( + resnet.Bottleneck(input_dim, num_filters // expansion, + downsample=downsample) + ) + input_dim = num_filters + return nn.Sequential(*layers) + +-- Chunk 2 -- +// utils.py:28-46 +def make_subsample_layers(input_dim, cfg): + num_filters = cfg.get('num_filters') + strides = cfg.get('strides') + kernel_sizes = cfg.get('kernel_sizes') + + param_desc = zip(num_filters, kernel_sizes, strides) + layers = [] + for out_dim, kernel_size, stride in param_desc: + layers.append( + ConvNormActiv( + input_dim, + out_dim, + kernel_size=kernel_size, + stride=stride, + **cfg, + ) + ) + input_dim = out_dim + return nn.Sequential(*layers), out_dim + +=== File: expose/models/nnutils/init_layer.py === + +-- Chunk 1 -- +// init_layer.py:8-37 +def init_weights(layer, + name='', + init_type='xavier', distr='uniform', + gain=1.0, + activ_type='leaky-relu', lrelu_slope=0.2, **kwargs): + if len(name) < 1: + name = str(layer) + logger.debug('Initializing {} with {}_{}: gain={}', name, init_type, distr, + gain) + weights = layer.weight + if init_type == 'xavier': + if distr == 'uniform': + nninit.xavier_uniform_(weights, gain=gain) + elif distr == 'normal': + nninit.xavier_normal_(weights, gain=gain) + else: + raise ValueError( + 'Unknown distribution "{}" for Xavier init'.format(distr)) + elif init_type == 'kaiming': + + activ_type = activ_type.replace('-', '_') + if distr == 'uniform': + nninit.kaiming_uniform_(weights, a=lrelu_slope, + nonlinearity=activ_type) + elif distr == 'normal': + nninit.kaiming_normal_(weights, a=lrelu_slope, + nonlinearity=activ_type) + else: + raise ValueError( + 'Unknown distribution "{}" for Kaiming init'.format(distr)) + +=== File: expose/models/nnutils/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/nnutils/__init__.py:1-1 +from .init_layer import init_weights + +=== File: expose/models/attention/head_predictor.py === + +-- Chunk 1 -- +// head_predictor.py:51-200 +ss HeadPredictor(nn.Module): + + def __init__(self, exp_cfg, + global_orient_desc, + jaw_pose_desc, + camera_data, + detach_mean=False, + dtype=torch.float32): + super(HeadPredictor, self).__init__() + + network_cfg = exp_cfg.get('network', {}) + attention_net_cfg = network_cfg.get('attention', {}) + head_net_cfg = attention_net_cfg.get('head', {}) + + self.neck_index = KEYPOINT_NAMES.index('neck') + + head_model_cfg = exp_cfg.get('head_model', {}) + # model_path = osp.expandvars(head_model_cfg.pop('model_folder', '')) + model_type = head_model_cfg.pop('type', 'flame') + self.head_model_type = model_type + # self.head_model = build_layer( + # model_path, + # model_type=model_type, + # dtype=dtype, + # **head_model_cfg) + # logger.info(f'Head model: {self.head_model}') + + self.num_stages = head_net_cfg.get('num_stages', 3) + self.append_params = head_net_cfg.get('append_params', True) + + logger.info(f'Building head predictor with {self.num_stages} stages') + + camera_cfg = head_net_cfg.get('camera', {}) + camera_data = build_cam_proj(camera_cfg, dtype=dtype) + self.projection = camera_data['camera'] + + camera_param_dim = camera_data['dim'] + camera_mean = camera_data['mean'] + self.register_buffer('camera_mean', camera_mean) + self.camera_scale_func = camera_data['scale_func'] + + self.num_betas = head_model_cfg.num_betas + # self.num_betas = self.head_model.num_betas + shape_mean = torch.zeros([self.num_betas], dtype=dtype) + self.register_buffer('shape_mean', shape_mean) + + # self.num_expression_coeffs = self.head_model.num_expression_coeffs + self.num_expression_coeffs = head_model_cfg.num_expression_coeffs + expression_mean = torch.zeros( + [self.num_expression_coeffs], dtype=dtype) + self.register_buffer('expression_mean', expression_mean) + + self.global_orient_decoder = global_orient_desc.decoder + + cfg = {'param_type': global_orient_desc.decoder.get_type()} + self.neck_pose_decoder = build_pose_decoder(cfg, 1) + neck_pose_mean = self.neck_pose_decoder.get_mean().clone() + neck_pose_type = cfg['param_type'] + if neck_pose_type == 'aa': + neck_pose_mean[0] = math.pi + elif neck_pose_type == 'cont_rot_repr': + neck_pose_mean[3] = -1 + neck_pose_dim = self.neck_pose_decoder.get_dim_size() + self.register_buffer('neck_pose_mean', neck_pose_mean) + + self.jaw_pose_decoder = jaw_pose_desc.decoder + jaw_pose_mean = jaw_pose_desc.mean + jaw_pose_dim = jaw_pose_desc.dim + + mean_lst = [] + start = 0 + neck_pose_idxs = list(range(start, start + neck_pose_dim)) + self.register_buffer('neck_pose_idxs', + torch.tensor(neck_pose_idxs, dtype=torch.long)) + start += neck_pose_dim + mean_lst.append(neck_pose_mean.view(-1)) + + jaw_pose_idxs = list(range( + start, start + jaw_pose_dim)) + self.register_buffer( + 'jaw_pose_idxs', torch.tensor(jaw_pose_idxs, dtype=torch.long)) + start += jaw_pose_dim + mean_lst.append(jaw_pose_mean.view(-1)) + + shape_idxs = list(range(start, start + self.num_betas)) + self.register_buffer( + 'shape_idxs', torch.tensor(shape_idxs, dtype=torch.long)) + start += self.num_betas + mean_lst.append(shape_mean.view(-1)) + + expression_idxs = list(range( + start, start + self.num_expression_coeffs)) + self.register_buffer( + 'expression_idxs', + torch.tensor(expression_idxs, dtype=torch.long)) + start += self.num_expression_coeffs + mean_lst.append(expression_mean.view(-1)) + + camera_idxs = list(range( + start, start + camera_param_dim)) + self.register_buffer( + 'camera_idxs', torch.tensor(camera_idxs, dtype=torch.long)) + start += camera_param_dim + mean_lst.append(camera_mean) + + param_mean = torch.cat(mean_lst).view(1, -1) + param_dim = param_mean.numel() + self.param_dim = param_dim + + # Construct the feature extraction backbone + backbone_cfg = head_net_cfg.get('backbone', {}) + self.backbone, feat_dims = build_backbone(backbone_cfg) + + self.append_params = head_net_cfg.get('append_params', True) + self.num_stages = head_net_cfg.get('num_stages', 1) + + self.feature_key = head_net_cfg.get('feature_key', 'avg_pooling') + feat_dim = feat_dims[self.feature_key] + self.feat_dim = feat_dim + + regressor_cfg = head_net_cfg.get('mlp', {}) + regressor = MLP(feat_dim + self.append_params * param_dim, + param_dim, **regressor_cfg) + self.regressor = IterativeRegression( + regressor, param_mean, detach_mean=detach_mean, + num_stages=self.num_stages) + + def get_feat_dim(self) -> int: + ''' Returns the dimension of the expected feature vector ''' + return self.feat_dim + + def get_param_dim(self) -> int: + ''' Returns the dimension of the predicted parameter vector ''' + return self.param_dim + + def get_num_stages(self) -> int: + ''' Returns the number of stages for the iterative predictor''' + return self.num_stages + + def get_num_betas(self) -> int: + return self.num_betas + + def get_num_expression_coeffs(self) -> int: + return self.num_expression_coeffs + + def param_tensor_to_dict( + self, param_tensor: Tensor) -> Dict[str, Tensor]: + ''' Converts a flattened tensor to a dictionary of tensors ''' + neck_pose = torch.index_select(param_tensor, 1, + self.neck_pose_idxs) + +-- Chunk 2 -- +// head_predictor.py:201-327 + jaw_pose = torch.index_select(param_tensor, 1, self.jaw_pose_idxs) + + betas = torch.index_select(param_tensor, 1, self.shape_idxs) + expression = torch.index_select(param_tensor, 1, self.expression_idxs) + + return dict(neck_pose=neck_pose, + jaw_pose=jaw_pose, + expression=expression, + betas=betas) + + def get_camera_mean(self, batch_size: int = 1) -> Tensor: + ''' Returns the camera mean ''' + return self.camera_mean.reshape(1, -1).expand(batch_size, -1) + + def get_neck_pose_mean(self, batch_size=1) -> Tensor: + ''' Returns neck pose mean ''' + return self.neck_pose_mean.reshape(1, -1).expand(batch_size, -1) + + def get_jaw_pose_mean(self, batch_size=1) -> Tensor: + ''' Returns jaw pose mean ''' + return self.jaw_pose_mean.reshape(1, -1).expand(batch_size, -1) + + def get_shape_mean(self, batch_size=1) -> Tensor: + ''' Returns shape mean ''' + return self.shape_mean.reshape(1, -1).expand(batch_size, -1) + + def get_expression_mean(self, batch_size=1) -> Tensor: + ''' Returns expression mean ''' + return self.expression_mean.reshape(1, -1).expand(batch_size, -1) + + def get_param_mean(self, batch_size: int = 1, + add_shape_noise: bool = False, + shape_mean: Tensor = None, + shape_std: float = 0.0, + shape_prob: float = 0.0, + add_expression_noise: bool = False, + expression_mean: Tensor = None, + expression_std: float = 0.0, + expression_prob: float = 0.0, + add_jaw_pose_noise: bool = False, + jaw_noise_prob: float = 0.0, + jaw_pose_min: float = None, + jaw_pose_max: float = 1.0, + targets: object = None, + randomize_global_orient: bool = False, + global_rot_noise_prob: float = 0.0, + global_rot_min: float = 0.0, + global_rot_max: float = 0.0, + epsilon=1e-10, + ): + ''' Return the mean that will be given to the iterative regressor + ''' + mean = self.regressor.get_mean().clone().reshape(1, -1).expand( + batch_size, -1).clone() + if not self.training: + return mean + raise NotImplementedError + + def forward(self, + head_imgs: Tensor, + global_orient_from_body_net: Optional[Tensor] = None, + body_pose_from_body_net: Optional[Tensor] = None, + left_hand_pose_from_body_net: Optional[Tensor] = None, + right_hand_pose_from_body_net: Optional[Tensor] = None, + jaw_pose_from_body_net: Optional[Tensor] = None, + num_head_imgs: int = 0, + head_mean: Optional[Tensor] = None, + device: torch.device = None, + ) -> Dict[str, Dict[str, Tensor]]: + ''' + ''' + batch_size = head_imgs.shape[0] + device, dtype = head_imgs.device, head_imgs.dtype + + num_body_data = batch_size - num_head_imgs + if batch_size == 0: + return {} + + head_features = self.backbone(head_imgs) + head_parameters, head_deltas = self.regressor( + head_features[self.feature_key], + cond=head_mean) + + head_model_params = [] + model_parameters = [] + for stage_idx, parameters in enumerate(head_parameters): + parameters_dict = self.param_tensor_to_dict(parameters) + + dec_neck_pose_abs = self.neck_pose_decoder( + parameters_dict['neck_pose']) + dec_jaw_pose = self.jaw_pose_decoder(parameters_dict['jaw_pose']) + + model_betas = parameters_dict['betas'] + # Parameters that will be returned + model_parameters.append( + dict(head_pose=dec_neck_pose_abs, + raw_jaw_pose=parameters_dict['jaw_pose'], + jaw_pose=dec_jaw_pose, + betas=model_betas, + expression=parameters_dict['expression'], + ) + ) + + # Parameters used to pose the model + if self.head_model_type == 'flame': + head_model_params.append( + dict(global_orient=dec_neck_pose_abs, + jaw_pose=dec_jaw_pose, + betas=model_betas, + expression=parameters_dict['expression'], + ) + ) + else: + raise RuntimeError( + f'Invalid head model type: {self.head_model_type}') + + output = { + 'num_stages': self.num_stages, + 'features': head_features[self.feature_key], + } + + for stage in range(self.num_stages): + # Only update the current stage if there are enough params + key = f'stage_{stage:02d}' + output[key] = model_parameters[stage] + + return output + +=== File: expose/models/attention/build.py === + +-- Chunk 1 -- +// build.py:22-23 + build_attention_head(cfg): + return SMPLXHead(cfg) + +=== File: expose/models/attention/predictor.py === + +-- Chunk 1 -- +// predictor.py:71-220 +ss SMPLXHead(nn.Module): + + def __init__( + self, + exp_cfg: CfgNode, + dtype=torch.float32 + ) -> None: + super(SMPLXHead, self).__init__() + + network_cfg = exp_cfg.get('network', {}) + attention_net_cfg = network_cfg.get('attention', {}) + smplx_net_cfg = attention_net_cfg.get('smplx', {}) + + self.predict_body = network_cfg.get('predict_body', True) + self.apply_hand_network_on_body = network_cfg.get( + 'apply_hand_network_on_body', True) + self.apply_hand_network_on_hands = network_cfg.get( + 'apply_hand_network_on_hands', True) + self.predict_hands = (self.apply_hand_network_on_body or + self.apply_hand_network_on_hands) + logger.warning( + f'Apply hand network on body: {self.apply_hand_network_on_body}') + logger.warning( + f'Apply hand network on hands: {self.apply_hand_network_on_hands}') + logger.warning(f'Predict hands: {self.predict_hands}') + self.apply_head_network_on_body = network_cfg.get( + 'apply_head_network_on_body', True) + self.apply_head_network_on_head = network_cfg.get( + 'apply_head_network_on_head', True) + self.predict_head = (self.apply_head_network_on_body or + self.apply_head_network_on_head) + logger.warning(f'Predict head: {self.predict_head}') + + self.detach_mean = attention_net_cfg.get('detach_mean', False) + + condition_hand_on_body = attention_net_cfg.get( + 'condition_hand_on_body', {}) + self.condition_hand_on_body = any(condition_hand_on_body.values()) + logger.info(f'Condition hand on body: {self.condition_hand_on_body}') + self.condition_hand_wrist_pose = condition_hand_on_body.get( + 'wrist_pose', True) + logger.info( + 'Condition hand wrist pose on body: ' + f'{self.condition_hand_wrist_pose}') + self.condition_hand_finger_pose = condition_hand_on_body.get( + 'finger_pose', True) + logger.info( + 'Condition hand finger pose on body: ' + f'{self.condition_hand_finger_pose}') + self.condition_hand_shape = condition_hand_on_body.get('shape', True) + logger.info( + f'Condition hand shape on body shape: {self.condition_hand_shape}') + + self.hand_add_shape_noise = network_cfg.get( + 'hand_add_shape_noise', False) + self.hand_shape_std = network_cfg.get('hand_shape_std', 0.0) + self.hand_shape_prob = network_cfg.get('hand_shape_prob', 0.0) + logger.debug( + 'Add shape noise: {} from N(0, {}), with prob {}', + self.hand_add_shape_noise, + self.hand_shape_std, + self.hand_shape_prob, + ) + + self.add_hand_pose_noise = network_cfg.get( + 'add_hand_pose_noise', False) + self.hand_pose_std = network_cfg.get('hand_pose_std', 0.0) + self.num_hand_components = network_cfg.get( + 'num_hand_components', 3) + self.hand_noise_prob = network_cfg.get('hand_noise_prob', 0.0) + logger.debug( + 'Add hand pose noise to {}: {} from N(0, {}) with prob {}', + self.num_hand_components, + self.add_hand_pose_noise, self.hand_pose_std, + self.hand_noise_prob,) + + self.hand_randomize_global_orient = network_cfg.get( + 'hand_randomize_global_orient', False) + self.hand_global_rot_max = network_cfg.get('hand_global_rot_max', 0.0) + self.hand_global_rot_min = network_cfg.get('hand_global_rot_min', 0.0) + self.hand_global_rot_noise_prob = network_cfg.get( + 'hand_global_rot_noise_prob', 0.0) + logger.debug('Randomize global pose: {} from U({}, {})', + self.hand_randomize_global_orient, + self.hand_global_rot_min, self.hand_global_rot_max) + + condition_head_on_body = attention_net_cfg.get( + 'condition_head_on_body', {}) + self.condition_head_on_body = any(condition_head_on_body.values()) + + self.condition_head_neck_pose = condition_head_on_body.get( + 'neck_pose', True) + self.condition_head_jaw_pose = condition_head_on_body.get( + 'jaw_pose', True) + self.condition_head_shape = condition_head_on_body.get( + 'shape', True) + self.condition_head_expression = condition_head_on_body.get( + 'expression', True) + logger.info(f'Condition head on body: {self.condition_head_on_body}') + logger.info( + f'Condition expression on body: {self.condition_head_expression}') + logger.info(f'Condition shape on body: {self.condition_head_shape}') + logger.info( + f'Condition neck pose on body: {self.condition_head_neck_pose}') + logger.info( + f'Condition jaw pose on body: {self.condition_head_jaw_pose}') + + self.head_add_shape_noise = network_cfg.get( + 'head_add_shape_noise', False) + self.head_shape_std = network_cfg.get('head_shape_std', 1.0) + self.head_shape_prob = network_cfg.get('head_shape_prob', 0.0) + logger.debug( + 'Add head shape noise: {} from N(0, {}), with prob {}', + self.head_add_shape_noise, + self.head_shape_std, + self.head_shape_prob, + ) + + self.add_expression_noise = network_cfg.get( + 'add_expression_noise', False) + self.expression_std = network_cfg.get('expression_std', None) + self.expression_prob = network_cfg.get('expression_prob', 1.0) + logger.debug( + 'Add expression noise: {} from N(0, {}), with prob {}', + self.add_expression_noise, + self.expression_std, + self.expression_prob, + ) + + self.add_jaw_pose_noise = network_cfg.get('add_jaw_pose_noise', False) + self.jaw_pose_min = network_cfg.get('jaw_pose_min', 0.0) + self.jaw_pose_max = network_cfg.get('jaw_pose_max', 0.0) + self.jaw_noise_prob = network_cfg.get('jaw_noise_prob', 1.0) + logger.debug( + 'Sampling random X-axis jaw rotation from U({}, {}) with prob {}', + self.jaw_pose_min, self.jaw_pose_max, self.jaw_noise_prob) + + self.head_randomize_global_orient = network_cfg.get( + 'head_randomize_global_orient', False) + self.head_global_rot_min = network_cfg.get('head_global_rot_min', 0.0) + self.head_global_rot_max = network_cfg.get('head_global_rot_max', 0.0) + self.head_global_rot_noise_prob = network_cfg.get( + 'head_global_rot_noise_prob', 1.0) + logger.debug( + 'Randomize head global pose: {} from U({}, {}) with prob {}', + self.head_randomize_global_orient, self.head_global_rot_min, + self.head_global_rot_max, self.head_global_rot_noise_prob, + ) + + body_model_cfg = exp_cfg.get('body_model', {}) + +-- Chunk 2 -- +// predictor.py:221-370 + body_use_face_contour = body_model_cfg.get('use_face_contour', True) + + self.refine_shape_from_hands = attention_net_cfg.get( + 'refine_shape_from_hands', False) + logger.debug( + f'Refine shape from hands: {self.refine_shape_from_hands}') + self.refine_shape_from_head = attention_net_cfg.get( + 'refine_shape_from_head', False) + logger.debug(f'Refine shape from head: {self.refine_shape_from_head}') + + self.hand_bbox_thresh = attention_net_cfg.get('hand_bbox_thresh', 0.4) + logger.debug( + f'Hand bounding box IoU threshold: {self.hand_bbox_thresh}') + self.head_bbox_thresh = attention_net_cfg.get('head_bbox_thresh', 0.4) + logger.debug( + f'Head bounding box IoU threshold: {self.head_bbox_thresh}') + + self.num_stages = smplx_net_cfg.get('num_stages', 3) + self.append_params = smplx_net_cfg.get('append_params', True) + + self.pose_last_stage = smplx_net_cfg.get('pose_last_stage', False) + + self.body_model_cfg = body_model_cfg.copy() + + model_path = osp.expandvars(body_model_cfg.pop('model_folder', '')) + model_type = body_model_cfg.pop('type', 'smplx') + self.body_model = build_body_model( + model_path, + model_type=model_type, + dtype=dtype, + **body_model_cfg) + logger.info(f'Body model: {self.body_model}') + + # The number of shape coefficients + num_betas = body_model_cfg.num_betas + self.num_betas = num_betas + + shape_mean_path = body_model_cfg.get('shape_mean_path', '') + shape_mean_path = osp.expandvars(shape_mean_path) + if osp.exists(shape_mean_path): + shape_mean = torch.from_numpy( + np.load(shape_mean_path, allow_pickle=True)).to( + dtype=dtype).reshape(1, -1)[:, :num_betas].reshape(-1) + else: + shape_mean = torch.zeros([num_betas], dtype=dtype) + + # The number of expression coefficients + num_expression_coeffs = body_model_cfg.num_expression_coeffs + self.num_expression_coeffs = num_expression_coeffs + expression_mean = torch.zeros( + [num_expression_coeffs], dtype=dtype) + + # Build the pose parameterization for all the parameters + pose_desc_dict = build_all_pose_params( + body_model_cfg, 0, self.body_model, + append_params=self.append_params, dtype=dtype) + + self.global_orient_decoder = pose_desc_dict['global_orient'].decoder + global_orient_mean = pose_desc_dict['global_orient'].mean + + global_orient_type = body_model_cfg.get('global_orient', {}).get( + 'param_type', 'cont_rot_repr') + # Rotate the model 180 degrees around the x-axis + if global_orient_type == 'aa': + global_orient_mean[0] = math.pi + elif global_orient_type == 'cont_rot_repr': + global_orient_mean[3] = -1 + global_orient_dim = pose_desc_dict['global_orient'].dim + + self.body_pose_decoder = pose_desc_dict['body_pose'].decoder + body_pose_mean = pose_desc_dict['body_pose'].mean + body_pose_dim = pose_desc_dict['body_pose'].dim + + self.left_hand_pose_decoder = pose_desc_dict['left_hand_pose'].decoder + left_hand_pose_mean = pose_desc_dict['left_hand_pose'].mean + left_hand_pose_dim = pose_desc_dict['left_hand_pose'].dim + left_hand_pose_ind_dim = pose_desc_dict['left_hand_pose'].ind_dim + + self.right_hand_pose_decoder = pose_desc_dict[ + 'right_hand_pose'].decoder + right_hand_pose_mean = pose_desc_dict['right_hand_pose'].mean + right_hand_pose_dim = pose_desc_dict['right_hand_pose'].dim + right_hand_pose_ind_dim = pose_desc_dict['right_hand_pose'].ind_dim + + self.jaw_pose_decoder = pose_desc_dict['jaw_pose'].decoder + jaw_pose_mean = pose_desc_dict['jaw_pose'].mean + jaw_pose_dim = pose_desc_dict['jaw_pose'].dim + + mean_lst = [] + + start = 0 + global_orient_idxs = list(range(start, start + global_orient_dim)) + + global_orient_idxs = torch.tensor(global_orient_idxs, dtype=torch.long) + self.register_buffer('global_orient_idxs', global_orient_idxs) + start += global_orient_dim + mean_lst.append(global_orient_mean.view(-1)) + + body_pose_idxs = list(range( + start, start + body_pose_dim)) + self.register_buffer( + 'body_pose_idxs', torch.tensor(body_pose_idxs, dtype=torch.long)) + start += body_pose_dim + mean_lst.append(body_pose_mean.view(-1)) + + left_hand_pose_idxs = list(range(start, start + left_hand_pose_dim)) + self.register_buffer( + 'left_hand_pose_idxs', + torch.tensor(left_hand_pose_idxs, dtype=torch.long)) + start += left_hand_pose_dim + mean_lst.append(left_hand_pose_mean.view(-1)) + + right_hand_pose_idxs = list(range( + start, start + right_hand_pose_dim)) + self.register_buffer( + 'right_hand_pose_idxs', + torch.tensor(right_hand_pose_idxs, dtype=torch.long)) + start += right_hand_pose_dim + mean_lst.append(right_hand_pose_mean.view(-1)) + + jaw_pose_idxs = list(range( + start, start + jaw_pose_dim)) + self.register_buffer( + 'jaw_pose_idxs', torch.tensor(jaw_pose_idxs, dtype=torch.long)) + start += jaw_pose_dim + mean_lst.append(jaw_pose_mean.view(-1)) + + shape_idxs = list(range(start, start + num_betas)) + self.register_buffer( + 'shape_idxs', torch.tensor(shape_idxs, dtype=torch.long)) + start += num_betas + mean_lst.append(shape_mean.view(-1)) + + expression_idxs = list(range( + start, start + num_expression_coeffs)) + self.register_buffer( + 'expression_idxs', torch.tensor(expression_idxs, dtype=torch.long)) + start += num_expression_coeffs + mean_lst.append(expression_mean.view(-1)) + + camera_cfg = smplx_net_cfg.get('camera', {}) + camera_data = build_cam_proj(camera_cfg, dtype=dtype) + self.projection = camera_data['camera'] + + camera_param_dim = camera_data['dim'] + camera_mean = camera_data['mean'] + # self.camera_mean = camera_mean + self.register_buffer('camera_mean', camera_mean) + self.camera_scale_func = camera_data['scale_func'] + + +-- Chunk 3 -- +// predictor.py:371-520 + camera_idxs = list(range( + start, start + camera_param_dim)) + self.register_buffer( + 'camera_idxs', torch.tensor(camera_idxs, dtype=torch.long)) + start += camera_param_dim + mean_lst.append(camera_mean) + + param_mean = torch.cat(mean_lst).view(1, -1) + param_dim = param_mean.numel() + + # Construct the feature extraction backbone + backbone_cfg = smplx_net_cfg.get('backbone', {}) + self.backbone, feat_dims = build_backbone(backbone_cfg) + + self.append_params = smplx_net_cfg.get('append_params', True) + self.num_stages = smplx_net_cfg.get('num_stages', 1) + + self.body_feature_key = smplx_net_cfg.get('feature_key', 'avg_pooling') + feat_dim = feat_dims[self.body_feature_key] + + regressor_cfg = smplx_net_cfg.get('mlp', {}) + regressor = MLP(feat_dim + self.append_params * param_dim, + param_dim, **regressor_cfg) + self.regressor = IterativeRegression( + regressor, param_mean, num_stages=self.num_stages) + + self.update_wrists = attention_net_cfg.get('update_wrists', True) + # Find the kinematic chain for the right wrist + right_wrist_idx = KEYPOINT_NAMES.index('right_wrist') + self.right_wrist_idx = right_wrist_idx + left_wrist_idx = KEYPOINT_NAMES.index('left_wrist') + self.left_wrist_idx = left_wrist_idx + + self.hand_predictor = HandPredictor( + exp_cfg, + pose_desc_dict['global_orient'], + pose_desc_dict['right_hand_pose'], + camera_data, + detach_mean=self.detach_mean, + mean_pose_path=body_model_cfg.mean_pose_path, + dtype=dtype) + + hand_crop_size = exp_cfg.get('datasets', {}).get('hand', {}).get( + 'transforms', {}).get('crop_size', 256) + self.hand_scale_factor = attention_net_cfg.get('hand', {}).get( + 'scale_factor', 2.0) + self.hand_crop_size = hand_crop_size + self.hand_cropper = CropSampler(hand_crop_size) + + head_crop_size = exp_cfg.get('datasets', {}).get('head', {}).get( + 'transforms', {}).get('crop_size', 256) + self.head_crop_size = head_crop_size + self.head_scale_factor = network_cfg.get('head', {}).get( + 'scale_factor', 2.0) + self.head_cropper = CropSampler(head_crop_size) + + self.head_predictor = HeadPredictor( + exp_cfg, + pose_desc_dict['global_orient'], + pose_desc_dict['jaw_pose'], camera_data, + detach_mean=self.detach_mean, + dtype=dtype) + self.points_to_crops = ToCrops() + + right_wrist_kin_chain = find_joint_kin_chain( + right_wrist_idx, + self.body_model.parents) + right_wrist_kin_chain = torch.tensor( + right_wrist_kin_chain, dtype=torch.long) + self.register_buffer('right_wrist_kin_chain', right_wrist_kin_chain) + + self.register_buffer( + 'abs_pose_mean', + self.global_orient_decoder.get_mean().unsqueeze(dim=0)) + + # Find the kinematic chain for the left wrist + left_wrist_kin_chain = find_joint_kin_chain( + left_wrist_idx, + self.body_model.parents) + left_wrist_kin_chain = torch.tensor( + left_wrist_kin_chain, dtype=torch.long) + self.register_buffer('left_wrist_kin_chain', left_wrist_kin_chain) + + # Find the kinematic chain for the neck + neck_idx = KEYPOINT_NAMES.index('neck') + neck_kin_chain = find_joint_kin_chain( + neck_idx, + self.body_model.parents) + self.register_buffer('neck_kin_chain', + torch.tensor(neck_kin_chain, dtype=torch.long)) + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + head_idxs = idxs_dict['head'] + if not body_use_face_contour: + head_idxs = head_idxs[:-17] + + self.register_buffer('body_idxs', torch.tensor(body_idxs)) + self.register_buffer('left_hand_idxs', torch.tensor(left_hand_idxs)) + self.register_buffer('right_hand_idxs', torch.tensor(right_hand_idxs)) + self.register_buffer('head_idxs', torch.tensor(head_idxs)) + + self.keyp_loss = KeypointLoss(exp_cfg) + + self.mask_hand_keyps = attention_net_cfg.get('mask_hand_keyps', True) + self.mask_head_keyps = attention_net_cfg.get('mask_head_keyps', True) + + loss_cfg = exp_cfg.get('losses', {}) + # Create a loss to apply on the keypoints from the head crop + head_crop_keypoint_loss_cfg = loss_cfg.get('head_crop_keypoints') + self.head_crop_keyps_weight = head_crop_keypoint_loss_cfg.get( + 'weight', 0.0) + self.head_crop_keyps_enable_at = head_crop_keypoint_loss_cfg.get( + 'enable', True) + if self.head_crop_keyps_weight > 0: + self.head_crop_keyps_loss = build_loss( + **head_crop_keypoint_loss_cfg) + logger.info( + '2D Head crop keyps loss: {}', self.head_crop_keyps_loss) + + left_hand_crop_keypoint_loss_cfg = loss_cfg.get( + 'left_hand_crop_keypoints') + self.left_hand_crop_keyps_weight = ( + left_hand_crop_keypoint_loss_cfg.get('weight', 0.0)) + self.left_hand_crop_keyps_enable_at = ( + left_hand_crop_keypoint_loss_cfg.get('enable', True)) + if self.left_hand_crop_keyps_weight > 0: + self.left_hand_crop_keyps_loss = build_loss( + **left_hand_crop_keypoint_loss_cfg) + logger.info( + '2D Left hand crop keyps loss: {}', + self.left_hand_crop_keyps_loss) + + right_hand_crop_keypoint_loss_cfg = loss_cfg.get( + 'right_hand_crop_keypoints') + self.right_hand_crop_keyps_weight = ( + right_hand_crop_keypoint_loss_cfg.get('weight', 0.0)) + self.right_hand_crop_keyps_enable_at = ( + right_hand_crop_keypoint_loss_cfg.get('enable', True)) + if self.right_hand_crop_keyps_weight > 0: + self.right_hand_crop_keyps_loss = build_loss( + **right_hand_crop_keypoint_loss_cfg) + logger.info( + '2D Left hand crop keyps loss: {}', + self.right_hand_crop_keyps_loss) + + self.body_loss = SMPLXLossModule( + loss_cfg, + +-- Chunk 4 -- +// predictor.py:521-670 + use_face_contour=body_use_face_contour) + self.body_regularizer = RegularizerModule( + loss_cfg, body_pose_mean=body_pose_mean, + left_hand_pose_mean=left_hand_pose_mean, + right_hand_pose_mean=right_hand_pose_mean, + jaw_pose_mean=jaw_pose_mean + ) + self.hand_loss = MANOLossModule(loss_cfg.get('hand', {})) + self.hand_regularizer = MANORegularizer(loss_cfg.get('hand', {})) + self.head_loss = FLAMELossModule( + loss_cfg.get('head', {}), use_face_contour=body_use_face_contour) + self.head_regularizer = FLAMERegularizer(loss_cfg.get('head', {})) + + self.freeze_body = attention_net_cfg.get('freeze_body', False) + if self.freeze_body: + for param in self.backbone.parameters(): + param.requires_grad = False + for param in self.regressor.parameters(): + param.requires_grad = False + # Stop updating batch norm statistics + self.backbone = FrozenBatchNorm2d.convert_frozen_batchnorm( + self.backbone) + self.regressor = FrozenBatchNorm2d.convert_frozen_batchnorm( + self.regressor) + + # Build part merging functions + hand_feat_dim = self.hand_predictor.get_feat_dim() + head_feat_dim = self.head_predictor.get_feat_dim() + # Right hand pose + merging_cfg = attention_net_cfg.get('merging', {}) + self.right_hand_pose_merging_func = self._build_merge_func( + merging_cfg, + 'right_hand_pose', + body_feat_dim=feat_dim, + body_param_dim=right_hand_pose_dim, + part_feat_dim=hand_feat_dim, + part_param_dim=right_hand_pose_dim, + ) + # Right wrist pose + if self.update_wrists: + self.right_wrist_pose_merging_func = self._build_merge_func( + merging_cfg, + 'right_wrist_pose', + body_feat_dim=feat_dim, + body_param_dim=right_hand_pose_ind_dim, + part_feat_dim=hand_feat_dim, + part_param_dim=right_hand_pose_ind_dim, + ) + # Left hand pose + self.left_hand_pose_merging_func = self._build_merge_func( + merging_cfg, + 'left_hand_pose', + body_feat_dim=feat_dim, + body_param_dim=left_hand_pose_dim, + part_feat_dim=hand_feat_dim, + part_param_dim=left_hand_pose_dim, + ) + # Left wrist pose + if self.update_wrists: + self.left_wrist_pose_merging_func = self._build_merge_func( + merging_cfg, + 'left_wrist_pose', + body_feat_dim=feat_dim, + body_param_dim=left_hand_pose_ind_dim, + part_feat_dim=hand_feat_dim, + part_param_dim=left_hand_pose_ind_dim, + ) + + # Jaw pose + self.jaw_pose_merging_func = self._build_merge_func( + merging_cfg, + 'jaw_pose', + body_feat_dim=feat_dim, + body_param_dim=jaw_pose_dim, + part_feat_dim=head_feat_dim, + part_param_dim=jaw_pose_dim, + ) + # Expression + self.expression_merging_func = self._build_merge_func( + merging_cfg, + 'expression', + body_feat_dim=feat_dim, + body_param_dim=num_expression_coeffs, + part_feat_dim=head_feat_dim, + part_param_dim=num_expression_coeffs, + ) + + hand_soft_weight_loss_cfg = loss_cfg.get('hand_soft_weight_loss', {}) + self.hand_soft_weight_loss = build_loss(**hand_soft_weight_loss_cfg) + self.hand_soft_weight_loss_weight = hand_soft_weight_loss_cfg.get( + 'weight', 0.0) + + head_soft_weight_loss_cfg = loss_cfg.get('head_soft_weight_loss', {}) + self.head_soft_weight_loss = build_loss(**head_soft_weight_loss_cfg) + self.head_soft_weight_loss_weight = head_soft_weight_loss_cfg.get( + 'weight', 0.0) + + def _build_merge_func( + self, cfg: CfgNode, + name: str, + body_feat_dim: int, body_param_dim: int, + part_feat_dim: int, part_param_dim: int, + ) -> Callable: + merge_type = cfg.get(name, {}).get('type', 'simple') + logger.debug(f'Building "{merge_type}" merging function for "{name}"') + if merge_type == 'none': + pass + elif merge_type == 'simple': + def func( + from_body: Tensor, from_part: Tensor, + body_feat: Optional[Tensor] = None, + part_feat: Optional[Tensor] = None, + mask: Optional[Tensor] = None + ) -> Dict[str, Tensor]: + output = {} + if self.training: + # During training, if a mask + output['merged'] = ( + torch.where( + mask, from_part, from_body) if mask is not None + else from_part + ) + else: + output['merged'] = from_part + output['weights'] = None + return output + return func + else: + raise ValueError(f'Merge function {merge_type} is not supported') + + def toggle_losses(self, iteration): + self.body_loss.toggle_losses(iteration) + self.keyp_loss.toggle_losses(iteration) + + def toggle_param_prediction(self, iteration): + pass + + def flat_body_params_to_dict(self, param_tensor): + global_orient = torch.index_select( + param_tensor, 1, self.global_orient_idxs) + body_pose = torch.index_select( + param_tensor, 1, self.body_pose_idxs) + left_hand_pose = torch.index_select( + param_tensor, 1, self.left_hand_pose_idxs) + right_hand_pose = torch.index_select( + param_tensor, 1, self.right_hand_pose_idxs) + jaw_pose = torch.index_select( + param_tensor, 1, self.jaw_pose_idxs) + betas = torch.index_select(param_tensor, 1, self.shape_idxs) + expression = torch.index_select(param_tensor, 1, self.expression_idxs) + +-- Chunk 5 -- +// predictor.py:671-820 + + return { + 'betas': betas, + 'expression': expression, + 'global_orient': global_orient, + 'body_pose': body_pose, + 'left_hand_pose': left_hand_pose, + 'right_hand_pose': right_hand_pose, + 'jaw_pose': jaw_pose, + } + + def find_joint_global_rotation( + self, + kin_chain: Tensor, + root_pose: Tensor, + body_pose: Tensor + ) -> Tensor: + ''' Computes the absolute rotation of a joint from the kinematic chain + ''' + # Create a single vector with all the poses + parents_pose = torch.cat( + [root_pose, body_pose], dim=1)[:, kin_chain] + output_pose = parents_pose[:, 0] + for idx in range(1, parents_pose.shape[1]): + output_pose = torch.bmm( + parents_pose[:, idx], output_pose) + return output_pose + + def build_hand_mean(self, global_orient: Tensor, + body_pose: Tensor, + betas: Tensor, + flipped_left_hand_pose: Tensor, + right_hand_pose: Tensor, + hand_targets: List, + num_body_imgs: int = 0, + num_hand_imgs: int = 0 + ) -> Tuple[Tensor, Tensor]: + ''' Builds the initial point for the iterative regressor of the hand + ''' + device, dtype = global_orient.device, global_orient.dtype + hand_only_mean, parent_rots = [], [] + if num_body_imgs > 0: + batch_size = num_body_imgs + # Compute the absolute pose of the right wrist + right_wrist_pose_abs = self.find_joint_global_rotation( + self.right_wrist_kin_chain, global_orient, + body_pose) + + right_wrist_parent_rot = self.find_joint_global_rotation( + self.right_wrist_kin_chain[1:], global_orient, + body_pose) + + left_wrist_parent_rot = self.find_joint_global_rotation( + self.left_wrist_kin_chain[1:], global_orient, body_pose) + left_to_right_wrist_parent_rot = flip_pose( + left_wrist_parent_rot, pose_format='rot-mat') + + parent_rots += [ + right_wrist_parent_rot, left_to_right_wrist_parent_rot] + + # if self.condition_hand_on_body: + # Convert the absolute pose to the latent representation + if self.condition_hand_wrist_pose: + right_wrist_pose = self.global_orient_decoder.encode( + right_wrist_pose_abs.unsqueeze(dim=1)).reshape( + batch_size, -1) + + # Compute the absolute rotation for the left wrist + left_wrist_pose_abs = self.find_joint_global_rotation( + self.left_wrist_kin_chain, global_orient, body_pose) + # Flip the left wrist to the right + left_to_right_wrist_pose = flip_pose( + left_wrist_pose_abs, pose_format='rot-mat') + # Convert to the latent representation + left_to_right_wrist_pose = self.global_orient_decoder.encode( + left_to_right_wrist_pose.unsqueeze(dim=1)).reshape( + batch_size, -1) + else: + right_wrist_pose = self.hand_predictor.get_wrist_pose_mean( + batch_size=batch_size) + left_to_right_wrist_pose = ( + self.hand_predictor.get_wrist_pose_mean( + batch_size=batch_size)) + + # Convert the pose of the left hand to the right hand and project + # it to the encoder space + left_to_right_hand_pose = self.right_hand_pose_decoder.encode( + flipped_left_hand_pose).reshape(batch_size, -1) + + camera_mean = self.hand_predictor.get_camera_mean().expand( + batch_size, -1) + + shape_condition = ( + betas if self.condition_hand_shape else + self.hand_predictor.get_shape_mean(batch_size) + ) + right_finger_pose_condition = ( + right_hand_pose if self.condition_hand_finger_pose else + self.hand_predictor.get_finger_pose_mean(batch_size) + ) + right_hand_mean = torch.cat( + [ + right_wrist_pose, right_finger_pose_condition, + shape_condition, camera_mean, + ], dim=1) + left_finger_pose_condition = ( + left_to_right_hand_pose if self.condition_hand_finger_pose else + self.hand_predictor.get_finger_pose_mean(batch_size) + ) + # Should be Bx31 + left_hand_mean = torch.cat( + [ + left_to_right_wrist_pose, + left_finger_pose_condition, + shape_condition, + camera_mean, + ], dim=1 + ) + hand_only_mean += [right_hand_mean, left_hand_mean] + + if num_hand_imgs > 0: + mean_param = self.hand_predictor.get_param_mean( + batch_size=num_hand_imgs, + add_shape_noise=self.hand_add_shape_noise, + shape_std=self.hand_shape_std, + shape_prob=self.hand_shape_prob, + num_hand_components=self.num_hand_components, + add_hand_pose_noise=self.add_hand_pose_noise, + hand_pose_std=self.hand_pose_std, + hand_noise_prob=self.hand_noise_prob, + targets=hand_targets, + randomize_global_orient=self.hand_randomize_global_orient, + global_rot_min=self.hand_global_rot_min, + global_rot_max=self.hand_global_rot_max, + global_rot_noise_prob=self.hand_global_rot_noise_prob, + ) + + hand_only_mean.append(mean_param) + hand_only_parent_rots = torch.eye( + 3, device=device, dtype=dtype).reshape( + 1, 3, 3).expand(num_hand_imgs, -1, -1).clone() + hand_only_parent_rots[:, 1, 1] = -1 + hand_only_parent_rots[:, 2, 2] = -1 + parent_rots.append(hand_only_parent_rots) + + hand_only_mean = torch.cat(hand_only_mean, dim=0) + parent_rots = torch.cat(parent_rots, dim=0) + return hand_only_mean, parent_rots + + def build_head_mean( + +-- Chunk 6 -- +// predictor.py:821-970 + self, + global_orient: Tensor, + body_pose: Tensor, + betas: Tensor, + expression: Tensor, + jaw_pose: Tensor, + head_targets: List, + num_body_imgs: int = 0, + num_head_imgs: int = 0 + ) -> Tensor: + ''' Builds the initial point of the head regressor + ''' + head_only_mean = [] + if num_body_imgs > 0: + batch_size = num_body_imgs + + # Compute the absolute pose of the right wrist + neck_pose_abs = self.find_joint_global_rotation( + self.neck_kin_chain, global_orient, body_pose) + # Convert the absolute neck pose to offsets + neck_latent = self.global_orient_decoder.encode( + neck_pose_abs.unsqueeze(dim=1)) + neck_pose = neck_latent.reshape(batch_size, -1) + + camera_mean = self.head_predictor.get_camera_mean( + batch_size=batch_size) + + neck_pose_condition = ( + neck_pose if self.condition_head_neck_pose else + self.head_predictor.get_neck_pose_mean(batch_size)) + jaw_pose_condition = ( + jaw_pose.reshape(batch_size, -1) + if self.condition_head_jaw_pose else + self.head_predictor.get_jaw_pose_mean(batch_size) + ) + head_num_betas = self.head_predictor.get_num_betas() + shape_padding_size = head_num_betas - self.num_betas + betas_condition = ( + F.pad(betas.reshape(batch_size, -1), (0, shape_padding_size)) + if self.condition_head_shape else + self.head_predictor.get_shape_mean(batch_size=batch_size) + ) + + head_num_expression_coeffs = ( + self.head_predictor.get_num_expression_coeffs()) + expr_padding_size = (head_num_expression_coeffs - + self.num_expression_coeffs) + expression_condition = ( + F.pad( + expression.reshape(batch_size, -1), (0, expr_padding_size)) + if self.condition_head_expression else + self.head_predictor.get_expression_mean(batch_size=batch_size) + ) + + # Should be Bx(Head pose params) + head_only_mean.append(torch.cat( + [neck_pose_condition, jaw_pose_condition, + betas_condition, expression_condition, + camera_mean.reshape(batch_size, -1), + ], dim=1 + )) + + if num_head_imgs > 0: + mean_param = self.head_predictor.get_param_mean( + batch_size=num_head_imgs, + add_shape_noise=self.head_add_shape_noise, + shape_std=self.head_shape_std, + shape_prob=self.head_shape_prob, + expression_prob=self.expression_prob, + add_expression_noise=self.add_expression_noise, + expression_std=self.expression_std, + add_jaw_pose_noise=self.add_jaw_pose_noise, + jaw_noise_prob=self.jaw_noise_prob, + jaw_pose_min=self.jaw_pose_min, + jaw_pose_max=self.jaw_pose_max, + randomize_global_orient=self.head_randomize_global_orient, + global_rot_noise_prob=self.head_global_rot_noise_prob, + global_rot_min=self.head_global_rot_min, + global_rot_max=self.head_global_rot_max, + targets=head_targets, + ) + head_only_mean.append(mean_param) + + head_only_mean = torch.cat(head_only_mean, dim=0) + return head_only_mean + + def get_hand_model(self) -> nn.Module: + ''' Return the hand predictor ''' + return self.hand_predictor + + def get_head_model(self) -> nn.Module: + ''' Return the head predictor ''' + return self.head_predictor + + @torch.no_grad() + def bboxes_to_mask( + self, + targets: List, + key: str, + est_center: Tensor, est_bbox_size: Tensor, + thresh: float = 0.0) -> Tensor: + ''' Converts bounding boxes to a binary mask ''' + if thresh <= 0: + return torch.ones([len(targets), 1], dtype=torch.bool, + device=est_center.device) + + ious = torch.zeros(len(targets), dtype=est_center.dtype, + device=est_center.device) + gt_idxs = [] + gt_bboxes = [] + for ii, t in enumerate(targets): + if not t.has_field(key): + continue + gt_idxs.append(ii) + bbox_field = t.get_field(key) + gt_bboxes.append(bbox_field.bbox) + + if len(gt_bboxes) < 1: + return ious.unsqueeze(dim=-1).to(dtype=torch.bool) + est_bboxes = center_size_to_bbox(est_center, est_bbox_size) + gt_bboxes = torch.stack(gt_bboxes).to(dtype=est_bboxes.dtype) + gt_idxs = torch.tensor( + gt_idxs, dtype=torch.long, device=est_bboxes.device) + ious[gt_idxs] = bbox_iou(gt_bboxes, est_bboxes[gt_idxs]) + + return ious.ge(thresh).unsqueeze(dim=-1) + + def forward(self, + images: Tensor, + targets: List = None, + hand_imgs: Optional[Tensor] = None, + hand_targets: Optional[List] = None, + head_imgs: Optional[Tensor] = None, + head_targets: Optional[List] = None, + full_imgs: Optional[Union[ImageList, ImageListPacked]] = None, + ) -> Dict[str, Dict[str, Tensor]]: + ''' Forward pass of the attention predictor + ''' + batch_size, _, crop_size, _ = images.shape + device = images.device + dtype = images.dtype + + feat_dict = self.backbone(images) + body_features = feat_dict[self.body_feature_key] + + body_parameters, body_deltas = self.regressor(body_features) + + losses = {} + # A list of dicts for the parameters predicted at each stage. The key + # is the name of the parameters and the value is the prediction of the + +-- Chunk 7 -- +// predictor.py:971-1120 + # model at the i-th stage of the iteration + param_dicts = [] + # A dict of lists. Each key is the name of the parameter and the + # corresponding item is a list of offsets that are predicted by the + # model + deltas_dict = defaultdict(lambda: []) + param_delta_iter = zip(body_parameters, body_deltas) + for idx, (params, deltas) in enumerate(param_delta_iter): + curr_params_dict = self.flat_body_params_to_dict(params) + + out_dict = {} + for key, val in curr_params_dict.items(): + if hasattr(self, f'{key}_decoder'): + decoder = getattr(self, f'{key}_decoder') + out_dict[key] = decoder(val) + out_dict[f'raw_{key}'] = val.clone() + else: + out_dict[key] = val + + param_dicts.append(out_dict) + curr_params_dict.clear() + for key, val in self.flat_body_params_to_dict(deltas).items(): + deltas_dict[key].append(val) + + for key in deltas_dict: + deltas_dict[key] = torch.stack(deltas_dict[key], dim=1).sum(dim=1) + + if self.pose_last_stage: + merged_params = param_dicts[-1] + else: + merged_params = {} + for key in param_dicts[0].keys(): + param = [] + for idx in range(self.num_stages): + if param_dicts[idx][key] is None: + continue + param.append(param_dicts[idx][key]) + merged_params[key] = torch.cat(param, dim=0) + + # Compute the body surface using the current estimation of the pose and + # the shape + body_model_output = self.body_model( + get_skin=True, return_shaped=True, **merged_params) + + # Split the vertices, joints, etc. to stages + out_params = defaultdict(lambda: dict()) + for key in body_model_output: + if torch.is_tensor(body_model_output[key]): + curr_val = body_model_output[key] + out_list = torch.split( + curr_val, batch_size, dim=0) + # If the number of outputs is equal to the number of stages + # then store each stage + if len(out_list) == self.num_stages: + for idx in range(len(out_list)): + out_params[f'stage_{idx:02d}'][key] = out_list[idx] + # Else add only the last + else: + out_key = f'stage_{self.num_stages - 1:02d}' + out_params[out_key][key] = out_list[-1] + + # Add the predicted parameters to the output dictionary + for stage in range(self.num_stages): + stage_key = f'stage_{stage:02d}' + if len(out_params[stage_key]) < 1: + continue + out_params[stage_key].update(param_dicts[stage]) + out_params[stage_key]['faces'] = self.body_model.faces + + global_orient_from_body_net = param_dicts[-1]['global_orient'].clone() + body_pose_from_body_net = param_dicts[-1]['body_pose'].clone() + + raw_body_pose_from_body_net = param_dicts[-1]['raw_body_pose'].clone( + ).reshape(batch_size, 21, -1) + raw_right_hand_pose_from_body_net = param_dicts[-1][ + 'raw_right_hand_pose'].clone() + left_hand_pose = param_dicts[-1]['left_hand_pose'].clone() + right_hand_pose = param_dicts[-1]['right_hand_pose'].clone() + jaw_pose = param_dicts[-1]['jaw_pose'].clone() + + # Extract the camera parameters estimated by the body only image + camera_params = torch.index_select( + body_parameters[-1], 1, self.camera_idxs) + scale = camera_params[:, 0].view(-1, 1) + translation = camera_params[:, 1:3] + # Pass the predicted scale through exp() to make sure that the + # scale values are always positive + scale = self.camera_scale_func(scale) + + # Extract the final shape and expression parameters predicted by the + # body only model + betas = param_dicts[-1].get('betas').clone() + expression = param_dicts[-1].get('expression') + + # Project the joints on the image plane + proj_joints = self.projection( + out_params[f'stage_{self.num_stages - 1:02d}']['joints'], + scale=scale, translation=translation) + + # Add the projected joints + out_params['proj_joints'] = proj_joints + # the number of stages + out_params['num_stages'] = self.num_stages + # and the camera parameters to the output + out_params['camera_parameters'] = CameraParams( + translation=translation, scale=scale) + + # Clone the body pose so that we can update it with the predicted + # sub-parts + if self.predict_head or self.predict_hands: + final_body_pose = raw_body_pose_from_body_net.clone() + + hand_predictions, head_predictions = {}, {} + num_hand_imgs = 0 + left_hand_mask, right_hand_mask = None, None + if self.predict_hands: + if self.apply_hand_network_on_body: + # Get the left, right and head crops from the full body + left_hand_joints = ( + (torch.index_select(proj_joints, 1, self.left_hand_idxs) * + 0.5 + 0.5) * crop_size) + # left_hand_joints = torch.index_select( + # proj_joints, 1, self.left_hand_idxs) + left_hand_points_to_crop = self.points_to_crops( + full_imgs, left_hand_joints, targets, + scale_factor=self.hand_scale_factor, crop_size=crop_size, + ) + left_hand_center = left_hand_points_to_crop['center'] + left_hand_orig_bbox_size = left_hand_points_to_crop[ + 'orig_bbox_size'] + left_hand_bbox_size = left_hand_points_to_crop['bbox_size'] + left_hand_inv_crop_transforms = left_hand_points_to_crop[ + 'inv_crop_transforms'] + + left_hand_cropper_out = self.hand_cropper( + full_imgs, left_hand_center, left_hand_orig_bbox_size) + left_hand_crops = left_hand_cropper_out['images'] + left_hand_points = left_hand_cropper_out['sampling_grid'] + left_hand_crop_transform = left_hand_cropper_out['transform'] + + right_hand_joints = (torch.index_select( + proj_joints, 1, self.right_hand_idxs) * 0.5 + 0.5) * crop_size + right_hand_points_to_crop = self.points_to_crops( + full_imgs, right_hand_joints, targets, + scale_factor=self.hand_scale_factor, crop_size=crop_size, + ) + right_hand_center = right_hand_points_to_crop['center'] + right_hand_orig_bbox_size = right_hand_points_to_crop[ + 'orig_bbox_size'] + right_hand_bbox_size = right_hand_points_to_crop['bbox_size'] + +-- Chunk 8 -- +// predictor.py:1121-1270 + + right_hand_cropper_out = self.hand_cropper( + full_imgs, right_hand_center, right_hand_orig_bbox_size) + right_hand_crops = right_hand_cropper_out['images'] + right_hand_points = right_hand_cropper_out['sampling_grid'] + right_hand_crop_transform = right_hand_cropper_out['transform'] + + # Store the transformation parameters + out_params['left_hand_crops'] = left_hand_crops.detach() + out_params['left_hand_points'] = left_hand_points.detach() + out_params['right_hand_crops'] = right_hand_crops.detach() + out_params['right_hand_points'] = right_hand_points.detach() + + out_params['right_hand_crop_transform'] = ( + right_hand_crop_transform.detach()) + out_params['left_hand_crop_transform'] = ( + left_hand_crop_transform.detach()) + + out_params['left_hand_hd_to_crop'] = ( + left_hand_cropper_out['hd_to_crop']) + out_params['left_hand_inv_crop_transforms'] = ( + left_hand_points_to_crop['inv_crop_transforms']) + + out_params['right_hand_hd_to_crop'] = ( + right_hand_cropper_out['hd_to_crop']) + out_params['right_hand_inv_crop_transforms'] = ( + right_hand_points_to_crop['inv_crop_transforms']) + + # Flip the left hand to a right hand + all_hand_imgs = [] + hand_global_orient = [] + hand_body_pose = [] + if self.apply_hand_network_on_body: + all_hand_imgs.append(right_hand_crops) + all_hand_imgs.append(torch.flip(left_hand_crops, dims=(-1,))) + hand_global_orient += [ + global_orient_from_body_net, + flip_pose( + global_orient_from_body_net, pose_format='rot-mat')] + hand_body_pose += [ + body_pose_from_body_net, body_pose_from_body_net] + + if hand_imgs is not None and self.apply_hand_network_on_hands: + # Add the hand only images + num_hand_imgs = len(hand_imgs) + all_hand_imgs.append(hand_imgs) + + body_identity = torch.eye( + 3, device=device, dtype=dtype).reshape(1, 1, 3, 3).expand( + num_hand_imgs, body_pose_from_body_net.shape[1], -1, + -1) + hand_body_pose.append(body_identity) + global_identity = torch.eye( + 3, device=device, dtype=dtype).reshape( + 1, 1, 3, 3).expand( + num_hand_imgs, + global_orient_from_body_net.shape[1], -1, -1).clone() + global_identity[:, :, 1, 1] = -1 + global_identity[:, :, 2, 2] = -1 + hand_global_orient.append(global_identity) + + num_body_imgs = ( + batch_size if self.apply_hand_network_on_body else 0) + num_hand_net_ins = len(hand_body_pose) + num_body_imgs + if num_hand_net_ins > 0: + hand_body_pose = torch.cat(hand_body_pose, dim=0) + hand_global_orient = torch.cat(hand_global_orient, dim=0) + + # Flip the pose of the left hand + flipped_left_hand_pose = flip_pose( + param_dicts[-1]['left_hand_pose'], pose_format='rot-mat') + + # Build the mean used to condition the hand network using the + # parameters estimated by the body network + hand_mean, parent_rots = self.build_hand_mean( + param_dicts[-1]['global_orient'], + param_dicts[-1]['body_pose'], + betas=param_dicts[-1]['betas'], + flipped_left_hand_pose=flipped_left_hand_pose, + right_hand_pose=param_dicts[-1]['raw_right_hand_pose'], + hand_targets=hand_targets, + num_body_imgs=num_body_imgs, + num_hand_imgs=num_hand_imgs, + ) + + # Feed the hand images and the offsets to the hand-only + # predictor + all_hand_imgs = torch.cat(all_hand_imgs, dim=0) + + hand_predictions = self.hand_predictor( + all_hand_imgs, + hand_mean=hand_mean, + global_orient_from_body_net=hand_global_orient, + body_pose_from_body_net=hand_body_pose, + parent_rots=parent_rots, + num_hand_imgs=num_hand_imgs, + ) + num_hand_stages = hand_predictions.get('num_stages', 1) + hand_network_output = hand_predictions.get( + f'stage_{num_hand_stages - 1:02d}') + + if self.apply_hand_network_on_body: + # Find which images belong to the left hand and which ones to + # the right hand + hands_from_body_idxs = torch.arange( + 0, 2 * batch_size, dtype=torch.long, device=device) + right_hand_from_body_idxs = hands_from_body_idxs[ + :batch_size] + left_hand_from_body_idxs = hands_from_body_idxs[batch_size:] + + right_hand_features = hand_predictions.get( + 'features')[right_hand_from_body_idxs] + left_hand_features = hand_predictions.get( + 'features')[left_hand_from_body_idxs] + + right_hand_mask = None + raw_right_hand_pose_dict = self.right_hand_pose_merging_func( + from_body=raw_right_hand_pose_from_body_net, + from_part=hand_network_output.get( + 'raw_right_hand_pose')[right_hand_from_body_idxs], + body_feat=body_features, + part_feat=right_hand_features, + mask=right_hand_mask, + ) + raw_right_hand_pose = raw_right_hand_pose_dict['merged'] + + if self.update_wrists: + right_wrist_pose_from_part = hand_network_output.get( + 'raw_right_wrist_pose') + right_wrist_pose_from_body = raw_body_pose_from_body_net[ + :, self.right_wrist_idx - 1] + raw_right_wrist_pose_dict = ( + self.right_wrist_pose_merging_func( + from_body=right_wrist_pose_from_body, + from_part=right_wrist_pose_from_part, + body_feat=body_features, + part_feat=right_hand_features, + mask=right_hand_mask, + ) + ) + raw_right_wrist_pose = raw_right_wrist_pose_dict['merged'] + final_body_pose[:, self.right_wrist_idx - 1] = ( + raw_right_wrist_pose) + + # Project the flipped left hand pose to the rotation latent + # space using the decoder for the right hand + raw_left_to_right_hand_pose = ( + self.right_hand_pose_decoder.encode( + flipped_left_hand_pose).reshape(batch_size, -1)) + # Convert the pose of the left hand to the right hand and + +-- Chunk 9 -- +// predictor.py:1271-1420 + # project it to the encoder space + raw_left_to_right_hand_pose_from_body = ( + self.right_hand_pose_decoder.encode( + flipped_left_hand_pose).reshape(batch_size, -1)) + # Merge the predictions of the body network and the part + # network for the articulation of the left hand + left_hand_pose_from_part = hand_network_output.get( + 'raw_right_hand_pose')[left_hand_from_body_idxs] + raw_left_to_right_hand_pose_dict = ( + self.left_hand_pose_merging_func( + from_body=raw_left_to_right_hand_pose_from_body, + from_part=left_hand_pose_from_part, + body_feat=body_features, + part_feat=left_hand_features, + mask=left_hand_mask, + ) + ) + raw_left_to_right_hand_pose = raw_left_to_right_hand_pose_dict[ + 'merged'] + + if self.update_wrists: + left_wrist_pose_from_part = hand_network_output.get( + 'raw_left_wrist_pose') + left_wrist_pose_from_body = raw_body_pose_from_body_net[ + :, self.left_wrist_idx - 1] + raw_left_wrist_pose_dict = ( + self.left_wrist_pose_merging_func( + from_body=left_wrist_pose_from_body, + from_part=left_wrist_pose_from_part, + body_feat=body_features, + part_feat=left_hand_features, + mask=left_hand_mask, + ) + ) + raw_left_wrist_pose = raw_left_wrist_pose_dict['merged'] + final_body_pose[:, self.left_wrist_idx - 1] = ( + raw_left_wrist_pose) + + right_hand_pose = self.right_hand_pose_decoder( + raw_right_hand_pose) + # Decode the predicted pose and flip it back to the left hand + # space + left_hand_pose = flip_pose(self.right_hand_pose_decoder( + raw_left_to_right_hand_pose), pose_format='rot-mat') + + num_head_imgs = 0 + head_mask = None + if self.predict_head: + if self.apply_head_network_on_body: + head_joints = (torch.index_select( + proj_joints, 1, self.head_idxs) * 0.5 + 0.5) * crop_size + # head_joints = torch.index_select( + # proj_joints, 1, self.head_idxs) + head_point_to_crop_output = self.points_to_crops( + full_imgs, head_joints, targets, + scale_factor=self.head_scale_factor, crop_size=crop_size, + ) + head_center = head_point_to_crop_output['center'] + head_orig_bbox_size = head_point_to_crop_output[ + 'orig_bbox_size'] + head_bbox_size = head_point_to_crop_output['bbox_size'] + head_inv_crop_transforms = head_point_to_crop_output[ + 'inv_crop_transforms'] + + head_cropper_out = self.head_cropper( + full_imgs, head_center, head_orig_bbox_size) + head_crops = head_cropper_out['images'] + head_points = head_cropper_out['sampling_grid'] + # Contains the transformation that is used to transform the + # sampling grid from head image coordinates to HD image + # coordinates. + head_crop_transform = head_cropper_out['transform'] + + out_params['head_crops'] = head_crops.detach() + out_params['head_points'] = head_points.detach() + out_params['head_crop_transform'] = ( + head_crop_transform.detach()) + + out_params['head_hd_to_crop'] = head_cropper_out['hd_to_crop'] + out_params['head_inv_crop_transforms'] = ( + head_point_to_crop_output['inv_crop_transforms']) + + all_head_imgs = [] + if self.apply_head_network_on_body: + all_head_imgs.append(head_crops) + + # The global and body pose data used to pose the model inside the + # head-only sub-network. + head_global_orient, head_body_pose = [], [] + if self.apply_head_network_on_body: + head_global_orient += [global_orient_from_body_net] + head_body_pose += [body_pose_from_body_net] + + if head_imgs is not None and self.apply_head_network_on_head: + all_head_imgs.append(head_imgs) + num_head_imgs = len(head_imgs) + body_identity = torch.eye( + 3, device=device, dtype=dtype).reshape( + 1, 1, 3, 3).expand( + num_head_imgs, body_pose_from_body_net.shape[1], + -1, -1) + head_body_pose.append(body_identity) + global_identity = torch.eye( + 3, device=device, dtype=dtype).reshape( + 1, 1, 3, 3).expand(num_head_imgs, -1, -1, -1).clone() + global_identity[:, :, 1, 1] = -1 + global_identity[:, :, 2, 2] = -1 + head_global_orient.append(global_identity) + + num_body_imgs = ( + batch_size if self.apply_head_network_on_body else 0 + ) + num_head_net_ins = len(head_global_orient) + num_body_imgs + if num_head_net_ins > 0: + head_global_orient = torch.cat(head_global_orient, dim=0) + head_body_pose = torch.cat(head_body_pose, dim=0) + + head_mean = self.build_head_mean( + param_dicts[-1]['global_orient'], + param_dicts[-1]['body_pose'], + betas=param_dicts[-1]['betas'], + expression=param_dicts[-1]['expression'], + jaw_pose=param_dicts[-1]['raw_jaw_pose'], + num_head_imgs=num_head_imgs, + num_body_imgs=num_body_imgs, + head_targets=head_targets, + ) + all_head_imgs = torch.cat(all_head_imgs, dim=0) + + head_predictions = self.head_predictor( + all_head_imgs, + head_mean=head_mean, + global_orient_from_body_net=head_global_orient, + body_pose_from_body_net=head_body_pose, + num_head_imgs=num_head_imgs, + ) + + num_head_stages = head_predictions.get('num_stages', 1) + head_network_output = head_predictions.get( + f'stage_{num_head_stages - 1:02d}') + if self.apply_head_network_on_body: + head_from_body_idxs = torch.arange( + 0, batch_size, dtype=torch.long, device=device) + head_features = head_predictions.get( + 'features')[head_from_body_idxs] + # During training only use predictions from bounding boxes + # with enough IoU. + head_mask = None + raw_jaw_pose_from_body = param_dicts[-1].get( + 'raw_jaw_pose') + +-- Chunk 10 -- +// predictor.py:1421-1570 + # Replace the jaw pose only from the predictions taken from + # valid head crops + raw_jaw_pose_from_part = head_network_output.get( + 'raw_jaw_pose')[head_from_body_idxs] + raw_jaw_pose_dict = self.jaw_pose_merging_func( + from_body=raw_jaw_pose_from_body, + from_part=raw_jaw_pose_from_part, + body_feat=body_features, + part_feat=head_features, + mask=head_mask, + ) + raw_jaw_pose = raw_jaw_pose_dict['merged'] + + expression_from_body = param_dicts[-1].get('expression') + expression_from_head = head_network_output.get( + 'expression')[head_from_body_idxs, + :self.num_expression_coeffs] + expression_dict = self.expression_merging_func( + from_body=expression_from_body, + from_part=expression_from_head, + body_feat=body_features, + part_feat=head_features, + mask=head_mask, + ) + expression = expression_dict['merged'] + jaw_pose = self.jaw_pose_decoder(raw_jaw_pose) + + + if self.predict_head or self.predict_hands: + body_pose = self.body_pose_decoder( + final_body_pose.reshape(batch_size, -1)) + else: + body_pose = body_pose_from_body_net + + final_body_parameters = { + 'global_orient': param_dicts[-1].get('global_orient'), + 'body_pose': body_pose, + 'left_hand_pose': left_hand_pose, + 'right_hand_pose': right_hand_pose, + 'jaw_pose': jaw_pose, + 'betas': betas, + 'expression': expression + } + + if self.apply_hand_network_on_body or self.apply_head_network_on_body: + # Compute the mesh using the new hand and face parameters + final_body_model_output = self.body_model( + get_skin=True, return_shaped=True, **final_body_parameters) + param_dicts.append({ + **final_body_parameters, **final_body_model_output}) + + if (self.apply_hand_network_on_body or + self.apply_head_network_on_body): + out_params['final'] = { + **final_body_parameters, **final_body_model_output} + joints3d = final_body_model_output.get('joints') + proj_joints = self.projection( + joints3d, scale=scale, translation=translation) + out_params['final_proj_joints'] = proj_joints + # Update the camera parameters with the new projected joints + out_params['proj_joints'] = proj_joints + out_params['final']['proj_joints'] = proj_joints + else: + joints3d = out_params[f'stage_{self.num_stages - 1:02d}']['joints'] + + body_crop_size = images.shape[2] + # Convert the projected joints from [-1, 1] to body image + # coordinates + proj_joints_in_body_crop = ( + proj_joints * 0.5 + 0.5) * body_crop_size + + # Transform the projected points back to the HD image + if self.apply_head_network_on_body: + hd_proj_joints = torch.einsum( + 'bij,bkj->bki', + [head_inv_crop_transforms[:, :2, :2], + proj_joints_in_body_crop]) + head_inv_crop_transforms[ + :, :2, 2].unsqueeze(dim=1) + out_params['hd_proj_joints'] = hd_proj_joints.detach() + elif self.apply_hand_network_on_body: + hd_proj_joints = torch.einsum( + 'bij,bkj->bki', + [left_hand_inv_crop_transforms[:, :2, :2], + proj_joints_in_body_crop]) + left_hand_inv_crop_transforms[ + :, :2, 2].unsqueeze(dim=1) + out_params['hd_proj_joints'] = hd_proj_joints.detach() + + if self.apply_head_network_on_body: + inv_head_crop_transf = torch.inverse(head_crop_transform) + head_img_keypoints = torch.einsum( + 'bij,bkj->bki', + [inv_head_crop_transf[:, :2, :2], + hd_proj_joints]) + inv_head_crop_transf[:, :2, 2].unsqueeze( + dim=1) + out_params['head_proj_joints'] = ( + head_img_keypoints.detach() * self.head_crop_size) + + if self.apply_hand_network_on_body: + inv_left_hand_crop_transf = torch.inverse(left_hand_crop_transform) + left_hand_img_keypoints = torch.einsum( + 'bij,bkj->bki', + [inv_left_hand_crop_transf[:, :2, :2], + hd_proj_joints]) + inv_left_hand_crop_transf[ + :, :2, 2].unsqueeze(dim=1) + out_params['left_hand_proj_joints'] = ( + left_hand_img_keypoints.detach() * self.hand_crop_size) + + inv_right_hand_crop_transf = torch.inverse( + right_hand_crop_transform) + right_hand_img_keypoints = torch.einsum( + 'bij,bkj->bki', + [inv_right_hand_crop_transf[:, :2, :2], + hd_proj_joints]) + inv_right_hand_crop_transf[ + :, :2, 2].unsqueeze(dim=1) + out_params['right_hand_proj_joints'] = ( + right_hand_img_keypoints.detach() * self.hand_crop_size) + + if self.training: + # Create the tensor of ground-truth HD keypoints + gt_hd_keypoints = [] + for t in targets: + gt_hd_keypoints.append(t.get_field('keypoints_hd')) + + gt_hd_keypoints_with_conf = torch.tensor( + gt_hd_keypoints, dtype=dtype, device=device) + gt_hd_keypoints_conf = gt_hd_keypoints_with_conf[:, :, -1] + gt_hd_keypoints = gt_hd_keypoints_with_conf[:, :, :-1] + out_params['gt_conf'] = gt_hd_keypoints_conf.detach() + + if self.apply_head_network_on_body: + # Convert the ground-truth HD keypoints to the head image space + gt_head_keypoints = torch.einsum( + 'bij,bkj->bki', + [inv_head_crop_transf[:, :2, :2], + gt_hd_keypoints]) + inv_head_crop_transf[ + :, :2, 2].unsqueeze(dim=1) + out_params['gt_head_keypoints'] = ( + gt_head_keypoints.detach() * self.head_crop_size) + + # Convert the ground-truth HD keypoints to the left and right hand + # image space + if self.apply_hand_network_on_body: + gt_right_hand_keypoints = ( + torch.einsum( + 'bij,bkj->bki', + [inv_right_hand_crop_transf[:, :2, :2], + gt_hd_keypoints]) + + inv_right_hand_crop_transf[:, :2, 2].unsqueeze(dim=1)) + gt_left_hand_keypoints = ( + torch.einsum( + +-- Chunk 11 -- +// predictor.py:1571-1586 + 'bij,bkj->bki', + [inv_left_hand_crop_transf[:, :2, :2], + gt_hd_keypoints]) + + inv_left_hand_crop_transf[:, :2, 2].unsqueeze(dim=1)) + + out_params['gt_right_hand_keypoints'] = ( + gt_right_hand_keypoints.detach() * self.hand_crop_size) + out_params['gt_left_hand_keypoints'] = ( + gt_left_hand_keypoints.detach() * self.hand_crop_size) + + output = { + 'body': out_params, + 'losses': losses + } + + return output + +=== File: expose/models/attention/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/attention/__init__.py:1-17 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from .build import build_attention_head + +=== File: expose/models/attention/hand_predictor.py === + +-- Chunk 1 -- +// hand_predictor.py:52-201 +ss HandPredictor(nn.Module): + + def __init__(self, exp_cfg, + global_orient_desc, + hand_pose_desc, + camera_data, + wrist_pose_mean=None, + detach_mean=False, + mean_pose_path='', + dtype=torch.float32): + super(HandPredictor, self).__init__() + + network_cfg = exp_cfg.get('network', {}) + attention_net_cfg = network_cfg.get('attention', {}) + hand_net_cfg = attention_net_cfg.get('hand', {}) + + self.hand_model_type = hand_net_cfg.get('type', 'mano') + + hand_model_cfg = exp_cfg.get('hand_model', {}) + self.hand_model_cfg = hand_model_cfg.copy() + + self.right_wrist_index = KEYPOINT_NAMES.index('right_wrist') + self.left_wrist_index = KEYPOINT_NAMES.index('left_wrist') + + camera_cfg = hand_net_cfg.get('camera', {}) + camera_data = build_cam_proj(camera_cfg, dtype=dtype) + self.projection = camera_data['camera'] + + camera_param_dim = camera_data['dim'] + camera_mean = camera_data['mean'] + # self.camera_mean = camera_mean + self.register_buffer('camera_mean', camera_mean) + self.camera_scale_func = camera_data['scale_func'] + + # The number of shape coefficients + self.num_betas = self.hand_model_cfg['num_betas'] + shape_mean = torch.zeros([self.num_betas], dtype=dtype) + self.register_buffer('shape_mean', shape_mean) + + self.global_orient_decoder = global_orient_desc.decoder + cfg = {'param_type': global_orient_desc.decoder.get_type()} + self.wrist_pose_decoder = build_pose_decoder(cfg, 1) + wrist_pose_mean = self.wrist_pose_decoder.get_mean() + wrist_pose_dim = self.wrist_pose_decoder.get_dim_size() + self.register_buffer('wrist_pose_mean', wrist_pose_mean) + + self.register_buffer( + 'global_orient_mean', wrist_pose_mean.unsqueeze(dim=0)) + + self.hand_pose_decoder = hand_pose_desc.decoder + hand_pose_mean = hand_pose_desc.mean + self.register_buffer('hand_pose_mean', hand_pose_mean) + hand_pose_dim = hand_pose_desc.dim + + mean_lst = [] + start = 0 + wrist_pose_idxs = list(range(start, start + wrist_pose_dim)) + self.register_buffer('wrist_pose_idxs', + torch.tensor(wrist_pose_idxs, dtype=torch.long)) + start += wrist_pose_dim + mean_lst.append(wrist_pose_mean.view(-1)) + + hand_pose_idxs = list(range( + start, start + hand_pose_dim)) + self.register_buffer( + 'hand_pose_idxs', torch.tensor(hand_pose_idxs, dtype=torch.long)) + start += hand_pose_dim + mean_lst.append(hand_pose_mean.view(-1)) + + shape_idxs = list(range(start, start + self.num_betas)) + self.register_buffer( + 'shape_idxs', torch.tensor(shape_idxs, dtype=torch.long)) + start += self.num_betas + mean_lst.append(shape_mean.view(-1)) + + camera_idxs = list(range( + start, start + camera_param_dim)) + self.register_buffer( + 'camera_idxs', torch.tensor(camera_idxs, dtype=torch.long)) + start += camera_param_dim + mean_lst.append(camera_mean) + + self.register_buffer('camera_mean', camera_mean.unsqueeze(dim=0)) + + param_mean = torch.cat(mean_lst).view(1, -1) + param_dim = param_mean.numel() + self.param_dim = param_dim + + # Construct the feature extraction backbone + backbone_cfg = hand_net_cfg.get('backbone', {}) + self.backbone, feat_dims = build_backbone(backbone_cfg) + + self.append_params = hand_net_cfg.get('append_params', True) + self.num_stages = hand_net_cfg.get('num_stages', 1) + + self.feature_key = hand_net_cfg.get('feature_key', 'avg_pooling') + feat_dim = feat_dims[self.feature_key] + self.feat_dim = feat_dim + + regressor_cfg = hand_net_cfg.get('mlp', {}) + regressor = MLP(feat_dim + self.append_params * param_dim, + param_dim, **regressor_cfg) + self.regressor = IterativeRegression( + regressor, param_mean, detach_mean=detach_mean, + num_stages=self.num_stages) + + def get_feat_dim(self) -> int: + ''' Returns the dimension of the expected feature vector ''' + return self.feat_dim + + def get_param_dim(self) -> int: + ''' Returns the dimension of the predicted parameter vector ''' + return self.param_dim + + def get_num_stages(self) -> int: + ''' Returns the number of stages for the iterative predictor''' + return self.num_stages + + def get_shape_mean(self, batch_size: int = 1) -> Tensor: + ''' Returns the mean shape for the hands ''' + return self.shape_mean.reshape(1, -1).expand(batch_size, -1) + + def get_camera_mean(self, batch_size: int = 1) -> Tensor: + ''' Returns the camera mean ''' + return self.camera_mean.reshape(1, -1).expand(batch_size, -1) + + def get_wrist_pose_mean(self, batch_size=1) -> Tensor: + ''' Returns wrist pose mean ''' + return self.wrist_pose_mean.reshape(1, -1).expand(batch_size, -1) + + def get_finger_pose_mean(self, batch_size=1) -> Tensor: + ''' Returns neck pose mean ''' + return self.hand_pose_mean.reshape(1, -1).expand(batch_size, -1) + + def get_param_mean(self, + batch_size: int = 1, + add_shape_noise: bool = False, + shape_mean: Tensor = None, + shape_std: float = 0.0, + shape_prob: float = 0.0, + num_hand_components: int = 3, + add_hand_pose_noise: bool = False, + hand_pose_mean: Tensor = None, + hand_pose_std: float = 1.0, + hand_noise_prob: float = 0.0, + targets: List = None, + randomize_global_orient: bool = False, + global_rot_noise_prob: float = 0.0, + global_rot_min: bool = 0.0, + global_rot_max: bool = 0.0, + +-- Chunk 2 -- +// hand_predictor.py:202-316 + ) -> Tensor: + ''' Returns the mean vector given to the iterative regressor + ''' + mean = self.regressor.get_mean().clone().reshape(1, -1).expand( + batch_size, -1).clone() + if not self.training: + return mean + + raise NotImplementedError + + def param_tensor_to_dict(self, param_tensor): + wrist_pose = torch.index_select(param_tensor, 1, self.wrist_pose_idxs) + hand_pose = torch.index_select(param_tensor, 1, self.hand_pose_idxs) + + betas = torch.index_select(param_tensor, 1, self.shape_idxs) + + return dict(wrist_pose=wrist_pose, hand_pose=hand_pose, betas=betas) + + def forward(self, + hand_imgs: Tensor, + hand_mean: Optional[Tensor] = None, + global_orient_from_body_net: Optional[Tensor] = None, + body_pose_from_body_net: Optional[Tensor] = None, + parent_rots: Optional[Tensor] = None, + num_hand_imgs: int = 0, + device: torch.device = None, + ) -> Dict[str, Dict[str, Tensor]]: + ''' Forward pass of the hand predictor ''' + batch_size = hand_imgs.shape[0] + num_body_data = batch_size - num_hand_imgs + if batch_size == 0: + return {} + + if device is None: + device = hand_imgs.device + dtype = hand_imgs.dtype + + if parent_rots is None: + parent_rots = torch.eye(3, dtype=dtype, device=device).reshape( + 1, 1, 3, 3).expand(batch_size, -1, -1, -1).clone() + + right_hand_idxs = torch.arange( + 0, num_body_data // 2, dtype=torch.long, device=device) + left_hand_idxs = torch.arange( + num_body_data // 2, num_body_data, dtype=torch.long, device=device) + + hand_features = self.backbone(hand_imgs) + hand_parameters, hand_deltas = self.regressor( + hand_features[self.feature_key], cond=hand_mean) + + hand_model_parameters = [] + model_parameters = [] + for stage_idx, parameters in enumerate(hand_parameters): + parameters_dict = self.param_tensor_to_dict(parameters) + + # Decode the predicted wrist pose as a rotation matrix + dec_wrist_pose_abs = self.wrist_pose_decoder( + parameters_dict['wrist_pose']) + + # Undo the rotation of the parent joints to make the wrist rotation + # relative again + dec_wrist_pose = torch.matmul( + parent_rots.reshape(-1, 3, 3).transpose(1, 2), + dec_wrist_pose_abs.reshape(-1, 3, 3) + ) + raw_right_wrist_pose, raw_left_wrist_pose = None, None + if len(right_hand_idxs) > 0: + raw_right_wrist_pose = self.global_orient_decoder.encode( + dec_wrist_pose[right_hand_idxs].unsqueeze(dim=1)).reshape( + num_body_data // 2, -1) + + if len(left_hand_idxs) > 0: + left_wrist_poses = flip_pose( + dec_wrist_pose[left_hand_idxs], pose_format='rot-mat') + raw_left_wrist_pose = self.global_orient_decoder.encode( + left_wrist_poses.unsqueeze(dim=1)).reshape( + num_body_data // 2, -1) + + dec_hand_pose = self.hand_pose_decoder( + parameters_dict['hand_pose']) + model_betas = parameters_dict['betas'] + + model_parameters.append( + dict(right_hand_pose=dec_hand_pose, + betas=model_betas, + wrist_pose=dec_wrist_pose_abs, + hand_pose=dec_hand_pose, + raw_right_wrist_pose=raw_right_wrist_pose, + raw_left_wrist_pose=raw_left_wrist_pose, + raw_right_hand_pose=parameters_dict['hand_pose'], + ) + ) + + if self.hand_model_type == 'mano': + hand_model_parameters.append( + dict( + betas=model_betas, + wrist_pose=dec_wrist_pose_abs, + hand_pose=dec_hand_pose, + ) + ) + else: + raise RuntimeError( + f'Invalid hand model type: {self.hand_model_type}') + + output = {'num_stages': self.num_stages, + 'features': hand_features[self.feature_key], + } + + for stage in range(self.num_stages): + # Only update the current stage if the parameters exist + key = f'stage_{stage:02d}' + output[key] = model_parameters[stage] + + return output + +=== File: expose/models/common/mano_loss_modules.py === + +-- Chunk 1 -- +// mano_loss_modules.py:47-196 +ss MANOLossModule(nn.Module): + ''' + ''' + + def __init__(self, loss_cfg): + super(MANOLossModule, self).__init__() + + self.penalize_final_only = loss_cfg.get('penalize_final_only', True) + + self.loss_enabled = defaultdict(lambda: True) + self.loss_activ_step = {} + + idxs_dict = get_part_idxs() + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + + self.register_buffer('hand_idxs', torch.tensor(hand_idxs)) + + self.register_buffer('left_hand_idxs', torch.tensor(left_hand_idxs)) + self.register_buffer('right_hand_idxs', torch.tensor(right_hand_idxs)) + + shape_loss_cfg = loss_cfg.shape + self.shape_weight = shape_loss_cfg.get('weight', 0.0) + self.shape_loss = build_loss(**shape_loss_cfg) + self.loss_activ_step['shape'] = shape_loss_cfg.enable + + vertices_loss_cfg = loss_cfg.vertices + self.vertices_weight = vertices_loss_cfg.get('weight', 0.0) + self.vertices_loss = build_loss(**vertices_loss_cfg) + self.loss_activ_step['vertices'] = vertices_loss_cfg.enable + + self.use_alignment = vertices_loss_cfg.get('use_alignment', False) + if self.use_alignment: + self.alignment = RotationTranslationAlignment() + + edge_loss_cfg = loss_cfg.get('edge', {}) + self.edge_weight = edge_loss_cfg.get('weight', 0.0) + self.edge_loss = build_loss(**edge_loss_cfg) + self.loss_activ_step['edge'] = edge_loss_cfg.get('enable', 0) + + global_orient_cfg = loss_cfg.global_orient + self.global_orient_loss = build_loss(**global_orient_cfg) + logger.debug('Global pose loss: {}', self.global_orient_loss) + self.global_orient_weight = global_orient_cfg.weight + self.loss_activ_step['global_orient'] = global_orient_cfg.enable + + hand_pose_cfg = loss_cfg.get('hand_pose', {}) + hand_pose_loss_type = loss_cfg.hand_pose.type + self.hand_use_conf = hand_pose_cfg.get('use_conf_weight', False) + + self.hand_pose_weight = loss_cfg.hand_pose.weight + if self.hand_pose_weight > 0: + self.hand_pose_loss_type = hand_pose_loss_type + self.hand_pose_loss = build_loss(**loss_cfg.hand_pose) + self.loss_activ_step['hand_pose'] = loss_cfg.hand_pose.enable + + joints2d_cfg = loss_cfg.joints_2d + self.joints_2d_weight = joints2d_cfg.weight + self.joints_2d_enable_at = joints2d_cfg.enable + if self.joints_2d_weight > 0: + self.joints_2d_loss = build_loss(**joints2d_cfg) + logger.debug('2D hand joints loss: {}', self.joints_2d_loss) + self.joints_2d_active = False + + hand_edge_2d_cfg = loss_cfg.get('hand_edge_2d', {}) + self.hand_edge_2d_weight = hand_edge_2d_cfg.get('weight', 0.0) + self.hand_edge_2d_enable_at = hand_edge_2d_cfg.get('enable', 0) + if self.hand_edge_2d_weight > 0: + self.hand_edge_2d_loss = build_loss( + type='edge', connections=HAND_CONNECTIONS, **hand_edge_2d_cfg) + logger.debug('2D hand edge loss: {}', self.hand_edge_2d_loss) + self.hand_edge_2d_active = False + + joints3d_cfg = loss_cfg.joints_3d + self.joints_3d_weight = joints3d_cfg.weight + self.joints_3d_enable_at = joints3d_cfg.enable + if self.joints_3d_weight > 0: + joints_3d_loss_type = joints3d_cfg.type + self.joints_3d_loss = build_loss(**joints3d_cfg) + logger.debug('3D hand joints loss: {}', self.joints_3d_loss) + self.joints_3d_active = False + + def is_active(self) -> bool: + return any(self.loss_enabled.values()) + + def toggle_losses(self, step) -> None: + for key in self.loss_activ_step: + self.loss_enabled[key] = step >= self.loss_activ_step[key] + + def extra_repr(self) -> str: + msg = [] + msg.append('Shape weight: {self.shape_weight}') + msg.append(f'Global pose weight: {self.global_orient_weight}') + if self.hand_pose_weight > 0: + msg.append(f'Hand pose weight: {self.hand_pose_weight}') + return '\n'.join(msg) + + def single_loss_step(self, parameters, + global_orient=None, + hand_pose=None, + gt_hand_pose_idxs=None, + shape=None, + gt_vertices=None, + gt_vertex_idxs=None, + device=None, + keyp_confs=None): + losses = defaultdict( + lambda: torch.tensor(0, device=device, dtype=torch.float32)) + + param_vertices = parameters.get('vertices', None) + compute_vertex_loss = (self.vertices_weight > 0 and + len(gt_vertex_idxs) > 0 and + param_vertices is not None and + gt_vertices is not None) + if gt_vertex_idxs is not None: + if len(gt_vertex_idxs) > 0: + param_vertices = param_vertices[gt_vertex_idxs] + + if compute_vertex_loss: + if self.use_alignment: + aligned_verts = self.alignment(param_vertices, gt_vertices) + else: + aligned_verts = param_vertices + losses['vertex_loss'] = self.vertices_weight * self.vertices_loss( + aligned_verts, gt_vertices) + + compute_edge_loss = (self.edge_weight > 0 and + len(gt_vertex_idxs) > 0 and + param_vertices is not None and + gt_vertices is not None) + if compute_edge_loss: + edge_loss_val = self.edge_loss( + gt_vertices=gt_vertices, + est_vertices=param_vertices) + losses['mesh_edge_loss'] = self.edge_weight * edge_loss_val + + if (self.shape_weight > 0 and self.loss_enabled['betas'] and + shape is not None): + losses['shape_loss'] = ( + self.shape_loss(parameters['betas'], shape) * + self.shape_weight) + + if (self.global_orient_weight > 0 and self.loss_enabled['globals'] and + global_orient is not None): + losses['global_orient_loss'] = ( + self.global_orient_loss( + parameters['wrist_pose'], global_orient) * + self.global_orient_weight) + + +-- Chunk 2 -- +// mano_loss_modules.py:197-312 + if (self.hand_pose_weight > 0 and + self.loss_enabled['hand_pose'] and + hand_pose is not None): + # num_joints = parameters['hand_pose'].shape[1] + # weights = ( + # keyp_confs['hand'].mean(axis=1, keepdim=True).expand( + # -1, num_joints).reshape(-1) + # if self.hand_use_conf and keyp_confs is not None else None) + # if weights is not None: + # num_ones = [1] * len( + # parameters['hand_pose'].shape[2:]) + # weights = weights.view(-1, num_joints, *num_ones) + losses['hand_pose_loss'] = ( + self.hand_pose_loss( + parameters['right_hand_pose'], hand_pose) * + self.hand_pose_weight) + + return losses + + def forward(self, input_dict, + hand_targets, + device=None): + if device is None: + device = torch.device('cpu') + + # Stack the GT keypoints and conf for the predictions of the right hand + hand_keyps = torch.stack( + [t.smplx_keypoints for t in hand_targets]) + hand_conf = torch.stack([t.conf for t in hand_targets]) + + # Get the GT pose of the right hand + gt_hand_pose = torch.stack( + [t.get_field('hand_pose').right_hand_pose + for t in hand_targets + if t.has_field('hand_pose') + ]) + gt_hand_pose_idxs = [ii for ii, t in enumerate(hand_targets) + if t.has_field('hand_pose')] + # Get the GT pose of the right hand + global_orient = torch.stack( + [t.get_field('global_orient').global_orient for t in hand_targets + if t.has_field('global_orient')]) + + gt_vertex_idxs = [ii for ii, t in enumerate(hand_targets) + if t.has_field('vertices')] + gt_vertices = None + if len(gt_vertex_idxs) > 0: + gt_vertices = torch.stack([ + t.get_field('vertices').vertices + for t in hand_targets + if t.has_field('vertices')]) + + output_losses = {} + compute_2d_loss = ('proj_joints' in input_dict and + self.joints_2d_weight > 0) + if compute_2d_loss: + hand_proj_joints = input_dict['proj_joints'] + hand_joints2d_loss = self.joints_2d_loss( + hand_proj_joints, + hand_keyps[:, self.right_hand_idxs], + weights=hand_conf[:, self.right_hand_idxs]) + output_losses['joints2d'] = ( + hand_joints2d_loss * self.joints_2d_weight) + + # Stack the GT keypoints and conf for the predictions of the + # right hand + hand_keyps_3d = [t.get_field('keypoints3d').smplx_keypoints + for t in hand_targets if t.has_field('keypoints3d')] + hand_conf_3d = [t.get_field('keypoints3d').conf + for t in hand_targets if t.has_field('keypoints3d')] + + num_stages = input_dict.get('num_stages', 1) + curr_params = input_dict.get(f'stage_{num_stages - 1:02d}', None) + joints3d = input_dict['joints'] + compute_3d_joint_loss = (self.joints_3d_weight > 0 and + len(hand_conf_3d) > 0) + + if compute_3d_joint_loss: + hand_keyps_3d = torch.stack(hand_keyps_3d)[:, self.right_hand_idxs] + hand_conf_3d = torch.stack(hand_conf_3d)[:, self.right_hand_idxs] + + pred_joints = joints3d + # Center the joints according to the wrist + centered_pred_joints = pred_joints - pred_joints[:, [0]] + gt_hand_keyps_3d = hand_keyps_3d - hand_keyps_3d[:, [0]] + hand_keyp3d_loss = self.joints_3d_loss( + centered_pred_joints, + gt_hand_keyps_3d, + weights=hand_conf_3d, + ) * self.joints_3d_weight + output_losses['joints3d'] = hand_keyp3d_loss + + for n in range(1, num_stages + 1): + if self.penalize_final_only and n < num_stages: + continue + + curr_params = input_dict.get(f'stage_{n - 1:02d}', None) + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_loss_step( + curr_params, + hand_pose=gt_hand_pose, + gt_hand_pose_idxs=gt_hand_pose_idxs, + global_orient=global_orient, + gt_vertices=gt_vertices, + gt_vertex_idxs=gt_vertex_idxs, + device=device) + for key in curr_losses: + out_key = f'stage_{n - 1:02d}_{key}' + output_losses[out_key] = curr_losses[key] + + return output_losses + + + +-- Chunk 3 -- +// mano_loss_modules.py:313-384 +ss RegularizerModule(nn.Module): + def __init__(self, loss_cfg, + body_pose_mean=None, hand_pose_mean=None): + super(RegularizerModule, self).__init__() + + self.regularize_final_only = loss_cfg.get( + 'regularize_final_only', True) + + # Construct the shape prior + shape_prior_type = loss_cfg.shape.prior.type + self.shape_prior_weight = loss_cfg.shape.prior.weight + if self.shape_prior_weight > 0: + self.shape_prior = build_prior(shape_prior_type, + **loss_cfg.shape.prior) + logger.debug(f'Shape prior {self.shape_prior}') + + hand_prior_cfg = loss_cfg.hand_pose.prior + hand_pose_prior_type = hand_prior_cfg.type + self.hand_pose_prior_weight = hand_prior_cfg.weight + if self.hand_pose_prior_weight > 0: + self.hand_pose_prior = build_prior( + hand_pose_prior_type, + mean=hand_pose_mean, + **hand_prior_cfg) + logger.debug(f'Hand pose prior {self.hand_pose_prior}') + + logger.debug(self) + + def extra_repr(self) -> str: + msg = [] + if self.shape_prior_weight > 0: + msg.append(f'Shape prior weight: {self.shape_prior_weight}') + if self.hand_pose_prior_weight > 0: + msg.append( + f'Hand pose prior weight: {self.hand_pose_prior_weight}') + return '\n'.join(msg) + + def single_regularization_step(self, parameters, **kwargs): + prior_losses = {} + + betas = parameters.get('betas', None) + if self.shape_prior_weight > 0 and betas is not None: + prior_losses['shape_prior'] = ( + self.shape_prior_weight * self.shape_prior(betas)) + + hand_pose = parameters.get('right_hand_pose', None) + if (self.hand_pose_prior_weight > 0 and + hand_pose is not None): + prior_losses['hand_pose_prior'] = ( + self.hand_pose_prior(hand_pose) * + self.hand_pose_prior_weight) + + return prior_losses + + def forward(self, + input_dict, **kwargs) -> Dict[str, Tensor]: + + prior_losses = defaultdict(lambda: 0) + num_stages = input_dict.get('num_stages', 1) + for n in range(1, num_stages + 1): + if self.regularize_final_only and n < num_stages: + continue + curr_params = input_dict.get(f'stage_{n - 1:02d}', None) + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_regularization_step(curr_params) + for key in curr_losses: + out_key = f'stage_{n - 1:02d}_{key}' + prior_losses[out_key] = curr_losses[key] + return prior_losses + +=== File: expose/models/common/keypoint_loss.py === + +-- Chunk 1 -- +// keypoint_loss.py:31-180 +ss KeypointLoss(nn.Module): + def __init__(self, exp_cfg): + super(KeypointLoss, self).__init__() + self.left_hip_idx = KEYPOINT_NAMES.index('left_hip') + self.right_hip_idx = KEYPOINT_NAMES.index('right_hip') + + self.body_joints_2d_weight = exp_cfg.losses.body_joints_2d.weight + if self.body_joints_2d_weight > 0: + self.body_joints_2d_loss = build_loss( + **exp_cfg.losses.body_joints_2d) + logger.debug('2D body joints loss: {}', self.body_joints_2d_loss) + + hand_joints2d_cfg = exp_cfg.losses.hand_joints_2d + self.hand_joints_2d_weight = hand_joints2d_cfg.weight + self.hand_joints_2d_enable_at = hand_joints2d_cfg.enable + self.hand_joints_2d_active = False + if self.hand_joints_2d_weight > 0: + hand_joints2d_cfg = exp_cfg.losses.hand_joints_2d + self.hand_joints_2d_loss = build_loss(**hand_joints2d_cfg) + logger.debug('2D hand joints loss: {}', self.hand_joints_2d_loss) + + face_joints2d_cfg = exp_cfg.losses.face_joints_2d + self.face_joints_2d_weight = face_joints2d_cfg.weight + self.face_joints_2d_enable_at = face_joints2d_cfg.enable + self.face_joints_2d_active = False + if self.face_joints_2d_weight > 0: + self.face_joints_2d_loss = build_loss(**face_joints2d_cfg) + logger.debug('2D face joints loss: {}', self.face_joints_2d_loss) + + use_face_contour = exp_cfg.datasets.use_face_contour + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + face_idxs = idxs_dict['face'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + + self.register_buffer('body_idxs', torch.tensor(body_idxs)) + self.register_buffer('hand_idxs', torch.tensor(hand_idxs)) + self.register_buffer('face_idxs', torch.tensor(face_idxs)) + + self.body_joints_3d_weight = exp_cfg.losses.body_joints_3d.weight + if self.body_joints_3d_weight > 0: + self.body_joints_3d_loss = build_loss( + **exp_cfg.losses.body_joints_3d) + logger.debug('3D body_joints loss: {}', self.body_joints_3d_loss) + + hand_joints3d_cfg = exp_cfg.losses.hand_joints_3d + self.hand_joints_3d_weight = hand_joints3d_cfg.weight + self.hand_joints_3d_enable_at = hand_joints3d_cfg.enable + if self.hand_joints_3d_weight > 0: + self.hand_joints_3d_loss = build_loss(**hand_joints3d_cfg) + logger.debug('3D hand joints loss: {}', self.hand_joints_3d_loss) + self.hand_joints_3d_active = False + + face_joints3d_cfg = exp_cfg.losses.face_joints_3d + self.face_joints_3d_weight = face_joints3d_cfg.weight + self.face_joints_3d_enable_at = face_joints3d_cfg.enable + if self.face_joints_3d_weight > 0: + face_joints3d_cfg = exp_cfg.losses.face_joints_3d + self.face_joints_3d_loss = build_loss(**face_joints3d_cfg) + logger.debug('3D face joints loss: {}', self.face_joints_3d_loss) + self.face_joints_3d_active = False + + body_edge_2d_cfg = exp_cfg.losses.get('body_edge_2d', {}) + self.body_edge_2d_weight = body_edge_2d_cfg.weight + self.body_edge_2d_enable_at = body_edge_2d_cfg.enable + if self.body_edge_2d_weight > 0: + self.body_edge_2d_loss = build_loss(type='keypoint-edge', + connections=BODY_CONNECTIONS, + **body_edge_2d_cfg) + logger.debug('2D body edge loss: {}', self.body_edge_2d_loss) + self.body_edge_2d_active = False + + hand_edge_2d_cfg = exp_cfg.losses.get('hand_edge_2d', {}) + self.hand_edge_2d_weight = hand_edge_2d_cfg.get('weight', 0.0) + self.hand_edge_2d_enable_at = hand_edge_2d_cfg.get('enable', 0) + if self.hand_edge_2d_weight > 0: + self.hand_edge_2d_loss = build_loss(type='keypoint-edge', + connections=HAND_CONNECTIONS, + **hand_edge_2d_cfg) + logger.debug('2D hand edge loss: {}', self.hand_edge_2d_loss) + self.hand_edge_2d_active = False + + face_edge_2d_cfg = exp_cfg.losses.get('face_edge_2d', {}) + self.face_edge_2d_weight = face_edge_2d_cfg.get('weight', 0.0) + self.face_edge_2d_enable_at = face_edge_2d_cfg.get('enable', 0) + if self.face_edge_2d_weight > 0: + face_connections = [] + for conn in FACE_CONNECTIONS: + if ('contour' in KEYPOINT_NAMES[conn[0]] or + 'contour' in KEYPOINT_NAMES[conn[1]]): + if not use_face_contour: + continue + face_connections.append(conn) + + self.face_edge_2d_loss = build_loss( + type='keypoint-edge', connections=face_connections, + **face_edge_2d_cfg) + logger.debug('2D face edge loss: {}', self.face_edge_2d_loss) + self.face_edge_2d_active = False + + def extra_repr(self): + msg = [] + msg.append(f'Body joints 2D: {self.body_joints_2d_weight}') + msg.append(f'Hand joints 2D: {self.hand_joints_2d_weight}') + msg.append(f'Face joints 2D: {self.face_joints_2d_weight}') + + msg.append(f'Body joints 3D: {self.body_joints_3d_weight}') + msg.append(f'Hand joints 3D: {self.hand_joints_3d_weight}') + msg.append(f'Face joints 3D: {self.face_joints_3d_weight}') + + msg.append(f'Body edge 2D: {self.body_edge_2d_weight}') + msg.append(f'Hand edge 2D: {self.hand_edge_2d_weight}') + msg.append(f'Face edge 2D: {self.face_edge_2d_weight}') + + return '\n'.join(msg) + + def toggle_losses(self, iteration: int) -> None: + if hasattr(self, 'hand_joints_2d_enable_at'): + self.hand_joints_2d_active = ( + iteration >= self.hand_joints_2d_enable_at) + if hasattr(self, 'face_joints_2d_enable_at'): + self.face_joints_2d_active = (iteration >= + self.face_joints_2d_enable_at) + if hasattr(self, 'hand_joints_3d_enable_at'): + self.hand_joints_3d_active = (iteration >= + self.hand_joints_3d_enable_at) + if hasattr(self, 'face_joints_3d_enable_at'): + self.face_joints_3d_active = ( + iteration >= self.face_joints_3d_enable_at) + if hasattr(self, 'body_edge_2d_enable_at'): + self.body_edge_2d_active = ( + iteration >= self.body_edge_2d_enable_at) + if hasattr(self, 'hand_edge_2d_enable_at'): + self.hand_edge_2d_active = ( + iteration >= self.hand_edge_2d_enable_at) + if hasattr(self, 'face_edge_2d_enable_at'): + self.face_edge_2d_active = ( + iteration >= self.face_edge_2d_enable_at) + + def forward(self, proj_joints, joints3d, targets, device=None): + if device is None: + device = torch.device('cpu') + + losses = {} + # If training calculate 2D projection loss + if self.training and proj_joints is not None: + target_keypoints2d = torch.stack( + [target.smplx_keypoints + +-- Chunk 2 -- +// keypoint_loss.py:181-300 + for target in targets]) + target_conf = torch.stack( + [target.conf for target in targets]) + + if self.body_joints_2d_weight > 0: + body_joints_2d_loss = ( + self.body_joints_2d_weight * self.body_joints_2d_loss( + proj_joints[:, self.body_idxs], + target_keypoints2d[:, self.body_idxs], + weights=target_conf[:, self.body_idxs])) + losses.update(body_joints_2d_loss=body_joints_2d_loss) + + if self.hand_joints_2d_active and self.hand_joints_2d_weight > 0: + hand_joints_2d_loss = ( + self.hand_joints_2d_weight * self.hand_joints_2d_loss( + proj_joints[:, self.hand_idxs], + target_keypoints2d[:, self.hand_idxs], + weights=target_conf[:, self.hand_idxs])) + losses.update(hand_joints_2d_loss=hand_joints_2d_loss) + + if self.face_joints_2d_active and self.face_joints_2d_weight > 0: + face_joints_2d_loss = ( + self.face_joints_2d_weight * self.face_joints_2d_loss( + proj_joints[:, self.face_idxs], + target_keypoints2d[:, self.face_idxs], + weights=target_conf[:, self.face_idxs])) + losses.update(face_joints_2d_loss=face_joints_2d_loss) + + if self.body_edge_2d_weight > 0 and self.body_edge_2d_active: + body_edge_2d_loss = ( + self.body_edge_2d_weight * self.body_edge_2d_loss( + proj_joints, target_keypoints2d, weights=target_conf)) + losses.update(body_edge_2d_loss=body_edge_2d_loss) + + if self.hand_edge_2d_weight > 0 and self.hand_edge_2d_active: + hand_edge_2d_loss = ( + self.hand_edge_2d_weight * self.hand_edge_2d_loss( + proj_joints, target_keypoints2d, weights=target_conf)) + losses.update(hand_edge_2d_loss=hand_edge_2d_loss) + + if self.face_edge_2d_weight > 0 and self.face_edge_2d_active: + face_edge_2d_loss = ( + self.face_edge_2d_weight * self.face_edge_2d_loss( + proj_joints, target_keypoints2d, weights=target_conf)) + losses.update(face_edge_2d_loss=face_edge_2d_loss) + + # If training calculate 3D joints loss + if (self.training and self.body_joints_3d_weight > 0 and + joints3d is not None): + # Get the indices of the targets that have 3D keypoint annotations + target_idxs = [] + start_idx = 0 + for idx, target in enumerate(targets): + # If there are no 3D annotations, skip and add to the starting + # index the number of bounding boxes + if len(target) < 1: + continue + if not target.has_field('keypoints3d'): + start_idx += 1 + continue + # keyp3d_field = target.get_field('keypoints3d') + end_idx = start_idx + 1 + target_idxs += list(range(start_idx, end_idx)) + start_idx += 1 + + # TODO: Add flag for procrustes alignment between keypoints + if len(target_idxs) > 0: + target_idxs = torch.tensor(np.asarray(target_idxs), + device=device, + dtype=torch.long) + + target_keypoints3d = torch.stack( + [target.get_field('keypoints3d').smplx_keypoints + for target in targets + if target.has_field('keypoints3d') and + len(target) > 0]) + target_conf = torch.stack( + [target.get_field('keypoints3d')['conf'] + for target in targets + if target.has_field('keypoints3d') and + len(target) > 0]) + + # Center the predictions using the pelvis + pred_pelvis = joints3d[target_idxs][ + :, [self.left_hip_idx, self.right_hip_idx], :].mean( + dim=1, keepdim=True) + centered_pred_joints = joints3d[target_idxs] - pred_pelvis + + gt_pelvis = target_keypoints3d[ + :, [self.left_hip_idx, self.right_hip_idx], :].mean( + dim=1, keepdim=True) + centered_gt_joints = target_keypoints3d - gt_pelvis + + if self.body_joints_3d_weight > 0: + body_joints_3d_loss = ( + self.body_joints_3d_weight * self.body_joints_3d_loss( + centered_pred_joints[:, self.body_idxs], + centered_gt_joints[:, self.body_idxs], + weights=target_conf[:, self.body_idxs])) + losses.update(body_joints_3d_loss=body_joints_3d_loss) + + if (self.hand_joints_3d_active and + self.hand_joints_3d_weight > 0): + hand_joints_3d_loss = ( + self.hand_joints_3d_weight * self.hand_joints_3d_loss( + joints3d[target_idxs][:, self.hand_idxs], + target_keypoints3d[:, self.hand_idxs], + weights=target_conf[:, self.hand_idxs])) + losses.update(hand_joints_3d_loss=hand_joints_3d_loss) + + if (self.face_joints_3d_active and + self.face_joints_3d_weight > 0): + face_joints_3d_loss = ( + self.face_joints_3d_weight * self.face_joints_3d_loss( + joints3d[target_idxs][:, self.face_idxs], + target_keypoints3d[:, self.face_idxs], + weights=target_conf[:, self.face_idxs])) + losses.update(face_joints_3d_loss=face_joints_3d_loss) + + return losses + +=== File: expose/models/common/smplx_loss_modules.py === + +-- Chunk 1 -- +// smplx_loss_modules.py:39-188 +ss SMPLXLossModule(nn.Module): + ''' + ''' + + def __init__(self, loss_cfg, num_stages=3, + use_face_contour=False): + super(SMPLXLossModule, self).__init__() + + self.stages_to_penalize = loss_cfg.get('stages_to_penalize', [-1]) + logger.info(f'Stages to penalize: {self.stages_to_penalize}') + + self.loss_enabled = defaultdict(lambda: True) + self.loss_activ_step = {} + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + face_idxs = idxs_dict['face'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + + self.register_buffer('body_idxs', torch.tensor(body_idxs)) + self.register_buffer('hand_idxs', torch.tensor(hand_idxs)) + self.register_buffer('face_idxs', torch.tensor(face_idxs)) + + self.register_buffer('left_hand_idxs', torch.tensor(left_hand_idxs)) + self.register_buffer('right_hand_idxs', torch.tensor(right_hand_idxs)) + + shape_loss_cfg = loss_cfg.shape + self.shape_weight = shape_loss_cfg.get('weight', 0.0) + self.shape_loss = build_loss(**shape_loss_cfg) + self.loss_activ_step['shape'] = shape_loss_cfg.enable + + expression_cfg = loss_cfg.get('expression', {}) + self.expr_use_conf_weight = expression_cfg.get( + 'use_conf_weight', False) + + self.expression_weight = expression_cfg.weight + if self.expression_weight > 0: + self.expression_loss = build_loss(**expression_cfg) + self.loss_activ_step['expression'] = expression_cfg.enable + + global_orient_cfg = loss_cfg.global_orient + global_orient_loss_type = global_orient_cfg.type + self.global_orient_loss_type = global_orient_loss_type + self.global_orient_loss = build_loss(**global_orient_cfg) + logger.debug('Global pose loss: {}', self.global_orient_loss) + self.global_orient_weight = global_orient_cfg.weight + self.loss_activ_step['global_orient'] = global_orient_cfg.enable + + self.body_pose_weight = loss_cfg.body_pose.weight + body_pose_loss_type = loss_cfg.body_pose.type + self.body_pose_loss_type = body_pose_loss_type + self.body_pose_loss = build_loss(**loss_cfg.body_pose) + logger.debug('Body pose loss: {}', self.global_orient_loss) + self.body_pose_weight = loss_cfg.body_pose.weight + self.loss_activ_step['body_pose'] = loss_cfg.body_pose.enable + + left_hand_pose_cfg = loss_cfg.get('left_hand_pose', {}) + left_hand_pose_loss_type = loss_cfg.left_hand_pose.type + self.lhand_use_conf = left_hand_pose_cfg.get('use_conf_weight', False) + + self.left_hand_pose_weight = loss_cfg.left_hand_pose.weight + if self.left_hand_pose_weight > 0: + self.left_hand_pose_loss_type = left_hand_pose_loss_type + self.left_hand_pose_loss = build_loss(**loss_cfg.left_hand_pose) + self.loss_activ_step[ + 'left_hand_pose'] = loss_cfg.left_hand_pose.enable + + right_hand_pose_cfg = loss_cfg.get('right_hand_pose', {}) + right_hand_pose_loss_type = loss_cfg.right_hand_pose.type + self.right_hand_pose_weight = loss_cfg.right_hand_pose.weight + self.rhand_use_conf = right_hand_pose_cfg.get('use_conf_weight', False) + if self.right_hand_pose_weight > 0: + self.right_hand_pose_loss_type = right_hand_pose_loss_type + self.right_hand_pose_loss = build_loss(**loss_cfg.right_hand_pose) + self.loss_activ_step[ + 'right_hand_pose'] = loss_cfg.right_hand_pose.enable + + jaw_pose_loss_type = loss_cfg.jaw_pose.type + self.jaw_pose_weight = loss_cfg.jaw_pose.weight + + jaw_pose_cfg = loss_cfg.get('jaw_pose', {}) + self.jaw_use_conf_weight = jaw_pose_cfg.get('use_conf_weight', False) + if self.jaw_pose_weight > 0: + self.jaw_pose_loss_type = jaw_pose_loss_type + self.jaw_pose_loss = build_loss(**loss_cfg.jaw_pose) + logger.debug('Jaw pose loss: {}', self.global_orient_loss) + self.loss_activ_step['jaw_pose'] = loss_cfg.jaw_pose.enable + + edge_loss_cfg = loss_cfg.get('edge', {}) + self.edge_weight = edge_loss_cfg.get('weight', 0.0) + self.edge_loss = build_loss(**edge_loss_cfg) + self.loss_activ_step['edge'] = edge_loss_cfg.get('enable', 0) + + def is_active(self) -> bool: + return any(self.loss_enabled.values()) + + def toggle_losses(self, step) -> None: + for key in self.loss_activ_step: + self.loss_enabled[key] = step >= self.loss_activ_step[key] + + def extra_repr(self) -> str: + msg = [] + if self.shape_weight > 0: + msg.append(f'Shape weight: {self.shape_weight}') + if self.expression_weight > 0: + msg.append(f'Expression weight: {self.expression_weight}') + if self.global_orient_weight > 0: + msg.append(f'Global pose weight: {self.global_orient_weight}') + if self.body_pose_weight > 0: + msg.append(f'Body pose weight: {self.body_pose_weight}') + if self.left_hand_pose_weight > 0: + msg.append(f'Left hand pose weight: {self.left_hand_pose_weight}') + if self.right_hand_pose_weight > 0: + msg.append(f'Right hand pose weight {self.right_hand_pose_weight}') + if self.jaw_pose_weight > 0: + msg.append(f'Jaw pose prior weight: {self.jaw_pose_weight}') + return '\n'.join(msg) + + def single_loss_step(self, parameters, target_params, + target_param_idxs, + gt_vertices=None, + device=None, + keyp_confs=None, + penalize_only_parts=False, + ): + losses = defaultdict( + lambda: torch.tensor(0, device=device, dtype=torch.float32)) + + param_vertices = parameters.get('vertices', None) + compute_edge_loss = (self.edge_weight > 0 and + param_vertices is not None and + gt_vertices is not None and + not penalize_only_parts) + if compute_edge_loss: + edge_loss_val = self.edge_loss( + gt_vertices=gt_vertices, est_vertices=param_vertices) + losses['mesh_edge_loss'] = self.edge_weight * edge_loss_val + + compute_shape_loss = ( + self.shape_weight > 0 and self.loss_enabled['betas'] and + 'betas' in target_params and not penalize_only_parts + ) + if compute_shape_loss: + losses['shape_loss'] = ( + self.shape_loss( + parameters['betas'][target_param_idxs['betas']], + +-- Chunk 2 -- +// smplx_loss_modules.py:189-338 + target_params['betas']) * + self.shape_weight) + + compute_expr_loss = (self.expression_weight > 0 and + self.loss_enabled['expression'] and + 'expression' in target_param_idxs) + if compute_expr_loss: + expr_idxs = target_param_idxs['expression'] + weights = ( + keyp_confs['face'].mean(axis=1) + if self.expr_use_conf_weight else None) + if weights is not None: + num_ones = [1] * len(parameters['expression'].shape[1:]) + weights = weights.view(-1, *num_ones) + weights = weights[expr_idxs] + + losses['expression_loss'] = ( + self.expression_loss( + parameters['expression'][expr_idxs], + target_params['expression'], + weights=weights) * + self.expression_weight) + + compute_global_orient_loss = ( + self.global_orient_weight > 0 and self.loss_enabled['betas'] and + 'global_orient' in target_params and not penalize_only_parts + ) + if compute_global_orient_loss: + global_orient_idxs = target_param_idxs['global_orient'] + losses['global_orient_loss'] = ( + self.global_orient_loss( + parameters['global_orient'][global_orient_idxs], + target_params['global_orient']) * + self.global_orient_weight) + + compute_body_pose_loss = ( + self.body_pose_weight > 0 and self.loss_enabled['betas'] and + 'body_pose' in target_params and not penalize_only_parts) + + if compute_body_pose_loss: + body_pose_idxs = target_param_idxs['body_pose'] + losses['body_pose_loss'] = ( + self.body_pose_loss( + parameters['body_pose'][body_pose_idxs], + target_params['body_pose']) * + self.body_pose_weight) + + if (self.left_hand_pose_weight > 0 and + self.loss_enabled['left_hand_pose'] and + 'left_hand_pose' in target_param_idxs): + num_left_hand_joints = parameters['left_hand_pose'].shape[1] + weights = ( + keyp_confs['left_hand'].mean(axis=1, keepdim=True).expand( + -1, num_left_hand_joints).reshape(-1) + if self.lhand_use_conf else None) + if weights is not None: + num_ones = [1] * len( + parameters['left_hand_pose'].shape[2:]) + weights = weights.view(-1, num_left_hand_joints, *num_ones) + weights = weights[target_param_idxs['left_hand_pose']] + losses['left_hand_pose_loss'] = ( + self.left_hand_pose_loss( + parameters['left_hand_pose'][ + target_param_idxs['left_hand_pose']], + target_params['left_hand_pose'], + weights=weights) * + self.left_hand_pose_weight) + + if (self.right_hand_pose_weight > 0 and + self.loss_enabled['right_hand_pose'] and + 'right_hand_pose' in target_param_idxs): + num_right_hand_joints = parameters['right_hand_pose'].shape[1] + weights = ( + keyp_confs['right_hand'].mean(axis=1, keepdim=True).expand( + -1, num_right_hand_joints).reshape(-1) + if self.rhand_use_conf else None) + if weights is not None: + num_ones = [1] * len( + parameters['right_hand_pose'].shape[2:]) + weights = weights.view(-1, num_left_hand_joints, *num_ones) + weights = weights[target_param_idxs['right_hand_pose']] + losses['right_hand_pose_loss'] = ( + self.right_hand_pose_loss( + parameters['right_hand_pose'][ + target_param_idxs['right_hand_pose']], + target_params['right_hand_pose'], + weights=weights) * + self.right_hand_pose_weight) + + if (self.jaw_pose_weight > 0 and self.loss_enabled['jaw_pose'] and + 'jaw_pose' in target_param_idxs): + weights = ( + keyp_confs['face'].mean(axis=1) + if self.jaw_use_conf_weight else None) + if weights is not None: + num_ones = [1] * len(parameters['jaw_pose'].shape[2:]) + weights = weights.view(-1, 1, *num_ones) + weights = weights[target_param_idxs['jaw_pose']] + + losses['jaw_pose_loss'] = ( + self.jaw_pose_loss( + parameters['jaw_pose'][target_param_idxs['jaw_pose']], + target_params['jaw_pose'], + weights=weights) * + self.jaw_pose_weight) + + return losses + + def forward(self, network_params, targets, num_stages=3, device=None): + if device is None: + device = torch.device('cpu') + + start_idxs = defaultdict(lambda: 0) + in_target_param_idxs = defaultdict(lambda: []) + in_target_params = defaultdict(lambda: []) + + keyp_confs = defaultdict(lambda: []) + for idx, target in enumerate(targets): + # If there are no 3D annotations, skip and add to the starting + # index the number of bounding boxes + if len(target) < 1: + continue + + conf = target.conf + + keyp_confs['body'].append(conf[self.body_idxs]) + keyp_confs['left_hand'].append(conf[self.left_hand_idxs]) + keyp_confs['right_hand'].append(conf[self.right_hand_idxs]) + keyp_confs['face'].append(conf[self.face_idxs]) + + for param_key in PARAM_KEYS: + if not target.has_field(param_key): + start_idxs[param_key] += len(target) + continue + end_idx = start_idxs[param_key] + 1 + in_target_param_idxs[param_key] += list( + range(start_idxs[param_key], end_idx)) + start_idxs[param_key] += 1 + + in_target_params[param_key].append( + target.get_field(param_key)) + + # Stack all confidences + for key in keyp_confs: + keyp_confs[key] = torch.stack(keyp_confs[key]) + + target_params = {} + for key, val in in_target_params.items(): + if key == 'hand_pose': + target_params['left_hand_pose'] = torch.stack([ + +-- Chunk 3 -- +// smplx_loss_modules.py:339-392 + t.left_hand_pose + for t in val]) + target_params['right_hand_pose'] = torch.stack([ + t.right_hand_pose + for t in val]) + else: + target_params[key] = torch.stack([ + getattr(t, key) + for t in val]) + + target_param_idxs = {} + for key in in_target_param_idxs.keys(): + if key == 'hand_pose': + target_param_idxs['left_hand_pose'] = torch.tensor( + np.asarray(in_target_param_idxs[key]), + device=device, + dtype=torch.long) + target_param_idxs['right_hand_pose'] = target_param_idxs[ + 'left_hand_pose'].clone() + else: + target_param_idxs[key] = torch.tensor( + np.asarray(in_target_param_idxs[key]), + device=device, + dtype=torch.long) + + has_vertices = all([t.has_field('vertices') for t in targets]) + gt_vertices = None + if has_vertices: + gt_vertices = torch.stack([ + t.get_field('vertices').vertices for t in targets]) + + stages_to_penalize = self.stages_to_penalize.copy() + if -1 in stages_to_penalize: + stages_to_penalize[stages_to_penalize.index(-1)] = num_stages + 1 + output_losses = {} + for n in range(1, len(network_params) + 1): + if n not in stages_to_penalize: + continue + curr_params = network_params[n - 1] + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_loss_step( + curr_params, target_params, + target_param_idxs, device=device, + keyp_confs=keyp_confs, + gt_vertices=gt_vertices) + for key in curr_losses: + output_losses[f'stage_{n - 1:02d}_{key}'] = curr_losses[key] + + return output_losses + + + +-- Chunk 4 -- +// smplx_loss_modules.py:393-542 +ss RegularizerModule(nn.Module): + def __init__(self, loss_cfg, + body_pose_mean=None, left_hand_pose_mean=None, + right_hand_pose_mean=None, jaw_pose_mean=None): + super(RegularizerModule, self).__init__() + + self.stages_to_regularize = loss_cfg.get('stages_to_penalize', [-1]) + logger.info(f'Stages to regularize: {self.stages_to_regularize}') + + # Construct the shape prior + shape_prior_type = loss_cfg.shape.prior.type + self.shape_prior_weight = loss_cfg.shape.prior.weight + if self.shape_prior_weight > 0: + self.shape_prior = build_prior(shape_prior_type, + **loss_cfg.shape.prior) + logger.debug(f'Shape prior {self.shape_prior}') + + # Construct the expression prior + expression_prior_cfg = loss_cfg.expression.prior + expression_prior_type = expression_prior_cfg.type + self.expression_prior_weight = expression_prior_cfg.weight + if self.expression_prior_weight > 0: + self.expression_prior = build_prior( + expression_prior_type, + **expression_prior_cfg) + logger.debug(f'Expression prior {self.expression_prior}') + + # Construct the body pose prior + body_pose_prior_cfg = loss_cfg.body_pose.prior + body_pose_prior_type = body_pose_prior_cfg.type + self.body_pose_prior_weight = body_pose_prior_cfg.weight + if self.body_pose_prior_weight > 0: + self.body_pose_prior = build_prior( + body_pose_prior_type, + mean=body_pose_mean, + **body_pose_prior_cfg) + logger.debug(f'Body pose prior {self.body_pose_prior}') + + # Construct the left hand pose prior + left_hand_prior_cfg = loss_cfg.left_hand_pose.prior + left_hand_pose_prior_type = left_hand_prior_cfg.type + self.left_hand_pose_prior_weight = left_hand_prior_cfg.weight + if self.left_hand_pose_prior_weight > 0: + self.left_hand_pose_prior = build_prior( + left_hand_pose_prior_type, + mean=left_hand_pose_mean, + **left_hand_prior_cfg) + logger.debug(f'Left hand pose prior {self.left_hand_pose_prior}') + + # Construct the right hand pose prior + right_hand_prior_cfg = loss_cfg.right_hand_pose.prior + right_hand_pose_prior_type = right_hand_prior_cfg.type + self.right_hand_pose_prior_weight = right_hand_prior_cfg.weight + if self.right_hand_pose_prior_weight > 0: + self.right_hand_pose_prior = build_prior( + right_hand_pose_prior_type, mean=right_hand_pose_mean, + **right_hand_prior_cfg) + logger.debug(f'Right hand pose prior {self.right_hand_pose_prior}') + + # Construct the jaw pose prior + jaw_pose_prior_cfg = loss_cfg.jaw_pose.prior + jaw_pose_prior_type = jaw_pose_prior_cfg.type + self.jaw_pose_prior_weight = jaw_pose_prior_cfg.weight + if self.jaw_pose_prior_weight > 0: + self.jaw_pose_prior = build_prior( + jaw_pose_prior_type, mean=jaw_pose_mean, **jaw_pose_prior_cfg) + logger.debug(f'Jaw pose prior {self.jaw_pose_prior}') + + logger.debug(self) + + def extra_repr(self) -> str: + msg = [] + if self.shape_prior_weight > 0: + msg.append('Shape prior weight: {}'.format( + self.shape_prior_weight)) + if self.expression_prior_weight > 0: + msg.append('Expression prior weight: {}'.format( + self.expression_prior_weight)) + if self.body_pose_prior_weight > 0: + msg.append('Body pose prior weight: {}'.format( + self.body_pose_prior_weight)) + if self.left_hand_pose_prior_weight > 0: + msg.append('Left hand pose prior weight: {}'.format( + self.left_hand_pose_prior_weight)) + if self.right_hand_pose_prior_weight > 0: + msg.append('Right hand pose prior weight {}'.format( + self.right_hand_pose_prior_weight)) + if self.jaw_pose_prior_weight > 0: + msg.append('Jaw pose prior weight: {}'.format( + self.jaw_pose_prior_weight)) + return '\n'.join(msg) + + def single_regularization_step(self, parameters, + penalize_only_parts=False, + **kwargs): + prior_losses = {} + + betas = parameters.get('betas', None) + reg_shape = (self.shape_prior_weight > 0 and betas is not None and + not penalize_only_parts) + if reg_shape: + prior_losses['shape_prior'] = ( + self.shape_prior_weight * self.shape_prior(betas)) + + expression = parameters.get('expression', None) + reg_expression = ( + self.expression_prior_weight > 0 and expression is not None) + if reg_expression: + prior_losses['expression_prior'] = ( + self.expression_prior(expression) * + self.expression_prior_weight) + + body_pose = parameters.get('body_pose', None) + betas = parameters.get('betas', None) + reg_body_pose = ( + self.body_pose_prior_weight > 0 and body_pose is not None and + not penalize_only_parts) + if reg_body_pose: + prior_losses['body_pose_prior'] = ( + self.body_pose_prior(body_pose) * + self.body_pose_prior_weight) + + left_hand_pose = parameters.get('left_hand_pose', None) + if (self.left_hand_pose_prior_weight > 0 and + left_hand_pose is not None): + prior_losses['left_hand_pose_prior'] = ( + self.left_hand_pose_prior(left_hand_pose) * + self.left_hand_pose_prior_weight) + + right_hand_pose = parameters.get('right_hand_pose', None) + if (self.right_hand_pose_prior_weight > 0 and + right_hand_pose is not None): + prior_losses['right_hand_pose_prior'] = ( + self.right_hand_pose_prior(right_hand_pose) * + self.right_hand_pose_prior_weight) + + jaw_pose = parameters.get('jaw_pose', None) + if self.jaw_pose_prior_weight > 0 and jaw_pose is not None: + prior_losses['jaw_pose_prior'] = ( + self.jaw_pose_prior(jaw_pose) * + self.jaw_pose_prior_weight) + + return prior_losses + + def forward(self, + param_list, + num_stages=3, + **kwargs) -> Dict[str, Tensor]: + + prior_losses = defaultdict(lambda: 0) + +-- Chunk 5 -- +// smplx_loss_modules.py:543-561 + for n in range(1, num_stages + 1): + if n not in self.stages_to_regularize: + continue + curr_params = param_list[n - 1] + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_regularization_step(curr_params) + for key in curr_losses: + prior_losses[f'stage_{n - 1:02d}_{key}'] = curr_losses[key] + + if num_stages < len(param_list): + curr_params = param_list[-1] + final_losses = self.single_regularization_step(curr_params) + for key in final_losses: + prior_losses[ + f'stage_{num_stages:02d}_{key}'] = final_losses[key] + return prior_losses + +=== File: expose/models/common/networks.py === + +-- Chunk 1 -- +// networks.py:33-44 + create_activation(activ_type='relu', lrelu_slope=0.2, + inplace=True, **kwargs): + if activ_type == 'relu': + return nn.ReLU(inplace=inplace) + elif activ_type == 'leaky-relu': + return nn.LeakyReLU(negative_slope=lrelu_slope, inplace=inplace) + elif activ_type == 'none': + return None + else: + raise ValueError(f'Unknown activation type: {activ_type}') + + + +-- Chunk 2 -- +// networks.py:45-63 + create_norm_layer(input_dim, norm_type='none', num_groups=32, dim=1, + **kwargs): + if norm_type == 'bn': + if dim == 1: + return nn.BatchNorm1d(input_dim) + elif dim == 2: + return nn.BatchNorm2d(input_dim) + else: + raise ValueError(f'Wrong dimension for BN: {dim}') + if norm_type == 'ln': + return nn.LayerNorm(input_dim) + elif norm_type == 'gn': + return nn.GroupNorm(num_groups, input_dim) + elif norm_type.lower() == 'none': + return None + else: + raise ValueError(f'Unknown normalization type: {norm_type}') + + + +-- Chunk 3 -- +// networks.py:64-75 + create_adapt_pooling(name='avg', dim='2d', ksize=1): + if dim == '2d': + if name == 'avg': + return nn.AdaptiveAvgPool2d(ksize) + elif name == 'max': + return nn.AdaptiveMaxPool2d(ksize) + else: + raise ValueError(f'Unknown pooling type: {name}') + else: + raise ValueError('Unknown pooling dimensionality: {dim}') + + + +-- Chunk 4 -- +// networks.py:76-147 +ss FrozenBatchNorm2d(nn.Module): + """ + BatchNorm2d where the batch statistics and the affine parameters + are fixed + """ + + def __init__(self, n): + super(FrozenBatchNorm2d, self).__init__() + self.register_buffer("weight", torch.ones(n)) + self.register_buffer("bias", torch.zeros(n)) + self.register_buffer("running_mean", torch.zeros(n)) + self.register_buffer("running_var", torch.ones(n)) + + @staticmethod + def from_bn(module: nn.BatchNorm2d): + ''' Initializes a frozen batch norm module from a batch norm module + ''' + dim = len(module.weight.data) + + frozen_module = FrozenBatchNorm2d(dim) + frozen_module.weight.data = module.weight.data + + missing, not_found = frozen_module.load_state_dict( + module.state_dict(), strict=False) + return frozen_module + + @classmethod + def convert_frozen_batchnorm(cls, module): + """ + Convert BatchNorm/SyncBatchNorm in module into FrozenBatchNorm. + + Args: + module (torch.nn.Module): + + Returns: + If module is BatchNorm/SyncBatchNorm, returns a new module. + Otherwise, in-place convert module and return it. + + Similar to convert_sync_batchnorm in + https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/batchnorm.py + """ + bn_module = nn.modules.batchnorm + bn_module = (bn_module.BatchNorm2d, bn_module.SyncBatchNorm) + res = module + if isinstance(module, bn_module): + res = cls(module.num_features) + if module.affine: + res.weight.data = module.weight.data.clone().detach() + res.bias.data = module.bias.data.clone().detach() + res.running_mean.data = module.running_mean.data + res.running_var.data = module.running_var.data + res.eps = module.eps + else: + for name, child in module.named_children(): + new_child = cls.convert_frozen_batchnorm(child) + if new_child is not child: + res.add_module(name, new_child) + return res + + def forward(self, x): + # Cast all fixed parameters to half() if necessary + if x.dtype == torch.float16: + self.weight = self.weight.half() + self.bias = self.bias.half() + self.running_mean = self.running_mean.half() + self.running_var = self.running_var.half() + + return F.batch_norm( + x, self.running_mean, self.running_var, self.weight, self.bias, + False) + + + +-- Chunk 5 -- +// networks.py:148-178 +ss ConvNormActiv(nn.Module): + def __init__(self, input_dim, output_dim, kernel_size=1, + activation='relu', + norm_type='bn', + padding=0, + **kwargs): + super(ConvNormActiv, self).__init__() + layers = [] + + norm_layer = create_norm_layer(output_dim, norm_type, + dim=2, + **kwargs) + bias = norm_layer is None + + layers.append( + nn.Conv2d(input_dim, output_dim, kernel_size=kernel_size, + padding=padding, + bias=bias)) + if norm_layer is not None: + layers.append(norm_layer) + + activ = create_activation(**kwargs) + if activ is not None: + layers.append(activ) + + self.model = nn.Sequential(*layers) + + def forward(self, x): + return self.model(x) + + + +-- Chunk 6 -- +// networks.py:179-260 +ss MLP(nn.Module): + def __init__( + self, + input_dim: int, + output_dim: int, + layers: Optional[List[int]] = None, + activation: str = 'relu', + norm_type: str = 'bn', + dropout: float = 0.0, + gain: float = 0.01, + preactivated: bool = False, + flatten: bool = True, + **kwargs + ): + ''' Simple MLP module + ''' + super(MLP, self).__init__() + if layers is None: + layers = [] + self.flatten = flatten + + curr_input_dim = input_dim + self.num_layers = len(layers) + + self.blocks = [] + for layer_idx, layer_dim in enumerate(layers): + activ = create_activation(**kwargs) + norm_layer = create_norm_layer(layer_dim, norm_type, **kwargs) + bias = norm_layer is None + + linear = nn.Linear(curr_input_dim, layer_dim, bias=bias) + curr_input_dim = layer_dim + + layer = [] + if preactivated: + if norm_layer is not None: + layer.append(norm_layer) + + if activ is not None: + layer.append(activ) + + layer.append(linear) + + if dropout > 0.0: + layer.append(nn.Dropout(dropout)) + else: + layer.append(linear) + + if activ is not None: + layer.append(activ) + + if norm_layer is not None: + layer.append(norm_layer) + + if dropout > 0.0: + layer.append(nn.Dropout(dropout)) + + block = nn.Sequential(*layer) + self.add_module('layer_{:03d}'.format(layer_idx), block) + self.blocks.append(block) + + self.output_layer = nn.Linear(curr_input_dim, output_dim) + init_weights(self.output_layer, gain=gain, + init_type='xavier', + distr='uniform') + + def extra_repr(self): + msg = [] + msg.append('Flatten: {}'.format(self.flatten)) + return '\n'.join(msg) + + def forward(self, module_input): + batch_size = module_input.shape[0] + # Flatten all dimensions + curr_input = module_input + if self.flatten: + curr_input = curr_input.view(batch_size, -1) + for block in self.blocks: + curr_input = block(curr_input) + return self.output_layer(curr_input) + + + +-- Chunk 7 -- +// networks.py:261-344 +ss IterativeRegression(nn.Module): + def __init__(self, module, mean_param, num_stages=1, + append_params=True, learn_mean=False, + detach_mean=False, dim=1, + **kwargs): + super(IterativeRegression, self).__init__() + logger.info(f'Building iterative regressor with {num_stages} stages') + + self.module = module + self._num_stages = num_stages + self.dim = dim + + if learn_mean: + self.register_parameter('mean_param', + nn.Parameter(mean_param, + requires_grad=True)) + else: + self.register_buffer('mean_param', mean_param) + + self.append_params = append_params + self.detach_mean = detach_mean + logger.info(f'Detach mean: {self.detach_mean}') + + def get_mean(self): + return self.mean_param.clone() + + @property + def num_stages(self): + return self._num_stages + + def extra_repr(self): + msg = [ + f'Num stages = {self.num_stages}', + f'Concatenation dimension: {self.dim}', + f'Detach mean: {self.detach_mean}', + ] + return '\n'.join(msg) + + def forward( + self, + features: Tensor, + cond: Optional[Tensor] = None + ) -> Tuple[List[Tensor], List[Tensor]]: + ''' Computes deltas on top of condition iteratively + + Parameters + ---------- + features: torch.Tensor + Input features + ''' + batch_size = features.shape[0] + expand_shape = [batch_size] + [-1] * len(features.shape[1:]) + + parameters = [] + deltas = [] + module_input = features + + if cond is None: + cond = self.mean_param.expand(*expand_shape).clone() + + # Detach mean + if self.detach_mean: + cond = cond.detach() + + if self.append_params: + assert features is not None, ( + 'Features are none even though append_params is True') + + module_input = torch.cat([ + module_input, + cond], + dim=self.dim) + deltas.append(self.module(module_input)) + num_params = deltas[-1].shape[1] + parameters.append(cond[:, :num_params].clone() + deltas[-1]) + + for stage_idx in range(1, self.num_stages): + module_input = torch.cat( + [features, parameters[stage_idx - 1]], dim=-1) + params_upd = self.module(module_input) + deltas.append(params_upd) + parameters.append(parameters[stage_idx - 1] + params_upd) + + return parameters, deltas + +=== File: expose/models/common/flame_loss_modules.py === + +-- Chunk 1 -- +// flame_loss_modules.py:39-188 +ss FLAMELossModule(nn.Module): + ''' + ''' + + def __init__(self, loss_cfg, use_face_contour=False): + super(FLAMELossModule, self).__init__() + + self.penalize_final_only = loss_cfg.get('penalize_final_only', True) + self.loss_enabled = defaultdict(lambda: True) + self.loss_activ_step = {} + + idxs_dict = get_part_idxs() + head_idxs = idxs_dict['flame'] + if not use_face_contour: + head_idxs = head_idxs[:-17] + + self.register_buffer('head_idxs', torch.tensor(head_idxs)) + + # TODO: Add vertex loss + vertices_loss_cfg = loss_cfg.vertices + self.vertices_weight = vertices_loss_cfg.get('weight', 0.0) + self.vertices_loss = build_loss(**vertices_loss_cfg) + self.loss_activ_step['vertices'] = vertices_loss_cfg.enable + + self.use_alignment = vertices_loss_cfg.get('use_alignment', False) + if self.use_alignment: + self.alignment = RotationTranslationAlignment() + + edge_loss_cfg = loss_cfg.get('edge', {}) + self.edge_weight = edge_loss_cfg.get('weight', 0.0) + self.edge_loss = build_loss(**edge_loss_cfg) + self.loss_activ_step['edge'] = edge_loss_cfg.get('enable', 0) + + shape_loss_cfg = loss_cfg.shape + self.shape_weight = shape_loss_cfg.weight + self.shape_loss = build_loss(**shape_loss_cfg) + self.loss_activ_step['shape'] = shape_loss_cfg.enable + + expression_cfg = loss_cfg.get('expression', {}) + + self.expression_weight = expression_cfg.weight + if self.expression_weight > 0: + self.expression_loss = build_loss(**expression_cfg) + self.loss_activ_step[ + 'expression'] = expression_cfg.enable + + global_orient_cfg = loss_cfg.global_orient + self.global_orient_loss = build_loss(**global_orient_cfg) + logger.debug(f'Global pose loss: {self.global_orient_loss}') + self.global_orient_weight = global_orient_cfg.weight + self.loss_activ_step['global_orient'] = global_orient_cfg.enable + + jaw_pose_cfg = loss_cfg.get('jaw_pose', {}) + jaw_pose_loss_type = jaw_pose_cfg.type + self.jaw_pose_weight = jaw_pose_cfg.weight + + if self.jaw_pose_weight > 0: + self.jaw_pose_loss_type = jaw_pose_loss_type + self.jaw_pose_loss = build_loss(**jaw_pose_cfg) + logger.debug('Jaw pose loss: {}', self.jaw_pose_loss) + self.loss_activ_step['jaw_pose'] = jaw_pose_cfg.enable + + face_edge_2d_cfg = loss_cfg.get('face_edge_2d', {}) + self.face_edge_2d_weight = face_edge_2d_cfg.get('weight', 0.0) + self.face_edge_2d_enable_at = face_edge_2d_cfg.get('enable', 0) + if self.face_edge_2d_weight > 0: + face_connections = [] + for conn in FACE_CONNECTIONS: + if ('contour' in KEYPOINT_NAMES[conn[0]] or + 'contour' in KEYPOINT_NAMES[conn[1]]): + if not use_face_contour: + continue + face_connections.append(conn) + + self.face_edge_2d_loss = build_loss( + type='edge', connections=face_connections, **face_edge_2d_cfg) + logger.debug('2D face edge loss: {}', self.face_edge_2d_loss) + self.face_edge_2d_active = False + + face_joints2d_cfg = loss_cfg.joints_2d + self.face_joints_2d_weight = face_joints2d_cfg.weight + self.face_joints_2d_enable_at = face_joints2d_cfg.enable + if self.face_joints_2d_weight > 0: + self.face_joints_2d_loss = build_loss(**face_joints2d_cfg) + logger.debug('2D face joints loss: {}', self.face_joints_2d_loss) + self.face_joints_2d_active = False + + face_joints3d_cfg = loss_cfg.joints_3d + self.face_joints_3d_weight = face_joints3d_cfg.weight + self.face_joints_3d_enable_at = face_joints3d_cfg.enable + if self.face_joints_3d_weight > 0: + self.face_joints_3d_loss = build_loss(**face_joints3d_cfg) + logger.debug('3D face joints loss: {}', self.face_joints_3d_loss) + self.face_joints_3d_active = False + + def is_active(self) -> bool: + return any(self.loss_enabled.values()) + + def toggle_losses(self, step) -> None: + for key in self.loss_activ_step: + self.loss_enabled[key] = step >= self.loss_activ_step[key] + + def extra_repr(self) -> str: + msg = [] + msg.append('Shape weight: {}'.format(self.shape_weight)) + if self.expression_weight > 0: + msg.append(f'Expression weight: {self.expression_weight}') + msg.append(f'Global pose weight: {self.global_orient_weight}') + if self.jaw_pose_weight > 0: + msg.append(f'Jaw pose weight: {self.jaw_pose_weight}') + return '\n'.join(msg) + + def single_loss_step(self, parameters, + global_orient=None, + jaw_pose=None, + betas=None, + expression=None, + gt_vertices=None, + device=None, + keyp_confs=None, + gt_expression_idxs=None, + ): + losses = defaultdict( + lambda: torch.tensor(0, device=device, dtype=torch.float32)) + + if (self.shape_weight > 0 and self.loss_enabled['betas'] and + betas is not None): + shape_common_dim = min(parameters['betas'].shape[-1], + betas.shape[-1]) + losses['shape_loss'] = ( + self.shape_loss(parameters['betas'][:, :shape_common_dim], + betas[:, :shape_common_dim]) * + self.shape_weight) + + param_vertices = parameters.get('vertices', None) + compute_vertex_loss = (self.vertices_weight > 0 and + param_vertices is not None and + gt_vertices is not None) + if compute_vertex_loss: + if self.use_alignment: + aligned_verts = self.alignment(param_vertices, gt_vertices) + else: + aligned_verts = param_vertices + losses['vertex_loss'] = self.vertices_weight * self.vertices_loss( + aligned_verts, gt_vertices) + + compute_edge_loss = (self.edge_weight > 0 and + param_vertices is not None and + gt_vertices is not None) + if compute_edge_loss: + +-- Chunk 2 -- +// flame_loss_modules.py:189-316 + edge_loss_val = self.edge_loss( + gt_vertices=gt_vertices, est_vertices=param_vertices) + losses['mesh_edge_loss'] = self.edge_weight * edge_loss_val + + if (self.expression_weight > 0 and self.loss_enabled['expression'] and + expression is not None): + expr_common_dim = min( + parameters['expression'].shape[-1], expression.shape[-1]) + pred_expr = parameters['expression'][:, :expr_common_dim] + if gt_expression_idxs is not None: + pred_expr = pred_expr[gt_expression_idxs] + + losses['expression_loss'] = ( + self.expression_loss( + pred_expr, expression[:, :expr_common_dim]) * + self.expression_weight) + + if (self.global_orient_weight > 0 and + self.loss_enabled['global_orient'] and + global_orient is not None): + losses['global_orient_loss'] = ( + self.global_orient_loss( + parameters['head_pose'], global_orient) * + self.global_orient_weight) + + if (self.jaw_pose_weight > 0 and self.loss_enabled['jaw_pose'] and + jaw_pose is not None): + losses['jaw_pose_loss'] = ( + self.jaw_pose_loss( + parameters['jaw_pose'], jaw_pose) * + self.jaw_pose_weight) + + return losses + + def forward(self, input_dict, + head_targets, + device=None): + if device is None: + device = torch.device('cpu') + + # Stack the GT keypoints and conf for the predictions of the right hand + face_keyps = torch.stack([t.smplx_keypoints for t in head_targets]) + face_conf = torch.stack([t.conf for t in head_targets]) + + # Get the GT pose of the right hand + global_orient = torch.stack( + [t.get_field('global_orient').global_orient for t in head_targets]) + # Get the GT pose of the right hand + gt_jaw_pose = torch.stack( + [t.get_field('jaw_pose').jaw_pose + for t in head_targets]) + + has_vertices = all( + [t.has_field('vertices') for t in head_targets]) + gt_vertices = None + if has_vertices: + gt_vertices = torch.stack([ + t.get_field('vertices').vertices + for t in head_targets]) + # Get the GT pose of the right hand + gt_expression = torch.stack([t.get_field('expression').expression + for t in head_targets + if t.has_field('expression')]) + gt_expression_idxs = torch.tensor( + [idx for idx, t in enumerate(head_targets) + if t.has_field('expression')], device=device, dtype=torch.long) + + output_losses = {} + compute_2d_loss = ('proj_joints' in input_dict and + self.face_joints_2d_weight > 0) + if compute_2d_loss: + face_proj_joints = input_dict['proj_joints'] + face_joints2d = self.face_joints_2d_loss( + face_proj_joints, + face_keyps[:, self.head_idxs], + weights=face_conf[:, self.head_idxs]) + output_losses['head_branch_joints2d'] = ( + face_joints2d * self.face_joints_2d_weight) + + head_keyps = [t.get_field('keypoints3d').smplx_keypoints + for t in head_targets + if t.has_field('keypoints3d')] + head_conf = [t.get_field('keypoints3d').conf for t in head_targets + if t.has_field('keypoints3d')] + # Keep the indices of the targets that have 3D joint annotations + head_idxs = [idx for idx, t in enumerate(head_targets) + if t.has_field('keypoints3d')] + + num_stages = input_dict.get('num_stages', 1) + curr_params = input_dict.get(f'stage_{num_stages - 1:02d}', None) + joints3d = curr_params['joints'] + compute_3d_joint_loss = (self.face_joints_3d_weight > 0 and + len(head_conf) > 0) + if compute_3d_joint_loss: + all_keyps3d = torch.stack(head_keyps, dim=0)[:, self.head_idxs] + all_conf3d = torch.stack(head_conf, dim=0)[:, self.head_idxs] + + head_keyp3d_loss = self.face_joints_3d_loss( + joints3d[head_idxs], + all_keyps3d, + weights=all_conf3d + ) * self.face_joints_3d_weight + output_losses['head_branch_joints3d'] = head_keyp3d_loss + + for n in range(1, num_stages + 1): + if self.penalize_final_only and n < num_stages: + continue + curr_params = input_dict.get(f'stage_{n - 1:02d}', None) + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_loss_step( + curr_params, + jaw_pose=gt_jaw_pose, + global_orient=global_orient, + expression=gt_expression, + gt_vertices=gt_vertices, + device=device, + gt_expression_idxs=gt_expression_idxs, + ) + for key in curr_losses: + out_key = f'stage_{n - 1:02d}_{key}' + output_losses[out_key] = curr_losses[key] + + return output_losses + + + +-- Chunk 3 -- +// flame_loss_modules.py:317-407 +ss RegularizerModule(nn.Module): + def __init__(self, loss_cfg, + num_stages=3, jaw_pose_mean=None): + super(RegularizerModule, self).__init__() + + self.regularize_final_only = loss_cfg.get( + 'regularize_final_only', True) + self.num_stages = num_stages + + # Construct the shape prior + shape_prior_type = loss_cfg.shape.prior.type + self.shape_prior_weight = loss_cfg.shape.prior.weight + if self.shape_prior_weight > 0: + self.shape_prior = build_prior(shape_prior_type, + **loss_cfg.shape.prior) + logger.debug(f'Shape prior {self.shape_prior}') + + # Construct the expression prior + expression_prior_cfg = loss_cfg.expression.prior + expression_prior_type = expression_prior_cfg.type + self.expression_prior_weight = expression_prior_cfg.weight + if self.expression_prior_weight > 0: + self.expression_prior = build_prior( + expression_prior_type, + **expression_prior_cfg) + logger.debug(f'Expression prior {self.expression_prior}') + + # Construct the jaw pose prior + jaw_pose_prior_cfg = loss_cfg.jaw_pose.prior + jaw_pose_prior_type = jaw_pose_prior_cfg.type + self.jaw_pose_prior_weight = jaw_pose_prior_cfg.weight + if self.jaw_pose_prior_weight > 0: + self.jaw_pose_prior = build_prior( + jaw_pose_prior_type, mean=jaw_pose_mean, **jaw_pose_prior_cfg) + logger.debug(f'Jaw pose prior {self.jaw_pose_prior}') + + logger.debug(self) + + def extra_repr(self) -> str: + msg = [] + if self.shape_prior_weight > 0: + msg.append(f'Shape prior weight: {self.shape_prior_weight}') + if self.expression_prior_weight > 0: + msg.append( + f'Expression prior weight: {self.expression_prior_weight}') + if self.jaw_pose_prior_weight > 0: + msg.append(f'Jaw pose prior weight: {self.jaw_pose_prior_weight}') + return '\n'.join(msg) + + def single_regularization_step(self, parameters, **kwargs): + prior_losses = {} + + betas = parameters.get('betas', None) + if self.shape_prior_weight > 0 and betas is not None: + prior_losses['shape_prior'] = ( + self.shape_prior_weight * self.shape_prior(betas)) + + expression = parameters.get('expression', None) + if self.expression_prior_weight > 0 and expression is not None: + prior_losses['expression_prior'] = ( + self.expression_prior(expression) * + self.expression_prior_weight) + + jaw_pose = parameters.get('jaw_pose', None) + if self.jaw_pose_prior_weight > 0 and jaw_pose is not None: + prior_losses['jaw_pose_prior'] = ( + self.jaw_pose_prior(jaw_pose) * + self.jaw_pose_prior_weight) + + return prior_losses + + def forward(self, + input_dict, + **kwargs) -> Dict[str, Tensor]: + + prior_losses = defaultdict(lambda: 0) + num_stages = input_dict.get('num_stages', 1) + for n in range(1, num_stages + 1): + if self.regularize_final_only and n < self.num_stages: + continue + curr_params = input_dict.get(f'stage_{n - 1:02d}', None) + if curr_params is None: + logger.warning(f'Network output for stage {n} is None') + continue + + curr_losses = self.single_regularization_step(curr_params) + for key, val in curr_losses.items(): + out_key = f'stage_{n - 1:02d}_{key}' + prior_losses[out_key] = val + + return prior_losses + +=== File: expose/models/common/rigid_alignment.py === + +-- Chunk 1 -- +// rigid_alignment.py:28-101 +ss RotationTranslationAlignment(nn.Module): + def __init__(self) -> None: + ''' Implements rotation and translation alignment with least squares + + For more information see: + + Least-Squares Rigid Motion Using SVD + Olga Sorkine-Hornung and Michael Rabinovich + + ''' + super(RotationTranslationAlignment, self).__init__() + + def forward( + self, + p: Tensor, + q: Tensor) -> Tensor: + ''' Aligns two point clouds using the optimal R, T + + Parameters + ---------- + p: BxNx3, torch.Tensor + The first of points + q: BxNx3, torch.Tensor + + Returns + ------- + p_hat: BxNx3, torch.Tensor + The points p after least squares alignment to q + ''' + batch_size = p.shape[0] + dtype = p.dtype + device = p.device + + p_transpose = p.transpose(1, 2) + q_transpose = q.transpose(1, 2) + + # 1. Remove mean. + p_mean = torch.mean(p_transpose, dim=-1, keepdim=True) + q_mean = torch.mean(q_transpose, dim=-1, keepdim=True) + + p_centered = p_transpose - p_mean + q_centered = q_transpose - q_mean + + # 2. Compute variance of X1 used for scale. + var_p = torch.sum(p_centered.pow(2), dim=(1, 2), keepdim=True) + # var_q = torch.sum(q_centered.pow(2), dim=(1, 2), keepdim=True) + + # Compute the outer product of the two point sets + # Should be Bx3x3 + K = torch.bmm(p_centered, q_centered.transpose(1, 2)) + # Apply SVD on the outer product matrix to recover the rotation + U, S, V = torch.svd(K) + + # Make sure that the computed rotation does not contain a reflection + Z = torch.eye(3, dtype=dtype, device=device).view( + 1, 3, 3).expand(batch_size, -1, -1).contiguous() + + raw_product = torch.bmm(U, V.transpose(1, 2)) + Z[:, -1, -1] *= torch.sign(torch.det(raw_product)) + + # Compute the final rotation matrix + rotation = torch.bmm(V, torch.bmm(Z, U.transpose(1, 2))) + + scale = torch.einsum('bii->b', [torch.bmm(rotation, K)]) / var_p.view( + -1) + + # Compute the translation vector + translation = q_mean - scale.reshape(batch_size, 1, 1) * torch.bmm( + rotation, p_mean) + + return ( + scale.reshape(batch_size, 1, 1) * + torch.bmm(rotation, p_transpose) + + translation).transpose(1, 2) + +=== File: expose/models/common/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/models/common/__init__.py:1-15 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +=== File: expose/models/common/bbox_sampler.py === + +-- Chunk 1 -- +// bbox_sampler.py:30-76 +ss ToCrops(nn.Module): + def __init__(self) -> None: + super(ToCrops, self).__init__() + + def forward( + self, + full_imgs: Union[ImageList, ImageListPacked], + points: Tensor, + targets: GenericTarget, + scale_factor: float = 1.0, + crop_size: int = 256 + ) -> Dict[str, Tensor]: + num_imgs, _, H, W = full_imgs.shape + device = points.device + dtype = points.dtype + + # Get the image to crop transformations and bounding box sizes + crop_transforms = [] + img_bbox_sizes = [] + for t in targets: + crop_transforms.append(t.get_field('crop_transform')) + img_bbox_sizes.append(t.get_field('bbox_size')) + + img_bbox_sizes = torch.tensor( + img_bbox_sizes, dtype=dtype, device=device) + + crop_transforms = torch.tensor( + crop_transforms, dtype=dtype, device=device) + inv_crop_transforms = torch.inverse(crop_transforms) + + center_body_crop, bbox_size = points_to_bbox( + points, bbox_scale_factor=scale_factor) + + orig_bbox_size = bbox_size / crop_size * img_bbox_sizes + # Compute the center of the crop in the original image + center = (torch.einsum( + 'bij,bj->bi', [inv_crop_transforms[:, :2, :2], center_body_crop]) + + inv_crop_transforms[:, :2, 2]) + + return {'center': center.reshape(-1, 2), + 'orig_bbox_size': orig_bbox_size, + 'bbox_size': bbox_size.reshape(-1), + 'inv_crop_transforms': inv_crop_transforms, + 'center_body_crop': 2 * center_body_crop / crop_size - 1, + } + + + +-- Chunk 2 -- +// bbox_sampler.py:77-226 +ss CropSampler(nn.Module): + def __init__( + self, + crop_size: int = 256 + ) -> None: + ''' Uses bilinear sampling to extract square crops + + This module expects a high resolution image as input and a bounding + box, described by its' center and size. It then proceeds to extract + a sub-image using the provided information through bilinear + interpolation. + + Parameters + ---------- + crop_size: int + The desired size for the crop. + ''' + super(CropSampler, self).__init__() + + self.crop_size = crop_size + x = torch.arange(0, crop_size, dtype=torch.float32) / (crop_size - 1) + grid_y, grid_x = torch.meshgrid(x, x) + + points = torch.stack([grid_y.flatten(), grid_x.flatten()], axis=1) + + self.register_buffer('grid', points.unsqueeze(dim=0)) + + def extra_repr(self) -> str: + return f'Crop size: {self.crop_size}' + + def bilinear_sampling(x0, x1, y0, y1): + pass + + def _sample_packed(self, full_imgs: ImageListPacked, sampling_grid, + padding_mode='zeros'): + device, dtype = sampling_grid.device, sampling_grid.dtype + batch_size = sampling_grid.shape[0] + tensor = full_imgs.as_tensor() + + flat_sampling_grid = sampling_grid.reshape(batch_size, -1, 2) + x, y = flat_sampling_grid[:, :, 0], flat_sampling_grid[:, :, 1] + + # Get the closest spatial locations + x0 = torch.floor(x).to(dtype=torch.long) + x1 = x0 + 1 + + y0 = torch.floor(y).to(dtype=torch.long) + y1 = y0 + 1 + + # Size: B + start_idxs = torch.tensor( + full_imgs.starts, dtype=torch.long, device=device) + # Size: 3 + rgb_idxs = torch.arange(3, dtype=torch.long, device=device) + # Size: B + height_tensor = torch.tensor( + full_imgs.heights, dtype=torch.long, device=device) + # Size: B + width_tensor = torch.tensor( + full_imgs.widths, dtype=torch.long, device=device) + + # Size: BxP + x0_in_bounds = x0.ge(0) & x0.le(width_tensor[:, None] - 1) + x1_in_bounds = x0.ge(0) & x0.le(width_tensor[:, None] - 1) + y0_in_bounds = y0.ge(0) & y0.le(height_tensor[:, None] - 1) + y1_in_bounds = y0.ge(0) & y0.le(height_tensor[:, None] - 1) + + zero = torch.tensor(0, dtype=torch.long, device=device) + x0 = torch.max( + torch.min(x0, width_tensor[:, None] - 1), zero) + x1 = torch.max(torch.min(x1, width_tensor[:, None] - 1), zero) + y0 = torch.max(torch.min(y0, height_tensor[:, None] - 1), zero) + y1 = torch.max(torch.min(y1, height_tensor[:, None] - 1), zero) + + flat_rgb_idxs = ( + rgb_idxs[None, :, None] * (width_tensor[:, None, None]) * + height_tensor[:, None, None]) + x0_y0_in_bounds = (x0_in_bounds & y0_in_bounds).unsqueeze( + dim=1).expand(-1, 3, -1) + x1_y0_in_bounds = (x1_in_bounds & y0_in_bounds).unsqueeze( + dim=1).expand(-1, 3, -1) + x0_y1_in_bounds = (x0_in_bounds & y1_in_bounds).unsqueeze( + dim=1).expand(-1, 3, -1) + x1_y1_in_bounds = (x1_in_bounds & y1_in_bounds).unsqueeze( + dim=1).expand(-1, 3, -1) + + idxs_x0_y0 = (start_idxs[:, None, None] + + flat_rgb_idxs + + y0[:, None, :] * + width_tensor[:, None, None] + x0[:, None, :]) + idxs_x1_y0 = (start_idxs[:, None, None] + + flat_rgb_idxs + + y0[:, None, :] * + width_tensor[:, None, None] + x1[:, None, :]) + idxs_x0_y1 = (start_idxs[:, None, None] + + flat_rgb_idxs + + y1[:, None, :] * width_tensor[:, None, None] + + x0[:, None, :]) + idxs_x1_y1 = (start_idxs[:, None, None] + + flat_rgb_idxs + + y1[:, None, :] * width_tensor[:, None, None] + + x1[:, None, :]) + + Ia = torch.zeros(idxs_x0_y0.shape, dtype=dtype, device=device) + Ia[x0_y0_in_bounds] = tensor[idxs_x0_y0[x0_y0_in_bounds]] + + Ib = torch.zeros(idxs_x1_y0.shape, dtype=dtype, device=device) + Ib[x1_y0_in_bounds] = tensor[idxs_x1_y0[x1_y0_in_bounds]] + + Ic = torch.zeros(idxs_x0_y1.shape, dtype=dtype, device=device) + Ic[x0_y1_in_bounds] = tensor[idxs_x0_y1[x0_y1_in_bounds]] + + Id = torch.zeros(idxs_x1_y1.shape, dtype=dtype, device=device) + Id[x1_y1_in_bounds] = tensor[idxs_x1_y1[x1_y1_in_bounds]] + + f1 = (x1 - x)[:, None] * Ia + (x - x0)[:, None] * Ib + f2 = (x1 - x)[:, None] * Ic + (x - x0)[:, None] * Id + + output = (y1 - y)[:, None] * f1 + (y - y0)[:, None] * f2 + return output.reshape(batch_size, 3, self.crop_size, self.crop_size) + + def _sample_padded( + self, + full_imgs: Union[ImageList, Tensor], + sampling_grid: Tensor + ) -> Tensor: + ''' + ''' + tensor = ( + full_imgs.as_tensor() if isinstance(full_imgs, (ImageList,)) else + full_imgs + ) + # Get the sub-images using bilinear interpolation + return F.grid_sample(tensor, sampling_grid, align_corners=True) + + def forward( + self, + full_imgs: Union[Tensor, ImageList, ImageListPacked], + center: Tensor, + bbox_size: Tensor + ) -> Tuple[Tensor, Tensor]: + ''' Crops the HD images using the provided bounding boxes + + Parameters + ---------- + full_imgs: ImageList + An image list structure with the full resolution images + center: torch.Tensor + A Bx2 tensor that contains the coordinates of the center of + the bounding box that will be cropped from the original + +-- Chunk 3 -- +// bbox_sampler.py:227-301 + image + bbox_size: torch.Tensor + A size B tensor that contains the size of the corp + + Returns + ------- + cropped_images: torch.Tensoror + The images cropped from the high resolution input + sampling_grid: torch.Tensor + The grid used to sample the crops + ''' + + batch_size, _, H, W = full_imgs.shape + transforms = torch.eye( + 3, dtype=full_imgs.dtype, device=full_imgs.device).reshape( + 1, 3, 3).expand(batch_size, -1, -1).contiguous() + + hd_to_crop = torch.eye( + 3, dtype=full_imgs.dtype, device=full_imgs.device).reshape( + 1, 3, 3).expand(batch_size, -1, -1).contiguous() + + # Create the transformation that maps crop pixels to image coordinates, + # i.e. pixel (0, 0) from the crop_size x crop_size grid gets mapped to + # the top left of the bounding box, pixel + # (crop_size - 1, crop_size - 1) to the bottom right corner of the + # bounding box + transforms[:, 0, 0] = bbox_size # / (self.crop_size - 1) + transforms[:, 1, 1] = bbox_size # / (self.crop_size - 1) + transforms[:, 0, 2] = center[:, 0] - bbox_size * 0.5 + transforms[:, 1, 2] = center[:, 1] - bbox_size * 0.5 + + hd_to_crop[:, 0, 0] = 2 * (self.crop_size - 1) / bbox_size + hd_to_crop[:, 1, 1] = 2 * (self.crop_size - 1) / bbox_size + hd_to_crop[:, 0, 2] = -( + center[:, 0] - bbox_size * 0.5) * hd_to_crop[:, 0, 0] - 1 + hd_to_crop[:, 1, 2] = -( + center[:, 1] - bbox_size * 0.5) * hd_to_crop[:, 1, 1] - 1 + + size_bbox_sizer = torch.eye( + 3, dtype=full_imgs.dtype, device=full_imgs.device).reshape( + 1, 3, 3).expand(batch_size, -1, -1).contiguous() + + if isinstance(full_imgs, (ImageList, torch.Tensor)): + # Normalize the coordinates to [-1, 1] for the grid_sample function + size_bbox_sizer[:, 0, 0] = 2.0 / (W - 1) + size_bbox_sizer[:, 1, 1] = 2.0 / (H - 1) + size_bbox_sizer[:, :2, 2] = -1 + + # full_transform = transforms + full_transform = torch.bmm(size_bbox_sizer, transforms) + + batch_grid = self.grid.expand(batch_size, -1, -1) + # Convert the grid to image coordinates using the transformations above + sampling_grid = (torch.bmm( + full_transform[:, :2, :2], + batch_grid.transpose(1, 2)) + + full_transform[:, :2, [2]]).transpose(1, 2) + sampling_grid = sampling_grid.reshape( + -1, self.crop_size, self.crop_size, 2).transpose(1, 2) + + if isinstance(full_imgs, (ImageList, torch.Tensor)): + out_images = self._sample_padded( + full_imgs, sampling_grid + ) + elif isinstance(full_imgs, (ImageListPacked, )): + out_images = self._sample_packed(full_imgs, sampling_grid) + else: + raise TypeError( + f'Crop sampling not supported for type: {type(full_imgs)}') + + return {'images': out_images, + 'sampling_grid': sampling_grid.reshape(batch_size, -1, 2), + 'transform': transforms, + 'hd_to_crop': hd_to_crop, + } + +=== File: expose/models/common/pose_utils.py === + +-- Chunk 1 -- +// pose_utils.py:36-56 +ss PoseParameterization(object): + KEYS = ['regressor', 'decoder', 'dim', 'mean', 'ind_dim'] + + def __init__(self, regressor=None, decoder=None, dim=0, ind_dim=0, + mean=None): + super(PoseParameterization, self).__init__() + + self.regressor = regressor + self.decoder = decoder + self.dim = dim + self.mean = mean + self.ind_dim = ind_dim + + def keys(self): + return [key for key in self.KEYS + if getattr(self, key) is not None] + + def __getitem__(self, key): + return getattr(self, key) + + + +-- Chunk 2 -- +// pose_utils.py:57-75 + build_pose_regressor(input_dim: int, + num_angles: int, + pose_cfg: Dict, + network_cfg: Dict, + mean_pose: np.array = None, + pca_basis: np.array = None, + append_params=True) -> Tuple[nn.Module, nn.Module]: + pose_decoder = build_pose_decoder( + pose_cfg, num_angles, mean_pose=mean_pose, + pca_basis=pca_basis) + + pose_dim_size = pose_decoder.get_dim_size() + reg_input_dim = input_dim + append_params * pose_dim_size + + regressor = MLP(reg_input_dim, pose_dim_size, **network_cfg) + + return pose_decoder, regressor + + + +-- Chunk 3 -- +// pose_utils.py:76-128 + create_pose_parameterization(input_dim, num_angles, param_type='aa', + num_pca_comps=12, + latent_dim_size=32, + append_params=True, + create_regressor=True, + **kwargs): + + logger.debug('Creating {} for {} joints', param_type, num_angles) + + regressor = None + + if param_type == 'aa': + input_dim += append_params * num_angles * 3 + if create_regressor: + regressor = MLP(input_dim, num_angles * 3, **kwargs) + decoder = AADecoder(num_angles=num_angles, **kwargs) + dim = decoder.get_dim_size() + ind_dim = 3 + mean = decoder.get_mean() + elif param_type == 'pca': + input_dim += append_params * num_pca_comps + if create_regressor: + regressor = MLP(input_dim, num_pca_comps, **kwargs) + decoder = PCADecoder(num_pca_comps=num_pca_comps, **kwargs) + ind_dim = num_pca_comps + dim = decoder.get_dim_size() + mean = decoder.get_mean() + elif param_type == 'cont_rot_repr': + input_dim += append_params * num_angles * 6 + if create_regressor: + regressor = MLP(input_dim, num_angles * 6, **kwargs) + decoder = ContinuousRotReprDecoder(num_angles, **kwargs) + dim = decoder.get_dim_size() + ind_dim = 6 + mean = decoder.get_mean() + elif param_type == 'rot_mats': + input_dim += append_params * num_angles * 9 + if create_regressor: + regressor = MLP(input_dim, num_angles * 9, **kwargs) + decoder = SVDRotationProjection() + dim = decoder.get_dim_size() + mean = decoder.get_mean() + ind_dim = 9 + else: + raise ValueError(f'Unknown pose parameterization: {param_type}') + + return PoseParameterization(regressor=regressor, + decoder=decoder, + dim=dim, + ind_dim=ind_dim, + mean=mean) + + + +-- Chunk 4 -- +// pose_utils.py:129-144 + build_pose_decoder(cfg, num_angles, mean_pose=None, pca_basis=None): + param_type = cfg.get('param_type', 'aa') + logger.debug('Creating {} for {} joints', param_type, num_angles) + if param_type == 'aa': + decoder = AADecoder(num_angles=num_angles, mean=mean_pose, **cfg) + elif param_type == 'pca': + decoder = PCADecoder(pca_basis=pca_basis, mean=mean_pose, **cfg) + elif param_type == 'cont_rot_repr': + decoder = ContinuousRotReprDecoder(num_angles, mean=mean_pose, **cfg) + elif param_type == 'rot_mats': + decoder = SVDRotationProjection() + else: + raise ValueError(f'Unknown pose decoder: {param_type}') + return decoder + + + +-- Chunk 5 -- +// pose_utils.py:145-213 + build_all_pose_params(body_model_cfg, + feat_extract_depth, + body_model, + append_params=True, + dtype=torch.float32): + mean_pose_path = osp.expandvars(body_model_cfg.mean_pose_path) + mean_poses_dict = {} + if osp.exists(mean_pose_path): + logger.debug('Loading mean pose from: {} ', mean_pose_path) + with open(mean_pose_path, 'rb') as f: + mean_poses_dict = pickle.load(f) + + global_orient_desc = create_pose_parameterization( + feat_extract_depth, 1, dtype=dtype, + append_params=append_params, + create_regressor=False, **body_model_cfg.global_orient) + + global_orient_type = body_model_cfg.get('global_orient', {}).get( + 'param_type', 'cont_rot_repr') + logger.debug('Global pose parameterization, decoder: {}, {}', + global_orient_type, global_orient_desc.decoder) + # Rotate the model 180 degrees around the x-axis + if global_orient_type == 'aa': + global_orient_desc.decoder.mean[0] = math.pi + elif global_orient_type == 'cont_rot_repr': + global_orient_desc.decoder.mean[3] = -1 + + body_pose_desc = create_pose_parameterization( + feat_extract_depth, num_angles=body_model.NUM_BODY_JOINTS, + ignore_hands=True, dtype=dtype, + append_params=append_params, create_regressor=False, + mean=mean_poses_dict.get('body_pose', None), + **body_model_cfg.body_pose) + logger.debug('Body pose decoder: {}', body_pose_desc.decoder) + + left_hand_cfg = body_model_cfg.left_hand_pose + right_hand_cfg = body_model_cfg.right_hand_pose + left_hand_pose_desc = create_pose_parameterization( + feat_extract_depth, num_angles=15, dtype=dtype, + append_params=append_params, + pca_basis=body_model.left_hand_components, + mean=mean_poses_dict.get('left_hand_pose', None), + create_regressor=False, **left_hand_cfg) + logger.debug('Left hand pose decoder: {}', left_hand_pose_desc.decoder) + + right_hand_pose_desc = create_pose_parameterization( + feat_extract_depth, num_angles=15, dtype=dtype, + append_params=append_params, + mean=mean_poses_dict.get('right_hand_pose', None), + pca_basis=body_model.right_hand_components, + create_regressor=False, **right_hand_cfg) + logger.debug('Right hand pose decoder: {}', right_hand_pose_desc.decoder) + + jaw_pose_desc = create_pose_parameterization( + feat_extract_depth, 1, dtype=dtype, + append_params=append_params, + create_regressor=False, **body_model_cfg.jaw_pose) + + logger.debug('Jaw pose decoder: {}', jaw_pose_desc.decoder) + + return { + 'global_orient': global_orient_desc, + 'body_pose': body_pose_desc, + 'left_hand_pose': left_hand_pose_desc, + 'right_hand_pose': right_hand_pose_desc, + 'jaw_pose': jaw_pose_desc, + } + + + +-- Chunk 6 -- +// pose_utils.py:214-245 +ss RotationMatrixRegressor(nn.Linear): + + def __init__(self, input_dim, num_angles, dtype=torch.float32, + append_params=True, **kwargs): + super(RotationMatrixRegressor, self).__init__( + input_dim + append_params * num_angles * 3, + num_angles * 9) + self.num_angles = num_angles + self.dtype = dtype + self.svd_projector = SVDRotationProjection() + + def get_param_dim(self): + return 9 + + def get_dim_size(self): + return self.num_angles * 9 + + def get_mean(self): + return torch.eye(3, dtype=self.dtype).unsqueeze(dim=0).expand( + self.num_angles, -1, -1) + + def forward(self, module_input): + rot_mats = super(RotationMatrixRegressor, self).forward( + module_input).view(-1, 3, 3) + + # Project the matrices on the manifold of rotation matrices using SVD + rot_mats = self.svd_projector(rot_mats).view( + -1, self.num_angles, 3, 3) + + return rot_mats + + + +-- Chunk 7 -- +// pose_utils.py:246-328 +ss ContinuousRotReprDecoder(nn.Module): + ''' Decoder for transforming a latent representation to rotation matrices + + Implements the decoding method described in: + "On the Continuity of Rotation Representations in Neural Networks" + ''' + + def __init__(self, num_angles, dtype=torch.float32, mean=None, + **kwargs): + super(ContinuousRotReprDecoder, self).__init__() + self.num_angles = num_angles + self.dtype = dtype + + if isinstance(mean, dict): + mean = mean.get('cont_rot_repr', None) + if mean is None: + mean = torch.tensor( + [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], + dtype=self.dtype).unsqueeze(dim=0).expand( + self.num_angles, -1).contiguous().view(-1) + + if not torch.is_tensor(mean): + mean = torch.tensor(mean) + mean = mean.reshape(-1, 6) + + if mean.shape[0] < self.num_angles: + logger.debug(mean.shape) + mean = mean.repeat( + self.num_angles // mean.shape[0] + 1, 1).contiguous() + mean = mean[:self.num_angles] + elif mean.shape[0] > self.num_angles: + mean = mean[:self.num_angles] + + mean = mean.reshape(-1) + self.register_buffer('mean', mean) + + def get_type(self): + return 'cont_rot_repr' + + def extra_repr(self): + msg = 'Num angles: {}\n'.format(self.num_angles) + msg += 'Mean: {}'.format(self.mean.shape) + return msg + + def get_param_dim(self): + return 6 + + def get_dim_size(self): + return self.num_angles * 6 + + def get_mean(self): + return self.mean.clone() + + def to_offsets(self, x): + latent = x.reshape(-1, 3, 3)[:, :3, :2].reshape(-1, 6) + return (latent - self.mean).reshape(x.shape[0], -1, 6) + + def encode(self, x, subtract_mean=False): + orig_shape = x.shape + if subtract_mean: + raise NotImplementedError + output = x.reshape(-1, 3, 3)[:, :3, :2].contiguous() + return output.reshape( + orig_shape[0], orig_shape[1], 3, 2) + + def forward(self, module_input): + batch_size = module_input.shape[0] + reshaped_input = module_input.view(-1, 3, 2) + + # Normalize the first vector + b1 = F.normalize(reshaped_input[:, :, 0].clone(), dim=1) + + dot_prod = torch.sum( + b1 * reshaped_input[:, :, 1].clone(), dim=1, keepdim=True) + # Compute the second vector by finding the orthogonal complement to it + b2 = F.normalize(reshaped_input[:, :, 1] - dot_prod * b1, dim=1) + # Finish building the basis by taking the cross product + b3 = torch.cross(b1, b2, dim=1) + rot_mats = torch.stack([b1, b2, b3], dim=-1) + + return rot_mats.view(batch_size, -1, 3, 3) + + + +-- Chunk 8 -- +// pose_utils.py:329-362 +ss ContinuousRotReprRegressor(nn.Linear): + def __init__(self, input_dim, num_angles, dtype=torch.float32, + append_params=True, **kwargs): + super(ContinuousRotReprRegressor, self).__init__( + input_dim + append_params * num_angles * 6, num_angles * 6) + self.append_params = append_params + self.num_angles = num_angles + self.repr_decoder = ContinuousRotReprDecoder(num_angles) + + def get_dim_size(self): + return self.num_angles * 9 + + def get_mean(self): + if self.to_aa: + return torch.zeros([1, self.num_angles * 3], dtype=self.dtype) + else: + return torch.zeros([1, self.num_angles, 3, 3], dtype=self.dtype) + + def forward(self, module_input, prev_val): + if self.append_params: + if self.to_aa: + prev_val = batch_rodrigues(prev_val) + prev_val = prev_val[:, :, :2].contiguous().view( + -1, self.num_angles * 6) + + module_input = torch.cat([module_input, prev_val], dim=-1) + + cont_repr = super(ContinuousRotReprRegressor, + self).forward(module_input) + + output = self.repr_decoder(cont_repr).view(-1, self.num_angles, 3, 3) + return output + + + +-- Chunk 9 -- +// pose_utils.py:363-397 +ss SVDRotationProjection(nn.Module): + def __init__(self, **kwargs): + super(SVDRotationProjection, self).__init__() + + def forward(self, module_input): + # Before converting the output rotation matrices of the VAE to + # axis-angle representation, we first need to make them in to valid + # rotation matrices + with torch.no_grad(): + # TODO: Replace with Batch SVD once merged + # Iterate over the batch dimension and compute the SVD + svd_input = module_input.detach().cpu() + # svd_input = output + norm_rotation = torch.zeros_like(svd_input) + for bidx in range(module_input.shape[0]): + U, _, V = torch.svd(svd_input[bidx]) + + # Multiply the U, V matrices to get the closest orthonormal + # matrix + norm_rotation[bidx] = torch.matmul(U, V.t()) + norm_rotation = norm_rotation.to(module_input.device) + + # torch.svd supports backprop only for full-rank matrices. + # The output is calculated as the valid rotation matrix plus the + # output minus the detached output. If one writes down the + # computational graph for this operation, it will become clear the + # output is the desired valid rotation matrix, while for the + # backward pass gradients are propagated only to the original + # matrix + # Source: PyTorch Gumbel-Softmax hard sampling + # https://pytorch.org/docs/stable/_modules/torch/nn/functional.html#gumbel_softmax + correct_rot = norm_rotation - module_input.detach() + module_input + return correct_rot + + + +-- Chunk 10 -- +// pose_utils.py:398-423 +ss AARegressor(nn.Linear): + def __init__(self, input_dim, num_angles, dtype=torch.float32, + append_params=True, to_aa=True, **kwargs): + super(AARegressor, self).__init__( + input_dim + append_params * num_angles * 3, num_angles * 3) + self.num_angles = num_angles + self.to_aa = to_aa + self.dtype = dtype + + def get_param_dim(self): + return 3 + + def get_dim_size(self): + return self.num_angles * 3 + + def get_mean(self): + return torch.zeros([self.num_angles * 3], dtype=self.dtype) + + def forward(self, features): + aa_vectors = super(AARegressor, self).forward(features).view( + -1, self.num_angles, 3) + + return batch_rodrigues(aa_vectors.view(-1, 3)).view( + -1, self.num_angles, 3, 3) + + + +-- Chunk 11 -- +// pose_utils.py:424-451 +ss AADecoder(nn.Module): + def __init__(self, num_angles, dtype=torch.float32, mean=None, **kwargs): + super(AADecoder, self).__init__() + self.num_angles = num_angles + self.dtype = dtype + + if isinstance(mean, dict): + mean = mean.get('aa', None) + if mean is None: + mean = torch.zeros([num_angles * 3], dtype=dtype) + + if not torch.is_tensor(mean): + mean = torch.tensor(mean, dtype=dtype) + mean = mean.reshape(-1) + self.register_buffer('mean', mean) + + def get_dim_size(self): + return self.num_angles * 3 + + def get_mean(self): + return torch.zeros([self.get_dim_size()], dtype=self.dtype) + + def forward(self, module_input): + output = batch_rodrigues(module_input.view(-1, 3)).view( + -1, self.num_angles, 3, 3) + return output + + + +-- Chunk 12 -- +// pose_utils.py:452-523 +ss PCADecoder(nn.Module): + def __init__(self, num_pca_comps=12, pca_basis=None, dtype=torch.float32, + mean=None, + **kwargs): + super(PCADecoder, self).__init__() + self.num_pca_comps = num_pca_comps + self.dtype = dtype + pca_basis_tensor = torch.tensor(pca_basis, dtype=dtype) + self.register_buffer('pca_basis', + pca_basis_tensor[:self.num_pca_comps]) + inv_basis = torch.inverse( + pca_basis_tensor.t()).unsqueeze(dim=0) + self.register_buffer('inv_pca_basis', inv_basis) + + if isinstance(mean, dict): + mean = mean.get('aa', None) + + if mean is None: + mean = torch.zeros([45], dtype=dtype) + + if not torch.is_tensor(mean): + mean = torch.tensor(mean, dtype=dtype) + mean = mean.reshape(-1).reshape(1, -1) + self.register_buffer('mean', mean) + + def get_param_dim(self): + return self.num_pca_comps + + def extra_repr(self): + msg = 'PCA Components = {}'.format(self.num_pca_comps) + return msg + + def get_mean(self): + return self.mean.clone() + + def get_dim_size(self): + return self.num_pca_comps + + def to_offsets(self, x): + batch_size = x.shape[0] + # Convert the rotation matrices to axis angle + aa = batch_rot2aa(x.reshape(-1, 3, 3)).reshape(batch_size, 45, 1) + + # Project to the PCA space + offsets = torch.matmul( + self.inv_pca_basis, aa + ).reshape(batch_size, -1)[:, :self.num_pca_comps] + + return offsets - self.mean + + def encode(self, x, subtract_mean=False): + batch_size = x.shape[0] + # Convert the rotation matrices to axis angle + aa = batch_rot2aa(x.reshape(-1, 3, 3)).reshape(batch_size, 45, 1) + + # Project to the PCA space + output = torch.matmul( + self.inv_pca_basis, aa + ).reshape(batch_size, -1)[:, :self.num_pca_comps] + if subtract_mean: + # Remove the mean offset + output -= self.mean + + return output + + def forward(self, pca_coeffs): + batch_size = pca_coeffs.shape[0] + decoded = torch.einsum( + 'bi,ij->bj', [pca_coeffs, self.pca_basis]) + self.mean + + return batch_rodrigues(decoded.view(-1, 3)).view( + batch_size, -1, 3, 3) + +=== File: expose/data/utils/sampling.py === + +-- Chunk 1 -- +// sampling.py:23-124 +ss EqualSampler(dutils.Sampler): + def __init__(self, datasets, batch_size=1, ratio_2d=0.5, shuffle=False): + super(EqualSampler, self).__init__(datasets) + self.num_datasets = len(datasets) + self.ratio_2d = ratio_2d + + self.shuffle = shuffle + self.dset_sizes = {} + self.elements_per_index = {} + self.only_2d = {} + self.offsets = {} + start = 0 + for dset in datasets: + self.dset_sizes[dset.name()] = len(dset) + self.offsets[dset.name()] = start + self.only_2d[dset.name()] = dset.only_2d() + self.elements_per_index[ + dset.name()] = dset.get_elements_per_index() + + start += len(dset) + + if ratio_2d < 1.0 and sum(self.only_2d.values()) == len(self.only_2d): + raise ValueError( + f'Invalid 2D ratio value: {ratio_2d} with only 2D data') + + self.length = sum(map(lambda x: len(x), datasets)) + + self.batch_size = batch_size + self._can_reuse_batches = False + logger.info(self) + + def __repr__(self): + msg = 'EqualSampler(batch_size={}, shuffle={}, ratio_2d={}\n'.format( + self.batch_size, self.shuffle, self.ratio_2d) + for dset_name in self.dset_sizes: + msg += '\t{}: {}, only 2D is {}\n'.format( + dset_name, self.dset_sizes[dset_name], + self.only_2d[dset_name]) + + return msg + ')' + + def _prepare_batches(self): + batch_idxs = [] + + dset_idxs = {} + for dset_name, dset_size in self.dset_sizes.items(): + if self.shuffle: + dset_idxs[dset_name] = cycle( + iter(torch.randperm(dset_size).tolist())) + else: + dset_idxs[dset_name] = cycle(range(dset_size)) + + num_batches = self.length // self.batch_size + for bidx in range(num_batches): + curr_idxs = [] + num_samples = 0 + num_2d_only = 0 + max_num_2d = int(self.batch_size * self.ratio_2d) + idxs_add = defaultdict(lambda: 0) + while num_samples < self.batch_size: + for dset_name in dset_idxs: + # If we already have self.ratio_2d * batch_size items with + # 2D annotations then ignore this dataset for now + if num_2d_only >= max_num_2d and self.only_2d[dset_name]: + continue + try: + curr_idxs.append( + next(dset_idxs[dset_name]) + + self.offsets[dset_name]) + num_samples += self.elements_per_index[dset_name] + # If the dataset has only 2D annotations increase the + # count + num_2d_only += (self.elements_per_index[dset_name] * + self.only_2d[dset_name]) + idxs_add[dset_name] += ( + self.elements_per_index[dset_name]) + finally: + pass + if num_samples >= self.batch_size: + break + + curr_idxs = np.array(curr_idxs) + if self.shuffle: + np.random.shuffle(curr_idxs) + batch_idxs.append(curr_idxs) + return batch_idxs + + def __len__(self): + if not hasattr(self, '_batch_idxs'): + self._batch_idxs = self._prepare_batches() + self._can_reuse_bathces = True + return len(self._batch_idxs) + + def __iter__(self): + if self._can_reuse_batches: + batch_idxs = self._batch_idxs + self._can_reuse_batches = False + else: + batch_idxs = self._prepare_batches() + + self._batch_idxs = batch_idxs + return iter(batch_idxs) + +=== File: expose/data/utils/transforms.py === + +-- Chunk 1 -- +// transforms.py:21-34 + flip_pose(pose_vector, pose_format='aa'): + if pose_format == 'aa': + if torch.is_tensor(pose_vector): + dim_flip = DIM_FLIP_TENSOR + else: + dim_flip = DIM_FLIP + return (pose_vector.reshape(-1, 3) * dim_flip).reshape(-1) + elif pose_format == 'rot-mat': + rot_mats = pose_vector.reshape(-1, 9).clone() + + rot_mats[:, [1, 2, 3, 6]] *= -1 + return rot_mats.view_as(pose_vector) + else: + raise ValueError(f'Unknown rotation format: {pose_format}') + +=== File: expose/data/utils/bbox.py === + +-- Chunk 1 -- +// bbox.py:26-46 + points_to_bbox( + points: Tensor, + bbox_scale_factor: float = 1.0) -> Tuple[Tensor, Tensor]: + + min_coords, _ = torch.min(points, dim=1) + xmin, ymin = min_coords[:, 0], min_coords[:, 1] + max_coords, _ = torch.max(points, dim=1) + xmax, ymax = max_coords[:, 0], max_coords[:, 1] + + center = torch.stack( + [xmax + xmin, ymax + ymin], dim=-1) * 0.5 + + width = (xmax - xmin) + height = (ymax - ymin) + + # Convert the bounding box to a square box + size = torch.max(width, height) * bbox_scale_factor + + return center, size + + + +-- Chunk 2 -- +// bbox.py:47-56 + center_size_to_bbox(center: Tensor, size: Tensor) -> Tensor: + xmin = center[:, 0] - size * 0.5 + ymin = center[:, 1] - size * 0.5 + + xmax = center[:, 0] + size * 0.5 + ymax = center[:, 1] + size * 0.5 + + return torch.stack([xmin, ymin, xmax, ymax], axis=-1) + + + +-- Chunk 3 -- +// bbox.py:57-89 + keyps_to_bbox(keypoints, conf, img_size=None, clip_to_img=False, + min_valid_keypoints=6, scale=1.0): + valid_keypoints = keypoints[conf > 0] + if len(valid_keypoints) < min_valid_keypoints: + return None + + xmin, ymin = np.amin(valid_keypoints, axis=0) + xmax, ymax = np.amax(valid_keypoints, axis=0) + # Clip to the image + if img_size is not None and clip_to_img: + H, W, _ = img_size + xmin = np.clip(xmin, 0, W) + xmax = np.clip(xmax, 0, W) + ymin = np.clip(ymin, 0, H) + ymax = np.clip(ymax, 0, H) + + width = (xmax - xmin) * scale + height = (ymax - ymin) * scale + + x_center = 0.5 * (xmax + xmin) + y_center = 0.5 * (ymax + ymin) + xmin = x_center - 0.5 * width + xmax = x_center + 0.5 * width + ymin = y_center - 0.5 * height + ymax = y_center + 0.5 * height + + bbox = np.stack([xmin, ymin, xmax, ymax], axis=0).astype(np.float32) + if bbox_area(bbox) > 0: + return bbox + else: + return None + + + +-- Chunk 4 -- +// bbox.py:90-102 + bbox_to_center_scale(bbox, dset_scale_factor=1.0, ref_bbox_size=200): + if bbox is None: + return None, None, None + bbox = bbox.reshape(-1) + bbox_size = dset_scale_factor * max( + bbox[2] - bbox[0], bbox[3] - bbox[1]) + scale = bbox_size / ref_bbox_size + center = np.stack( + [(bbox[0] + bbox[2]) * 0.5, + (bbox[1] + bbox[3]) * 0.5]).astype(np.float32) + return center, scale, bbox_size + + + +-- Chunk 5 -- +// bbox.py:103-106 + scale_to_bbox_size(scale, ref_bbox_size=200): + return scale * ref_bbox_size + + + +-- Chunk 6 -- +// bbox.py:107-119 + bbox_area(bbox): + if torch.is_tensor(bbox): + if bbox is None: + return 0.0 + xmin, ymin, xmax, ymax = torch.split(bbox.reshape(-1, 4), 1, dim=1) + return torch.abs((xmax - xmin) * (ymax - ymin)).squeeze(dim=-1) + else: + if bbox is None: + return 0.0 + xmin, ymin, xmax, ymax = np.split(bbox.reshape(-1, 4), 4, axis=1) + return np.abs((xmax - xmin) * (ymax - ymin)) + + + +-- Chunk 7 -- +// bbox.py:120-126 + bbox_to_wh(bbox): + if bbox is None: + return (0.0, 0.0) + xmin, ymin, xmax, ymax = np.split(bbox.reshape(-1, 4), 4, axis=1) + return xmax - xmin, ymax - ymin + + + +-- Chunk 8 -- +// bbox.py:127-171 + bbox_iou(bbox1, bbox2, epsilon=1e-9): + ''' Computes IoU between bounding boxes + + Parameters + ---------- + bbox1: torch.Tensor or np.ndarray + A Nx4 array of bounding boxes in xyxy format + bbox2: torch.Tensor or np.ndarray + A Nx4 array of bounding boxes in xyxy format + Returns + ------- + ious: torch.Tensor or np.ndarray + A N dimensional array that contains the IoUs between bounding + box pairs + ''' + if torch.is_tensor(bbox1): + # B + bbox1 = bbox1.reshape(-1, 4) + bbox2 = bbox2.reshape(-1, 4) + + # Should be B + left_top = torch.max(bbox1[:, :2], bbox2[:, :2]) + right_bottom = torch.min(bbox1[:, 2:], bbox2[:, 2:]) + + wh = (right_bottom - left_top).clamp(min=0) + + area1, area2 = bbox_area(bbox1), bbox_area(bbox2) + + isect = wh[:, 0] * wh[:, 1].reshape(bbox1.shape[0]) + union = (area1 + area2 - isect).reshape(bbox1.shape[0]) + else: + bbox1 = bbox1.reshape(4) + bbox2 = bbox2.reshape(4) + + left_top = np.maximum(bbox1[:2], bbox2[:2]) + right_bottom = np.minimum(bbox1[2:], bbox2[2:]) + + wh = right_bottom - left_top + + area1, area2 = bbox_area(bbox1), bbox_area(bbox2) + + isect = np.clip(wh[0] * wh[1], 0, float('inf')) + union = (area1 + area2 - isect).squeeze() + + return isect / (union + epsilon) + +=== File: expose/data/utils/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/data/utils/__init__.py:1-23 +# -*- coding: utf-8 -*- +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + + +from .keypoints import read_keypoints +from .sampling import EqualSampler +from .bbox import (bbox_area, bbox_to_wh, points_to_bbox, bbox_iou, + center_size_to_bbox, scale_to_bbox_size, + bbox_to_center_scale, + ) +from .transforms import flip_pose + +=== File: expose/data/utils/keypoints.py === + +-- Chunk 1 -- +// keypoints.py:19-62 + read_keypoints(keypoint_fn, use_hands=True, use_face=True, + use_face_contour=True): + with open(keypoint_fn) as keypoint_file: + data = json.load(keypoint_file) + + all_keypoints = [] + for idx, person_data in enumerate(data['people']): + body_keypoints = np.array(person_data['pose_keypoints_2d'], + dtype=np.float32) + body_keypoints = body_keypoints.reshape([-1, 3]) + + left_hand_keyps = person_data.get('hand_left_keypoints_2d', []) + if len(left_hand_keyps) < 1: + left_hand_keyps = [0] * (21 * 3) + left_hand_keyps = np.array( + left_hand_keyps, dtype=np.float32).reshape([-1, 3]) + + right_hand_keyps = person_data.get('hand_right_keypoints_2d', []) + if len(right_hand_keyps) < 1: + right_hand_keyps = [0] * (21 * 3) + right_hand_keyps = np.array( + right_hand_keyps, dtype=np.float32).reshape([-1, 3]) + + face_keypoints = person_data.get('face_keypoints_2d', []) + if len(face_keypoints) < 1: + face_keypoints = [0] * (70 * 3) + + face_keypoints = np.array( + face_keypoints, + dtype=np.float32).reshape([-1, 3]) + + face_keypoints = face_keypoints[:-2] + + all_keypoints.append( + np.concatenate([ + body_keypoints, + left_hand_keyps, right_hand_keyps, + face_keypoints], axis=0) + ) + + if len(all_keypoints) < 1: + return None + all_keypoints = np.stack(all_keypoints) + return all_keypoints + +=== File: expose/data/datasets/ehf.py === + +-- Chunk 1 -- +// ehf.py:43-192 +ss EHF(dutils.Dataset): + + def __init__(self, data_folder, img_folder='images', + # keyp_folder='keypoints', + alignments_folder='alignments', + num_betas=10, num_expr_coeffs=10, + use_face_contour=False, + dtype=torch.float32, + transforms=None, + split='train', + keyp_format='coco25', + metrics=None, + use_joint_conf=True, + head_only=False, + hand_only=False, + is_right=True, + binarization=True, + body_thresh=0.1, + hand_thresh=0.2, + face_thresh=0.4, + **kwargs): + super(EHF, self).__init__() + if metrics is None: + metrics = ['v2v'] + self.metrics = metrics + + self.dtype = dtype + self.data_folder = osp.expandvars(data_folder) + self.img_folder = img_folder + # self.keyp_folder = keyp_folder + self.alignments_folder = alignments_folder + self.use_joint_conf = use_joint_conf + + # keypoint_fname = osp.join(self.data_folder, 'gt_keyps.npy') + keypoint_fname = osp.join(self.data_folder, 'gt_keyps.npz') + keypoint_data = np.load(keypoint_fname) + self.keypoints = keypoint_data['gt_keypoints_2d'] + self.keypoints3d = keypoint_data['gt_keypoints_3d'] + self.joints14 = keypoint_data['gt_joints14'] + if not use_face_contour: + self.keypoints = self.keypoints[:, :-17] + + self.is_train = 'train' in split + self.split = split + self.keyp_format = keyp_format + self.is_right = is_right + self.head_only = head_only + self.hand_only = hand_only + self.body_thresh = body_thresh + self.hand_thresh = hand_thresh + self.face_thresh = face_thresh + self.binarization = binarization + + annot_fn = osp.join(self.data_folder, 'annotations.yaml') + with open(annot_fn, 'r') as annot_file: + annotations = yaml.load(annot_file) + self.annotations = annotations + self.annotations = (self.annotations['train'] + + self.annotations['test']) + + self.transforms = transforms + + self.num_betas = num_betas + self.num_expr_coeffs = num_expr_coeffs + self.use_face_contour = use_face_contour + + self.img_fns = sorted( + os.listdir(osp.join(self.data_folder, self.img_folder))) + # source_idxs, target_idxs = dset_to_body_model( + # dset='openpose25+hands+face', + # model_type='smplx', use_hands=True, use_face=True, + # use_face_contour=self.use_face_contour, + # keyp_format=self.keyp_format) + + # self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + # self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + face_idxs = idxs_dict['face'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.face_idxs = np.asarray(face_idxs) + self.left_hand_idxs = np.asarray(left_hand_idxs) + self.right_hand_idxs = np.asarray(right_hand_idxs) + + self.body_dset_factor = 1.2 + self.head_dset_factor = 2.0 + self.hand_dset_factor = 2.0 + + def __repr__(self): + return 'EHF' + + def name(self): + return 'EHF/Test' + + def get_num_joints(self): + return 14 + + def __len__(self): + return len(self.img_fns) + + def get_elements_per_index(self): + return 1 + + def __getitem__(self, index): + fn = self.annotations[index] + img_path = osp.join(self.data_folder, self.img_folder, + fn + '.png') + img = read_img(img_path) + + _, fn = os.path.split(fn) + + # TODO: Add 3D Keypoints + # keypoints2d = data_tuple['keypoints'].squeeze() + + # Copy keypoints from the GT data + output_keypoints2d = np.zeros( + [127 + 17 * self.use_face_contour, 3], dtype=np.float32) + output_keypoints2d[:, :-1] = self.keypoints[index].copy() + output_keypoints2d[:, -1] = 1.0 + + output_keypoints3d = np.zeros( + [127 + 17 * self.use_face_contour, 4], dtype=np.float32) + output_keypoints3d[:, :-1] = self.keypoints3d[index].copy() + output_keypoints3d[:, -1] = 1.0 + + is_right = self.is_right + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.head_only or self.hand_only: + body_conf[:] = 0.0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf = output_keypoints2d[self.left_hand_idxs, -1] + right_hand_conf = output_keypoints2d[self.right_hand_idxs, -1] + if self.head_only: + left_hand_conf[:] = 0.0 + right_hand_conf[:] = 0.0 + + face_conf = output_keypoints2d[self.face_idxs, -1] + +-- Chunk 2 -- +// ehf.py:193-293 + if self.hand_only: + face_conf[:] = 0.0 + if is_right: + left_hand_conf[:] = 0 + else: + right_hand_conf[:] = 0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf[left_hand_conf < self.hand_thresh] = 0.0 + right_hand_conf[right_hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + left_hand_conf = ( + left_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + right_hand_conf = ( + right_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.left_hand_idxs, -1] = left_hand_conf + output_keypoints2d[self.right_hand_idxs, -1] = right_hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + if self.head_only: + dset_scale_factor = self.head_dset_factor + elif self.hand_only: + dset_scale_factor = self.hand_dset_factor + else: + dset_scale_factor = self.body_dset_factor + + center, scale, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.size), + dset_scale_factor=dset_scale_factor, + ) + if center is None: + return None, None, None, None + + if self.hand_only: + target.add_field('is_right', is_right) + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + + target.add_field( + 'keypoints3d', + Keypoints3D(output_keypoints3d, img.shape, flip_axis=0) + ) + + orig_center, _, orig_bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.size), + dset_scale_factor=1.0, + ) + target.add_field('orig_center', orig_center) + target.add_field('orig_bbox_size', bbox_size) + + alignment_path = osp.join(self.data_folder, self.alignments_folder, + fn.replace('.07_C', '') + '.pkl') + with open(alignment_path, 'rb') as alignment_file: + alignment_data = pickle.load(alignment_file, encoding='latin1') + transl = np.array([-0.03609917, 0.43416458, 2.37101226]) + camera_pose = np.array([-2.9874789618512025, 0.011724572107320893, + -0.05704686818955933]) + camera_pose = cv2.Rodrigues(camera_pose)[0] + + vertices = alignment_data['v'] + cam_vertices = vertices.dot(camera_pose.T) + transl.reshape(1, 3) + + vertices_field = Vertices(cam_vertices) + target.add_field('vertices', vertices_field) + + H, W, _ = img.shape + intrinsics = np.array([[1498.22426237, 0, 790.263706], + [0, 1498.22426237, 578.90334], + [0, 0, 1]], dtype=np.float32) + target.add_field('intrinsics', intrinsics) + + joints3d = self.joints14[index] + joints = Joints(joints3d[:14]) + target.add_field('joints14', joints) + + if self.transforms is not None: + force_flip = False + if self.hand_only and not is_right: + force_flip = True + img, cropped_image, target = self.transforms( + img, target, dset_scale_factor=1.2, force_flip=force_flip) + + target.add_field('fname', fn) + return img, cropped_image, target, index + +=== File: expose/data/datasets/ffhq.py === + +-- Chunk 1 -- +// ffhq.py:45-194 +ss FFHQ(dutils.Dataset): + def __init__(self, data_path='data/ffhq', + img_folder='images', + param_fname='ffhq_parameters.npz', + head_only=True, + split='train', + dtype=torch.float32, + joints_to_ign=None, + metrics=None, + transforms=None, + return_params=True, + return_shape=False, + return_vertices=False, + vertex_folder='vertices', + use_face_contour=False, + split_size=0.8, + vertex_flip_correspondences='', + **kwargs): + super(FFHQ, self).__init__() + assert head_only, 'FFHQ can only be used as a head only dataset' + + if metrics is None: + metrics = [] + self.metrics = metrics + + self.split = split + self.is_train = 'train' in split + self.return_params = return_params + self.return_vertices = return_vertices + self.use_face_contour = use_face_contour + + self.return_shape = return_shape + self.data_path = osp.expandvars(osp.expanduser(data_path)) + self.img_folder = osp.join(self.data_path, img_folder) + + self.transforms = transforms + self.dtype = dtype + + param_path = osp.join(self.data_path, param_fname) + self.vertex_path = osp.join(self.data_path, vertex_folder) + + vertex_flip_correspondences = osp.expandvars( + vertex_flip_correspondences) + err_msg = ( + 'Vertex flip correspondences path does not exist:' + + f' {vertex_flip_correspondences}' + ) + assert osp.exists(vertex_flip_correspondences), err_msg + flip_data = np.load(vertex_flip_correspondences) + self.bc = flip_data['bc'] + self.closest_faces = flip_data['closest_faces'] + + params = np.load(param_path) + params_dict = {key: params[key] for key in params.keys()} + + self.global_pose = params_dict['global_pose'].astype(np.float32).copy() + self.jaw_pose = params_dict['jaw_pose'].astype(np.float32).copy() + self.betas = params_dict['betas'].astype(np.float32).copy() + self.expression = params_dict['expression'].astype(np.float32).copy() + self.keypoints2d = params_dict['keypoints2D'].astype(np.float32).copy() + self.img_fnames = np.asarray(params_dict['img_fnames']) + + self.return_vertices = return_vertices + # if return_vertices: + # assert 'vertices' in params_dict, ( + # 'Requested vertices but these are not in the npz file') + # self.vertices = params_dict['vertices'].astype(np.float32).copy() + + num_items = len(self.betas) + idxs = np.arange(num_items) + if self.is_train: + self.idxs = idxs[:int(num_items * split_size)] + else: + self.idxs = idxs[int(num_items * split_size):] + self.num_items = len(self.idxs) + + folder_map_fname = osp.expandvars( + osp.join(self.data_path, img_folder, split, FOLDER_MAP_FNAME)) + self.use_folder_split = osp.exists(folder_map_fname) + if self.use_folder_split: + self.img_folder = osp.join(self.data_path, img_folder, split) + with open(folder_map_fname, 'rb') as f: + data_dict = pickle.load(f) + self.items_per_folder = max(data_dict.values()) + + source_idxs, target_idxs = dset_to_body_model( + dset='ffhq', use_face_contour=self.use_face_contour) + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + def get_elements_per_index(self): + return 1 + + def __repr__(self): + return 'FFHQ( \n\t Split: {self.split}\n)' + + def name(self): + return f'FFHQ/{self.split}' + + def get_num_joints(self): + return 51 + self.use_face_contour * 17 + + def only_2d(self): + return False + + def __len__(self): + return self.num_items + + def __getitem__(self, index): + data_idx = self.idxs[index] + + if self.use_folder_split: + folder_idx = index // self.items_per_folder + file_idx = index + + global_pose = self.global_pose[data_idx] + jaw_pose = self.jaw_pose[data_idx] + expression = self.expression[data_idx] + keypoints2d = self.keypoints2d[data_idx] + + if self.use_folder_split: + img_fn = osp.join( + self.img_folder, f'folder_{folder_idx:010d}', + f'{file_idx:010d}.jpg') + else: + img_fn = osp.join(self.img_folder, + str(self.img_fnames[data_idx])) + + img = read_img(img_fn.replace('.png', '.jpg')) + + output_keypoints2d = np.zeros( + [127 + 17 * self.use_face_contour, 3], dtype=np.float32) + output_keypoints2d[self.target_idxs, :-1] = keypoints2d[ + self.source_idxs] + output_keypoints2d[self.target_idxs, -1] = 1.0 + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + + center = np.array([512, 512], dtype=np.float32) + scale = IMAGE_SIZE / REF_BOX_SIZE + target.add_field('orig_center', center) + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', IMAGE_SIZE) + H, W, _ = img.shape + fscale = img.shape[0] / 256 + intrinsics = np.array( + [[DEFAULT_FOCAL_LENGTH * fscale, 0.0, W * 0.5], + [0.0, DEFAULT_FOCAL_LENGTH * fscale, H * 0.5], + [0.0, 0.0, 1.0]] + +-- Chunk 2 -- +// ffhq.py:195-218 + ) + target.add_field('intrinsics', intrinsics) + if self.return_params: + global_pose_field = GlobalPose(global_pose=global_pose) + target.add_field('global_pose', global_pose_field) + jaw_pose_field = JawPose(jaw_pose=jaw_pose) + target.add_field('jaw_pose', jaw_pose_field) + expression_field = Expression(expression=expression) + target.add_field('expression', expression_field) + if self.return_vertices: + fname, _ = osp.splitext(self.img_fnames[data_idx]) + vertex_fname = osp.join(self.vertex_path, f'{fname}.npy') + vertices = np.load(vertex_fname) + vertex_field = Vertices( + vertices, bc=self.bc, closest_faces=self.closest_faces) + target.add_field('vertices', vertex_field) + if self.return_shape: + target.add_field('betas', Betas(self.betas[data_idx])) + + if self.transforms is not None: + img, cropped_image, target = self.transforms( + img, target, dset_scale_factor=2.0) + target.add_field('name', self.name()) + return img, cropped_image, target, index + +=== File: expose/data/datasets/threedpw.py === + +-- Chunk 1 -- +// threedpw.py:38-187 +ss ThreeDPW(dutils.Dataset): + def __init__(self, data_path='data/3dpw', + img_folder='', + seq_folder='sequenceFiles', + param_folder='smplx_npz_data', + split='val', + use_face=True, use_hands=True, use_face_contour=False, + model_type='smplx', + dtype=torch.float32, + vertex_folder='smplx_vertices', + return_vertices=True, + joints_to_ign=None, + use_joint_conf=True, + metrics=None, + transforms=None, + body_thresh=0.3, + binarization=True, + min_visible=6, + **kwargs): + super(ThreeDPW, self).__init__() + + if metrics is None: + metrics = [] + self.metrics = metrics + self.binarization = binarization + self.return_vertices = return_vertices + + self.split = split + self.is_train = 'train' in split + + self.data_path = osp.expandvars(osp.expanduser(data_path)) + seq_path = osp.join(self.data_path, seq_folder) + if self.split == 'train': + seq_split_path = osp.join(seq_path, 'train') + npz_fn = osp.join(self.data_path, param_folder, '3dpw_train.npz') + elif self.split == 'val': + seq_split_path = osp.join(seq_path, 'validation') + npz_fn = osp.join( + self.data_path, param_folder, '3dpw_validation.npz') + elif self.split == 'test': + seq_split_path = osp.join(seq_path, 'test') + npz_fn = osp.join(self.data_path, param_folder, '3dpw_test.npz') + + self.vertex_folder = osp.join( + self.data_path, vertex_folder, self.split) + + self.img_folder = osp.join(self.data_path, img_folder) + folder_map_fname = osp.expandvars( + osp.join(self.img_folder, split, FOLDER_MAP_FNAME)) + self.use_folder_split = osp.exists(folder_map_fname) + if self.use_folder_split: + with open(folder_map_fname, 'rb') as f: + data_dict = pickle.load(f) + self.items_per_folder = max(data_dict.values()) + self.img_folder = osp.join(self.img_folder, split) + + data_dict = np.load(npz_fn) + # data_dict = {key: data[key] for key in data.keys()} + + if 'cam_intrinsics' in data_dict: + self.cam_intrinsics = data_dict['cam_intrinsics'] + + self.img_paths = np.asarray(data_dict['img_paths']) + + # idxs = [ii for ii, path in enumerate(self.img_paths) + # if 'downtown_walking_00' in path] + idxs = np.arange(len(self.img_paths)) + # idxs = np.array(idxs) + self.idxs = idxs + self.img_paths = self.img_paths[idxs] + + if 'keypoints2d' in data_dict: + self.keypoints2d = np.asarray( + data_dict['keypoints2d']).astype(np.float32)[idxs] + elif 'keypoints2D' in data_dict: + self.keypoints2d = np.asarray( + data_dict['keypoints2D']).astype(np.float32)[idxs] + else: + raise KeyError(f'Keypoints2D not in 3DPW {split} dictionary') + self.joints3d = np.asarray( + data_dict['joints3d']).astype(np.float32)[idxs] + # self.v_shaped = np.asarray(data_dict['v_shaped']).astype(np.float32) + self.num_items = len(self.img_paths) + # self.pids = np.asarray(data_dict['person_ids'], dtype=np.int32) + self.pids = np.asarray(data_dict['pid'], dtype=np.int32) + self.center = np.asarray( + data_dict['center'], dtype=np.float32)[idxs] + self.scale = np.asarray( + data_dict['scale'], dtype=np.float32)[idxs] + self.bbox_size = np.asarray( + data_dict['bbox_size'], dtype=np.float32)[idxs] + + self.transforms = transforms + self.dtype = dtype + + self.use_face = use_face + self.use_hands = use_hands + self.use_face_contour = use_face_contour + self.model_type = model_type + self.use_joint_conf = use_joint_conf + self.body_thresh = body_thresh + + source_idxs, target_idxs = dset_to_body_model( + dset='3dpw', model_type='smplx', + use_face_contour=self.use_face_contour) + self.source_idxs = np.asarray(source_idxs) + self.target_idxs = np.asarray(target_idxs) + + def get_elements_per_index(self): + return 1 + + def __repr__(self): + return '3DPW( \n\t Split: {}\n)'.format(self.split) + + def name(self): + return '3DPW/{}'.format(self.split) + + def get_num_joints(self): + return 14 + + def __len__(self): + return self.num_items + + def only_2d(self): + return False + + def __getitem__(self, index): + # start = time.perf_counter() + img_fn = self.img_paths[index] + + if self.use_folder_split: + folder_idx = (index + self.idxs[0]) // self.items_per_folder + img_fn = osp.join(self.img_folder, + 'folder_{:010d}'.format(folder_idx), + f'{index + self.idxs[0]:010d}.jpg') + img = read_img(img_fn) + # print('read img:', time.perf_counter() - start) + + keypoints2d = self.keypoints2d[index, :] + # print('read data:', time.perf_counter() - start) + # start = time.perf_counter() + # logger.info('V + J: {}'.format(time.perf_counter() - start)) + + # # Pad to compensate for extra keypoints + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + output_keypoints2d[self.target_idxs] = keypoints2d[self.source_idxs] + + # Remove joints with negative confidence + +-- Chunk 2 -- +// threedpw.py:188-250 + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + output_keypoints2d[ + output_keypoints2d[:, -1] < self.body_thresh, -1] = 0 + + # If we don't want to use the confidence scores as weights for the loss + if self.binarization: + # then set those above the conf thresh to 1 + output_keypoints2d[:, -1] = ( + output_keypoints2d[:, -1] >= self.body_thresh).astype( + output_keypoints2d.dtype) + + center = self.center[index] + scale = self.scale[index] + bbox_size = self.bbox_size[index] + + # keypoints = output_keypoints2d[:, :-1] + # conf = output_keypoints2d[:, -1] + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + target.add_field('center', center) + target.add_field('orig_center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('orig_bbox_size', bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + + target.add_field('filename', self.img_paths[index]) + + head, fname = osp.split(self.img_paths[index]) + _, seq_name = osp.split(head) + target.add_field('fname', f'{seq_name}/{fname}_{self.pids[index]}') + + if self.return_vertices: + vertex_fname = osp.join( + self.vertex_folder, + f'{index + self.idxs[0]:06d}.npy') + vertices = np.load(vertex_fname) + + vertex_field = Vertices(vertices.reshape(-1, 3)) + target.add_field('vertices', vertex_field) + + intrinsics = self.cam_intrinsics[index] + target.add_field('intrinsics', intrinsics) + + if not self.is_train: + joints3d = self.joints3d[index] + joints = Joints(joints3d[:14]) + target.add_field('joints14', joints) + + if hasattr(self, 'v_shaped'): + v_shaped = self.v_shaped[index] + target.add_field('v_shaped', Vertices(v_shaped)) + # print('SMPL-HF Field {}'.format(time.perf_counter() - start)) + + # start = time.perf_counter() + if self.transforms is not None: + img, cropped_image, target = self.transforms( + img, target, dset_scale_factor=1.2, force_flip=False) + # logger.info('Transforms: {}'.format(time.perf_counter() - start)) + + return img, cropped_image, target, index + +=== File: expose/data/datasets/image_folder.py === + +-- Chunk 1 -- +// image_folder.py:37-68 +ss ImageFolder(dutils.Dataset): + def __init__(self, + data_folder='data/images', + transforms=None, + **kwargs): + super(ImageFolder, self).__init__() + + paths = [] + self.transforms = transforms + data_folder = osp.expandvars(data_folder) + for fname in os.listdir(data_folder): + if not any(fname.endswith(ext) for ext in EXTS): + continue + paths.append(osp.join(data_folder, fname)) + + self.paths = np.stack(paths) + + def __len__(self): + return len(self.paths) + + def __getitem__(self, index): + img = read_img(self.paths[index]) + + if self.transforms is not None: + img = self.transforms(img) + + return { + 'images': img, + 'paths': self.paths[index] + } + + + +-- Chunk 2 -- +// image_folder.py:69-108 +ss ImageFolderWithBoxes(dutils.Dataset): + def __init__(self, + img_paths, + bboxes, + transforms=None, + scale_factor=1.2, + **kwargs): + super(ImageFolderWithBoxes, self).__init__() + + self.transforms = transforms + + self.paths = np.stack(img_paths) + self.bboxes = np.stack(bboxes) + self.scale_factor = scale_factor + + def __len__(self): + return len(self.paths) + + def __getitem__(self, index): + img = read_img(self.paths[index]) + + bbox = self.bboxes[index] + + target = BoundingBox(bbox, size=img.shape) + + center, scale, bbox_size = bbox_to_center_scale( + bbox, dset_scale_factor=self.scale_factor) + target.add_field('bbox_size', bbox_size) + target.add_field('orig_bbox_size', bbox_size) + target.add_field('orig_center', center) + target.add_field('center', center) + target.add_field('scale', scale) + + _, fname = osp.split(self.paths[index]) + target.add_field('fname', f'{fname}_{index:03d}') + + if self.transforms is not None: + full_img, cropped_image, target = self.transforms(img, target) + + return full_img, cropped_image, target, index + +=== File: expose/data/datasets/spin.py === + +-- Chunk 1 -- +// spin.py:45-194 +ss SPIN(dutils.Dataset): + def __init__(self, img_folder, npz_files=[], dtype=torch.float32, + use_face_contour=False, + binarization=True, + body_thresh=0.1, + hand_thresh=0.2, + face_thresh=0.4, + min_hand_keypoints=8, + min_head_keypoints=8, + transforms=None, + split='train', + return_shape=False, + return_full_pose=False, + return_params=True, + return_gender=False, + vertex_folder='vertices', + return_vertices=True, + vertex_flip_correspondences='', + **kwargs): + super(SPIN, self).__init__() + + self.img_folder = osp.expandvars(img_folder) + self.transforms = transforms + self.use_face_contour = use_face_contour + self.body_thresh = body_thresh + self.hand_thresh = hand_thresh + self.face_thresh = face_thresh + self.binarization = binarization + self.dtype = dtype + self.split = split + + self.min_hand_keypoints = min_hand_keypoints + self.min_head_keypoints = min_head_keypoints + + self.return_vertices = return_vertices + self.return_gender = return_gender + self.return_params = return_params + self.return_shape = return_shape + self.return_full_pose = return_full_pose + + self.vertex_folder = osp.join( + osp.split(self.img_folder)[0], vertex_folder) + + vertex_flip_correspondences = osp.expandvars( + vertex_flip_correspondences) + err_msg = ( + 'Vertex flip correspondences path does not exist:' + + f' {vertex_flip_correspondences}' + ) + assert osp.exists(vertex_flip_correspondences), err_msg + flip_data = np.load(vertex_flip_correspondences) + self.bc = flip_data['bc'] + self.closest_faces = flip_data['closest_faces'] + + self.spin_data = {} + start = 0 + for npz_fn in npz_files: + npz_fn = osp.expandvars(npz_fn) + dset = osp.splitext(osp.split(npz_fn)[1])[0] + + data = np.load(npz_fn) + has_smpl = np.asarray(data['has_smpl']).astype(np.bool) + data = {key: data[key][has_smpl] for key in data.keys()} + + logger.info(start) + data['dset'] = [dset] * data['pose'].shape[0] + start += data['pose'].shape[0] + if 'genders' not in data and self.return_gender: + data['genders'] = [''] * len(data['pose']) + data['indices'] = np.arange(data['pose'].shape[0]) + if dset == 'lsp': + data['part'][26, [9, 11], :] = data['part'][26, [11, 9], :] + self.spin_data[dset] = data + + folder_map_fname = osp.expandvars( + osp.join(img_folder, FOLDER_MAP_FNAME)) + with open(folder_map_fname, 'rb') as f: + data_dict = pickle.load(f) + self.items_per_folder = max(data_dict.values()) + + self.indices = np.concatenate( + [self.spin_data[dset]['indices'] for dset in self.spin_data], + axis=0).astype(np.int32) + self.centers = np.concatenate( + [self.spin_data[dset]['center'] for dset in self.spin_data], + axis=0).astype(np.float32) + self.scales = np.concatenate( + [self.spin_data[dset]['scale'] for dset in self.spin_data], + axis=0).astype(np.float32) + self.poses = np.concatenate( + [self.spin_data[dset]['pose'] + for dset in self.spin_data], axis=0).astype(np.float32) + self.keypoints2d = np.concatenate( + [self.spin_data[dset]['part'] for dset in self.spin_data], + axis=0).astype(np.float32) + self.imgname = np.concatenate( + [self.spin_data[dset]['imgname'] + for dset in self.spin_data], + axis=0).astype(np.string_) + self.dset = np.concatenate([self.spin_data[dset]['dset'] + for dset in self.spin_data], + axis=0).astype(np.string_) + if self.return_gender: + gender = [] + for dset in self.spin_data: + gender.append(self.spin_data[dset]['genders']) + self.gender = np.concatenate(gender).astype(np.string_) + + if self.return_shape: + self.betas = np.concatenate( + [self.spin_data[dset]['betas'] + for dset in self.spin_data], axis=0).astype(np.float32) + + # self.dset_names = list(self.spin_data.keys()) + dset_sizes = list( + map(lambda x: x['pose'].shape[0], self.spin_data.values())) + # logger.info(self.dset_sizes) + + self.num_items = sum(dset_sizes) + # logger.info(self.num_items) + + source_idxs, target_idxs = dset_to_body_model( + model_type='smplx', use_hands=True, use_face=True, + dset='spin', use_face_contour=self.use_face_contour) + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + face_idxs = idxs_dict['face'] + if not self.use_face_contour: + face_idxs = face_idxs[:-17] + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.face_idxs = np.asarray(face_idxs) + + def get_elements_per_index(self): + return 1 + + def name(self): + return 'SPIN/{}'.format(self.split) + + def only_2d(self): + return False + + def __len__(self): + return self.num_items + + def __getitem__(self, index): + +-- Chunk 2 -- +// spin.py:195-301 + folder_idx = index // self.items_per_folder + file_idx = index + + img_fn = osp.join(self.img_folder, + 'folder_{:010d}'.format(folder_idx), + '{:010d}.jpg'.format(file_idx)) + img = read_img(img_fn) + keypoints2d = self.keypoints2d[index] + + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + output_keypoints2d[self.target_idxs] = keypoints2d[self.source_idxs] + + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + hand_conf = output_keypoints2d[self.hand_idxs, -1] + face_conf = output_keypoints2d[self.face_idxs, -1] + + body_conf[body_conf < self.body_thresh] = 0.0 + hand_conf[hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + hand_conf = ( + hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.hand_idxs, -1] = hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + _, _, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=1.2 + ) + center = self.centers[index] + scale = self.scales[index] + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + + if self.return_params: + pose = self.poses[index].reshape(-1, 3) + + global_pose_target = GlobalPose(pose[0].reshape(-1)) + target.add_field('global_pose', global_pose_target) + if self.return_full_pose: + body_pose = pose[1:] + else: + body_pose = pose[1:22] + body_pose_target = BodyPose(body_pose.reshape(-1)) + target.add_field('body_pose', body_pose_target) + + if self.return_shape: + betas = self.betas[index] + target.add_field('betas', Betas(betas)) + if self.return_vertices: + fname = osp.join(self.vertex_folder, f'{index:06d}.npy') + H, W, _ = img.shape + + fscale = H / bbox_size + intrinsics = np.array([[5000 * fscale, 0, 0], + [0, 5000 * fscale, 0], + [0, 0, 1]], dtype=np.float32) + + target.add_field('intrinsics', intrinsics) + vertices = np.load(fname) + vertex_field = Vertices( + vertices, bc=self.bc, closest_faces=self.closest_faces) + target.add_field('vertices', vertex_field) + + if self.transforms is not None: + force_flip = False + full_img, cropped_image, cropped_target = self.transforms( + img, target, dset_scale_factor=1.2, force_flip=force_flip) + target.add_field('name', self.name()) + + dict_key = [f'spin/{self.dset[index].decode("utf-8")}', + self.imgname[index].decode('utf-8'), index] + if hasattr(self, 'gender') and self.return_gender: + gender = self.gender[index].decode('utf-8') + if gender == 'F' or gender == 'M': + target.add_field('gender', gender) + dict_key.append(gender) + + # Add the key used to access the fit dict + dict_key = tuple(dict_key) + target.add_field('dict_key', dict_key) + + return full_img, cropped_image, cropped_target, index + + + +-- Chunk 3 -- +// spin.py:302-451 +ss SPINX(SPIN): + def __init__(self, return_params=True, + head_only=False, + hand_only=False, + return_expression=True, + *args, **kwargs): + super(SPINX, self).__init__(return_params=return_params, + *args, **kwargs) + assert nand(head_only, hand_only), ( + 'Hand only and head only can\'t be True at the same time') + + self.return_expression = return_expression + self.head_only = head_only + self.hand_only = hand_only + + self.keypoints2d = np.concatenate( + [self.spin_data[dset]['body_keypoints'] + for dset in self.spin_data], + axis=0).astype(np.float32) + self.left_hand_keypoints = np.concatenate( + [self.spin_data[dset]['left_hand_keypoints'] + for dset in self.spin_data], axis=0) + self.right_hand_keypoints = np.concatenate( + [self.spin_data[dset]['right_hand_keypoints'] + for dset in self.spin_data], axis=0) + self.face_keypoints = np.concatenate( + [self.spin_data[dset]['face_keypoints'] + for dset in self.spin_data], axis=0) + + self.spin_keypoints = np.concatenate( + [self.spin_data[dset]['spin_keypoints'] + for dset in self.spin_data], axis=0) + + if self.return_expression: + self.expression = np.concatenate( + [self.spin_data[dset]['expression'] + for dset in self.spin_data], axis=0).astype(np.float32) + + self.translation = np.concatenate( + [self.spin_data[dset]['translation'] + for dset in self.spin_data], axis=0).astype(np.float32) + + source_idxs, target_idxs = dset_to_body_model( + model_type='smplx', use_hands=True, use_face=True, + dset='openpose25+hands+face', + # dset='spinx', + use_face_contour=self.use_face_contour) + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + face_idxs = idxs_dict['face'] + head_idxs = idxs_dict['head'] + if not self.use_face_contour: + face_idxs = face_idxs[:-17] + head_idxs = head_idxs[:-17] + + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.left_hand_idxs = np.asarray(left_hand_idxs) + self.right_hand_idxs = np.asarray(right_hand_idxs) + self.face_idxs = np.asarray(face_idxs) + self.head_idxs = np.asarray(head_idxs) + + def get_elements_per_index(self): + return 1 + + def name(self): + return 'SPINX/{}'.format(self.split) + + def only_2d(self): + return False + + def __len__(self): + return self.num_items + + def __getitem__(self, index): + folder_idx = index // self.items_per_folder + file_idx = index + + img_fn = osp.join(self.img_folder, + 'folder_{:010d}'.format(folder_idx), + '{:010d}.jpg'.format(file_idx)) + img = read_img(img_fn) + + body_keypoints = self.keypoints2d[index] + left_hand_keypoints = self.left_hand_keypoints[index] + right_hand_keypoints = self.right_hand_keypoints[index] + face_keypoints = self.face_keypoints[index] + + keypoints2d = np.concatenate( + [body_keypoints, left_hand_keypoints, right_hand_keypoints, + face_keypoints], axis=0) + + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + output_keypoints2d[self.target_idxs] = keypoints2d[self.source_idxs] + + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + hand_conf = output_keypoints2d[self.hand_idxs, -1] + face_conf = output_keypoints2d[self.face_idxs, -1] + + body_conf[body_conf < self.body_thresh] = 0.0 + hand_conf[hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + hand_conf = ( + hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.hand_idxs, -1] = hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + if self.head_only: + keypoints = output_keypoints2d[self.head_idxs, :-1] + conf = output_keypoints2d[self.head_idxs, -1] + elif self.hand_only: + keypoints = output_keypoints2d[self.hand_idxs, :-1] + conf = output_keypoints2d[self.hand_idxs, -1] + else: + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + center, scale, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=1.2, + ) + + target = Keypoints2D( + output_keypoints2d, img.shape[:-1], flip_axis=0, dtype=self.dtype) + _, _, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=1.2) + + center = self.centers[index] + +-- Chunk 4 -- +// spin.py:452-550 + scale = self.scales[index] + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + + target.add_field('keypoints_hd', output_keypoints2d) + + target.add_field('orig_center', center) + target.add_field('orig_bbox_size', scale * 200) + + left_hand_bbox = keyps_to_bbox( + output_keypoints2d[self.left_hand_idxs, :-1], + output_keypoints2d[self.left_hand_idxs, -1], + img_size=img.shape, scale=1.5) + left_hand_bbox_target = BoundingBox(left_hand_bbox, img.shape) + has_left_hand = (output_keypoints2d[self.left_hand_idxs, -1].sum() > + self.min_hand_keypoints) + if has_left_hand: + target.add_field('left_hand_bbox', left_hand_bbox_target) + target.add_field( + 'orig_left_hand_bbox', + BoundingBox(left_hand_bbox, img.shape, transform=False)) + + right_hand_bbox = keyps_to_bbox( + output_keypoints2d[self.right_hand_idxs, :-1], + output_keypoints2d[self.right_hand_idxs, -1], + img_size=img.shape, scale=1.5) + right_hand_bbox_target = BoundingBox(right_hand_bbox, img.shape) + has_right_hand = (output_keypoints2d[self.right_hand_idxs, -1].sum() > + self.min_hand_keypoints) + if has_right_hand: + target.add_field('right_hand_bbox', right_hand_bbox_target) + target.add_field( + 'orig_right_hand_bbox', + BoundingBox(right_hand_bbox, img.shape, transform=False)) + + head_bbox = keyps_to_bbox( + output_keypoints2d[self.head_idxs, :-1], + output_keypoints2d[self.head_idxs, -1], + img_size=img.shape, scale=1.2) + head_bbox_target = BoundingBox(head_bbox, img.shape) + has_head = (output_keypoints2d[self.head_idxs, -1].sum() > + self.min_head_keypoints) + if has_head: + target.add_field('head_bbox', head_bbox_target) + target.add_field( + 'orig_head_bbox', + BoundingBox(head_bbox, img.shape, transform=False)) + + if self.return_params: + pose = self.poses[index].reshape(-1, 3) + + global_pose_target = GlobalPose(pose[0].reshape(-1)) + target.add_field('global_pose', global_pose_target) + body_pose = pose[1:22] + body_pose_target = BodyPose(body_pose.reshape(-1)) + target.add_field('body_pose', body_pose_target) + + jaw_pose = pose[22] + jaw_pose_target = JawPose(jaw_pose.reshape(-1)) + target.add_field('jaw_pose', jaw_pose_target) + + left_hand_pose = pose[25:25 + 15] + right_hand_pose = pose[-15:] + hand_pose_target = HandPose(left_hand_pose.reshape(-1), + right_hand_pose.reshape(-1)) + target.add_field('hand_pose', hand_pose_target) + + if self.return_shape: + betas = self.betas[index] + target.add_field('betas', Betas(betas)) + + expression = self.expression[index] + target.add_field('expression', Expression(expression)) + + if self.transforms is not None: + force_flip = False + full_img, cropped_image, cropped_target = self.transforms( + img, target, force_flip=force_flip) + + target.add_field('name', self.name()) + + dict_key = [f'spinx/{self.dset[index].decode("utf-8")}', + self.imgname[index].decode('utf-8'), + self.indices[index]] + + if hasattr(self, 'gender') and self.return_gender: + gender = self.gender[index].decode('utf-8') + if gender == 'F' or gender == 'M': + target.add_field('gender', gender) + dict_key.append(gender) + + # Add the key used to access the fit dict + dict_key = tuple(dict_key) + target.add_field('dict_key', dict_key) + + return full_img, cropped_image, cropped_target, index + + + +-- Chunk 5 -- +// spin.py:551-654 +ss LSPTest(dutils.Dataset): + def __init__(self, data_path, + return_full_pose=False, + return_params=True, + transforms=None, + use_face_contour=True, + dtype=torch.float32, + **kwargs, + ): + super(LSPTest, self).__init__() + + self.img_folder = osp.expandvars( + '/ps/project/handsproject/SMPL_HF/lsp/lsp_dataset_original/images') + self.data_path = osp.expandvars(data_path) + self.transforms = transforms + self.use_face_contour = use_face_contour + self.dtype = dtype + self.return_vertices = False + + data = np.load(self.data_path) + # has_smpl = np.asarray(data['has_smpl']).astype(np.bool) + self.centers = data['center'].astype(np.float32) + self.scales = data['scale'].astype(np.float32) + self.keypoints2d = data['part'].astype(np.float32) + logger.info(self.keypoints2d.shape) + self.imgname = data['imgname'].astype(np.string_) + + logger.info(self.scales.shape) + self.num_items = len(self.scales) + data.close() + + source_idxs, target_idxs = dset_to_body_model( + model_type='smplx', use_hands=True, use_face=True, + dset='lsp', use_face_contour=self.use_face_contour) + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + face_idxs = idxs_dict['face'] + if not self.use_face_contour: + face_idxs = face_idxs[:-17] + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.face_idxs = np.asarray(face_idxs) + + def __len__(self): + return self.num_items + + def name(self): + return 'LSP/{Test}' + + def __getitem__(self, index): + img_name = self.imgname[index].decode('utf-8') + img_path = osp.join(self.img_folder, img_name) + + img = read_img(img_path) + keypoints2d = self.keypoints2d[index] + + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + output_keypoints2d[self.target_idxs, :-1] = keypoints2d[ + self.source_idxs] + output_keypoints2d[self.target_idxs, -1] = 1.0 + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + + center = self.centers[index] + scale = self.scales[index] + bbox_size = scale * 200 + + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + target.add_field('name', self.name()) + target.add_field('fname', img_name) + + target.add_field('orig_center', center) + target.add_field('orig_bbox_size', scale * 200) + + if self.return_vertices: + H, W, _ = img.shape + + intrinsics = np.array([[5000, 0, 0.5 * W], + [0, 5000, 0.5 * H], + [0, 0, 1]], dtype=np.float32) + target.add_field('intrinsics', intrinsics) + + fname = osp.join(self.vertex_folder, f'{index:06d}.npy') + vertices = np.load(fname) + self.translation[index] + vertex_field = Vertices( + vertices, bc=self.bc, closest_faces=self.closest_faces) + target.add_field('vertices', vertex_field) + + if self.transforms is not None: + force_flip = False + full_img, cropped_image, cropped_target = self.transforms( + img, target, force_flip=force_flip) + + return full_img, cropped_image, cropped_target, index + +=== File: expose/data/datasets/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/data/datasets/__init__.py:1-26 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + + +from .image_folder import ImageFolder, ImageFolderWithBoxes +from .ehf import EHF +from .curated_fittings import CuratedFittings +from .threedpw import ThreeDPW +from .spin import SPIN, SPINX, LSPTest +from .openpose import OpenPose, OpenPoseTracks +from .freihand import FreiHand +from .ffhq import FFHQ +from .stirling import Stirling3D + +=== File: expose/data/datasets/openpose.py === + +-- Chunk 1 -- +// openpose.py:39-188 +ss OpenPose(dutils.Dataset): + def __init__(self, data_folder='data/openpose', + img_folder='images', + keyp_folder='keypoints', + split='train', + head_only=False, + hand_only=False, + is_right=False, + use_face=True, use_hands=True, use_face_contour=False, + model_type='smplx', + keyp_format='coco25', + dtype=torch.float32, + joints_to_ign=None, + use_joint_conf=True, + metrics=None, + transforms=None, + body_thresh=0.1, + hand_thresh=0.2, + face_thresh=0.4, + binarization=True, + **kwargs): + + super(OpenPose, self).__init__() + assert nand(head_only, hand_only), ( + 'Hand only and head only can\'t be True at the same time') + + self.is_right = is_right + self.head_only = head_only + self.hand_only = hand_only + logger.info(f'Hand only: {self.hand_only}') + logger.info(f'Is right: {self.is_right}') + + self.split = split + self.is_train = 'train' in split + + self.data_folder = osp.expandvars(osp.expanduser(data_folder)) + self.img_folder = osp.join(self.data_folder, img_folder) + self.keyp_folder = osp.join(self.data_folder, keyp_folder) + + self.transforms = transforms + self.dtype = dtype + + self.use_face = use_face + self.use_hands = use_hands + self.use_face_contour = use_face_contour + self.model_type = model_type + self.keyp_format = keyp_format + self.use_joint_conf = use_joint_conf + self.body_thresh = body_thresh + self.hand_thresh = hand_thresh + self.face_thresh = face_thresh + self.binarization = binarization + + self.img_paths = [] + self.keypoints = [] + for img_fname in os.listdir(self.img_folder): + fname, _ = osp.splitext(img_fname) + + keyp_path = osp.join( + self.keyp_folder, '{}_keypoints.json'.format(fname)) + if not osp.exists(keyp_path): + continue + + keypoints = read_keypoints(keyp_path) + if keypoints is None: + continue + + img_path = osp.join(self.img_folder, img_fname) + self.img_paths += [img_path] * keypoints.shape[0] + self.keypoints.append(keypoints) + # self.img_fnames.append(osp.join(self.img_folder, img_fname)) + # self.keyp_fnames.append(keyp_path) + + self.keypoints = np.concatenate(self.keypoints, axis=0) + self.num_items = len(self.img_paths) + + source_idxs, target_idxs = dset_to_body_model( + dset='openpose25+hands+face', + model_type='smplx', use_hands=True, use_face=True, + use_face_contour=self.use_face_contour, + keyp_format=self.keyp_format) + self.source_idxs = source_idxs + self.target_idxs = target_idxs + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + face_idxs = idxs_dict['face'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.face_idxs = np.asarray(face_idxs) + self.left_hand_idxs = np.asarray(left_hand_idxs) + self.right_hand_idxs = np.asarray(right_hand_idxs) + + self.body_dset_factor = 1.2 + self.head_dset_factor = 2.0 + self.hand_dset_factor = 2.0 + + def __repr__(self): + return 'OpenPose( \n\t Split: {}\n)'.format(self.split) + + def name(self): + return 'OpenPose' + + def __len__(self): + return self.num_items + + def get_elements_per_index(self): + return 1 + + def only_2d(self): + return True + + def __getitem__(self, index): + img_fn = self.img_paths[index] + img = read_img(img_fn) + + # keypoints2d = read_keypoints() + + # Pad to compensate for extra keypoints + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + keypoints = self.keypoints[index] + output_keypoints2d[self.target_idxs] = keypoints[self.source_idxs] + + is_right = self.is_right + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.head_only or self.hand_only: + body_conf[:] = 0.0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf = output_keypoints2d[self.left_hand_idxs, -1] + right_hand_conf = output_keypoints2d[self.right_hand_idxs, -1] + if self.head_only: + left_hand_conf[:] = 0.0 + right_hand_conf[:] = 0.0 + + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.hand_only: + +-- Chunk 2 -- +// openpose.py:189-259 + face_conf[:] = 0.0 + if is_right: + left_hand_conf[:] = 0 + else: + right_hand_conf[:] = 0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf[left_hand_conf < self.hand_thresh] = 0.0 + right_hand_conf[right_hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + left_hand_conf = ( + left_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + right_hand_conf = ( + right_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.left_hand_idxs, -1] = left_hand_conf + output_keypoints2d[self.right_hand_idxs, -1] = right_hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + if self.head_only: + dset_scale_factor = self.head_dset_factor + elif self.hand_only: + dset_scale_factor = self.hand_dset_factor + else: + dset_scale_factor = self.body_dset_factor + + center, scale, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=dset_scale_factor, + ) + if center is None: + return None, None, None, None + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + + orig_center, _, orig_bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=dset_scale_factor, + ) + target.add_field('orig_center', orig_center) + target.add_field('orig_bbox_size', orig_bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + + # start = time.perf_counter() + if self.transforms is not None: + force_flip = not self.is_right and self.hand_only + img, cropped_image, target = self.transforms( + img, target, force_flip=force_flip) + + img_fn = osp.split(img_fn)[1] + target.add_field('fname', img_fn) + # logger.info('Transforms: {}'.format(time.perf_counter() - start)) + + return img, cropped_image, target, index + + + +-- Chunk 3 -- +// openpose.py:260-409 +ss OpenPoseTracks(dutils.Dataset): + def __init__(self, data_folder='data/openpose_tracks', + img_folder='images', + keyp_folder='keypoints', + split='train', + head_only=False, + hand_only=False, + is_right=False, + use_face=True, use_hands=True, use_face_contour=False, + pid=4, + model_type='smplx', + keyp_format='coco25', + dtype=torch.float32, + joints_to_ign=None, + use_joint_conf=True, + metrics=None, + transforms=None, + body_thresh=0.1, + hand_thresh=0.2, + face_thresh=0.4, + binarization=True, + limit=1500, + **kwargs): + + super(OpenPoseTracks, self).__init__() + assert nand(head_only, hand_only), ( + 'Hand only and head only can\'t be True at the same time') + + self.is_right = is_right + self.head_only = head_only + self.hand_only = hand_only + logger.info(f'Hand only: {self.hand_only}') + logger.info(f'Is right: {self.is_right}') + + self.split = split + self.is_train = 'train' in split + + self.data_folder = osp.expandvars(osp.expanduser(data_folder)) + self.img_folder = osp.join(self.data_folder, img_folder) + self.keyp_folder = osp.join(self.data_folder, keyp_folder) + + self.transforms = transforms + self.dtype = dtype + + self.use_face = use_face + self.use_hands = use_hands + self.use_face_contour = use_face_contour + self.model_type = model_type + self.keyp_format = keyp_format + self.use_joint_conf = use_joint_conf + self.body_thresh = body_thresh + self.hand_thresh = hand_thresh + self.face_thresh = face_thresh + self.binarization = binarization + + track_path = osp.join(self.data_folder, 'by_id.json') + with open(track_path, 'r') as f: + track_data = json.load(f)[f'{pid}'] + + self.num_items = len(track_data) + + logger.info(track_data[0].keys()) + imgnames = [] + keypoints = [] + for idx, d in enumerate(track_data): + keyps = np.array(d['keypoints'], dtype=np.float32)[:-2] + keypoints.append(keyps) + imgnames.append(d['fname']) + self.keypoints = np.stack(keypoints) + self.imgnames = np.stack(imgnames) + if limit > 0: + self.keypoints = self.keypoints[:-limit] + self.imgnames = self.imgnames[:-limit] + + source_idxs, target_idxs = dset_to_body_model( + dset='openpose25+hands+face', + model_type='smplx', use_hands=True, use_face=True, + use_face_contour=self.use_face_contour, + keyp_format=self.keyp_format) + self.source_idxs = source_idxs + self.target_idxs = target_idxs + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + face_idxs = idxs_dict['face'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.face_idxs = np.asarray(face_idxs) + self.left_hand_idxs = np.asarray(left_hand_idxs) + self.right_hand_idxs = np.asarray(right_hand_idxs) + + self.body_dset_factor = 1.2 + self.head_dset_factor = 2.0 + self.hand_dset_factor = 2.0 + + def __repr__(self): + return 'OpenPose( \n\t Split: {}\n)'.format(self.split) + + def name(self): + return 'OpenPose' + + def __len__(self): + return self.num_items + + def get_elements_per_index(self): + return 1 + + def only_2d(self): + return True + + def __getitem__(self, index): + img_fn = osp.join(self.img_folder, self.imgnames[index]) + img = read_img(img_fn) + + # keypoints2d = read_keypoints() + + # Pad to compensate for extra keypoints + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + keypoints = self.keypoints[index] + output_keypoints2d[self.target_idxs] = keypoints[self.source_idxs] + + is_right = self.is_right + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.head_only or self.hand_only: + body_conf[:] = 0.0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf = output_keypoints2d[self.left_hand_idxs, -1] + right_hand_conf = output_keypoints2d[self.right_hand_idxs, -1] + if self.head_only: + left_hand_conf[:] = 0.0 + right_hand_conf[:] = 0.0 + + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.hand_only: + face_conf[:] = 0.0 + if is_right: + +-- Chunk 4 -- +// openpose.py:410-475 + left_hand_conf[:] = 0 + else: + right_hand_conf[:] = 0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf[left_hand_conf < self.hand_thresh] = 0.0 + right_hand_conf[right_hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + left_hand_conf = ( + left_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + right_hand_conf = ( + right_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.left_hand_idxs, -1] = left_hand_conf + output_keypoints2d[self.right_hand_idxs, -1] = right_hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + if self.head_only: + dset_scale_factor = self.head_dset_factor + elif self.hand_only: + dset_scale_factor = self.hand_dset_factor + else: + dset_scale_factor = self.body_dset_factor + + center, scale, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=dset_scale_factor, + ) + if center is None: + return None, None, None, None + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + + orig_center, _, orig_bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=dset_scale_factor, + ) + target.add_field('orig_center', orig_center) + target.add_field('orig_bbox_size', orig_bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + + target.add_field('fname', self.imgnames[index]) + # start = time.perf_counter() + if self.transforms is not None: + force_flip = not self.is_right and self.hand_only + img, cropped_image, target = self.transforms( + img, target, force_flip=force_flip) + + # logger.info('Transforms: {}'.format(time.perf_counter() - start)) + + return img, cropped_image, target, index + +=== File: expose/data/datasets/stirling.py === + +-- Chunk 1 -- +// stirling.py:38-105 +ss Stirling3D(dutils.Dataset): + def __init__(self, data_path='data/stirling/HQ', + head_only=True, + split='train', + dtype=torch.float32, + metrics=None, + transforms=None, + **kwargs): + super(Stirling3D, self).__init__() + assert head_only, 'Stirling3D can only be used as a head only dataset' + + self.split = split + assert 'test' in split, ( + f'Stirling3D can only be used for testing, but got split: {split}' + ) + if metrics is None: + metrics = [] + self.metrics = metrics + + self.data_path = osp.expandvars(osp.expanduser(data_path)) + self.transforms = transforms + self.dtype = dtype + + self.img_paths = np.array( + [osp.join(self.data_path, fname) + for fname in sorted(os.listdir(self.data_path))] + ) + self.num_items = len(self.img_paths) + + def get_elements_per_index(self): + return 1 + + def __repr__(self): + return 'Stirling3D( \n\t Split: {self.split}\n)' + + def name(self): + return f'Stirling3D/{self.split}' + + def get_num_joints(self): + return 0 + + def only_2d(self): + return False + + def __len__(self): + return self.num_items + + def __getitem__(self, index): + img = read_img(self.img_paths[index]) + + H, W, _ = img.shape + bbox = np.array([0, 0, W - 1, H - 1], dtype=np.float32) + target = BoundingBox(bbox, size=img.shape) + + center = np.array([W, H], dtype=np.float32) * 0.5 + target.add_field('center', center) + + center, scale, bbox_size = bbox_to_center_scale(bbox) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('image_size', img.shape) + + if self.transforms is not None: + img, cropped_image, target = self.transforms(img, target) + + target.add_field('name', self.name()) + target.add_field('fname', osp.split(self.img_paths[index])[1]) + return img, cropped_image, target, index + +=== File: expose/data/datasets/curated_fittings.py === + +-- Chunk 1 -- +// curated_fittings.py:42-191 +ss CuratedFittings(dutils.Dataset): + def __init__(self, data_path='data/curated_fits', + split='train', + img_folder='', + use_face=True, use_hands=True, use_face_contour=False, + head_only=False, + hand_only=False, + model_type='smplx', + keyp_format='coco25', + dtype=torch.float32, + metrics=None, + transforms=None, + num_betas=10, + num_expression_coeffs=10, + body_thresh=0.1, + hand_thresh=0.2, + face_thresh=0.4, + min_hand_keypoints=8, + min_head_keypoints=8, + binarization=True, + return_params=True, + vertex_folder='vertices', + vertex_flip_correspondences='', + **kwargs): + super(CuratedFittings, self).__init__() + + assert nand(head_only, hand_only), ( + 'Hand only and head only can\'t be True at the same time') + + self.binarization = binarization + if metrics is None: + metrics = [] + self.metrics = metrics + self.min_hand_keypoints = min_hand_keypoints + self.min_head_keypoints = min_head_keypoints + + if 'test' in split: + split = 'val' + self.split = split + self.is_train = 'train' in split + self.num_betas = num_betas + self.return_params = return_params + + self.head_only = head_only + self.hand_only = hand_only + + data_path = osp.expandvars(osp.expanduser(data_path)) + self.data_path = osp.join(data_path, f'{split}.npz') + self.transforms = transforms + self.dtype = dtype + + vertex_flip_correspondences = osp.expandvars( + vertex_flip_correspondences) + err_msg = ( + 'Vertex flip correspondences path does not exist:' + + f' {vertex_flip_correspondences}' + ) + assert osp.exists(vertex_flip_correspondences), err_msg + flip_data = np.load(vertex_flip_correspondences) + self.bc = flip_data['bc'] + self.closest_faces = flip_data['closest_faces'] + + self.img_folder = osp.expandvars(osp.join(img_folder, split)) + folder_map_fname = osp.expandvars( + osp.join(self.img_folder, FOLDER_MAP_FNAME)) + self.use_folder_split = osp.exists(folder_map_fname) + if self.use_folder_split: + with open(folder_map_fname, 'rb') as f: + data_dict = pickle.load(f) + self.items_per_folder = max(data_dict.values()) + + self.use_face = use_face + self.use_hands = use_hands + self.use_face_contour = use_face_contour + self.model_type = model_type + self.keyp_format = keyp_format + self.num_expression_coeffs = num_expression_coeffs + self.body_thresh = body_thresh + self.hand_thresh = hand_thresh + self.face_thresh = face_thresh + + data = np.load(self.data_path, allow_pickle=True) + data = {key: data[key] for key in data.keys()} + + self.betas = data['betas'].astype(np.float32) + self.expression = data['expression'].astype(np.float32) + self.keypoints2D = data['keypoints2D'].astype(np.float32) + self.pose = data['pose'].astype(np.float32) + self.img_fns = np.asarray(data['img_fns'], dtype=np.string_) + self.indices = None + if 'indices' in data: + self.indices = np.asarray(data['indices'], dtype=np.int64) + self.is_right = None + if 'is_right' in data: + self.is_right = np.asarray(data['is_right'], dtype=np.bool_) + if 'dset_name' in data: + self.dset_name = np.asarray(data['dset_name'], dtype=np.string_) + self.vertex_folder = osp.join(data_path, vertex_folder, split) + + if self.use_folder_split: + self.num_items = sum(data_dict.values()) + # assert self.num_items == self.pose.shape[0] + else: + self.num_items = self.pose.shape[0] + + data.clear() + del data + + source_idxs, target_idxs = dset_to_body_model( + dset='openpose25+hands+face', + model_type='smplx', use_hands=True, use_face=True, + use_face_contour=self.use_face_contour, + keyp_format=self.keyp_format) + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + idxs_dict = get_part_idxs() + body_idxs = idxs_dict['body'] + hand_idxs = idxs_dict['hand'] + left_hand_idxs = idxs_dict['left_hand'] + right_hand_idxs = idxs_dict['right_hand'] + face_idxs = idxs_dict['face'] + head_idxs = idxs_dict['head'] + if not use_face_contour: + face_idxs = face_idxs[:-17] + head_idxs = head_idxs[:-17] + + self.body_idxs = np.asarray(body_idxs) + self.hand_idxs = np.asarray(hand_idxs) + self.left_hand_idxs = np.asarray(left_hand_idxs) + self.right_hand_idxs = np.asarray(right_hand_idxs) + self.face_idxs = np.asarray(face_idxs) + self.head_idxs = np.asarray(head_idxs) + + self.body_dset_factor = 1.2 + self.head_dset_factor = 2.0 + self.hand_dset_factor = 2.0 + + def get_elements_per_index(self): + return 1 + + def __repr__(self): + return 'Curated Fittings( \n\t Split: {}\n)'.format(self.split) + + def name(self): + return 'Curated Fittings/{}'.format(self.split) + + def get_num_joints(self): + return 25 + 2 * 21 + 51 + 17 * self.use_face_contour + + +-- Chunk 2 -- +// curated_fittings.py:192-341 + def __len__(self): + return self.num_items + + def only_2d(self): + return False + + def __getitem__(self, index): + img_index = index + if self.indices is not None: + img_index = self.indices[index] + + if self.use_folder_split: + folder_idx = img_index // self.items_per_folder + file_idx = img_index + + is_right = None + if self.is_right is not None: + is_right = self.is_right[index] + + pose = self.pose[index].copy() + betas = self.betas[index, :self.num_betas] + expression = self.expression[index] + + eye_offset = 0 if pose.shape[0] == 53 else 2 + global_pose = pose[0].reshape(-1) + + body_pose = pose[1:22, :].reshape(-1) + jaw_pose = pose[22].reshape(-1) + left_hand_pose = pose[ + 23 + eye_offset:23 + eye_offset + 15].reshape(-1) + right_hand_pose = pose[23 + 15 + eye_offset:].reshape(-1) + + # start = time.perf_counter() + keypoints2d = self.keypoints2D[index] + # logger.info('Reading keypoints: {}', time.perf_counter() - start) + + if self.use_folder_split: + img_fn = osp.join(self.img_folder, + 'folder_{:010d}'.format(folder_idx), + '{:010d}.jpg'.format(file_idx)) + else: + img_fn = self.img_fns[index].decode('utf-8') + + # start = time.perf_counter() + img = read_img(img_fn) + # logger.info('Reading image: {}'.format(time.perf_counter() - start)) + + # Pad to compensate for extra keypoints + output_keypoints2d = np.zeros([127 + 17 * self.use_face_contour, + 3], dtype=np.float32) + + output_keypoints2d[self.target_idxs] = keypoints2d[self.source_idxs] + + # Remove joints with negative confidence + output_keypoints2d[output_keypoints2d[:, -1] < 0, -1] = 0 + if self.body_thresh > 0: + # Only keep the points with confidence above a threshold + body_conf = output_keypoints2d[self.body_idxs, -1] + if self.head_only or self.hand_only: + body_conf[:] = 0.0 + + left_hand_conf = output_keypoints2d[self.left_hand_idxs, -1] + right_hand_conf = output_keypoints2d[self.right_hand_idxs, -1] + if self.head_only: + left_hand_conf[:] = 0.0 + right_hand_conf[:] = 0.0 + + face_conf = output_keypoints2d[self.face_idxs, -1] + if self.hand_only: + face_conf[:] = 0.0 + if is_right: + left_hand_conf[:] = 0 + else: + right_hand_conf[:] = 0 + + body_conf[body_conf < self.body_thresh] = 0.0 + left_hand_conf[left_hand_conf < self.hand_thresh] = 0.0 + right_hand_conf[right_hand_conf < self.hand_thresh] = 0.0 + face_conf[face_conf < self.face_thresh] = 0.0 + if self.binarization: + body_conf = ( + body_conf >= self.body_thresh).astype( + output_keypoints2d.dtype) + left_hand_conf = ( + left_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + right_hand_conf = ( + right_hand_conf >= self.hand_thresh).astype( + output_keypoints2d.dtype) + face_conf = ( + face_conf >= self.face_thresh).astype( + output_keypoints2d.dtype) + + output_keypoints2d[self.body_idxs, -1] = body_conf + output_keypoints2d[self.left_hand_idxs, -1] = left_hand_conf + output_keypoints2d[self.right_hand_idxs, -1] = right_hand_conf + output_keypoints2d[self.face_idxs, -1] = face_conf + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + + if self.head_only: + keypoints = output_keypoints2d[self.head_idxs, :-1] + conf = output_keypoints2d[self.head_idxs, -1] + elif self.hand_only: + keypoints = output_keypoints2d[self.hand_idxs, :-1] + conf = output_keypoints2d[self.hand_idxs, -1] + else: + keypoints = output_keypoints2d[:, :-1] + conf = output_keypoints2d[:, -1] + + left_hand_bbox = keyps_to_bbox( + output_keypoints2d[self.left_hand_idxs, :-1], + output_keypoints2d[self.left_hand_idxs, -1], + img_size=img.shape, scale=1.5) + left_hand_bbox_target = BoundingBox(left_hand_bbox, img.shape) + has_left_hand = (output_keypoints2d[self.left_hand_idxs, -1].sum() > + self.min_hand_keypoints) + if has_left_hand: + target.add_field('left_hand_bbox', left_hand_bbox_target) + target.add_field( + 'orig_left_hand_bbox', + BoundingBox(left_hand_bbox, img.shape, transform=False)) + + right_hand_bbox = keyps_to_bbox( + output_keypoints2d[self.right_hand_idxs, :-1], + output_keypoints2d[self.right_hand_idxs, -1], + img_size=img.shape, scale=1.5) + right_hand_bbox_target = BoundingBox(right_hand_bbox, img.shape) + has_right_hand = (output_keypoints2d[self.right_hand_idxs, -1].sum() > + self.min_hand_keypoints) + if has_right_hand: + target.add_field('right_hand_bbox', right_hand_bbox_target) + target.add_field( + 'orig_right_hand_bbox', + BoundingBox(right_hand_bbox, img.shape, transform=False)) + + head_bbox = keyps_to_bbox( + output_keypoints2d[self.head_idxs, :-1], + output_keypoints2d[self.head_idxs, -1], + img_size=img.shape, scale=1.2) + head_bbox_target = BoundingBox(head_bbox, img.shape) + has_head = (output_keypoints2d[self.head_idxs, -1].sum() > + self.min_head_keypoints) + if has_head: + target.add_field('head_bbox', head_bbox_target) + target.add_field( + 'orig_head_bbox', + BoundingBox(head_bbox, img.shape, transform=False)) + + +-- Chunk 3 -- +// curated_fittings.py:342-402 + if self.head_only: + dset_scale_factor = self.head_dset_factor + elif self.hand_only: + dset_scale_factor = self.hand_dset_factor + else: + dset_scale_factor = self.body_dset_factor + center, scale, bbox_size = bbox_to_center_scale( + keyps_to_bbox(keypoints, conf, img_size=img.shape), + dset_scale_factor=dset_scale_factor, + ) + target.add_field('center', center) + target.add_field('scale', scale) + target.add_field('bbox_size', bbox_size) + target.add_field('keypoints_hd', output_keypoints2d) + target.add_field('orig_center', center) + target.add_field('orig_bbox_size', bbox_size) + + # # start = time.perf_counter() + if self.return_params: + betas_field = Betas(betas=betas) + target.add_field('betas', betas_field) + + expression_field = Expression(expression=expression) + target.add_field('expression', expression_field) + + global_pose_field = GlobalPose(global_pose=global_pose) + target.add_field('global_pose', global_pose_field) + body_pose_field = BodyPose(body_pose=body_pose) + target.add_field('body_pose', body_pose_field) + hand_pose_field = HandPose(left_hand_pose=left_hand_pose, + right_hand_pose=right_hand_pose) + target.add_field('hand_pose', hand_pose_field) + jaw_pose_field = JawPose(jaw_pose=jaw_pose) + target.add_field('jaw_pose', jaw_pose_field) + + if hasattr(self, 'dset_name'): + dset_name = self.dset_name[index].decode('utf-8') + vertex_fname = osp.join( + self.vertex_folder, f'{dset_name}_{index:06d}.npy') + vertices = np.load(vertex_fname) + H, W, _ = img.shape + + intrinsics = np.array([[5000, 0, 0.5 * W], + [0, 5000, 0.5 * H], + [0, 0, 1]], dtype=np.float32) + + target.add_field('intrinsics', intrinsics) + vertex_field = Vertices( + vertices, bc=self.bc, closest_faces=self.closest_faces) + target.add_field('vertices', vertex_field) + + target.add_field('fname', f'{index:05d}.jpg') + cropped_image = None + if self.transforms is not None: + force_flip = False + if is_right is not None: + force_flip = not is_right and self.hand_only + img, cropped_image, cropped_target = self.transforms( + img, target, force_flip=force_flip) + + return img, cropped_image, cropped_target, index + +=== File: expose/data/datasets/freihand.py === + +-- Chunk 1 -- +// freihand.py:47-196 +ss FreiHand(dutils.Dataset): + def __init__(self, data_path='data/freihand', + hand_only=True, + split='train', + dtype=torch.float32, + joints_to_ign=None, + metrics=None, + transforms=None, + return_params=True, + return_vertices=True, + use_face_contour=False, + return_shape=False, + file_format='json', + **kwargs): + + super(FreiHand, self).__init__() + + assert hand_only, 'FreiHand can only be used as a hand dataset' + + if metrics is None: + metrics = [] + self.metrics = metrics + + self.split = split + self.is_train = 'train' in split + self.return_params = return_params + self.return_vertices = return_vertices + self.use_face_contour = use_face_contour + + self.return_shape = return_shape + key = ('training' if 'val' in split or 'train' in split else + 'evaluation') + self.data_path = osp.expandvars(osp.expanduser(data_path)) + self.img_folder = osp.join(self.data_path, key, 'rgb') + self.transforms = transforms + self.dtype = dtype + + intrinsics_path = osp.join(self.data_path, f'{key}_K.json') + param_path = osp.join(self.data_path, f'{key}_mano.json') + xyz_path = osp.join(self.data_path, f'{key}_xyz.json') + vertices_path = osp.join(self.data_path, f'{key}_verts.json') + + start = time.perf_counter() + if file_format == 'json': + with open(intrinsics_path, 'r') as f: + intrinsics = json.load(f) + if self.split != 'test': + with open(param_path, 'r') as f: + param = json.load(f) + with open(xyz_path, 'r') as f: + xyz = json.load(f) + if self.return_vertices: + with open(vertices_path, 'r') as f: + vertices = json.load(f) + elif file_format == 'npz': + param_path = osp.join(self.data_path, f'{key}.npz') + data = np.load(param_path) + intrinsics = data['intrinsics'] + param = data['param'] + xyz = data['xyz'] + if self.return_vertices: + vertices = data['vertices'] + self.translation = np.asarray(data['translation']) + + data.close() + elapsed = time.perf_counter() - start + logger.info(f'Loading parameters: {elapsed}') + + mean_pose_path = osp.expandvars( + '$CLUSTER_HOME/SMPL_HF_Regressor_data/data/all_means.pkl') + mean_poses_dict = {} + if osp.exists(mean_pose_path): + logger.info('Loading mean pose from: {} ', mean_pose_path) + with open(mean_pose_path, 'rb') as f: + mean_poses_dict = pickle.load(f) + + if self.split != 'test': + split_size = 0.8 + # num_items = len(xyz) * 4 + num_green_bg = len(xyz) + # For green background images + train_idxs = np.arange(0, int(split_size * num_green_bg)) + val_idxs = np.arange(int(split_size * num_green_bg), num_green_bg) + + all_train_idxs = [] + all_val_idxs = [] + for idx in range(4): + all_val_idxs.append(val_idxs + num_green_bg * idx) + all_train_idxs.append(train_idxs + num_green_bg * idx) + self.train_idxs = np.concatenate(all_train_idxs) + self.val_idxs = np.concatenate(all_val_idxs) + + if split == 'train': + self.img_idxs = self.train_idxs + self.param_idxs = self.train_idxs % num_green_bg + self.start = 0 + elif split == 'val': + self.img_idxs = self.val_idxs + self.param_idxs = self.val_idxs % num_green_bg + # self.start = len(self.train_idxs) + elif 'test' in split: + self.img_idxs = np.arange(len(intrinsics)) + self.param_idxs = np.arange(len(intrinsics)) + + self.num_items = len(self.img_idxs) + + self.intrinsics = intrinsics + if 'test' not in split: + xyz = np.asarray(xyz, dtype=np.float32) + param = np.asarray(param, dtype=np.float32).reshape(len(xyz), -1) + if self.return_vertices: + vertices = np.asarray(vertices, dtype=np.float32) + + right_hand_mean = mean_poses_dict['right_hand_pose']['aa'].squeeze() + self.poses = param[:, :48].reshape(num_green_bg, -1, 3) + self.poses[:, 1:] += right_hand_mean[np.newaxis] + self.betas = param[:, 48:58].copy() + + intrinsics = np.asarray(intrinsics, dtype=np.float32) + + if self.return_vertices: + self.vertices = vertices + self.xyz = xyz + + folder_map_fname = osp.expandvars( + osp.join(self.data_path, split, FOLDER_MAP_FNAME)) + self.use_folder_split = osp.exists(folder_map_fname) + if self.use_folder_split: + self.img_folder = osp.join(self.data_path, split) + logger.info(self.img_folder) + with open(folder_map_fname, 'rb') as f: + data_dict = pickle.load(f) + self.items_per_folder = max(data_dict.values()) + + if joints_to_ign is None: + joints_to_ign = [] + self.joints_to_ign = np.array(joints_to_ign, dtype=np.int32) + + source_idxs, target_idxs = dset_to_body_model(dset='freihand') + self.source_idxs = np.asarray(source_idxs, dtype=np.int64) + self.target_idxs = np.asarray(target_idxs, dtype=np.int64) + + def get_elements_per_index(self): + return 1 + + def __repr__(self): + return 'FreiHand( \n\t Split: {}\n)'.format(self.split) + + def name(self): + return 'FreiHand/{}'.format(self.split) + +-- Chunk 2 -- +// freihand.py:197-329 + + def get_num_joints(self): + return 21 + + def __len__(self): + return self.num_items + + def only_2d(self): + return False + + def project_points(self, K, xyz): + uv = np.matmul(K, xyz.T).T + return uv[:, :2] / uv[:, -1:] + + def __getitem__(self, index): + img_idx = self.img_idxs[index] + param_idx = self.param_idxs[index] + + if self.use_folder_split: + folder_idx = index // self.items_per_folder + file_idx = index + + K = self.intrinsics[param_idx].copy() + if 'test' not in self.split: + pose = self.poses[param_idx].copy() + + global_pose = pose[0].reshape(-1) + right_hand_pose = pose[1:].reshape(-1) + + scale = 0.5 * (K[0, 0] + K[1, 1]) + # focal = scale * 2 / IMG_SIZE + # pp = K[:2, 2] / scale - IMG_SIZE / (2 * scale) + + keypoints3d = self.xyz[param_idx].copy() + keypoints2d = self.project_points(K, keypoints3d) + # pp -= keypoints3d[0, :2] + + keypoints3d -= keypoints3d[0] + + keypoints2d = np.concatenate( + [keypoints2d, np.ones_like(keypoints2d[:, [-1]])], axis=-1 + ) + keypoints3d = np.concatenate( + [keypoints3d, np.ones_like(keypoints2d[:, [-1]])], axis=-1 + ) + + # logger.info('Reading keypoints: {}', time.perf_counter() - start) + + if self.use_folder_split: + img_fn = osp.join( + self.img_folder, f'folder_{folder_idx:010d}', + f'{file_idx:010d}.jpg') + else: + img_fn = osp.join(self.img_folder, f'{img_idx:08d}.jpg') + + # start = time.perf_counter() + img = read_img(img_fn) + # logger.info('Reading image: {}'.format(time.perf_counter() - start)) + + if 'test' in self.split: + bbox = np.array([0, 0, 224, 224], dtype=np.float32) + target = BoundingBox(bbox, size=img.shape) + else: + # Pad to compensate for extra keypoints + output_keypoints2d = np.zeros( + [127 + 17 * self.use_face_contour, 3], dtype=np.float32) + output_keypoints3d = np.zeros( + [127 + 17 * self.use_face_contour, 4], dtype=np.float32) + + output_keypoints2d[self.target_idxs] = keypoints2d[self.source_idxs] + output_keypoints3d[self.target_idxs] = keypoints3d[self.source_idxs] + + target = Keypoints2D( + output_keypoints2d, img.shape, flip_axis=0, dtype=self.dtype) + # _, scale, _ = bbox_to_center_scale( + # keyps_to_bbox(output_keypoints2d[:, :-1], + # output_keypoints2d[:, -1], img_size=img.shape), + # dset_scale_factor=2.0, ref_bbox_size=224, + # ) + keyp3d_target = Keypoints3D( + output_keypoints3d, img.shape[:-1], flip_axis=0, dtype=self.dtype) + target.add_field('keypoints3d', keyp3d_target) + target.add_field('intrinsics', K) + + target.add_field('bbox_size', IMG_SIZE / 2) + center = np.array([IMG_SIZE, IMG_SIZE], dtype=np.float32) * 0.5 + target.add_field('orig_center', np.asarray(img.shape[:-1]) * 0.5) + target.add_field('center', center) + scale = IMG_SIZE / REF_BOX_SIZE + target.add_field('scale', scale) + # target.bbox = np.asarray([0, 0, IMG_SIZE, IMG_SIZE], dtype=np.float32) + + # target.add_field('camera', WeakPerspectiveCamera(focal, pp)) + + # start = time.perf_counter() + if self.return_params: + global_pose_field = GlobalPose(global_pose=global_pose) + target.add_field('global_pose', global_pose_field) + hand_pose_field = HandPose(right_hand_pose=right_hand_pose, + left_hand_pose=None) + target.add_field('hand_pose', hand_pose_field) + + if hasattr(self, 'translation'): + translation = self.translation[param_idx] + else: + translation = np.zeros([3], dtype=np.float32) + target.add_field('translation', translation) + + if self.return_vertices: + vertices = self.vertices[param_idx] + hand_vertices_field = Vertices(vertices) + target.add_field('vertices', hand_vertices_field) + if self.return_shape: + target.add_field('betas', Betas(self.betas[param_idx])) + + # print('SMPL-HF Field {}'.format(time.perf_counter() - start)) + + # start = time.perf_counter() + if self.transforms is not None: + full_img, cropped_image, target = self.transforms( + img, target, dset_scale_factor=2.0) + # logger.info('Transforms: {}'.format(time.perf_counter() - start)) + + target.add_field('name', self.name()) + # Key used to access the fit dict + # img_fn = osp.split(self.img_fns[index])[1].decode('utf-8') + + # dict_key = ['curated_fits', img_fn, index] + + # dict_key = tuple(dict_key) + # target.add_field('dict_key', dict_key) + + return full_img, cropped_image, target, index + +=== File: expose/data/transforms/transforms.py === + +-- Chunk 1 -- +// transforms.py:37-57 +ss Compose(object): + def __init__(self, transforms): + self.transforms = transforms + self.timers = {} + + def __call__(self, image, target, **kwargs): + next_input = (image, target) + for t in self.transforms: + output = t(*next_input, **kwargs) + next_input = output + return next_input + + def __repr__(self): + format_string = self.__class__.__name__ + "(" + for t in self.transforms: + format_string += "\n" + format_string += " {0}".format(t) + format_string += "\n)" + return format_string + + + +-- Chunk 2 -- +// transforms.py:58-127 +ss RandomHorizontalFlip(object): + def __init__(self, prob=0.5): + self.prob = prob + + def __str__(self): + return 'RandomHorizontalFlip({:.03f})'.format(self.prob) + + def _flip(self, img): + if img is None: + return None + if 'numpy.ndarray' in str(type(img)): + return np.ascontiguousarray(img[:, ::-1, :]).copy() + else: + return F.hflip(img) + + def __call__(self, image, target, force_flip=False, **kwargs): + flip = random.random() < self.prob + target.add_field('is_flipped', flip) + if flip or force_flip: + output_image = self._flip(image) + flipped_target = target.transpose(0) + + _, W, _ = output_image.shape + + left_hand_bbox, right_hand_bbox = None, None + if flipped_target.has_field('left_hand_bbox'): + left_hand_bbox = flipped_target.get_field('left_hand_bbox') + if target.has_field('right_hand_bbox'): + right_hand_bbox = flipped_target.get_field('right_hand_bbox') + if left_hand_bbox is not None: + flipped_target.add_field('right_hand_bbox', left_hand_bbox) + if right_hand_bbox is not None: + flipped_target.add_field('left_hand_bbox', right_hand_bbox) + + width = target.size[1] + center = target.get_field('center') + TO_REMOVE = 1 + center[0] = width - center[0] - TO_REMOVE + + if target.has_field('keypoints_hd'): + keypoints_hd = target.get_field('keypoints_hd') + flipped_keypoints_hd = keypoints_hd.copy() + flipped_keypoints_hd[:, 0] = ( + width - flipped_keypoints_hd[:, 0] - TO_REMOVE) + flipped_keypoints_hd = flipped_keypoints_hd[target.FLIP_INDS] + flipped_target.add_field('keypoints_hd', flipped_keypoints_hd) + + # Update the center + flipped_target.add_field('center', center) + if target.has_field('orig_center'): + orig_center = target.get_field('orig_center').copy() + orig_center[0] = width - orig_center[0] - TO_REMOVE + flipped_target.add_field('orig_center', orig_center) + + if target.has_field('intrinsics'): + intrinsics = target.get_field('intrinsics') + cam_center = intrinsics[:2, 2].copy() + cam_center[0] = width - cam_center[0] - TO_REMOVE + intrinsics[:2, 2] = cam_center + flipped_target.add_field('intrinsics', intrinsics) + # Expressions are not symmetric, so we remove them from the targets + # when the image is flipped + if flipped_target.has_field('expression'): + flipped_target.delete_field('expression') + + return output_image, flipped_target + else: + return image, target + + + +-- Chunk 3 -- +// transforms.py:128-162 +ss BBoxCenterJitter(object): + def __init__(self, factor=0.0, dist='normal'): + super(BBoxCenterJitter, self).__init__() + self.factor = factor + self.dist = dist + assert self.dist in ['normal', 'uniform'], ( + f'Distribution must be normal or uniform, not {self.dist}') + + def __str__(self): + return f'BBoxCenterJitter({self.factor:0.2f})' + + def __call__(self, image, target, **kwargs): + if self.factor <= 1e-3: + return image, target + + bbox_size = target.get_field('bbox_size') + + jitter = bbox_size * self.factor + + if self.dist == 'normal': + center_jitter = np.random.randn(2) * jitter + elif self.dist == 'uniform': + center_jitter = np.random.rand(2) * 2 * jitter - jitter + + center = target.get_field('center') + H, W, _ = target.size + new_center = center + center_jitter + new_center[0] = np.clip(new_center[0], 0, W) + new_center[1] = np.clip(new_center[1], 0, H) + + target.add_field('center', new_center) + + return image, target + + + +-- Chunk 4 -- +// transforms.py:163-229 +ss SimulateLowRes(object): + def __init__( + self, + dist: str = 'categorical', + factor: float = 1.0, + cat_factors: Tuple[float] = (1.0,), + factor_min: float = 1.0, + factor_max: float = 1.0 + ) -> None: + self.factor_min = factor_min + self.factor_max = factor_max + self.dist = dist + self.cat_factors = cat_factors + assert dist in ['uniform', 'categorical'] + + def __str__(self) -> str: + if self.dist == 'uniform': + dist_str = ( + f'{self.dist.title()}: [{self.factor_min}, {self.factor_max}]') + else: + dist_str = ( + f'{self.dist.title()}: [{self.cat_factors}]') + return f'SimulateLowResolution({dist_str})' + + def _sample_low_res( + self, + image: Union[np.ndarray, pil_img.Image] + ) -> np.ndarray: + ''' + ''' + if self.dist == 'uniform': + downsample = self.factor_min != self.factor_max + if not downsample: + return image + factor = np.random.rand() * ( + self.factor_max - self.factor_min) + self.factor_min + elif self.dist == 'categorical': + if len(self.cat_factors) < 2: + return image + idx = np.random.randint(0, len(self.cat_factors)) + factor = self.cat_factors[idx] + + H, W, _ = image.shape + downsampled_image = cv2.resize( + image, (int(W // factor), int(H // factor)), cv2.INTER_NEAREST + ) + resized_image = cv2.resize( + downsampled_image, (W, H), cv2.INTER_LINEAR_EXACT) + return resized_image + + def __call__( + self, + image: Union[np.ndarray, pil_img.Image], + cropped_image: Union[np.ndarray, pil_img.Image], + target: GenericTarget, + **kwargs + ) -> Tuple[np.ndarray, np.ndarray, GenericTarget]: + ''' + ''' + if torch.is_tensor(cropped_image): + raise NotImplementedError + elif isinstance(cropped_image, (pil_img.Image, np.ndarray)): + resized_image = self._sample_low_res(cropped_image) + + return image, resized_image, target + + + +-- Chunk 5 -- +// transforms.py:230-270 +ss ChannelNoise(object): + def __init__(self, noise_scale=0.0): + self.noise_scale = noise_scale + + def __str__(self): + return 'ChannelNoise: {:.4f}'.format(self.noise_scale) + + def __call__( + self, + image: Union[np.ndarray, pil_img.Image], + cropped_image: Union[np.ndarray, pil_img.Image], + target: GenericTarget, + **kwargs + ) -> Tuple[np.ndarray, np.ndarray, GenericTarget]: + ''' + ''' + if self.noise_scale > 0: + if image.dtype == np.float32: + img_max = 1.0 + elif image.dtype == np.uint8: + img_max = 255 + # Each channel is multiplied with a number + # in the area [1 - self.noise_scale,1 + self.noise_scale] + pn = np.random.uniform(1 - self.noise_scale, + 1 + self.noise_scale, 3) + if not isinstance(image, (np.ndarray, )): + image = np.asarray(image) + if not isinstance(cropped_image, (np.ndarray,)): + cropped_image = np.asarray(cropped_image) + output_image = np.clip( + image * pn[np.newaxis, np.newaxis], 0, + img_max).astype(image.dtype) + output_cropped_image = np.clip( + cropped_image * pn[np.newaxis, np.newaxis], 0, + img_max).astype(image.dtype) + + return output_image, output_cropped_image, target + else: + return image, cropped_image, target + + + +-- Chunk 6 -- +// transforms.py:271-339 +ss RandomRotation(object): + def __init__(self, is_train: bool = True, + rotation_factor: float = 0): + self.is_train = is_train + self.rotation_factor = rotation_factor + + def __str__(self): + return f'RandomRotation(rotation_factor={self.rotation_factor})' + + def __repr__(self): + msg = [ + f'Training: {self.is_training}', + f'Rotation factor: {self.rotation_factor}' + ] + return '\n'.join(msg) + + def __call__(self, image, target, **kwargs): + rot = 0.0 + if not self.is_train: + return image, target + if self.is_train: + rot = min(2 * self.rotation_factor, + max(-2 * self.rotation_factor, + np.random.randn() * self.rotation_factor)) + if np.random.uniform() <= 0.6: + rot = 0 + if rot == 0.0: + return image, target + + (h, w) = image.shape[:2] + (cX, cY) = (w // 2, h // 2) + M = cv2.getRotationMatrix2D((cX, cY), rot, 1.0) + cos = np.abs(M[0, 0]) + sin = np.abs(M[0, 1]) + # compute the new bounding dimensions of the image + nW = int((h * sin) + (w * cos)) + nH = int((h * cos) + (w * sin)) + # adjust the rotation matrix to take into account translation + M[0, 2] += (nW / 2) - cX + M[1, 2] += (nH / 2) - cY + # perform the actual rotation and return the image + rotated_image = cv2.warpAffine(image, M, (nW, nH)) + + new_target = target.rotate(rot=rot) + + center = target.get_field('center') + center = np.dot(M[:2, :2], center) + M[:2, 2] + new_target.add_field('center', center) + + if target.has_field('keypoints_hd'): + keypoints_hd = target.get_field('keypoints_hd') + rotated_keyps = ( + np.dot(keypoints_hd[:, :2], M[:2, :2].T) + M[:2, 2] + + 1).astype(np.int) + rotated_keyps = np.concatenate( + [rotated_keyps, keypoints_hd[:, [2]]], axis=-1) + new_target.add_field('keypoints_hd', rotated_keyps) + + if target.has_field('intrinsics'): + intrinsics = target.get_field('intrinsics').copy() + + cam_center = intrinsics[:2, 2] + intrinsics[:2, 2] = ( + np.dot(M[:2, :2], cam_center) + M[:2, 2]) + new_target.add_field('intrinsics', intrinsics) + + return rotated_image, new_target + + + +-- Chunk 7 -- +// transforms.py:340-425 +ss Crop(object): + def __init__(self, is_train=True, + crop_size=224, + scale_factor_min=0.00, + scale_factor_max=0.00, + scale_factor=0.0, + scale_dist='uniform', + rotation_factor=0, + min_hand_bbox_dim=20, + min_head_bbox_dim=20, + ): + super(Crop, self).__init__() + self.crop_size = crop_size + + self.is_train = is_train + self.scale_factor_min = scale_factor_min + self.scale_factor_max = scale_factor_max + self.scale_factor = scale_factor + self.scale_dist = scale_dist + + self.rotation_factor = rotation_factor + self.min_hand_bbox_dim = min_hand_bbox_dim + self.min_head_bbox_dim = min_head_bbox_dim + + part_idxs = get_part_idxs() + self.left_hand_idxs = part_idxs['left_hand'] + self.right_hand_idxs = part_idxs['right_hand'] + self.head_idxs = part_idxs['head'] + + def __str__(self): + return 'Crop(size={}, scale={}, rotation_factor={})'.format( + self.crop_size, self.scale_factor, self.rotation_factor) + + def __repr__(self): + msg = 'Training: {}\n'.format(self.is_train) + msg += 'Crop size: {}\n'.format(self.crop_size) + msg += 'Scale factor augm: {}\n'.format(self.scale_factor) + msg += 'Rotation factor augm: {}'.format(self.rotation_factor) + return msg + + def __call__(self, image, target, **kwargs): + sc = 1.0 + if self.is_train: + if self.scale_dist == 'normal': + sc = min(1 + self.scale_factor, + max(1 - self.scale_factor, + np.random.randn() * self.scale_factor + 1)) + elif self.scale_dist == 'uniform': + if self.scale_factor_max == 0.0 and self.scale_factor_min == 0: + sc = 1.0 + else: + sc = (np.random.rand() * + (self.scale_factor_max - self.scale_factor_min) + + self.scale_factor_min) + + scale = target.get_field('scale') * sc + center = target.get_field('center') + orig_bbox_size = target.get_field('bbox_size') + bbox_size = orig_bbox_size * sc + + np_image = np.asarray(image) + cropped_image = crop( + np_image, center, scale, [self.crop_size, self.crop_size]) + cropped_target = target.crop( + center, scale, crop_size=self.crop_size) + + transf = get_transform( + center, scale, [self.crop_size, self.crop_size]) + + cropped_target.add_field('crop_transform', transf) + cropped_target.add_field('bbox_size', bbox_size) + + if target.has_field('intrinsics'): + intrinsics = target.get_field('intrinsics').copy() + fscale = cropped_image.shape[0] / orig_bbox_size + intrinsics[0, 0] *= (fscale / sc) + intrinsics[1, 1] *= (fscale / sc) + + cam_center = intrinsics[:2, 2] + intrinsics[:2, 2] = ( + np.dot(transf[:2, :2], cam_center) + transf[:2, 2]) + cropped_target.add_field('intrinsics', intrinsics) + + return np_image, cropped_image, cropped_target + + + +-- Chunk 8 -- +// transforms.py:426-449 +ss ColorJitter(object): + def __init__(self, brightness=0.0, contrast=0, saturation=0, hue=0): + super(ColorJitter, self).__init__() + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.hue = hue + + self.transform = torchvision.transforms.ColorJitter( + brightness=brightness, contrast=contrast, + saturation=saturation, hue=hue) + + def __repr__(self): + name = 'ColorJitter(\n' + name += f'brightness={self.brightness:.2f}\n' + name += f'contrast={self.contrast:.2f}\n' + name += f'saturation={self.saturation:.2f}\n' + name += f'hue={self.hue:.2f}' + return name + + def __call__(self, image, target, **kwargs): + return self.transform(image), target + + + +-- Chunk 9 -- +// transforms.py:450-464 +ss ToTensor(object): + def __init__(self): + super(ToTensor, self).__init__() + + def __repr__(self): + return 'ToTensor()' + + def __str__(self): + return 'ToTensor()' + + def __call__(self, image, cropped_image, target, **kwargs): + target.to_tensor() + return F.to_tensor(image), F.to_tensor(cropped_image), target + + + +-- Chunk 10 -- +// transforms.py:465-486 +ss Normalize(object): + def __init__(self, mean, std): + super(Normalize, self).__init__() + self.mean = mean + self.std = std + + def __str__(self): + msg = 'Mean: {}, '.format(self.mean) + msg += 'Std: {}\n'.format(self.std) + return msg + + def __repr__(self): + msg = 'Mean: {}\n'.format(self.mean) + msg += 'Std: {}\n'.format(self.std) + return msg + + def __call__(self, image, cropped_image, target, **kwargs): + output_image = F.normalize( + image, mean=self.mean, std=self.std) + output_cropped_image = F.normalize( + cropped_image, mean=self.mean, std=self.std) + return output_image, output_cropped_image, target + +=== File: expose/data/transforms/build.py === + +-- Chunk 1 -- +// build.py:23-86 + build_transforms(transf_cfg, is_train): + if is_train: + flip_prob = transf_cfg.get('flip_prob', 0) + downsample_dist = transf_cfg.get('downsample_dist', 'categorical') + downsample_cat_factors = transf_cfg.get( + 'downsample_cat_factors', (1.0, )) + downsample_factor_min = transf_cfg.get('downsample_factor_min', 1.0) + downsample_factor_max = transf_cfg.get('downsample_factor_max', 1.0) + scale_factor = transf_cfg.get('scale_factor', 0.0) + scale_factor_min = transf_cfg.get('scale_factor_min', 0.0) + scale_factor_max = transf_cfg.get('scale_factor_max', 0.0) + scale_dist = transf_cfg.get('scale_dist', 'uniform') + rotation_factor = transf_cfg.get('rotation_factor', 0.0) + noise_scale = transf_cfg.get('noise_scale', 0.0) + center_jitter_factor = transf_cfg.get('center_jitter_factor', 0.0) + center_jitter_dist = transf_cfg.get('center_jitter_dist', 'normal') + else: + flip_prob = 0.0 + downsample_dist = 'categorical' + downsample_cat_factors = (1.0,) + downsample_factor_min = 1.0 + downsample_factor_max = 1.0 + scale_factor = 0.0 + scale_factor_min = 1.0 + scale_factor_max = 1.0 + rotation_factor = 0.0 + noise_scale = 0.0 + center_jitter_factor = 0.0 + center_jitter_dist = transf_cfg.get('center_jitter_dist', 'normal') + scale_dist = transf_cfg.get('scale_dist', 'uniform') + + normalize_transform = T.Normalize( + transf_cfg.get('mean'), transf_cfg.get('std')) + logger.debug('Normalize {}', normalize_transform) + + crop_size = transf_cfg.get('crop_size') + crop = T.Crop(crop_size=crop_size, is_train=is_train, + scale_factor_max=scale_factor_max, + scale_factor_min=scale_factor_min, + scale_factor=scale_factor, + scale_dist=scale_dist) + pixel_noise = T.ChannelNoise(noise_scale=noise_scale) + logger.debug('Crop {}', crop) + + downsample = T.SimulateLowRes( + dist=downsample_dist, + cat_factors=downsample_cat_factors, + factor_min=downsample_factor_min, + factor_max=downsample_factor_max) + + transform = T.Compose( + [ + T.BBoxCenterJitter(center_jitter_factor, dist=center_jitter_dist), + T.RandomHorizontalFlip(flip_prob), + T.RandomRotation( + is_train=is_train, rotation_factor=rotation_factor), + crop, + pixel_noise, + downsample, + T.ToTensor(), + normalize_transform, + ] + ) + return transform + +=== File: expose/data/transforms/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/data/transforms/__init__.py:1-18 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + +from .build import build_transforms +from .transforms import * + +=== File: expose/data/targets/generic_target.py === + +-- Chunk 1 -- +// generic_target.py:24-84 +ss GenericTarget(ABC): + def __init__(self): + super(GenericTarget, self).__init__() + self.extra_fields = {} + + def __del__(self): + if hasattr(self, 'extra_fields'): + self.extra_fields.clear() + + def add_field(self, field, field_data): + self.extra_fields[field] = field_data + + def get_field(self, field): + return self.extra_fields[field] + + def has_field(self, field): + return field in self.extra_fields + + def delete_field(self, field): + if field in self.extra_fields: + del self.extra_fields[field] + + def transpose(self, method): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + self.add_field(k, v) + self.add_field('is_flipped', True) + return self + + def rotate(self, *args, **kwargs): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.rotate(*args, **kwargs) + self.add_field('rot', kwargs.get('rot', 0)) + return self + + def crop(self, *args, **kwargs): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(*args, **kwargs) + return self + + def resize(self, *args, **kwargs): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.resize(*args, **kwargs) + self.add_field(k, v) + return self + + def to_tensor(self, *args, **kwargs): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + self.add_field(k, v) + + def to(self, *args, **kwargs): + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + return self + +=== File: expose/data/targets/global_pose.py === + +-- Chunk 1 -- +// global_pose.py:31-101 +ss GlobalPose(GenericTarget): + + def __init__(self, global_pose, **kwargs): + super(GlobalPose, self).__init__() + self.global_pose = global_pose + + def to_tensor(self, to_rot=True, *args, **kwargs): + if not torch.is_tensor(self.global_pose): + self.global_pose = torch.from_numpy(self.global_pose) + + if to_rot: + self.global_pose = batch_rodrigues( + self.global_pose.view(-1, 3)).view(1, 3, 3) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def transpose(self, method): + + if method not in (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT and FLIP_TOP_BOTTOM implemented" + ) + + if torch.is_tensor(self.global_pose): + dim_flip = torch.tensor([1, -1, -1], dtype=self.global_pose.dtype) + global_pose = self.global_pose.clone().squeeze() * dim_flip + else: + dim_flip = np.array([1, -1, -1], dtype=self.global_pose.dtype) + global_pose = self.global_pose.copy().squeeze() * dim_flip + + field = type(self)(global_pose=global_pose) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + field.add_field(k, v) + self.add_field('is_flipped', True) + return field + + def rotate(self, rot=0, *args, **kwargs): + global_pose = self.global_pose.copy() + if rot != 0: + R = np.array([[np.cos(np.deg2rad(-rot)), + -np.sin(np.deg2rad(-rot)), 0], + [np.sin(np.deg2rad(-rot)), + np.cos(np.deg2rad(-rot)), 0], + [0, 0, 1]], dtype=np.float32) + + # find the rotation of the body in camera frame + per_rdg, _ = cv2.Rodrigues(global_pose) + # apply the global rotation to the global orientation + resrot, _ = cv2.Rodrigues(np.dot(R, per_rdg)) + global_pose = (resrot.T)[0].reshape(3) + field = type(self)(global_pose=global_pose) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(rot=rot, *args, **kwargs) + field.add_field(k, v) + + self.add_field('rot', rot) + return field + + def to(self, *args, **kwargs): + field = type(self)(global_pose=self.global_pose.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field + +=== File: expose/data/targets/image_list.py === + +-- Chunk 1 -- +// image_list.py:25-73 +ss ImageList(object): + def __init__(self, images: torch.Tensor, + img_sizes: List[torch.Size], + padding=None): + self.images = images + self.img_sizes = img_sizes + self.sizes_tensor = torch.stack( + [torch.tensor(s) if not torch.is_tensor(s) else s + for s in img_sizes]).to(dtype=self.images.dtype) + if padding is not None: + self.padding_tensor = torch.stack( + [torch.tensor(s) if not torch.is_tensor(s) else s + for s in padding]).to(dtype=self.images.dtype) + self._shape = self.images.shape + + def as_image_list(self) -> List[Tensor]: + return self.images + + def as_tensor(self) -> Tensor: + return self.images + + @property + def shape(self): + return self._shape + + @property + def device(self): + return self.images.device + + @property + def dtype(self): + return self.images.dtype + + def pin_memory(self): + if not self.images.is_pinned(): + self.images = self.images.pin_memory() + return self + + def __del__(self): + del self.images + del self.sizes_tensor + del self.img_sizes + + def to(self, *args, **kwargs): + images = self.images.to(*args, **kwargs) + sizes_tensor = self.sizes_tensor.to(*args, **kwargs) + return ImageList(images, sizes_tensor) + + + +-- Chunk 2 -- +// image_list.py:74-136 +ss ImageListPacked(object): + def __init__( + self, + packed_tensor: Tensor, + starts: List[int], + num_elements: List[int], + img_sizes: List[torch.Size], + ) -> None: + ''' + ''' + self.packed_tensor = packed_tensor + self.starts = starts + self.num_elements = num_elements + self.img_sizes = img_sizes + + self._shape = [len(starts)] + [max(s) for s in zip(*img_sizes)] + + _, self.heights, self.widths = zip(*img_sizes) + + def as_tensor(self): + return self.packed_tensor + + def as_image_list(self): + out_list = [] + + sizes = [shape[1:] for shape in self.img_sizes] + H, W = [max(s) for s in zip(*sizes)] + + out_shape = (3, H, W) + for ii in range(len(self.img_sizes)): + start = self.starts[ii] + end = self.starts[ii] + self.num_elements[ii] + c, h, w = self.img_sizes[ii] + img = self.packed_tensor[start:end].reshape(c, h, w) + out_img = torch.zeros( + out_shape, device=self.device, dtype=self.dtype) + out_img[:c, :h, :w] = img + out_list.append(out_img.detach().cpu().numpy()) + + return out_list + + @property + def shape(self): + return self._shape + + @property + def device(self): + return self.packed_tensor.device + + @property + def dtype(self): + return self.packed_tensor.dtype + + def pin_memory(self): + if not self.images.is_pinned(): + self.images = self.images.pin_memory() + return self + + def to(self, *args, **kwargs): + self.packed_tensor = self.packed_tensor.to(*args, **kwargs) + return self + + + +-- Chunk 3 -- +// image_list.py:137-163 + to_image_list_concat( + images: List[Tensor] +) -> ImageList: + if images is None: + return images + if isinstance(images, ImageList): + return images + sizes = [img.shape[1:] for img in images] + # logger.info(sizes) + H, W = [max(s) for s in zip(*sizes)] + + batch_size = len(images) + batched_shape = (batch_size, images[0].shape[0], H, W) + batched = torch.zeros( + batched_shape, device=images[0].device, dtype=images[0].dtype) + + # for img, padded in zip(images, batched): + # shape = img.shape + # padded[:shape[0], :shape[1], :shape[2]] = img + padding = None + for ii, img in enumerate(images): + shape = img.shape + batched[ii, :shape[0], :shape[1], :shape[2]] = img + + return ImageList(batched, sizes, padding=padding) + + + +-- Chunk 4 -- +// image_list.py:164-180 + to_image_list_packed(images: List[Tensor]) -> ImageListPacked: + if images is None: + return images + if isinstance(images, ImageListPacked): + return images + # Store the size of each image + # Compute the number of elements in each image + sizes = [img.shape for img in images] + num_element_list = [np.prod(s) for s in sizes] + # Compute the total number of elements + + packed = torch.cat([img.flatten() for img in images]) + # Compute the start index of each image tensor in the packed tensor + starts = [0] + list(np.cumsum(num_element_list))[:-1] + return ImageListPacked(packed, starts, num_element_list, sizes) + + + +-- Chunk 5 -- +// image_list.py:181-188 + to_image_list( + images: List[Tensor], + use_packed=False +) -> Union[ImageList, ImageListPacked]: + ''' + ''' + func = to_image_list_packed if use_packed else to_image_list_concat + return func(images) + +=== File: expose/data/targets/joints.py === + +-- Chunk 1 -- +// joints.py:24-55 +ss Joints(GenericTarget): + def __init__(self, joints, **kwargs): + super(Joints, self).__init__() + self.joints = joints + + def __repr__(self): + s = self.__class__.__name__ + return s + + def to_tensor(self, *args, **kwargs): + self.joints = torch.tensor(self.joints) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def __getitem__(self, key): + if key == 'joints': + return self.joints + else: + raise ValueError('Unknown key: {}'.format(key)) + + def __len__(self): + return 1 + + def to(self, *args, **kwargs): + joints = type(self)(self.joints.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + joints.add_field(k, v) + return joints + +=== File: expose/data/targets/betas.py === + +-- Chunk 1 -- +// betas.py:26-48 +ss Betas(GenericTarget): + """ Stores the shape params + """ + + def __init__(self, betas, dtype=torch.float32, **kwargs): + super(Betas, self).__init__() + + self.betas = betas + + def to_tensor(self, *args, **kwargs): + if not torch.is_tensor(self.betas): + self.betas = torch.from_numpy(self.betas) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def to(self, *args, **kwargs): + field = type(self)(betas=self.betas.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field + +=== File: expose/data/targets/expression.py === + +-- Chunk 1 -- +// expression.py:30-79 +ss Expression(GenericTarget): + """ Stores the expression params + """ + + def __init__(self, expression, dtype=torch.float32, **kwargs): + super(Expression, self).__init__() + self.expression = expression + + def to_tensor(self, *args, **kwargs): + if not torch.is_tensor(self.expression): + self.expression = torch.from_numpy(self.expression) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def transpose(self, method): + field = type(self)(expression=deepcopy(self.expression)) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + field.add_field(k, v) + self.add_field('is_flipped', True) + return field + + def resize(self, size, *args, **kwargs): + field = type(self)(expression=self.expression) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.resize(size, *args, **kwargs) + field.add_field(k, v) + return field + + def crop(self, rot=0, *args, **kwargs): + field = type(self)(expression=self.expression) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(rot=rot, *args, **kwargs) + field.add_field(k, v) + + self.add_field('rot', rot) + return field + + def to(self, *args, **kwargs): + field = type(self)(expression=self.expression.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field + +=== File: expose/data/targets/jaw_pose.py === + +-- Chunk 1 -- +// jaw_pose.py:46-90 +ss JawPose(GenericTarget): + """ Contains the jaw pose parameters + """ + + def __init__(self, jaw_pose, dtype=torch.float32, **kwargs): + super(JawPose, self).__init__() + self.jaw_pose = jaw_pose + + def to_tensor(self, to_rot=True, *args, **kwargs): + if not torch.is_tensor(self.jaw_pose): + self.jaw_pose = torch.from_numpy(self.jaw_pose) + + if to_rot: + self.jaw_pose = batch_rodrigues( + self.jaw_pose.view(-1, 3)).view(-1, 3, 3) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def transpose(self, method): + + if method not in (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT and FLIP_TOP_BOTTOM implemented" + ) + + dim_flip = np.array([1, -1, -1], dtype=self.jaw_pose.dtype) + jaw_pose = self.jaw_pose.copy() * dim_flip + + field = type(self)(jaw_pose=jaw_pose) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + field.add_field(k, v) + self.add_field('is_flipped', True) + return field + + def to(self, *args, **kwargs): + field = type(self)(jaw_pose=self.jaw_pose.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field + +=== File: expose/data/targets/hand_pose.py === + +-- Chunk 1 -- +// hand_pose.py:37-106 +ss HandPose(GenericTarget): + """ Contains the hand pose parameters + """ + + def __init__(self, left_hand_pose, right_hand_pose, **kwargs): + super(HandPose, self).__init__() + self.left_hand_pose = left_hand_pose + self.right_hand_pose = right_hand_pose + + def to_tensor(self, to_rot=True, *args, **kwargs): + if not torch.is_tensor(self.left_hand_pose): + if self.left_hand_pose is not None: + self.left_hand_pose = torch.from_numpy(self.left_hand_pose) + if not torch.is_tensor(self.right_hand_pose): + if self.right_hand_pose is not None: + self.right_hand_pose = torch.from_numpy( + self.right_hand_pose) + if to_rot: + if self.left_hand_pose is not None: + self.left_hand_pose = batch_rodrigues( + self.left_hand_pose.view(-1, 3)).view(-1, 3, 3) + if self.right_hand_pose is not None: + self.right_hand_pose = batch_rodrigues( + self.right_hand_pose.view(-1, 3)).view(-1, 3, 3) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def transpose(self, method): + + if method not in (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT and FLIP_TOP_BOTTOM implemented" + ) + + if torch.is_tensor(self.left_hand_pose): + dim_flip = torch.tensor( + [1, -1, -1], dtype=self.left_hand_pose.dtype) + else: + dim_flip = np.array([1, -1, -1], dtype=self.left_hand_pose.dtype) + + left_hand_pose = (self.right_hand_pose.reshape(15, 3) * + dim_flip).reshape(45) + right_hand_pose = (self.left_hand_pose.reshape(15, 3) * + dim_flip).reshape(45) + field = type(self)(left_hand_pose=left_hand_pose, + right_hand_pose=right_hand_pose) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + field.add_field(k, v) + self.add_field('is_flipped', True) + return field + + def to(self, *args, **kwargs): + left_hand_pose = self.left_hand_pose + right_hand_pose = self.right_hand_pose + if left_hand_pose is not None: + left_hand_pose = left_hand_pose.to(*args, **kwargs) + if right_hand_pose is not None: + right_hand_pose = right_hand_pose.to(*args, **kwargs) + field = type(self)( + left_hand_pose=left_hand_pose, right_hand_pose=right_hand_pose) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field + +=== File: expose/data/targets/bbox.py === + +-- Chunk 1 -- +// bbox.py:37-178 +ss BoundingBox(GenericTarget): + def __init__(self, bbox, size, flip_axis=0, transform=True, **kwargs): + super(BoundingBox, self).__init__() + self.bbox = bbox + self.flip_axis = flip_axis + self.size = size + self.transform = transform + + def __repr__(self): + msg = ', '.join(map(str, map(float, self.bbox))) + return f'Bounding box: {msg}' + + def to_tensor(self, *args, **kwargs): + if not torch.is_tensor(self.bbox): + self.bbox = torch.from_numpy(self.bbox) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def rotate(self, rot=0, *args, **kwargs): + (h, w) = self.size[:2] + (cX, cY) = (w // 2, h // 2) + M = cv2.getRotationMatrix2D((cX, cY), rot, 1.0) + cos = np.abs(M[0, 0]) + sin = np.abs(M[0, 1]) + # compute the new bounding dimensions of the image + nW = int((h * sin) + (w * cos)) + nH = int((h * cos) + (w * sin)) + # adjust the rotation matrix to take into account translation + M[0, 2] += (nW / 2) - cX + M[1, 2] += (nH / 2) - cY + + if self.transform: + bbox = self.bbox.copy().reshape(4) + xmin, ymin, xmax, ymax = bbox + points = np.array( + [[xmin, ymin], + [xmin, ymax], + [xmax, ymin], + [xmax, ymax]], + ) + + bbox = (np.dot(points, M[:2, :2].T) + M[:2, 2] + 1) + xmin, ymin = np.amin(bbox, axis=0) + xmax, ymax = np.amax(bbox, axis=0) + + new_bbox = np.array([xmin, ymin, xmax, ymax]) + else: + new_bbox = self.bbox.copy().reshape(4) + + bbox_target = type(self)( + new_bbox, size=(nH, nW, 3), transform=self.transform) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.rotate(rot=rot, *args, **kwargs) + bbox_target.add_field(k, v) + + return bbox_target + + def crop(self, center, scale, rot=0, crop_size=224, *args, **kwargs): + if self.transform: + bbox = self.bbox.copy().reshape(4) + xmin, ymin, xmax, ymax = bbox + points = np.array( + [[xmin, ymin], + [xmin, ymax], + [xmax, ymin], + [xmax, ymax]], + ) + transf = get_transform( + center, scale, (crop_size, crop_size), rot=rot) + + bbox = (np.dot(points, transf[:2, :2].T) + transf[:2, 2] + 1) + xmin, ymin = np.amin(bbox, axis=0) + xmax, ymax = np.amax(bbox, axis=0) + + new_bbox = np.array([xmin, ymin, xmax, ymax]) + else: + new_bbox = self.bbox.copy().reshape(4) + + bbox_target = type(self)(new_bbox, size=(crop_size, crop_size), + transform=self.transform) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(center=center, scale=scale, + crop_size=crop_size, rot=rot, + *args, **kwargs) + bbox_target.add_field(k, v) + + return bbox_target + + def resize(self, size, *args, **kwargs): + raise NotImplementedError + + def __len__(self): + return 1 + + def transpose(self, method): + if method not in (FLIP_LEFT_RIGHT,): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT implemented") + + xmin, xmax = self.bbox.reshape(-1)[[0, 2]] + # logger.info(f'Before: {xmin}, {xmax}') + W = self.size[1] + new_xmin = W - xmax + new_xmax = W - xmin + new_ymin, new_ymax = self.bbox[[1, 3]] + # logger.info(f'After: {xmin}, {xmax}') + + if torch.is_tensor(self.bbox): + flipped_bbox = torch.tensor( + [new_xmin, new_ymin, new_xmax, new_ymax], + dtype=self.bbox.dtype, device=self.bbox.device) + else: + flipped_bbox = np.array( + [new_xmin, new_ymin, new_xmax, new_ymax], + dtype=self.bbox.dtype) + + bbox_target = type(self)(flipped_bbox, self.size, + transform=self.transform) + # logger.info(bbox_target) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + bbox_target.add_field(k, v) + + bbox_target.add_field('is_flipped', True) + return bbox_target + + def to(self, *args, **kwargs): + bbox_tensor = self.bbox + if not torch.is_tensor(self.bbox): + bbox_tensor = torch.tensor(bbox_tensor) + bbox_target = type(self)(bbox_tensor.to(*args, **kwargs), self.size, + transform=self.transform) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + bbox_target.add_field(k, v) + return bbox_target + +=== File: expose/data/targets/__init__.py === + +-- Chunk 1 -- +// /app/repos/repo_8/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/repos/repo_0/expose/data/targets/__init__.py:1-32 +# -*- coding: utf-8 -*- + +# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is +# holder of all proprietary rights on this computer program. +# You can only use this computer program if you have closed +# a license agreement with MPG or you get the right to use the computer +# program from someone who is authorized to grant you that right. +# Any use of the computer program without a valid license is prohibited and +# liable to prosecution. +# +# Copyright©2020 Max-Planck-Gesellschaft zur Förderung +# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute +# for Intelligent Systems. All rights reserved. +# +# Contact: ps-license@tuebingen.mpg.de + + +from .generic_target import GenericTarget +from .keypoints import Keypoints2D, Keypoints3D + +from .betas import Betas +from .expression import Expression +from .global_pose import GlobalPose +from .body_pose import BodyPose +from .hand_pose import HandPose +from .jaw_pose import JawPose + +from .vertices import Vertices +from .joints import Joints +from .bbox import BoundingBox + +from .image_list import ImageList, ImageListPacked + +=== File: expose/data/targets/keypoints.py === + +-- Chunk 1 -- +// keypoints.py:34-183 +ss Keypoints2D(GenericTarget): + def __init__(self, keypoints, size, + flip_axis=0, + use_face_contour=False, + bbox=None, + center=None, + scale=1.0, + source='', + **kwargs): + super(Keypoints2D, self).__init__() + self.size = size + self.source = source + self.bbox = bbox + self.center = center + self.scale = scale + + self.flip_axis = flip_axis + + self.smplx_keypoints = keypoints[:, :-1] + self.conf = keypoints[:, -1] + + def __repr__(self): + s = self.__class__.__name__ + '(' + s += 'Number of keypoints={}, '.format(self.smplx_keypoints.shape[0]) + s += 'image_width={}, '.format(self.size[1]) + s += 'image_height={})'.format(self.size[0]) + return s + + def to_tensor(self, *args, **kwargs): + if not torch.is_tensor(self.smplx_keypoints): + self.smplx_keypoints = torch.from_numpy(self.smplx_keypoints) + self.conf = torch.from_numpy(self.conf) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def normalize(self, bboxes): + center = (bboxes[:, 2:] + bboxes[:, :2]) * 0.5 + bbox_width = bboxes[:, 2] - bboxes[:, 0] + bbox_height = bboxes[:, 3] - bboxes[:, 1] + + if center.shape[0] < 1: + return + if self.smplx_keypoints.shape[0] < 1: + return + self.smplx_keypoints[:, :, :2] -= center.unsqueeze(dim=1) + + self.smplx_keypoints[:, :, 0] = ( + self.smplx_keypoints[:, :, 0] / bbox_width[:, np.newaxis]) * 2 + self.smplx_keypoints[:, :, 1] = ( + self.smplx_keypoints[:, :, 1] / bbox_height[:, np.newaxis]) * 2 + + def rotate(self, rot=0, *args, **kwargs): + (h, w) = self.size[:2] + (cX, cY) = (w // 2, h // 2) + M = cv2.getRotationMatrix2D((cX, cY), rot, 1.0) + cos = np.abs(M[0, 0]) + sin = np.abs(M[0, 1]) + # compute the new bounding dimensions of the image + nW = int((h * sin) + (w * cos)) + nH = int((h * cos) + (w * sin)) + + # adjust the rotation matrix to take into account translation + M[0, 2] += (nW / 2) - cX + M[1, 2] += (nH / 2) - cY + kp = self.smplx_keypoints.copy() + kp = (np.dot(kp, M[:2, :2].T) + M[:2, 2] + 1).astype(np.int) + + conf = self.conf.copy().reshape(-1, 1) + kp = np.concatenate([kp, conf], axis=1).astype(np.float32) + keypoints = type(self)(kp, size=(nH, nW, 3)) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.rotate(rot=rot, *args, **kwargs) + keypoints.add_field(k, v) + + self.add_field('rot', rot) + return keypoints + + def crop(self, center, scale, crop_size=224, *args, **kwargs): + kp = self.smplx_keypoints.copy() + transf = get_transform(center, scale, (crop_size, crop_size)) + kp = (np.dot(kp, transf[:2, :2].T) + transf[:2, 2] + 1).astype(np.int) + + kp = 2.0 * kp / crop_size - 1.0 + + conf = self.conf.copy().reshape(-1, 1) + kp = np.concatenate([kp, conf], axis=1).astype(np.float32) + keypoints = type(self)(kp, size=(crop_size, crop_size, 3)) + keypoints.source = self.source + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(center=center, scale=scale, + crop_size=crop_size, *args, **kwargs) + keypoints.add_field(k, v) + + return keypoints + + def get_keypoints_and_conf(self, key='all'): + if key == 'all': + keyp_data = [self.smplx_keypoints, self.conf] + elif key == 'body': + keyp_data = [self.smplx_keypoints[BODY_IDXS], + self.conf[BODY_IDXS]] + elif key == 'left_hand': + keyp_data = [self.smplx_keypoints[LEFT_HAND_IDXS], + self.conf[LEFT_HAND_IDXS]] + elif key == 'right_hand': + keyp_data = [self.smplx_keypoints[RIGHT_HAND_IDXS], + self.conf[RIGHT_HAND_IDXS]] + elif key == 'head': + keyp_data = [self.smplx_keypoints[HEAD_IDXS], + self.conf[HEAD_IDXS]] + else: + raise ValueError(f'Unknown key: {key}') + if torch.is_tensor(keyp_data[0]): + return torch.cat( + [keyp_data[0], keyp_data[1][..., None]], dim=-1) + else: + return np.concatenate( + [keyp_data[0], keyp_data[1][..., None]], axis=-1) + + def resize(self, size, *args, **kwargs): + ratios = tuple(float(s) / float(s_orig) + for s, s_orig in zip(size, self.size)) + ratio_w, ratio_h = ratios + resized_data = self.smplx_keypoints.copy() + + resized_data[..., 0] *= ratio_w + resized_data[..., 1] *= ratio_h + + resized_keyps = np.concatenate([resized_data, + self.conf.unsqueeze(dim=-1)], axis=-1) + + keypoints = type(self)(resized_keyps, size=size) + keypoints.source = self.source + # bbox._copy_extra_fields(self) + for k, v in self.extra_fields.items(): + if not isinstance(v, torch.Tensor): + v = v.resize(size, *args, **kwargs) + keypoints.add_field(k, v) + + return keypoints + + def __getitem__(self, key): + if key == 'keypoints': + return self.smplx_keypoints + elif key == 'conf': + return self.conf + +-- Chunk 2 -- +// keypoints.py:184-256 + else: + raise ValueError('Unknown key: {}'.format(key)) + + def __len__(self): + return 1 + + def transpose(self, method): + if method not in (FLIP_LEFT_RIGHT,): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT implemented") + + width = self.size[1] + TO_REMOVE = 1 + flip_inds = type(self).FLIP_INDS + if torch.is_tensor(self.smplx_keypoints): + flipped_data = torch.cat([self.smplx_keypoints, + self.conf.unsqueeze(dim=-1)], + dim=-1) + + num_joints = flipped_data.shape[0] + # flipped_data[torch.arange(num_joints)] = torch.index_select( + # flipped_data, 0, flip_inds[:num_joints]) + flipped_data[np.arange(num_joints)] = flipped_data[ + flip_inds[:num_joints]] + # width = self.size[0] + # TO_REMOVE = 1 + # Flip x coordinates + # flipped_data[..., 0] = width - flipped_data[..., 0] - TO_REMOVE + flipped_data[..., :, self.flip_axis] = width - flipped_data[ + ..., :, self.flip_axis] - TO_REMOVE + + # Maintain COCO convention that if visibility == 0, then x, y = 0 + # inds = flipped_data[..., 2] == 0 + # flipped_data[inds] = 0 + else: + flipped_data = np.concatenate( + [self.smplx_keypoints, self.conf[..., np.newaxis]], axis=-1) + + num_joints = flipped_data.shape[0] + flipped_data[np.arange(num_joints)] = flipped_data[ + flip_inds[:num_joints]] + # Flip x coordinates + flipped_data[..., 0] = width - flipped_data[..., 0] - TO_REMOVE + + # Maintain COCO convention that if visibility == 0, then x, y = 0 + # inds = flipped_data[..., 2] == 0 + # flipped_data[inds] = 0 + + keypoints = type(self)(flipped_data, self.size) + keypoints.source = self.source + if self.bbox is not None: + keypoints.bbox = self.bbox.copy() + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + keypoints.add_field(k, v) + + self.add_field('is_flipped', True) + return keypoints + + def to(self, *args, **kwargs): + keyp_tensor = torch.cat([self.smplx_keypoints, + self.conf.unsqueeze(dim=-1)], dim=-1) + keypoints = type(self)(keyp_tensor.to(*args, **kwargs), self.size) + keypoints.source = self.source + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + keypoints.add_field(k, v) + return keypoints + + + +-- Chunk 3 -- +// keypoints.py:575-619 + get_part_idxs(): + body_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'body' in val]) + hand_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'hand' in val]) + + left_hand_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'hand' in val and 'left' in KEYPOINT_NAMES[idx]]) + + right_hand_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'hand' in val and 'right' in KEYPOINT_NAMES[idx]]) + + face_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'face' in val]) + head_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'head' in val]) + flame_idxs = np.asarray([ + idx + for idx, val in enumerate(KEYPOINT_PARTS.values()) + if 'flame' in val]) + # joint_weights[hand_idxs] = hand_weight + # joint_weights[face_idxs] = face_weight + return { + 'body': body_idxs.astype(np.int64), + 'hand': hand_idxs.astype(np.int64), + 'face': face_idxs.astype(np.int64), + 'head': head_idxs.astype(np.int64), + 'left_hand': left_hand_idxs.astype(np.int64), + 'right_hand': right_hand_idxs.astype(np.int64), + 'flame': flame_idxs.astype(np.int64), + } + + + +-- Chunk 4 -- +// keypoints.py:799-948 + kp_connections(keypoints): + kp_lines = [ + [keypoints.index('left_eye'), keypoints.index('nose')], + [keypoints.index('right_eye'), keypoints.index('nose')], + [keypoints.index('right_eye'), keypoints.index('right_ear')], + [keypoints.index('left_eye'), keypoints.index('left_ear')], + [keypoints.index('right_shoulder'), keypoints.index('right_elbow')], + [keypoints.index('right_elbow'), keypoints.index('right_wrist')], + # Right Thumb + [keypoints.index('right_wrist'), keypoints.index('right_thumb1')], + [keypoints.index('right_thumb1'), keypoints.index('right_thumb2')], + [keypoints.index('right_thumb2'), keypoints.index('right_thumb3')], + [keypoints.index('right_thumb3'), keypoints.index('right_thumb')], + # Right Index + [keypoints.index('right_wrist'), keypoints.index('right_index1')], + [keypoints.index('right_index1'), keypoints.index('right_index2')], + [keypoints.index('right_index2'), keypoints.index('right_index3')], + [keypoints.index('right_index3'), keypoints.index('right_index')], + # Right Middle + [keypoints.index('right_wrist'), keypoints.index('right_middle1')], + [keypoints.index('right_middle1'), keypoints.index('right_middle2')], + [keypoints.index('right_middle2'), keypoints.index('right_middle3')], + [keypoints.index('right_middle3'), keypoints.index('right_middle')], + # Right Ring + [keypoints.index('right_wrist'), keypoints.index('right_ring1')], + [keypoints.index('right_ring1'), keypoints.index('right_ring2')], + [keypoints.index('right_ring2'), keypoints.index('right_ring3')], + [keypoints.index('right_ring3'), keypoints.index('right_ring')], + # Right Pinky + [keypoints.index('right_wrist'), keypoints.index('right_pinky1')], + [keypoints.index('right_pinky1'), keypoints.index('right_pinky2')], + [keypoints.index('right_pinky2'), keypoints.index('right_pinky3')], + [keypoints.index('right_pinky3'), keypoints.index('right_pinky')], + # Left Hand + [keypoints.index('left_shoulder'), keypoints.index('left_elbow')], + [keypoints.index('left_elbow'), keypoints.index('left_wrist')], + # Left Thumb + [keypoints.index('left_wrist'), keypoints.index('left_thumb1')], + [keypoints.index('left_thumb1'), keypoints.index('left_thumb2')], + [keypoints.index('left_thumb2'), keypoints.index('left_thumb3')], + [keypoints.index('left_thumb3'), keypoints.index('left_thumb')], + # Left Index + [keypoints.index('left_wrist'), keypoints.index('left_index1')], + [keypoints.index('left_index1'), keypoints.index('left_index2')], + [keypoints.index('left_index2'), keypoints.index('left_index3')], + [keypoints.index('left_index3'), keypoints.index('left_index')], + # Left Middle + [keypoints.index('left_wrist'), keypoints.index('left_middle1')], + [keypoints.index('left_middle1'), keypoints.index('left_middle2')], + [keypoints.index('left_middle2'), keypoints.index('left_middle3')], + [keypoints.index('left_middle3'), keypoints.index('left_middle')], + # Left Ring + [keypoints.index('left_wrist'), keypoints.index('left_ring1')], + [keypoints.index('left_ring1'), keypoints.index('left_ring2')], + [keypoints.index('left_ring2'), keypoints.index('left_ring3')], + [keypoints.index('left_ring3'), keypoints.index('left_ring')], + # Left Pinky + [keypoints.index('left_wrist'), keypoints.index('left_pinky1')], + [keypoints.index('left_pinky1'), keypoints.index('left_pinky2')], + [keypoints.index('left_pinky2'), keypoints.index('left_pinky3')], + [keypoints.index('left_pinky3'), keypoints.index('left_pinky')], + + # Right Foot + [keypoints.index('right_hip'), keypoints.index('right_knee')], + [keypoints.index('right_knee'), keypoints.index('right_ankle')], + [keypoints.index('right_ankle'), keypoints.index('right_heel')], + [keypoints.index('right_ankle'), keypoints.index('right_big_toe')], + [keypoints.index('right_ankle'), keypoints.index('right_small_toe')], + + [keypoints.index('left_hip'), keypoints.index('left_knee')], + [keypoints.index('left_knee'), keypoints.index('left_ankle')], + [keypoints.index('left_ankle'), keypoints.index('left_heel')], + [keypoints.index('left_ankle'), keypoints.index('left_big_toe')], + [keypoints.index('left_ankle'), keypoints.index('left_small_toe')], + + [keypoints.index('neck'), keypoints.index('right_shoulder')], + [keypoints.index('neck'), keypoints.index('left_shoulder')], + [keypoints.index('neck'), keypoints.index('nose')], + [keypoints.index('pelvis'), keypoints.index('neck')], + [keypoints.index('pelvis'), keypoints.index('left_hip')], + [keypoints.index('pelvis'), keypoints.index('right_hip')], + + # Left Eye brow + [keypoints.index('left_eye_brow1'), keypoints.index('left_eye_brow2')], + [keypoints.index('left_eye_brow2'), keypoints.index('left_eye_brow3')], + [keypoints.index('left_eye_brow3'), keypoints.index('left_eye_brow4')], + [keypoints.index('left_eye_brow4'), keypoints.index('left_eye_brow5')], + + # Right Eye brow + [keypoints.index('right_eye_brow1'), + keypoints.index('right_eye_brow2')], + [keypoints.index('right_eye_brow2'), + keypoints.index('right_eye_brow3')], + [keypoints.index('right_eye_brow3'), + keypoints.index('right_eye_brow4')], + [keypoints.index('right_eye_brow4'), + keypoints.index('right_eye_brow5')], + + # Left Eye + [keypoints.index('left_eye1'), keypoints.index('left_eye2')], + [keypoints.index('left_eye2'), keypoints.index('left_eye3')], + [keypoints.index('left_eye3'), keypoints.index('left_eye4')], + [keypoints.index('left_eye4'), keypoints.index('left_eye5')], + [keypoints.index('left_eye5'), keypoints.index('left_eye6')], + [keypoints.index('left_eye6'), keypoints.index('left_eye1')], + + # Right Eye + [keypoints.index('right_eye1'), keypoints.index('right_eye2')], + [keypoints.index('right_eye2'), keypoints.index('right_eye3')], + [keypoints.index('right_eye3'), keypoints.index('right_eye4')], + [keypoints.index('right_eye4'), keypoints.index('right_eye5')], + [keypoints.index('right_eye5'), keypoints.index('right_eye6')], + [keypoints.index('right_eye6'), keypoints.index('right_eye1')], + + # Nose Vertical + [keypoints.index('nose1'), keypoints.index('nose2')], + [keypoints.index('nose2'), keypoints.index('nose3')], + [keypoints.index('nose3'), keypoints.index('nose4')], + + # Nose Horizontal + [keypoints.index('nose_middle'), keypoints.index('nose4')], + [keypoints.index('left_nose_1'), keypoints.index('left_nose_2')], + [keypoints.index('left_nose_1'), keypoints.index('nose_middle')], + [keypoints.index('nose_middle'), keypoints.index('right_nose_1')], + [keypoints.index('right_nose_2'), keypoints.index('right_nose_1')], + + # Mouth + [keypoints.index('left_mouth_1'), keypoints.index('left_mouth_2')], + [keypoints.index('left_mouth_2'), keypoints.index('left_mouth_3')], + [keypoints.index('left_mouth_3'), keypoints.index('mouth_top')], + [keypoints.index('mouth_top'), keypoints.index('right_mouth_3')], + [keypoints.index('right_mouth_3'), keypoints.index('right_mouth_2')], + [keypoints.index('right_mouth_2'), keypoints.index('right_mouth_1')], + [keypoints.index('right_mouth_1'), keypoints.index('right_mouth_5')], + [keypoints.index('right_mouth_5'), keypoints.index('right_mouth_4')], + [keypoints.index('right_mouth_4'), keypoints.index('mouth_bottom')], + [keypoints.index('mouth_bottom'), keypoints.index('left_mouth_4')], + [keypoints.index('left_mouth_4'), keypoints.index('left_mouth_5')], + [keypoints.index('left_mouth_5'), keypoints.index('left_mouth_1')], + + # Lips + [keypoints.index('left_lip_1'), keypoints.index('left_lip_2')], + [keypoints.index('left_lip_2'), keypoints.index('lip_top')], + [keypoints.index('lip_top'), keypoints.index('right_lip_2')], + [keypoints.index('right_lip_2'), keypoints.index('right_lip_1')], + [keypoints.index('right_lip_1'), keypoints.index('right_lip_3')], + [keypoints.index('right_lip_3'), keypoints.index('lip_bottom')], + [keypoints.index('lip_bottom'), keypoints.index('left_lip_3')], + [keypoints.index('left_lip_3'), keypoints.index('left_lip_1')], + + +-- Chunk 5 -- +// keypoints.py:949-978 + # Contour + [keypoints.index('left_contour_1'), keypoints.index('left_contour_2')], + [keypoints.index('left_contour_2'), keypoints.index('left_contour_3')], + [keypoints.index('left_contour_3'), keypoints.index('left_contour_4')], + [keypoints.index('left_contour_4'), keypoints.index('left_contour_5')], + [keypoints.index('left_contour_5'), keypoints.index('left_contour_6')], + [keypoints.index('left_contour_6'), keypoints.index('left_contour_7')], + [keypoints.index('left_contour_7'), keypoints.index('left_contour_8')], + [keypoints.index('left_contour_8'), keypoints.index('contour_middle')], + + [keypoints.index('contour_middle'), + keypoints.index('right_contour_8')], + [keypoints.index('right_contour_8'), + keypoints.index('right_contour_7')], + [keypoints.index('right_contour_7'), + keypoints.index('right_contour_6')], + [keypoints.index('right_contour_6'), + keypoints.index('right_contour_5')], + [keypoints.index('right_contour_5'), + keypoints.index('right_contour_4')], + [keypoints.index('right_contour_4'), + keypoints.index('right_contour_3')], + [keypoints.index('right_contour_3'), + keypoints.index('right_contour_2')], + [keypoints.index('right_contour_2'), + keypoints.index('right_contour_1')], + ] + return kp_lines + + + +-- Chunk 6 -- +// keypoints.py:987-995 + _create_flip_indices(names, flip_map): + full_flip_map = flip_map.copy() + full_flip_map.update({v: k for k, v in flip_map.items()}) + flipped_names = [i if i not in full_flip_map else full_flip_map[i] + for i in names] + flip_indices = [names.index(i) for i in flipped_names] + return torch.tensor(flip_indices) + + + +-- Chunk 7 -- +// keypoints.py:1038-1124 +ss Keypoints3D(Keypoints2D): + def __init__(self, *args, **kwargs): + super(Keypoints3D, self).__init__(*args, **kwargs) + + def rotate(self, rot=0, *args, **kwargs): + kp = self.smplx_keypoints.copy() + conf = self.conf.copy().reshape(-1, 1) + + if rot != 0: + R = np.array([[np.cos(np.deg2rad(-rot)), + -np.sin(np.deg2rad(-rot)), 0], + [np.sin(np.deg2rad(-rot)), + np.cos(np.deg2rad(-rot)), 0], + [0, 0, 1]], dtype=np.float32) + kp = np.dot(kp, R.T) + + kp = np.concatenate([kp, conf], axis=1).astype(np.float32) + + keypoints = type(self)(kp, size=self.size) + for k, v in self.extra_fields.items(): + if not isinstance(v, torch.Tensor): + v = v.rotate(rot=rot, *args, **kwargs) + keypoints.add_field(k, v) + self.add_field('rot', kwargs.get('rot', 0)) + return keypoints + + def crop(self, center, scale, crop_size=224, *args, **kwargs): + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(center=center, scale=scale, + crop_size=crop_size, *args, **kwargs) + return self + + def center_by_keyp(self, keyp_name='pelvis'): + keyp_idx = KEYPOINT_NAMES.index(keyp_name) + self.smplx_keypoints -= self.smplx_keypoints[[keyp_idx]] + + def transpose(self, method): + if method not in (FLIP_LEFT_RIGHT,): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT implemented") + + flip_inds = type(self).FLIP_INDS + if torch.is_tensor(self.smplx_keypoints): + flipped_data = torch.cat([self.smplx_keypoints, + self.conf.unsqueeze(dim=-1)], + dim=-1) + + num_joints = flipped_data.shape[0] + # flipped_data[torch.arange(num_joints)] = torch.index_select( + # flipped_data, 0, flip_inds[:num_joints]) + flipped_data[np.arange(num_joints)] = flipped_data[ + flip_inds[:num_joints]] + # width = self.size[0] + # TO_REMOVE = 1 + # Flip x coordinates + # flipped_data[..., 0] = width - flipped_data[..., 0] - TO_REMOVE + flipped_data[..., :, self.flip_axis] *= (-1) + + # Maintain COCO convention that if visibility == 0, then x, y = 0 + # inds = flipped_data[..., 2] == 0 + # flipped_data[inds] = 0 + else: + flipped_data = np.concatenate([self.smplx_keypoints, + self.conf[..., np.newaxis]], axis=-1) + + num_joints = flipped_data.shape[0] + # flipped_data[torch.arange(num_joints)] = torch.index_select( + # flipped_data, 0, flip_inds[:num_joints]) + flipped_data[np.arange(num_joints)] = flipped_data[ + flip_inds[:num_joints]] + # width = self.size[0] + # TO_REMOVE = 1 + # Flip x coordinates + # flipped_data[..., 0] = width - flipped_data[..., 0] - TO_REMOVE + flipped_data[..., :, self.flip_axis] *= (-1) + + keypoints = type(self)(flipped_data, self.size) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + keypoints.add_field(k, v) + self.add_field('is_flipped', True) + + return keypoints + + + +-- Chunk 8 -- +// keypoints.py:1597-1638 + body_model_to_dset(model_type='smplx', dset='coco', joints_to_ign=None, + use_face_contour=False, **kwargs): + if joints_to_ign is None: + joints_to_ign = [] + + mapping = {} + if model_type == 'smplx': + keypoint_names = KEYPOINT_NAMES + elif model_type == 'mano': + keypoint_names = MANO_NAMES + + if dset == 'coco': + dset_keyp_names = COCO_KEYPOINTS + elif dset == 'openpose19': + dset_keyp_names = OPENPOSE_JOINTS[:19] + elif dset == 'openpose19+hands': + dset_keyp_names = OPENPOSE_JOINTS[19:19 + 2 * 21] + elif dset == 'openpose19+hands+face': + dset_keyp_names = OPENPOSE_JOINTS + elif dset == 'openpose25': + dset_keyp_names = OPENPOSE_JOINTS25[:25] + elif dset == 'openpose25+hands': + dset_keyp_names = OPENPOSE_JOINTS25[:25 + 2 * 21] + elif dset == 'openpose25+hands+face': + dset_keyp_names = OPENPOSE_JOINTS25 + elif dset == 'freihand': + dset_keyp_names = FREIHAND_NAMES + else: + raise ValueError('Unknown dset dataset: {}'.format(dset)) + + for idx, name in enumerate(dset_keyp_names): + if 'contour' in name and not use_face_contour: + continue + if name in keypoint_names: + mapping[idx] = keypoint_names.index(name) + + dset_keyp_idxs = np.array(list(mapping.keys()), dtype=np.long) + model_keyps_idxs = np.array(list(mapping.values()), dtype=np.long) + + return dset_keyp_idxs, model_keyps_idxs + + + +-- Chunk 9 -- +// keypoints.py:1639-1702 + dset_to_body_model(model_type='smplx', dset='coco', joints_to_ign=None, + use_face_contour=False, **kwargs): + if joints_to_ign is None: + joints_to_ign = [] + + mapping = {} + + if dset == 'coco': + dset_keyp_names = COCO_KEYPOINTS + elif dset == 'openpose19': + dset_keyp_names = OPENPOSE_JOINTS[:19] + elif dset == 'openpose19+hands': + dset_keyp_names = OPENPOSE_JOINTS[19:19 + 2 * 21] + elif dset == 'openpose19+hands': + dset_keyp_names = OPENPOSE_JOINTS[19:19 + 2 * 21] + elif dset == 'openpose25': + dset_keyp_names = OPENPOSE_JOINTS25[:25] + elif dset == 'openpose25+hands': + dset_keyp_names = OPENPOSE_JOINTS25[:25 + 2 * 21] + elif dset == 'openpose25+hands+face': + dset_keyp_names = OPENPOSE_JOINTS25 + elif dset == 'posetrack': + dset_keyp_names = POSETRACK_KEYPOINT_NAMES + elif dset == 'mpii': + dset_keyp_names = MPII_JOINTS + elif dset == 'left-mpii-hands': + dset_keyp_names = MPII_JOINTS[-2 * 21:-21] + elif dset == 'right-mpii-hands': + dset_keyp_names = MPII_JOINTS[-21:] + elif dset == 'aich': + dset_keyp_names = AICH_KEYPOINT_NAMES + elif dset == 'spin': + dset_keyp_names = SPIN_KEYPOINT_NAMES + elif dset == 'spinx': + dset_keyp_names = SPINX_KEYPOINT_NAMES + elif dset == 'panoptic': + dset_keyp_names = PANOPTIC_KEYPOINT_NAMES + elif dset == 'mano': + dset_keyp_names = MANO_NAMES + elif dset == '3dpw': + dset_keyp_names = THREEDPW_JOINTS + elif dset == 'freihand': + dset_keyp_names = FREIHAND_NAMES + elif dset == 'h36m': + dset_keyp_names = H36M_NAMES + elif dset == 'raw_h36m': + dset_keyp_names = RAW_H36M_NAMES + elif dset == 'ffhq': + dset_keyp_names = FFHQ_KEYPOINTS + elif dset == 'lsp': + dset_keyp_names = LSP_NAMES + else: + raise ValueError('Unknown dset dataset: {}'.format(dset)) + + for idx, name in enumerate(KEYPOINT_NAMES): + if 'contour' in name and not use_face_contour: + continue + if name in dset_keyp_names: + mapping[idx] = dset_keyp_names.index(name) + + model_keyps_idxs = np.array(list(mapping.keys()), dtype=np.long) + dset_keyps_idxs = np.array(list(mapping.values()), dtype=np.long) + + return dset_keyps_idxs, model_keyps_idxs + +=== File: expose/data/targets/vertices.py === + +-- Chunk 1 -- +// vertices.py:30-130 +ss Vertices(GenericTarget): + def __init__(self, vertices, + bc=None, + closest_faces=None, + flip=True, + flip_index=0, dtype=torch.float32): + super(Vertices, self).__init__() + self.vertices = vertices + self.flip_index = flip_index + self.closest_faces = closest_faces + self.bc = bc + self.flip = flip + + def __getitem__(self, key): + if key == 'vertices': + return self.vertices + else: + raise ValueError('Unknown key: {}'.format(key)) + + def transpose(self, method): + if not self.flip: + return self + if method not in (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT and FLIP_TOP_BOTTOM implemented" + ) + + if self.closest_faces is None or self.bc is None: + raise RuntimeError(f'Cannot support flip without correspondences') + + flipped_vertices = self.vertices.copy() + flipped_vertices[:, self.flip_index] *= -1 + + closest_tri_vertices = flipped_vertices[self.closest_faces].copy() + # flipped_vertices = flipped_vertices[ + # self.flip_correspondences].copy() + flipped_vertices = ( + self.bc[:, :, np.newaxis] * closest_tri_vertices).sum(axis=1) + flipped_vertices = flipped_vertices.astype(self.vertices.dtype) + + vertices = type(self)(flipped_vertices, flip_index=self.flip_index, + bc=self.bc, closest_faces=self.closest_faces) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + vertices.add_field(k, v) + self.add_field('is_flipped', True) + return vertices + + def to_tensor(self, *args, **kwargs): + self.vertices = torch.from_numpy(self.vertices) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def crop(self, *args, **kwargs): + vertices = self.vertices.copy() + field = type(self)(vertices, flip_index=self.flip_index, + bc=self.bc, + closest_faces=self.closest_faces) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(*args, **kwargs) + field.add_field(k, v) + + self.add_field('rot', kwargs.get('rot', 0)) + return field + + def rotate(self, rot=0, *args, **kwargs): + if rot == 0: + return self + vertices = self.vertices.copy() + R = np.array([[np.cos(np.deg2rad(-rot)), + -np.sin(np.deg2rad(-rot)), 0], + [np.sin(np.deg2rad(-rot)), + np.cos(np.deg2rad(-rot)), 0], + [0, 0, 1]], dtype=np.float32) + vertices = np.dot(vertices, R.T) + + vertices = type(self)(vertices, flip_index=self.flip_index, + bc=self.bc, closest_faces=self.closest_faces) + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.rotate(rot=rot, *args, **kwargs) + vertices.add_field(k, v) + + self.add_field('rot', rot) + return vertices + + def to(self, *args, **kwargs): + vertices = type(self)( + self.vertices.to(*args, **kwargs), flip_index=self.flip_index, + bc=self.bc, + closest_faces=self.closest_faces) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + vertices.add_field(k, v) + return vertices + +=== File: expose/data/targets/body_pose.py === + +-- Chunk 1 -- +// body_pose.py:45-102 +ss BodyPose(GenericTarget): + """ Stores the SMPL-HF params for all persons in an image + """ + + def __init__(self, body_pose, **kwargs): + super(BodyPose, self).__init__() + self.body_pose = body_pose + + def to_tensor(self, to_rot=True, *args, **kwargs): + self.body_pose = torch.from_numpy(self.body_pose) + + if to_rot: + self.body_pose = batch_rodrigues( + self.body_pose.view(-1, 3)).view(-1, 3, 3) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v.to_tensor(*args, **kwargs) + + def transpose(self, method): + if method not in (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM): + raise NotImplementedError( + "Only FLIP_LEFT_RIGHT and FLIP_TOP_BOTTOM implemented" + ) + + if torch.is_tensor(self.body_pose): + dim_flip = torch.tensor([1, -1, -1], dtype=self.body_pose.dtype) + else: + dim_flip = np.array([1, -1, -1], dtype=self.body_pose.dtype) + + body_pose = (self.body_pose.reshape(-1)[SIGN_FLIP].reshape(21, 3) * + dim_flip).reshape(21 * 3).copy() + field = type(self)(body_pose=body_pose) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.transpose(method) + field.add_field(k, v) + self.add_field('is_flipped', True) + return field + + def crop(self, rot=0, *args, **kwargs): + field = type(self)(body_pose=self.body_pose) + + for k, v in self.extra_fields.items(): + if isinstance(v, GenericTarget): + v = v.crop(rot=rot, *args, **kwargs) + field.add_field(k, v) + self.add_field('rot', rot) + return field + + def to(self, *args, **kwargs): + field = type(self)(body_pose=self.body_pose.to(*args, **kwargs)) + for k, v in self.extra_fields.items(): + if hasattr(v, "to"): + v = v.to(*args, **kwargs) + field.add_field(k, v) + return field diff --git a/.kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/data_level0.bin b/.kno/embedding_SBERTEmbedding_1746865869449_9651d09/3ef38626-2253-4113-a4c5-d60eeb8cc88e/data_level0.bin new file mode 100644 index 0000000000000000000000000000000000000000..092c7ff7d98d746bf8b9c00e3ff11a08638584c1 GIT binary patch literal 3212000 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
`2 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

;qiB(GO9(eTArMNwVnakZq#TDu+aewhGRZ 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