Skip to content

Commit 0c18902

Browse files
committed
release candidate
1 parent 7ce1a62 commit 0c18902

File tree

6 files changed

+212
-43
lines changed

6 files changed

+212
-43
lines changed

README.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,25 @@
1-
# Minimal-IK
1+
# Minimal-IK
2+
3+
A simple and naive inverse kinematics solver for MANO hand model, SMPL body model, and SMPLH body+hand model.
4+
5+
Briefly, given joint coordinates (and optional other keypoints), the solver gives the corresponding model parameters.
6+
7+
Levenberg–Marquardt algorithm is used, the energy is simply the L2 distance between the keypoints.
8+
9+
## Usage
10+
11+
### Models
12+
13+
1. Download the official model from MPI.
14+
2. See `config.py` and set the official model path.
15+
3. See `prepare_model.py`, use the provided function to pre-process the model.
16+
17+
### Solver
18+
19+
1. See `example.py`, un-comment the corresponding code.
20+
2. `python example.py`.
21+
3. The example ground truth mesh and estimated mesh are saved to `gt.obj` and `est.obj` respectively.
22+
23+
### Dependencies
24+
25+
Every required package is available via `pip install`.

armatures.py

+24-29
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
# it must be assured that parent joint appears before child joint
22

33
class MANOArmature:
4+
# number of movable joints
45
n_joints = 16
56

67
# indices of extended keypoints
78
keypoints_ext = [333, 444, 672, 555, 744]
89

910
n_keypoints = n_joints + len(keypoints_ext)
1011

11-
root = 0
12-
13-
center = 4
14-
1512
labels = [
1613
'W', #0
1714
'I0', 'I1', 'I2', #3
@@ -27,37 +24,35 @@ class MANOArmature:
2724
class SMPLArmature:
2825
n_joints = 24
2926

30-
# indices of extended keypoints (limb ends)
31-
# lfinger, rfinger, ltoe, rtoe, head-top
3227
keypoints_ext = [2446, 5907, 3216, 6618, 411]
3328

3429
n_keypoints = n_joints + len(keypoints_ext)
3530

3631
labels = [
3732
'pelvis',
38-
'llegroot', 'rlegroot',
33+
'left leg root', 'right leg root',
3934
'lowerback',
40-
'lknee', 'rknee',
35+
'left knee', 'right knee',
4136
'upperback',
42-
'lankle', 'rankle',
37+
'left ankle', 'right ankle',
4338
'thorax',
44-
'ltoes', 'rtoes',
39+
'left toes', 'right toes',
4540
'lowerneck',
46-
'lclavicle', 'rclavicle',
41+
'left clavicle', 'right clavicle',
4742
'upperneck',
48-
'larmroot', 'rarmroot',
49-
'lelbow', 'relbow',
50-
'lwrist', 'rwrist',
51-
'lhand', 'rhand'
43+
'left armroot', 'right armroot',
44+
'left elbow', 'right elbow',
45+
'left wrist', 'right wrist',
46+
'left hand', 'right hand'
5247
# extended
53-
'lfinger_tip', 'rfinger_tip', 'ltoe_tip', 'r_toe_tip', 'head_top'
48+
'left finger tip', 'right finger tip', 'left toe tip', 'right toe tip',
49+
'head_top'
5450
]
5551

5652

5753
class SMPLHArmature:
5854
n_joints = 52
5955

60-
# indices of extended keypoints (limb ends)
6156
keypoints_ext = [
6257
2746, 2320, 2446, 2557, 2674,
6358
6191, 5781, 5907, 6018, 6135,
@@ -68,22 +63,22 @@ class SMPLHArmature:
6863

6964
labels = [
7065
'pelvis',
71-
'llegroot', 'rlegroot',
66+
'left leg root', 'right leg root',
7267
'lowerback',
73-
'lknee', 'rknee',
68+
'left knee', 'right knee',
7469
'upperback',
75-
'lankle', 'rankle',
70+
'left ankle', 'right ankle',
7671
'thorax',
77-
'ltoes', 'rtoes',
72+
'left toes', 'right toes',
7873
'lowerneck',
79-
'lclavicle', 'rclavicle',
74+
'left clavicle', 'right clavicle',
8075
'upperneck',
81-
'larmroot', 'rarmroot',
82-
'lelbow', 'relbow',
83-
'lwrist', 'rwrist',
84-
'lhand', 'rhand'
76+
'left armroot', 'right armroot',
77+
'left elbow', 'right elbow',
78+
'left wrist', 'right wrist',
79+
'left hand', 'right hand'
8580
# extended
86-
'left-thumb', 'li', 'lm', 'lr', 'll',
87-
'rt', 'ri', 'rm', 'rr', 'rl',
88-
'ltoe-tip', 'rtoe-tip', 'heat-top'
81+
'left thumb', 'left index', 'left middle', 'left ring', 'left little',
82+
'right thumb', 'right index', 'right middle', 'right ring', 'right little',
83+
'left toe tip', 'right toe tip', 'heat-top'
8984
]

example.py

+16-12
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,35 @@
66

77

88
np.random.seed(20160923)
9-
pose_glb = np.zeros([1, 3])
9+
pose_glb = np.zeros([1, 3]) # global rotation
1010

1111

1212
########################## mano settings #########################
13-
# n_pose = 12
14-
# n_shape = 10
15-
# pose_pca = np.random.normal(size=n_pose)
16-
# shape = np.random.normal(size=n_shape)
17-
# mesh = KinematicModel(config.MANO_MODEL_PATH, MANOArmature, scale=1000)
13+
n_pose = 12 # number of pose pca coefficients, in mano the maximum is 45
14+
n_shape = 10 # number of shape pca coefficients
15+
pose_pca = np.random.normal(size=n_pose)
16+
shape = np.random.normal(size=n_shape)
17+
mesh = KinematicModel(config.MANO_MODEL_PATH, MANOArmature, scale=1000)
1818

1919

2020
########################## smpl settings ##########################
21-
# n_pose = 23 * 3
21+
# note that in smpl and smpl-h no pca for pose is provided
22+
# therefore in the model we fake an identity matrix as the pca coefficients
23+
# to make the code compatible
24+
25+
# n_pose = 23 * 3 # degrees of freedom, (n_joints - 1) * 3
2226
# n_shape = 10
2327
# pose_pca = np.random.uniform(-0.2, 0.2, size=n_pose)
2428
# shape = np.random.normal(size=n_shape)
2529
# mesh = KinematicModel(config.SMPL_MODEL_PATH, SMPLArmature, scale=10)
2630

2731

2832
########################## smpl-h settings ##########################
29-
n_pose = 51 * 3
30-
n_shape = 16
31-
pose_pca = np.random.uniform(-0.2, 0.2, size=n_pose)
32-
shape = np.random.normal(size=n_shape)
33-
mesh = KinematicModel(config.SMPLH_MODEL_PATH, SMPLHArmature, scale=10)
33+
# n_pose = 51 * 3
34+
# n_shape = 16
35+
# pose_pca = np.random.uniform(-0.2, 0.2, size=n_pose)
36+
# shape = np.random.normal(size=n_shape)
37+
# mesh = KinematicModel(config.SMPLH_MODEL_PATH, SMPLHArmature, scale=10)
3438

3539

3640
########################## solving example ############################

models.py

+88-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@
33

44

55
class KinematicModel():
6+
"""
7+
Kinematic model that takes in model parameters and outputs mesh, keypoints,
8+
etc.
9+
"""
610
def __init__(self, model_path, armature, scale=1):
11+
"""
12+
Parameters
13+
----------
14+
model_path : str
15+
Path to the model to be loaded.
16+
armature : object
17+
An armature class from `aramatures.py`.
18+
scale : int, optional
19+
Scale of the model to make the solving easier, by default 1
20+
"""
721
with open(model_path, 'rb') as f:
822
params = pickle.load(f)
923

@@ -14,7 +28,7 @@ def __init__(self, model_path, armature, scale=1):
1428

1529
self.skinning_weights = params['skinning_weights']
1630

17-
self.mesh_pose_basis = params['mesh_pose_basis']
31+
self.mesh_pose_basis = params['mesh_pose_basis'] # pose blend shape
1832
self.mesh_shape_basis = params['mesh_shape_basis']
1933
self.mesh_template = params['mesh_template']
2034

@@ -43,6 +57,28 @@ def __init__(self, model_path, armature, scale=1):
4357
self.update()
4458

4559
def set_params(self, pose_abs=None, pose_pca=None, pose_glb=None, shape=None):
60+
"""
61+
Set model parameters and get the mesh. Do not set `pose_abs` and `pose_pca`
62+
at the same time.
63+
64+
Parameters
65+
----------
66+
pose_abs : np.ndarray, shape [n_joints, 3], optional
67+
The absolute model pose in axis-angle, by default None
68+
pose_pca : np.ndarray, optional
69+
The PCA coefficients of the pose, shape [n_pose, 3], by default None
70+
pose_glb : np.ndarray, shape [1, 3], optional
71+
Global rotation for the model, by default None
72+
shape : np.ndarray, shape [n_shape], optional
73+
Shape coefficients of the pose, by default None
74+
75+
Returns
76+
-------
77+
np.ndarray, shape [N, 3]
78+
Vertices coordinates of the mesh, scale applied.
79+
np.ndarray, shape [K, 3]
80+
Keypoints coordinates of the model, scale applied.
81+
"""
4682
if pose_abs is not None:
4783
self.pose = pose_abs
4884
elif pose_pca is not None:
@@ -59,6 +95,16 @@ def set_params(self, pose_abs=None, pose_pca=None, pose_glb=None, shape=None):
5995
return self.update()
6096

6197
def update(self):
98+
"""
99+
Re-compute vertices and keypoints with given parameters.
100+
101+
Returns
102+
-------
103+
np.ndarray, shape [N, 3]
104+
Vertices coordinates of the mesh, scale applied.
105+
np.ndarray, shape [K, 3]
106+
Keypoints coordinates of the model, scale applied.
107+
"""
62108
verts = self.mesh_template + self.mesh_shape_basis.dot(self.shape)
63109
self.J = self.J_regressor.dot(verts)
64110
self.R = self.rodrigues(self.pose.reshape((-1, 1, 3)))
@@ -167,19 +213,60 @@ def save_obj(self, path):
167213

168214

169215
class KinematicPCAWrapper():
216+
"""
217+
A wrapper for `KinematicsModel` to be compatible to the solver.
218+
"""
170219
def __init__(self, core, n_pose=12):
220+
"""
221+
Parameters
222+
----------
223+
core : KinematicModel
224+
Core model to be manipulated.
225+
n_pose : int, optional
226+
Degrees of freedom for pose, by default 12
227+
"""
171228
self.core = core
172229
self.n_pose = n_pose
173230
self.n_shape = core.n_shape_params
174231
self.n_glb = 3
175232
self.n_params = self.n_pose + self.n_shape + self.n_glb
176233

177234
def run(self, params):
235+
"""
236+
Set the parameters, return the corresponding result.
237+
238+
Parameters
239+
----------
240+
params : np.ndarray
241+
Model parameters.
242+
243+
Returns
244+
-------
245+
np.ndarray
246+
Corresponding result.
247+
"""
178248
shape, pose_pca, pose_glb = self.decode(params)
179249
return \
180250
self.core.set_params(pose_glb=pose_glb, pose_pca=pose_pca, shape=shape)[1]
181251

182252
def decode(self, params):
253+
"""
254+
Decode the compact model parameters into semantic parameters.
255+
256+
Parameters
257+
----------
258+
params : np.ndarray
259+
Model parameters.
260+
261+
Returns
262+
-------
263+
np.ndarray
264+
Shape parameters.
265+
np.ndarray
266+
Pose parameters.
267+
np.ndarray
268+
Global rotation.
269+
"""
183270
pose_glb = params[:self.n_glb]
184271
pose_pca = params[self.n_glb:-self.n_shape]
185272
shape = params[-self.n_shape:]

prepare_model.py

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55

66
def prepare_mano_model():
7+
"""
8+
Convert the official MANO model into compatible format with this project.
9+
"""
710
with open(OFFICIAL_MANO_PATH, 'rb') as f:
811
data = pickle.load(f, encoding='latin1')
912
params = {
@@ -24,6 +27,9 @@ def prepare_mano_model():
2427

2528

2629
def prepare_smpl_model():
30+
"""
31+
Convert the official SMPL model into compatible format with this project.
32+
"""
2733
with open(OFFICIAL_SMPL_PATH, 'rb') as f:
2834
data = pickle.load(f, encoding='latin1')
2935
params = {
@@ -45,6 +51,9 @@ def prepare_smpl_model():
4551

4652

4753
def prepare_smplh_model():
54+
"""
55+
Convert the official SMPLH model into compatible format with this project.
56+
"""
4857
data = np.load(OFFICIAL_SMPLH_PATH)
4958
params = {
5059
# SMPL does not provide pose PCA

0 commit comments

Comments
 (0)