This commit is contained in:
logtd
2024-08-28 14:44:03 -06:00
parent 04c5f1b0c9
commit 12645eb974
84 changed files with 8447 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
**/__pycache__
.DS_Store
+8
View File
@@ -0,0 +1,8 @@
Linux
```
apt-get install freeglut3-dev
apt-get install libosmesa6-dev
```
+29
View File
@@ -0,0 +1,29 @@
import os
REPO_PATH = os.path.dirname(os.path.abspath(__file__))
from folder_paths import models_dir
SMPL_PATH = os.path.join(models_dir, 'smpl')
os.makedirs(SMPL_PATH, exist_ok=True)
from .nodes.process_humans_node import ProcessHumansNode
from .nodes.load_detectron_node import LoadDetectronNode
from .nodes.load_hmr_node import LoadHMRNode
# from .nodes.select_human_node import SelectHumanNode
NODE_CLASS_MAPPINGS = {
'ProcessHumans': ProcessHumansNode,
'LoadDetectron': LoadDetectronNode,
'LoadHMR': LoadHMRNode,
# 'SelectHuman': SelectHumanNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
'ProcessHumans': 'Process 4D Humans',
'LoadDetectron': 'Load Detectron Model',
'LoadHMR': 'Load HMR Model',
# 'SelectHuman': 'Select 4D Human'
}
+129
View File
@@ -0,0 +1,129 @@
## coco_loader_lsj.py
import detectron2.data.transforms as T
from detectron2 import model_zoo
from detectron2.config import LazyCall as L
# Data using LSJ
image_size = 1024
dataloader = model_zoo.get_config("common/data/coco.py").dataloader
dataloader.train.mapper.augmentations = [
L(T.RandomFlip)(horizontal=True), # flip first
L(T.ResizeScale)(
min_scale=0.1, max_scale=2.0, target_height=image_size, target_width=image_size
),
L(T.FixedSizeCrop)(crop_size=(image_size, image_size), pad=False),
]
dataloader.train.mapper.image_format = "RGB"
dataloader.train.total_batch_size = 64
# recompute boxes due to cropping
dataloader.train.mapper.recompute_boxes = True
dataloader.test.mapper.augmentations = [
L(T.ResizeShortestEdge)(short_edge_length=image_size, max_size=image_size),
]
from functools import partial
from fvcore.common.param_scheduler import MultiStepParamScheduler
from detectron2 import model_zoo
from detectron2.config import LazyCall as L
from detectron2.solver import WarmupParamScheduler
from detectron2.modeling.backbone.vit import get_vit_lr_decay_rate
# mask_rcnn_vitdet_b_100ep.py
model = model_zoo.get_config("common/models/mask_rcnn_vitdet.py").model
# Initialization and trainer settings
train = model_zoo.get_config("common/train.py").train
train.amp.enabled = True
train.ddp.fp16_compression = True
train.init_checkpoint = "detectron2://ImageNetPretrained/MAE/mae_pretrain_vit_base.pth"
# Schedule
# 100 ep = 184375 iters * 64 images/iter / 118000 images/ep
train.max_iter = 184375
lr_multiplier = L(WarmupParamScheduler)(
scheduler=L(MultiStepParamScheduler)(
values=[1.0, 0.1, 0.01],
milestones=[163889, 177546],
num_updates=train.max_iter,
),
warmup_length=250 / train.max_iter,
warmup_factor=0.001,
)
# Optimizer
optimizer = model_zoo.get_config("common/optim.py").AdamW
optimizer.params.lr_factor_func = partial(get_vit_lr_decay_rate, num_layers=12, lr_decay_rate=0.7)
optimizer.params.overrides = {"pos_embed": {"weight_decay": 0.0}}
# cascade_mask_rcnn_vitdet_b_100ep.py
from detectron2.config import LazyCall as L
from detectron2.layers import ShapeSpec
from detectron2.modeling.box_regression import Box2BoxTransform
from detectron2.modeling.matcher import Matcher
from detectron2.modeling.roi_heads import (
FastRCNNOutputLayers,
FastRCNNConvFCHead,
CascadeROIHeads,
)
# arguments that don't exist for Cascade R-CNN
[model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]]
model.roi_heads.update(
_target_=CascadeROIHeads,
box_heads=[
L(FastRCNNConvFCHead)(
input_shape=ShapeSpec(channels=256, height=7, width=7),
conv_dims=[256, 256, 256, 256],
fc_dims=[1024],
conv_norm="LN",
)
for _ in range(3)
],
box_predictors=[
L(FastRCNNOutputLayers)(
input_shape=ShapeSpec(channels=1024),
test_score_thresh=0.05,
box2box_transform=L(Box2BoxTransform)(weights=(w1, w1, w2, w2)),
cls_agnostic_bbox_reg=True,
num_classes="${...num_classes}",
)
for (w1, w2) in [(10, 5), (20, 10), (30, 15)]
],
proposal_matchers=[
L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False)
for th in [0.5, 0.6, 0.7]
],
)
# cascade_mask_rcnn_vitdet_h_75ep.py
from functools import partial
train.init_checkpoint = "detectron2://ImageNetPretrained/MAE/mae_pretrain_vit_huge_p14to16.pth"
model.backbone.net.embed_dim = 1280
model.backbone.net.depth = 32
model.backbone.net.num_heads = 16
model.backbone.net.drop_path_rate = 0.5
# 7, 15, 23, 31 for global attention
model.backbone.net.window_block_indexes = (
list(range(0, 7)) + list(range(8, 15)) + list(range(16, 23)) + list(range(24, 31))
)
optimizer.params.lr_factor_func = partial(get_vit_lr_decay_rate, lr_decay_rate=0.9, num_layers=32)
optimizer.params.overrides = {}
optimizer.params.weight_decay_norm = None
train.max_iter = train.max_iter * 3 // 4 # 100ep -> 75ep
lr_multiplier.scheduler.milestones = [
milestone * 3 // 4 for milestone in lr_multiplier.scheduler.milestones
]
lr_multiplier.scheduler.num_updates = train.max_iter
+148
View File
@@ -0,0 +1,148 @@
task_name: train
tags:
- dev
train: true
test: false
ckpt_path: null
seed: null
DATASETS:
SUPPRESS_KP_CONF_THRESH: 0.3
FILTER_NUM_KP: 4
FILTER_NUM_KP_THRESH: 0.0
FILTER_REPROJ_THRESH: 31000
SUPPRESS_BETAS_THRESH: 3.0
SUPPRESS_BAD_POSES: true
POSES_BETAS_SIMULTANEOUS: true
FILTER_NO_POSES: false
TRAIN:
H36M-TRAIN-WMASK:
WEIGHT: 0.1
MPII-TRAIN-WMASK:
WEIGHT: 0.1
COCO-TRAIN-2014-WMASK-PRUNED:
WEIGHT: 0.1
COCO-TRAIN-2014-VITPOSE-REPLICATE-PRUNED12:
WEIGHT: 0.1
MPI-INF-TRAIN-PRUNED:
WEIGHT: 0.02
AVA-TRAIN-MIDFRAMES-1FPS-WMASK:
WEIGHT: 0.19
AIC-TRAIN-WMASK:
WEIGHT: 0.19
INSTA-TRAIN-WMASK:
WEIGHT: 0.2
VAL:
COCO-VAL:
WEIGHT: 1.0
MOCAP: CMU-MOCAP
CONFIG:
SCALE_FACTOR: 0.3
ROT_FACTOR: 30
TRANS_FACTOR: 0.02
COLOR_SCALE: 0.2
ROT_AUG_RATE: 0.6
TRANS_AUG_RATE: 0.5
DO_FLIP: true
FLIP_AUG_RATE: 0.5
EXTREME_CROP_AUG_RATE: 0.1
EXTREME_CROP_AUG_LEVEL: 1
BETAS_REG: true
trainer:
_target_: pytorch_lightning.Trainer
default_root_dir: ${paths.output_dir}
accelerator: gpu
devices: 8
deterministic: false
num_sanity_val_steps: 0
log_every_n_steps: ${GENERAL.LOG_STEPS}
val_check_interval: ${GENERAL.VAL_STEPS}
precision: 16
max_steps: ${GENERAL.TOTAL_STEPS}
move_metrics_to_cpu: true
limit_val_batches: 1
track_grad_norm: -1
strategy: ddp
num_nodes: 1
sync_batchnorm: true
paths:
root_dir: ${oc.env:PROJECT_ROOT}
data_dir: ${paths.root_dir}/data/
log_dir: /fsx/shubham/code/hmr2023/logs_hydra/
output_dir: ${hydra:runtime.output_dir}
work_dir: ${hydra:runtime.cwd}
extras:
ignore_warnings: false
enforce_tags: true
print_config: true
exp_name: hmr2
SMPL:
MODEL_PATH: data/smpl
GENDER: neutral
NUM_BODY_JOINTS: 23
JOINT_REGRESSOR_EXTRA: data/SMPL_to_J19.pkl
MEAN_PARAMS: data/smpl_mean_params.npz
EXTRA:
FOCAL_LENGTH: 5000
NUM_LOG_IMAGES: 4
NUM_LOG_SAMPLES_PER_IMAGE: 8
PELVIS_IND: 39
MODEL:
IMAGE_SIZE: 256
IMAGE_MEAN:
- 0.485
- 0.456
- 0.406
IMAGE_STD:
- 0.229
- 0.224
- 0.225
BACKBONE:
TYPE: vit
NUM_LAYERS: 50
OUT_CHANNELS: 2048
ADD_NECK: false
FLOW:
DIM: 144
NUM_LAYERS: 4
CONTEXT_FEATURES: 2048
LAYER_HIDDEN_FEATURES: 1024
LAYER_DEPTH: 2
FC_HEAD:
NUM_FEATURES: 1024
SMPL_HEAD:
TYPE: transformer_decoder
IN_CHANNELS: 2048
TRANSFORMER_DECODER:
depth: 6
heads: 8
mlp_dim: 1024
dim_head: 64
dropout: 0.0
emb_dropout: 0.0
norm: layer
context_dim: 1280
GENERAL:
TOTAL_STEPS: 1000000
LOG_STEPS: 1000
VAL_STEPS: 1000
CHECKPOINT_STEPS: 10000
CHECKPOINT_SAVE_TOP_K: 1
NUM_WORKERS: 6
PREFETCH_FACTOR: 2
TRAIN:
LR: 1.0e-05
WEIGHT_DECAY: 0.0001
BATCH_SIZE: 48
LOSS_REDUCTION: mean
NUM_TRAIN_SAMPLES: 2
NUM_TEST_SAMPLES: 64
POSE_2D_NOISE_RATIO: 0.01
SMPL_PARAM_NOISE_RATIO: 0.005
LOSS_WEIGHTS:
KEYPOINTS_3D: 0.05
KEYPOINTS_2D: 0.01
GLOBAL_ORIENT: 0.001
BODY_POSE: 0.001
BETAS: 0.0005
ADVERSARIAL: 0.0005
local: {}
+148
View File
@@ -0,0 +1,148 @@
# Specific
/logs*/
/results/
/sandbox/
*.lock
*.pt
*.npy
/example_data/downloaded*
*.tar
*.tar.gz
/discord_sandbox/
/demo_out/
token_channel.csv
/hmr2_training_data/
/hmr2_evaluation_data/
/outputs/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
/checkpoints/
/data/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 UC Regents, Shubham Goel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+104
View File
@@ -0,0 +1,104 @@
# 4DHumans: Reconstructing and Tracking Humans with Transformers
Code repository for the paper:
**Humans in 4D: Reconstructing and Tracking Humans with Transformers**
[Shubham Goel](https://people.eecs.berkeley.edu/~shubham-goel/), [Georgios Pavlakos](https://geopavlakos.github.io/), [Jathushan Rajasegaran](http://people.eecs.berkeley.edu/~jathushan/), [Angjoo Kanazawa](https://people.eecs.berkeley.edu/~kanazawa/)<sup>\*</sup>, [Jitendra Malik](http://people.eecs.berkeley.edu/~malik/)<sup>\*</sup>
[![arXiv](https://img.shields.io/badge/arXiv-2305.20091-00ff00.svg)](https://arxiv.org/pdf/2305.20091.pdf) [![Website shields.io](https://img.shields.io/website-up-down-green-red/http/shields.io.svg)](https://shubham-goel.github.io/4dhumans/) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1Ex4gE5v1bPR3evfhtG7sDHxQGsWwNwby?usp=sharing) [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/brjathu/HMR2.0)
![teaser](assets/teaser.png)
## Installation and Setup
First, clone the repo. Then, we recommend creating a clean [conda](https://docs.conda.io/) environment, installing all dependencies, and finally activating the environment, as follows:
```bash
git clone https://github.com/shubham-goel/4D-Humans.git
cd 4D-Humans
conda env create -f environment.yml
conda activate 4D-humans
```
If conda is too slow, you can use pip:
```bash
conda create --name 4D-humans python=3.10
conda activate 4D-humans
pip install torch
pip install -e .[all]
```
All checkpoints and data will automatically be downloaded to `$HOME/.cache/4DHumans` the first time you run the demo code.
Besides these files, you also need to download the *SMPL* model. You will need the [neutral model](http://smplify.is.tue.mpg.de) for training and running the demo code. Please go to the corresponding website and register to get access to the downloads section. Download the model and place `basicModel_neutral_lbs_10_207_0_v1.0.0.pkl` in `./data/`.
## Run demo on images
The following command will run ViTDet and HMR2.0 on all images in the specified `--img_folder`, and save renderings of the reconstructions in `--out_folder`. `--batch_size` batches the images together for faster processing. The `--side_view` flags additionally renders the side view of the reconstructed mesh, `--full_frame` renders all people together in front view, `--save_mesh` saves meshes as `.obj`s.
```bash
python demo.py \
--img_folder example_data/images \
--out_folder demo_out \
--batch_size=48 --side_view --save_mesh --full_frame
```
## Run tracking demo on videos
Our tracker builds on PHALP, please install that first:
```bash
pip install git+https://github.com/brjathu/PHALP.git
```
Now, run `track.py` to reconstruct and track humans in any video. Input video source may be a video file, a folder of frames, or a youtube link:
```bash
# Run on video file
python track.py video.source="example_data/videos/gymnasts.mp4"
# Run on extracted frames
python track.py video.source="/path/to/frames_folder/"
# Run on a youtube link (depends on pytube working properly)
python track.py video.source=\'"https://www.youtube.com/watch?v=xEH_5T9jMVU"\'
```
The output directory (`./outputs` by default) will contain a video rendering of the tracklets and a `.pkl` file containing the tracklets with 3D pose and shape. Please see the [PHALP](https://github.com/brjathu/PHALP) repository for details.
## Training
Download the [training data](https://www.dropbox.com/sh/mjdwu59fxuhls5h/AACQ6FCGSrggUXmRzuubRHXIa) to `./hmr2_training_data/`, then start training using the following command:
```
bash fetch_training_data.sh
python train.py exp_name=hmr2 data=mix_all experiment=hmr_vit_transformer trainer=gpu launcher=local
```
Checkpoints and logs will be saved to `./logs/`. We trained on 8 A100 GPUs for 7 days using PyTorch 1.13.1 and PyTorch-Lightning 1.8.1 with CUDA 11.6 on a Linux system. You may adjust batch size and number of GPUs per your convenience.
## Evaluation
Download the [evaluation metadata](https://www.dropbox.com/scl/fi/kl79djemdgqcl6d691er7/hmr2_evaluation_data.tar.gz?rlkey=ttmbdu3x5etxwqqyzwk581zjl) to `./hmr2_evaluation_data/`. Additionally, download the Human3.6M, 3DPW, LSP-Extended, COCO, and PoseTrack dataset images and update the corresponding paths in `hmr2/configs/datasets_eval.yaml`.
Run evaluation on multiple datasets as follows, results are stored in `results/eval_regression.csv`.
```bash
python eval.py --dataset 'H36M-VAL-P2,3DPW-TEST,LSP-EXTENDED,POSETRACK-VAL,COCO-VAL'
```
By default, our code uses the released checkpoint (mentioned as HMR2.0b in the paper). To use the HMR2.0a checkpoint, you may download and untar from [here](https://people.eecs.berkeley.edu/~jathushan/projects/4dhumans/hmr2a_model.tar.gz)
## Preprocess code
To preprocess LSP Extended and Posetrack into metadata zip files for evaluation, see `hmr2/datasets/preprocess`.
Training data preprocessing coming soon.
## Acknowledgements
Parts of the code are taken or adapted from the following repos:
- [ProHMR](https://github.com/nkolot/ProHMR)
- [SPIN](https://github.com/nkolot/SPIN)
- [SMPLify-X](https://github.com/vchoutas/smplify-x)
- [HMR](https://github.com/akanazawa/hmr)
- [ViTPose](https://github.com/ViTAE-Transformer/ViTPose)
- [Detectron2](https://github.com/facebookresearch/detectron2)
Additionally, we thank [StabilityAI](https://stability.ai/) for a generous compute grant that enabled this work.
## Citing
If you find this code useful for your research, please consider citing the following paper:
```bibtex
@inproceedings{goel2023humans,
title={Humans in 4{D}: Reconstructing and Tracking Humans with Transformers},
author={Goel, Shubham and Pavlakos, Georgios and Rajasegaran, Jathushan and Kanazawa, Angjoo and Malik, Jitendra},
booktitle={ICCV},
year={2023}
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

+171
View File
@@ -0,0 +1,171 @@
from pathlib import Path
import torch
import argparse
import os
import cv2
import numpy as np
from hmr2.configs import CACHE_DIR_4DHUMANS
from hmr2.models import HMR2, download_models, load_hmr2, DEFAULT_CHECKPOINT
from hmr2.utils import recursive_to
from hmr2.datasets.vitdet_dataset import ViTDetDataset, DEFAULT_MEAN, DEFAULT_STD
from hmr2.utils.renderer import Renderer, cam_crop_to_full
LIGHT_BLUE=(0.65098039, 0.74117647, 0.85882353)
def main():
import time
start = time.time()
parser = argparse.ArgumentParser(description='HMR2 demo code')
parser.add_argument('--checkpoint', type=str, default=DEFAULT_CHECKPOINT, help='Path to pretrained model checkpoint')
parser.add_argument('--img_folder', type=str, default='example_data/images', help='Folder with input images')
parser.add_argument('--out_folder', type=str, default='demo_out', help='Output folder to save rendered results')
parser.add_argument('--side_view', dest='side_view', action='store_true', default=False, help='If set, render side view also')
parser.add_argument('--top_view', dest='top_view', action='store_true', default=False, help='If set, render top view also')
parser.add_argument('--full_frame', dest='full_frame', action='store_true', default=False, help='If set, render all people together also')
parser.add_argument('--save_mesh', dest='save_mesh', action='store_true', default=False, help='If set, save meshes to disk also')
parser.add_argument('--detector', type=str, default='vitdet', choices=['vitdet', 'regnety'], help='Using regnety improves runtime')
parser.add_argument('--batch_size', type=int, default=1, help='Batch size for inference/fitting')
parser.add_argument('--file_type', nargs='+', default=['*.jpg', '*.png'], help='List of file extensions to consider')
args = parser.parse_args()
# Download and load checkpoints
download_models(CACHE_DIR_4DHUMANS)
model, model_cfg = load_hmr2(args.checkpoint)
# Setup HMR2.0 model
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
model = model.to(device)
model.eval()
# Load detector
from hmr2.utils.utils_detectron2 import DefaultPredictor_Lazy
if args.detector == 'vitdet':
from detectron2.config import LazyConfig
import hmr2
cfg_path = Path(hmr2.__file__).parent/'configs'/'cascade_mask_rcnn_vitdet_h_75ep.py'
detectron2_cfg = LazyConfig.load(str(cfg_path))
detectron2_cfg.train.init_checkpoint = "https://dl.fbaipublicfiles.com/detectron2/ViTDet/COCO/cascade_mask_rcnn_vitdet_h/f328730692/model_final_f05665.pkl"
for i in range(3):
detectron2_cfg.model.roi_heads.box_predictors[i].test_score_thresh = 0.25
detector = DefaultPredictor_Lazy(detectron2_cfg)
elif args.detector == 'regnety':
from detectron2 import model_zoo
from detectron2.config import get_cfg
detectron2_cfg = model_zoo.get_config('new_baselines/mask_rcnn_regnety_4gf_dds_FPN_400ep_LSJ.py', trained=True)
detectron2_cfg.model.roi_heads.box_predictor.test_score_thresh = 0.5
detectron2_cfg.model.roi_heads.box_predictor.test_nms_thresh = 0.4
detector = DefaultPredictor_Lazy(detectron2_cfg)
# Setup the renderer
renderer = Renderer(model_cfg, faces=model.smpl.faces)
# Make output directory if it does not exist
os.makedirs(args.out_folder, exist_ok=True)
# Get all demo images that end with .jpg or .png
img_paths = [img for end in args.file_type for img in Path(args.img_folder).glob(end)]
# Iterate over all images in folder
for img_path in img_paths:
img_cv2 = cv2.imread(str(img_path))
# Detect humans in image
det_out = detector(img_cv2)
det_instances = det_out['instances']
valid_idx = (det_instances.pred_classes==0) & (det_instances.scores > 0.5)
boxes=det_instances.pred_boxes.tensor[valid_idx].cpu().numpy()
# Run HMR2.0 on all detected humans
dataset = ViTDetDataset(model_cfg, img_cv2, boxes)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0)
all_verts = []
all_cam_t = []
for batch in dataloader:
batch = recursive_to(batch, device)
with torch.no_grad():
out = model(batch)
pred_cam = out['pred_cam']
box_center = batch["box_center"].float()
box_size = batch["box_size"].float()
img_size = batch["img_size"].float()
scaled_focal_length = model_cfg.EXTRA.FOCAL_LENGTH / model_cfg.MODEL.IMAGE_SIZE * img_size.max()
pred_cam_t_full = cam_crop_to_full(pred_cam, box_center, box_size, img_size, scaled_focal_length).detach().cpu().numpy()
# Render the result
batch_size = batch['img'].shape[0]
for n in range(batch_size):
# Get filename from path img_path
img_fn, _ = os.path.splitext(os.path.basename(img_path))
person_id = int(batch['personid'][n])
white_img = (torch.ones_like(batch['img'][n]).cpu() - DEFAULT_MEAN[:,None,None]/255) / (DEFAULT_STD[:,None,None]/255)
input_patch = batch['img'][n].cpu() * (DEFAULT_STD[:,None,None]/255) + (DEFAULT_MEAN[:,None,None]/255)
input_patch = input_patch.permute(1,2,0).numpy()
regression_img = renderer(out['pred_vertices'][n].detach().cpu().numpy(),
out['pred_cam_t'][n].detach().cpu().numpy(),
batch['img'][n],
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
)
final_img = np.concatenate([input_patch, regression_img], axis=1)
if args.side_view:
side_img = renderer(out['pred_vertices'][n].detach().cpu().numpy(),
out['pred_cam_t'][n].detach().cpu().numpy(),
white_img,
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
side_view=True)
final_img = np.concatenate([final_img, side_img], axis=1)
if args.top_view:
top_img = renderer(out['pred_vertices'][n].detach().cpu().numpy(),
out['pred_cam_t'][n].detach().cpu().numpy(),
white_img,
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
top_view=True)
final_img = np.concatenate([final_img, top_img], axis=1)
cv2.imwrite(os.path.join(args.out_folder, f'{img_fn}_{person_id}.png'), 255*final_img[:, :, ::-1])
# Add all verts and cams to list
verts = out['pred_vertices'][n].detach().cpu().numpy()
cam_t = pred_cam_t_full[n]
all_verts.append(verts)
all_cam_t.append(cam_t)
# Save all meshes to disk
if args.save_mesh:
camera_translation = cam_t.copy()
tmesh = renderer.vertices_to_trimesh(verts, camera_translation, LIGHT_BLUE)
tmesh.export(os.path.join(args.out_folder, f'{img_fn}_{person_id}.obj'))
# Render front view
if args.full_frame and len(all_verts) > 0:
misc_args = dict(
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
focal_length=scaled_focal_length,
)
cam_view = renderer.render_rgba_multiple(all_verts, cam_t=all_cam_t, render_res=img_size[n], **misc_args)
# Overlay image
input_img = img_cv2.astype(np.float32)[:,:,::-1]/255.0
input_img = np.concatenate([input_img, np.ones_like(input_img[:,:,:1])], axis=2) # Add alpha channel
input_img_overlay = input_img[:,:,:3] * (1-cam_view[:,:,3:]) + cam_view[:,:,:3] * cam_view[:,:,3:]
cv2.imwrite(os.path.join(args.out_folder, f'{img_fn}_all.png'), 255*input_img_overlay[:, :, ::-1])
end = time.time()
print(end - start)
if __name__ == '__main__':
main()
+33
View File
@@ -0,0 +1,33 @@
name: 4D-humans
channels:
- pytorch
- nvidia
- conda-forge
dependencies:
- python=3.10
- numpy
- pytorch
- pytorch-cuda=11.8
- torchvision
- pip
- pip:
- pytorch-lightning
- smplx==0.1.28
- pyrender
- opencv-python
- yacs
- scikit-image
- einops
- timm
- dill
- pandas
- git+https://github.com/facebookresearch/detectron2
- git+https://github.com/mattloper/chumpy
# For training
- rich
- hydra-core
- hydra-submitit-launcher
- hydra-colorlog
- pyrootutils
- webdataset
+131
View File
@@ -0,0 +1,131 @@
import argparse
import os
from pathlib import Path
import traceback
from typing import Optional
import pandas as pd
import torch
from filelock import FileLock
from hmr2.configs import dataset_eval_config
from hmr2.datasets import create_dataset
from hmr2.utils import Evaluator, recursive_to
from tqdm import tqdm
from hmr2.configs import CACHE_DIR_4DHUMANS
from hmr2.models import HMR2, download_models, load_hmr2, DEFAULT_CHECKPOINT
def main():
parser = argparse.ArgumentParser(description='Evaluate trained models')
parser.add_argument('--checkpoint', type=str, default=DEFAULT_CHECKPOINT, help='Path to pretrained model checkpoint')
parser.add_argument('--results_file', type=str, default='results/eval_regression.csv', help='Path to results file.')
parser.add_argument('--dataset', type=str, default='H36M-VAL-P2,3DPW-TEST,LSP-EXTENDED,POSETRACK-VAL,COCO-VAL', help='Dataset to evaluate') # choices=['H36M-VAL-P2', '3DPW-TEST', 'MPI-INF-TEST']
parser.add_argument('--batch_size', type=int, default=16, help='Batch size for inference')
parser.add_argument('--num_samples', type=int, default=1, help='Number of test samples to draw')
parser.add_argument('--num_workers', type=int, default=8, help='Number of workers used for data loading')
parser.add_argument('--log_freq', type=int, default=10, help='How often to log results')
parser.add_argument('--shuffle', dest='shuffle', action='store_true', default=False, help='Shuffle the dataset during evaluation')
parser.add_argument('--exp_name', type=str, default=None, help='Experiment name')
args = parser.parse_args()
# Download and load checkpoints
download_models(CACHE_DIR_4DHUMANS)
model, model_cfg = load_hmr2(args.checkpoint)
# Setup HMR2.0 model
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
model = model.to(device)
model.eval()
# Load config and run eval, one dataset at a time
print('Evaluating on datasets: {}'.format(args.dataset), flush=True)
for dataset in args.dataset.split(','):
dataset_cfg = dataset_eval_config()[dataset]
args.dataset = dataset
run_eval(model, model_cfg, dataset_cfg, device, args)
def run_eval(model, model_cfg, dataset_cfg, device, args):
# Create dataset and data loader
dataset = create_dataset(model_cfg, dataset_cfg, train=False)
dataloader = torch.utils.data.DataLoader(dataset, args.batch_size, shuffle=args.shuffle, num_workers=args.num_workers)
# List of metrics to log
if args.dataset in ['H36M-VAL-P2','3DPW-TEST']:
metrics = ['mode_re', 'mode_mpjpe']
pck_thresholds = None
if args.dataset in ['LSP-EXTENDED', 'POSETRACK-VAL', 'COCO-VAL']:
metrics = ['mode_kpl2']
pck_thresholds = [0.05, 0.1]
# Setup evaluator object
evaluator = Evaluator(
dataset_length=int(1e8),
keypoint_list=dataset_cfg.KEYPOINT_LIST,
pelvis_ind=model_cfg.EXTRA.PELVIS_IND,
metrics=metrics,
pck_thresholds=pck_thresholds,
)
# Go over the images in the dataset.
try:
for i, batch in enumerate(tqdm(dataloader)):
batch = recursive_to(batch, device)
with torch.no_grad():
out = model(batch)
evaluator(out, batch)
if i % args.log_freq == args.log_freq - 1:
evaluator.log()
evaluator.log()
error = None
except (Exception, KeyboardInterrupt) as e:
traceback.print_exc()
error = repr(e)
i = 0
# Append results to file
metrics_dict = evaluator.get_metrics_dict()
save_eval_result(args.results_file, metrics_dict, args.checkpoint, args.dataset, error=error, iters_done=i, exp_name=args.exp_name)
def save_eval_result(
csv_path: str,
metric_dict: float,
checkpoint_path: str,
dataset_name: str,
# start_time: pd.Timestamp,
error: Optional[str] = None,
iters_done=None,
exp_name=None,
) -> None:
"""Save evaluation results for a single scene file to a common CSV file."""
timestamp = pd.Timestamp.now()
exists: bool = os.path.exists(csv_path)
exp_name = exp_name or Path(checkpoint_path).parent.parent.name
# save each metric as different row to the csv path
metric_names = list(metric_dict.keys())
metric_values = list(metric_dict.values())
N = len(metric_names)
df = pd.DataFrame(
dict(
timestamp=[timestamp] * N,
checkpoint_path=[checkpoint_path] * N,
exp_name=[exp_name] * N,
dataset=[dataset_name] * N,
metric_name=metric_names,
metric_value=metric_values,
error=[error] * N,
iters_done=[iters_done] * N,
),
index=list(range(N)),
)
# Lock the file to prevent multiple processes from writing to it at the same time.
lock = FileLock(f"{csv_path}.lock", timeout=10)
with lock:
df.to_csv(csv_path, mode="a", header=not exists, index=False)
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Google drive links to download the training data
gdown https://drive.google.com/uc?id=19e5w1x8JIJkYUT9TIPcFhQF1SXny_Qwz # hmr2_training_data_part1.tar
gdown https://drive.google.com/uc?id=10glv5LKLhqPHHpaWmvkg4wT3wSmL6E2f # hmr2_training_data_part2.tar
gdown https://drive.google.com/uc?id=19AJMbE0nKD6Lao22aAaPLngiuvrnxF5V # hmr2_training_data_part3.tar
gdown https://drive.google.com/uc?id=1ntyjT3-JbtDxoHqmCJzJ8JemMKxM78Xj # hmr2_training_data_part4a.tar
gdown https://drive.google.com/uc?id=1H1xBQqePHcayR65oRn3cnxdgau_WLrLD # hmr2_training_data_part4b.tar
gdown https://drive.google.com/uc?id=1TnYqqG-wQMfMjd0EHbMEqxDt8DXG3vbW # hmr2_training_data_part4c.tar
gdown https://drive.google.com/uc?id=1tY6N8RDtgWfHSnTLgUNfbGhPI5X9fMg9 # hmr2_training_data_part4d.tar
# Alternatively, consider using the dropbox links:
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AACJetGWdtee9TP_ejmBMdKOa/hmr2_training_data_part1.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AAA7vh5e4jubZ3cYRyOtYYQZa/hmr2_training_data_part2.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AACtCMwexHgDCMxh5CYP1VEya/hmr2_training_data_part3.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AAB7C9Y33MEX5G9_UCHQ2KyLa/hmr2_training_data_part4a.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AABkok2N00PHFGHiKV8-RjpNa/hmr2_training_data_part4b.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AADYg_LKlGbjpjvrOQWoOgrna/hmr2_training_data_part4c.tar
#wget https://www.dropbox.com/sh/4y90ay6a6nki9v7/AACARYZJMoxq9nS6cDMAL8Pya/hmr2_training_data_part4d.tar
for f in hmr2_training_data_part*.tar; do
tar --warning=no-unknown-keyword --exclude=".*" -xvf $f
done
+187
View File
@@ -0,0 +1,187 @@
from pathlib import Path
import tempfile
import torch
import argparse
import os
import numpy as np
import gradio as gr
from PIL import Image
from hmr2.configs import CACHE_DIR_4DHUMANS
from hmr2.models import HMR2, download_models, load_hmr2, DEFAULT_CHECKPOINT
from hmr2.utils import recursive_to
from hmr2.datasets.vitdet_dataset import ViTDetDataset, DEFAULT_MEAN, DEFAULT_STD
from hmr2.utils.renderer import Renderer, cam_crop_to_full
LIGHT_BLUE=(0.65098039, 0.74117647, 0.85882353)
def main():
parser = argparse.ArgumentParser(description='HMR2 demo code')
parser.add_argument('--checkpoint', type=str, default=DEFAULT_CHECKPOINT, help='Path to pretrained model checkpoint')
# parser.add_argument('--img_folder', type=str, default='example_data/images', help='Folder with input images')
parser.add_argument('--out_folder', type=str, default='demo_out', help='Output folder to save rendered results')
# parser.add_argument('--side_view', dest='side_view', action='store_true', default=False, help='If set, render side view also')
# parser.add_argument('--full_frame', dest='full_frame', action='store_true', default=False, help='If set, render all people together also')
# parser.add_argument('--save_mesh', dest='save_mesh', action='store_true', default=False, help='If set, save meshes to disk also')
parser.add_argument('--batch_size', type=int, default=64, help='Batch size for inference/fitting')
args = parser.parse_args()
# Download and load checkpoints
download_models(CACHE_DIR_4DHUMANS)
model, model_cfg = load_hmr2(args.checkpoint)
# Setup HMR2.0 model
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
model = model.to(device)
model.eval()
# Load detector
from hmr2.utils.utils_detectron2 import DefaultPredictor_Lazy
from detectron2.config import LazyConfig
import hmr2
cfg_path = Path(hmr2.__file__).parent/'configs'/'cascade_mask_rcnn_vitdet_h_75ep.py'
detectron2_cfg = LazyConfig.load(str(cfg_path))
detectron2_cfg.train.init_checkpoint = "https://dl.fbaipublicfiles.com/detectron2/ViTDet/COCO/cascade_mask_rcnn_vitdet_h/f328730692/model_final_f05665.pkl"
for i in range(3):
detectron2_cfg.model.roi_heads.box_predictors[i].test_score_thresh = 0.25
detector = DefaultPredictor_Lazy(detectron2_cfg)
# Setup the renderer
renderer = Renderer(model_cfg, faces=model.smpl.faces)
# Make output directory if it does not exist
os.makedirs(args.out_folder, exist_ok=True)
# # Iterate over all images in folder
# for img_path in Path(args.img_folder).glob('*.jpg'):
# img_cv2 = cv2.imread(str(img_path))
def infer(in_pil_img, in_threshold=0.8, out_pil_img=None):
# Convert RGB to BGR
img_cv2 = np.array(in_pil_img)[:, :, ::-1].copy()
# Detect humans in image
det_out = detector(img_cv2)
det_instances = det_out['instances']
valid_idx = (det_instances.pred_classes==0) & (det_instances.scores > 0.5)
boxes=det_instances.pred_boxes.tensor[valid_idx].cpu().numpy()
# Run HMR2.0 on all detected humans
dataset = ViTDetDataset(model_cfg, img_cv2, boxes)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0)
all_verts = []
all_cam_t = []
all_mesh_paths = []
temp_name = next(tempfile._get_candidate_names())
for batch in dataloader:
batch = recursive_to(batch, device)
with torch.no_grad():
out = model(batch)
pred_cam = out['pred_cam']
box_center = batch["box_center"].float()
box_size = batch["box_size"].float()
img_size = batch["img_size"].float()
scaled_focal_length = model_cfg.EXTRA.FOCAL_LENGTH / model_cfg.MODEL.IMAGE_SIZE * img_size.max()
pred_cam_t_full = cam_crop_to_full(pred_cam, box_center, box_size, img_size, scaled_focal_length).detach().cpu().numpy()
# Render the result
batch_size = batch['img'].shape[0]
for n in range(batch_size):
# Get filename from path img_path
# img_fn, _ = os.path.splitext(os.path.basename(img_path))
person_id = int(batch['personid'][n])
white_img = (torch.ones_like(batch['img'][n]).cpu() - DEFAULT_MEAN[:,None,None]/255) / (DEFAULT_STD[:,None,None]/255)
input_patch = batch['img'][n].cpu() * (DEFAULT_STD[:,None,None]/255) + (DEFAULT_MEAN[:,None,None]/255)
input_patch = input_patch.permute(1,2,0).numpy()
# regression_img = renderer(out['pred_vertices'][n].detach().cpu().numpy(),
# out['pred_cam_t'][n].detach().cpu().numpy(),
# batch['img'][n],
# mesh_base_color=LIGHT_BLUE,
# scene_bg_color=(1, 1, 1),
# )
# if args.side_view:
# side_img = renderer(out['pred_vertices'][n].detach().cpu().numpy(),
# out['pred_cam_t'][n].detach().cpu().numpy(),
# white_img,
# mesh_base_color=LIGHT_BLUE,
# scene_bg_color=(1, 1, 1),
# side_view=True)
# final_img = np.concatenate([input_patch, regression_img, side_img], axis=1)
# else:
# final_img = np.concatenate([input_patch, regression_img], axis=1)
# cv2.imwrite(os.path.join(args.out_folder, f'{img_fn}_{person_id}.png'), 255*final_img[:, :, ::-1])
# Add all verts and cams to list
verts = out['pred_vertices'][n].detach().cpu().numpy()
cam_t = pred_cam_t_full[n]
all_verts.append(verts)
all_cam_t.append(cam_t)
# Save all meshes to disk
# if args.save_mesh:
if True:
camera_translation = cam_t.copy()
tmesh = renderer.vertices_to_trimesh(verts, camera_translation, LIGHT_BLUE)
temp_path = os.path.join(args.out_folder, f'{temp_name}_{person_id}.obj')
tmesh.export(temp_path)
all_mesh_paths.append(temp_path)
# Render front view
if len(all_verts) > 0:
misc_args = dict(
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
focal_length=scaled_focal_length,
)
cam_view = renderer.render_rgba_multiple(all_verts, cam_t=all_cam_t, render_res=img_size[n], **misc_args)
# Overlay image
input_img = img_cv2.astype(np.float32)[:,:,::-1]/255.0
input_img = np.concatenate([input_img, np.ones_like(input_img[:,:,:1])], axis=2) # Add alpha channel
input_img_overlay = input_img[:,:,:3] * (1-cam_view[:,:,3:]) + cam_view[:,:,:3] * cam_view[:,:,3:]
# cv2.imwrite(os.path.join(args.out_folder, f'{img_fn}_all.png'), 255*input_img_overlay[:, :, ::-1])
# convert to PIL image
out_pil_img = Image.fromarray((input_img_overlay*255).astype(np.uint8))
return out_pil_img, all_mesh_paths
else:
return None, []
with gr.Blocks(title="4DHumans", css=".gradio-container") as demo:
gr.HTML("""<div style="font-weight:bold; text-align:center; color:royalblue;">HMR 2.0</div>""")
with gr.Row():
with gr.Column():
input_image = gr.Image(label="Input image", type="pil")
with gr.Column():
output_image = gr.Image(label="Reconstructions", type="pil")
output_meshes = gr.File(label="3D meshes")
gr.HTML("""<br/>""")
with gr.Row():
threshold = gr.Slider(0, 1.0, value=0.6, label='Detection Threshold')
send_btn = gr.Button("Infer")
send_btn.click(fn=infer, inputs=[input_image, threshold], outputs=[output_image, output_meshes])
gr.Examples([
['example_data/images/pexels-anete-lusina-4793258.jpg'],
['example_data/images/skates.png'],
],
inputs=[input_image, 0.6])
demo.launch(debug=True)
if __name__ == '__main__':
main()
View File
+115
View File
@@ -0,0 +1,115 @@
import os
from typing import Dict
from yacs.config import CfgNode as CN
CACHE_DIR = os.path.join(os.environ.get("HOME"), ".cache")
CACHE_DIR_4DHUMANS = os.path.join(CACHE_DIR, "4DHumans")
def to_lower(x: Dict) -> Dict:
"""
Convert all dictionary keys to lowercase
Args:
x (dict): Input dictionary
Returns:
dict: Output dictionary with all keys converted to lowercase
"""
return {k.lower(): v for k, v in x.items()}
_C = CN(new_allowed=True)
_C.GENERAL = CN(new_allowed=True)
_C.GENERAL.RESUME = True
_C.GENERAL.TIME_TO_RUN = 3300
_C.GENERAL.VAL_STEPS = 100
_C.GENERAL.LOG_STEPS = 100
_C.GENERAL.CHECKPOINT_STEPS = 20000
_C.GENERAL.CHECKPOINT_DIR = "checkpoints"
_C.GENERAL.SUMMARY_DIR = "tensorboard"
_C.GENERAL.NUM_GPUS = 1
_C.GENERAL.NUM_WORKERS = 4
_C.GENERAL.MIXED_PRECISION = True
_C.GENERAL.ALLOW_CUDA = True
_C.GENERAL.PIN_MEMORY = False
_C.GENERAL.DISTRIBUTED = False
_C.GENERAL.LOCAL_RANK = 0
_C.GENERAL.USE_SYNCBN = False
_C.GENERAL.WORLD_SIZE = 1
_C.TRAIN = CN(new_allowed=True)
_C.TRAIN.NUM_EPOCHS = 100
_C.TRAIN.BATCH_SIZE = 32
_C.TRAIN.SHUFFLE = True
_C.TRAIN.WARMUP = False
_C.TRAIN.NORMALIZE_PER_IMAGE = False
_C.TRAIN.CLIP_GRAD = False
_C.TRAIN.CLIP_GRAD_VALUE = 1.0
_C.LOSS_WEIGHTS = CN(new_allowed=True)
_C.DATASETS = CN(new_allowed=True)
_C.MODEL = CN(new_allowed=True)
_C.MODEL.IMAGE_SIZE = 224
_C.EXTRA = CN(new_allowed=True)
_C.EXTRA.FOCAL_LENGTH = 5000
_C.DATASETS.CONFIG = CN(new_allowed=True)
_C.DATASETS.CONFIG.SCALE_FACTOR = 0.3
_C.DATASETS.CONFIG.ROT_FACTOR = 30
_C.DATASETS.CONFIG.TRANS_FACTOR = 0.02
_C.DATASETS.CONFIG.COLOR_SCALE = 0.2
_C.DATASETS.CONFIG.ROT_AUG_RATE = 0.6
_C.DATASETS.CONFIG.TRANS_AUG_RATE = 0.5
_C.DATASETS.CONFIG.DO_FLIP = True
_C.DATASETS.CONFIG.FLIP_AUG_RATE = 0.5
_C.DATASETS.CONFIG.EXTREME_CROP_AUG_RATE = 0.10
def default_config() -> CN:
"""
Get a yacs CfgNode object with the default config values.
"""
# Return a clone so that the defaults will not be altered
# This is for the "local variable" use pattern
return _C.clone()
def dataset_config(name='datasets_tar.yaml') -> CN:
"""
Get dataset config file
Returns:
CfgNode: Dataset config as a yacs CfgNode object.
"""
cfg = CN(new_allowed=True)
config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), name)
cfg.merge_from_file(config_file)
cfg.freeze()
return cfg
def dataset_eval_config() -> CN:
return dataset_config('datasets_eval.yaml')
def get_config(config_file: str, merge: bool = True, update_cachedir: bool = False) -> CN:
"""
Read a config file and optionally merge it with the default config file.
Args:
config_file (str): Path to config file.
merge (bool): Whether to merge with the default config or not.
Returns:
CfgNode: Config as a yacs CfgNode object.
"""
if merge:
cfg = default_config()
else:
cfg = CN(new_allowed=True)
cfg.merge_from_file(config_file)
if update_cachedir:
def update_path(path: str) -> str:
if os.path.isabs(path):
return path
return os.path.join(CACHE_DIR_4DHUMANS, path)
# cfg.SMPL.MODEL_PATH = update_path(cfg.SMPL.MODEL_PATH)
# cfg.SMPL.JOINT_REGRESSOR_EXTRA = update_path(cfg.SMPL.JOINT_REGRESSOR_EXTRA)
# cfg.SMPL.MEAN_PARAMS = update_path(cfg.SMPL.MEAN_PARAMS)
return cfg
@@ -0,0 +1,129 @@
## coco_loader_lsj.py
import detectron2.data.transforms as T
from detectron2 import model_zoo
from detectron2.config import LazyCall as L
# Data using LSJ
image_size = 1024
dataloader = model_zoo.get_config("common/data/coco.py").dataloader
dataloader.train.mapper.augmentations = [
L(T.RandomFlip)(horizontal=True), # flip first
L(T.ResizeScale)(
min_scale=0.1, max_scale=2.0, target_height=image_size, target_width=image_size
),
L(T.FixedSizeCrop)(crop_size=(image_size, image_size), pad=False),
]
dataloader.train.mapper.image_format = "RGB"
dataloader.train.total_batch_size = 64
# recompute boxes due to cropping
dataloader.train.mapper.recompute_boxes = True
dataloader.test.mapper.augmentations = [
L(T.ResizeShortestEdge)(short_edge_length=image_size, max_size=image_size),
]
from functools import partial
from fvcore.common.param_scheduler import MultiStepParamScheduler
from detectron2 import model_zoo
from detectron2.config import LazyCall as L
from detectron2.solver import WarmupParamScheduler
from detectron2.modeling.backbone.vit import get_vit_lr_decay_rate
# mask_rcnn_vitdet_b_100ep.py
model = model_zoo.get_config("common/models/mask_rcnn_vitdet.py").model
# Initialization and trainer settings
train = model_zoo.get_config("common/train.py").train
train.amp.enabled = True
train.ddp.fp16_compression = True
train.init_checkpoint = "detectron2://ImageNetPretrained/MAE/mae_pretrain_vit_base.pth"
# Schedule
# 100 ep = 184375 iters * 64 images/iter / 118000 images/ep
train.max_iter = 184375
lr_multiplier = L(WarmupParamScheduler)(
scheduler=L(MultiStepParamScheduler)(
values=[1.0, 0.1, 0.01],
milestones=[163889, 177546],
num_updates=train.max_iter,
),
warmup_length=250 / train.max_iter,
warmup_factor=0.001,
)
# Optimizer
optimizer = model_zoo.get_config("common/optim.py").AdamW
optimizer.params.lr_factor_func = partial(get_vit_lr_decay_rate, num_layers=12, lr_decay_rate=0.7)
optimizer.params.overrides = {"pos_embed": {"weight_decay": 0.0}}
# cascade_mask_rcnn_vitdet_b_100ep.py
from detectron2.config import LazyCall as L
from detectron2.layers import ShapeSpec
from detectron2.modeling.box_regression import Box2BoxTransform
from detectron2.modeling.matcher import Matcher
from detectron2.modeling.roi_heads import (
FastRCNNOutputLayers,
FastRCNNConvFCHead,
CascadeROIHeads,
)
# arguments that don't exist for Cascade R-CNN
[model.roi_heads.pop(k) for k in ["box_head", "box_predictor", "proposal_matcher"]]
model.roi_heads.update(
_target_=CascadeROIHeads,
box_heads=[
L(FastRCNNConvFCHead)(
input_shape=ShapeSpec(channels=256, height=7, width=7),
conv_dims=[256, 256, 256, 256],
fc_dims=[1024],
conv_norm="LN",
)
for _ in range(3)
],
box_predictors=[
L(FastRCNNOutputLayers)(
input_shape=ShapeSpec(channels=1024),
test_score_thresh=0.05,
box2box_transform=L(Box2BoxTransform)(weights=(w1, w1, w2, w2)),
cls_agnostic_bbox_reg=True,
num_classes="${...num_classes}",
)
for (w1, w2) in [(10, 5), (20, 10), (30, 15)]
],
proposal_matchers=[
L(Matcher)(thresholds=[th], labels=[0, 1], allow_low_quality_matches=False)
for th in [0.5, 0.6, 0.7]
],
)
# cascade_mask_rcnn_vitdet_h_75ep.py
from functools import partial
train.init_checkpoint = "detectron2://ImageNetPretrained/MAE/mae_pretrain_vit_huge_p14to16.pth"
model.backbone.net.embed_dim = 1280
model.backbone.net.depth = 32
model.backbone.net.num_heads = 16
model.backbone.net.drop_path_rate = 0.5
# 7, 15, 23, 31 for global attention
model.backbone.net.window_block_indexes = (
list(range(0, 7)) + list(range(8, 15)) + list(range(16, 23)) + list(range(24, 31))
)
optimizer.params.lr_factor_func = partial(get_vit_lr_decay_rate, lr_decay_rate=0.9, num_layers=32)
optimizer.params.overrides = {}
optimizer.params.weight_decay_norm = None
train.max_iter = train.max_iter * 3 // 4 # 100ep -> 75ep
lr_multiplier.scheduler.milestones = [
milestone * 3 // 4 for milestone in lr_multiplier.scheduler.milestones
]
lr_multiplier.scheduler.num_updates = train.max_iter
+31
View File
@@ -0,0 +1,31 @@
H36M-VAL-P2:
TYPE: ImageDataset
DATASET_FILE: hmr2_evaluation_data/h36m_val_p2.npz
IMG_DIR: /shared/pavlakos/datasets/h36m/images/
KEYPOINT_LIST: [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 43]
USE_HIPS: True
3DPW-TEST:
TYPE: ImageDataset
DATASET_FILE: hmr2_evaluation_data/3dpw_test.npz
IMG_DIR: /shared/pavlakos/datasets/3DPW/
KEYPOINT_LIST: [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 43]
USE_HIPS: False
POSETRACK-VAL:
TYPE: ImageDataset
DATASET_FILE: hmr2_evaluation_data/posetrack_2018_val.npz
IMG_DIR: /shared/pavlakos/datasets/posetrack/posetrack2018/posetrack_data/
KEYPOINT_LIST: [0] # Dummy
LSP-EXTENDED:
TYPE: ImageDataset
DATASET_FILE: hmr2_evaluation_data/hr-lspet_train.npz
IMG_DIR: /shared/pavlakos/datasets/hr-lspet/
KEYPOINT_LIST: [0] # Dummy
COCO-VAL:
TYPE: ImageDataset
DATASET_FILE: hmr2_evaluation_data/coco_val.npz
IMG_DIR: /shared/pavlakos/datasets/coco/
KEYPOINT_LIST: [0] # Dummy
+38
View File
@@ -0,0 +1,38 @@
MPI-INF-TRAIN-PRUNED:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/mpi-inf-train-pruned/{000000..00006}.tar
epoch_size: 12_000
H36M-TRAIN-WMASK:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/h36m-train/{000000..000312}.tar
epoch_size: 314_000 # Can be changed arbitrarily. Enables resampling.
MPII-TRAIN-WMASK:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/mpii-train/{000000..000009}.tar
epoch_size: 100_000 # Increase to ensure dataset doesn't end before epoch
COCO-TRAIN-2014-WMASK-PRUNED:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/coco-train-2014-pruned/{000000..000017}.tar
epoch_size: 18_000
AVA-TRAIN-MIDFRAMES-1FPS-WMASK:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/ava-train-midframes-1fps-vitpose/{000000..000092}.tar
epoch_size: 200_000
AIC-TRAIN-WMASK:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/aic-train-vitpose/{000000..000104}.tar
epoch_size: 200_000
INSTA-TRAIN-WMASK:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/insta-train-vitpose-replicate/{000000..003657}.tar
epoch_size: 4_000_000
COCO-TRAIN-2014-VITPOSE-REPLICATE-PRUNED12:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/coco-train-2014-vitpose-pruned/{000000..000044}.tar
epoch_size: 45_000
COCO-VAL:
TYPE: ImageDataset
URLS: hmr2_training_data/dataset_tars/coco-val/{000000..000000}.tar
KEYPOINT_LIST: [0] # Dummy
CMU-MOCAP:
DATASET_FILE: hmr2_training_data/cmu_mocap.npz
@@ -0,0 +1,27 @@
# @package _global_
defaults:
- /data_filtering: low1
DATASETS:
TRAIN:
H36M-TRAIN-WMASK:
WEIGHT: 0.1
MPII-TRAIN-WMASK:
WEIGHT: 0.1
COCO-TRAIN-2014-WMASK-PRUNED:
WEIGHT: 0.1
COCO-TRAIN-2014-VITPOSE-REPLICATE-PRUNED12:
WEIGHT: 0.1
MPI-INF-TRAIN-PRUNED:
WEIGHT: 0.02
AVA-TRAIN-MIDFRAMES-1FPS-WMASK:
WEIGHT: 0.19
AIC-TRAIN-WMASK:
WEIGHT: 0.19
INSTA-TRAIN-WMASK:
WEIGHT: 0.2
VAL:
COCO-VAL:
WEIGHT: 1.0
MOCAP: CMU-MOCAP
@@ -0,0 +1,13 @@
# @package _global_
DATASETS:
# Data filtering during training
SUPPRESS_KP_CONF_THRESH: 0.3
FILTER_NUM_KP: 4
FILTER_NUM_KP_THRESH: 0.0
FILTER_REPROJ_THRESH: 31000
SUPPRESS_BETAS_THRESH: 3.0
SUPPRESS_BAD_POSES: True
POSES_BETAS_SIMULTANEOUS: True
FILTER_NO_POSES: False # If True, filters images that don't have poses
@@ -0,0 +1,29 @@
# @package _global_
SMPL:
DATA_DIR: ${oc.env:HOME}/.cache/4DHumans/data/
MODEL_PATH: ${SMPL.DATA_DIR}/smpl
GENDER: neutral
NUM_BODY_JOINTS: 23
JOINT_REGRESSOR_EXTRA: ${SMPL.DATA_DIR}/SMPL_to_J19.pkl
MEAN_PARAMS: ${SMPL.DATA_DIR}/smpl_mean_params.npz
EXTRA:
FOCAL_LENGTH: 5000
NUM_LOG_IMAGES: 4
NUM_LOG_SAMPLES_PER_IMAGE: 8
PELVIS_IND: 39
DATASETS:
BETAS_REG: True
CONFIG:
SCALE_FACTOR: 0.3
ROT_FACTOR: 30
TRANS_FACTOR: 0.02
COLOR_SCALE: 0.2
ROT_AUG_RATE: 0.6
TRANS_AUG_RATE: 0.5
DO_FLIP: True
FLIP_AUG_RATE: 0.5
EXTREME_CROP_AUG_RATE: 0.10
EXTREME_CROP_AUG_LEVEL: 1
@@ -0,0 +1,51 @@
# @package _global_
defaults:
- default.yaml
GENERAL:
TOTAL_STEPS: 1_000_000
LOG_STEPS: 1000
VAL_STEPS: 1000
CHECKPOINT_STEPS: 10000
CHECKPOINT_SAVE_TOP_K: 1
NUM_WORKERS: 6
PREFETCH_FACTOR: 2
TRAIN:
LR: 1e-5
WEIGHT_DECAY: 1e-4
BATCH_SIZE: 48
LOSS_REDUCTION: mean
NUM_TRAIN_SAMPLES: 2
NUM_TEST_SAMPLES: 64
POSE_2D_NOISE_RATIO: 0.01
SMPL_PARAM_NOISE_RATIO: 0.005
MODEL:
IMAGE_SIZE: 256
IMAGE_MEAN: [0.485, 0.456, 0.406]
IMAGE_STD: [0.229, 0.224, 0.225]
BACKBONE:
TYPE: vit
PRETRAINED_WEIGHTS: hmr2_training_data/vitpose_backbone.pth
SMPL_HEAD:
TYPE: transformer_decoder
IN_CHANNELS: 2048
TRANSFORMER_DECODER:
depth: 6
heads: 8
mlp_dim: 1024
dim_head: 64
dropout: 0.0
emb_dropout: 0.0
norm: layer
context_dim: 1280 # from vitpose-H
LOSS_WEIGHTS:
KEYPOINTS_3D: 0.05
KEYPOINTS_2D: 0.01
GLOBAL_ORIENT: 0.001
BODY_POSE: 0.001
BETAS: 0.0005
ADVERSARIAL: 0.0005
@@ -0,0 +1,8 @@
# disable python warnings if they annoy you
ignore_warnings: False
# ask user for tags if none are provided in the config
enforce_tags: True
# pretty print config tree at the start of the run using Rich library
print_config: True
@@ -0,0 +1,26 @@
# @package _global_
# https://hydra.cc/docs/configure_hydra/intro/
# enable color logging
defaults:
- override /hydra/hydra_logging: colorlog
- override /hydra/job_logging: colorlog
# exp_name: ovrd_${hydra:job.override_dirname}
exp_name: ${now:%Y-%m-%d}_${now:%H-%M-%S}
hydra:
run:
dir: ${paths.log_dir}/${task_name}/runs/${exp_name}
sweep:
dir: ${paths.log_dir}/${task_name}/multiruns/${exp_name}
subdir: ${hydra.job.num}
job:
config:
override_dirname:
exclude_keys:
- trainer
- trainer.devices
- trainer.num_nodes
- callbacks
- debug
@@ -0,0 +1,13 @@
# @package _global_
defaults:
- override /hydra/launcher: submitit_local
hydra:
launcher:
timeout_min: 10_080 # 7 days
nodes: 1
tasks_per_node: ${trainer.devices}
cpus_per_task: 6
gpus_per_node: ${trainer.devices}
name: hmr2
@@ -0,0 +1,22 @@
# @package _global_
defaults:
- override /hydra/launcher: submitit_slurm
hydra:
launcher:
timeout_min: 10_080 # 7 days
max_num_timeout: 3
partition: g40
qos: idle
nodes: 1
tasks_per_node: ${trainer.devices}
gpus_per_task: null
cpus_per_task: 12
gpus_per_node: ${trainer.devices}
cpus_per_gpu: null
comment: laion
name: hmr2
setup:
- module load cuda openmpi libfabric-aws
- export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
@@ -0,0 +1,18 @@
# path to root directory
# this requires PROJECT_ROOT environment variable to exist
# PROJECT_ROOT is inferred and set by pyrootutils package in `train.py` and `eval.py`
root_dir: ${oc.env:PROJECT_ROOT}
# path to data directory
data_dir: ${paths.root_dir}/data/
# path to logging directory
log_dir: logs/
# path to output directory, created dynamically by hydra
# path generation pattern is specified in `configs/hydra/default.yaml`
# use it to store all files generated during the run, like ckpts and metrics
output_dir: ${hydra:runtime.output_dir}
# path to working directory
work_dir: ${hydra:runtime.cwd}
+47
View File
@@ -0,0 +1,47 @@
# @package _global_
# specify here default configuration
# order of defaults determines the order in which configs override each other
defaults:
- _self_
- data: mix_all.yaml
- trainer: ddp.yaml
- paths: default.yaml
- extras: default.yaml
- hydra: default.yaml
# experiment configs allow for version control of specific hyperparameters
# e.g. best hyperparameters for given model and datamodule
- experiment: null
- texture_exp: null
# optional local config for machine/user specific settings
# it's optional since it doesn't need to exist and is excluded from version control
- optional launcher: local.yaml
# - optional launcher: slurm.yaml
# debugging config (enable through command line, e.g. `python train.py debug=default)
- debug: null
# task name, determines output directory path
task_name: "train"
# tags to help you identify your experiments
# you can overwrite this in experiment configs
# overwrite from command line with `python train.py tags="[first_tag, second_tag]"`
# appending lists from command line is currently not supported :(
# https://github.com/facebookresearch/hydra/issues/1547
tags: ["dev"]
# set False to skip model training
train: True
# evaluate on test set, using best model weights achieved during training
# lightning chooses best weights based on the metric specified in checkpoint callback
test: False
# simply provide checkpoint path to resume training
ckpt_path: null
# seed for random number generators in pytorch, numpy and python.random
seed: null
@@ -0,0 +1,6 @@
defaults:
- default.yaml
- default_hmr.yaml
accelerator: cpu
devices: 1
@@ -0,0 +1,14 @@
defaults:
- default.yaml
- default_hmr.yaml
# use "ddp_spawn" instead of "ddp",
# it's slower but normal "ddp" currently doesn't work ideally with hydra
# https://github.com/facebookresearch/hydra/issues/2070
# https://pytorch-lightning.readthedocs.io/en/latest/accelerators/gpu_intermediate.html#distributed-data-parallel-spawn
strategy: ddp
accelerator: gpu
devices: 8
num_nodes: 1
sync_batchnorm: True
@@ -0,0 +1,10 @@
_target_: pytorch_lightning.Trainer
default_root_dir: ${paths.output_dir}
accelerator: cpu
devices: 1
# set True to to ensure deterministic results
# makes training slower but gives more reproducibility than just setting seeds
deterministic: False
@@ -0,0 +1,8 @@
num_sanity_val_steps: 0
log_every_n_steps: ${GENERAL.LOG_STEPS}
val_check_interval: ${GENERAL.VAL_STEPS}
precision: 16
max_steps: ${GENERAL.TOTAL_STEPS}
# move_metrics_to_cpu: True
limit_val_batches: 1
# track_grad_norm: -1
@@ -0,0 +1,6 @@
defaults:
- default.yaml
- default_hmr.yaml
accelerator: gpu
devices: 1
@@ -0,0 +1,6 @@
defaults:
- default.yaml
- default_hmr.yaml
accelerator: mps
devices: 1
+88
View File
@@ -0,0 +1,88 @@
from typing import Dict, Optional
import torch
import numpy as np
import pytorch_lightning as pl
from yacs.config import CfgNode
import webdataset as wds
from ..configs import to_lower
from .dataset import Dataset
from .image_dataset import ImageDataset
from .mocap_dataset import MoCapDataset
def create_dataset(cfg: CfgNode, dataset_cfg: CfgNode, train: bool = True, **kwargs) -> Dataset:
"""
Instantiate a dataset from a config file.
Args:
cfg (CfgNode): Model configuration file.
dataset_cfg (CfgNode): Dataset configuration info.
train (bool): Variable to select between train and val datasets.
"""
dataset_type = Dataset.registry[dataset_cfg.TYPE]
return dataset_type(cfg, **to_lower(dataset_cfg), train=train, **kwargs)
def create_webdataset(cfg: CfgNode, dataset_cfg: CfgNode, train: bool = True) -> Dataset:
"""
Like `create_dataset` but load data from tars.
"""
dataset_type = Dataset.registry[dataset_cfg.TYPE]
return dataset_type.load_tars_as_webdataset(cfg, **to_lower(dataset_cfg), train=train)
class MixedWebDataset(wds.WebDataset):
def __init__(self, cfg: CfgNode, dataset_cfg: CfgNode, train: bool = True) -> None:
super(wds.WebDataset, self).__init__()
dataset_list = cfg.DATASETS.TRAIN if train else cfg.DATASETS.VAL
datasets = [create_webdataset(cfg, dataset_cfg[dataset], train=train) for dataset, v in dataset_list.items()]
weights = np.array([v.WEIGHT for dataset, v in dataset_list.items()])
weights = weights / weights.sum() # normalize
self.append(wds.RandomMix(datasets, weights))
class HMR2DataModule(pl.LightningDataModule):
def __init__(self, cfg: CfgNode, dataset_cfg: CfgNode) -> None:
"""
Initialize LightningDataModule for HMR2 training
Args:
cfg (CfgNode): Config file as a yacs CfgNode containing necessary dataset info.
dataset_cfg (CfgNode): Dataset configuration file
"""
super().__init__()
self.cfg = cfg
self.dataset_cfg = dataset_cfg
self.train_dataset = None
self.val_dataset = None
self.test_dataset = None
self.mocap_dataset = None
def setup(self, stage: Optional[str] = None) -> None:
"""
Load datasets necessary for training
Args:
cfg (CfgNode): Config file as a yacs CfgNode containing necessary dataset info.
"""
if self.train_dataset == None:
self.train_dataset = MixedWebDataset(self.cfg, self.dataset_cfg, train=True).with_epoch(100_000).shuffle(4000)
self.val_dataset = MixedWebDataset(self.cfg, self.dataset_cfg, train=False).shuffle(4000)
self.mocap_dataset = MoCapDataset(**to_lower(self.dataset_cfg[self.cfg.DATASETS.MOCAP]))
def train_dataloader(self) -> Dict:
"""
Setup training data loader.
Returns:
Dict: Dictionary containing image and mocap data dataloaders
"""
train_dataloader = torch.utils.data.DataLoader(self.train_dataset, self.cfg.TRAIN.BATCH_SIZE, drop_last=True, num_workers=self.cfg.GENERAL.NUM_WORKERS, prefetch_factor=self.cfg.GENERAL.PREFETCH_FACTOR)
mocap_dataloader = torch.utils.data.DataLoader(self.mocap_dataset, self.cfg.TRAIN.NUM_TRAIN_SAMPLES * self.cfg.TRAIN.BATCH_SIZE, shuffle=True, drop_last=True, num_workers=1)
return {'img': train_dataloader, 'mocap': mocap_dataloader}
def val_dataloader(self) -> torch.utils.data.DataLoader:
"""
Setup val data loader.
Returns:
torch.utils.data.DataLoader: Validation dataloader
"""
val_dataloader = torch.utils.data.DataLoader(self.val_dataset, self.cfg.TRAIN.BATCH_SIZE, drop_last=True, num_workers=self.cfg.GENERAL.NUM_WORKERS)
return val_dataloader
+27
View File
@@ -0,0 +1,27 @@
"""
This file contains the defition of the base Dataset class.
"""
class DatasetRegistration(type):
"""
Metaclass for registering different datasets
"""
def __init__(cls, name, bases, nmspc):
super().__init__(name, bases, nmspc)
if not hasattr(cls, 'registry'):
cls.registry = dict()
cls.registry[name] = cls
# Metamethods, called on class objects:
def __iter__(cls):
return iter(cls.registry)
def __str__(cls):
return str(cls.registry)
class Dataset(metaclass=DatasetRegistration):
"""
Base Dataset class
"""
def __init__(self, *args, **kwargs):
pass
+454
View File
@@ -0,0 +1,454 @@
import copy
import os
import numpy as np
import torch
from typing import Any, Dict, List
from yacs.config import CfgNode
import braceexpand
import cv2
from .dataset import Dataset
from .utils import get_example, expand_to_aspect_ratio
from .smplh_prob_filter import poses_check_probable, load_amass_hist_smooth
def expand(s):
return os.path.expanduser(os.path.expandvars(s))
def expand_urls(urls: str|List[str]):
if isinstance(urls, str):
urls = [urls]
urls = [u for url in urls for u in braceexpand.braceexpand(expand(url))]
return urls
AIC_TRAIN_CORRUPT_KEYS = {
'0a047f0124ae48f8eee15a9506ce1449ee1ba669',
'1a703aa174450c02fbc9cfbf578a5435ef403689',
'0394e6dc4df78042929b891dbc24f0fd7ffb6b6d',
'5c032b9626e410441544c7669123ecc4ae077058',
'ca018a7b4c5f53494006ebeeff9b4c0917a55f07',
'4a77adb695bef75a5d34c04d589baf646fe2ba35',
'a0689017b1065c664daef4ae2d14ea03d543217e',
'39596a45cbd21bed4a5f9c2342505532f8ec5cbb',
'3d33283b40610d87db660b62982f797d50a7366b',
}
CORRUPT_KEYS = {
*{f'aic-train/{k}' for k in AIC_TRAIN_CORRUPT_KEYS},
*{f'aic-train-vitpose/{k}' for k in AIC_TRAIN_CORRUPT_KEYS},
}
body_permutation = [0, 1, 5, 6, 7, 2, 3, 4, 8, 12, 13, 14, 9, 10, 11, 16, 15, 18, 17, 22, 23, 24, 19, 20, 21]
extra_permutation = [5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7, 6, 12, 13, 14, 15, 16, 17, 18]
FLIP_KEYPOINT_PERMUTATION = body_permutation + [25 + i for i in extra_permutation]
DEFAULT_MEAN = 255. * np.array([0.485, 0.456, 0.406])
DEFAULT_STD = 255. * np.array([0.229, 0.224, 0.225])
DEFAULT_IMG_SIZE = 256
class ImageDataset(Dataset):
def __init__(self,
cfg: CfgNode,
dataset_file: str,
img_dir: str,
train: bool = True,
prune: Dict[str, Any] = {},
**kwargs):
"""
Dataset class used for loading images and corresponding annotations.
Args:
cfg (CfgNode): Model config file.
dataset_file (str): Path to npz file containing dataset info.
img_dir (str): Path to image folder.
train (bool): Whether it is for training or not (enables data augmentation).
"""
super(ImageDataset, self).__init__()
self.train = train
self.cfg = cfg
self.img_size = cfg.MODEL.IMAGE_SIZE
self.mean = 255. * np.array(self.cfg.MODEL.IMAGE_MEAN)
self.std = 255. * np.array(self.cfg.MODEL.IMAGE_STD)
self.img_dir = img_dir
self.data = np.load(dataset_file, allow_pickle=True)
self.imgname = self.data['imgname']
self.personid = np.zeros(len(self.imgname), dtype=np.int32)
self.extra_info = self.data.get('extra_info', [{} for _ in range(len(self.imgname))])
self.flip_keypoint_permutation = copy.copy(FLIP_KEYPOINT_PERMUTATION)
num_pose = 3 * (self.cfg.SMPL.NUM_BODY_JOINTS + 1)
# Bounding boxes are assumed to be in the center and scale format
self.center = self.data['center']
self.scale = self.data['scale'].reshape(len(self.center), -1) / 200.0
if self.scale.shape[1] == 1:
self.scale = np.tile(self.scale, (1, 2))
assert self.scale.shape == (len(self.center), 2)
# Get gt SMPLX parameters, if available
try:
self.body_pose = self.data['body_pose'].astype(np.float32)
self.has_body_pose = self.data['has_body_pose'].astype(np.float32)
except KeyError:
self.body_pose = np.zeros((len(self.imgname), num_pose), dtype=np.float32)
self.has_body_pose = np.zeros(len(self.imgname), dtype=np.float32)
try:
self.betas = self.data['betas'].astype(np.float32)
self.has_betas = self.data['has_betas'].astype(np.float32)
except KeyError:
self.betas = np.zeros((len(self.imgname), 10), dtype=np.float32)
self.has_betas = np.zeros(len(self.imgname), dtype=np.float32)
# Try to get 2d keypoints, if available
try:
body_keypoints_2d = self.data['body_keypoints_2d']
except KeyError:
body_keypoints_2d = np.zeros((len(self.center), 25, 3))
# Try to get extra 2d keypoints, if available
try:
extra_keypoints_2d = self.data['extra_keypoints_2d']
except KeyError:
extra_keypoints_2d = np.zeros((len(self.center), 19, 3))
self.keypoints_2d = np.concatenate((body_keypoints_2d, extra_keypoints_2d), axis=1).astype(np.float32)
# Try to get 3d keypoints, if available
try:
body_keypoints_3d = self.data['body_keypoints_3d'].astype(np.float32)
except KeyError:
body_keypoints_3d = np.zeros((len(self.center), 25, 4), dtype=np.float32)
# Try to get extra 3d keypoints, if available
try:
extra_keypoints_3d = self.data['extra_keypoints_3d'].astype(np.float32)
except KeyError:
extra_keypoints_3d = np.zeros((len(self.center), 19, 4), dtype=np.float32)
body_keypoints_3d[:, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], -1] = 0
self.keypoints_3d = np.concatenate((body_keypoints_3d, extra_keypoints_3d), axis=1).astype(np.float32)
def __len__(self) -> int:
return len(self.scale)
def __getitem__(self, idx: int) -> Dict:
"""
Returns an example from the dataset.
"""
try:
image_file_rel = self.imgname[idx].decode('utf-8')
except AttributeError:
image_file_rel = self.imgname[idx]
image_file = os.path.join(self.img_dir, image_file_rel)
keypoints_2d = self.keypoints_2d[idx].copy()
keypoints_3d = self.keypoints_3d[idx].copy()
center = self.center[idx].copy()
center_x = center[0]
center_y = center[1]
scale = self.scale[idx]
BBOX_SHAPE = self.cfg.MODEL.get('BBOX_SHAPE', None)
bbox_size = expand_to_aspect_ratio(scale*200, target_aspect_ratio=BBOX_SHAPE).max()
bbox_expand_factor = bbox_size / ((scale*200).max())
body_pose = self.body_pose[idx].copy().astype(np.float32)
betas = self.betas[idx].copy().astype(np.float32)
has_body_pose = self.has_body_pose[idx].copy()
has_betas = self.has_betas[idx].copy()
smpl_params = {'global_orient': body_pose[:3],
'body_pose': body_pose[3:],
'betas': betas
}
has_smpl_params = {'global_orient': has_body_pose,
'body_pose': has_body_pose,
'betas': has_betas
}
smpl_params_is_axis_angle = {'global_orient': True,
'body_pose': True,
'betas': False
}
augm_config = self.cfg.DATASETS.CONFIG
# Crop image and (possibly) perform data augmentation
img_patch, keypoints_2d, keypoints_3d, smpl_params, has_smpl_params, img_size = get_example(image_file,
center_x, center_y,
bbox_size, bbox_size,
keypoints_2d, keypoints_3d,
smpl_params, has_smpl_params,
self.flip_keypoint_permutation,
self.img_size, self.img_size,
self.mean, self.std, self.train, augm_config)
item = {}
# These are the keypoints in the original image coordinates (before cropping)
orig_keypoints_2d = self.keypoints_2d[idx].copy()
item['img'] = img_patch
item['keypoints_2d'] = keypoints_2d.astype(np.float32)
item['keypoints_3d'] = keypoints_3d.astype(np.float32)
item['orig_keypoints_2d'] = orig_keypoints_2d
item['box_center'] = self.center[idx].copy()
item['box_size'] = bbox_size
item['bbox_expand_factor'] = bbox_expand_factor
item['img_size'] = 1.0 * img_size[::-1].copy()
item['smpl_params'] = smpl_params
item['has_smpl_params'] = has_smpl_params
item['smpl_params_is_axis_angle'] = smpl_params_is_axis_angle
item['imgname'] = image_file
item['imgname_rel'] = image_file_rel
item['personid'] = int(self.personid[idx])
item['extra_info'] = copy.deepcopy(self.extra_info[idx])
item['idx'] = idx
item['_scale'] = scale
return item
@staticmethod
def load_tars_as_webdataset(cfg: CfgNode, urls: str|List[str], train: bool,
resampled=False,
epoch_size=None,
cache_dir=None,
**kwargs) -> Dataset:
"""
Loads the dataset from a webdataset tar file.
"""
IMG_SIZE = cfg.MODEL.IMAGE_SIZE
BBOX_SHAPE = cfg.MODEL.get('BBOX_SHAPE', None)
MEAN = 255. * np.array(cfg.MODEL.IMAGE_MEAN)
STD = 255. * np.array(cfg.MODEL.IMAGE_STD)
def split_data(source):
for item in source:
datas = item['data.pyd']
for data in datas:
if 'detection.npz' in item:
det_idx = data['extra_info']['detection_npz_idx']
mask = item['detection.npz']['masks'][det_idx]
else:
mask = np.ones_like(item['jpg'][:,:,0], dtype=bool)
yield {
'__key__': item['__key__'],
'jpg': item['jpg'],
'data.pyd': data,
'mask': mask,
}
def suppress_bad_kps(item, thresh=0.0):
if thresh > 0:
kp2d = item['data.pyd']['keypoints_2d']
kp2d_conf = np.where(kp2d[:, 2] < thresh, 0.0, kp2d[:, 2])
item['data.pyd']['keypoints_2d'] = np.concatenate([kp2d[:,:2], kp2d_conf[:,None]], axis=1)
return item
def filter_numkp(item, numkp=4, thresh=0.0):
kp_conf = item['data.pyd']['keypoints_2d'][:, 2]
return (kp_conf > thresh).sum() > numkp
def filter_reproj_error(item, thresh=10**4.5):
losses = item['data.pyd'].get('extra_info', {}).get('fitting_loss', np.array({})).item()
reproj_loss = losses.get('reprojection_loss', None)
return reproj_loss is None or reproj_loss < thresh
def filter_bbox_size(item, thresh=1):
bbox_size_min = item['data.pyd']['scale'].min().item() * 200.
return bbox_size_min > thresh
def filter_no_poses(item):
return (item['data.pyd']['has_body_pose'] > 0)
def supress_bad_betas(item, thresh=3):
has_betas = item['data.pyd']['has_betas']
if thresh > 0 and has_betas:
betas_abs = np.abs(item['data.pyd']['betas'])
if (betas_abs > thresh).any():
item['data.pyd']['has_betas'] = False
return item
amass_poses_hist100_smooth = load_amass_hist_smooth()
def supress_bad_poses(item):
has_body_pose = item['data.pyd']['has_body_pose']
if has_body_pose:
body_pose = item['data.pyd']['body_pose']
pose_is_probable = poses_check_probable(torch.from_numpy(body_pose)[None, 3:], amass_poses_hist100_smooth).item()
if not pose_is_probable:
item['data.pyd']['has_body_pose'] = False
return item
def poses_betas_simultaneous(item):
# We either have both body_pose and betas, or neither
has_betas = item['data.pyd']['has_betas']
has_body_pose = item['data.pyd']['has_body_pose']
item['data.pyd']['has_betas'] = item['data.pyd']['has_body_pose'] = np.array(float((has_body_pose>0) and (has_betas>0)))
return item
def set_betas_for_reg(item):
# Always have betas set to true
has_betas = item['data.pyd']['has_betas']
betas = item['data.pyd']['betas']
if not (has_betas>0):
item['data.pyd']['has_betas'] = np.array(float((True)))
item['data.pyd']['betas'] = betas * 0
return item
# Load the dataset
if epoch_size is not None:
resampled = True
corrupt_filter = lambda sample: (sample['__key__'] not in CORRUPT_KEYS)
import webdataset as wds
dataset = wds.WebDataset(expand_urls(urls),
nodesplitter=wds.split_by_node,
shardshuffle=True,
resampled=resampled,
cache_dir=cache_dir,
).select(corrupt_filter)
if train:
dataset = dataset.shuffle(100)
dataset = dataset.decode('rgb8').rename(jpg='jpg;jpeg;png')
# Process the dataset
dataset = dataset.compose(split_data)
# Filter/clean the dataset
SUPPRESS_KP_CONF_THRESH = cfg.DATASETS.get('SUPPRESS_KP_CONF_THRESH', 0.0)
SUPPRESS_BETAS_THRESH = cfg.DATASETS.get('SUPPRESS_BETAS_THRESH', 0.0)
SUPPRESS_BAD_POSES = cfg.DATASETS.get('SUPPRESS_BAD_POSES', False)
POSES_BETAS_SIMULTANEOUS = cfg.DATASETS.get('POSES_BETAS_SIMULTANEOUS', False)
BETAS_REG = cfg.DATASETS.get('BETAS_REG', False)
FILTER_NO_POSES = cfg.DATASETS.get('FILTER_NO_POSES', False)
FILTER_NUM_KP = cfg.DATASETS.get('FILTER_NUM_KP', 4)
FILTER_NUM_KP_THRESH = cfg.DATASETS.get('FILTER_NUM_KP_THRESH', 0.0)
FILTER_REPROJ_THRESH = cfg.DATASETS.get('FILTER_REPROJ_THRESH', 0.0)
FILTER_MIN_BBOX_SIZE = cfg.DATASETS.get('FILTER_MIN_BBOX_SIZE', 0.0)
if SUPPRESS_KP_CONF_THRESH > 0:
dataset = dataset.map(lambda x: suppress_bad_kps(x, thresh=SUPPRESS_KP_CONF_THRESH))
if SUPPRESS_BETAS_THRESH > 0:
dataset = dataset.map(lambda x: supress_bad_betas(x, thresh=SUPPRESS_BETAS_THRESH))
if SUPPRESS_BAD_POSES:
dataset = dataset.map(lambda x: supress_bad_poses(x))
if POSES_BETAS_SIMULTANEOUS:
dataset = dataset.map(lambda x: poses_betas_simultaneous(x))
if FILTER_NO_POSES:
dataset = dataset.select(lambda x: filter_no_poses(x))
if FILTER_NUM_KP > 0:
dataset = dataset.select(lambda x: filter_numkp(x, numkp=FILTER_NUM_KP, thresh=FILTER_NUM_KP_THRESH))
if FILTER_REPROJ_THRESH > 0:
dataset = dataset.select(lambda x: filter_reproj_error(x, thresh=FILTER_REPROJ_THRESH))
if FILTER_MIN_BBOX_SIZE > 0:
dataset = dataset.select(lambda x: filter_bbox_size(x, thresh=FILTER_MIN_BBOX_SIZE))
if BETAS_REG:
dataset = dataset.map(lambda x: set_betas_for_reg(x)) # NOTE: Must be at the end
use_skimage_antialias = cfg.DATASETS.get('USE_SKIMAGE_ANTIALIAS', False)
border_mode = {
'constant': cv2.BORDER_CONSTANT,
'replicate': cv2.BORDER_REPLICATE,
}[cfg.DATASETS.get('BORDER_MODE', 'constant')]
# Process the dataset further
dataset = dataset.map(lambda x: ImageDataset.process_webdataset_tar_item(x, train,
augm_config=cfg.DATASETS.CONFIG,
MEAN=MEAN, STD=STD, IMG_SIZE=IMG_SIZE,
BBOX_SHAPE=BBOX_SHAPE,
use_skimage_antialias=use_skimage_antialias,
border_mode=border_mode,
))
if epoch_size is not None:
dataset = dataset.with_epoch(epoch_size)
return dataset
@staticmethod
def process_webdataset_tar_item(item, train,
augm_config=None,
MEAN=DEFAULT_MEAN,
STD=DEFAULT_STD,
IMG_SIZE=DEFAULT_IMG_SIZE,
BBOX_SHAPE=None,
use_skimage_antialias=False,
border_mode=cv2.BORDER_CONSTANT,
):
# Read data from item
key = item['__key__']
image = item['jpg']
data = item['data.pyd']
mask = item['mask']
keypoints_2d = data['keypoints_2d']
keypoints_3d = data['keypoints_3d']
center = data['center']
scale = data['scale']
body_pose = data['body_pose']
betas = data['betas']
has_body_pose = data['has_body_pose']
has_betas = data['has_betas']
# image_file = data['image_file']
# Process data
orig_keypoints_2d = keypoints_2d.copy()
center_x = center[0]
center_y = center[1]
bbox_size = expand_to_aspect_ratio(scale*200, target_aspect_ratio=BBOX_SHAPE).max()
if bbox_size < 1:
breakpoint()
smpl_params = {'global_orient': body_pose[:3],
'body_pose': body_pose[3:],
'betas': betas
}
has_smpl_params = {'global_orient': has_body_pose,
'body_pose': has_body_pose,
'betas': has_betas
}
smpl_params_is_axis_angle = {'global_orient': True,
'body_pose': True,
'betas': False
}
augm_config = copy.deepcopy(augm_config)
# Crop image and (possibly) perform data augmentation
img_rgba = np.concatenate([image, mask.astype(np.uint8)[:,:,None]*255], axis=2)
img_patch_rgba, keypoints_2d, keypoints_3d, smpl_params, has_smpl_params, img_size, trans = get_example(img_rgba,
center_x, center_y,
bbox_size, bbox_size,
keypoints_2d, keypoints_3d,
smpl_params, has_smpl_params,
FLIP_KEYPOINT_PERMUTATION,
IMG_SIZE, IMG_SIZE,
MEAN, STD, train, augm_config,
is_bgr=False, return_trans=True,
use_skimage_antialias=use_skimage_antialias,
border_mode=border_mode,
)
img_patch = img_patch_rgba[:3,:,:]
mask_patch = (img_patch_rgba[3,:,:] / 255.0).clip(0,1)
if (mask_patch < 0.5).all():
mask_patch = np.ones_like(mask_patch)
item = {}
item['img'] = img_patch
item['mask'] = mask_patch
# item['img_og'] = image
# item['mask_og'] = mask
item['keypoints_2d'] = keypoints_2d.astype(np.float32)
item['keypoints_3d'] = keypoints_3d.astype(np.float32)
item['orig_keypoints_2d'] = orig_keypoints_2d
item['box_center'] = center.copy()
item['box_size'] = bbox_size
item['img_size'] = 1.0 * img_size[::-1].copy()
item['smpl_params'] = smpl_params
item['has_smpl_params'] = has_smpl_params
item['smpl_params_is_axis_angle'] = smpl_params_is_axis_angle
item['_scale'] = scale
item['_trans'] = trans
item['imgname'] = key
# item['idx'] = idx
return item
+25
View File
@@ -0,0 +1,25 @@
import numpy as np
from typing import Dict
class MoCapDataset:
def __init__(self, dataset_file: str):
"""
Dataset class used for loading a dataset of unpaired SMPL parameter annotations
Args:
cfg (CfgNode): Model config file.
dataset_file (str): Path to npz file containing dataset info.
"""
data = np.load(dataset_file)
self.pose = data['body_pose'].astype(np.float32)[:, 3:]
self.betas = data['betas'].astype(np.float32)
self.length = len(self.pose)
def __getitem__(self, idx: int) -> Dict:
pose = self.pose[idx].copy()
betas = self.betas[idx].copy()
item = {'body_pose': pose, 'betas': betas}
return item
def __len__(self) -> int:
return self.length
@@ -0,0 +1,74 @@
# Adapted from https://raw.githubusercontent.com/nkolot/SPIN/master/datasets/preprocess/hr_lspet.py
import os
import glob
import numpy as np
import scipy.io as sio
# from .read_openpose import read_openpose
def hr_lspet_extract(dataset_path, out_path):
# training mode
png_path = os.path.join(dataset_path, '*.png')
imgs = glob.glob(png_path)
imgs.sort()
# structs we use
imgnames_, scales_, centers_, parts_, openposes_= [], [], [], [], []
# scale factor
scaleFactor = 1.2
# annotation files
annot_file = os.path.join(dataset_path, 'joints.mat')
joints = sio.loadmat(annot_file)['joints']
# main loop
for i, imgname in enumerate(imgs):
# image name
imgname = imgname.split('/')[-1]
# read keypoints
part14 = joints[:,:2,i]
# scale and center
bbox = [min(part14[:,0]), min(part14[:,1]),
max(part14[:,0]), max(part14[:,1])]
center = [(bbox[2]+bbox[0])/2, (bbox[3]+bbox[1])/2]
# scale = scaleFactor*max(bbox[2]-bbox[0], bbox[3]-bbox[1]) # Don't /200
scale = scaleFactor*np.array([bbox[2]-bbox[0], bbox[3]-bbox[1]]) # Don't /200
# update keypoints
part = np.zeros([24,3])
part[:14] = np.hstack([part14, np.ones([14,1])])
# # read openpose detections
# json_file = os.path.join(openpose_path, 'hrlspet',
# imgname.replace('.png', '_keypoints.json'))
# openpose = read_openpose(json_file, part, 'hrlspet')
# store the data
imgnames_.append(imgname)
centers_.append(center)
scales_.append(scale)
parts_.append(part)
# openposes_.append(openpose)
# Populate extra_keypoints_2d: N,19,3
# extra_keypoints_2d[:14] = parts[:14]
extra_keypoints_2d = np.zeros((len(parts_), 19, 3))
extra_keypoints_2d[:,:14,:] = np.stack(parts_)[:,:14,:3]
print(f'{extra_keypoints_2d.shape=}')
# store the data struct
if not os.path.isdir(out_path):
os.makedirs(out_path)
out_file = os.path.join(out_path, 'hr-lspet_train.npz')
np.savez(out_file, imgname=imgnames_,
center=centers_,
scale=scales_,
part=parts_,
extra_keypoints_2d=extra_keypoints_2d,
# openpose=openposes_
)
if __name__ == '__main__':
hr_lspet_extract('/shared/pavlakos/datasets/hr-lspet/', 'hmr2_evaluation_data/')
@@ -0,0 +1,92 @@
# Adapted from https://raw.githubusercontent.com/nkolot/SPIN/master/datasets/preprocess/coco.py
import os
from os.path import join
import sys
import json
import numpy as np
from pathlib import Path
# from .read_openpose import read_openpose
def coco_extract(dataset_path, out_path):
# # convert joints to global order
# joints_idx = [19, 20, 21, 22, 23, 9, 8, 10, 7, 11, 6, 3, 2, 4, 1, 5, 0]
# bbox expansion factor
scaleFactor = 1.2
# structs we need
imgnames_, scales_, centers_, parts_, openposes_ = [], [], [], [], []
# json annotation file
SPLIT='val'
json_paths = (Path(dataset_path)/'posetrack_data/annotations'/SPLIT).glob('*.json')
for json_path in json_paths:
json_data = json.load(open(json_path, 'r'))
imgs = {}
for img in json_data['images']:
imgs[img['id']] = img
for annot in json_data['annotations']:
# keypoints processing
keypoints = annot['keypoints']
keypoints = np.reshape(keypoints, (17,3))
keypoints[keypoints[:,2]>0,2] = 1
# check if all major body joints are annotated
if sum(keypoints[5:,2]>0) < 12:
continue
# image name
image_id = annot['image_id']
img_name = str(imgs[image_id]['file_name'])
# img_name_full = f'images/{SPLIT}/{json_path.stem}/{img_name}'
img_name_full = img_name
# keypoints
part = np.zeros([17,3])
# part[joints_idx] = keypoints
part = keypoints
# scale and center
bbox = annot['bbox']
center = [bbox[0] + bbox[2]/2, bbox[1] + bbox[3]/2]
# scale = scaleFactor*max(bbox[2], bbox[3]) # Don't do /200
scale = scaleFactor*np.array([bbox[2], bbox[3]]) # Don't /200
# # read openpose detections
# json_file = os.path.join(openpose_path, 'coco',
# img_name.replace('.jpg', '_keypoints.json'))
# openpose = read_openpose(json_file, part, 'coco')
# store data
imgnames_.append(img_name_full)
centers_.append(center)
scales_.append(scale)
parts_.append(part)
# openposes_.append(openpose)
# NOTE: Posetrack val doesn't annotate ears (17,18)
# But Posetrack does annotate head, neck so that wil have to live in extra_kps.
posetrack_to_op_extra = [0, 37, 38, 18, 17, 5, 2, 6, 3, 7, 4, 12, 9, 13, 10, 14, 11] # Will contain 15 keypoints.
all_keypoints_2d = np.zeros((len(parts_), 44, 3))
all_keypoints_2d[:,posetrack_to_op_extra] = np.stack(parts_)[:,:len(posetrack_to_op_extra),:3]
body_keypoints_2d = all_keypoints_2d[:,:25,:]
extra_keypoints_2d = all_keypoints_2d[:,25:,:]
print(f'{extra_keypoints_2d.shape=}')
# store the data struct
if not os.path.isdir(out_path):
os.makedirs(out_path)
out_file = os.path.join(out_path, f'posetrack_2018_{SPLIT}.npz')
np.savez(out_file, imgname=imgnames_,
center=centers_,
scale=scales_,
part=parts_,
body_keypoints_2d=body_keypoints_2d,
extra_keypoints_2d=extra_keypoints_2d,
# openpose=openposes_
)
if __name__ == '__main__':
coco_extract('/shared/pavlakos/datasets/posetrack/posetrack2018/', 'hmr2_evaluation_data/')
+154
View File
@@ -0,0 +1,154 @@
import os
import numpy as np
import torch
import torch.nn.functional as F
JOINT_NAMES = [
'left_hip',
'right_hip',
'spine1',
'left_knee',
'right_knee',
'spine2',
'left_ankle',
'right_ankle',
'spine3',
'left_foot',
'right_foot',
'neck',
'left_collar',
'right_collar',
'head',
'left_shoulder',
'right_shoulder',
'left_elbow',
'right_elbow',
'left_wrist',
'right_wrist'
]
# Manually chosen probability density thresholds for each joint
# Probablities computed using SIGMA=2 gaussian blur on AMASS pose 3D histogram for range (-pi,pi) with 100x100x100 bins
JOINT_NAME_PROB_THRESHOLDS = {
'left_hip': 5e-5,
'right_hip': 5e-5,
'spine1': 2e-3,
'left_knee': 5e-6,
'right_knee': 5e-6,
'spine2': 0.01,
'left_ankle': 5e-6,
'right_ankle': 5e-6,
'spine3': 0.025,
'left_foot': 0,
'right_foot': 0,
'neck': 2e-4,
'left_collar': 4.5e-4 ,
'right_collar': 4.5e-4,
'head': 5e-4,
'left_shoulder': 2e-4,
'right_shoulder': 2e-4,
'left_elbow': 4e-5,
'right_elbow': 4e-5,
'left_wrist': 1e-3,
'right_wrist': 1e-3,
}
JOINT_IDX_PROB_THRESHOLDS = torch.tensor([JOINT_NAME_PROB_THRESHOLDS[joint_name] for joint_name in JOINT_NAMES])
###################################################################
POSE_RANGE_MIN = -np.pi
POSE_RANGE_MAX = np.pi
# Create 21x100x100x100 histogram of all 21 AMASS body pose joints using `create_pose_hist(amass_poses, nbins=100)`
AMASS_HIST100_PATH = 'hmr2_training_data/amass_poses_hist100_SMPL+H_G.npy'
if not os.path.exists(AMASS_HIST100_PATH):
AMASS_HIST100_PATH = '/shared/shubham/code/hmr2023/amass_poses_hist100_SMPL+H_G.npy'
if not os.path.exists(AMASS_HIST100_PATH):
AMASS_HIST100_PATH = '/fsx/shubham/code/stable-humans/notebooks/amass_poses_hist100_SMPL+H_G.npy'
def create_pose_hist(poses: np.ndarray, nbins: int = 100) -> np.ndarray:
N,K,C = poses.shape
assert C==3, poses.shape
poses_21x3 = normalize_axis_angle(torch.fromnumpy(poses).view(N*K,3)).numpy().reshape(N, K, 3)
assert (poses_21x3 > -np.pi).all() and (poses_21x3 < np.pi).all()
Hs, Es = [], []
for i in range(K):
H, edges = np.histogramdd(poses_21x3[:, i, :], bins=nbins, range=[(-np.pi, np.pi)]*3)
Hs.append(H)
Es.append(edges)
Hs = np.stack(Hs, axis=0)
return Hs
def load_amass_hist_smooth(sigma=2) -> torch.Tensor:
amass_poses_hist100 = np.load(AMASS_HIST100_PATH)
amass_poses_hist100 = torch.from_numpy(amass_poses_hist100)
assert amass_poses_hist100.shape == (21,100,100,100)
nbins = amass_poses_hist100.shape[1]
amass_poses_hist100 = amass_poses_hist100/amass_poses_hist100.sum() / (2*np.pi/nbins)**3
# Gaussian filter on amass_poses_hist100
from scipy.ndimage import gaussian_filter
amass_poses_hist100_smooth = gaussian_filter(amass_poses_hist100.numpy(), sigma=sigma, mode='constant')
amass_poses_hist100_smooth = torch.from_numpy(amass_poses_hist100_smooth)
return amass_poses_hist100_smooth
# Normalize axis angle representation s.t. angle is in [-pi, pi]
def normalize_axis_angle(poses: torch.Tensor) -> torch.Tensor:
# poses: N, 3
# print(f'normalize_axis_angle ...')
assert poses.shape[1] == 3, poses.shape
angle = poses.norm(dim=1)
axis = F.normalize(poses, p=2, dim=1, eps=1e-8)
angle_fixed = angle.clone()
axis_fixed = axis.clone()
eps = 1e-6
ii = 0
while True:
# print(f'normalize_axis_angle iter {ii}')
ii += 1
angle_too_big = (angle_fixed > np.pi + eps)
if not angle_too_big.any():
break
angle_fixed[angle_too_big] -= 2 * np.pi
angle_too_small = (angle_fixed < -eps)
axis_fixed[angle_too_small] *= -1
angle_fixed[angle_too_small] *= -1
return axis_fixed * angle_fixed[:,None]
def poses_to_joint_probs(poses: torch.Tensor, amass_poses_100_smooth: torch.Tensor) -> torch.Tensor:
# poses: Nx69
# amass_poses_100_smooth: 21xBINSxBINSxBINS
# returns: poses_prob: Nx21
N=poses.shape[0]
assert poses.shape == (N,69)
poses = poses[:,:63].reshape(N*21,3)
nbins = amass_poses_100_smooth.shape[1]
assert amass_poses_100_smooth.shape == (21,nbins,nbins,nbins)
poses_bin = (poses - POSE_RANGE_MIN) / (POSE_RANGE_MAX - POSE_RANGE_MIN) * (nbins - 1e-6)
poses_bin = poses_bin.long().clip(0, nbins-1)
joint_id = torch.arange(21, device=poses.device).view(1,21).expand(N,21).reshape(N*21)
poses_prob = amass_poses_100_smooth[joint_id, poses_bin[:,0], poses_bin[:,1], poses_bin[:,2]]
poses_bad = ((poses < POSE_RANGE_MIN) | (poses >= POSE_RANGE_MAX)).any(dim=1)
poses_prob[poses_bad] = 0
return poses_prob.view(N,21)
def poses_check_probable(
poses: torch.Tensor,
amass_poses_100_smooth: torch.Tensor,
prob_thresholds: torch.Tensor = JOINT_IDX_PROB_THRESHOLDS
) -> torch.Tensor:
N,C=poses.shape
poses_norm = normalize_axis_angle(poses.reshape(N*(C//3),3)).reshape(N,C)
poses_prob = poses_to_joint_probs(poses_norm, amass_poses_100_smooth)
return (poses_prob > prob_thresholds).all(dim=1)
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
from typing import Dict
import cv2
import numpy as np
from skimage.filters import gaussian
from yacs.config import CfgNode
import torch
from .utils import (convert_cvimg_to_tensor,
expand_to_aspect_ratio,
generate_image_patch_cv2)
DEFAULT_MEAN = 255. * np.array([0.485, 0.456, 0.406])
DEFAULT_STD = 255. * np.array([0.229, 0.224, 0.225])
class ViTDetDataset(torch.utils.data.Dataset):
def __init__(self,
cfg: CfgNode,
img_cv2: np.array,
boxes: np.array,
train: bool = False,
**kwargs):
super().__init__()
self.cfg = cfg
self.img_cv2 = img_cv2
# self.boxes = boxes
assert train == False, "ViTDetDataset is only for inference"
self.train = train
self.img_size = cfg.MODEL.IMAGE_SIZE
self.mean = 255. * np.array(self.cfg.MODEL.IMAGE_MEAN)
self.std = 255. * np.array(self.cfg.MODEL.IMAGE_STD)
# Preprocess annotations
boxes = boxes.astype(np.float32)
self.center = (boxes[:, 2:4] + boxes[:, 0:2]) / 2.0
self.scale = (boxes[:, 2:4] - boxes[:, 0:2]) / 200.0
self.personid = np.arange(len(boxes), dtype=np.int32)
def __len__(self) -> int:
return len(self.personid)
def __getitem__(self, idx: int) -> Dict[str, np.array]:
center = self.center[idx].copy()
center_x = center[0]
center_y = center[1]
scale = self.scale[idx]
BBOX_SHAPE = self.cfg.MODEL.get('BBOX_SHAPE', None)
bbox_size = expand_to_aspect_ratio(scale*200, target_aspect_ratio=BBOX_SHAPE).max()
patch_width = patch_height = self.img_size
# 3. generate image patch
# if use_skimage_antialias:
cvimg = self.img_cv2.copy()
if True:
# Blur image to avoid aliasing artifacts
downsampling_factor = ((bbox_size*1.0) / patch_width)
print(f'{downsampling_factor=}')
downsampling_factor = downsampling_factor / 2.0
if downsampling_factor > 1.1:
cvimg = gaussian(cvimg, sigma=(downsampling_factor-1)/2, channel_axis=2, preserve_range=True)
img_patch_cv, trans = generate_image_patch_cv2(cvimg,
center_x, center_y,
bbox_size, bbox_size,
patch_width, patch_height,
False, 1.0, 0,
border_mode=cv2.BORDER_CONSTANT)
img_patch_cv = img_patch_cv[:, :, ::-1]
img_patch = convert_cvimg_to_tensor(img_patch_cv)
# apply normalization
for n_c in range(min(self.img_cv2.shape[2], 3)):
img_patch[n_c, :, :] = (img_patch[n_c, :, :] - self.mean[n_c]) / self.std[n_c]
item = {
'img': img_patch,
'personid': int(self.personid[idx]),
}
item['box_center'] = self.center[idx].copy()
item['box_size'] = bbox_size
item['img_size'] = 1.0 * np.array([cvimg.shape[1], cvimg.shape[0]])
return item
+94
View File
@@ -0,0 +1,94 @@
import os
from .smpl_wrapper import SMPL
from .hmr2 import HMR2
from .discriminator import Discriminator
from ..utils.download import cache_url
from ..configs import CACHE_DIR_4DHUMANS
from .... import REPO_PATH
def download_models(folder=CACHE_DIR_4DHUMANS):
"""Download checkpoints and files for running inference.
"""
os.makedirs(folder, exist_ok=True)
download_files = {
"hmr2_data.tar.gz" : ["https://people.eecs.berkeley.edu/~jathushan/projects/4dhumans/hmr2_data.tar.gz", folder],
}
for file_name, url in download_files.items():
output_path = os.path.join(url[1], file_name)
if not os.path.exists(output_path):
print("Downloading file: " + file_name)
# output = gdown.cached_download(url[0], output_path, fuzzy=True)
output = cache_url(url[0], output_path)
assert os.path.exists(output_path), f"{output} does not exist"
# if ends with tar.gz, tar -xzf
if file_name.endswith(".tar.gz"):
print("Extracting file: " + file_name)
os.system("tar -xvf " + output_path + " -C " + url[1])
def check_smpl_exists(smpl_dir):
import os
candidates = [
f'{smpl_dir}/SMPL_NEUTRAL.pkl',
f'{smpl_dir}/basicModel_neutral_lbs_10_207_0_v1.0.0.pkl',
]
candidates_exist = [os.path.exists(c) for c in candidates]
if not any(candidates_exist):
raise FileNotFoundError(f"SMPL model not found. Please download it from https://smplify.is.tue.mpg.de/ and place it at {candidates[1]}")
# Code edxpects SMPL model at CACHE_DIR_4DHUMANS/data/smpl/SMPL_NEUTRAL.pkl. Copy there if needed
if (not candidates_exist[0]) and candidates_exist[1]:
convert_pkl(candidates[1], candidates[0])
return True
# Convert SMPL pkl file to be compatible with Python 3
# Script is from https://rebeccabilbro.github.io/convert-py2-pickles-to-py3/
def convert_pkl(old_pkl, new_pkl):
"""
Convert a Python 2 pickle to Python 3
"""
import dill
import pickle
# Convert Python 2 "ObjectType" to Python 3 object
dill._dill._reverse_typemap["ObjectType"] = object
# Open the pickle using latin1 encoding
with open(old_pkl, "rb") as f:
loaded = pickle.load(f, encoding="latin1")
# Re-save as Python 3 pickle
with open(new_pkl, "wb") as outfile:
pickle.dump(loaded, outfile)
def load_hmr2(checkpoint_path, smpl_dir):
from pathlib import Path
from ..configs import get_config
model_cfg = os.path.join(REPO_PATH, 'configs', 'model_config.yaml')
model_cfg = get_config(model_cfg, update_cachedir=True)
model_cfg.SMPL.MODEL_PATH = smpl_dir
model_cfg.SMPL.JOINT_REGRESSION_EXTRA = os.path.join(smpl_dir, 'SMPL_to_J19.pkl')
model_cfg.SMPL.JOINT_REGRESSOR_EXTRA = os.path.join(smpl_dir, 'SMPL_to_J19.pkl')
model_cfg.SMPL.MEAN_PARAMS = os.path.join(smpl_dir, 'smpl_mean_params.npz')
model_cfg.freeze()
# Override some config values, to crop bbox correctly
if (model_cfg.MODEL.BACKBONE.TYPE == 'vit') and ('BBOX_SHAPE' not in model_cfg.MODEL):
model_cfg.defrost()
assert model_cfg.MODEL.IMAGE_SIZE == 256, f"MODEL.IMAGE_SIZE ({model_cfg.MODEL.IMAGE_SIZE}) should be 256 for ViT backbone"
model_cfg.MODEL.BBOX_SHAPE = [192,256]
model_cfg.freeze()
# Ensure SMPL model exists
check_smpl_exists(smpl_dir)
model = HMR2.load_from_checkpoint(checkpoint_path, strict=False, cfg=model_cfg)
return model, model_cfg
@@ -0,0 +1,7 @@
from .vit import vit
def create_backbone(cfg):
if cfg.MODEL.BACKBONE.TYPE == 'vit':
return vit(cfg)
else:
raise NotImplementedError('Backbone type is not implemented')
+348
View File
@@ -0,0 +1,348 @@
# Copyright (c) OpenMMLab. All rights reserved.
import math
import torch
from functools import partial
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
from timm.models.layers import drop_path, to_2tuple, trunc_normal_
def vit(cfg):
return ViT(
img_size=(256, 192),
patch_size=16,
embed_dim=1280,
depth=32,
num_heads=16,
ratio=1,
use_checkpoint=False,
mlp_ratio=4,
qkv_bias=True,
drop_path_rate=0.55,
)
def get_abs_pos(abs_pos, h, w, ori_h, ori_w, has_cls_token=True):
"""
Calculate absolute positional embeddings. If needed, resize embeddings and remove cls_token
dimension for the original embeddings.
Args:
abs_pos (Tensor): absolute positional embeddings with (1, num_position, C).
has_cls_token (bool): If true, has 1 embedding in abs_pos for cls token.
hw (Tuple): size of input image tokens.
Returns:
Absolute positional embeddings after processing with shape (1, H, W, C)
"""
cls_token = None
B, L, C = abs_pos.shape
if has_cls_token:
cls_token = abs_pos[:, 0:1]
abs_pos = abs_pos[:, 1:]
if ori_h != h or ori_w != w:
new_abs_pos = F.interpolate(
abs_pos.reshape(1, ori_h, ori_w, -1).permute(0, 3, 1, 2),
size=(h, w),
mode="bicubic",
align_corners=False,
).permute(0, 2, 3, 1).reshape(B, -1, C)
else:
new_abs_pos = abs_pos
if cls_token is not None:
new_abs_pos = torch.cat([cls_token, new_abs_pos], dim=1)
return new_abs_pos
class DropPath(nn.Module):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
"""
def __init__(self, drop_prob=None):
super(DropPath, self).__init__()
self.drop_prob = drop_prob
def forward(self, x):
return drop_path(x, self.drop_prob, self.training)
def extra_repr(self):
return 'p={}'.format(self.drop_prob)
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Linear(in_features, hidden_features)
self.act = act_layer()
self.fc2 = nn.Linear(hidden_features, out_features)
self.drop = nn.Dropout(drop)
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.fc2(x)
x = self.drop(x)
return x
class Attention(nn.Module):
def __init__(
self, dim, num_heads=8, qkv_bias=False, qk_scale=None, attn_drop=0.,
proj_drop=0., attn_head_dim=None,):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.dim = dim
if attn_head_dim is not None:
head_dim = attn_head_dim
all_head_dim = head_dim * self.num_heads
self.scale = qk_scale or head_dim ** -0.5
self.qkv = nn.Linear(dim, all_head_dim * 3, bias=qkv_bias)
self.attn_drop = nn.Dropout(attn_drop)
self.proj = nn.Linear(all_head_dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
def forward(self, x):
B, N, C = x.shape
qkv = self.qkv(x)
qkv = qkv.reshape(B, N, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple)
q = q * self.scale
attn = (q @ k.transpose(-2, -1))
attn = attn.softmax(dim=-1)
attn = self.attn_drop(attn)
x = (attn @ v).transpose(1, 2).reshape(B, N, -1)
x = self.proj(x)
x = self.proj_drop(x)
return x
class Block(nn.Module):
def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False, qk_scale=None,
drop=0., attn_drop=0., drop_path=0., act_layer=nn.GELU,
norm_layer=nn.LayerNorm, attn_head_dim=None
):
super().__init__()
self.norm1 = norm_layer(dim)
self.attn = Attention(
dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale,
attn_drop=attn_drop, proj_drop=drop, attn_head_dim=attn_head_dim
)
# NOTE: drop path for stochastic depth, we shall see if this is better than dropout here
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
self.norm2 = norm_layer(dim)
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
def forward(self, x):
x = x + self.drop_path(self.attn(self.norm1(x)))
x = x + self.drop_path(self.mlp(self.norm2(x)))
return x
class PatchEmbed(nn.Module):
""" Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768, ratio=1):
super().__init__()
img_size = to_2tuple(img_size)
patch_size = to_2tuple(patch_size)
num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // patch_size[0]) * (ratio ** 2)
self.patch_shape = (int(img_size[0] // patch_size[0] * ratio), int(img_size[1] // patch_size[1] * ratio))
self.origin_patch_shape = (int(img_size[0] // patch_size[0]), int(img_size[1] // patch_size[1]))
self.img_size = img_size
self.patch_size = patch_size
self.num_patches = num_patches
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=(patch_size[0] // ratio), padding=4 + 2 * (ratio//2-1))
def forward(self, x, **kwargs):
B, C, H, W = x.shape
x = self.proj(x)
Hp, Wp = x.shape[2], x.shape[3]
x = x.flatten(2).transpose(1, 2)
return x, (Hp, Wp)
class HybridEmbed(nn.Module):
""" CNN Feature Map Embedding
Extract feature map from CNN, flatten, project to embedding dim.
"""
def __init__(self, backbone, img_size=224, feature_size=None, in_chans=3, embed_dim=768):
super().__init__()
assert isinstance(backbone, nn.Module)
img_size = to_2tuple(img_size)
self.img_size = img_size
self.backbone = backbone
if feature_size is None:
with torch.no_grad():
training = backbone.training
if training:
backbone.eval()
o = self.backbone(torch.zeros(1, in_chans, img_size[0], img_size[1]))[-1]
feature_size = o.shape[-2:]
feature_dim = o.shape[1]
backbone.train(training)
else:
feature_size = to_2tuple(feature_size)
feature_dim = self.backbone.feature_info.channels()[-1]
self.num_patches = feature_size[0] * feature_size[1]
self.proj = nn.Linear(feature_dim, embed_dim)
def forward(self, x):
x = self.backbone(x)[-1]
x = x.flatten(2).transpose(1, 2)
x = self.proj(x)
return x
class ViT(nn.Module):
def __init__(self,
img_size=224, patch_size=16, in_chans=3, num_classes=80, embed_dim=768, depth=12,
num_heads=12, mlp_ratio=4., qkv_bias=False, qk_scale=None, drop_rate=0., attn_drop_rate=0.,
drop_path_rate=0., hybrid_backbone=None, norm_layer=None, use_checkpoint=False,
frozen_stages=-1, ratio=1, last_norm=True,
patch_padding='pad', freeze_attn=False, freeze_ffn=False,
):
# Protect mutable default arguments
super(ViT, self).__init__()
norm_layer = norm_layer or partial(nn.LayerNorm, eps=1e-6)
self.num_classes = num_classes
self.num_features = self.embed_dim = embed_dim # num_features for consistency with other models
self.frozen_stages = frozen_stages
self.use_checkpoint = use_checkpoint
self.patch_padding = patch_padding
self.freeze_attn = freeze_attn
self.freeze_ffn = freeze_ffn
self.depth = depth
if hybrid_backbone is not None:
self.patch_embed = HybridEmbed(
hybrid_backbone, img_size=img_size, in_chans=in_chans, embed_dim=embed_dim)
else:
self.patch_embed = PatchEmbed(
img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim, ratio=ratio)
num_patches = self.patch_embed.num_patches
# since the pretraining model has class token
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule
self.blocks = nn.ModuleList([
Block(
dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale,
drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[i], norm_layer=norm_layer,
)
for i in range(depth)])
self.last_norm = norm_layer(embed_dim) if last_norm else nn.Identity()
if self.pos_embed is not None:
trunc_normal_(self.pos_embed, std=.02)
self._freeze_stages()
def _freeze_stages(self):
"""Freeze parameters."""
if self.frozen_stages >= 0:
self.patch_embed.eval()
for param in self.patch_embed.parameters():
param.requires_grad = False
for i in range(1, self.frozen_stages + 1):
m = self.blocks[i]
m.eval()
for param in m.parameters():
param.requires_grad = False
if self.freeze_attn:
for i in range(0, self.depth):
m = self.blocks[i]
m.attn.eval()
m.norm1.eval()
for param in m.attn.parameters():
param.requires_grad = False
for param in m.norm1.parameters():
param.requires_grad = False
if self.freeze_ffn:
self.pos_embed.requires_grad = False
self.patch_embed.eval()
for param in self.patch_embed.parameters():
param.requires_grad = False
for i in range(0, self.depth):
m = self.blocks[i]
m.mlp.eval()
m.norm2.eval()
for param in m.mlp.parameters():
param.requires_grad = False
for param in m.norm2.parameters():
param.requires_grad = False
def init_weights(self):
"""Initialize the weights in backbone.
Args:
pretrained (str, optional): Path to pre-trained weights.
Defaults to None.
"""
def _init_weights(m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
self.apply(_init_weights)
def get_num_layers(self):
return len(self.blocks)
@torch.jit.ignore
def no_weight_decay(self):
return {'pos_embed', 'cls_token'}
def forward_features(self, x):
B, C, H, W = x.shape
x, (Hp, Wp) = self.patch_embed(x)
if self.pos_embed is not None:
# fit for multiple GPU training
# since the first element for pos embed (sin-cos manner) is zero, it will cause no difference
x = x + self.pos_embed[:, 1:] + self.pos_embed[:, :1]
for blk in self.blocks:
if self.use_checkpoint:
x = checkpoint.checkpoint(blk, x)
else:
x = blk(x)
x = self.last_norm(x)
xp = x.permute(0, 2, 1).reshape(B, -1, Hp, Wp).contiguous()
return xp
def forward(self, x):
x = self.forward_features(x)
return x
def train(self, mode=True):
"""Convert the model into training mode."""
super().train(mode)
self._freeze_stages()
@@ -0,0 +1,17 @@
# import mmcv
# import mmpose
# from mmpose.models import build_posenet
# from mmcv.runner import load_checkpoint
# from pathlib import Path
# def vit(cfg):
# vitpose_dir = Path(mmpose.__file__).parent.parent
# config = f'{vitpose_dir}/configs/body/2d_kpt_sview_rgb_img/topdown_heatmap/coco/ViTPose_huge_coco_256x192.py'
# # checkpoint = f'{vitpose_dir}/models/vitpose-h-multi-coco.pth'
# config = mmcv.Config.fromfile(config)
# config.model.pretrained = None
# model = build_posenet(config.model)
# # load_checkpoint(model, checkpoint, map_location='cpu')
# return model.backbone
@@ -0,0 +1,358 @@
from inspect import isfunction
from typing import Callable, Optional
import torch
from einops import rearrange
from einops.layers.torch import Rearrange
from torch import nn
from .t_cond_mlp import (
AdaptiveLayerNorm1D,
FrequencyEmbedder,
normalization_layer,
)
# from .vit import Attention, FeedForward
def exists(val):
return val is not None
def default(val, d):
if exists(val):
return val
return d() if isfunction(d) else d
class PreNorm(nn.Module):
def __init__(self, dim: int, fn: Callable, norm: str = "layer", norm_cond_dim: int = -1):
super().__init__()
self.norm = normalization_layer(norm, dim, norm_cond_dim)
self.fn = fn
def forward(self, x: torch.Tensor, *args, **kwargs):
if isinstance(self.norm, AdaptiveLayerNorm1D):
return self.fn(self.norm(x, *args), **kwargs)
else:
return self.fn(self.norm(x), **kwargs)
class FeedForward(nn.Module):
def __init__(self, dim, hidden_dim, dropout=0.0):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim, hidden_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, dim),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Attention(nn.Module):
def __init__(self, dim, heads=8, dim_head=64, dropout=0.0):
super().__init__()
inner_dim = dim_head * heads
project_out = not (heads == 1 and dim_head == dim)
self.heads = heads
self.scale = dim_head**-0.5
self.attend = nn.Softmax(dim=-1)
self.dropout = nn.Dropout(dropout)
self.to_qkv = nn.Linear(dim, inner_dim * 3, bias=False)
self.to_out = (
nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout))
if project_out
else nn.Identity()
)
def forward(self, x):
qkv = self.to_qkv(x).chunk(3, dim=-1)
q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=self.heads), qkv)
dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
attn = self.attend(dots)
attn = self.dropout(attn)
out = torch.matmul(attn, v)
out = rearrange(out, "b h n d -> b n (h d)")
return self.to_out(out)
class CrossAttention(nn.Module):
def __init__(self, dim, context_dim=None, heads=8, dim_head=64, dropout=0.0):
super().__init__()
inner_dim = dim_head * heads
project_out = not (heads == 1 and dim_head == dim)
self.heads = heads
self.scale = dim_head**-0.5
self.attend = nn.Softmax(dim=-1)
self.dropout = nn.Dropout(dropout)
context_dim = default(context_dim, dim)
self.to_kv = nn.Linear(context_dim, inner_dim * 2, bias=False)
self.to_q = nn.Linear(dim, inner_dim, bias=False)
self.to_out = (
nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout))
if project_out
else nn.Identity()
)
def forward(self, x, context=None):
context = default(context, x)
k, v = self.to_kv(context).chunk(2, dim=-1)
q = self.to_q(x)
q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=self.heads), [q, k, v])
dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
attn = self.attend(dots)
attn = self.dropout(attn)
out = torch.matmul(attn, v)
out = rearrange(out, "b h n d -> b n (h d)")
return self.to_out(out)
class Transformer(nn.Module):
def __init__(
self,
dim: int,
depth: int,
heads: int,
dim_head: int,
mlp_dim: int,
dropout: float = 0.0,
norm: str = "layer",
norm_cond_dim: int = -1,
):
super().__init__()
self.layers = nn.ModuleList([])
for _ in range(depth):
sa = Attention(dim, heads=heads, dim_head=dim_head, dropout=dropout)
ff = FeedForward(dim, mlp_dim, dropout=dropout)
self.layers.append(
nn.ModuleList(
[
PreNorm(dim, sa, norm=norm, norm_cond_dim=norm_cond_dim),
PreNorm(dim, ff, norm=norm, norm_cond_dim=norm_cond_dim),
]
)
)
def forward(self, x: torch.Tensor, *args):
for attn, ff in self.layers:
x = attn(x, *args) + x
x = ff(x, *args) + x
return x
class TransformerCrossAttn(nn.Module):
def __init__(
self,
dim: int,
depth: int,
heads: int,
dim_head: int,
mlp_dim: int,
dropout: float = 0.0,
norm: str = "layer",
norm_cond_dim: int = -1,
context_dim: Optional[int] = None,
):
super().__init__()
self.layers = nn.ModuleList([])
for _ in range(depth):
sa = Attention(dim, heads=heads, dim_head=dim_head, dropout=dropout)
ca = CrossAttention(
dim, context_dim=context_dim, heads=heads, dim_head=dim_head, dropout=dropout
)
ff = FeedForward(dim, mlp_dim, dropout=dropout)
self.layers.append(
nn.ModuleList(
[
PreNorm(dim, sa, norm=norm, norm_cond_dim=norm_cond_dim),
PreNorm(dim, ca, norm=norm, norm_cond_dim=norm_cond_dim),
PreNorm(dim, ff, norm=norm, norm_cond_dim=norm_cond_dim),
]
)
)
def forward(self, x: torch.Tensor, *args, context=None, context_list=None):
if context_list is None:
context_list = [context] * len(self.layers)
if len(context_list) != len(self.layers):
raise ValueError(f"len(context_list) != len(self.layers) ({len(context_list)} != {len(self.layers)})")
for i, (self_attn, cross_attn, ff) in enumerate(self.layers):
x = self_attn(x, *args) + x
x = cross_attn(x, *args, context=context_list[i]) + x
x = ff(x, *args) + x
return x
class DropTokenDropout(nn.Module):
def __init__(self, p: float = 0.1):
super().__init__()
if p < 0 or p > 1:
raise ValueError(
"dropout probability has to be between 0 and 1, " "but got {}".format(p)
)
self.p = p
def forward(self, x: torch.Tensor):
# x: (batch_size, seq_len, dim)
if self.training and self.p > 0:
zero_mask = torch.full_like(x[0, :, 0], self.p).bernoulli().bool()
# TODO: permutation idx for each batch using torch.argsort
if zero_mask.any():
x = x[:, ~zero_mask, :]
return x
class ZeroTokenDropout(nn.Module):
def __init__(self, p: float = 0.1):
super().__init__()
if p < 0 or p > 1:
raise ValueError(
"dropout probability has to be between 0 and 1, " "but got {}".format(p)
)
self.p = p
def forward(self, x: torch.Tensor):
# x: (batch_size, seq_len, dim)
if self.training and self.p > 0:
zero_mask = torch.full_like(x[:, :, 0], self.p).bernoulli().bool()
# Zero-out the masked tokens
x[zero_mask, :] = 0
return x
class TransformerEncoder(nn.Module):
def __init__(
self,
num_tokens: int,
token_dim: int,
dim: int,
depth: int,
heads: int,
mlp_dim: int,
dim_head: int = 64,
dropout: float = 0.0,
emb_dropout: float = 0.0,
emb_dropout_type: str = "drop",
emb_dropout_loc: str = "token",
norm: str = "layer",
norm_cond_dim: int = -1,
token_pe_numfreq: int = -1,
):
super().__init__()
if token_pe_numfreq > 0:
token_dim_new = token_dim * (2 * token_pe_numfreq + 1)
self.to_token_embedding = nn.Sequential(
Rearrange("b n d -> (b n) d", n=num_tokens, d=token_dim),
FrequencyEmbedder(token_pe_numfreq, token_pe_numfreq - 1),
Rearrange("(b n) d -> b n d", n=num_tokens, d=token_dim_new),
nn.Linear(token_dim_new, dim),
)
else:
self.to_token_embedding = nn.Linear(token_dim, dim)
self.pos_embedding = nn.Parameter(torch.randn(1, num_tokens, dim))
if emb_dropout_type == "drop":
self.dropout = DropTokenDropout(emb_dropout)
elif emb_dropout_type == "zero":
self.dropout = ZeroTokenDropout(emb_dropout)
else:
raise ValueError(f"Unknown emb_dropout_type: {emb_dropout_type}")
self.emb_dropout_loc = emb_dropout_loc
self.transformer = Transformer(
dim, depth, heads, dim_head, mlp_dim, dropout, norm=norm, norm_cond_dim=norm_cond_dim
)
def forward(self, inp: torch.Tensor, *args, **kwargs):
x = inp
if self.emb_dropout_loc == "input":
x = self.dropout(x)
x = self.to_token_embedding(x)
if self.emb_dropout_loc == "token":
x = self.dropout(x)
b, n, _ = x.shape
x += self.pos_embedding[:, :n]
if self.emb_dropout_loc == "token_afterpos":
x = self.dropout(x)
x = self.transformer(x, *args)
return x
class TransformerDecoder(nn.Module):
def __init__(
self,
num_tokens: int,
token_dim: int,
dim: int,
depth: int,
heads: int,
mlp_dim: int,
dim_head: int = 64,
dropout: float = 0.0,
emb_dropout: float = 0.0,
emb_dropout_type: str = 'drop',
norm: str = "layer",
norm_cond_dim: int = -1,
context_dim: Optional[int] = None,
skip_token_embedding: bool = False,
):
super().__init__()
if not skip_token_embedding:
self.to_token_embedding = nn.Linear(token_dim, dim)
else:
self.to_token_embedding = nn.Identity()
if token_dim != dim:
raise ValueError(
f"token_dim ({token_dim}) != dim ({dim}) when skip_token_embedding is True"
)
self.pos_embedding = nn.Parameter(torch.randn(1, num_tokens, dim))
if emb_dropout_type == "drop":
self.dropout = DropTokenDropout(emb_dropout)
elif emb_dropout_type == "zero":
self.dropout = ZeroTokenDropout(emb_dropout)
elif emb_dropout_type == "normal":
self.dropout = nn.Dropout(emb_dropout)
self.transformer = TransformerCrossAttn(
dim,
depth,
heads,
dim_head,
mlp_dim,
dropout,
norm=norm,
norm_cond_dim=norm_cond_dim,
context_dim=context_dim,
)
def forward(self, inp: torch.Tensor, *args, context=None, context_list=None):
x = self.to_token_embedding(inp)
b, n, _ = x.shape
x = self.dropout(x)
x += self.pos_embedding[:, :n]
x = self.transformer(x, *args, context=context, context_list=context_list)
return x
@@ -0,0 +1,199 @@
import copy
from typing import List, Optional
import torch
class AdaptiveLayerNorm1D(torch.nn.Module):
def __init__(self, data_dim: int, norm_cond_dim: int):
super().__init__()
if data_dim <= 0:
raise ValueError(f"data_dim must be positive, but got {data_dim}")
if norm_cond_dim <= 0:
raise ValueError(f"norm_cond_dim must be positive, but got {norm_cond_dim}")
self.norm = torch.nn.LayerNorm(
data_dim
) # TODO: Check if elementwise_affine=True is correct
self.linear = torch.nn.Linear(norm_cond_dim, 2 * data_dim)
torch.nn.init.zeros_(self.linear.weight)
torch.nn.init.zeros_(self.linear.bias)
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
# x: (batch, ..., data_dim)
# t: (batch, norm_cond_dim)
# return: (batch, data_dim)
x = self.norm(x)
alpha, beta = self.linear(t).chunk(2, dim=-1)
# Add singleton dimensions to alpha and beta
if x.dim() > 2:
alpha = alpha.view(alpha.shape[0], *([1] * (x.dim() - 2)), alpha.shape[1])
beta = beta.view(beta.shape[0], *([1] * (x.dim() - 2)), beta.shape[1])
return x * (1 + alpha) + beta
class SequentialCond(torch.nn.Sequential):
def forward(self, input, *args, **kwargs):
for module in self:
if isinstance(module, (AdaptiveLayerNorm1D, SequentialCond, ResidualMLPBlock)):
# print(f'Passing on args to {module}', [a.shape for a in args])
input = module(input, *args, **kwargs)
else:
# print(f'Skipping passing args to {module}', [a.shape for a in args])
input = module(input)
return input
def normalization_layer(norm: Optional[str], dim: int, norm_cond_dim: int = -1):
if norm == "batch":
return torch.nn.BatchNorm1d(dim)
elif norm == "layer":
return torch.nn.LayerNorm(dim)
elif norm == "ada":
assert norm_cond_dim > 0, f"norm_cond_dim must be positive, got {norm_cond_dim}"
return AdaptiveLayerNorm1D(dim, norm_cond_dim)
elif norm is None:
return torch.nn.Identity()
else:
raise ValueError(f"Unknown norm: {norm}")
def linear_norm_activ_dropout(
input_dim: int,
output_dim: int,
activation: torch.nn.Module = torch.nn.ReLU(),
bias: bool = True,
norm: Optional[str] = "layer", # Options: ada/batch/layer
dropout: float = 0.0,
norm_cond_dim: int = -1,
) -> SequentialCond:
layers = []
layers.append(torch.nn.Linear(input_dim, output_dim, bias=bias))
if norm is not None:
layers.append(normalization_layer(norm, output_dim, norm_cond_dim))
layers.append(copy.deepcopy(activation))
if dropout > 0.0:
layers.append(torch.nn.Dropout(dropout))
return SequentialCond(*layers)
def create_simple_mlp(
input_dim: int,
hidden_dims: List[int],
output_dim: int,
activation: torch.nn.Module = torch.nn.ReLU(),
bias: bool = True,
norm: Optional[str] = "layer", # Options: ada/batch/layer
dropout: float = 0.0,
norm_cond_dim: int = -1,
) -> SequentialCond:
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.extend(
linear_norm_activ_dropout(
prev_dim, hidden_dim, activation, bias, norm, dropout, norm_cond_dim
)
)
prev_dim = hidden_dim
layers.append(torch.nn.Linear(prev_dim, output_dim, bias=bias))
return SequentialCond(*layers)
class ResidualMLPBlock(torch.nn.Module):
def __init__(
self,
input_dim: int,
hidden_dim: int,
num_hidden_layers: int,
output_dim: int,
activation: torch.nn.Module = torch.nn.ReLU(),
bias: bool = True,
norm: Optional[str] = "layer", # Options: ada/batch/layer
dropout: float = 0.0,
norm_cond_dim: int = -1,
):
super().__init__()
if not (input_dim == output_dim == hidden_dim):
raise NotImplementedError(
f"input_dim {input_dim} != output_dim {output_dim} is not implemented"
)
layers = []
prev_dim = input_dim
for i in range(num_hidden_layers):
layers.append(
linear_norm_activ_dropout(
prev_dim, hidden_dim, activation, bias, norm, dropout, norm_cond_dim
)
)
prev_dim = hidden_dim
self.model = SequentialCond(*layers)
self.skip = torch.nn.Identity()
def forward(self, x: torch.Tensor, *args, **kwargs) -> torch.Tensor:
return x + self.model(x, *args, **kwargs)
class ResidualMLP(torch.nn.Module):
def __init__(
self,
input_dim: int,
hidden_dim: int,
num_hidden_layers: int,
output_dim: int,
activation: torch.nn.Module = torch.nn.ReLU(),
bias: bool = True,
norm: Optional[str] = "layer", # Options: ada/batch/layer
dropout: float = 0.0,
num_blocks: int = 1,
norm_cond_dim: int = -1,
):
super().__init__()
self.input_dim = input_dim
self.model = SequentialCond(
linear_norm_activ_dropout(
input_dim, hidden_dim, activation, bias, norm, dropout, norm_cond_dim
),
*[
ResidualMLPBlock(
hidden_dim,
hidden_dim,
num_hidden_layers,
hidden_dim,
activation,
bias,
norm,
dropout,
norm_cond_dim,
)
for _ in range(num_blocks)
],
torch.nn.Linear(hidden_dim, output_dim, bias=bias),
)
def forward(self, x: torch.Tensor, *args, **kwargs) -> torch.Tensor:
return self.model(x, *args, **kwargs)
class FrequencyEmbedder(torch.nn.Module):
def __init__(self, num_frequencies, max_freq_log2):
super().__init__()
frequencies = 2 ** torch.linspace(0, max_freq_log2, steps=num_frequencies)
self.register_buffer("frequencies", frequencies)
def forward(self, x):
# x should be of size (N,) or (N, D)
N = x.size(0)
if x.dim() == 1: # (N,)
x = x.unsqueeze(1) # (N, D) where D=1
x_unsqueezed = x.unsqueeze(-1) # (N, D, 1)
scaled = self.frequencies.view(1, 1, -1) * x_unsqueezed # (N, D, num_frequencies)
s = torch.sin(scaled)
c = torch.cos(scaled)
embedded = torch.cat([s, c, x_unsqueezed], dim=-1).view(
N, -1
) # (N, D * 2 * num_frequencies + D)
return embedded
+99
View File
@@ -0,0 +1,99 @@
import torch
import torch.nn as nn
class Discriminator(nn.Module):
def __init__(self):
"""
Pose + Shape discriminator proposed in HMR
"""
super(Discriminator, self).__init__()
self.num_joints = 23
# poses_alone
self.D_conv1 = nn.Conv2d(9, 32, kernel_size=1)
nn.init.xavier_uniform_(self.D_conv1.weight)
nn.init.zeros_(self.D_conv1.bias)
self.relu = nn.ReLU(inplace=True)
self.D_conv2 = nn.Conv2d(32, 32, kernel_size=1)
nn.init.xavier_uniform_(self.D_conv2.weight)
nn.init.zeros_(self.D_conv2.bias)
pose_out = []
for i in range(self.num_joints):
pose_out_temp = nn.Linear(32, 1)
nn.init.xavier_uniform_(pose_out_temp.weight)
nn.init.zeros_(pose_out_temp.bias)
pose_out.append(pose_out_temp)
self.pose_out = nn.ModuleList(pose_out)
# betas
self.betas_fc1 = nn.Linear(10, 10)
nn.init.xavier_uniform_(self.betas_fc1.weight)
nn.init.zeros_(self.betas_fc1.bias)
self.betas_fc2 = nn.Linear(10, 5)
nn.init.xavier_uniform_(self.betas_fc2.weight)
nn.init.zeros_(self.betas_fc2.bias)
self.betas_out = nn.Linear(5, 1)
nn.init.xavier_uniform_(self.betas_out.weight)
nn.init.zeros_(self.betas_out.bias)
# poses_joint
self.D_alljoints_fc1 = nn.Linear(32*self.num_joints, 1024)
nn.init.xavier_uniform_(self.D_alljoints_fc1.weight)
nn.init.zeros_(self.D_alljoints_fc1.bias)
self.D_alljoints_fc2 = nn.Linear(1024, 1024)
nn.init.xavier_uniform_(self.D_alljoints_fc2.weight)
nn.init.zeros_(self.D_alljoints_fc2.bias)
self.D_alljoints_out = nn.Linear(1024, 1)
nn.init.xavier_uniform_(self.D_alljoints_out.weight)
nn.init.zeros_(self.D_alljoints_out.bias)
def forward(self, poses: torch.Tensor, betas: torch.Tensor) -> torch.Tensor:
"""
Forward pass of the discriminator.
Args:
poses (torch.Tensor): Tensor of shape (B, 23, 3, 3) containing a batch of SMPL body poses (excluding the global orientation).
betas (torch.Tensor): Tensor of shape (B, 10) containign a batch of SMPL beta coefficients.
Returns:
torch.Tensor: Discriminator output with shape (B, 25)
"""
#import ipdb; ipdb.set_trace()
#bn = poses.shape[0]
# poses B x 207
#poses = poses.reshape(bn, -1)
# poses B x num_joints x 1 x 9
poses = poses.reshape(-1, self.num_joints, 1, 9)
bn = poses.shape[0]
# poses B x 9 x num_joints x 1
poses = poses.permute(0, 3, 1, 2).contiguous()
# poses_alone
poses = self.D_conv1(poses)
poses = self.relu(poses)
poses = self.D_conv2(poses)
poses = self.relu(poses)
poses_out = []
for i in range(self.num_joints):
poses_out_ = self.pose_out[i](poses[:, :, i, 0])
poses_out.append(poses_out_)
poses_out = torch.cat(poses_out, dim=1)
# betas
betas = self.betas_fc1(betas)
betas = self.relu(betas)
betas = self.betas_fc2(betas)
betas = self.relu(betas)
betas_out = self.betas_out(betas)
# poses_joint
poses = poses.reshape(bn,-1)
poses_all = self.D_alljoints_fc1(poses)
poses_all = self.relu(poses_all)
poses_all = self.D_alljoints_fc2(poses_all)
poses_all = self.relu(poses_all)
poses_all_out = self.D_alljoints_out(poses_all)
disc_out = torch.cat((poses_out, betas_out, poses_all_out), 1)
return disc_out
+1
View File
@@ -0,0 +1 @@
from .smpl_head import build_smpl_head
+111
View File
@@ -0,0 +1,111 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import einops
from ...utils.geometry import rot6d_to_rotmat, aa_to_rotmat
from ..components.pose_transformer import TransformerDecoder
def build_smpl_head(cfg):
smpl_head_type = cfg.MODEL.SMPL_HEAD.get('TYPE', 'hmr')
if smpl_head_type == 'transformer_decoder':
return SMPLTransformerDecoderHead(cfg)
else:
raise ValueError('Unknown SMPL head type: {}'.format(smpl_head_type))
class SMPLTransformerDecoderHead(nn.Module):
""" Cross-attention based SMPL Transformer decoder
"""
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.joint_rep_type = cfg.MODEL.SMPL_HEAD.get('JOINT_REP', '6d')
self.joint_rep_dim = {'6d': 6, 'aa': 3}[self.joint_rep_type]
npose = self.joint_rep_dim * (cfg.SMPL.NUM_BODY_JOINTS + 1)
self.npose = npose
self.input_is_mean_shape = cfg.MODEL.SMPL_HEAD.get('TRANSFORMER_INPUT', 'zero') == 'mean_shape'
transformer_args = dict(
num_tokens=1,
token_dim=(npose + 10 + 3) if self.input_is_mean_shape else 1,
dim=1024,
)
transformer_args = (transformer_args | dict(cfg.MODEL.SMPL_HEAD.TRANSFORMER_DECODER))
self.transformer = TransformerDecoder(
**transformer_args
)
dim=transformer_args['dim']
self.decpose = nn.Linear(dim, npose)
self.decshape = nn.Linear(dim, 10)
self.deccam = nn.Linear(dim, 3)
if cfg.MODEL.SMPL_HEAD.get('INIT_DECODER_XAVIER', False):
# True by default in MLP. False by default in Transformer
nn.init.xavier_uniform_(self.decpose.weight, gain=0.01)
nn.init.xavier_uniform_(self.decshape.weight, gain=0.01)
nn.init.xavier_uniform_(self.deccam.weight, gain=0.01)
mean_params = np.load(cfg.SMPL.MEAN_PARAMS)
init_body_pose = torch.from_numpy(mean_params['pose'].astype(np.float32)).unsqueeze(0)
init_betas = torch.from_numpy(mean_params['shape'].astype('float32')).unsqueeze(0)
init_cam = torch.from_numpy(mean_params['cam'].astype(np.float32)).unsqueeze(0)
self.register_buffer('init_body_pose', init_body_pose)
self.register_buffer('init_betas', init_betas)
self.register_buffer('init_cam', init_cam)
def forward(self, x, **kwargs):
batch_size = x.shape[0]
# vit pretrained backbone is channel-first. Change to token-first
x = einops.rearrange(x, 'b c h w -> b (h w) c')
init_body_pose = self.init_body_pose.expand(batch_size, -1)
init_betas = self.init_betas.expand(batch_size, -1)
init_cam = self.init_cam.expand(batch_size, -1)
# TODO: Convert init_body_pose to aa rep if needed
if self.joint_rep_type == 'aa':
raise NotImplementedError
pred_body_pose = init_body_pose
pred_betas = init_betas
pred_cam = init_cam
pred_body_pose_list = []
pred_betas_list = []
pred_cam_list = []
for i in range(self.cfg.MODEL.SMPL_HEAD.get('IEF_ITERS', 1)):
# Input token to transformer is zero token
if self.input_is_mean_shape:
token = torch.cat([pred_body_pose, pred_betas, pred_cam], dim=1)[:,None,:]
else:
token = torch.zeros(batch_size, 1, 1).to(x.device)
# Pass through transformer
token_out = self.transformer(token, context=x)
token_out = token_out.squeeze(1) # (B, C)
# Readout from token_out
pred_body_pose = self.decpose(token_out) + pred_body_pose
pred_betas = self.decshape(token_out) + pred_betas
pred_cam = self.deccam(token_out) + pred_cam
pred_body_pose_list.append(pred_body_pose)
pred_betas_list.append(pred_betas)
pred_cam_list.append(pred_cam)
# Convert self.joint_rep_type -> rotmat
joint_conversion_fn = {
'6d': rot6d_to_rotmat,
'aa': lambda x: aa_to_rotmat(x.view(-1, 3).contiguous())
}[self.joint_rep_type]
pred_smpl_params_list = {}
pred_smpl_params_list['body_pose'] = torch.cat([joint_conversion_fn(pbp).view(batch_size, -1, 3, 3)[:, 1:, :, :] for pbp in pred_body_pose_list], dim=0)
pred_smpl_params_list['betas'] = torch.cat(pred_betas_list, dim=0)
pred_smpl_params_list['cam'] = torch.cat(pred_cam_list, dim=0)
pred_body_pose = joint_conversion_fn(pred_body_pose).view(batch_size, self.cfg.SMPL.NUM_BODY_JOINTS+1, 3, 3)
pred_smpl_params = {'global_orient': pred_body_pose[:, [0]],
'body_pose': pred_body_pose[:, 1:],
'betas': pred_betas}
return pred_smpl_params, pred_cam, pred_smpl_params_list
+355
View File
@@ -0,0 +1,355 @@
import torch
import pytorch_lightning as pl
from typing import Any, Dict, Mapping, Tuple
from yacs.config import CfgNode
from ..utils import SkeletonRenderer, MeshRenderer
from ..utils.geometry import aa_to_rotmat, perspective_projection
from ..utils.pylogger import get_pylogger
from .backbones import create_backbone
from .heads import build_smpl_head
from .discriminator import Discriminator
from .losses import Keypoint3DLoss, Keypoint2DLoss, ParameterLoss
from . import SMPL
log = get_pylogger(__name__)
class HMR2(pl.LightningModule):
def __init__(self, cfg: CfgNode, init_renderer: bool = True):
"""
Setup HMR2 model
Args:
cfg (CfgNode): Config file as a yacs CfgNode
"""
super().__init__()
# Save hyperparameters
self.save_hyperparameters(logger=False, ignore=['init_renderer'])
self.cfg = cfg
# Create backbone feature extractor
self.backbone = create_backbone(cfg)
if cfg.MODEL.BACKBONE.get('PRETRAINED_WEIGHTS', None):
log.info(f'Loading backbone weights from {cfg.MODEL.BACKBONE.PRETRAINED_WEIGHTS}')
self.backbone.load_state_dict(torch.load(cfg.MODEL.BACKBONE.PRETRAINED_WEIGHTS, map_location='cpu')['state_dict'])
# Create SMPL head
self.smpl_head = build_smpl_head(cfg)
# Create discriminator
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
self.discriminator = Discriminator()
# Define loss functions
self.keypoint_3d_loss = Keypoint3DLoss(loss_type='l1')
self.keypoint_2d_loss = Keypoint2DLoss(loss_type='l1')
self.smpl_parameter_loss = ParameterLoss()
# Instantiate SMPL model
smpl_cfg = {k.lower(): v for k,v in dict(cfg.SMPL).items()}
self.smpl = SMPL(**smpl_cfg)
# Buffer that shows whetheer we need to initialize ActNorm layers
self.register_buffer('initialized', torch.tensor(False))
# Setup renderer for visualization
if init_renderer:
self.renderer = SkeletonRenderer(self.cfg)
self.mesh_renderer = MeshRenderer(self.cfg, faces=self.smpl.faces)
else:
self.renderer = None
self.mesh_renderer = None
# Disable automatic optimization since we use adversarial training
self.automatic_optimization = False
def get_parameters(self):
all_params = list(self.smpl_head.parameters())
all_params += list(self.backbone.parameters())
return all_params
def configure_optimizers(self) -> Tuple[torch.optim.Optimizer, torch.optim.Optimizer]:
"""
Setup model and distriminator Optimizers
Returns:
Tuple[torch.optim.Optimizer, torch.optim.Optimizer]: Model and discriminator optimizers
"""
param_groups = [{'params': filter(lambda p: p.requires_grad, self.get_parameters()), 'lr': self.cfg.TRAIN.LR}]
optimizer = torch.optim.AdamW(params=param_groups,
# lr=self.cfg.TRAIN.LR,
weight_decay=self.cfg.TRAIN.WEIGHT_DECAY)
optimizer_disc = torch.optim.AdamW(params=self.discriminator.parameters(),
lr=self.cfg.TRAIN.LR,
weight_decay=self.cfg.TRAIN.WEIGHT_DECAY)
return optimizer, optimizer_disc
def forward_step(self, batch: Dict, train: bool = False) -> Dict:
"""
Run a forward step of the network
Args:
batch (Dict): Dictionary containing batch data
train (bool): Flag indicating whether it is training or validation mode
Returns:
Dict: Dictionary containing the regression output
"""
# Use RGB image as input
x = batch['img']
batch_size = x.shape[0]
# Compute conditioning features using the backbone
# if using ViT backbone, we need to use a different aspect ratio
conditioning_feats = self.backbone(x[:,:,:,32:-32])
pred_smpl_params, pred_cam, _ = self.smpl_head(conditioning_feats)
# Store useful regression outputs to the output dict
output = {}
output['pred_cam'] = pred_cam
output['pred_smpl_params'] = {k: v.clone() for k,v in pred_smpl_params.items()}
# Compute camera translation
device = pred_smpl_params['body_pose'].device
dtype = pred_smpl_params['body_pose'].dtype
focal_length = self.cfg.EXTRA.FOCAL_LENGTH * torch.ones(batch_size, 2, device=device, dtype=dtype)
pred_cam_t = torch.stack([pred_cam[:, 1],
pred_cam[:, 2],
2*focal_length[:, 0]/(self.cfg.MODEL.IMAGE_SIZE * pred_cam[:, 0] +1e-9)],dim=-1)
output['pred_cam_t'] = pred_cam_t
output['focal_length'] = focal_length
# Compute model vertices, joints and the projected joints
pred_smpl_params['global_orient'] = pred_smpl_params['global_orient'].reshape(batch_size, -1, 3, 3)
pred_smpl_params['body_pose'] = pred_smpl_params['body_pose'].reshape(batch_size, -1, 3, 3)
pred_smpl_params['betas'] = pred_smpl_params['betas'].reshape(batch_size, -1)
smpl_output = self.smpl(**{k: v.float() for k,v in pred_smpl_params.items()}, pose2rot=False)
pred_keypoints_3d = smpl_output.joints
pred_vertices = smpl_output.vertices
output['pred_keypoints_3d'] = pred_keypoints_3d.reshape(batch_size, -1, 3)
output['pred_vertices'] = pred_vertices.reshape(batch_size, -1, 3)
pred_cam_t = pred_cam_t.reshape(-1, 3)
focal_length = focal_length.reshape(-1, 2)
pred_keypoints_2d = perspective_projection(pred_keypoints_3d,
translation=pred_cam_t,
focal_length=focal_length / self.cfg.MODEL.IMAGE_SIZE)
output['pred_keypoints_2d'] = pred_keypoints_2d.reshape(batch_size, -1, 2)
return output
def compute_loss(self, batch: Dict, output: Dict, train: bool = True) -> torch.Tensor:
"""
Compute losses given the input batch and the regression output
Args:
batch (Dict): Dictionary containing batch data
output (Dict): Dictionary containing the regression output
train (bool): Flag indicating whether it is training or validation mode
Returns:
torch.Tensor : Total loss for current batch
"""
pred_smpl_params = output['pred_smpl_params']
pred_keypoints_2d = output['pred_keypoints_2d']
pred_keypoints_3d = output['pred_keypoints_3d']
batch_size = pred_smpl_params['body_pose'].shape[0]
device = pred_smpl_params['body_pose'].device
dtype = pred_smpl_params['body_pose'].dtype
# Get annotations
gt_keypoints_2d = batch['keypoints_2d']
gt_keypoints_3d = batch['keypoints_3d']
gt_smpl_params = batch['smpl_params']
has_smpl_params = batch['has_smpl_params']
is_axis_angle = batch['smpl_params_is_axis_angle']
# Compute 3D keypoint loss
loss_keypoints_2d = self.keypoint_2d_loss(pred_keypoints_2d, gt_keypoints_2d)
loss_keypoints_3d = self.keypoint_3d_loss(pred_keypoints_3d, gt_keypoints_3d, pelvis_id=25+14)
# Compute loss on SMPL parameters
loss_smpl_params = {}
for k, pred in pred_smpl_params.items():
gt = gt_smpl_params[k].view(batch_size, -1)
if is_axis_angle[k].all():
gt = aa_to_rotmat(gt.reshape(-1, 3)).view(batch_size, -1, 3, 3)
has_gt = has_smpl_params[k]
loss_smpl_params[k] = self.smpl_parameter_loss(pred.reshape(batch_size, -1), gt.reshape(batch_size, -1), has_gt)
loss = self.cfg.LOSS_WEIGHTS['KEYPOINTS_3D'] * loss_keypoints_3d+\
self.cfg.LOSS_WEIGHTS['KEYPOINTS_2D'] * loss_keypoints_2d+\
sum([loss_smpl_params[k] * self.cfg.LOSS_WEIGHTS[k.upper()] for k in loss_smpl_params])
losses = dict(loss=loss.detach(),
loss_keypoints_2d=loss_keypoints_2d.detach(),
loss_keypoints_3d=loss_keypoints_3d.detach())
for k, v in loss_smpl_params.items():
losses['loss_' + k] = v.detach()
output['losses'] = losses
return loss
# Tensoroboard logging should run from first rank only
@pl.utilities.rank_zero.rank_zero_only
def tensorboard_logging(self, batch: Dict, output: Dict, step_count: int, train: bool = True, write_to_summary_writer: bool = True) -> None:
"""
Log results to Tensorboard
Args:
batch (Dict): Dictionary containing batch data
output (Dict): Dictionary containing the regression output
step_count (int): Global training step count
train (bool): Flag indicating whether it is training or validation mode
"""
mode = 'train' if train else 'val'
batch_size = batch['keypoints_2d'].shape[0]
images = batch['img']
images = images * torch.tensor([0.229, 0.224, 0.225], device=images.device).reshape(1,3,1,1)
images = images + torch.tensor([0.485, 0.456, 0.406], device=images.device).reshape(1,3,1,1)
#images = 255*images.permute(0, 2, 3, 1).cpu().numpy()
pred_keypoints_3d = output['pred_keypoints_3d'].detach().reshape(batch_size, -1, 3)
pred_vertices = output['pred_vertices'].detach().reshape(batch_size, -1, 3)
focal_length = output['focal_length'].detach().reshape(batch_size, 2)
gt_keypoints_3d = batch['keypoints_3d']
gt_keypoints_2d = batch['keypoints_2d']
losses = output['losses']
pred_cam_t = output['pred_cam_t'].detach().reshape(batch_size, 3)
pred_keypoints_2d = output['pred_keypoints_2d'].detach().reshape(batch_size, -1, 2)
if write_to_summary_writer:
summary_writer = self.logger.experiment
for loss_name, val in losses.items():
summary_writer.add_scalar(mode +'/' + loss_name, val.detach().item(), step_count)
num_images = min(batch_size, self.cfg.EXTRA.NUM_LOG_IMAGES)
gt_keypoints_3d = batch['keypoints_3d']
pred_keypoints_3d = output['pred_keypoints_3d'].detach().reshape(batch_size, -1, 3)
# We render the skeletons instead of the full mesh because rendering a lot of meshes will make the training slow.
#predictions = self.renderer(pred_keypoints_3d[:num_images],
# gt_keypoints_3d[:num_images],
# 2 * gt_keypoints_2d[:num_images],
# images=images[:num_images],
# camera_translation=pred_cam_t[:num_images])
predictions = self.mesh_renderer.visualize_tensorboard(pred_vertices[:num_images].cpu().numpy(),
pred_cam_t[:num_images].cpu().numpy(),
images[:num_images].cpu().numpy(),
pred_keypoints_2d[:num_images].cpu().numpy(),
gt_keypoints_2d[:num_images].cpu().numpy(),
focal_length=focal_length[:num_images].cpu().numpy())
if write_to_summary_writer:
summary_writer.add_image('%s/predictions' % mode, predictions, step_count)
return predictions
def forward(self, batch: Dict) -> Dict:
"""
Run a forward step of the network in val mode
Args:
batch (Dict): Dictionary containing batch data
Returns:
Dict: Dictionary containing the regression output
"""
return self.forward_step(batch, train=False)
def training_step_discriminator(self, batch: Dict,
body_pose: torch.Tensor,
betas: torch.Tensor,
optimizer: torch.optim.Optimizer) -> torch.Tensor:
"""
Run a discriminator training step
Args:
batch (Dict): Dictionary containing mocap batch data
body_pose (torch.Tensor): Regressed body pose from current step
betas (torch.Tensor): Regressed betas from current step
optimizer (torch.optim.Optimizer): Discriminator optimizer
Returns:
torch.Tensor: Discriminator loss
"""
batch_size = body_pose.shape[0]
gt_body_pose = batch['body_pose']
gt_betas = batch['betas']
gt_rotmat = aa_to_rotmat(gt_body_pose.view(-1,3)).view(batch_size, -1, 3, 3)
disc_fake_out = self.discriminator(body_pose.detach(), betas.detach())
loss_fake = ((disc_fake_out - 0.0) ** 2).sum() / batch_size
disc_real_out = self.discriminator(gt_rotmat, gt_betas)
loss_real = ((disc_real_out - 1.0) ** 2).sum() / batch_size
loss_disc = loss_fake + loss_real
loss = self.cfg.LOSS_WEIGHTS.ADVERSARIAL * loss_disc
optimizer.zero_grad()
self.manual_backward(loss)
optimizer.step()
return loss_disc.detach()
def training_step(self, joint_batch: Dict, batch_idx: int) -> Dict:
"""
Run a full training step
Args:
joint_batch (Dict): Dictionary containing image and mocap batch data
batch_idx (int): Unused.
batch_idx (torch.Tensor): Unused.
Returns:
Dict: Dictionary containing regression output.
"""
batch = joint_batch['img']
mocap_batch = joint_batch['mocap']
optimizer = self.optimizers(use_pl_optimizer=True)
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
optimizer, optimizer_disc = optimizer
batch_size = batch['img'].shape[0]
output = self.forward_step(batch, train=True)
pred_smpl_params = output['pred_smpl_params']
if self.cfg.get('UPDATE_GT_SPIN', False):
self.update_batch_gt_spin(batch, output)
loss = self.compute_loss(batch, output, train=True)
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
disc_out = self.discriminator(pred_smpl_params['body_pose'].reshape(batch_size, -1), pred_smpl_params['betas'].reshape(batch_size, -1))
loss_adv = ((disc_out - 1.0) ** 2).sum() / batch_size
loss = loss + self.cfg.LOSS_WEIGHTS.ADVERSARIAL * loss_adv
# Error if Nan
if torch.isnan(loss):
raise ValueError('Loss is NaN')
optimizer.zero_grad()
self.manual_backward(loss)
# Clip gradient
if self.cfg.TRAIN.get('GRAD_CLIP_VAL', 0) > 0:
gn = torch.nn.utils.clip_grad_norm_(self.get_parameters(), self.cfg.TRAIN.GRAD_CLIP_VAL, error_if_nonfinite=True)
self.log('train/grad_norm', gn, on_step=True, on_epoch=True, prog_bar=True, logger=True)
optimizer.step()
if self.cfg.LOSS_WEIGHTS.ADVERSARIAL > 0:
loss_disc = self.training_step_discriminator(mocap_batch, pred_smpl_params['body_pose'].reshape(batch_size, -1), pred_smpl_params['betas'].reshape(batch_size, -1), optimizer_disc)
output['losses']['loss_gen'] = loss_adv
output['losses']['loss_disc'] = loss_disc
if self.global_step > 0 and self.global_step % self.cfg.GENERAL.LOG_STEPS == 0:
self.tensorboard_logging(batch, output, self.global_step, train=True)
self.log('train/loss', output['losses']['loss'], on_step=True, on_epoch=True, prog_bar=True, logger=False)
return output
def validation_step(self, batch: Dict, batch_idx: int, dataloader_idx=0) -> Dict:
"""
Run a validation step and log to Tensorboard
Args:
batch (Dict): Dictionary containing batch data
batch_idx (int): Unused.
Returns:
Dict: Dictionary containing regression output.
"""
# batch_size = batch['img'].shape[0]
output = self.forward_step(batch, train=False)
loss = self.compute_loss(batch, output, train=False)
output['loss'] = loss
self.tensorboard_logging(batch, output, self.global_step, train=False)
return output
+92
View File
@@ -0,0 +1,92 @@
import torch
import torch.nn as nn
class Keypoint2DLoss(nn.Module):
def __init__(self, loss_type: str = 'l1'):
"""
2D keypoint loss module.
Args:
loss_type (str): Choose between l1 and l2 losses.
"""
super(Keypoint2DLoss, self).__init__()
if loss_type == 'l1':
self.loss_fn = nn.L1Loss(reduction='none')
elif loss_type == 'l2':
self.loss_fn = nn.MSELoss(reduction='none')
else:
raise NotImplementedError('Unsupported loss function')
def forward(self, pred_keypoints_2d: torch.Tensor, gt_keypoints_2d: torch.Tensor) -> torch.Tensor:
"""
Compute 2D reprojection loss on the keypoints.
Args:
pred_keypoints_2d (torch.Tensor): Tensor of shape [B, S, N, 2] containing projected 2D keypoints (B: batch_size, S: num_samples, N: num_keypoints)
gt_keypoints_2d (torch.Tensor): Tensor of shape [B, S, N, 3] containing the ground truth 2D keypoints and confidence.
Returns:
torch.Tensor: 2D keypoint loss.
"""
conf = gt_keypoints_2d[:, :, -1].unsqueeze(-1).clone()
batch_size = conf.shape[0]
loss = (conf * self.loss_fn(pred_keypoints_2d, gt_keypoints_2d[:, :, :-1])).sum(dim=(1,2))
return loss.sum()
class Keypoint3DLoss(nn.Module):
def __init__(self, loss_type: str = 'l1'):
"""
3D keypoint loss module.
Args:
loss_type (str): Choose between l1 and l2 losses.
"""
super(Keypoint3DLoss, self).__init__()
if loss_type == 'l1':
self.loss_fn = nn.L1Loss(reduction='none')
elif loss_type == 'l2':
self.loss_fn = nn.MSELoss(reduction='none')
else:
raise NotImplementedError('Unsupported loss function')
def forward(self, pred_keypoints_3d: torch.Tensor, gt_keypoints_3d: torch.Tensor, pelvis_id: int = 39):
"""
Compute 3D keypoint loss.
Args:
pred_keypoints_3d (torch.Tensor): Tensor of shape [B, S, N, 3] containing the predicted 3D keypoints (B: batch_size, S: num_samples, N: num_keypoints)
gt_keypoints_3d (torch.Tensor): Tensor of shape [B, S, N, 4] containing the ground truth 3D keypoints and confidence.
Returns:
torch.Tensor: 3D keypoint loss.
"""
batch_size = pred_keypoints_3d.shape[0]
gt_keypoints_3d = gt_keypoints_3d.clone()
pred_keypoints_3d = pred_keypoints_3d - pred_keypoints_3d[:, pelvis_id, :].unsqueeze(dim=1)
gt_keypoints_3d[:, :, :-1] = gt_keypoints_3d[:, :, :-1] - gt_keypoints_3d[:, pelvis_id, :-1].unsqueeze(dim=1)
conf = gt_keypoints_3d[:, :, -1].unsqueeze(-1).clone()
gt_keypoints_3d = gt_keypoints_3d[:, :, :-1]
loss = (conf * self.loss_fn(pred_keypoints_3d, gt_keypoints_3d)).sum(dim=(1,2))
return loss.sum()
class ParameterLoss(nn.Module):
def __init__(self):
"""
SMPL parameter loss module.
"""
super(ParameterLoss, self).__init__()
self.loss_fn = nn.MSELoss(reduction='none')
def forward(self, pred_param: torch.Tensor, gt_param: torch.Tensor, has_param: torch.Tensor):
"""
Compute SMPL parameter loss.
Args:
pred_param (torch.Tensor): Tensor of shape [B, S, ...] containing the predicted parameters (body pose / global orientation / betas)
gt_param (torch.Tensor): Tensor of shape [B, S, ...] containing the ground truth SMPL parameters.
Returns:
torch.Tensor: L2 parameter loss loss.
"""
batch_size = pred_param.shape[0]
num_dims = len(pred_param.shape)
mask_dimension = [batch_size] + [1] * (num_dims-1)
has_param = has_param.type(pred_param.type()).view(*mask_dimension)
loss_param = (has_param * self.loss_fn(pred_param, gt_param))
return loss_param.sum()
+41
View File
@@ -0,0 +1,41 @@
import torch
import numpy as np
import pickle
from typing import Optional
import smplx
from smplx.lbs import vertices2joints
from smplx.utils import SMPLOutput
class SMPL(smplx.SMPLLayer):
def __init__(self, *args, joint_regressor_extra: Optional[str] = None, update_hips: bool = False, **kwargs):
"""
Extension of the official SMPL implementation to support more joints.
Args:
Same as SMPLLayer.
joint_regressor_extra (str): Path to extra joint regressor.
"""
super(SMPL, self).__init__(*args, **kwargs)
smpl_to_openpose = [24, 12, 17, 19, 21, 16, 18, 20, 0, 2, 5, 8, 1, 4,
7, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
if joint_regressor_extra is not None:
self.register_buffer('joint_regressor_extra', torch.tensor(pickle.load(open(joint_regressor_extra, 'rb'), encoding='latin1'), dtype=torch.float32))
self.register_buffer('joint_map', torch.tensor(smpl_to_openpose, dtype=torch.long))
self.update_hips = update_hips
def forward(self, *args, **kwargs) -> SMPLOutput:
"""
Run forward pass. Same as SMPL and also append an extra set of joints if joint_regressor_extra is specified.
"""
smpl_output = super(SMPL, self).forward(*args, **kwargs)
joints = smpl_output.joints[:, self.joint_map, :]
if self.update_hips:
joints[:,[9,12]] = joints[:,[9,12]] + \
0.25*(joints[:,[9,12]]-joints[:,[12,9]]) + \
0.5*(joints[:,[8]] - 0.5*(joints[:,[9,12]] + joints[:,[12,9]]))
if hasattr(self, 'joint_regressor_extra'):
extra_joints = vertices2joints(self.joint_regressor_extra, smpl_output.vertices)
joints = torch.cat([joints, extra_joints], dim=1)
smpl_output.joints = joints
return smpl_output
+25
View File
@@ -0,0 +1,25 @@
import torch
from typing import Any
from .renderer import Renderer
from .mesh_renderer import MeshRenderer
from .skeleton_renderer import SkeletonRenderer
from .pose_utils import eval_pose, Evaluator
def recursive_to(x: Any, target: torch.device):
"""
Recursively transfer a batch of data to the target device
Args:
x (Any): Batch of data.
target (torch.device): Target device.
Returns:
Batch of data where all tensors are transfered to the target device.
"""
if isinstance(x, dict):
return {k: recursive_to(v, target) for k, v in x.items()}
elif isinstance(x, torch.Tensor):
return x.to(target)
elif isinstance(x, list):
return [recursive_to(i, target) for i in x]
else:
return x
+66
View File
@@ -0,0 +1,66 @@
import os
import re
import sys
from urllib import request as urlrequest
def _progress_bar(count, total):
"""Report download progress. Credit:
https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console/27871113
"""
bar_len = 60
filled_len = int(round(bar_len * count / float(total)))
percents = round(100.0 * count / float(total), 1)
bar = "=" * filled_len + "-" * (bar_len - filled_len)
sys.stdout.write(
" [{}] {}% of {:.1f}MB file \r".format(bar, percents, total / 1024 / 1024)
)
sys.stdout.flush()
if count >= total:
sys.stdout.write("\n")
def download_url(url, dst_file_path, chunk_size=8192, progress_hook=_progress_bar):
"""Download url and write it to dst_file_path. Credit:
https://stackoverflow.com/questions/2028517/python-urllib2-progress-hook
"""
# url = url + "?dl=1" if "dropbox" in url else url
req = urlrequest.Request(url)
response = urlrequest.urlopen(req)
total_size = response.info().get("Content-Length")
if total_size is None:
raise ValueError("Cannot determine size of download from {}".format(url))
total_size = int(total_size.strip())
bytes_so_far = 0
with open(dst_file_path, "wb") as f:
while 1:
chunk = response.read(chunk_size)
bytes_so_far += len(chunk)
if not chunk:
break
if progress_hook:
progress_hook(bytes_so_far, total_size)
f.write(chunk)
return bytes_so_far
def cache_url(url_or_file, cache_file_path, download=True):
"""Download the file specified by the URL to the cache_dir and return the path to
the cached file. If the argument is not a URL, simply return it as is.
"""
is_url = re.match(r"^(?:http)s?://", url_or_file, re.IGNORECASE) is not None
if not is_url:
return url_or_file
url = url_or_file
if os.path.exists(cache_file_path):
return cache_file_path
cache_file_dir = os.path.dirname(cache_file_path)
if not os.path.exists(cache_file_dir):
os.makedirs(cache_file_dir)
if download:
print("Downloading remote file {} to {}".format(url, cache_file_path))
download_url(url, cache_file_path)
return cache_file_path
+102
View File
@@ -0,0 +1,102 @@
from typing import Optional
import torch
from torch.nn import functional as F
def aa_to_rotmat(theta: torch.Tensor):
"""
Convert axis-angle representation to rotation matrix.
Works by first converting it to a quaternion.
Args:
theta (torch.Tensor): Tensor of shape (B, 3) containing axis-angle representations.
Returns:
torch.Tensor: Corresponding rotation matrices with shape (B, 3, 3).
"""
norm = torch.norm(theta + 1e-8, p = 2, dim = 1)
angle = torch.unsqueeze(norm, -1)
normalized = torch.div(theta, angle)
angle = angle * 0.5
v_cos = torch.cos(angle)
v_sin = torch.sin(angle)
quat = torch.cat([v_cos, v_sin * normalized], dim = 1)
return quat_to_rotmat(quat)
def quat_to_rotmat(quat: torch.Tensor) -> torch.Tensor:
"""
Convert quaternion representation to rotation matrix.
Args:
quat (torch.Tensor) of shape (B, 4); 4 <===> (w, x, y, z).
Returns:
torch.Tensor: Corresponding rotation matrices with shape (B, 3, 3).
"""
norm_quat = quat
norm_quat = norm_quat/norm_quat.norm(p=2, dim=1, keepdim=True)
w, x, y, z = norm_quat[:,0], norm_quat[:,1], norm_quat[:,2], norm_quat[:,3]
B = quat.size(0)
w2, x2, y2, z2 = w.pow(2), x.pow(2), y.pow(2), z.pow(2)
wx, wy, wz = w*x, w*y, w*z
xy, xz, yz = x*y, x*z, y*z
rotMat = torch.stack([w2 + x2 - y2 - z2, 2*xy - 2*wz, 2*wy + 2*xz,
2*wz + 2*xy, w2 - x2 + y2 - z2, 2*yz - 2*wx,
2*xz - 2*wy, 2*wx + 2*yz, w2 - x2 - y2 + z2], dim=1).view(B, 3, 3)
return rotMat
def rot6d_to_rotmat(x: torch.Tensor) -> torch.Tensor:
"""
Convert 6D rotation representation to 3x3 rotation matrix.
Based on Zhou et al., "On the Continuity of Rotation Representations in Neural Networks", CVPR 2019
Args:
x (torch.Tensor): (B,6) Batch of 6-D rotation representations.
Returns:
torch.Tensor: Batch of corresponding rotation matrices with shape (B,3,3).
"""
x = x.reshape(-1,2,3).permute(0, 2, 1).contiguous()
a1 = x[:, :, 0]
a2 = x[:, :, 1]
b1 = F.normalize(a1)
b2 = F.normalize(a2 - torch.einsum('bi,bi->b', b1, a2).unsqueeze(-1) * b1)
b3 = torch.cross(b1, b2)
return torch.stack((b1, b2, b3), dim=-1)
def perspective_projection(points: torch.Tensor,
translation: torch.Tensor,
focal_length: torch.Tensor,
camera_center: Optional[torch.Tensor] = None,
rotation: Optional[torch.Tensor] = None) -> torch.Tensor:
"""
Computes the perspective projection of a set of 3D points.
Args:
points (torch.Tensor): Tensor of shape (B, N, 3) containing the input 3D points.
translation (torch.Tensor): Tensor of shape (B, 3) containing the 3D camera translation.
focal_length (torch.Tensor): Tensor of shape (B, 2) containing the focal length in pixels.
camera_center (torch.Tensor): Tensor of shape (B, 2) containing the camera center in pixels.
rotation (torch.Tensor): Tensor of shape (B, 3, 3) containing the camera rotation.
Returns:
torch.Tensor: Tensor of shape (B, N, 2) containing the projection of the input points.
"""
batch_size = points.shape[0]
if rotation is None:
rotation = torch.eye(3, device=points.device, dtype=points.dtype).unsqueeze(0).expand(batch_size, -1, -1)
if camera_center is None:
camera_center = torch.zeros(batch_size, 2, device=points.device, dtype=points.dtype)
# Populate intrinsic camera matrix K.
K = torch.zeros([batch_size, 3, 3], device=points.device, dtype=points.dtype)
K[:,0,0] = focal_length[:,0]
K[:,1,1] = focal_length[:,1]
K[:,2,2] = 1.
K[:,:-1, -1] = camera_center
# Transform points
points = torch.einsum('bij,bkj->bki', rotation, points)
points = points + translation.unsqueeze(1)
# Apply perspective distortion
projected_points = points / points[:,:,-1].unsqueeze(-1)
# Apply camera intrinsics
projected_points = torch.einsum('bij,bkj->bki', K, projected_points)
return projected_points[:, :, :-1]
+149
View File
@@ -0,0 +1,149 @@
import os
if 'PYOPENGL_PLATFORM' not in os.environ:
os.environ['PYOPENGL_PLATFORM'] = 'egl'
import torch
from torchvision.utils import make_grid
import numpy as np
import pyrender
import trimesh
import cv2
import torch.nn.functional as F
from .render_openpose import render_openpose
def create_raymond_lights():
import pyrender
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
class MeshRenderer:
def __init__(self, cfg, faces=None):
self.cfg = cfg
self.focal_length = cfg.EXTRA.FOCAL_LENGTH
self.img_res = cfg.MODEL.IMAGE_SIZE
self.renderer = pyrender.OffscreenRenderer(viewport_width=self.img_res,
viewport_height=self.img_res,
point_size=1.0)
self.camera_center = [self.img_res // 2, self.img_res // 2]
self.faces = faces
def visualize(self, vertices, camera_translation, images, focal_length=None, nrow=3, padding=2):
images_np = np.transpose(images, (0,2,3,1))
rend_imgs = []
for i in range(vertices.shape[0]):
fl = self.focal_length
rend_img = torch.from_numpy(np.transpose(self.__call__(vertices[i], camera_translation[i], images_np[i], focal_length=fl, side_view=False), (2,0,1))).float()
rend_img_side = torch.from_numpy(np.transpose(self.__call__(vertices[i], camera_translation[i], images_np[i], focal_length=fl, side_view=True), (2,0,1))).float()
rend_imgs.append(torch.from_numpy(images[i]))
rend_imgs.append(rend_img)
rend_imgs.append(rend_img_side)
rend_imgs = make_grid(rend_imgs, nrow=nrow, padding=padding)
return rend_imgs
def visualize_tensorboard(self, vertices, camera_translation, images, pred_keypoints, gt_keypoints, focal_length=None, nrow=5, padding=2):
images_np = np.transpose(images, (0,2,3,1))
rend_imgs = []
pred_keypoints = np.concatenate((pred_keypoints, np.ones_like(pred_keypoints)[:, :, [0]]), axis=-1)
pred_keypoints = self.img_res * (pred_keypoints + 0.5)
gt_keypoints[:, :, :-1] = self.img_res * (gt_keypoints[:, :, :-1] + 0.5)
keypoint_matches = [(1, 12), (2, 8), (3, 7), (4, 6), (5, 9), (6, 10), (7, 11), (8, 14), (9, 2), (10, 1), (11, 0), (12, 3), (13, 4), (14, 5)]
for i in range(vertices.shape[0]):
fl = self.focal_length
rend_img = torch.from_numpy(np.transpose(self.__call__(vertices[i], camera_translation[i], images_np[i], focal_length=fl, side_view=False), (2,0,1))).float()
rend_img_side = torch.from_numpy(np.transpose(self.__call__(vertices[i], camera_translation[i], images_np[i], focal_length=fl, side_view=True), (2,0,1))).float()
body_keypoints = pred_keypoints[i, :25]
extra_keypoints = pred_keypoints[i, -19:]
for pair in keypoint_matches:
body_keypoints[pair[0], :] = extra_keypoints[pair[1], :]
pred_keypoints_img = render_openpose(255 * images_np[i].copy(), body_keypoints) / 255
body_keypoints = gt_keypoints[i, :25]
extra_keypoints = gt_keypoints[i, -19:]
for pair in keypoint_matches:
if extra_keypoints[pair[1], -1] > 0 and body_keypoints[pair[0], -1] == 0:
body_keypoints[pair[0], :] = extra_keypoints[pair[1], :]
gt_keypoints_img = render_openpose(255*images_np[i].copy(), body_keypoints) / 255
rend_imgs.append(torch.from_numpy(images[i]))
rend_imgs.append(rend_img)
rend_imgs.append(rend_img_side)
rend_imgs.append(torch.from_numpy(pred_keypoints_img).permute(2,0,1))
rend_imgs.append(torch.from_numpy(gt_keypoints_img).permute(2,0,1))
rend_imgs = make_grid(rend_imgs, nrow=nrow, padding=padding)
return rend_imgs
def __call__(self, vertices, camera_translation, image, focal_length=5000, text=None, resize=None, side_view=False, baseColorFactor=(1.0, 1.0, 0.9, 1.0), rot_angle=90):
renderer = pyrender.OffscreenRenderer(viewport_width=image.shape[1],
viewport_height=image.shape[0],
point_size=1.0)
material = pyrender.MetallicRoughnessMaterial(
metallicFactor=0.0,
alphaMode='OPAQUE',
baseColorFactor=baseColorFactor)
camera_translation[0] *= -1.
mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy())
if side_view:
rot = trimesh.transformations.rotation_matrix(
np.radians(rot_angle), [0, 1, 0])
mesh.apply_transform(rot)
rot = trimesh.transformations.rotation_matrix(
np.radians(180), [1, 0, 0])
mesh.apply_transform(rot)
mesh = pyrender.Mesh.from_trimesh(mesh, material=material)
scene = pyrender.Scene(bg_color=[0.0, 0.0, 0.0, 0.0],
ambient_light=(0.3, 0.3, 0.3))
scene.add(mesh, 'mesh')
camera_pose = np.eye(4)
camera_pose[:3, 3] = camera_translation
camera_center = [image.shape[1] / 2., image.shape[0] / 2.]
camera = pyrender.IntrinsicsCamera(fx=focal_length, fy=focal_length,
cx=camera_center[0], cy=camera_center[1])
scene.add(camera, pose=camera_pose)
light_nodes = create_raymond_lights()
for node in light_nodes:
scene.add_node(node)
color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA)
color = color.astype(np.float32) / 255.0
valid_mask = (color[:, :, -1] > 0)[:, :, np.newaxis]
if not side_view:
output_img = (color[:, :, :3] * valid_mask +
(1 - valid_mask) * image)
else:
output_img = color[:, :, :3]
if resize is not None:
output_img = cv2.resize(output_img, resize)
output_img = output_img.astype(np.float32)
renderer.delete()
return output_img
+203
View File
@@ -0,0 +1,203 @@
import time
import warnings
from importlib.util import find_spec
from pathlib import Path
from typing import Callable, List
import hydra
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Callback
from pytorch_lightning.loggers import Logger
from pytorch_lightning.utilities import rank_zero_only
from . import pylogger, rich_utils
log = pylogger.get_pylogger(__name__)
def task_wrapper(task_func: Callable) -> Callable:
"""Optional decorator that wraps the task function in extra utilities.
Makes multirun more resistant to failure.
Utilities:
- Calling the `utils.extras()` before the task is started
- Calling the `utils.close_loggers()` after the task is finished
- Logging the exception if occurs
- Logging the task total execution time
- Logging the output dir
"""
def wrap(cfg: DictConfig):
# apply extra utilities
extras(cfg)
# execute the task
try:
start_time = time.time()
ret = task_func(cfg=cfg)
except Exception as ex:
log.exception("") # save exception to `.log` file
raise ex
finally:
path = Path(cfg.paths.output_dir, "exec_time.log")
content = f"'{cfg.task_name}' execution time: {time.time() - start_time} (s)"
save_file(path, content) # save task execution time (even if exception occurs)
close_loggers() # close loggers (even if exception occurs so multirun won't fail)
log.info(f"Output dir: {cfg.paths.output_dir}")
return ret
return wrap
def extras(cfg: DictConfig) -> None:
"""Applies optional utilities before the task is started.
Utilities:
- Ignoring python warnings
- Setting tags from command line
- Rich config printing
"""
# return if no `extras` config
if not cfg.get("extras"):
log.warning("Extras config not found! <cfg.extras=null>")
return
# disable python warnings
if cfg.extras.get("ignore_warnings"):
log.info("Disabling python warnings! <cfg.extras.ignore_warnings=True>")
warnings.filterwarnings("ignore")
# prompt user to input tags from command line if none are provided in the config
if cfg.extras.get("enforce_tags"):
log.info("Enforcing tags! <cfg.extras.enforce_tags=True>")
rich_utils.enforce_tags(cfg, save_to_file=True)
# pretty print config tree using Rich library
if cfg.extras.get("print_config"):
log.info("Printing config tree with Rich! <cfg.extras.print_config=True>")
rich_utils.print_config_tree(cfg, resolve=True, save_to_file=True)
@rank_zero_only
def save_file(path: str, content: str) -> None:
"""Save file in rank zero mode (only on one process in multi-GPU setup)."""
with open(path, "w+") as file:
file.write(content)
def instantiate_callbacks(callbacks_cfg: DictConfig) -> List[Callback]:
"""Instantiates callbacks from config."""
callbacks: List[Callback] = []
if not callbacks_cfg:
log.warning("Callbacks config is empty.")
return callbacks
if not isinstance(callbacks_cfg, DictConfig):
raise TypeError("Callbacks config must be a DictConfig!")
for _, cb_conf in callbacks_cfg.items():
if isinstance(cb_conf, DictConfig) and "_target_" in cb_conf:
log.info(f"Instantiating callback <{cb_conf._target_}>")
callbacks.append(hydra.utils.instantiate(cb_conf))
return callbacks
def instantiate_loggers(logger_cfg: DictConfig) -> List[Logger]:
"""Instantiates loggers from config."""
logger: List[Logger] = []
if not logger_cfg:
log.warning("Logger config is empty.")
return logger
if not isinstance(logger_cfg, DictConfig):
raise TypeError("Logger config must be a DictConfig!")
for _, lg_conf in logger_cfg.items():
if isinstance(lg_conf, DictConfig) and "_target_" in lg_conf:
log.info(f"Instantiating logger <{lg_conf._target_}>")
logger.append(hydra.utils.instantiate(lg_conf))
return logger
@rank_zero_only
def log_hyperparameters(object_dict: dict) -> None:
"""Controls which config parts are saved by lightning loggers.
Additionally saves:
- Number of model parameters
"""
hparams = {}
cfg = object_dict["cfg"]
model = object_dict["model"]
trainer = object_dict["trainer"]
if not trainer.logger:
log.warning("Logger not found! Skipping hyperparameter logging...")
return
# save number of model parameters
hparams["model/params/total"] = sum(p.numel() for p in model.parameters())
hparams["model/params/trainable"] = sum(
p.numel() for p in model.parameters() if p.requires_grad
)
hparams["model/params/non_trainable"] = sum(
p.numel() for p in model.parameters() if not p.requires_grad
)
for k in cfg.keys():
hparams[k] = cfg.get(k)
# Resolve all interpolations
def _resolve(_cfg):
if isinstance(_cfg, DictConfig):
_cfg = OmegaConf.to_container(_cfg, resolve=True)
return _cfg
hparams = {k: _resolve(v) for k, v in hparams.items()}
# send hparams to all loggers
trainer.logger.log_hyperparams(hparams)
def get_metric_value(metric_dict: dict, metric_name: str) -> float:
"""Safely retrieves value of the metric logged in LightningModule."""
if not metric_name:
log.info("Metric name is None! Skipping metric value retrieval...")
return None
if metric_name not in metric_dict:
raise Exception(
f"Metric value not found! <metric_name={metric_name}>\n"
"Make sure metric name logged in LightningModule is correct!\n"
"Make sure `optimized_metric` name in `hparams_search` config is correct!"
)
metric_value = metric_dict[metric_name].item()
log.info(f"Retrieved metric value! <{metric_name}={metric_value}>")
return metric_value
def close_loggers() -> None:
"""Makes sure all loggers closed properly (prevents logging failure during multirun)."""
log.info("Closing loggers...")
if find_spec("wandb"): # if wandb is installed
import wandb
if wandb.run:
log.info("Closing wandb!")
wandb.finish()
+94
View File
@@ -0,0 +1,94 @@
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
def _calc_distances(preds, targets, mask, normalize):
"""Calculate the normalized distances between preds and target.
Note:
batch_size: N
num_keypoints: K
dimension of keypoints: D (normally, D=2 or D=3)
Args:
preds (np.ndarray[N, K, D]): Predicted keypoint location.
targets (np.ndarray[N, K, D]): Groundtruth keypoint location.
mask (np.ndarray[N, K]): Visibility of the target. False for invisible
joints, and True for visible. Invisible joints will be ignored for
accuracy calculation.
normalize (np.ndarray[N, D]): Typical value is heatmap_size
Returns:
np.ndarray[K, N]: The normalized distances. \
If target keypoints are missing, the distance is -1.
"""
N, K, _ = preds.shape
# set mask=0 when normalize==0
_mask = mask.copy()
_mask[np.where((normalize == 0).sum(1))[0], :] = False
distances = np.full((N, K), -1, dtype=np.float32)
# handle invalid values
normalize[np.where(normalize <= 0)] = 1e6
distances[_mask] = np.linalg.norm(
((preds - targets) / normalize[:, None, :])[_mask], axis=-1)
return distances.T
def _distance_acc(distances, thr=0.5):
"""Return the percentage below the distance threshold, while ignoring
distances values with -1.
Note:
batch_size: N
Args:
distances (np.ndarray[N, ]): The normalized distances.
thr (float): Threshold of the distances.
Returns:
float: Percentage of distances below the threshold. \
If all target keypoints are missing, return -1.
"""
distance_valid = distances != -1
num_distance_valid = distance_valid.sum()
if num_distance_valid > 0:
return (distances[distance_valid] < thr).sum() / num_distance_valid
return -1
def keypoint_pck_accuracy(pred, gt, mask, thr, normalize):
"""Calculate the pose accuracy of PCK for each individual keypoint and the
averaged accuracy across all keypoints for coordinates.
Note:
PCK metric measures accuracy of the localization of the body joints.
The distances between predicted positions and the ground-truth ones
are typically normalized by the bounding box size.
The threshold (thr) of the normalized distance is commonly set
as 0.05, 0.1 or 0.2 etc.
- batch_size: N
- num_keypoints: K
Args:
pred (np.ndarray[N, K, 2]): Predicted keypoint location.
gt (np.ndarray[N, K, 2]): Groundtruth keypoint location.
mask (np.ndarray[N, K]): Visibility of the target. False for invisible
joints, and True for visible. Invisible joints will be ignored for
accuracy calculation.
thr (float): Threshold of PCK calculation.
normalize (np.ndarray[N, 2]): Normalization factor for H&W.
Returns:
tuple: A tuple containing keypoint accuracy.
- acc (np.ndarray[K]): Accuracy of each keypoint.
- avg_acc (float): Averaged accuracy across all keypoints.
- cnt (int): Number of valid keypoints.
"""
distances = _calc_distances(pred, gt, mask, normalize)
acc = np.array([_distance_acc(d, thr) for d in distances])
valid_acc = acc[acc >= 0]
cnt = len(valid_acc)
avg_acc = valid_acc.mean() if cnt > 0 else 0
return acc, avg_acc, cnt
+310
View File
@@ -0,0 +1,310 @@
"""
Code adapted from: https://github.com/akanazawa/hmr/blob/master/src/benchmark/eval_util.py
"""
import torch
import numpy as np
from typing import Optional, Dict, List, Tuple
def compute_similarity_transform(S1: torch.Tensor, S2: torch.Tensor) -> torch.Tensor:
"""
Computes a similarity transform (sR, t) in a batched way that takes
a set of 3D points S1 (B, N, 3) closest to a set of 3D points S2 (B, N, 3),
where R is a 3x3 rotation matrix, t 3x1 translation, s scale.
i.e. solves the orthogonal Procrutes problem.
Args:
S1 (torch.Tensor): First set of points of shape (B, N, 3).
S2 (torch.Tensor): Second set of points of shape (B, N, 3).
Returns:
(torch.Tensor): The first set of points after applying the similarity transformation.
"""
batch_size = S1.shape[0]
S1 = S1.permute(0, 2, 1)
S2 = S2.permute(0, 2, 1)
# 1. Remove mean.
mu1 = S1.mean(dim=2, keepdim=True)
mu2 = S2.mean(dim=2, keepdim=True)
X1 = S1 - mu1
X2 = S2 - mu2
# 2. Compute variance of X1 used for scale.
var1 = (X1**2).sum(dim=(1,2))
# 3. The outer product of X1 and X2.
K = torch.matmul(X1, X2.permute(0, 2, 1))
# 4. Solution that Maximizes trace(R'K) is R=U*V', where U, V are singular vectors of K.
U, s, V = torch.svd(K)
Vh = V.permute(0, 2, 1)
# Construct Z that fixes the orientation of R to get det(R)=1.
Z = torch.eye(U.shape[1], device=U.device).unsqueeze(0).repeat(batch_size, 1, 1)
Z[:, -1, -1] *= torch.sign(torch.linalg.det(torch.matmul(U, Vh)))
# Construct R.
R = torch.matmul(torch.matmul(V, Z), U.permute(0, 2, 1))
# 5. Recover scale.
trace = torch.matmul(R, K).diagonal(offset=0, dim1=-1, dim2=-2).sum(dim=-1)
scale = (trace / var1).unsqueeze(dim=-1).unsqueeze(dim=-1)
# 6. Recover translation.
t = mu2 - scale*torch.matmul(R, mu1)
# 7. Error:
S1_hat = scale*torch.matmul(R, S1) + t
return S1_hat.permute(0, 2, 1)
def reconstruction_error(S1, S2) -> np.array:
"""
Computes the mean Euclidean distance of 2 set of points S1, S2 after performing Procrustes alignment.
Args:
S1 (torch.Tensor): First set of points of shape (B, N, 3).
S2 (torch.Tensor): Second set of points of shape (B, N, 3).
Returns:
(np.array): Reconstruction error.
"""
S1_hat = compute_similarity_transform(S1, S2)
re = torch.sqrt( ((S1_hat - S2)** 2).sum(dim=-1)).mean(dim=-1)
return re
def eval_pose(pred_joints, gt_joints) -> Tuple[np.array, np.array]:
"""
Compute joint errors in mm before and after Procrustes alignment.
Args:
pred_joints (torch.Tensor): Predicted 3D joints of shape (B, N, 3).
gt_joints (torch.Tensor): Ground truth 3D joints of shape (B, N, 3).
Returns:
Tuple[np.array, np.array]: Joint errors in mm before and after alignment.
"""
# Absolute error (MPJPE)
mpjpe = torch.sqrt(((pred_joints - gt_joints) ** 2).sum(dim=-1)).mean(dim=-1).cpu().numpy()
# Reconstruction_error
r_error = reconstruction_error(pred_joints, gt_joints).cpu().numpy()
return 1000 * mpjpe, 1000 * r_error
class Evaluator:
def __init__(self,
dataset_length: int,
keypoint_list: List,
pelvis_ind: int,
metrics: List = ['mode_mpjpe', 'mode_re', 'min_mpjpe', 'min_re'],
pck_thresholds: Optional[List] = None):
"""
Class used for evaluating trained models on different 3D pose datasets.
Args:
dataset_length (int): Total dataset length.
keypoint_list [List]: List of keypoints used for evaluation.
pelvis_ind (int): Index of pelvis keypoint; used for aligning the predictions and ground truth.
metrics [List]: List of evaluation metrics to record.
"""
self.dataset_length = dataset_length
self.keypoint_list = keypoint_list
self.pelvis_ind = pelvis_ind
self.metrics = metrics
for metric in self.metrics:
setattr(self, metric, np.zeros((dataset_length,)))
self.counter = 0
if pck_thresholds is None:
self.pck_evaluator = None
else:
self.pck_evaluator = EvaluatorPCK(pck_thresholds)
def log(self):
"""
Print current evaluation metrics
"""
if self.counter == 0:
print('Evaluation has not started')
return
print(f'{self.counter} / {self.dataset_length} samples')
if self.pck_evaluator is not None:
self.pck_evaluator.log()
for metric in self.metrics:
if metric in ['mode_mpjpe', 'mode_re', 'min_mpjpe', 'min_re']:
unit = 'mm'
else:
unit = ''
print(f'{metric}: {getattr(self, metric)[:self.counter].mean()} {unit}')
print('***')
def get_metrics_dict(self) -> Dict:
"""
Returns:
Dict: Dictionary of evaluation metrics.
"""
d1 = {metric: getattr(self, metric)[:self.counter].mean() for metric in self.metrics}
if self.pck_evaluator is not None:
d2 = self.pck_evaluator.get_metrics_dict()
d1.update(d2)
return d1
def __call__(self, output: Dict, batch: Dict, opt_output: Optional[Dict] = None):
"""
Evaluate current batch.
Args:
output (Dict): Regression output.
batch (Dict): Dictionary containing images and their corresponding annotations.
opt_output (Dict): Optimization output.
"""
if self.pck_evaluator is not None:
self.pck_evaluator(output, batch, opt_output)
pred_keypoints_3d = output['pred_keypoints_3d'].detach()
pred_keypoints_3d = pred_keypoints_3d[:,None,:,:]
batch_size = pred_keypoints_3d.shape[0]
num_samples = pred_keypoints_3d.shape[1]
gt_keypoints_3d = batch['keypoints_3d'][:, :, :-1].unsqueeze(1).repeat(1, num_samples, 1, 1)
# Align predictions and ground truth such that the pelvis location is at the origin
pred_keypoints_3d -= pred_keypoints_3d[:, :, [self.pelvis_ind]]
gt_keypoints_3d -= gt_keypoints_3d[:, :, [self.pelvis_ind]]
# Compute joint errors
mpjpe, re = eval_pose(pred_keypoints_3d.reshape(batch_size * num_samples, -1, 3)[:, self.keypoint_list], gt_keypoints_3d.reshape(batch_size * num_samples, -1 ,3)[:, self.keypoint_list])
mpjpe = mpjpe.reshape(batch_size, num_samples)
re = re.reshape(batch_size, num_samples)
# Compute 2d keypoint errors
pred_keypoints_2d = output['pred_keypoints_2d'].detach()
pred_keypoints_2d = pred_keypoints_2d[:,None,:,:]
gt_keypoints_2d = batch['keypoints_2d'][:,None,:,:].repeat(1, num_samples, 1, 1)
conf = gt_keypoints_2d[:, :, :, -1].clone()
kp_err = torch.nn.functional.mse_loss(
pred_keypoints_2d,
gt_keypoints_2d[:, :, :, :-1],
reduction='none'
).sum(dim=3)
kp_l2_loss = (conf * kp_err).mean(dim=2)
kp_l2_loss = kp_l2_loss.detach().cpu().numpy()
# Compute joint errors after optimization, if available.
if opt_output is not None:
opt_keypoints_3d = opt_output['model_joints']
opt_keypoints_3d -= opt_keypoints_3d[:, [self.pelvis_ind]]
opt_mpjpe, opt_re = eval_pose(opt_keypoints_3d[:, self.keypoint_list], gt_keypoints_3d[:, 0, self.keypoint_list])
# The 0-th sample always corresponds to the mode
if hasattr(self, 'mode_mpjpe'):
mode_mpjpe = mpjpe[:, 0]
self.mode_mpjpe[self.counter:self.counter+batch_size] = mode_mpjpe
if hasattr(self, 'mode_re'):
mode_re = re[:, 0]
self.mode_re[self.counter:self.counter+batch_size] = mode_re
if hasattr(self, 'mode_kpl2'):
mode_kpl2 = kp_l2_loss[:, 0]
self.mode_kpl2[self.counter:self.counter+batch_size] = mode_kpl2
if hasattr(self, 'min_mpjpe'):
min_mpjpe = mpjpe.min(axis=-1)
self.min_mpjpe[self.counter:self.counter+batch_size] = min_mpjpe
if hasattr(self, 'min_re'):
min_re = re.min(axis=-1)
self.min_re[self.counter:self.counter+batch_size] = min_re
if hasattr(self, 'min_kpl2'):
min_kpl2 = kp_l2_loss.min(axis=-1)
self.min_kpl2[self.counter:self.counter+batch_size] = min_kpl2
if hasattr(self, 'opt_mpjpe'):
self.opt_mpjpe[self.counter:self.counter+batch_size] = opt_mpjpe
if hasattr(self, 'opt_re'):
self.opt_re[self.counter:self.counter+batch_size] = opt_re
self.counter += batch_size
if hasattr(self, 'mode_mpjpe') and hasattr(self, 'mode_re'):
return {
'mode_mpjpe': mode_mpjpe,
'mode_re': mode_re,
}
else:
return {}
class EvaluatorPCK:
def __init__(self, thresholds: List = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5],):
"""
Class used for evaluating trained models on different 3D pose datasets.
Args:
thresholds [List]: List of PCK thresholds to evaluate.
metrics [List]: List of evaluation metrics to record.
"""
self.thresholds = thresholds
self.pred_kp_2d = []
self.gt_kp_2d = []
self.gt_conf_2d = []
self.counter = 0
def log(self):
"""
Print current evaluation metrics
"""
if self.counter == 0:
print('Evaluation has not started')
return
print(f'{self.counter} samples')
metrics_dict = self.get_metrics_dict()
for metric in metrics_dict:
print(f'{metric}: {metrics_dict[metric]}')
print('***')
def get_metrics_dict(self) -> Dict:
"""
Returns:
Dict: Dictionary of evaluation metrics.
"""
pcks = self.compute_pcks()
metrics = {}
for thr, (acc,avg_acc,cnt) in zip(self.thresholds, pcks):
metrics.update({f'kp{i}_pck_{thr}': float(a) for i, a in enumerate(acc) if a>=0})
metrics.update({f'kpAvg_pck_{thr}': float(avg_acc)})
return metrics
def compute_pcks(self):
pred_kp_2d = np.concatenate(self.pred_kp_2d, axis=0)
gt_kp_2d = np.concatenate(self.gt_kp_2d, axis=0)
gt_conf_2d = np.concatenate(self.gt_conf_2d, axis=0)
assert pred_kp_2d.shape == gt_kp_2d.shape
assert pred_kp_2d[..., 0].shape == gt_conf_2d.shape
assert pred_kp_2d.shape[1] == 1 # num_samples
from .pck_accuracy import keypoint_pck_accuracy
pcks = [
keypoint_pck_accuracy(
pred_kp_2d[:, 0, :, :],
gt_kp_2d[:, 0, :, :],
gt_conf_2d[:, 0, :]>0.5,
thr=thr,
normalize = np.ones((len(pred_kp_2d),2)) # Already in [-0.5,0.5] range. No need to normalize
)
for thr in self.thresholds
]
return pcks
def __call__(self, output: Dict, batch: Dict, opt_output: Optional[Dict] = None):
"""
Evaluate current batch.
Args:
output (Dict): Regression output.
batch (Dict): Dictionary containing images and their corresponding annotations.
opt_output (Dict): Optimization output.
"""
pred_keypoints_2d = output['pred_keypoints_2d'].detach()
num_samples = 1
batch_size = pred_keypoints_2d.shape[0]
pred_keypoints_2d = pred_keypoints_2d[:,None,:,:]
gt_keypoints_2d = batch['keypoints_2d'][:,None,:,:].repeat(1, num_samples, 1, 1)
gt_bbox_expand_factor = (batch['box_size']/(batch['_scale']*200).max(dim=-1).values)
gt_bbox_expand_factor = gt_bbox_expand_factor[:,None,None,None].repeat(1, num_samples, 1, 1)
gt_bbox_expand_factor = gt_bbox_expand_factor.detach().cpu().numpy()
self.pred_kp_2d.append(pred_keypoints_2d[:, :, :, :2].detach().cpu().numpy() * gt_bbox_expand_factor)
self.gt_conf_2d.append(gt_keypoints_2d[:, :, :, -1].detach().cpu().numpy())
self.gt_kp_2d.append(gt_keypoints_2d[:, :, :, :2].detach().cpu().numpy() * gt_bbox_expand_factor)
self.counter += batch_size
+17
View File
@@ -0,0 +1,17 @@
import logging
from pytorch_lightning.utilities import rank_zero_only
def get_pylogger(name=__name__) -> logging.Logger:
"""Initializes multi-GPU-friendly python command line logger."""
logger = logging.getLogger(name)
# this ensures all logging levels get marked with the rank zero decorator
# otherwise logs would get multiplied for each GPU process in multi-GPU setup
logging_levels = ("debug", "info", "warning", "error", "exception", "fatal", "critical")
for level in logging_levels:
setattr(logger, level, rank_zero_only(getattr(logger, level)))
return logger
+149
View File
@@ -0,0 +1,149 @@
"""
Render OpenPose keypoints.
Code was ported to Python from the official C++ implementation https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/src/openpose/utilities/keypoint.cpp
"""
import cv2
import math
import numpy as np
from typing import List, Tuple
def get_keypoints_rectangle(keypoints: np.array, threshold: float) -> Tuple[float, float, float]:
"""
Compute rectangle enclosing keypoints above the threshold.
Args:
keypoints (np.array): Keypoint array of shape (N, 3).
threshold (float): Confidence visualization threshold.
Returns:
Tuple[float, float, float]: Rectangle width, height and area.
"""
valid_ind = keypoints[:, -1] > threshold
if valid_ind.sum() > 0:
valid_keypoints = keypoints[valid_ind][:, :-1]
max_x = valid_keypoints[:,0].max()
max_y = valid_keypoints[:,1].max()
min_x = valid_keypoints[:,0].min()
min_y = valid_keypoints[:,1].min()
width = max_x - min_x
height = max_y - min_y
area = width * height
return width, height, area
else:
return 0,0,0
def render_keypoints(img: np.array,
keypoints: np.array,
pairs: List,
colors: List,
thickness_circle_ratio: float,
thickness_line_ratio_wrt_circle: float,
pose_scales: List,
threshold: float = 0.1) -> np.array:
"""
Render keypoints on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
keypoints (np.array): Keypoint array of shape (N, 3).
pairs (List): List of keypoint pairs per limb.
colors: (List): List of colors per keypoint.
thickness_circle_ratio (float): Circle thickness ratio.
thickness_line_ratio_wrt_circle (float): Line thickness ratio wrt the circle.
pose_scales (List): List of pose scales.
threshold (float): Only visualize keypoints with confidence above the threshold.
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
img_orig = img.copy()
width, height = img.shape[1], img.shape[2]
area = width * height
lineType = 8
shift = 0
numberColors = len(colors)
thresholdRectangle = 0.1
person_width, person_height, person_area = get_keypoints_rectangle(keypoints, thresholdRectangle)
if person_area > 0:
ratioAreas = min(1, max(person_width / width, person_height / height))
thicknessRatio = np.maximum(np.round(math.sqrt(area) * thickness_circle_ratio * ratioAreas), 2)
thicknessCircle = np.maximum(1, thicknessRatio if ratioAreas > 0.05 else -np.ones_like(thicknessRatio))
thicknessLine = np.maximum(1, np.round(thicknessRatio * thickness_line_ratio_wrt_circle))
radius = thicknessRatio / 2
img = np.ascontiguousarray(img.copy())
for i, pair in enumerate(pairs):
index1, index2 = pair
if keypoints[index1, -1] > threshold and keypoints[index2, -1] > threshold:
thicknessLineScaled = int(round(min(thicknessLine[index1], thicknessLine[index2]) * pose_scales[0]))
colorIndex = index2
color = colors[colorIndex % numberColors]
keypoint1 = keypoints[index1, :-1].astype(np.int)
keypoint2 = keypoints[index2, :-1].astype(np.int)
cv2.line(img, tuple(keypoint1.tolist()), tuple(keypoint2.tolist()), tuple(color.tolist()), thicknessLineScaled, lineType, shift)
for part in range(len(keypoints)):
faceIndex = part
if keypoints[faceIndex, -1] > threshold:
radiusScaled = int(round(radius[faceIndex] * pose_scales[0]))
thicknessCircleScaled = int(round(thicknessCircle[faceIndex] * pose_scales[0]))
colorIndex = part
color = colors[colorIndex % numberColors]
center = keypoints[faceIndex, :-1].astype(np.int)
cv2.circle(img, tuple(center.tolist()), radiusScaled, tuple(color.tolist()), thicknessCircleScaled, lineType, shift)
return img
def render_body_keypoints(img: np.array,
body_keypoints: np.array) -> np.array:
"""
Render OpenPose body keypoints on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
body_keypoints (np.array): Keypoint array of shape (N, 3); 3 <====> (x, y, confidence).
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
thickness_circle_ratio = 1./75. * np.ones(body_keypoints.shape[0])
thickness_line_ratio_wrt_circle = 0.75
pairs = []
pairs = [1,8,1,2,1,5,2,3,3,4,5,6,6,7,8,9,9,10,10,11,8,12,12,13,13,14,1,0,0,15,15,17,0,16,16,18,14,19,19,20,14,21,11,22,22,23,11,24]
pairs = np.array(pairs).reshape(-1,2)
colors = [255., 0., 85.,
255., 0., 0.,
255., 85., 0.,
255., 170., 0.,
255., 255., 0.,
170., 255., 0.,
85., 255., 0.,
0., 255., 0.,
255., 0., 0.,
0., 255., 85.,
0., 255., 170.,
0., 255., 255.,
0., 170., 255.,
0., 85., 255.,
0., 0., 255.,
255., 0., 170.,
170., 0., 255.,
255., 0., 255.,
85., 0., 255.,
0., 0., 255.,
0., 0., 255.,
0., 0., 255.,
0., 255., 255.,
0., 255., 255.,
0., 255., 255.]
colors = np.array(colors).reshape(-1,3)
pose_scales = [1]
return render_keypoints(img, body_keypoints, pairs, colors, thickness_circle_ratio, thickness_line_ratio_wrt_circle, pose_scales, 0.1)
def render_openpose(img: np.array,
body_keypoints: np.array) -> np.array:
"""
Render keypoints in the OpenPose format on input image.
Args:
img (np.array): Input image of shape (H, W, 3) with pixel values in the [0,255] range.
body_keypoints (np.array): Keypoint array of shape (N, 3); 3 <====> (x, y, confidence).
Returns:
(np.array): Image of shape (H, W, 3) with keypoints drawn on top of the original image.
"""
img = render_body_keypoints(img, body_keypoints)
return img
+400
View File
@@ -0,0 +1,400 @@
import os
if 'PYOPENGL_PLATFORM' not in os.environ:
os.environ['PYOPENGL_PLATFORM'] = 'egl'
import torch
import numpy as np
import pyrender
import trimesh
import cv2
from yacs.config import CfgNode
from typing import List, Optional
def cam_crop_to_full(cam_bbox, box_center, box_size, img_size, focal_length=5000.):
# Convert cam_bbox to full image
img_w, img_h = img_size[:, 0], img_size[:, 1]
cx, cy, b = box_center[:, 0], box_center[:, 1], box_size
w_2, h_2 = img_w / 2., img_h / 2.
bs = b * cam_bbox[:, 0] + 1e-9
tz = 2 * focal_length / bs
tx = (2 * (cx - w_2) / bs) + cam_bbox[:, 1]
ty = (2 * (cy - h_2) / bs) + cam_bbox[:, 2]
full_cam = torch.stack([tx, ty, tz], dim=-1)
return full_cam
def get_light_poses(n_lights=5, elevation=np.pi / 3, dist=12):
# get lights in a circle around origin at elevation
thetas = elevation * np.ones(n_lights)
phis = 2 * np.pi * np.arange(n_lights) / n_lights
poses = []
trans = make_translation(torch.tensor([0, 0, dist]))
for phi, theta in zip(phis, thetas):
rot = make_rotation(rx=-theta, ry=phi, order="xyz")
poses.append((rot @ trans).numpy())
return poses
def make_translation(t):
return make_4x4_pose(torch.eye(3), t)
def make_rotation(rx=0, ry=0, rz=0, order="xyz"):
Rx = rotx(rx)
Ry = roty(ry)
Rz = rotz(rz)
if order == "xyz":
R = Rz @ Ry @ Rx
elif order == "xzy":
R = Ry @ Rz @ Rx
elif order == "yxz":
R = Rz @ Rx @ Ry
elif order == "yzx":
R = Rx @ Rz @ Ry
elif order == "zyx":
R = Rx @ Ry @ Rz
elif order == "zxy":
R = Ry @ Rx @ Rz
return make_4x4_pose(R, torch.zeros(3))
def make_4x4_pose(R, t):
"""
:param R (*, 3, 3)
:param t (*, 3)
return (*, 4, 4)
"""
dims = R.shape[:-2]
pose_3x4 = torch.cat([R, t.view(*dims, 3, 1)], dim=-1)
bottom = (
torch.tensor([0, 0, 0, 1], device=R.device)
.reshape(*(1,) * len(dims), 1, 4)
.expand(*dims, 1, 4)
)
return torch.cat([pose_3x4, bottom], dim=-2)
def rotx(theta):
return torch.tensor(
[
[1, 0, 0],
[0, np.cos(theta), -np.sin(theta)],
[0, np.sin(theta), np.cos(theta)],
],
dtype=torch.float32,
)
def roty(theta):
return torch.tensor(
[
[np.cos(theta), 0, np.sin(theta)],
[0, 1, 0],
[-np.sin(theta), 0, np.cos(theta)],
],
dtype=torch.float32,
)
def rotz(theta):
return torch.tensor(
[
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0],
[0, 0, 1],
],
dtype=torch.float32,
)
def create_raymond_lights() -> List[pyrender.Node]:
"""
Return raymond light nodes for the scene.
"""
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
class Renderer:
def __init__(self, cfg: CfgNode, faces: np.array):
"""
Wrapper around the pyrender renderer to render SMPL meshes.
Args:
cfg (CfgNode): Model config file.
faces (np.array): Array of shape (F, 3) containing the mesh faces.
"""
self.cfg = cfg
self.focal_length = cfg.EXTRA.FOCAL_LENGTH
self.img_res = cfg.MODEL.IMAGE_SIZE
self.camera_center = [self.img_res // 2, self.img_res // 2]
self.faces = faces
def __call__(self,
vertices: np.array,
camera_translation: np.array,
image: torch.Tensor,
full_frame: bool = False,
imgname: Optional[str] = None,
side_view=False, top_view=False,
rot_angle=90,
mesh_base_color=(1.0, 1.0, 0.9),
scene_bg_color=(0,0,0),
return_rgba=False,
) -> np.array:
"""
Render meshes on input image
Args:
vertices (np.array): Array of shape (V, 3) containing the mesh vertices.
camera_translation (np.array): Array of shape (3,) with the camera translation.
image (torch.Tensor): Tensor of shape (3, H, W) containing the image crop with normalized pixel values.
full_frame (bool): If True, then render on the full image.
imgname (Optional[str]): Contains the original image filenamee. Used only if full_frame == True.
"""
if full_frame:
image = cv2.imread(imgname).astype(np.float32)[:, :, ::-1] / 255.
else:
image = image.clone() * torch.tensor(self.cfg.MODEL.IMAGE_STD, device=image.device).reshape(3,1,1)
image = image + torch.tensor(self.cfg.MODEL.IMAGE_MEAN, device=image.device).reshape(3,1,1)
image = image.permute(1, 2, 0).cpu().numpy()
renderer = pyrender.OffscreenRenderer(viewport_width=image.shape[1],
viewport_height=image.shape[0],
point_size=1.0)
material = pyrender.MetallicRoughnessMaterial(
metallicFactor=0.0,
alphaMode='OPAQUE',
baseColorFactor=(*mesh_base_color, 1.0))
camera_translation[0] *= -1.
mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy())
if side_view:
rot = trimesh.transformations.rotation_matrix(
np.radians(rot_angle), [0, 1, 0])
mesh.apply_transform(rot)
elif top_view:
rot = trimesh.transformations.rotation_matrix(
np.radians(rot_angle), [1, 0, 0])
mesh.apply_transform(rot)
rot = trimesh.transformations.rotation_matrix(
np.radians(180), [1, 0, 0])
mesh.apply_transform(rot)
mesh = pyrender.Mesh.from_trimesh(mesh, material=material)
scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0],
ambient_light=(0.3, 0.3, 0.3))
scene.add(mesh, 'mesh')
camera_pose = np.eye(4)
camera_pose[:3, 3] = camera_translation
camera_center = [image.shape[1] / 2., image.shape[0] / 2.]
camera = pyrender.IntrinsicsCamera(fx=self.focal_length, fy=self.focal_length,
cx=camera_center[0], cy=camera_center[1], zfar=1e12)
scene.add(camera, pose=camera_pose)
light_nodes = create_raymond_lights()
for node in light_nodes:
scene.add_node(node)
color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA)
color = color.astype(np.float32) / 255.0
renderer.delete()
if return_rgba:
return color
valid_mask = (color[:, :, -1])[:, :, np.newaxis]
if not side_view and not top_view:
output_img = (color[:, :, :3] * valid_mask + (1 - valid_mask) * image)
else:
output_img = color[:, :, :3]
output_img = output_img.astype(np.float32)
return output_img
def vertices_to_trimesh(self, vertices, camera_translation, mesh_base_color=(1.0, 1.0, 0.9),
rot_axis=[1,0,0], rot_angle=0,):
# material = pyrender.MetallicRoughnessMaterial(
# metallicFactor=0.0,
# alphaMode='OPAQUE',
# baseColorFactor=(*mesh_base_color, 1.0))
vertex_colors = np.array([(*mesh_base_color, 1.0)] * vertices.shape[0])
print(vertices.shape, camera_translation.shape)
mesh = trimesh.Trimesh(vertices.copy() + camera_translation, self.faces.copy(), vertex_colors=vertex_colors)
# mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy())
rot = trimesh.transformations.rotation_matrix(
np.radians(rot_angle), rot_axis)
mesh.apply_transform(rot)
rot = trimesh.transformations.rotation_matrix(
np.radians(180), [1, 0, 0])
mesh.apply_transform(rot)
return mesh
def render_rgba(
self,
vertices: np.array,
cam_t = None,
rot=None,
rot_axis=[1,0,0],
rot_angle=0,
camera_z=3,
# camera_translation: np.array,
mesh_base_color=(1.0, 1.0, 0.9),
scene_bg_color=(0,0,0),
render_res=[256, 256],
):
renderer = pyrender.OffscreenRenderer(viewport_width=render_res[0],
viewport_height=render_res[1],
point_size=1.0)
# material = pyrender.MetallicRoughnessMaterial(
# metallicFactor=0.0,
# alphaMode='OPAQUE',
# baseColorFactor=(*mesh_base_color, 1.0))
if cam_t is not None:
camera_translation = cam_t.copy()
# camera_translation[0] *= -1.
else:
camera_translation = np.array([0, 0, camera_z * self.focal_length/render_res[1]])
mesh = self.vertices_to_trimesh(vertices, camera_translation, mesh_base_color, rot_axis, rot_angle)
mesh = pyrender.Mesh.from_trimesh(mesh)
# mesh = pyrender.Mesh.from_trimesh(mesh, material=material)
scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0],
ambient_light=(0.3, 0.3, 0.3))
scene.add(mesh, 'mesh')
camera_pose = np.eye(4)
# camera_pose[:3, 3] = camera_translation
camera_center = [render_res[0] / 2., render_res[1] / 2.]
camera = pyrender.IntrinsicsCamera(fx=self.focal_length, fy=self.focal_length,
cx=camera_center[0], cy=camera_center[1], zfar=1e12)
# Create camera node and add it to pyRender scene
camera_node = pyrender.Node(camera=camera, matrix=camera_pose)
scene.add_node(camera_node)
self.add_point_lighting(scene, camera_node)
self.add_lighting(scene, camera_node)
light_nodes = create_raymond_lights()
for node in light_nodes:
scene.add_node(node)
color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA)
color = color.astype(np.float32) / 255.0
renderer.delete()
return color
def render_rgba_multiple(
self,
vertices: List[np.array],
cam_t: List[np.array],
rot_axis=[1,0,0],
rot_angle=0,
mesh_base_color=(1.0, 1.0, 0.9),
scene_bg_color=(0,0,0),
render_res=[256, 256],
focal_length=None,
):
renderer = pyrender.OffscreenRenderer(viewport_width=render_res[0],
viewport_height=render_res[1],
point_size=1.0)
# material = pyrender.MetallicRoughnessMaterial(
# metallicFactor=0.0,
# alphaMode='OPAQUE',
# baseColorFactor=(*mesh_base_color, 1.0))
mesh_list = [pyrender.Mesh.from_trimesh(self.vertices_to_trimesh(vvv, ttt.copy(), mesh_base_color, rot_axis, rot_angle)) for vvv,ttt in zip(vertices, cam_t)]
scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0],
ambient_light=(0.3, 0.3, 0.3))
for i,mesh in enumerate(mesh_list):
scene.add(mesh, f'mesh_{i}')
camera_pose = np.eye(4)
# camera_pose[:3, 3] = camera_translation
camera_center = [render_res[0] / 2., render_res[1] / 2.]
focal_length = focal_length if focal_length is not None else self.focal_length
camera = pyrender.IntrinsicsCamera(fx=focal_length, fy=focal_length,
cx=camera_center[0], cy=camera_center[1], zfar=1e12)
# Create camera node and add it to pyRender scene
camera_node = pyrender.Node(camera=camera, matrix=camera_pose)
scene.add_node(camera_node)
self.add_point_lighting(scene, camera_node)
self.add_lighting(scene, camera_node)
light_nodes = create_raymond_lights()
for node in light_nodes:
scene.add_node(node)
color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA)
color = color.astype(np.float32) / 255.0
renderer.delete()
return color
def add_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0):
# from phalp.visualize.py_renderer import get_light_poses
light_poses = get_light_poses()
light_poses.append(np.eye(4))
cam_pose = scene.get_pose(cam_node)
for i, pose in enumerate(light_poses):
matrix = cam_pose @ pose
node = pyrender.Node(
name=f"light-{i:02d}",
light=pyrender.DirectionalLight(color=color, intensity=intensity),
matrix=matrix,
)
if scene.has_node(node):
continue
scene.add_node(node)
def add_point_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0):
# from phalp.visualize.py_renderer import get_light_poses
light_poses = get_light_poses(dist=0.5)
light_poses.append(np.eye(4))
cam_pose = scene.get_pose(cam_node)
for i, pose in enumerate(light_poses):
matrix = cam_pose @ pose
# node = pyrender.Node(
# name=f"light-{i:02d}",
# light=pyrender.DirectionalLight(color=color, intensity=intensity),
# matrix=matrix,
# )
node = pyrender.Node(
name=f"plight-{i:02d}",
light=pyrender.PointLight(color=color, intensity=intensity),
matrix=matrix,
)
if scene.has_node(node):
continue
scene.add_node(node)
+105
View File
@@ -0,0 +1,105 @@
from pathlib import Path
from typing import Sequence
import rich
import rich.syntax
import rich.tree
from hydra.core.hydra_config import HydraConfig
from omegaconf import DictConfig, OmegaConf, open_dict
from pytorch_lightning.utilities import rank_zero_only
from rich.prompt import Prompt
from . import pylogger
log = pylogger.get_pylogger(__name__)
@rank_zero_only
def print_config_tree(
cfg: DictConfig,
print_order: Sequence[str] = (
"datamodule",
"model",
"callbacks",
"logger",
"trainer",
"paths",
"extras",
),
resolve: bool = False,
save_to_file: bool = False,
) -> None:
"""Prints content of DictConfig using Rich library and its tree structure.
Args:
cfg (DictConfig): Configuration composed by Hydra.
print_order (Sequence[str], optional): Determines in what order config components are printed.
resolve (bool, optional): Whether to resolve reference fields of DictConfig.
save_to_file (bool, optional): Whether to export config to the hydra output folder.
"""
style = "dim"
tree = rich.tree.Tree("CONFIG", style=style, guide_style=style)
queue = []
# add fields from `print_order` to queue
for field in print_order:
queue.append(field) if field in cfg else log.warning(
f"Field '{field}' not found in config. Skipping '{field}' config printing..."
)
# add all the other fields to queue (not specified in `print_order`)
for field in cfg:
if field not in queue:
queue.append(field)
# generate config tree from queue
for field in queue:
branch = tree.add(field, style=style, guide_style=style)
config_group = cfg[field]
if isinstance(config_group, DictConfig):
branch_content = OmegaConf.to_yaml(config_group, resolve=resolve)
else:
branch_content = str(config_group)
branch.add(rich.syntax.Syntax(branch_content, "yaml"))
# print config tree
rich.print(tree)
# save config tree to file
if save_to_file:
with open(Path(cfg.paths.output_dir, "config_tree.log"), "w") as file:
rich.print(tree, file=file)
@rank_zero_only
def enforce_tags(cfg: DictConfig, save_to_file: bool = False) -> None:
"""Prompts user to input tags from command line if no tags are provided in config."""
if not cfg.get("tags"):
if "id" in HydraConfig().cfg.hydra.job:
raise ValueError("Specify tags before launching a multirun!")
log.warning("No tags provided in config. Prompting user to input tags...")
tags = Prompt.ask("Enter a list of comma separated tags", default="dev")
tags = [t.strip() for t in tags.split(",") if t != ""]
with open_dict(cfg):
cfg.tags = tags
log.info(f"Tags: {cfg.tags}")
if save_to_file:
with open(Path(cfg.paths.output_dir, "tags.log"), "w") as file:
rich.print(cfg.tags, file=file)
if __name__ == "__main__":
from hydra import compose, initialize
with initialize(version_base="1.2", config_path="../../configs"):
cfg = compose(config_name="train.yaml", return_hydra_config=False, overrides=[])
print_config_tree(cfg, resolve=False, save_to_file=False)
+122
View File
@@ -0,0 +1,122 @@
import torch
import numpy as np
import trimesh
from typing import Optional
from yacs.config import CfgNode
from .geometry import perspective_projection
from .render_openpose import render_openpose
class SkeletonRenderer:
def __init__(self, cfg: CfgNode):
"""
Object used to render 3D keypoints. Faster for use during training.
Args:
cfg (CfgNode): Model config file.
"""
self.cfg = cfg
def __call__(self,
pred_keypoints_3d: torch.Tensor,
gt_keypoints_3d: torch.Tensor,
gt_keypoints_2d: torch.Tensor,
images: Optional[np.array] = None,
camera_translation: Optional[torch.Tensor] = None) -> np.array:
"""
Render batch of 3D keypoints.
Args:
pred_keypoints_3d (torch.Tensor): Tensor of shape (B, S, N, 3) containing a batch of predicted 3D keypoints, with S samples per image.
gt_keypoints_3d (torch.Tensor): Tensor of shape (B, N, 4) containing corresponding ground truth 3D keypoints; last value is the confidence.
gt_keypoints_2d (torch.Tensor): Tensor of shape (B, N, 3) containing corresponding ground truth 2D keypoints.
images (torch.Tensor): Tensor of shape (B, H, W, 3) containing images with values in the [0,255] range.
camera_translation (torch.Tensor): Tensor of shape (B, 3) containing the camera translation.
Returns:
np.array : Image with the following layout. Each row contains the a) input image,
b) image with gt 2D keypoints,
c) image with projected gt 3D keypoints,
d_1, ... , d_S) image with projected predicted 3D keypoints,
e) gt 3D keypoints rendered from a side view,
f_1, ... , f_S) predicted 3D keypoints frorm a side view
"""
batch_size = pred_keypoints_3d.shape[0]
# num_samples = pred_keypoints_3d.shape[1]
pred_keypoints_3d = pred_keypoints_3d.clone().cpu().float()
gt_keypoints_3d = gt_keypoints_3d.clone().cpu().float()
gt_keypoints_3d[:, :, :-1] = gt_keypoints_3d[:, :, :-1] - gt_keypoints_3d[:, [25+14], :-1] + pred_keypoints_3d[:, [25+14]]
gt_keypoints_2d = gt_keypoints_2d.clone().cpu().float().numpy()
gt_keypoints_2d[:, :, :-1] = self.cfg.MODEL.IMAGE_SIZE * (gt_keypoints_2d[:, :, :-1] + 1.0) / 2.0
openpose_indices = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
gt_indices = [12, 8, 7, 6, 9, 10, 11, 14, 2, 1, 0, 3, 4, 5]
gt_indices = [25 + i for i in gt_indices]
keypoints_to_render = torch.ones(batch_size, gt_keypoints_3d.shape[1], 1)
rotation = torch.eye(3).unsqueeze(0)
if camera_translation is None:
camera_translation = torch.tensor([0.0, 0.0, 2 * self.cfg.EXTRA.FOCAL_LENGTH / (0.8 * self.cfg.MODEL.IMAGE_SIZE)]).unsqueeze(0).repeat(batch_size, 1)
else:
camera_translation = camera_translation.cpu()
if images is None:
images = np.zeros((batch_size, self.cfg.MODEL.IMAGE_SIZE, self.cfg.MODEL.IMAGE_SIZE, 3))
focal_length = torch.tensor([self.cfg.EXTRA.FOCAL_LENGTH, self.cfg.EXTRA.FOCAL_LENGTH]).reshape(1, 2)
camera_center = torch.tensor([self.cfg.MODEL.IMAGE_SIZE, self.cfg.MODEL.IMAGE_SIZE], dtype=torch.float).reshape(1, 2) / 2.
gt_keypoints_3d_proj = perspective_projection(gt_keypoints_3d[:, :, :-1], rotation=rotation.repeat(batch_size, 1, 1), translation=camera_translation[:, :], focal_length=focal_length.repeat(batch_size, 1), camera_center=camera_center.repeat(batch_size, 1))
pred_keypoints_3d_proj = perspective_projection(pred_keypoints_3d.reshape(batch_size, -1, 3), rotation=rotation.repeat(batch_size, 1, 1), translation=camera_translation.reshape(batch_size, -1), focal_length=focal_length.repeat(batch_size, 1), camera_center=camera_center.repeat(batch_size, 1)).reshape(batch_size, -1, 2)
gt_keypoints_3d_proj = torch.cat([gt_keypoints_3d_proj, gt_keypoints_3d[:, :, [-1]]], dim=-1).cpu().numpy()
pred_keypoints_3d_proj = torch.cat([pred_keypoints_3d_proj, keypoints_to_render.reshape(batch_size, -1, 1)], dim=-1).cpu().numpy()
rows = []
# Rotate keypoints to visualize side view
R = torch.tensor(trimesh.transformations.rotation_matrix(np.radians(90), [0, 1, 0])[:3, :3]).float()
gt_keypoints_3d_side = gt_keypoints_3d.clone()
gt_keypoints_3d_side[:, :, :-1] = torch.einsum('bni,ij->bnj', gt_keypoints_3d_side[:, :, :-1], R)
pred_keypoints_3d_side = pred_keypoints_3d.clone()
pred_keypoints_3d_side = torch.einsum('bni,ij->bnj', pred_keypoints_3d_side, R)
gt_keypoints_3d_proj_side = perspective_projection(gt_keypoints_3d_side[:, :, :-1], rotation=rotation.repeat(batch_size, 1, 1), translation=camera_translation[:, :], focal_length=focal_length.repeat(batch_size, 1), camera_center=camera_center.repeat(batch_size, 1))
pred_keypoints_3d_proj_side = perspective_projection(pred_keypoints_3d_side.reshape(batch_size, -1, 3), rotation=rotation.repeat(batch_size, 1, 1), translation=camera_translation.reshape(batch_size, -1), focal_length=focal_length.repeat(batch_size, 1), camera_center=camera_center.repeat(batch_size, 1)).reshape(batch_size, -1, 2)
gt_keypoints_3d_proj_side = torch.cat([gt_keypoints_3d_proj_side, gt_keypoints_3d_side[:, :, [-1]]], dim=-1).cpu().numpy()
pred_keypoints_3d_proj_side = torch.cat([pred_keypoints_3d_proj_side, keypoints_to_render.reshape(batch_size, -1, 1)], dim=-1).cpu().numpy()
for i in range(batch_size):
img = images[i]
side_img = np.zeros((self.cfg.MODEL.IMAGE_SIZE, self.cfg.MODEL.IMAGE_SIZE, 3))
# gt 2D keypoints
body_keypoints_2d = gt_keypoints_2d[i, :25].copy()
for op, gt in zip(openpose_indices, gt_indices):
if gt_keypoints_2d[i, gt, -1] > body_keypoints_2d[op, -1]:
body_keypoints_2d[op] = gt_keypoints_2d[i, gt]
gt_keypoints_img = render_openpose(img, body_keypoints_2d) / 255.
# gt 3D keypoints
body_keypoints_3d_proj = gt_keypoints_3d_proj[i, :25].copy()
for op, gt in zip(openpose_indices, gt_indices):
if gt_keypoints_3d_proj[i, gt, -1] > body_keypoints_3d_proj[op, -1]:
body_keypoints_3d_proj[op] = gt_keypoints_3d_proj[i, gt]
gt_keypoints_3d_proj_img = render_openpose(img, body_keypoints_3d_proj) / 255.
# gt 3D keypoints from the side
body_keypoints_3d_proj = gt_keypoints_3d_proj_side[i, :25].copy()
for op, gt in zip(openpose_indices, gt_indices):
if gt_keypoints_3d_proj_side[i, gt, -1] > body_keypoints_3d_proj[op, -1]:
body_keypoints_3d_proj[op] = gt_keypoints_3d_proj_side[i, gt]
gt_keypoints_3d_proj_img_side = render_openpose(side_img, body_keypoints_3d_proj) / 255.
# pred 3D keypoints
pred_keypoints_3d_proj_imgs = []
body_keypoints_3d_proj = pred_keypoints_3d_proj[i, :25].copy()
for op, gt in zip(openpose_indices, gt_indices):
if pred_keypoints_3d_proj[i, gt, -1] >= body_keypoints_3d_proj[op, -1]:
body_keypoints_3d_proj[op] = pred_keypoints_3d_proj[i, gt]
pred_keypoints_3d_proj_imgs.append(render_openpose(img, body_keypoints_3d_proj) / 255.)
pred_keypoints_3d_proj_img = np.concatenate(pred_keypoints_3d_proj_imgs, axis=1)
# gt 3D keypoints from the side
pred_keypoints_3d_proj_imgs_side = []
body_keypoints_3d_proj = pred_keypoints_3d_proj_side[i, :25].copy()
for op, gt in zip(openpose_indices, gt_indices):
if pred_keypoints_3d_proj_side[i, gt, -1] >= body_keypoints_3d_proj[op, -1]:
body_keypoints_3d_proj[op] = pred_keypoints_3d_proj_side[i, gt]
pred_keypoints_3d_proj_imgs_side.append(render_openpose(side_img, body_keypoints_3d_proj) / 255.)
pred_keypoints_3d_proj_img_side = np.concatenate(pred_keypoints_3d_proj_imgs_side, axis=1)
rows.append(np.concatenate((gt_keypoints_img, gt_keypoints_3d_proj_img, pred_keypoints_3d_proj_img, gt_keypoints_3d_proj_img_side, pred_keypoints_3d_proj_img_side), axis=1))
# Concatenate images
img = np.concatenate(rows, axis=0)
img[:, ::self.cfg.MODEL.IMAGE_SIZE, :] = 1.0
img[::self.cfg.MODEL.IMAGE_SIZE, :, :] = 1.0
img[:, (1+1+1)*self.cfg.MODEL.IMAGE_SIZE, :] = 0.5
return img
+85
View File
@@ -0,0 +1,85 @@
import numpy as np
import torch
from torch.nn import functional as F
# from psbody.mesh.visibility import visibility_compute
def uv_to_xyz_and_normals(verts, f, fmap, bmap, ftov):
vn = estimate_vertex_normals(verts, f, ftov)
pixels_to_set = torch.nonzero(fmap+1)
x_to_set = pixels_to_set[:,0]
y_to_set = pixels_to_set[:,1]
b_coords = bmap[x_to_set, y_to_set, :]
f_coords = fmap[x_to_set, y_to_set]
v_ids = f[f_coords]
points = (b_coords[:,0,None]*verts[:,v_ids[:,0]]
+ b_coords[:,1,None]*verts[:,v_ids[:,1]]
+ b_coords[:,2,None]*verts[:,v_ids[:,2]])
normals = (b_coords[:,0,None]*vn[:,v_ids[:,0]]
+ b_coords[:,1,None]*vn[:,v_ids[:,1]]
+ b_coords[:,2,None]*vn[:,v_ids[:,2]])
return points, normals, vn, f_coords
def estimate_vertex_normals(v, f, ftov):
face_normals = TriNormalsScaled(v, f)
non_scaled_normals = torch.einsum('ij,bjk->bik', ftov, face_normals)
norms = torch.sum(non_scaled_normals ** 2.0, 2) ** 0.5
norms[norms == 0] = 1.0
return torch.div(non_scaled_normals, norms[:,:,None])
def TriNormalsScaled(v, f):
return torch.cross(_edges_for(v, f, 1, 0), _edges_for(v, f, 2, 0))
def _edges_for(v, f, cplus, cminus):
return v[:,f[:,cplus]] - v[:,f[:,cminus]]
def psbody_get_face_visibility(v, n, f, cams, normal_threshold=0.5):
bn, nverts, _ = v.shape
nfaces, _ = f.shape
vis_f = np.zeros([bn, nfaces], dtype='float32')
for i in range(bn):
vis, n_dot_cam = visibility_compute(v=v[i], n=n[i], f=f, cams=cams)
vis_v = (vis == 1) & (n_dot_cam > normal_threshold)
vis_f[i] = np.all(vis_v[0,f],1)
return vis_f
def compute_uvsampler(vt, ft, tex_size=6):
"""
For this mesh, pre-computes the UV coordinates for
F x T x T points.
Returns F x T x T x 2
"""
uv = obj2nmr_uvmap(ft, vt, tex_size=tex_size)
uv = uv.reshape(-1, tex_size, tex_size, 2)
return uv
def obj2nmr_uvmap(ft, vt, tex_size=6):
"""
Converts obj uv_map to NMR uv_map (F x T x T x 2),
where tex_size (T) is the sample rate on each face.
"""
# This is F x 3 x 2
uv_map_for_verts = vt[ft]
# obj's y coordinate is [1-0], but image is [0-1]
uv_map_for_verts[:, :, 1] = 1 - uv_map_for_verts[:, :, 1]
# range [0, 1] -> [-1, 1]
uv_map_for_verts = (2 * uv_map_for_verts) - 1
alpha = np.arange(tex_size, dtype=float) / (tex_size - 1)
beta = np.arange(tex_size, dtype=float) / (tex_size - 1)
import itertools
# Barycentric coordinate values
coords = np.stack([p for p in itertools.product(*[alpha, beta])])
# Compute alpha, beta (this is the same order as NMR)
v2 = uv_map_for_verts[:, 2]
v0v2 = uv_map_for_verts[:, 0] - uv_map_for_verts[:, 2]
v1v2 = uv_map_for_verts[:, 1] - uv_map_for_verts[:, 2]
# Interpolate the vertex uv values: F x 2 x T*2
uv_map = np.dstack([v0v2, v1v2]).dot(coords.T) + v2.reshape(-1, 2, 1)
# F x T*2 x 2 -> F x T x T x 2
uv_map = np.transpose(uv_map, (0, 2, 1)).reshape(-1, tex_size, tex_size, 2)
return uv_map
+93
View File
@@ -0,0 +1,93 @@
import detectron2.data.transforms as T
import torch
from detectron2.checkpoint import DetectionCheckpointer
from detectron2.config import CfgNode, instantiate
from detectron2.data import MetadataCatalog
from omegaconf import OmegaConf
class DefaultPredictor_Lazy:
"""Create a simple end-to-end predictor with the given config that runs on single device for a
single input image.
Compared to using the model directly, this class does the following additions:
1. Load checkpoint from the weights specified in config (cfg.MODEL.WEIGHTS).
2. Always take BGR image as the input and apply format conversion internally.
3. Apply resizing defined by the config (`cfg.INPUT.{MIN,MAX}_SIZE_TEST`).
4. Take one input image and produce a single output, instead of a batch.
This is meant for simple demo purposes, so it does the above steps automatically.
This is not meant for benchmarks or running complicated inference logic.
If you'd like to do anything more complicated, please refer to its source code as
examples to build and use the model manually.
Attributes:
metadata (Metadata): the metadata of the underlying dataset, obtained from
test dataset name in the config.
Examples:
::
pred = DefaultPredictor(cfg)
inputs = cv2.imread("input.jpg")
outputs = pred(inputs)
"""
def __init__(self, cfg):
"""
Args:
cfg: a yacs CfgNode or a omegaconf dict object.
"""
if isinstance(cfg, CfgNode):
self.cfg = cfg.clone() # cfg can be modified by model
self.model = build_model(self.cfg) # noqa: F821
if len(cfg.DATASETS.TEST):
test_dataset = cfg.DATASETS.TEST[0]
checkpointer = DetectionCheckpointer(self.model)
checkpointer.load(cfg.MODEL.WEIGHTS)
self.aug = T.ResizeShortestEdge(
[cfg.INPUT.MIN_SIZE_TEST, cfg.INPUT.MIN_SIZE_TEST], cfg.INPUT.MAX_SIZE_TEST
)
self.input_format = cfg.INPUT.FORMAT
else: # new LazyConfig
self.cfg = cfg
self.model = instantiate(cfg.model)
test_dataset = OmegaConf.select(cfg, "dataloader.test.dataset.names", default=None)
if isinstance(test_dataset, (list, tuple)):
test_dataset = test_dataset[0]
checkpointer = DetectionCheckpointer(self.model)
checkpointer.load(OmegaConf.select(cfg, "train.init_checkpoint", default=""))
mapper = instantiate(cfg.dataloader.test.mapper)
self.aug = mapper.augmentations
self.input_format = mapper.image_format
self.model.eval().cuda()
if test_dataset:
self.metadata = MetadataCatalog.get(test_dataset)
assert self.input_format in ["RGB", "BGR"], self.input_format
def __call__(self, original_image):
"""
Args:
original_image (np.ndarray): an image of shape (H, W, C) (in BGR order).
Returns:
predictions (dict):
the output of the model for one image only.
See :doc:`/tutorials/models` for details about the format.
"""
with torch.no_grad():
if self.input_format == "RGB":
original_image = original_image[:, :, ::-1]
height, width = original_image.shape[:2]
image = self.aug(T.AugInput(original_image)).apply_image(original_image)
image = torch.as_tensor(image.astype("float32").transpose(2, 0, 1))
inputs = {"image": image, "height": height, "width": width}
predictions = self.model([inputs])[0]
return predictions
+31
View File
@@ -0,0 +1,31 @@
from setuptools import setup, find_packages
print('Found packages:', find_packages())
setup(
description='HMR2 as a package',
name='hmr2',
packages=find_packages(),
install_requires=[
'gdown',
'numpy',
'torch',
'torchvision',
'pytorch-lightning',
'smplx==0.1.28',
'pyrender',
'opencv-python',
'yacs',
'scikit-image',
'einops',
'timm',
'webdataset',
'dill',
'pandas',
'chumpy @ git+https://github.com/mattloper/chumpy',
],
extras_require={
'all': [
'detectron2 @ git+https://github.com/facebookresearch/detectron2',
],
},
)
+185
View File
@@ -0,0 +1,185 @@
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
import os
import hydra
import torch
import numpy as np
from hydra.core.config_store import ConfigStore
from omegaconf import DictConfig
from phalp.configs.base import FullConfig
from phalp.models.hmar.hmr import HMR2018Predictor
from phalp.trackers.PHALP import PHALP
from phalp.utils import get_pylogger
from phalp.configs.base import CACHE_DIR
from hmr2.datasets.utils import expand_bbox_to_aspect_ratio
warnings.filterwarnings('ignore')
log = get_pylogger(__name__)
class HMR2Predictor(HMR2018Predictor):
def __init__(self, cfg) -> None:
super().__init__(cfg)
# Setup our new model
from hmr2.models import download_models, load_hmr2
# Download and load checkpoints
download_models()
model, _ = load_hmr2()
self.model = model
self.model.eval()
def forward(self, x):
hmar_out = self.hmar_old(x)
batch = {
'img': x[:,:3,:,:],
'mask': (x[:,3,:,:]).clip(0,1),
}
model_out = self.model(batch)
out = hmar_out | {
'pose_smpl': model_out['pred_smpl_params'],
'pred_cam': model_out['pred_cam'],
}
return out
class HMR2023TextureSampler(HMR2Predictor):
def __init__(self, cfg) -> None:
super().__init__(cfg)
# Model's all set up. Now, load tex_bmap and tex_fmap
# Texture map atlas
bmap = np.load(os.path.join(CACHE_DIR, 'phalp/3D/bmap_256.npy'))
fmap = np.load(os.path.join(CACHE_DIR, 'phalp/3D/fmap_256.npy'))
self.register_buffer('tex_bmap', torch.tensor(bmap, dtype=torch.float))
self.register_buffer('tex_fmap', torch.tensor(fmap, dtype=torch.long))
self.img_size = 256 #self.cfg.MODEL.IMAGE_SIZE
self.focal_length = 5000. #self.cfg.EXTRA.FOCAL_LENGTH
import neural_renderer as nr
self.neural_renderer = nr.Renderer(dist_coeffs=None, orig_size=self.img_size,
image_size=self.img_size,
light_intensity_ambient=1,
light_intensity_directional=0,
anti_aliasing=False)
def forward(self, x):
batch = {
'img': x[:,:3,:,:],
'mask': (x[:,3,:,:]).clip(0,1),
}
model_out = self.model(batch)
# from hmr2.models.prohmr_texture import unproject_uvmap_to_mesh
def unproject_uvmap_to_mesh(bmap, fmap, verts, faces):
# bmap: 256,256,3
# fmap: 256,256
# verts: B,V,3
# faces: F,3
valid_mask = (fmap >= 0)
fmap_flat = fmap[valid_mask] # N
bmap_flat = bmap[valid_mask,:] # N,3
face_vids = faces[fmap_flat, :] # N,3
face_verts = verts[:, face_vids, :] # B,N,3,3
bs = face_verts.shape
map_verts = torch.einsum('bnij,ni->bnj', face_verts, bmap_flat) # B,N,3
return map_verts, valid_mask
pred_verts = model_out['pred_vertices'] + model_out['pred_cam_t'].unsqueeze(1)
device = pred_verts.device
face_tensor = torch.tensor(self.smpl.faces.astype(np.int64), dtype=torch.long, device=device)
map_verts, valid_mask = unproject_uvmap_to_mesh(self.tex_bmap, self.tex_fmap, pred_verts, face_tensor) # B,N,3
# Project map_verts to image using K,R,t
# map_verts_view = einsum('bij,bnj->bni', R, map_verts) + t # R=I t=0
focal = self.focal_length / (self.img_size / 2)
map_verts_proj = focal * map_verts[:, :, :2] / map_verts[:, :, 2:3] # B,N,2
map_verts_depth = map_verts[:, :, 2] # B,N
# Render Depth. Annoying but we need to create this
K = torch.eye(3, device=device)
K[0, 0] = K[1, 1] = self.focal_length
K[1, 2] = K[0, 2] = self.img_size / 2 # Because the neural renderer only support squared images
K = K.unsqueeze(0)
R = torch.eye(3, device=device).unsqueeze(0)
t = torch.zeros(3, device=device).unsqueeze(0)
rend_depth = self.neural_renderer(pred_verts,
face_tensor[None].expand(pred_verts.shape[0], -1, -1).int(),
# textures=texture_atlas_rgb,
mode='depth',
K=K, R=R, t=t)
rend_depth_at_proj = torch.nn.functional.grid_sample(rend_depth[:,None,:,:], map_verts_proj[:,None,:,:]) # B,1,1,N
rend_depth_at_proj = rend_depth_at_proj.squeeze(1).squeeze(1) # B,N
img_rgba = torch.cat([batch['img'], batch['mask'][:,None,:,:]], dim=1) # B,4,H,W
img_rgba_at_proj = torch.nn.functional.grid_sample(img_rgba, map_verts_proj[:,None,:,:]) # B,4,1,N
img_rgba_at_proj = img_rgba_at_proj.squeeze(2) # B,4,N
visibility_mask = map_verts_depth <= (rend_depth_at_proj + 1e-4) # B,N
img_rgba_at_proj[:,3,:][~visibility_mask] = 0
# Paste image back onto square uv_image
uv_image = torch.zeros((batch['img'].shape[0], 4, 256, 256), dtype=torch.float, device=device)
uv_image[:, :, valid_mask] = img_rgba_at_proj
out = {
'uv_image': uv_image,
'uv_vector' : self.hmar_old.process_uv_image(uv_image),
'pose_smpl': model_out['pred_smpl_params'],
'pred_cam': model_out['pred_cam'],
}
return out
class HMR2_4dhuman(PHALP):
def __init__(self, cfg):
super().__init__(cfg)
def setup_hmr(self):
self.HMAR = HMR2023TextureSampler(self.cfg)
def get_detections(self, image, frame_name, t_, additional_data=None, measurments=None):
(
pred_bbox, pred_bbox, pred_masks, pred_scores, pred_classes,
ground_truth_track_id, ground_truth_annotations
) = super().get_detections(image, frame_name, t_, additional_data, measurments)
# Pad bounding boxes
pred_bbox_padded = expand_bbox_to_aspect_ratio(pred_bbox, self.cfg.expand_bbox_shape)
return (
pred_bbox, pred_bbox_padded, pred_masks, pred_scores, pred_classes,
ground_truth_track_id, ground_truth_annotations
)
@dataclass
class Human4DConfig(FullConfig):
# override defaults if needed
expand_bbox_shape: Optional[Tuple[int]] = (192,256)
pass
cs = ConfigStore.instance()
cs.store(name="config", node=Human4DConfig)
@hydra.main(version_base="1.2", config_name="config")
def main(cfg: DictConfig) -> Optional[float]:
"""Main function for running the PHALP tracker."""
phalp_tracker = HMR2_4dhuman(cfg)
phalp_tracker.track()
if __name__ == "__main__":
main()
+111
View File
@@ -0,0 +1,111 @@
from typing import Optional, Tuple
import pyrootutils
root = pyrootutils.setup_root(
search_from=__file__,
indicator=[".git", "pyproject.toml"],
pythonpath=True,
dotenv=True,
)
import os
from pathlib import Path
import hydra
import pytorch_lightning as pl
from omegaconf import DictConfig, OmegaConf
from pytorch_lightning import Trainer
from pytorch_lightning.loggers import TensorBoardLogger
from pytorch_lightning.plugins.environments import SLURMEnvironment
from yacs.config import CfgNode
from hmr2.configs import dataset_config
from hmr2.datasets import HMR2DataModule
from hmr2.models.hmr2 import HMR2
from hmr2.utils.pylogger import get_pylogger
from hmr2.utils.misc import task_wrapper, log_hyperparameters
# HACK reset the signal handling so the lightning is free to set it
# Based on https://github.com/facebookincubator/submitit/issues/1709#issuecomment-1246758283
import signal
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
log = get_pylogger(__name__)
@pl.utilities.rank_zero.rank_zero_only
def save_configs(model_cfg: CfgNode, dataset_cfg: CfgNode, rootdir: str):
"""Save config files to rootdir."""
Path(rootdir).mkdir(parents=True, exist_ok=True)
OmegaConf.save(config=model_cfg, f=os.path.join(rootdir, 'model_config.yaml'))
with open(os.path.join(rootdir, 'dataset_config.yaml'), 'w') as f:
f.write(dataset_cfg.dump())
@task_wrapper
def train(cfg: DictConfig) -> Tuple[dict, dict]:
# Load dataset config
dataset_cfg = dataset_config()
# Save configs
save_configs(cfg, dataset_cfg, cfg.paths.output_dir)
# Setup training and validation datasets
datamodule = HMR2DataModule(cfg, dataset_cfg)
# Setup model
model = HMR2(cfg)
# Setup Tensorboard logger
logger = TensorBoardLogger(os.path.join(cfg.paths.output_dir, 'tensorboard'), name='', version='', default_hp_metric=False)
loggers = [logger]
# Setup checkpoint saving
checkpoint_callback = pl.callbacks.ModelCheckpoint(
dirpath=os.path.join(cfg.paths.output_dir, 'checkpoints'),
every_n_train_steps=cfg.GENERAL.CHECKPOINT_STEPS,
save_last=True,
save_top_k=cfg.GENERAL.CHECKPOINT_SAVE_TOP_K,
)
rich_callback = pl.callbacks.RichProgressBar()
lr_monitor = pl.callbacks.LearningRateMonitor(logging_interval='step')
callbacks = [
checkpoint_callback,
lr_monitor,
# rich_callback
]
log.info(f"Instantiating trainer <{cfg.trainer._target_}>")
trainer: Trainer = hydra.utils.instantiate(
cfg.trainer,
callbacks=callbacks,
logger=loggers,
plugins=(SLURMEnvironment(requeue_signal=signal.SIGUSR2) if (cfg.get('launcher',None) is not None) else None), # Submitit uses SIGUSR2
)
object_dict = {
"cfg": cfg,
"datamodule": datamodule,
"model": model,
"callbacks": callbacks,
"logger": logger,
"trainer": trainer,
}
if logger:
log.info("Logging hyperparameters!")
log_hyperparameters(object_dict)
# Train the model
trainer.fit(model, datamodule=datamodule, ckpt_path='last')
log.info("Fitting done")
@hydra.main(version_base="1.2", config_path=str(root/"hmr2/configs_hydra"), config_name="train.yaml")
def main(cfg: DictConfig) -> Optional[float]:
# train the model
train(cfg)
if __name__ == "__main__":
main()
+27
View File
@@ -0,0 +1,27 @@
import os
from folder_paths import models_dir
from ..utils.process_humans import load_detectron
DETECTRON_PATH = os.path.join(models_dir, 'detectron')
os.makedirs(DETECTRON_PATH, exist_ok=True)
class LoadDetectronNode:
@classmethod
def INPUT_TYPES(s):
return {"required": { "checkpoint": (os.listdir(DETECTRON_PATH), )}}
RETURN_TYPES = ("DETECTRON",)
FUNCTION = "load"
CATEGORY = "4dhumans"
def load(self, checkpoint):
checkpoint_path = os.path.join(DETECTRON_PATH, checkpoint)
model = load_detectron(checkpoint_path)
return (model,)
+30
View File
@@ -0,0 +1,30 @@
import os
import torch
from folder_paths import models_dir
from ..utils.process_humans import load_hmr_model
HMR_PATH = os.path.join(models_dir, 'hmr')
os.makedirs(HMR_PATH, exist_ok=True)
SMPL_PATH = os.path.join(models_dir, 'smpl')
os.makedirs(SMPL_PATH, exist_ok=True)
class LoadHMRNode:
@classmethod
def INPUT_TYPES(s):
return {"required": { "checkpoint": (os.listdir(HMR_PATH), )}}
RETURN_TYPES = ("HMR",)
FUNCTION = "load"
CATEGORY = "4dhumans"
def load(self, checkpoint):
checkpoint_path = os.path.join(HMR_PATH, checkpoint)
model_info = load_hmr_model(checkpoint_path, SMPL_PATH)
return (model_info,)
+27
View File
@@ -0,0 +1,27 @@
from folder_paths import models_dir
import comfy.model_management
from ..utils.process_humans import process_humans
class ProcessHumansNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"images": ("IMAGE",),
"hmr": ("HMR", ),
"detectron": ("DETECTRON", ),
}}
RETURN_TYPES = ("SMPL", "IMAGE", "MASK", "IMAGE")
RETURN_NAMES = ("smpl", "images", "masks", "semantic_maps")
FUNCTION = "process"
CATEGORY = "4dhumans"
def process(self, images, hmr, detectron):
intermediate_device = comfy.model_management.intermediate_device()
torch_device = comfy.model_management.get_torch_device()
results = process_humans(images, hmr, detectron, figure_scale=None, device=torch_device)
return (results['smpl'], results['images'], results['masks'], results['semantic_maps'])
+21
View File
@@ -0,0 +1,21 @@
from ..utils.process_humans import get_human
class SelectHumanNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"images": ("IMAGE",),
"smpl": ("SMPL", ),
"idx": ("INT", {"default": 0, "min": 0, "max": 999, "step": 1}),
}}
RETURN_TYPES = ("SMPL", "IMAGE", "MASK", "IMAGE")
RETURN_NAMES = ("smpl", "images", "masks", "semantic_maps")
FUNCTION = "process"
CATEGORY = "4dhumans"
def process(self, images, smpl, idx):
results = get_human(images, smpl, idx)
return (results['smpl'], results['images'], results['masks'], results['semantic_maps'])
+4
View File
@@ -0,0 +1,4 @@
# git+https://github.com/shubham-goel/4D-Humans
git+https://github.com/facebookresearch/detectron2
pyrender
yacs
+298
View File
@@ -0,0 +1,298 @@
import os
import torch
import numpy as np
import platform
import pyrender
# from pathlib import Path
from tqdm import tqdm
if "PYOPENGL_PLATFORM" not in os.environ:
os.environ["PYOPENGL_PLATFORM"] = "egl"
import comfy.utils
from ..humans4d.hmr2.models import load_hmr2
from ..humans4d.hmr2.utils import recursive_to
from ..humans4d.hmr2.datasets.vitdet_dataset import ViTDetDataset
from ..humans4d.hmr2.utils.renderer import cam_crop_to_full
# from ..humans4d.hmr2.configs import get_config
# from ..humans4d.hmr2.models import check_smpl_exists
from ..humans4d.hmr2.utils.utils_detectron2 import DefaultPredictor_Lazy
from detectron2.config import LazyConfig
from .visualize_humans import SemanticRenderer
from .. import REPO_PATH
# For Windows, remove PYOPENGL_PLATFORM to enable default rendering backend
sys_name = platform.system()
if sys_name == "Windows":
os.environ.pop("PYOPENGL_PLATFORM")
LIGHT_BLUE = (0.65098039, 0.74117647, 0.85882353)
def predict_smpl(batch, model, model_cfg, figure_scale=None):
all_verts = []
all_cam_t = []
with torch.no_grad():
out = model(batch)
pred_cam = out["pred_cam"]
pred_smpl_parameter = out["pred_smpl_params"]
if figure_scale is not None:
pred_smpl_parameter['betas'][0][1] = float(figure_scale)
smpl_output = model.smpl(
**{k: v.float() for k, v in pred_smpl_parameter.items()},
pose2rot=False,
)
pred_vertices = smpl_output.vertices
out["pred_vertices"] = pred_vertices.reshape(
batch["img"].shape[0], -1, 3
)
box_center = batch["box_center"].float()
box_size = batch["box_size"].float()
img_size = batch["img_size"].float()
scaled_focal_length = (
model_cfg.EXTRA.FOCAL_LENGTH
/ model_cfg.MODEL.IMAGE_SIZE
* img_size.max()
)
pred_cam_t_full = cam_crop_to_full(
pred_cam, box_center, box_size, img_size, scaled_focal_length
).detach().cpu().numpy()
# Render the result
batch_size = batch["img"].shape[0]
for n in range(batch_size):
# Add all verts and cams to list
verts = out["pred_vertices"][n].detach().cpu().numpy()
cam_t = pred_cam_t_full[n]
all_verts.append(verts)
all_cam_t.append(cam_t)
misc_args = dict(
mesh_base_color=LIGHT_BLUE,
scene_bg_color=(1, 1, 1),
focal_length=scaled_focal_length,
)
smpl_outs = {
k: v.detach().cpu().numpy() for k, v in pred_smpl_parameter.items()
}
results_dict_for_rendering = {
"verts": all_verts,
"cam_t": all_cam_t,
"render_res": img_size[n].cpu().numpy(),
"smpls": smpl_outs,
"scaled_focal_length": scaled_focal_length.cpu().numpy(),
}
return results_dict_for_rendering, misc_args
def load_image(model_cfg, image, detector):
# Detect humans in image
# image = image.permute(1,2,0)*255
det_out = detector(image)
# image = image.numpy()
det_instances = det_out["instances"]
valid_idx = (det_instances.pred_classes == 0) & (det_instances.scores > 0.5)
boxes = det_instances.pred_boxes.tensor[valid_idx].cpu().numpy()
# Run HMR2.0 on all detected humans
dataset = ViTDetDataset(model_cfg, image, boxes)
return torch.utils.data.DataLoader(
dataset, batch_size=8, shuffle=False, num_workers=0
)
def load_hmr_model(hmr2_checkpoint_path, smpl_dir_path):
model, model_cfg = load_hmr2(hmr2_checkpoint_path,smpl_dir_path)
return {
'model': model,
'model_cfg': model_cfg
}
def load_detectron(detectron_checkpoint_path):
cfg_path = os.path.join(REPO_PATH, "configs", "cascade_mask_rcnn_vitdet_h_75ep.py")
detectron2_cfg = LazyConfig.load(str(cfg_path))
detectron2_cfg.train.init_checkpoint = detectron_checkpoint_path
for i in range(3):
detectron2_cfg.model.roi_heads.box_predictors[i].test_score_thresh = 0.25
detector = DefaultPredictor_Lazy(detectron2_cfg)
return detector
def process_humans(images, hmr, detector, figure_scale, device):
images = images.permute(0, 3, 1, 2)
_, _, height, width = images.shape
model, model_cfg = hmr['model'], hmr['model_cfg']
model = model.to(device)
detector.model.to(device)
# This PyRender is only used for visualizing, we use Blender after to render different conditions.
renderer = SemanticRenderer(
model_cfg,
faces=model.smpl.faces,
lbs=model.smpl.lbs_weights,
viewport_size=(720, 720),
)
results = []
rendered_images = []
semantic_maps = []
masks = []
focal_length = None
mesh_base_color = None
scene_bg_color = None
pbar = comfy.utils.ProgressBar(len(images))
for image in tqdm(images, desc="Processing Human Images:"):
image = image.permute(1,2,0)*255
image = image.numpy()
renderer.renderer.delete()
renderer.renderer = pyrender.OffscreenRenderer(
viewport_width=width,
viewport_height=height,
point_size=1.0,
)
dataloader = load_image(model_cfg, image, detector)
for batch in dataloader:
batch = recursive_to(batch, device)
results_dict_for_rendering, misc_args = predict_smpl(batch, model, model_cfg, figure_scale)
mesh_base_color = misc_args['mesh_base_color']
scene_bg_color = misc_args['scene_bg_color']
focal_length = misc_args['focal_length'].to('cpu')
rendering_results = renderer.render_all_multiple(
results_dict_for_rendering["verts"],
cam_t=results_dict_for_rendering["cam_t"],
render_res=results_dict_for_rendering["render_res"], **misc_args
)
# Overlay image
valid_mask = rendering_results["Image"][:, :, -1][:, :, np.newaxis]
cam_view = (
valid_mask * rendering_results["Image"][:, :, [2, 1, 0]]
+ (1 - valid_mask) * image.astype(np.float32)[:, :, ::-1] / 255
)
rendered_images.append(torch.from_numpy(cam_view[:, :, ::-1].copy()))
semantic_maps.append(torch.from_numpy(rendering_results.get("SemanticMap")))
masks.append(torch.from_numpy(rendering_results.get("Mask")[:, :, 0]))
results.append(results_dict_for_rendering) # still in np
pbar.update(1)
smpl = {
'results': results,
'width': width,
'height': height,
'model_cfg': model_cfg,
'faces': model.smpl.faces,
'lbs': model.smpl.lbs_weights,
'viewport_size': (720, 720),
'misc_args': { 'mesh_base_color': mesh_base_color, 'scene_bg_color': scene_bg_color, 'focal_length': focal_length }
}
return {
'smpl': smpl,
'images': torch.stack(rendered_images),
'masks': torch.stack(masks).to(torch.float16),
'semantic_maps': torch.stack(semantic_maps),
}
def get_human(images, smpl, idx):
results = []
rendered_images = []
semantic_maps = []
masks = []
model_cfg, faces, lbs = smpl['model_cfg'], smpl['faces'], smpl['lbs']
viewport_size = smpl['viewport_size']
width, height = smpl['width'], smpl['height']
misc_args = smpl['misc_args']
renderer = SemanticRenderer(
model_cfg,
faces=faces,
lbs=lbs,
viewport_size=viewport_size,
)
renderer.renderer = pyrender.OffscreenRenderer(
viewport_width=width,
viewport_height=height,
point_size=1.0,
)
human_smpl = []
keys = ['verts']
smpl_keys = ['global_orient', 'body_pose', 'betas']
for results_dict_for_rendering in smpl['results']:
result = {
'scaled_focal_length': results_dict_for_rendering['scaled_focal_length'],
'smpls': {},
'cam_t': results_dict_for_rendering['cam_t'],
'render_res': results_dict_for_rendering['render_res']
}
good = True
for key in keys:
if idx < len(results_dict_for_rendering[key]):
result[key] = [results_dict_for_rendering[key][idx]]
else:
good = False
for key in smpl_keys:
if idx < len(results_dict_for_rendering['smpls'][key]):
result['smpls'][key] = [results_dict_for_rendering['smpls'][key][idx]]
else:
good = False
if good:
human_smpl.append(result)
else:
human_smpl.append(None)
for image_orig, results_dict_for_rendering in zip(images, human_smpl):
image = image_orig.permute(1,2,0)*255
image = image.numpy()
if results_dict_for_rendering is not None:
rendering_results = renderer.render_all_multiple(
results_dict_for_rendering["verts"],
cam_t=results_dict_for_rendering["cam_t"],
render_res=results_dict_for_rendering["render_res"], **misc_args
)
# Overlay image
valid_mask = rendering_results["Image"][:, :, -1][:, :, np.newaxis]
cam_view = (
valid_mask * rendering_results["Image"][:, :, [2, 1, 0]]
+ (1 - valid_mask) * image.astype(np.float32)[:, :, ::-1] / 255
)
rendered_images.append(torch.from_numpy(cam_view[:, :, ::-1].copy()))
semantic_maps.append(torch.from_numpy(rendering_results.get("SemanticMap")))
masks.append(torch.from_numpy(rendering_results.get("Mask")[:, :, 0]))
else:
rendered_images.append(image_orig)
semantic_maps.append(torch.ones_like(image_orig))
masks.append(torch.zeros_like(image_orig))
results.append(results_dict_for_rendering) # still in np
smpl = {
'results': results,
'width': width,
'height': height,
'model_cfg': model_cfg,
'faces': faces,
'lbs': lbs,
'viewport_size': (720, 720),
'misc_args': misc_args
}
return {
'smpl': smpl,
'images': torch.stack(rendered_images),
'masks': torch.stack(masks).to(torch.float16),
'semantic_maps': torch.stack(semantic_maps),
}
+294
View File
@@ -0,0 +1,294 @@
import os
import platform
if "PYOPENGL_PLATFORM" not in os.environ:
os.environ["PYOPENGL_PLATFORM"] = "egl"
import torch
import numpy as np
import pyrender
import trimesh
import cv2
from yacs.config import CfgNode
from typing import List, Optional
from ..humans4d.hmr2.utils.renderer import Renderer
# For Windows, remove PYOPENGL_PLATFORM to enable default rendering backend
sys_name = platform.system()
if sys_name == "Windows":
os.environ.pop("PYOPENGL_PLATFORM")
def get_light_poses(n_lights=5, elevation=np.pi / 3, dist=12):
# get lights in a circle around origin at elevation
thetas = elevation * np.ones(n_lights)
phis = 2 * np.pi * np.arange(n_lights) / n_lights
poses = []
trans = make_translation(torch.tensor([0, 0, dist]))
for phi, theta in zip(phis, thetas):
rot = make_rotation(rx=-theta, ry=phi, order="xyz")
poses.append((rot @ trans).numpy())
return poses
def make_translation(t):
return make_4x4_pose(torch.eye(3), t)
def make_rotation(rx=0, ry=0, rz=0, order="xyz"):
Rx = rotx(rx)
Ry = roty(ry)
Rz = rotz(rz)
if order == "xyz":
R = Rz @ Ry @ Rx
elif order == "xzy":
R = Ry @ Rz @ Rx
elif order == "yxz":
R = Rz @ Rx @ Ry
elif order == "yzx":
R = Rx @ Rz @ Ry
elif order == "zyx":
R = Rx @ Ry @ Rz
elif order == "zxy":
R = Ry @ Rx @ Rz
return make_4x4_pose(R, torch.zeros(3))
def make_4x4_pose(R, t):
"""
:param R (*, 3, 3)
:param t (*, 3)
return (*, 4, 4)
"""
dims = R.shape[:-2]
pose_3x4 = torch.cat([R, t.view(*dims, 3, 1)], dim=-1)
bottom = (
torch.tensor([0, 0, 0, 1], device=R.device)
.reshape(*(1,) * len(dims), 1, 4)
.expand(*dims, 1, 4)
)
return torch.cat([pose_3x4, bottom], dim=-2)
def rotx(theta):
return torch.tensor(
[
[1, 0, 0],
[0, np.cos(theta), -np.sin(theta)],
[0, np.sin(theta), np.cos(theta)],
],
dtype=torch.float32,
)
def roty(theta):
return torch.tensor(
[
[np.cos(theta), 0, np.sin(theta)],
[0, 1, 0],
[-np.sin(theta), 0, np.cos(theta)],
],
dtype=torch.float32,
)
def rotz(theta):
return torch.tensor(
[
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0],
[0, 0, 1],
],
dtype=torch.float32,
)
def create_raymond_lights() -> List[pyrender.Node]:
"""
Return raymond light nodes for the scene.
"""
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
class SemanticRenderer(Renderer):
def __init__(
self, cfg: CfgNode, faces: np.array, lbs: np.array, viewport_size=(768, 768)
):
"""
Wrapper around the pyrender renderer to render SMPL meshes's semantic map for Champ.
Args:
cfg (CfgNode): Model config file.
faces (np.array): Array of shape (F, 3) containing the mesh faces.
"""
self.cfg = cfg
self.focal_length = cfg.EXTRA.FOCAL_LENGTH
self.img_res = cfg.MODEL.IMAGE_SIZE
self.camera_center = [self.img_res // 2, self.img_res // 2]
self.faces = faces
self.lbs = lbs
# self.joint_names = smplx.joint_names.JOINT_NAMES
self.vertex_labels = np.argmax(self.lbs.cpu().numpy(), axis=1)
vertex_colors = cv2.applyColorMap(
(10 * self.vertex_labels).astype(np.uint8), cv2.COLORMAP_VIRIDIS
)
semantic_background_rgb = cv2.applyColorMap(np.uint8([0]), cv2.COLORMAP_VIRIDIS)
self.vertex_colors = np.squeeze(vertex_colors, axis=1)
self.semantic_background_rgb = (
np.squeeze(semantic_background_rgb.astype(np.float32), axis=1) / 255
)
self.renderer = pyrender.OffscreenRenderer(
viewport_width=viewport_size[0],
viewport_height=viewport_size[1],
point_size=1.0,
)
def vertices_to_trimesh(
self,
vertices,
camera_translation,
mesh_base_color=(1.0, 1.0, 0.9),
rot_axis=[1, 0, 0],
rot_angle=0,
):
vertex_colors = np.array([(*mesh_base_color, 1.0)] * vertices.shape[0])
mesh = trimesh.Trimesh(
vertices.copy() + camera_translation,
self.faces.copy(),
vertex_colors=vertex_colors,
)
# mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy())
rot = trimesh.transformations.rotation_matrix(np.radians(rot_angle), rot_axis)
mesh.apply_transform(rot)
rot = trimesh.transformations.rotation_matrix(np.radians(180), [1, 0, 0])
mesh.apply_transform(rot)
return mesh
def render_all_multiple(
self,
vertices: List[np.array],
cam_t: List[np.array],
rot_axis=[1, 0, 0],
rot_angle=0,
mesh_base_color=(1.0, 1.0, 0.9),
scene_bg_color=(0, 0, 0),
render_res=[256, 256],
focal_length=None,
):
renderer = self.renderer
trimesh_list = [
self.vertices_to_trimesh(
vvv, ttt.copy(), mesh_base_color, rot_axis, rot_angle
)
for vvv, ttt in zip(vertices, cam_t)
]
for trimesh in trimesh_list:
trimesh.visual.vertex_colors = self.vertex_colors
mesh_list = [pyrender.Mesh.from_trimesh(trimesh) for trimesh in trimesh_list]
scene = pyrender.Scene(
bg_color=[*scene_bg_color, 0.0], ambient_light=(0.3, 0.3, 0.3)
)
for i, mesh in enumerate(mesh_list):
scene.add(mesh, f"mesh_{i}")
camera_pose = np.eye(4)
camera_center = [render_res[0] / 2.0, render_res[1] / 2.0]
focal_length = focal_length if focal_length is not None else self.focal_length
camera = pyrender.IntrinsicsCamera(
fx=focal_length,
fy=focal_length,
cx=camera_center[0],
cy=camera_center[1],
zfar=1e12,
)
# Create camera node and add it to pyRender scene
camera_node = pyrender.Node(camera=camera, matrix=camera_pose)
scene.add_node(camera_node)
self.add_point_lighting(scene, camera_node)
self.add_lighting(scene, camera_node)
light_nodes = create_raymond_lights()
for node in light_nodes:
scene.add_node(node)
color, rend_depth = renderer.render(
scene, flags=pyrender.RenderFlags.FLAT | pyrender.RenderFlags.RGBA
)
color = color.astype(np.float32) / 255.0
# renderer.delete()
valid_mask = (color[:, :, -1])[:, :, np.newaxis]
semantic_map = (
color[:, :, :3] * valid_mask
+ (1 - valid_mask) * self.semantic_background_rgb
)
semantic_map = semantic_map.astype(np.float32)
return {
"Image": color,
"Mask": valid_mask.astype(bool),
"SemanticMap": semantic_map,
"Depth": rend_depth,
}
def add_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0):
# from phalp.visualize.py_renderer import get_light_poses
light_poses = get_light_poses()
light_poses.append(np.eye(4))
cam_pose = scene.get_pose(cam_node)
for i, pose in enumerate(light_poses):
matrix = cam_pose @ pose
node = pyrender.Node(
name=f"light-{i:02d}",
light=pyrender.DirectionalLight(color=color, intensity=intensity),
matrix=matrix,
)
if scene.has_node(node):
continue
scene.add_node(node)
def add_point_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0):
# from phalp.visualize.py_renderer import get_light_poses
light_poses = get_light_poses(dist=0.5)
light_poses.append(np.eye(4))
cam_pose = scene.get_pose(cam_node)
for i, pose in enumerate(light_poses):
matrix = cam_pose @ pose
# node = pyrender.Node(
# name=f"light-{i:02d}",
# light=pyrender.DirectionalLight(color=color, intensity=intensity),
# matrix=matrix,
# )
node = pyrender.Node(
name=f"plight-{i:02d}",
light=pyrender.PointLight(color=color, intensity=intensity),
matrix=matrix,
)
if scene.has_node(node):
continue
scene.add_node(node)