SMPL-X based Animatable Avatar
Browse files- .gitignore +1 -0
- README.md +26 -14
- apps/avatarizer.py +95 -63
- apps/infer.py +1 -1
- docs/installation.md +1 -1
- lib/smplx/body_models.py +23 -1
- lib/smplx/lbs.py +17 -3
.gitignore
CHANGED
|
@@ -16,3 +16,4 @@ build
|
|
| 16 |
dist
|
| 17 |
*egg-info
|
| 18 |
*.so
|
|
|
|
|
|
| 16 |
dist
|
| 17 |
*egg-info
|
| 18 |
*.so
|
| 19 |
+
run.sh
|
README.md
CHANGED
|
@@ -23,6 +23,8 @@
|
|
| 23 |
<br>
|
| 24 |
<a href="https://pytorch.org/get-started/locally/"><img alt="PyTorch" src="https://img.shields.io/badge/PyTorch-ee4c2c?logo=pytorch&logoColor=white"></a>
|
| 25 |
<a href="https://pytorchlightning.ai/"><img alt="Lightning" src="https://img.shields.io/badge/-Lightning-792ee5?logo=pytorchlightning&logoColor=white"></a>
|
|
|
|
|
|
|
| 26 |
<br></br>
|
| 27 |
<a href=''>
|
| 28 |
<img src='https://img.shields.io/badge/Paper-PDF (coming soon)-green?style=for-the-badge&logo=arXiv&logoColor=green' alt='Paper PDF'>
|
|
@@ -36,7 +38,7 @@
|
|
| 36 |
|
| 37 |
<br/>
|
| 38 |
|
| 39 |
-
ECON is designed for
|
| 40 |
<br/>
|
| 41 |
<br/>
|
| 42 |
|
|
@@ -61,6 +63,9 @@ ECON is designed for **"Human digitization from a color image"**, which combines
|
|
| 61 |
<li>
|
| 62 |
<a href="#demo">Demo</a>
|
| 63 |
</li>
|
|
|
|
|
|
|
|
|
|
| 64 |
<li>
|
| 65 |
<a href="#tricks">Tricks</a>
|
| 66 |
</li>
|
|
@@ -87,6 +92,9 @@ python -m apps.infer -cfg ./configs/econ.yaml -in_dir ./examples -out_dir ./resu
|
|
| 87 |
|
| 88 |
# To generate the demo video of reconstruction results
|
| 89 |
python -m apps.multi_render -n {filename}
|
|
|
|
|
|
|
|
|
|
| 90 |
```
|
| 91 |
|
| 92 |
## Tricks
|
|
@@ -101,24 +109,28 @@ python -m apps.multi_render -n {filename}
|
|
| 101 |
- ["hand"]: only use the **visible** hands from SMPL-X
|
| 102 |
- ["hand", "face"]: use both **visible** hands and face from SMPL-X
|
| 103 |
- `thickness: 2cm`
|
| 104 |
-
- could be increased accordingly in case **xx_full.obj** looks flat
|
| 105 |
-
- `hps_type:
|
| 106 |
- "pixie": more accurate for face and hands
|
| 107 |
- "pymafx": more robust for challenging poses
|
|
|
|
|
|
|
| 108 |
|
| 109 |
<br/>
|
| 110 |
|
| 111 |
## More Qualitative Results
|
| 112 |
|
| 113 |
-
|
|
| 114 |
-
|
|
| 115 |
-
|
|
| 116 |
-
|
|
| 117 |
-
|
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
<br/>
|
| 124 |
<br/>
|
|
@@ -127,7 +139,7 @@ python -m apps.multi_render -n {filename}
|
|
| 127 |
|
| 128 |
```bibtex
|
| 129 |
@misc{xiu2022econ,
|
| 130 |
-
title={ECON: Explicit Clothed humans Obtained from Normals},
|
| 131 |
author={Xiu, Yuliang and Yang, Jinlong and Cao, Xu and Tzionas, Dimitrios and Black, Michael J.},
|
| 132 |
year={2022}
|
| 133 |
publisher={arXiv},
|
|
@@ -146,6 +158,7 @@ Here are some great resources we benefit from:
|
|
| 146 |
- [ICON](https://github.com/YuliangXiu/ICON) for Body Fitting
|
| 147 |
- [MonoPortDataset](https://github.com/Project-Splinter/MonoPortDataset) for Data Processing
|
| 148 |
- [rembg](https://github.com/danielgatis/rembg) for Human Segmentation
|
|
|
|
| 149 |
- [smplx](https://github.com/vchoutas/smplx), [PyMAF-X](https://www.liuyebin.com/pymaf-x/), [PIXIE](https://github.com/YadiraF/PIXIE) for Human Pose & Shape Estimation
|
| 150 |
- [CAPE](https://github.com/qianlim/CAPE) and [THuman](https://github.com/ZhengZerong/DeepHuman/tree/master/THUmanDataset) for Dataset
|
| 151 |
- [PyTorch3D](https://github.com/facebookresearch/pytorch3d) for Differential Rendering
|
|
@@ -171,4 +184,3 @@ MJB has received research gift funds from Adobe, Intel, Nvidia, Meta/Facebook, a
|
|
| 171 |
For technical questions, please contact [email protected]
|
| 172 |
|
| 173 |
For commercial licensing, please contact [email protected]
|
| 174 |
-
|
|
|
|
| 23 |
<br>
|
| 24 |
<a href="https://pytorch.org/get-started/locally/"><img alt="PyTorch" src="https://img.shields.io/badge/PyTorch-ee4c2c?logo=pytorch&logoColor=white"></a>
|
| 25 |
<a href="https://pytorchlightning.ai/"><img alt="Lightning" src="https://img.shields.io/badge/-Lightning-792ee5?logo=pytorchlightning&logoColor=white"></a>
|
| 26 |
+
<a href="https://cupy.dev/"><img alt="cupy" src="https://img.shields.io/badge/-Cupy-46C02B?logo=numpy&logoColor=white"></a>
|
| 27 |
+
<a href="https://twitter.com/yuliangxiu"><img alt='Twitter' src="https://img.shields.io/twitter/follow/yuliangxiu?label=%40yuliangxiu"></a>
|
| 28 |
<br></br>
|
| 29 |
<a href=''>
|
| 30 |
<img src='https://img.shields.io/badge/Paper-PDF (coming soon)-green?style=for-the-badge&logo=arXiv&logoColor=green' alt='Paper PDF'>
|
|
|
|
| 38 |
|
| 39 |
<br/>
|
| 40 |
|
| 41 |
+
ECON is designed for "Human digitization from a color image", which combines the best properties of implicit and explicit representations, to infer high-fidelity 3D clothed humans from in-the-wild images, even with **loose clothing** or in **challenging poses**. ECON also supports **multi-person reconstruction** and **SMPL-X based animation**.
|
| 42 |
<br/>
|
| 43 |
<br/>
|
| 44 |
|
|
|
|
| 63 |
<li>
|
| 64 |
<a href="#demo">Demo</a>
|
| 65 |
</li>
|
| 66 |
+
<li>
|
| 67 |
+
<a href="#applications">Applications</a>
|
| 68 |
+
</li>
|
| 69 |
<li>
|
| 70 |
<a href="#tricks">Tricks</a>
|
| 71 |
</li>
|
|
|
|
| 92 |
|
| 93 |
# To generate the demo video of reconstruction results
|
| 94 |
python -m apps.multi_render -n {filename}
|
| 95 |
+
|
| 96 |
+
# To animate the reconstruction with SMPL-X pose parameters
|
| 97 |
+
python -m apps.avatarizer -n {filename}
|
| 98 |
```
|
| 99 |
|
| 100 |
## Tricks
|
|
|
|
| 109 |
- ["hand"]: only use the **visible** hands from SMPL-X
|
| 110 |
- ["hand", "face"]: use both **visible** hands and face from SMPL-X
|
| 111 |
- `thickness: 2cm`
|
| 112 |
+
- could be increased accordingly in case final reconstruction **xx_full.obj** looks flat
|
| 113 |
+
- `hps_type: PIXIE`
|
| 114 |
- "pixie": more accurate for face and hands
|
| 115 |
- "pymafx": more robust for challenging poses
|
| 116 |
+
- `k: 4`
|
| 117 |
+
- could be reduced accordingly in case the surface of **xx_full.obj** has discontinous artifacts
|
| 118 |
|
| 119 |
<br/>
|
| 120 |
|
| 121 |
## More Qualitative Results
|
| 122 |
|
| 123 |
+
|  |
|
| 124 |
+
| :------------------------------------: |
|
| 125 |
+
| _Challenging Poses_ |
|
| 126 |
+
|  |
|
| 127 |
+
| _Loose Clothes_ |
|
| 128 |
+
|
| 129 |
+
## Applications
|
| 130 |
+
|
| 131 |
+
|  |  |
|
| 132 |
+
| :----------------------------------------------------------------------------------------------------: | :-----------------------------------------: |
|
| 133 |
+
| _ECON could provide pseudo 3D GT for [SHHQ Dataset](https://github.com/stylegan-human/StyleGAN-Human)_ | _ECON supports multi-person reconstruction_ |
|
| 134 |
|
| 135 |
<br/>
|
| 136 |
<br/>
|
|
|
|
| 139 |
|
| 140 |
```bibtex
|
| 141 |
@misc{xiu2022econ,
|
| 142 |
+
title={{ECON: Explicit Clothed humans Obtained from Normals}},
|
| 143 |
author={Xiu, Yuliang and Yang, Jinlong and Cao, Xu and Tzionas, Dimitrios and Black, Michael J.},
|
| 144 |
year={2022}
|
| 145 |
publisher={arXiv},
|
|
|
|
| 158 |
- [ICON](https://github.com/YuliangXiu/ICON) for Body Fitting
|
| 159 |
- [MonoPortDataset](https://github.com/Project-Splinter/MonoPortDataset) for Data Processing
|
| 160 |
- [rembg](https://github.com/danielgatis/rembg) for Human Segmentation
|
| 161 |
+
- [PyTorch-NICP](https://github.com/wuhaozhe/pytorch-nicp) for non-rigid registration
|
| 162 |
- [smplx](https://github.com/vchoutas/smplx), [PyMAF-X](https://www.liuyebin.com/pymaf-x/), [PIXIE](https://github.com/YadiraF/PIXIE) for Human Pose & Shape Estimation
|
| 163 |
- [CAPE](https://github.com/qianlim/CAPE) and [THuman](https://github.com/ZhengZerong/DeepHuman/tree/master/THUmanDataset) for Dataset
|
| 164 |
- [PyTorch3D](https://github.com/facebookresearch/pytorch3d) for Differential Rendering
|
|
|
|
| 184 |
For technical questions, please contact [email protected]
|
| 185 |
|
| 186 |
For commercial licensing, please contact [email protected]
|
|
|
apps/avatarizer.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import numpy as np
|
| 2 |
import trimesh
|
| 3 |
import torch
|
|
|
|
| 4 |
import os.path as osp
|
| 5 |
import lib.smplx as smplx
|
| 6 |
from pytorch3d.ops import SubdivideMeshes
|
|
@@ -12,10 +13,16 @@ from scipy.spatial import cKDTree
|
|
| 12 |
from lib.dataset.mesh_util import SMPLX
|
| 13 |
from lib.common.local_affine import register
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
smplx_container = SMPLX()
|
| 16 |
-
device = torch.device("cuda:
|
| 17 |
|
| 18 |
-
prefix = "./results/
|
| 19 |
smpl_path = f"{prefix}_smpl_00.npy"
|
| 20 |
econ_path = f"{prefix}_0_full.obj"
|
| 21 |
|
|
@@ -27,7 +34,6 @@ econ_obj.vertices -= smplx_param["transl"].cpu().numpy()
|
|
| 27 |
|
| 28 |
for key in smplx_param.keys():
|
| 29 |
smplx_param[key] = smplx_param[key].cpu().view(1, -1)
|
| 30 |
-
# print(key, smplx_param[key].device, smplx_param[key].shape)
|
| 31 |
|
| 32 |
smpl_model = smplx.create(
|
| 33 |
smplx_container.model_dir,
|
|
@@ -40,109 +46,135 @@ smpl_model = smplx.create(
|
|
| 40 |
num_expression_coeffs=50,
|
| 41 |
ext='pkl')
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
smpl_tree = cKDTree(smpl_verts.cpu().numpy())
|
| 58 |
dist, idx = smpl_tree.query(econ_obj.vertices, k=5)
|
| 59 |
|
| 60 |
-
if not osp.exists(f"{prefix}
|
| 61 |
|
| 62 |
-
#
|
| 63 |
econ_verts = torch.tensor(econ_obj.vertices).float()
|
| 64 |
-
|
| 65 |
homo_coord = torch.ones_like(econ_verts)[..., :1]
|
| 66 |
-
econ_cano_verts =
|
| 67 |
econ_cano_verts = econ_cano_verts[:, :3, 0].cpu()
|
| 68 |
econ_cano = trimesh.Trimesh(econ_cano_verts, econ_obj.faces)
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
# remove hands from ECON for next registeration
|
| 79 |
-
|
| 80 |
mano_mask = ~np.isin(idx[:, 0], smplx_container.smplx_mano_vid)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
|
| 85 |
# remove SMPL-X hand and face
|
| 86 |
register_mask = ~np.isin(
|
| 87 |
-
np.arange(
|
| 88 |
np.concatenate([smplx_container.smplx_mano_vid, smplx_container.smplx_front_flame_vid]))
|
| 89 |
register_mask *= ~smplx_container.eyeball_vertex_mask.bool().numpy()
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# upsample the
|
| 96 |
-
|
| 97 |
-
verts=[torch.tensor(
|
| 98 |
-
faces=[torch.tensor(
|
| 99 |
).to(device)
|
| 100 |
-
sm = SubdivideMeshes(
|
| 101 |
-
|
| 102 |
|
| 103 |
# remove over-streched+hand faces from ECON
|
| 104 |
-
|
| 105 |
edge_before = np.sqrt(
|
| 106 |
((econ_obj.vertices[econ_cano.edges[:, 0]] - econ_obj.vertices[econ_cano.edges[:, 1]])**2).sum(axis=1))
|
| 107 |
-
edge_after = np.sqrt(
|
| 108 |
-
((econ_cano.vertices[econ_cano.edges[:, 0]] - econ_cano.vertices[econ_cano.edges[:, 1]])**2).sum(axis=1))
|
| 109 |
edge_diff = edge_after / edge_before.clip(1e-2)
|
| 110 |
streched_mask = np.unique(econ_cano.edges[edge_diff > 6])
|
| 111 |
mano_mask = ~np.isin(idx[:, 0], smplx_container.smplx_mano_vid)
|
| 112 |
mano_mask[streched_mask] = False
|
| 113 |
-
|
| 114 |
-
|
| 115 |
|
| 116 |
# stitch the registered SMPL-X body and floating hands to ECON
|
| 117 |
-
|
| 118 |
-
dist, idx =
|
| 119 |
-
|
| 120 |
-
|
| 121 |
|
| 122 |
-
smpl_hand =
|
| 123 |
smpl_hand.update_faces(smplx_container.mano_vertex_mask.numpy()[smpl_hand.faces].all(axis=1))
|
| 124 |
smpl_hand.remove_unreferenced_vertices()
|
| 125 |
-
|
| 126 |
-
|
| 127 |
else:
|
| 128 |
-
|
| 129 |
-
|
| 130 |
|
| 131 |
-
smpl_tree = cKDTree(
|
| 132 |
-
dist, idx = smpl_tree.query(
|
| 133 |
knn_weights = np.exp(-dist**2)
|
| 134 |
knn_weights /= knn_weights.sum(axis=1, keepdims=True)
|
|
|
|
| 135 |
econ_J_regressor = (smpl_model.J_regressor[:, idx] * knn_weights[None]).sum(axis=-1)
|
| 136 |
econ_lbs_weights = (smpl_model.lbs_weights.T[:, idx] * knn_weights[None]).sum(axis=-1).T
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
econ_J_regressor /= econ_J_regressor.sum(axis=1, keepdims=True)
|
| 138 |
econ_lbs_weights /= econ_lbs_weights.sum(axis=1, keepdims=True)
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
posed_econ_verts, _ = general_lbs(
|
| 141 |
-
pose=
|
| 142 |
-
v_template=
|
|
|
|
| 143 |
J_regressor=econ_J_regressor,
|
| 144 |
parents=smpl_model.parents,
|
| 145 |
lbs_weights=econ_lbs_weights)
|
| 146 |
|
| 147 |
-
econ_pose = trimesh.Trimesh(posed_econ_verts[0].detach(),
|
| 148 |
econ_pose.export(f"{prefix}_econ_pose.obj")
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import trimesh
|
| 3 |
import torch
|
| 4 |
+
import argparse
|
| 5 |
import os.path as osp
|
| 6 |
import lib.smplx as smplx
|
| 7 |
from pytorch3d.ops import SubdivideMeshes
|
|
|
|
| 13 |
from lib.dataset.mesh_util import SMPLX
|
| 14 |
from lib.common.local_affine import register
|
| 15 |
|
| 16 |
+
# loading cfg file
|
| 17 |
+
parser = argparse.ArgumentParser()
|
| 18 |
+
parser.add_argument("-n", "--name", type=str, default="")
|
| 19 |
+
parser.add_argument("-g", "--gpu", type=int, default=0)
|
| 20 |
+
args = parser.parse_args()
|
| 21 |
+
|
| 22 |
smplx_container = SMPLX()
|
| 23 |
+
device = torch.device(f"cuda:{args.gpu}")
|
| 24 |
|
| 25 |
+
prefix = f"./results/econ/obj/{args.name}"
|
| 26 |
smpl_path = f"{prefix}_smpl_00.npy"
|
| 27 |
econ_path = f"{prefix}_0_full.obj"
|
| 28 |
|
|
|
|
| 34 |
|
| 35 |
for key in smplx_param.keys():
|
| 36 |
smplx_param[key] = smplx_param[key].cpu().view(1, -1)
|
|
|
|
| 37 |
|
| 38 |
smpl_model = smplx.create(
|
| 39 |
smplx_container.model_dir,
|
|
|
|
| 46 |
num_expression_coeffs=50,
|
| 47 |
ext='pkl')
|
| 48 |
|
| 49 |
+
smpl_out_lst = []
|
| 50 |
+
|
| 51 |
+
for pose_type in ["t-pose", "da-pose", "pose"]:
|
| 52 |
+
smpl_out_lst.append(
|
| 53 |
+
smpl_model(
|
| 54 |
+
body_pose=smplx_param["body_pose"],
|
| 55 |
+
global_orient=smplx_param["global_orient"],
|
| 56 |
+
betas=smplx_param["betas"],
|
| 57 |
+
expression=smplx_param["expression"],
|
| 58 |
+
jaw_pose=smplx_param["jaw_pose"],
|
| 59 |
+
left_hand_pose=smplx_param["left_hand_pose"],
|
| 60 |
+
right_hand_pose=smplx_param["right_hand_pose"],
|
| 61 |
+
return_verts=True,
|
| 62 |
+
return_full_pose=True,
|
| 63 |
+
return_joint_transformation=True,
|
| 64 |
+
return_vertex_transformation=True,
|
| 65 |
+
pose_type=pose_type))
|
| 66 |
+
|
| 67 |
+
smpl_verts = smpl_out_lst[2].vertices.detach()[0]
|
| 68 |
smpl_tree = cKDTree(smpl_verts.cpu().numpy())
|
| 69 |
dist, idx = smpl_tree.query(econ_obj.vertices, k=5)
|
| 70 |
|
| 71 |
+
if not osp.exists(f"{prefix}_econ_da.obj") or not osp.exists(f"{prefix}_smpl_da.obj"):
|
| 72 |
|
| 73 |
+
# t-pose for ECON
|
| 74 |
econ_verts = torch.tensor(econ_obj.vertices).float()
|
| 75 |
+
rot_mat_t = smpl_out_lst[2].vertex_transformation.detach()[0][idx[:, 0]]
|
| 76 |
homo_coord = torch.ones_like(econ_verts)[..., :1]
|
| 77 |
+
econ_cano_verts = torch.inverse(rot_mat_t) @ torch.cat([econ_verts, homo_coord], dim=1).unsqueeze(-1)
|
| 78 |
econ_cano_verts = econ_cano_verts[:, :3, 0].cpu()
|
| 79 |
econ_cano = trimesh.Trimesh(econ_cano_verts, econ_obj.faces)
|
| 80 |
|
| 81 |
+
# da-pose for ECON
|
| 82 |
+
rot_mat_da = smpl_out_lst[1].vertex_transformation.detach()[0][idx[:, 0]]
|
| 83 |
+
econ_da_verts = rot_mat_da @ torch.cat([econ_cano_verts, homo_coord], dim=1).unsqueeze(-1)
|
| 84 |
+
econ_da = trimesh.Trimesh(econ_da_verts[:, :3, 0].cpu(), econ_obj.faces)
|
| 85 |
+
|
| 86 |
+
# da-pose for SMPL-X
|
| 87 |
+
smpl_da = trimesh.Trimesh(smpl_out_lst[1].vertices.detach()[0], smpl_model.faces, maintain_orders=True, process=False)
|
| 88 |
+
smpl_da.export(f"{prefix}_smpl_da.obj")
|
| 89 |
|
| 90 |
# remove hands from ECON for next registeration
|
| 91 |
+
econ_da_body = econ_da.copy()
|
| 92 |
mano_mask = ~np.isin(idx[:, 0], smplx_container.smplx_mano_vid)
|
| 93 |
+
econ_da_body.update_faces(mano_mask[econ_da.faces].all(axis=1))
|
| 94 |
+
econ_da_body.remove_unreferenced_vertices()
|
| 95 |
+
econ_da_body = keep_largest(econ_da_body)
|
| 96 |
|
| 97 |
# remove SMPL-X hand and face
|
| 98 |
register_mask = ~np.isin(
|
| 99 |
+
np.arange(smpl_da.vertices.shape[0]),
|
| 100 |
np.concatenate([smplx_container.smplx_mano_vid, smplx_container.smplx_front_flame_vid]))
|
| 101 |
register_mask *= ~smplx_container.eyeball_vertex_mask.bool().numpy()
|
| 102 |
+
smpl_da_body = smpl_da.copy()
|
| 103 |
+
smpl_da_body.update_faces(register_mask[smpl_da.faces].all(axis=1))
|
| 104 |
+
smpl_da_body.remove_unreferenced_vertices()
|
| 105 |
+
smpl_da_body = keep_largest(smpl_da_body)
|
| 106 |
+
|
| 107 |
+
# upsample the smpl_da_body and do registeration
|
| 108 |
+
smpl_da_body = Meshes(
|
| 109 |
+
verts=[torch.tensor(smpl_da_body.vertices).float()],
|
| 110 |
+
faces=[torch.tensor(smpl_da_body.faces).long()],
|
| 111 |
).to(device)
|
| 112 |
+
sm = SubdivideMeshes(smpl_da_body)
|
| 113 |
+
smpl_da_body = register(econ_da_body, sm(smpl_da_body), device)
|
| 114 |
|
| 115 |
# remove over-streched+hand faces from ECON
|
| 116 |
+
econ_da_body = econ_da.copy()
|
| 117 |
edge_before = np.sqrt(
|
| 118 |
((econ_obj.vertices[econ_cano.edges[:, 0]] - econ_obj.vertices[econ_cano.edges[:, 1]])**2).sum(axis=1))
|
| 119 |
+
edge_after = np.sqrt(((econ_da.vertices[econ_cano.edges[:, 0]] - econ_da.vertices[econ_cano.edges[:, 1]])**2).sum(axis=1))
|
|
|
|
| 120 |
edge_diff = edge_after / edge_before.clip(1e-2)
|
| 121 |
streched_mask = np.unique(econ_cano.edges[edge_diff > 6])
|
| 122 |
mano_mask = ~np.isin(idx[:, 0], smplx_container.smplx_mano_vid)
|
| 123 |
mano_mask[streched_mask] = False
|
| 124 |
+
econ_da_body.update_faces(mano_mask[econ_cano.faces].all(axis=1))
|
| 125 |
+
econ_da_body.remove_unreferenced_vertices()
|
| 126 |
|
| 127 |
# stitch the registered SMPL-X body and floating hands to ECON
|
| 128 |
+
econ_da_tree = cKDTree(econ_da.vertices)
|
| 129 |
+
dist, idx = econ_da_tree.query(smpl_da_body.vertices, k=1)
|
| 130 |
+
smpl_da_body.update_faces((dist > 0.02)[smpl_da_body.faces].all(axis=1))
|
| 131 |
+
smpl_da_body.remove_unreferenced_vertices()
|
| 132 |
|
| 133 |
+
smpl_hand = smpl_da.copy()
|
| 134 |
smpl_hand.update_faces(smplx_container.mano_vertex_mask.numpy()[smpl_hand.faces].all(axis=1))
|
| 135 |
smpl_hand.remove_unreferenced_vertices()
|
| 136 |
+
econ_da = sum([smpl_hand, smpl_da_body, econ_da_body])
|
| 137 |
+
econ_da = poisson(econ_da, f"{prefix}_econ_da.obj")
|
| 138 |
else:
|
| 139 |
+
econ_da = trimesh.load(f"{prefix}_econ_da.obj")
|
| 140 |
+
smpl_da = trimesh.load(f"{prefix}_smpl_da.obj", maintain_orders=True, process=False)
|
| 141 |
|
| 142 |
+
smpl_tree = cKDTree(smpl_da.vertices)
|
| 143 |
+
dist, idx = smpl_tree.query(econ_da.vertices, k=5)
|
| 144 |
knn_weights = np.exp(-dist**2)
|
| 145 |
knn_weights /= knn_weights.sum(axis=1, keepdims=True)
|
| 146 |
+
|
| 147 |
econ_J_regressor = (smpl_model.J_regressor[:, idx] * knn_weights[None]).sum(axis=-1)
|
| 148 |
econ_lbs_weights = (smpl_model.lbs_weights.T[:, idx] * knn_weights[None]).sum(axis=-1).T
|
| 149 |
+
|
| 150 |
+
num_posedirs = smpl_model.posedirs.shape[0]
|
| 151 |
+
econ_posedirs = (smpl_model.posedirs.view(num_posedirs, -1, 3)[:, idx, :] *
|
| 152 |
+
knn_weights[None, ..., None]).sum(axis=-2).view(num_posedirs, -1).float()
|
| 153 |
+
|
| 154 |
econ_J_regressor /= econ_J_regressor.sum(axis=1, keepdims=True)
|
| 155 |
econ_lbs_weights /= econ_lbs_weights.sum(axis=1, keepdims=True)
|
| 156 |
|
| 157 |
+
# re-compute da-pose rot_mat for ECON
|
| 158 |
+
rot_mat_da = smpl_out_lst[1].vertex_transformation.detach()[0][idx[:, 0]]
|
| 159 |
+
econ_da_verts = torch.tensor(econ_da.vertices).float()
|
| 160 |
+
econ_cano_verts = torch.inverse(rot_mat_da) @ torch.cat([econ_da_verts, torch.ones_like(econ_da_verts)[..., :1]],
|
| 161 |
+
dim=1).unsqueeze(-1)
|
| 162 |
+
econ_cano_verts = econ_cano_verts[:, :3, 0].double()
|
| 163 |
+
|
| 164 |
+
# ----------------------------------------------------
|
| 165 |
+
# use any SMPL-X pose to animate ECON reconstruction
|
| 166 |
+
# ----------------------------------------------------
|
| 167 |
+
|
| 168 |
+
new_pose = smpl_out_lst[2].full_pose
|
| 169 |
+
new_pose[:, :3] = 0.
|
| 170 |
+
|
| 171 |
posed_econ_verts, _ = general_lbs(
|
| 172 |
+
pose=new_pose,
|
| 173 |
+
v_template=econ_cano_verts.unsqueeze(0),
|
| 174 |
+
posedirs=econ_posedirs,
|
| 175 |
J_regressor=econ_J_regressor,
|
| 176 |
parents=smpl_model.parents,
|
| 177 |
lbs_weights=econ_lbs_weights)
|
| 178 |
|
| 179 |
+
econ_pose = trimesh.Trimesh(posed_econ_verts[0].detach(), econ_da.faces)
|
| 180 |
econ_pose.export(f"{prefix}_econ_pose.obj")
|
apps/infer.py
CHANGED
|
@@ -100,7 +100,7 @@ if __name__ == "__main__":
|
|
| 100 |
print(colored("Use SMPL-X (Explicit) for completion", "green"))
|
| 101 |
|
| 102 |
dataset = TestDataset(dataset_param, device)
|
| 103 |
-
|
| 104 |
print(colored(f"Dataset Size: {len(dataset)}", "green"))
|
| 105 |
|
| 106 |
pbar = tqdm(dataset)
|
|
|
|
| 100 |
print(colored("Use SMPL-X (Explicit) for completion", "green"))
|
| 101 |
|
| 102 |
dataset = TestDataset(dataset_param, device)
|
| 103 |
+
|
| 104 |
print(colored(f"Dataset Size: {len(dataset)}", "green"))
|
| 105 |
|
| 106 |
pbar = tqdm(dataset)
|
docs/installation.md
CHANGED
|
@@ -27,7 +27,7 @@ conda activate econ
|
|
| 27 |
pip install -r requirements.txt
|
| 28 |
|
| 29 |
# install libmesh & libvoxelize
|
| 30 |
-
cd lib/
|
| 31 |
python setup.py build_ext --inplace
|
| 32 |
cd ../libvoxelize
|
| 33 |
python setup.py build_ext --inplace
|
|
|
|
| 27 |
pip install -r requirements.txt
|
| 28 |
|
| 29 |
# install libmesh & libvoxelize
|
| 30 |
+
cd lib/common/libmesh
|
| 31 |
python setup.py build_ext --inplace
|
| 32 |
cd ../libvoxelize
|
| 33 |
python setup.py build_ext --inplace
|
lib/smplx/body_models.py
CHANGED
|
@@ -1151,6 +1151,7 @@ class SMPLX(SMPLH):
|
|
| 1151 |
pose2rot: bool = True,
|
| 1152 |
return_joint_transformation: bool = False,
|
| 1153 |
return_vertex_transformation: bool = False,
|
|
|
|
| 1154 |
**kwargs,
|
| 1155 |
) -> SMPLXOutput:
|
| 1156 |
"""
|
|
@@ -1240,9 +1241,30 @@ class SMPLX(SMPLH):
|
|
| 1240 |
dim=1,
|
| 1241 |
)
|
| 1242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
# Add the mean pose of the model. Does not affect the body, only the
|
| 1244 |
# hands when flat_hand_mean == False
|
| 1245 |
-
full_pose += self.pose_mean
|
| 1246 |
|
| 1247 |
batch_size = max(betas.shape[0], global_orient.shape[0], body_pose.shape[0])
|
| 1248 |
# Concatenate the shape and expression coefficients
|
|
|
|
| 1151 |
pose2rot: bool = True,
|
| 1152 |
return_joint_transformation: bool = False,
|
| 1153 |
return_vertex_transformation: bool = False,
|
| 1154 |
+
pose_type: str = 'posed',
|
| 1155 |
**kwargs,
|
| 1156 |
) -> SMPLXOutput:
|
| 1157 |
"""
|
|
|
|
| 1241 |
dim=1,
|
| 1242 |
)
|
| 1243 |
|
| 1244 |
+
if pose_type == "t-pose":
|
| 1245 |
+
full_pose *= 0.0
|
| 1246 |
+
elif pose_type == "da-pose":
|
| 1247 |
+
body_pose = torch.zeros_like(body_pose).view(body_pose.shape[0], -1, 3)
|
| 1248 |
+
body_pose[:, 0] = torch.tensor([0., 0., 30 * np.pi / 180.])
|
| 1249 |
+
body_pose[:, 1] = torch.tensor([0., 0., -30 * np.pi / 180.])
|
| 1250 |
+
body_pose = body_pose.view(body_pose.shape[0], -1)
|
| 1251 |
+
|
| 1252 |
+
full_pose = torch.cat(
|
| 1253 |
+
[
|
| 1254 |
+
global_orient * 0.,
|
| 1255 |
+
body_pose,
|
| 1256 |
+
jaw_pose * 0.,
|
| 1257 |
+
leye_pose * 0.,
|
| 1258 |
+
reye_pose * 0.,
|
| 1259 |
+
left_hand_pose * 0.,
|
| 1260 |
+
right_hand_pose * 0.,
|
| 1261 |
+
],
|
| 1262 |
+
dim=1,
|
| 1263 |
+
)
|
| 1264 |
+
|
| 1265 |
# Add the mean pose of the model. Does not affect the body, only the
|
| 1266 |
# hands when flat_hand_mean == False
|
| 1267 |
+
# full_pose += self.pose_mean
|
| 1268 |
|
| 1269 |
batch_size = max(betas.shape[0], global_orient.shape[0], body_pose.shape[0])
|
| 1270 |
# Concatenate the shape and expression coefficients
|
lib/smplx/lbs.py
CHANGED
|
@@ -233,6 +233,7 @@ def lbs(
|
|
| 233 |
def general_lbs(
|
| 234 |
pose: Tensor,
|
| 235 |
v_template: Tensor,
|
|
|
|
| 236 |
J_regressor: Tensor,
|
| 237 |
parents: Tensor,
|
| 238 |
lbs_weights: Tensor,
|
|
@@ -246,6 +247,8 @@ def general_lbs(
|
|
| 246 |
The pose parameters in axis-angle format
|
| 247 |
v_template torch.tensor BxVx3
|
| 248 |
The template mesh that will be deformed
|
|
|
|
|
|
|
| 249 |
J_regressor : torch.tensor JxV
|
| 250 |
The regressor array that is used to calculate the joints from
|
| 251 |
the position of the vertices
|
|
@@ -277,10 +280,21 @@ def general_lbs(
|
|
| 277 |
# NxJx3 array
|
| 278 |
J = vertices2joints(J_regressor, v_template)
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
if pose2rot:
|
| 281 |
rot_mats = batch_rodrigues(pose.view(-1, 3)).view([batch_size, -1, 3, 3])
|
|
|
|
|
|
|
|
|
|
| 282 |
else:
|
| 283 |
rot_mats = pose.view(batch_size, -1, 3, 3)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
# 4. Get the global joint location
|
| 286 |
J_transformed, A = batch_rigid_transform(rot_mats, J, parents, dtype=dtype)
|
|
@@ -292,13 +306,13 @@ def general_lbs(
|
|
| 292 |
num_joints = J_regressor.shape[0]
|
| 293 |
T = torch.matmul(W, A.view(batch_size, num_joints, 16)).view(batch_size, -1, 4, 4)
|
| 294 |
|
| 295 |
-
homogen_coord = torch.ones([batch_size,
|
| 296 |
-
v_posed_homo = torch.cat([
|
| 297 |
v_homo = torch.matmul(T, torch.unsqueeze(v_posed_homo, dim=-1))
|
| 298 |
|
| 299 |
verts = v_homo[:, :, :3, 0]
|
| 300 |
|
| 301 |
-
return verts,
|
| 302 |
|
| 303 |
|
| 304 |
def vertices2joints(J_regressor: Tensor, vertices: Tensor) -> Tensor:
|
|
|
|
| 233 |
def general_lbs(
|
| 234 |
pose: Tensor,
|
| 235 |
v_template: Tensor,
|
| 236 |
+
posedirs: Tensor,
|
| 237 |
J_regressor: Tensor,
|
| 238 |
parents: Tensor,
|
| 239 |
lbs_weights: Tensor,
|
|
|
|
| 247 |
The pose parameters in axis-angle format
|
| 248 |
v_template torch.tensor BxVx3
|
| 249 |
The template mesh that will be deformed
|
| 250 |
+
posedirs : torch.tensor Px(V * 3)
|
| 251 |
+
The pose PCA coefficients
|
| 252 |
J_regressor : torch.tensor JxV
|
| 253 |
The regressor array that is used to calculate the joints from
|
| 254 |
the position of the vertices
|
|
|
|
| 280 |
# NxJx3 array
|
| 281 |
J = vertices2joints(J_regressor, v_template)
|
| 282 |
|
| 283 |
+
# Add pose blend shapes
|
| 284 |
+
# N x J x 3 x 3
|
| 285 |
+
ident = torch.eye(3, dtype=dtype, device=device)
|
| 286 |
+
|
| 287 |
if pose2rot:
|
| 288 |
rot_mats = batch_rodrigues(pose.view(-1, 3)).view([batch_size, -1, 3, 3])
|
| 289 |
+
pose_feature = (rot_mats[:, 1:, :, :] - ident).view([batch_size, -1])
|
| 290 |
+
# (N x P) x (P, V * 3) -> N x V x 3
|
| 291 |
+
pose_offsets = torch.matmul(pose_feature, posedirs).view(batch_size, -1, 3)
|
| 292 |
else:
|
| 293 |
rot_mats = pose.view(batch_size, -1, 3, 3)
|
| 294 |
+
pose_feature = pose[:, 1:].view(batch_size, -1, 3, 3) - ident
|
| 295 |
+
pose_offsets = torch.matmul(pose_feature.view(batch_size, -1), posedirs).view(batch_size, -1, 3)
|
| 296 |
+
|
| 297 |
+
v_posed = pose_offsets + v_template
|
| 298 |
|
| 299 |
# 4. Get the global joint location
|
| 300 |
J_transformed, A = batch_rigid_transform(rot_mats, J, parents, dtype=dtype)
|
|
|
|
| 306 |
num_joints = J_regressor.shape[0]
|
| 307 |
T = torch.matmul(W, A.view(batch_size, num_joints, 16)).view(batch_size, -1, 4, 4)
|
| 308 |
|
| 309 |
+
homogen_coord = torch.ones([batch_size, v_posed.shape[1], 1], dtype=dtype, device=device)
|
| 310 |
+
v_posed_homo = torch.cat([v_posed, homogen_coord], dim=2)
|
| 311 |
v_homo = torch.matmul(T, torch.unsqueeze(v_posed_homo, dim=-1))
|
| 312 |
|
| 313 |
verts = v_homo[:, :, :3, 0]
|
| 314 |
|
| 315 |
+
return verts, J_transformed
|
| 316 |
|
| 317 |
|
| 318 |
def vertices2joints(J_regressor: Tensor, vertices: Tensor) -> Tensor:
|