init
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
**/__pycache__
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
|
||||
Linux
|
||||
```
|
||||
apt-get install freeglut3-dev
|
||||
apt-get install libosmesa6-dev
|
||||
```
|
||||
+29
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: {}
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
[](https://arxiv.org/pdf/2305.20091.pdf) [](https://shubham-goel.github.io/4dhumans/) [](https://colab.research.google.com/drive/1Ex4gE5v1bPR3evfhtG7sDHxQGsWwNwby?usp=sharing) [](https://huggingface.co/spaces/brjathu/HMR2.0)
|
||||
|
||||
|
||||

|
||||
|
||||
## 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 |
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/')
|
||||
@@ -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
@@ -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
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
from .smpl_head import build_smpl_head
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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,)
|
||||
@@ -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,)
|
||||
@@ -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'])
|
||||
@@ -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'])
|
||||
@@ -0,0 +1,4 @@
|
||||
# git+https://github.com/shubham-goel/4D-Humans
|
||||
git+https://github.com/facebookresearch/detectron2
|
||||
pyrender
|
||||
yacs
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user