Skip to content

Commit 01f13be

Browse files
committed
add face emotion and landmarks APP
1 parent 1be392c commit 01f13be

File tree

12 files changed

+353
-1
lines changed

12 files changed

+353
-1
lines changed

docs/doc/en/vision/face_landmarks.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,48 @@ while not app.need_exit():
7272
- `landmarks_abs`: Specifies the coordinates of face keypoints in the original `img`. The `points` variable contains 478 keypoints in the order `x0, y0, x1, y1, ..., x477, y477`.
7373
- `landmarks_rel`: Outputs coordinates in `img_std` and appends the results to the `points` variable.
7474
- `points_z`: Represents depth estimation of the keypoints relative to the face's center of gravity. The closer to the camera, the larger the value. If behind the face's center, the value is negative. The values are proportional to the face's width.
75+
76+
## Extracting Partial Keypoints
77+
78+
The 478 keypoints may be excessive for some applications. If you only need specific ones, you can select them based on the <a href="../../assets/maixcam_face_landmarks_full.jpg" target="_blank">high-resolution image</a> index. Common subsets include:
79+
**Note: These are for reference only. Please rely on the actual model output for accuracy.**
80+
81+
* **146 Keypoints:**
82+
```python
83+
sub_146_idxes = [0, 1, 4, 5, 6, 7, 8, 10, 13, 14, 17, 21, 33, 37, 39, 40, 46, 52, 53, 54, 55, 58, 61, 63, 65, 66, 67, 70, 78, 80,
84+
81, 82, 84, 87, 88, 91, 93, 95, 103, 105, 107, 109, 127, 132, 133, 136, 144, 145, 146, 148, 149, 150, 152, 153,
85+
154, 155, 157, 158, 159, 160, 161, 162, 163, 168, 172, 173, 176, 178, 181, 185, 191, 195, 197, 234, 246, 249,
86+
251, 263, 267, 269, 270, 276, 282, 283, 284, 285, 288, 291, 293, 295, 296, 297, 300, 308, 310, 311, 312, 314,
87+
317, 318, 321, 323, 324, 332, 334, 336, 338, 356, 361, 362, 365, 373, 374, 375, 377, 378, 379, 380, 381, 382,
88+
384, 385, 386, 387, 388, 389, 390, 397, 398, 400, 402, 405, 409, 415, 454, 466, 468, 469, 470, 471, 472, 473,
89+
474, 475, 476, 477]
90+
```
91+
92+
* **68 Keypoints:**
93+
```python
94+
sub_68_idxes = [162, 234, 93, 58, 172, 136, 149, 148, 152, 377, 378, 365, 397, 288, 323, 454, 389, 71, 63, 105, 66, 107, 336,
95+
296, 334, 293, 301, 168, 197, 5, 4, 75, 97, 2, 326, 305, 33, 160, 158, 133, 153, 144, 362, 385, 387, 263, 373,
96+
380, 61, 39, 37, 0, 267, 269, 291, 405, 314, 17, 84, 181, 78, 82, 13, 312, 308, 317, 14, 87]
97+
```
98+
99+
* **5 Keypoints:**
100+
```python
101+
sub_5_idxes = [468, 473, 4, 61, 291]
102+
```
103+
104+
With these indices, you can use the following code to extract and display specific subsets of keypoints:
105+
106+
```python
107+
def get_sub_landmarks(points, points_z, idxes):
108+
new_points = []
109+
new_points_z = []
110+
for i in idxes:
111+
new_points.append(points[i * 2])
112+
new_points.append(points[i * 2 + 1])
113+
new_points_z.append(points_z[i])
114+
return new_points, new_points_z
115+
116+
sub_xy, sub_z = get_sub_landmarks(res.points, res.points_z, sub_146_idxes)
117+
landmarks_detector.draw_face(img, sub_xy, len(sub_z), sub_z)
75118
```
76119

docs/doc/zh/vision/face_landmarks.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ while not app.need_exit():
6060
if count >= max_face_num:
6161
break
6262
for res in results:
63-
landmarks_detector.draw_face(img, res.points, landmarks_detector.landmarks_num, res.points_z)
63+
landmarks_detector.draw_face(img, res.points, len(res.points_z), res.points_z)
6464
disp.show(img)
6565
```
6666

@@ -71,3 +71,42 @@ while not app.need_exit():
7171
* `points_z` 是关键点深度估计,值相对于面部重心,离镜头越近值越大,在面部重心之后则为负值,值与面部宽度成比例。
7272

7373

74+
## 取部分关键点
75+
76+
478 个关键点有点多,如果你只需要其中几个,可以根据 <a href="../../assets/maixcam_face_landmarks_full.jpg" target="_blank">高清大图</a> 的下标取部分,常见的:
77+
**注意只提供参考,以模型实际输出为准**
78+
* 146 个点:
79+
```python
80+
sub_146_idxes = [0, 1, 4, 5, 6, 7, 8, 10, 13, 14, 17, 21, 33, 37, 39, 40, 46, 52, 53, 54, 55, 58, 61, 63, 65, 66, 67, 70, 78, 80,
81+
81, 82, 84, 87, 88, 91, 93, 95, 103, 105, 107, 109, 127, 132, 133, 136, 144, 145, 146, 148, 149, 150, 152, 153, 154, 155, 157,
82+
158, 159, 160, 161, 162, 163, 168, 172, 173, 176, 178, 181, 185, 191, 195, 197, 234, 246, 249, 251, 263, 267, 269, 270, 276, 282,
83+
283, 284, 285, 288, 291, 293, 295, 296, 297, 300, 308, 310, 311, 312, 314, 317, 318, 321, 323, 324, 332, 334, 336, 338, 356, 361,
84+
362, 365, 373, 374, 375, 377, 378, 379, 380, 381, 382, 384, 385, 386, 387, 388, 389, 390, 397, 398, 400, 402, 405,
85+
409, 415, 454, 466, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477]
86+
```
87+
* 68 个点:
88+
```python
89+
sub_68_idxes = [162, 234, 93, 58, 172, 136, 149, 148, 152, 377, 378, 365, 397, 288, 323, 454, 389, 71, 63, 105, 66, 107, 336,
90+
296, 334, 293, 301, 168, 197, 5, 4, 75, 97, 2, 326, 305, 33, 160, 158, 133, 153, 144, 362, 385, 387, 263, 373,
91+
380, 61, 39, 37, 0, 267, 269, 291, 405, 314, 17, 84, 181, 78, 82, 13, 312, 308, 317, 14, 87]
92+
```
93+
* 5 个点
94+
```python
95+
sub_5_idxes = [468, 473, 4, 61, 291]
96+
```
97+
98+
有了这些下标,我们用代码提取部分出来显示:
99+
```python
100+
def get_sub_landmarks(points, points_z, idxes):
101+
new_points = []
102+
new_points_z = []
103+
for i in idxes:
104+
new_points.append(points[i*2])
105+
new_points.append(points[i*2 + 1])
106+
new_points_z.append(points_z[i])
107+
return new_points, new_points_z
108+
109+
sub_xy, sub_z = get_sub_landmarks(res.points, res.points_z, sub_146_idxes)
110+
landmarks_detector.draw_face(img, sub_xy, len(sub_z), sub_z)
111+
```
112+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
data
2+
__pycache__
3+
dist
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Face landmarks detection
2+
=====
3+
4+
5+
6+
visit https://wiki.sipeed.com/maixpy/doc/zh/vision/face_landmarks.html
7+

projects/app_face_emotion/app.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
id: face_emotion
2+
name: Face Emotion
3+
name[zh]: 人脸情绪
4+
version: 1.0.0
5+
icon: icon.png
6+
author: neucrack@Sipeed Ltd
7+
desc: Detect face emotion
8+
desc[zh]: 检测人脸情绪和表情
9+
exclude:
10+
- data
11+
- dist
12+
- .gitignore
13+

projects/app_face_emotion/icon.png

1.89 KB
Loading

projects/app_face_emotion/main.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from maix import camera, display, image, nn, app, time, touchscreen
2+
import math
3+
4+
5+
models = {
6+
"7 classes": "/root/models/face_emotion.mud"
7+
}
8+
models_keys = list(models.keys())
9+
curr_model = 0
10+
11+
def is_in_button(x, y, btn_pos):
12+
return x > btn_pos[0] and x < btn_pos[0] + btn_pos[2] and y > btn_pos[1] and y < btn_pos[1] + btn_pos[3]
13+
14+
def main(disp):
15+
global curr_model
16+
17+
detect_conf_th = 0.5
18+
detect_iou_th = 0.45
19+
emotion_conf_th = 0.5
20+
max_face_num = -1
21+
crop_scale = 1.2
22+
23+
# detect face model
24+
detector = nn.YOLOv8(model="/root/models/yolov8n_face.mud", dual_buff = False)
25+
# we only use one of it's function to crop face from image, wo we not init model actually
26+
landmarks_detector = nn.FaceLandmarks(model="")
27+
# emotion classify model
28+
classifier = nn.Classifier(model=models[models_keys[curr_model]], dual_buff=False)
29+
cam = camera.Camera(detector.input_width(), detector.input_height(), detector.input_format())
30+
31+
mode_pressed = False
32+
ts = touchscreen.TouchScreen()
33+
img_back = image.load("/maixapp/share/icon/ret.png")
34+
back_rect = [0, 0, 32, 32]
35+
mode_rect = [0, cam.height() - 26, image.string_size(models_keys[curr_model]).width() + 6, 30]
36+
back_rect_disp = image.resize_map_pos(cam.width(), cam.height(), disp.width(), disp.height(), image.Fit.FIT_CONTAIN, back_rect[0], back_rect[1], back_rect[2], back_rect[3])
37+
mode_rect_disp = image.resize_map_pos(cam.width(), cam.height(), disp.width(), disp.height(), image.Fit.FIT_CONTAIN, mode_rect[0], mode_rect[1], mode_rect[2], mode_rect[3])
38+
39+
40+
# for draw result info
41+
max_labels_length = 0
42+
for label in classifier.labels:
43+
size = image.string_size(label)
44+
if size.width() > max_labels_length:
45+
max_labels_length = size.width()
46+
47+
max_score_length = cam.width() / 4
48+
49+
while not app.need_exit():
50+
img = cam.read()
51+
results = []
52+
objs = detector.detect(img, conf_th = detect_conf_th, iou_th = detect_iou_th, sort = 1)
53+
count = 0
54+
idxes = []
55+
img_std_first : image.Image = None
56+
for i, obj in enumerate(objs):
57+
img_std = landmarks_detector.crop_image(img, obj.x, obj.y, obj.w, obj.h, obj.points,
58+
classifier.input_width(), classifier.input_height(), crop_scale)
59+
if img_std:
60+
img_std_gray = img_std.to_format(image.Format.FMT_GRAYSCALE)
61+
res = classifier.classify(img_std_gray, softmax=True)
62+
results.append(res)
63+
idxes.append(i)
64+
if i == 0:
65+
img_std_first = img_std
66+
count += 1
67+
if max_face_num > 0 and count >= max_face_num:
68+
break
69+
for i, res in enumerate(results):
70+
# draw fisrt face detailed info
71+
if i == 0:
72+
img.draw_image(0, 0, img_std_first)
73+
for j in range(len(classifier.labels)):
74+
idx = res[j][0]
75+
score = res[j][1]
76+
img.draw_string(0, img_std_first.height() + idx * 16, classifier.labels[idx], image.COLOR_WHITE)
77+
img.draw_rect(max_labels_length, int(img_std_first.height() + idx * 16), int(score * max_score_length), 8, image.COLOR_GREEN if score >= emotion_conf_th else image.COLOR_RED, -1)
78+
img.draw_string(int(max_labels_length + score * max_score_length + 2), int(img_std_first.height() + idx * 16), f"{score:.1f}", image.COLOR_RED)
79+
# draw on all face
80+
color = image.COLOR_GREEN if res[0][1] >= emotion_conf_th else image.COLOR_RED
81+
obj = objs[idxes[i]]
82+
img.draw_rect(obj.x, obj.y, obj.w, obj.h, color, 1)
83+
img.draw_string(obj.x, obj.y, f"{classifier.labels[res[0][0]]}: {res[0][1]:.1f}", color)
84+
85+
img.draw_image(0, 0, img_back)
86+
img.draw_rect(mode_rect[0], mode_rect[1], mode_rect[2], mode_rect[3], image.COLOR_WHITE)
87+
img.draw_string(4, img.height() - 20, f"{models_keys[curr_model]}")
88+
disp.show(img)
89+
x, y, preesed = ts.read()
90+
if preesed:
91+
mode_pressed = True
92+
elif mode_pressed:
93+
mode_pressed = False
94+
if is_in_button(x, y, back_rect_disp):
95+
app.set_exit_flag(True)
96+
if is_in_button(x, y, mode_rect_disp):
97+
curr_model = (curr_model + 1) % len(models_keys)
98+
msg = "switching model ..."
99+
size = image.string_size(msg, scale=1.3)
100+
img.draw_string((img.width() - size.width()) // 2, (img.height() - size.height())//2, msg, image.COLOR_RED, scale=1.3, thickness=-3)
101+
img.draw_string((img.width() - size.width()) // 2, (img.height() - size.height())//2, msg, image.COLOR_WHITE, scale=1.3)
102+
disp.show(img)
103+
del detector
104+
del landmarks_detector
105+
break
106+
107+
disp = display.Display()
108+
try:
109+
while not app.need_exit():
110+
main(disp)
111+
except Exception:
112+
import traceback
113+
msg = traceback.format_exc()
114+
img = image.Image(disp.width(), disp.height())
115+
img.draw_string(0, 0, msg, image.COLOR_WHITE)
116+
disp.show(img)
117+
while not app.need_exit():
118+
time.sleep_ms(100)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
data
2+
__pycache__
3+
dist
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Face landmarks detection
2+
=====
3+
4+
5+
6+
visit https://wiki.sipeed.com/maixpy/doc/zh/vision/face_landmarks.html
7+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
id: face_landmarks
2+
name: Face Landmarks
3+
name[zh]: 人脸关键点
4+
version: 1.0.0
5+
icon: icon.png
6+
author: neucrack@Sipeed Ltd
7+
desc: Detect face landmarks
8+
desc[zh]: 检测人脸关键点
9+
exclude:
10+
- data
11+
- dist
12+
- .gitignore
13+

0 commit comments

Comments
 (0)