H-Liu1997 commited on
Commit
8539c03
Β·
verified Β·
1 Parent(s): 155d0d0

Upload visualization/tools/smplh.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. visualization/tools/smplh.py +191 -0
visualization/tools/smplh.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SMPL-H model loading and GPU forward pass.
2
+
3
+ Provides a self-contained SMPL-H LBS implementation using PyTorch,
4
+ adapted from HumanML3D/custom_138/recover_and_render.py.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torch
11
+
12
+ from utils.paths import PATHS
13
+
14
+ # ─── Constants ──────────────────────────────────────────────────
15
+
16
+ SMPLH_MODEL_DIR = PATHS["deps"] / "smplh"
17
+
18
+ # Hand mean poses (axis-angle, 15 joints Γ— 3)
19
+ LEFT_HAND_MEAN_AA = np.array([
20
+ 0.1117, 0.0429, -0.4164, 0.1088, -0.0660, -0.7562, -0.0964, -0.0909,
21
+ -0.1885, -0.1181, 0.0509, -0.5296, -0.1437, 0.0552, -0.7049, -0.0192,
22
+ -0.0923, -0.3379, -0.4570, -0.1963, -0.6255, -0.2147, -0.0660, -0.5069,
23
+ -0.3697, -0.0603, -0.0795, -0.1419, -0.0859, -0.6355, -0.3033, -0.0579,
24
+ -0.6314, -0.1761, -0.1321, -0.3734, 0.8510, 0.2769, -0.0915, -0.4998,
25
+ 0.0266, 0.0529, 0.5356, 0.0460, -0.2774,
26
+ ], dtype=np.float32)
27
+
28
+ RIGHT_HAND_MEAN_AA = np.array([
29
+ 0.1117, -0.0429, 0.4164, 0.1088, 0.0660, 0.7562, -0.0964, 0.0909,
30
+ 0.1885, -0.1181, -0.0509, 0.5296, -0.1437, -0.0552, 0.7049, -0.0192,
31
+ 0.0923, 0.3379, -0.4570, 0.1963, 0.6255, -0.2147, 0.0660, 0.5069,
32
+ -0.3697, 0.0603, 0.0795, -0.1419, 0.0859, 0.6355, -0.3033, 0.0579,
33
+ 0.6314, -0.1761, 0.1321, 0.3734, 0.8510, -0.2769, 0.0915, -0.4998,
34
+ -0.0266, -0.0529, 0.5356, -0.0460, 0.2774,
35
+ ], dtype=np.float32)
36
+
37
+
38
+ # ─── Model loading (cached) ────────────────────────────────────
39
+
40
+ _model_cache = {}
41
+ _gpu_cache = {}
42
+ _GPU_DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
43
+
44
+
45
+ def load_smplh_model(gender="neutral"):
46
+ """Load SMPL-H model data for given gender (cached).
47
+
48
+ Returns dict with keys: v_template, f, shapedirs, posedirs,
49
+ J_regressor, kintree_table, weights.
50
+ """
51
+ if gender not in _model_cache:
52
+ model_path = SMPLH_MODEL_DIR / gender / "model.npz"
53
+ data = np.load(str(model_path), allow_pickle=True)
54
+ J_reg = data["J_regressor"]
55
+ if hasattr(J_reg, "toarray"):
56
+ J_reg = J_reg.toarray()
57
+ elif hasattr(J_reg, "A"):
58
+ J_reg = np.array(J_reg.A)
59
+ _model_cache[gender] = {
60
+ "v_template": data["v_template"].astype(np.float32),
61
+ "f": data["f"].astype(np.int32),
62
+ "shapedirs": data["shapedirs"].astype(np.float32),
63
+ "posedirs": data["posedirs"].astype(np.float32),
64
+ "J_regressor": np.asarray(J_reg, dtype=np.float32),
65
+ "kintree_table": data["kintree_table"].astype(np.int64),
66
+ "weights": data["weights"].astype(np.float32),
67
+ }
68
+ return _model_cache[gender]
69
+
70
+
71
+ def get_J0(model, betas):
72
+ """Compute pelvis rest position J[0] from model and shape params.
73
+
74
+ Used to convert pelvis absolute position to SMPLX translation convention:
75
+ trans_smplx = pelvis_abs - J0
76
+ """
77
+ J_reg = model["J_regressor"]
78
+ v_shaped = model["v_template"] + np.einsum(
79
+ "vci,i->vc", model["shapedirs"], betas.astype(np.float32)
80
+ )
81
+ return (J_reg @ v_shaped)[0]
82
+
83
+
84
+ # ─── GPU helpers ────────────────────────────────────────────────
85
+
86
+ def _get_model_gpu(model, gender, device=None):
87
+ """Get or create GPU-resident tensors for a model (cached by gender)."""
88
+ if gender not in _gpu_cache:
89
+ dev = device or _GPU_DEVICE
90
+ _gpu_cache[gender] = {
91
+ "v_template": torch.tensor(model["v_template"], dtype=torch.float32, device=dev),
92
+ "shapedirs": torch.tensor(model["shapedirs"], dtype=torch.float32, device=dev),
93
+ "posedirs": torch.tensor(model["posedirs"], dtype=torch.float32, device=dev),
94
+ "J_regressor": torch.tensor(model["J_regressor"], dtype=torch.float32, device=dev),
95
+ "weights": torch.tensor(model["weights"], dtype=torch.float32, device=dev),
96
+ "parents": model["kintree_table"][0],
97
+ }
98
+ return _gpu_cache[gender]
99
+
100
+
101
+ def _batch_rodrigues(aa):
102
+ """Axis-angle (N, 3) -> rotation matrices (N, 3, 3) in PyTorch."""
103
+ angle = torch.norm(aa + 1e-8, dim=1, keepdim=True)
104
+ axis = aa / angle
105
+ c = torch.cos(angle).unsqueeze(1)
106
+ s = torch.sin(angle).unsqueeze(1)
107
+ rx, ry, rz = axis[:, 0:1], axis[:, 1:2], axis[:, 2:3]
108
+ z = torch.zeros_like(rx)
109
+ K = torch.cat([z, -rz, ry, rz, z, -rx, -ry, rx, z], 1).view(-1, 3, 3)
110
+ I = torch.eye(3, device=aa.device, dtype=aa.dtype).unsqueeze(0)
111
+ return I + s * K + (1 - c) * torch.bmm(K, K)
112
+
113
+
114
+ # ─── Forward pass ───────────────────────────────────────────────
115
+
116
+ @torch.no_grad()
117
+ def smplh_forward(model, gender, betas, poses_aa, transl):
118
+ """SMPL-H forward pass on GPU.
119
+
120
+ Args:
121
+ model: dict from load_smplh_model (numpy arrays).
122
+ gender: str, one of "male", "female", "neutral".
123
+ betas: (16,) numpy shape parameters.
124
+ poses_aa: (T, 52, 3) numpy axis-angle poses.
125
+ transl: (T, 3) numpy root translation (SMPLX convention).
126
+
127
+ Returns:
128
+ verts: (T, 6890, 3) numpy float32 mesh vertices.
129
+ """
130
+ dev = _GPU_DEVICE
131
+ m = _get_model_gpu(model, gender, dev)
132
+ T = poses_aa.shape[0]
133
+ parents = m["parents"]
134
+
135
+ betas_t = torch.tensor(betas, dtype=torch.float32, device=dev)
136
+ poses_t = torch.tensor(poses_aa, dtype=torch.float32, device=dev)
137
+ transl_t = torch.tensor(transl, dtype=torch.float32, device=dev)
138
+
139
+ # 1. Shape blend shapes
140
+ v_shaped = m["v_template"] + torch.einsum("vci,i->vc", m["shapedirs"], betas_t)
141
+
142
+ # 2. Joint regression
143
+ J = m["J_regressor"] @ v_shaped # (52, 3)
144
+
145
+ # 3. Axis-angle -> rotation matrices
146
+ rot_mats = _batch_rodrigues(poses_t.reshape(-1, 3)).reshape(T, 52, 3, 3)
147
+
148
+ # 4. Pose blend shapes
149
+ I3 = torch.eye(3, device=dev, dtype=torch.float32)
150
+ pose_feature = (rot_mats[:, 1:] - I3).reshape(T, -1) # (T, 459)
151
+ v_posed = v_shaped.unsqueeze(0) + torch.einsum("vcp,tp->tvc", m["posedirs"], pose_feature)
152
+
153
+ # 5. FK chain
154
+ rel_J = J.clone()
155
+ for i in range(1, 52):
156
+ p = parents[i]
157
+ if 0 <= p < 52:
158
+ rel_J[i] = J[i] - J[p]
159
+
160
+ local_tf = torch.zeros(T, 52, 4, 4, device=dev, dtype=torch.float32)
161
+ local_tf[:, :, :3, :3] = rot_mats
162
+ local_tf[:, :, :3, 3] = rel_J.unsqueeze(0)
163
+ local_tf[:, :, 3, 3] = 1.0
164
+
165
+ global_tf = torch.zeros_like(local_tf)
166
+ global_tf[:, 0] = local_tf[:, 0]
167
+ for i in range(1, 52):
168
+ p = parents[i]
169
+ if 0 <= p < 52:
170
+ global_tf[:, i] = torch.bmm(global_tf[:, p], local_tf[:, i])
171
+ else:
172
+ global_tf[:, i] = local_tf[:, i]
173
+
174
+ # 6. Relative transforms
175
+ J_homo = torch.zeros(52, 4, device=dev, dtype=torch.float32)
176
+ J_homo[:, :3] = J
177
+ t_rest = torch.einsum("tjcd,jd->tjc", global_tf[:, :, :3, :], J_homo)
178
+ rel_tf = global_tf.clone()
179
+ rel_tf[:, :, :3, 3] -= t_rest[:, :, :3]
180
+
181
+ # 7. LBS
182
+ T_blend = torch.einsum("vj,tjcd->tvcd", m["weights"], rel_tf)
183
+ v_homo = torch.ones(T, v_posed.shape[1], 4, device=dev, dtype=torch.float32)
184
+ v_homo[:, :, :3] = v_posed
185
+ verts = torch.einsum("tvcd,tvd->tvc", T_blend[:, :, :3, :], v_homo)
186
+ verts += transl_t.unsqueeze(1)
187
+
188
+ result = verts.cpu().numpy()
189
+ del verts, v_homo, T_blend, rel_tf, global_tf, local_tf, v_posed, rot_mats, poses_t, transl_t
190
+ torch.cuda.empty_cache()
191
+ return result