Skip to content

Commit 62aa8a3

Browse files
committed
init
1 parent 640aee9 commit 62aa8a3

17 files changed

+983
-1
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
model/mano_v1_2
2+
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]

README.md

+56-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,56 @@
1-
# minimal-hand
1+
# Minimal Hand
2+
3+
A minimal solution to hand motion capture from a single color camera at over 100fps.
4+
Easy to use, plug to run.
5+
6+
![teaser](teaser.gif)
7+
8+
This is the official implementation of the paper "Monocular Real-time Hand Shape and Motion Capture using Multi-modal Data" (CVPR 2020).
9+
10+
This project provides the core components for hand motion capture:
11+
1. estimating joint **locations** from a monocular RGB image (DetNet)
12+
1. estimating joint **rotations** from locations (IKNet)
13+
14+
We focus on:
15+
1. ease of use (all you need is a webcam)
16+
1. time efficiency (on our 1080Ti, 8.9ms for DetNet, 0.9ms for IKNet)
17+
1. robustness to occlusion, hand-object interaction, fast motion, changing scale and view point
18+
19+
## Usage
20+
21+
### Install dependencies
22+
Please check `requirements.txt`. All dependencies are available via pip and conda.
23+
24+
### Prepare MANO hand model
25+
1. Download MANO model from [here](https://mano.is.tue.mpg.de/) and unzip it.
26+
1. In `config.py`, set `OFFICIAL_MANO_PATH` to the **left hand** model.
27+
1. Run `python prepare_mano.py`, you will get the converted MANO model that is compatible with this project at `config.HAND_MESH_MODEL_PATH`.
28+
29+
### Prepare pre-trained network models
30+
1. Download models from [here](https://drive.google.com/open?id=1ZnDYF9rHKbef27tiHkWrLznqe18le7ol).
31+
1. Put `detnet.ckpt.*` in `model/detnet`, and `iknet.ckpt.*` in `model/iknet`.
32+
1. Check `config.py`, make sure all required files are there.
33+
34+
### Run the demo for webcam input
35+
1. `python app.py`
36+
1. Put your **right hand** in front of the camera. The pre-trained model is for left hand, but the input would be flipped internally.
37+
1. Press `ESC` to quit.
38+
39+
### Use the models in your project
40+
Please check `wrappers.py`.
41+
42+
## Citation
43+
44+
If you find the project helpful, please consider citing us:
45+
```
46+
@inproceedings{zhou2019monocular,
47+
title={Monocular Real-time Hand Shape and Motion Capture using Multi-modal Data},
48+
author={Zhou, Yuxiao and Habermann, Marc and Xu, Weipeng and Habibie, Ikhsanul and Theobalt, Christian and Xu, Feng},
49+
booktitle={Proceedings of the IEEE International Conference on Computer Vision},
50+
pages={0--0},
51+
year={2020}
52+
}
53+
```
54+
55+
## IKNet Alternative
56+
We also provide an optimization-based IK solver [here](https://github.com/CalciferZh/Minimal-IK).

app.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import cv2
2+
import keyboard
3+
import numpy as np
4+
import open3d as o3d
5+
import pygame
6+
from transforms3d.axangles import axangle2mat
7+
8+
import config
9+
from capture import OpenCVCapture
10+
from hand_mesh import HandMesh
11+
from kinematics import mpii_to_mano
12+
from utils import OneEuroFilter, imresize
13+
from wrappers import ModelPipeline
14+
from utils import *
15+
16+
17+
def live_application(capture):
18+
"""
19+
Launch an application that reads from a webcam and estimates hand pose at
20+
real-time.
21+
22+
The captured hand must be the right hand, but will be flipped internally
23+
and rendered.
24+
25+
Parameters
26+
----------
27+
capture : object
28+
An object from `capture.py` to read capture stream from.
29+
"""
30+
############ output visualization ############
31+
view_mat = axangle2mat([1, 0, 0], np.pi) # align different coordinate systems
32+
window_size = 1080
33+
34+
hand_mesh = HandMesh(config.HAND_MESH_MODEL_PATH)
35+
mesh = o3d.geometry.TriangleMesh()
36+
mesh.triangles = o3d.utility.Vector3iVector(hand_mesh.faces)
37+
mesh.vertices = \
38+
o3d.utility.Vector3dVector(np.matmul(view_mat, hand_mesh.verts.T).T * 1000)
39+
mesh.compute_vertex_normals()
40+
41+
viewer = o3d.visualization.Visualizer()
42+
viewer.create_window(
43+
width=window_size + 1, height=window_size + 1,
44+
window_name='Minimal Hand - output'
45+
)
46+
viewer.add_geometry(mesh)
47+
48+
view_control = viewer.get_view_control()
49+
cam_params = view_control.convert_to_pinhole_camera_parameters()
50+
extrinsic = cam_params.extrinsic.copy()
51+
extrinsic[0:3, 3] = 0
52+
cam_params.extrinsic = extrinsic
53+
cam_params.intrinsic.set_intrinsics(
54+
window_size + 1, window_size + 1, config.CAM_FX, config.CAM_FY,
55+
window_size // 2, window_size // 2
56+
)
57+
view_control.convert_from_pinhole_camera_parameters(cam_params)
58+
view_control.set_constant_z_far(1000)
59+
60+
render_option = viewer.get_render_option()
61+
render_option.load_from_json('./render_option.json')
62+
viewer.update_renderer()
63+
64+
############ input visualization ############
65+
pygame.init()
66+
display = pygame.display.set_mode((window_size, window_size))
67+
pygame.display.set_caption('Minimal Hand - input')
68+
69+
############ misc ############
70+
mesh_smoother = OneEuroFilter(4.0, 0.0)
71+
clock = pygame.time.Clock()
72+
model = ModelPipeline()
73+
74+
while True:
75+
frame_large = capture.read()
76+
if frame_large is None:
77+
continue
78+
if frame_large.shape[0] > frame_large.shape[1]:
79+
margin = int((frame_large.shape[0] - frame_large.shape[1]) / 2)
80+
frame_large = frame_large[margin:-margin]
81+
else:
82+
margin = int((frame_large.shape[1] - frame_large.shape[0]) / 2)
83+
frame_large = frame_large[:, margin:-margin]
84+
85+
frame_large = np.flip(frame_large, axis=1).copy()
86+
frame = imresize(frame_large, (128, 128))
87+
88+
_, theta_mpii = model.process(frame)
89+
theta_mano = mpii_to_mano(theta_mpii)
90+
91+
v = hand_mesh.set_abs_quat(theta_mano)
92+
v *= 2 # for better visualization
93+
v = v * 1000 + np.array([0, 0, 400])
94+
v = mesh_smoother.process(v)
95+
mesh.triangles = o3d.utility.Vector3iVector(hand_mesh.faces)
96+
mesh.vertices = o3d.utility.Vector3dVector(np.matmul(view_mat, v.T).T)
97+
mesh.paint_uniform_color(config.HAND_COLOR)
98+
mesh.compute_triangle_normals()
99+
mesh.compute_vertex_normals()
100+
viewer.update_geometry()
101+
102+
viewer.poll_events()
103+
104+
display.blit(
105+
pygame.surfarray.make_surface(
106+
np.transpose(
107+
imresize(frame_large, (window_size, window_size)
108+
), (1, 0, 2))
109+
),
110+
(0, 0)
111+
)
112+
pygame.display.update()
113+
114+
if keyboard.is_pressed("esc"):
115+
break
116+
117+
clock.tick(30)
118+
119+
120+
if __name__ == '__main__':
121+
live_application(OpenCVCapture())

capture.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import cv2
2+
import numpy as np
3+
4+
5+
class OpenCVCapture:
6+
"""
7+
OpenCV wrapper to read from webcam.
8+
"""
9+
def __init__(self):
10+
"""
11+
Init.
12+
"""
13+
self.cap = cv2.VideoCapture(0)
14+
15+
def read(self):
16+
"""
17+
Read one frame. Note this function might be blocked by the sensor.
18+
19+
Returns
20+
-------
21+
np.ndarray
22+
Read frame. Might be `None` is the webcam fails to get on frame.
23+
"""
24+
flag, frame = self.cap.read()
25+
if not flag:
26+
return None
27+
return np.flip(frame, -1).copy() # BGR to RGB

config.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DETECTION_MODEL_PATH = './model/detnet/detnet.ckpt'
2+
IK_MODEL_PATH = './model/iknet/iknet.ckpt'
3+
HAND_MESH_MODEL_PATH = './model/hand_mesh/hand_mesh_model.pkl'
4+
# use left hand
5+
OFFICIAL_MANO_PATH = './model/mano_v1_2/models/MANO_LEFT.pkl'
6+
IK_UNIT_LENGTH = 0.09473151311686484 # in meter
7+
8+
HAND_COLOR = [228/255, 178/255, 148/255]
9+
10+
# only for rendering
11+
CAM_FX = 620.744
12+
CAM_FY = 621.151

hand_mesh.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import numpy as np
2+
from transforms3d.quaternions import quat2mat
3+
4+
from config import *
5+
from kinematics import *
6+
from utils import *
7+
8+
9+
class HandMesh():
10+
"""
11+
Wrapper for the MANO hand model.
12+
"""
13+
def __init__(self, model_path):
14+
"""
15+
Init.
16+
17+
Parameters
18+
----------
19+
model_path : str
20+
Path to the MANO model file. This model is converted by `prepare_mano.py`
21+
from official release.
22+
"""
23+
params = load_pkl(model_path)
24+
self.verts = params['verts']
25+
self.faces = params['faces']
26+
self.weights = params['weights']
27+
self.joints = params['joints']
28+
29+
self.n_verts = self.verts.shape[0]
30+
self.n_faces = self.faces.shape[0]
31+
32+
self.ref_pose = []
33+
self.ref_T = []
34+
for j in range(MANOHandJoints.n_joints):
35+
parent = MANOHandJoints.parents[j]
36+
if parent is None:
37+
self.ref_T.append(self.verts)
38+
self.ref_pose.append(self.joints[j])
39+
else:
40+
self.ref_T.append(self.verts - self.joints[parent])
41+
self.ref_pose.append(self.joints[j] - self.joints[parent])
42+
self.ref_pose = np.expand_dims(np.stack(self.ref_pose, 0), -1)
43+
self.ref_T = np.expand_dims(np.stack(self.ref_T, 1), -1)
44+
45+
def set_abs_quat(self, quat):
46+
"""
47+
Set absolute (global) rotation for the hand.
48+
49+
Parameters
50+
----------
51+
quat : np.ndarray, shape [J, 4]
52+
Absolute rotations for each joint in quaternion.
53+
54+
Returns
55+
-------
56+
np.ndarray, shape [V, 3]
57+
Mesh vertices after posing.
58+
"""
59+
mats = []
60+
for j in range(MANOHandJoints.n_joints):
61+
mats.append(quat2mat(quat[j]))
62+
mats = np.stack(mats, 0)
63+
64+
pose = np.matmul(mats, self.ref_pose)
65+
joint_xyz = [None] * MANOHandJoints.n_joints
66+
for j in range(MANOHandJoints.n_joints):
67+
joint_xyz[j] = pose[j]
68+
parent = MANOHandJoints.parents[j]
69+
if parent is not None:
70+
joint_xyz[j] += joint_xyz[parent]
71+
joint_xyz = np.stack(joint_xyz, 0)[..., 0]
72+
73+
T = np.matmul(np.expand_dims(mats, 0), self.ref_T)[..., 0]
74+
self.verts = [None] * MANOHandJoints.n_joints
75+
for j in range(MANOHandJoints.n_joints):
76+
self.verts[j] = T[:, j]
77+
parent = MANOHandJoints.parents[j]
78+
if parent is not None:
79+
self.verts[j] += joint_xyz[parent]
80+
self.verts = np.stack(self.verts, 1)
81+
self.verts = np.sum(self.verts * self.weights, 1)
82+
83+
return self.verts.copy()

0 commit comments

Comments
 (0)