Spaces:
Running
Running
Force push - update all files
Browse files- .gitattributes +4 -0
- .gitignore +174 -0
- README.md +5 -5
- component/__init__.py +3 -0
- component/bbox.py +48 -0
- component/control.py +123 -0
- component/skeleton.py +26 -0
- examples/a.png +3 -0
- examples/b.png +3 -0
- main.py +207 -0
- model/yolo.pt +3 -0
- requirements.txt +10 -0
- templates/fist-back.png +3 -0
- templates/fist-back.txt +5 -0
- templates/opened-palm.png +3 -0
- templates/opened-palm.txt +5 -0
.gitattributes
CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
examples/a.png filter=lfs diff=lfs merge=lfs -text
|
37 |
+
examples/b.png filter=lfs diff=lfs merge=lfs -text
|
38 |
+
templates/fist-back.png filter=lfs diff=lfs merge=lfs -text
|
39 |
+
templates/opened-palm.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
|
110 |
+
# pdm
|
111 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
112 |
+
#pdm.lock
|
113 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
114 |
+
# in version control.
|
115 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
116 |
+
.pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
121 |
+
__pypackages__/
|
122 |
+
|
123 |
+
# Celery stuff
|
124 |
+
celerybeat-schedule
|
125 |
+
celerybeat.pid
|
126 |
+
|
127 |
+
# SageMath parsed files
|
128 |
+
*.sage.py
|
129 |
+
|
130 |
+
# Environments
|
131 |
+
.env
|
132 |
+
.venv
|
133 |
+
env/
|
134 |
+
venv/
|
135 |
+
ENV/
|
136 |
+
env.bak/
|
137 |
+
venv.bak/
|
138 |
+
|
139 |
+
# Spyder project settings
|
140 |
+
.spyderproject
|
141 |
+
.spyproject
|
142 |
+
|
143 |
+
# Rope project settings
|
144 |
+
.ropeproject
|
145 |
+
|
146 |
+
# mkdocs documentation
|
147 |
+
/site
|
148 |
+
|
149 |
+
# mypy
|
150 |
+
.mypy_cache/
|
151 |
+
.dmypy.json
|
152 |
+
dmypy.json
|
153 |
+
|
154 |
+
# Pyre type checker
|
155 |
+
.pyre/
|
156 |
+
|
157 |
+
# pytype static type analyzer
|
158 |
+
.pytype/
|
159 |
+
|
160 |
+
# Cython debug symbols
|
161 |
+
cython_debug/
|
162 |
+
|
163 |
+
# PyCharm
|
164 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
165 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
166 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
167 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
168 |
+
#.idea/
|
169 |
+
|
170 |
+
# Ruff stuff:
|
171 |
+
.ruff_cache/
|
172 |
+
|
173 |
+
# PyPI configuration file
|
174 |
+
.pypirc
|
README.md
CHANGED
@@ -1,14 +1,14 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.21.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: cc
|
11 |
-
short_description:
|
12 |
---
|
13 |
|
14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: HandCraft
|
3 |
+
emoji: ๐๏ธ
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: pink
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.21.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
license: cc
|
11 |
+
short_description: Official sourcecode for HandCraft (WACV 2025)
|
12 |
---
|
13 |
|
14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
component/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from .bbox import bbox
|
2 |
+
from .skeleton import skeleton
|
3 |
+
from .control import control
|
component/bbox.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from ultralytics import YOLO
|
2 |
+
import cv2
|
3 |
+
import numpy as np
|
4 |
+
|
5 |
+
|
6 |
+
hand_dict = {0: 'Standard', 1: 'Non-Standard'}
|
7 |
+
|
8 |
+
def bbox(image, include_standard, mask_expand_ratio):
|
9 |
+
model = YOLO('model/yolo.pt')
|
10 |
+
|
11 |
+
solution = model.predict(image)
|
12 |
+
|
13 |
+
# detect bbox
|
14 |
+
result = []
|
15 |
+
for det in solution[0]:
|
16 |
+
x1, y1, x2, y2, conf, cls = list(det.boxes.data[0])
|
17 |
+
bbox = [int(x1), int(y1), int(x2), int(y2), int(cls), float(conf), hand_dict[int(cls)]]
|
18 |
+
result.append(bbox)
|
19 |
+
|
20 |
+
print('BBox Result:', bbox, image.shape)
|
21 |
+
|
22 |
+
# draw bbox
|
23 |
+
for bbox in result:
|
24 |
+
COLOR = (0, 255, 0) if bbox[4] == 0 else (255, 0, 0)
|
25 |
+
cv2.rectangle(image, (bbox[0], bbox[1]), (bbox[2], bbox[3]), COLOR, 2)
|
26 |
+
cv2.putText(image, str(bbox[6]), (bbox[0], bbox[1]), cv2.FONT_HERSHEY_SIMPLEX, 1, COLOR, 2, cv2.LINE_AA)
|
27 |
+
|
28 |
+
# get mask
|
29 |
+
image_mask = np.zeros(image.shape, dtype=np.uint8)
|
30 |
+
for bbox in result:
|
31 |
+
# only non-standard
|
32 |
+
if not include_standard and bbox[4] == 0:
|
33 |
+
continue
|
34 |
+
|
35 |
+
# expand
|
36 |
+
bbox[0] = max(0, bbox[0] - int((bbox[2] - bbox[0]) * (mask_expand_ratio - 1) * 0.5))
|
37 |
+
bbox[1] = max(0, bbox[1] - int((bbox[3] - bbox[1]) * (mask_expand_ratio - 1) * 0.5))
|
38 |
+
bbox[2] = min(image.shape[1], bbox[2] + int((bbox[2] - bbox[0]) * (mask_expand_ratio - 1) * 0.5))
|
39 |
+
bbox[3] = min(image.shape[0], bbox[3] + int((bbox[3] - bbox[1]) * (mask_expand_ratio - 1) * 0.5))
|
40 |
+
|
41 |
+
image_mask[bbox[1]:bbox[3], bbox[0]:bbox[2]] = 255
|
42 |
+
|
43 |
+
# get bbox info
|
44 |
+
bbox_info = f'Number of hands: {len(result)}\n'
|
45 |
+
for i, hand in enumerate(result):
|
46 |
+
bbox_info += f'Hand {i}: {hand[6]} with confidence {hand[5]:.4f}.\n'
|
47 |
+
|
48 |
+
return bbox_info, image, image_mask
|
component/control.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
import json
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
|
7 |
+
def read_template(name, type):
|
8 |
+
path_template = f'templates/{name}.png'
|
9 |
+
path_keypoint = f'templates/{name}.txt'
|
10 |
+
|
11 |
+
template = cv2.cvtColor(cv2.imread(path_template), cv2.COLOR_BGR2RGB)
|
12 |
+
template_keypoint = np.loadtxt(path_keypoint, dtype=np.int32, delimiter=',')
|
13 |
+
|
14 |
+
if type == 'b':
|
15 |
+
template = cv2.flip(template, 1)
|
16 |
+
template_keypoint[:, 0] = template.shape[1] - template_keypoint[:, 0]
|
17 |
+
|
18 |
+
return template, template_keypoint
|
19 |
+
|
20 |
+
|
21 |
+
def affine(image, image_keypoint, template, template_keypoint, expand_ratio):
|
22 |
+
# pre process
|
23 |
+
template_white = np.full(template.shape, 255, dtype=np.uint8)
|
24 |
+
|
25 |
+
# resize
|
26 |
+
scale = np.linalg.norm(image_keypoint[0] - image_keypoint[2]) / np.linalg.norm(
|
27 |
+
template_keypoint[0] - template_keypoint[2])
|
28 |
+
scale *= expand_ratio
|
29 |
+
M = np.array([
|
30 |
+
[scale, 0, 0],
|
31 |
+
[0, scale, 0],
|
32 |
+
], dtype=np.float32)
|
33 |
+
size = (int(template.shape[1] * scale), int(template.shape[0] * scale))
|
34 |
+
template_white = cv2.warpAffine(template_white, M, size)
|
35 |
+
template = cv2.warpAffine(template, M, size)
|
36 |
+
template_keypoint = template_keypoint * scale
|
37 |
+
|
38 |
+
# move
|
39 |
+
distance = image_keypoint[0] - template_keypoint[0]
|
40 |
+
M = np.array([
|
41 |
+
[1, 0, distance[0]],
|
42 |
+
[0, 1, distance[1]],
|
43 |
+
], dtype=np.float32)
|
44 |
+
template_white = cv2.warpAffine(template_white, M, image.shape[1::-1])
|
45 |
+
template = cv2.warpAffine(template, M, image.shape[1::-1])
|
46 |
+
|
47 |
+
# rotate
|
48 |
+
v1 = template_keypoint[2] - template_keypoint[0]
|
49 |
+
v2 = image_keypoint[2] - image_keypoint[0]
|
50 |
+
|
51 |
+
angle = np.rad2deg(np.arccos(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))))
|
52 |
+
if np.cross(v1, v2) > 0:
|
53 |
+
angle = -angle
|
54 |
+
|
55 |
+
M = cv2.getRotationMatrix2D(image_keypoint[0], angle, 1)
|
56 |
+
template_white = cv2.warpAffine(template_white, M, image.shape[1::-1])
|
57 |
+
template = cv2.warpAffine(template, M, image.shape[1::-1])
|
58 |
+
|
59 |
+
return template_white, template
|
60 |
+
|
61 |
+
|
62 |
+
def control(image, image_bbox_mask, res_skeleton, expand_ratio, template_name, image_skeleton, force):
|
63 |
+
# left hand
|
64 |
+
template_white_l, template_l = np.zeros_like(image), np.zeros_like(image)
|
65 |
+
image_keypoint = np.array(json.loads(res_skeleton))[0]
|
66 |
+
if np.max(image_keypoint) < 1:
|
67 |
+
image_keypoint[:, 0], image_keypoint[:, 1] = image_keypoint[:, 1] * image.shape[1], image_keypoint[:, 0] * image.shape[0]
|
68 |
+
# check hand be detected by yolo
|
69 |
+
be_detected = False
|
70 |
+
for keypoint in image_keypoint:
|
71 |
+
if np.sum(image_bbox_mask[int(keypoint[1]), int(keypoint[0])]) > 1:
|
72 |
+
be_detected = True
|
73 |
+
break
|
74 |
+
if be_detected or force:
|
75 |
+
|
76 |
+
if np.cross(image_keypoint[2] - image_keypoint[0], image_keypoint[3] - image_keypoint[1]) > 0:
|
77 |
+
template, template_keypoint = read_template(template_name, 'a')
|
78 |
+
else:
|
79 |
+
template, template_keypoint = read_template(template_name, 'b')
|
80 |
+
|
81 |
+
template_white_l, template_l = affine(image, image_keypoint, template, template_keypoint, expand_ratio)
|
82 |
+
|
83 |
+
# right hand
|
84 |
+
template_white_r, template_r = np.zeros_like(image), np.zeros_like(image)
|
85 |
+
|
86 |
+
image_keypoint = np.array(json.loads(res_skeleton))[1]
|
87 |
+
if np.max(image_keypoint) < 1:
|
88 |
+
image_keypoint[:, 0], image_keypoint[:, 1] = image_keypoint[:, 1] * image.shape[1], image_keypoint[:, 0] * image.shape[0]
|
89 |
+
# check hand be detected by yolo
|
90 |
+
be_detected = False
|
91 |
+
for keypoint in image_keypoint:
|
92 |
+
if np.sum(image_bbox_mask[int(keypoint[1]), int(keypoint[0])]) > 1:
|
93 |
+
be_detected = True
|
94 |
+
break
|
95 |
+
if be_detected or force:
|
96 |
+
|
97 |
+
if np.cross(image_keypoint[2] - image_keypoint[0], image_keypoint[3] - image_keypoint[1]) > 0:
|
98 |
+
template, template_keypoint = read_template(template_name, 'a')
|
99 |
+
else:
|
100 |
+
template, template_keypoint = read_template(template_name, 'b')
|
101 |
+
|
102 |
+
template_white_r, template_r = affine(image, image_keypoint, template, template_keypoint, expand_ratio)
|
103 |
+
|
104 |
+
|
105 |
+
# control image
|
106 |
+
template = np.clip(template_l.astype(np.int32) + template_r.astype(np.int32), 0, 255).astype(np.uint8)
|
107 |
+
# combine image
|
108 |
+
image_combine = np.clip(image.astype(np.int32) + template, 0, 255).astype(np.uint8)
|
109 |
+
# control mask
|
110 |
+
template_white = np.clip(template_white_l.astype(np.int32) + template_white_r.astype(np.int32), 0, 255).astype(np.uint8)
|
111 |
+
# union mask
|
112 |
+
image_union_mask = np.clip(image_bbox_mask.astype(np.int32) + template_white.astype(np.int32), 0, 255).astype(np.uint8)
|
113 |
+
|
114 |
+
# visualization
|
115 |
+
image_visualization = np.clip(
|
116 |
+
template.astype(np.int32) * - 1 +
|
117 |
+
image_skeleton.astype(np.int32) * 0.6 +
|
118 |
+
image_bbox_mask.astype(np.int32) * 0.3 +
|
119 |
+
template_white.astype(np.int32) * 0.3
|
120 |
+
, 0, 255).astype(np.uint8)
|
121 |
+
|
122 |
+
return image_visualization, template, template_white, image_union_mask
|
123 |
+
|
component/skeleton.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import mediapipe as mp
|
2 |
+
import numpy as np
|
3 |
+
import json
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
def skeleton(image):
|
7 |
+
mp_pose = mp.solutions.pose.Pose()
|
8 |
+
mp_drawing = mp.solutions.drawing_utils
|
9 |
+
|
10 |
+
solution = mp_pose.process(image)
|
11 |
+
|
12 |
+
# default value
|
13 |
+
result = np.zeros((33, 2))
|
14 |
+
|
15 |
+
# detect and draw skeleton
|
16 |
+
if solution.pose_landmarks:
|
17 |
+
mp_drawing.draw_landmarks(image, solution.pose_landmarks, mp.solutions.pose.POSE_CONNECTIONS)
|
18 |
+
result = np.array([
|
19 |
+
(landmark.y, landmark.x)
|
20 |
+
for landmark in solution.pose_landmarks.landmark
|
21 |
+
])
|
22 |
+
|
23 |
+
# serialization
|
24 |
+
skeleton_info = json.dumps(np.array([result[15:22:2], result[16:23:2]]).tolist())
|
25 |
+
|
26 |
+
return skeleton_info, image
|
examples/a.png
ADDED
![]() |
Git LFS Details
|
examples/b.png
ADDED
![]() |
Git LFS Details
|
main.py
ADDED
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import cv2
|
3 |
+
from component import bbox
|
4 |
+
from component import skeleton
|
5 |
+
from component import control
|
6 |
+
|
7 |
+
# Load example image
|
8 |
+
image_example = cv2.cvtColor(cv2.imread("examples/a.png"), cv2.COLOR_BGR2RGB)
|
9 |
+
uni_height = 800
|
10 |
+
|
11 |
+
# Create the interface with a cute theme
|
12 |
+
with gr.Blocks(theme=gr.themes.Soft()) as interface:
|
13 |
+
# Title with kawaii emojis
|
14 |
+
with gr.Row():
|
15 |
+
with gr.Column(scale=1):
|
16 |
+
gr.Markdown(f"""
|
17 |
+
<div style='text-align: center;
|
18 |
+
padding: 25px;
|
19 |
+
border-radius: 12px;
|
20 |
+
background: #f3f4f6;
|
21 |
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08),
|
22 |
+
0 2px 4px rgba(0, 0, 0, 0.05);
|
23 |
+
margin: 20px 0;
|
24 |
+
border: 1px solid rgba(0,0,0,0.1);
|
25 |
+
width: 100%;
|
26 |
+
position: relative;
|
27 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;'>
|
28 |
+
<h1 style='margin: 0;
|
29 |
+
color: #BE830E;
|
30 |
+
font-family: "Comic Sans MS", cursive;
|
31 |
+
text-shadow: 2px 2px 4px rgba(190, 131, 14, 0.1);
|
32 |
+
letter-spacing: 1.2px;
|
33 |
+
position: relative;
|
34 |
+
z-index: 2;'>
|
35 |
+
๐๏ธ HandCraft ๐๏ธ
|
36 |
+
</h1>
|
37 |
+
<h3 style='margin: 10px 0 0;
|
38 |
+
color: #374151;
|
39 |
+
font-family: "Comic Sans MS", cursive;
|
40 |
+
font-weight: 500;
|
41 |
+
position: relative;
|
42 |
+
z-index: 2;
|
43 |
+
line-height: 1.4;
|
44 |
+
text-shadow: 0 1px 2px rgba(0,0,0,0.05);'>
|
45 |
+
๐พโจ Anatomically Correct Restoration of Malformed Hands in Diffusion Generated Images โจ๐พ
|
46 |
+
</h3>
|
47 |
+
</div>
|
48 |
+
""")
|
49 |
+
|
50 |
+
# Shared input image at the top
|
51 |
+
input_image = gr.Image(
|
52 |
+
type="numpy",
|
53 |
+
label="๐ธ Upload Your Image with Hands",
|
54 |
+
height=uni_height,
|
55 |
+
value=image_example
|
56 |
+
)
|
57 |
+
|
58 |
+
# Button to trigger the cascade
|
59 |
+
generate_btn = gr.Button("โจ๐ช Generate Control Mask ๐ชโจ", variant="primary", size="lg")
|
60 |
+
|
61 |
+
# State variables to store intermediate results
|
62 |
+
bbox_mask_state = gr.State()
|
63 |
+
keypoints_state = gr.State()
|
64 |
+
skeleton_state = gr.State()
|
65 |
+
|
66 |
+
# Results section with tabs for each step
|
67 |
+
with gr.Tabs():
|
68 |
+
with gr.TabItem("๐พ Step 1: Malformed Hand Detection"):
|
69 |
+
with gr.Row():
|
70 |
+
with gr.Column(scale=1):
|
71 |
+
output_bbox_result = gr.Textbox(label="๐ท๏ธ Number of Hands & Classification with Confidence")
|
72 |
+
include_standard = gr.Checkbox(
|
73 |
+
label="๐คฒ Include Standard Hands",
|
74 |
+
value=False
|
75 |
+
)
|
76 |
+
expand_ratio = gr.Slider(
|
77 |
+
minimum=0.5,
|
78 |
+
maximum=2,
|
79 |
+
step=0.01,
|
80 |
+
value=1,
|
81 |
+
label="๐ Bounding Box Expand Ratio"
|
82 |
+
)
|
83 |
+
with gr.Column(scale=2):
|
84 |
+
with gr.Row():
|
85 |
+
with gr.Column(scale=1):
|
86 |
+
output_bbox_vis = gr.Image(type="numpy", label="๐ฆ Bounding Box", height=uni_height)
|
87 |
+
with gr.Column(scale=1):
|
88 |
+
output_bbox_mask = gr.Image(type="numpy", label="๐ญ Bounding Box Mask", height=uni_height)
|
89 |
+
|
90 |
+
with gr.TabItem("๐ Step 2: Body Pose Estimation"):
|
91 |
+
with gr.Row():
|
92 |
+
with gr.Column(scale=1):
|
93 |
+
output_keypoints = gr.Textbox(label="๐ Key Points String")
|
94 |
+
with gr.Column(scale=2):
|
95 |
+
output_skeleton = gr.Image(type="numpy", label="๐ช Body Skeleton", height=uni_height)
|
96 |
+
|
97 |
+
with gr.TabItem("๐จ Step 3: Control Image Generation"):
|
98 |
+
with gr.Row():
|
99 |
+
with gr.Column(scale=1):
|
100 |
+
hand_template = gr.Radio(
|
101 |
+
["opened-palm", "fist-back"],
|
102 |
+
label="๐ Hand Template",
|
103 |
+
value="opened-palm"
|
104 |
+
)
|
105 |
+
control_expand = gr.Slider(
|
106 |
+
minimum=0.5,
|
107 |
+
maximum=2,
|
108 |
+
step=0.01,
|
109 |
+
value=1,
|
110 |
+
label="๐ Control Image Expand Ratio"
|
111 |
+
)
|
112 |
+
include_undetected = gr.Checkbox(
|
113 |
+
label="๐ Include Undetected Hand",
|
114 |
+
value=False
|
115 |
+
)
|
116 |
+
with gr.Column(scale=2):
|
117 |
+
with gr.Row():
|
118 |
+
with gr.Column():
|
119 |
+
output_viz = gr.Image(type="numpy", label="๐๏ธ Visualization Image", height=300)
|
120 |
+
with gr.Column():
|
121 |
+
output_control = gr.Image(type="numpy", label="๐ฎ Control Image", height=300)
|
122 |
+
with gr.Row():
|
123 |
+
with gr.Column():
|
124 |
+
output_control_mask = gr.Image(type="numpy", label="๐ญ Control Mask", height=300)
|
125 |
+
with gr.Column():
|
126 |
+
output_union_mask = gr.Image(type="numpy", label="๐ Union Mask", height=300)
|
127 |
+
|
128 |
+
with gr.TabItem("๐ Output Control Image"):
|
129 |
+
gr.Markdown("""
|
130 |
+
### โจ๐ Control Image ๐โจ
|
131 |
+
Your hand-fixed image is ready! (๏พโใฎโ)๏พ*:๏ฝฅ๏พโง
|
132 |
+
""")
|
133 |
+
with gr.Row():
|
134 |
+
with gr.Column():
|
135 |
+
output_final_control = gr.Image(type="numpy", label="๐ Fixed Hand Image", interactive=False, height=uni_height)
|
136 |
+
with gr.Column():
|
137 |
+
gr.Markdown("""
|
138 |
+
### ๐โจ How to Use Your Control Image โจ๐
|
139 |
+
|
140 |
+
1. ๐ช Take this Control Image to your favorite Stable Diffusion model
|
141 |
+
2. ๐ Apply that to the ControlNet
|
142 |
+
3. ๐ฌ Sprinkle some parameters until it looks just right!
|
143 |
+
|
144 |
+
""")
|
145 |
+
|
146 |
+
# Citation information with cute emojis
|
147 |
+
with gr.Accordion("๐โจ Citation Information โจ๐", open=False):
|
148 |
+
gr.Markdown("""
|
149 |
+
If you find this tool helpful for your research, please cite our paper: ๐
|
150 |
+
|
151 |
+
```bibtex
|
152 |
+
@InProceedings{2025_wacv_handcraft,
|
153 |
+
author = {Qin, Zhenyue and Zhang, Yiqun and Liu, Yang and Campbell, Dylan},
|
154 |
+
title = {HandCraft: Anatomically Correct Restoration of Malformed Hands in Diffusion Generated Images},
|
155 |
+
booktitle = {Proceedings of the Winter Conference on Applications of Computer Vision (WACV)},
|
156 |
+
month = {February},
|
157 |
+
year = {2025},
|
158 |
+
pages = {3925-3933}
|
159 |
+
}
|
160 |
+
```
|
161 |
+
|
162 |
+
Thank you for using HandCraft! โจ๐โจ
|
163 |
+
""")
|
164 |
+
|
165 |
+
# Define the step functions with improved data flow
|
166 |
+
def run_step1(image, include_std, expand_r):
|
167 |
+
# Step 1: Run hand detection
|
168 |
+
bbox_result, bbox_vis, bbox_mask = bbox(image, include_std, expand_r)
|
169 |
+
return bbox_result, bbox_vis, bbox_mask, bbox_mask
|
170 |
+
|
171 |
+
def run_step2(image):
|
172 |
+
# Step 2: Run pose estimation
|
173 |
+
keypoints, skeleton_img = skeleton(image)
|
174 |
+
return keypoints, skeleton_img, keypoints, skeleton_img
|
175 |
+
|
176 |
+
def run_step3(image, bbox_mask, keypoints, control_exp, hand_tmpl, skeleton_img, include_undetect):
|
177 |
+
# Step 3: Generate control images
|
178 |
+
viz, control_img, control_mask, union_mask = control(
|
179 |
+
image, bbox_mask, keypoints, control_exp, hand_tmpl, skeleton_img, include_undetect
|
180 |
+
)
|
181 |
+
return viz, control_img, control_mask, union_mask, control_img
|
182 |
+
|
183 |
+
# Connect the Generate button to trigger all steps in sequence
|
184 |
+
generate_btn.click(
|
185 |
+
fn=run_step1,
|
186 |
+
inputs=[input_image, include_standard, expand_ratio],
|
187 |
+
outputs=[output_bbox_result, output_bbox_vis, output_bbox_mask, bbox_mask_state]
|
188 |
+
).then(
|
189 |
+
fn=run_step2,
|
190 |
+
inputs=[input_image],
|
191 |
+
outputs=[output_keypoints, output_skeleton, keypoints_state, skeleton_state]
|
192 |
+
).then(
|
193 |
+
fn=run_step3,
|
194 |
+
inputs=[
|
195 |
+
input_image,
|
196 |
+
bbox_mask_state,
|
197 |
+
keypoints_state,
|
198 |
+
control_expand,
|
199 |
+
hand_template,
|
200 |
+
skeleton_state,
|
201 |
+
include_undetected
|
202 |
+
],
|
203 |
+
outputs=[output_viz, output_control, output_control_mask, output_union_mask, output_final_control]
|
204 |
+
)
|
205 |
+
|
206 |
+
# Launch the interface
|
207 |
+
interface.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
model/yolo.pt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:8fa7a64d136d9b9356a72ed2238fa4744833cb1dc6078fc395e39c2d664372e2
|
3 |
+
size 136682110
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
torch==2.0.1
|
2 |
+
gradio==3.48.0
|
3 |
+
mediapipe==0.10.5
|
4 |
+
numpy==1.26.1
|
5 |
+
opencv_contrib_python==4.8.0.76
|
6 |
+
opencv_python==4.8.0.76
|
7 |
+
Pillow==10.1.0
|
8 |
+
ultralytics==8.0.180
|
9 |
+
accelerate==0.23.0
|
10 |
+
transformers==4.33.2
|
templates/fist-back.png
ADDED
![]() |
Git LFS Details
|
templates/fist-back.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
153, 84
|
2 |
+
87, 268
|
3 |
+
175, 342
|
4 |
+
270, 300
|
5 |
+
256, 267
|
templates/opened-palm.png
ADDED
![]() |
Git LFS Details
|
templates/opened-palm.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
414, 127
|
2 |
+
460, 365
|
3 |
+
362, 477
|
4 |
+
286, 393
|
5 |
+
646, 960
|