Abhishek Gola
commited on
Commit
·
a58bc70
1
Parent(s):
4b9f394
Added facial expression recognition to opencv spaces
Browse files- README.md +7 -0
- app.py +64 -0
- facial_fer_model.py +176 -0
- requirements.txt +4 -0
- yunet.py +55 -0
README.md
CHANGED
@@ -7,6 +7,13 @@ sdk: gradio
|
|
7 |
sdk_version: 5.34.2
|
8 |
app_file: app.py
|
9 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
---
|
11 |
|
12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
7 |
sdk_version: 5.34.2
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
+
short_description: Facial expression recognition using OpenCV
|
11 |
+
tags:
|
12 |
+
- opencv
|
13 |
+
- face-detection
|
14 |
+
- facial-expression
|
15 |
+
- onnx
|
16 |
+
- gradio
|
17 |
---
|
18 |
|
19 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2 as cv
|
2 |
+
import numpy as np
|
3 |
+
import gradio as gr
|
4 |
+
import datetime
|
5 |
+
from huggingface_hub import hf_hub_download
|
6 |
+
|
7 |
+
from facial_fer_model import FacialExpressionRecog
|
8 |
+
from yunet import YuNet
|
9 |
+
|
10 |
+
# Download ONNX model from Hugging Face
|
11 |
+
FD_MODEL_PATH = hf_hub_download(repo_id="opencv/face_detection_yunet", filename="face_detection_yunet_2023mar.onnx")
|
12 |
+
FER_MODEL_PATH = hf_hub_download(repo_id="opencv/facial_expression_recognition", filename="facial_expression_recognition_mobilefacenet_2022july.onnx")
|
13 |
+
|
14 |
+
backend_id = cv.dnn.DNN_BACKEND_OPENCV
|
15 |
+
target_id = cv.dnn.DNN_TARGET_CPU
|
16 |
+
|
17 |
+
fer_model = FacialExpressionRecog(modelPath=FER_MODEL_PATH, backendId=backend_id, targetId=target_id)
|
18 |
+
detect_model = YuNet(modelPath=FD_MODEL_PATH)
|
19 |
+
|
20 |
+
def visualize(image, det_res, fer_res):
|
21 |
+
output = image.copy()
|
22 |
+
landmark_color = [(255, 0, 0), (0, 0, 255), (0, 255, 0), (255, 0, 255), (0, 255, 255)]
|
23 |
+
|
24 |
+
for det, fer_type in zip(det_res, fer_res):
|
25 |
+
bbox = det[0:4].astype(np.int32)
|
26 |
+
fer_type_str = FacialExpressionRecog.getDesc(fer_type)
|
27 |
+
cv.rectangle(output, (bbox[0], bbox[1]), (bbox[0]+bbox[2], bbox[1]+bbox[3]), (0, 255, 0), 2)
|
28 |
+
cv.putText(output, fer_type_str, (bbox[0], bbox[1] - 10), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
|
29 |
+
|
30 |
+
landmarks = det[4:14].astype(np.int32).reshape((5, 2))
|
31 |
+
for idx, landmark in enumerate(landmarks):
|
32 |
+
cv.circle(output, landmark, 2, landmark_color[idx], 2)
|
33 |
+
|
34 |
+
return output
|
35 |
+
|
36 |
+
def detect_expression(input_image):
|
37 |
+
image = cv.cvtColor(input_image, cv.COLOR_RGB2BGR)
|
38 |
+
h, w, _ = image.shape
|
39 |
+
detect_model.setInputSize([w, h])
|
40 |
+
|
41 |
+
dets = detect_model.infer(image)
|
42 |
+
if dets is None:
|
43 |
+
return cv.cvtColor(image, cv.COLOR_BGR2RGB)
|
44 |
+
|
45 |
+
fer_res = []
|
46 |
+
for face_points in dets:
|
47 |
+
result = fer_model.infer(image, face_points[:-1])
|
48 |
+
fer_res.append(result[0])
|
49 |
+
|
50 |
+
output = visualize(image, dets, fer_res)
|
51 |
+
return cv.cvtColor(output, cv.COLOR_BGR2RGB)
|
52 |
+
|
53 |
+
# Gradio UI
|
54 |
+
demo = gr.Interface(
|
55 |
+
fn=detect_expression,
|
56 |
+
inputs=gr.Image(type="numpy", label="Upload Image"),
|
57 |
+
outputs=gr.Image(type="numpy", label="Facial Expression Result"),
|
58 |
+
title="Facial Expression Recognition (FER) with OpenCV DNN",
|
59 |
+
description="Detects faces and recognizes facial expressions using YuNet + MobileFaceNet ONNX models.",
|
60 |
+
allow_flagging="never"
|
61 |
+
)
|
62 |
+
|
63 |
+
if __name__ == "__main__":
|
64 |
+
demo.launch()
|
facial_fer_model.py
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file is part of OpenCV Zoo project.
|
2 |
+
# It is subject to the license terms in the LICENSE file found in the same directory.
|
3 |
+
#
|
4 |
+
# Copyright (C) 2022, Shenzhen Institute of Artificial Intelligence and Robotics for Society, all rights reserved.
|
5 |
+
# Third party copyrights are property of their respective owners.
|
6 |
+
|
7 |
+
import numpy as np
|
8 |
+
import cv2 as cv
|
9 |
+
|
10 |
+
class FacialExpressionRecog:
|
11 |
+
def __init__(self, modelPath, backendId=0, targetId=0):
|
12 |
+
self._modelPath = modelPath
|
13 |
+
self._backendId = backendId
|
14 |
+
self._targetId = targetId
|
15 |
+
|
16 |
+
self._model = cv.dnn.readNet(self._modelPath)
|
17 |
+
self._model.setPreferableBackend(self._backendId)
|
18 |
+
self._model.setPreferableTarget(self._targetId)
|
19 |
+
|
20 |
+
self._align_model = FaceAlignment()
|
21 |
+
|
22 |
+
self._inputNames = 'data'
|
23 |
+
self._outputNames = ['label']
|
24 |
+
self._inputSize = [112, 112]
|
25 |
+
self._mean = np.array([0.5, 0.5, 0.5])[np.newaxis, np.newaxis, :]
|
26 |
+
self._std = np.array([0.5, 0.5, 0.5])[np.newaxis, np.newaxis, :]
|
27 |
+
|
28 |
+
@property
|
29 |
+
def name(self):
|
30 |
+
return self.__class__.__name__
|
31 |
+
|
32 |
+
def setBackendAndTarget(self, backendId, targetId):
|
33 |
+
self._backendId = backendId
|
34 |
+
self._targetId = targetId
|
35 |
+
self._model.setPreferableBackend(self._backendId)
|
36 |
+
self._model.setPreferableTarget(self._targetId)
|
37 |
+
|
38 |
+
def _preprocess(self, image, bbox):
|
39 |
+
if bbox is not None:
|
40 |
+
image = self._align_model.get_align_image(image, bbox[4:].reshape(-1, 2))
|
41 |
+
image = cv.cvtColor(image, cv.COLOR_BGR2RGB)
|
42 |
+
image = image.astype(np.float32, copy=False) / 255.0
|
43 |
+
image -= self._mean
|
44 |
+
image /= self._std
|
45 |
+
return cv.dnn.blobFromImage(image)
|
46 |
+
|
47 |
+
def infer(self, image, bbox=None):
|
48 |
+
# Preprocess
|
49 |
+
inputBlob = self._preprocess(image, bbox)
|
50 |
+
|
51 |
+
# Forward
|
52 |
+
self._model.setInput(inputBlob, self._inputNames)
|
53 |
+
outputBlob = self._model.forward(self._outputNames)
|
54 |
+
|
55 |
+
# Postprocess
|
56 |
+
results = self._postprocess(outputBlob)
|
57 |
+
|
58 |
+
return results
|
59 |
+
|
60 |
+
def _postprocess(self, outputBlob):
|
61 |
+
result = np.argmax(outputBlob[0], axis=1).astype(np.uint8)
|
62 |
+
return result
|
63 |
+
|
64 |
+
@staticmethod
|
65 |
+
def getDesc(ind):
|
66 |
+
_expression_enum = ["angry", "disgust", "fearful", "happy", "neutral", "sad", "surprised"]
|
67 |
+
return _expression_enum[ind]
|
68 |
+
|
69 |
+
|
70 |
+
class FaceAlignment():
|
71 |
+
def __init__(self, reflective=False):
|
72 |
+
self._std_points = np.array([[38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041]])
|
73 |
+
self.reflective = reflective
|
74 |
+
|
75 |
+
def __tformfwd(self, trans, uv):
|
76 |
+
uv = np.hstack((uv, np.ones((uv.shape[0], 1))))
|
77 |
+
xy = np.dot(uv, trans)
|
78 |
+
xy = xy[:, 0:-1]
|
79 |
+
return xy
|
80 |
+
|
81 |
+
def __tforminv(self, trans, uv):
|
82 |
+
Tinv = np.linalg.inv(trans)
|
83 |
+
xy = self.__tformfwd(Tinv, uv)
|
84 |
+
return xy
|
85 |
+
|
86 |
+
def __findNonreflectiveSimilarity(self, uv, xy, options=None):
|
87 |
+
options = {"K": 2}
|
88 |
+
|
89 |
+
K = options["K"]
|
90 |
+
M = xy.shape[0]
|
91 |
+
x = xy[:, 0].reshape((-1, 1)) # use reshape to keep a column vector
|
92 |
+
y = xy[:, 1].reshape((-1, 1)) # use reshape to keep a column vector
|
93 |
+
# print '--->x, y:\n', x, y
|
94 |
+
|
95 |
+
tmp1 = np.hstack((x, y, np.ones((M, 1)), np.zeros((M, 1))))
|
96 |
+
tmp2 = np.hstack((y, -x, np.zeros((M, 1)), np.ones((M, 1))))
|
97 |
+
X = np.vstack((tmp1, tmp2))
|
98 |
+
# print '--->X.shape: ', X.shape
|
99 |
+
# print 'X:\n', X
|
100 |
+
|
101 |
+
u = uv[:, 0].reshape((-1, 1)) # use reshape to keep a column vector
|
102 |
+
v = uv[:, 1].reshape((-1, 1)) # use reshape to keep a column vector
|
103 |
+
U = np.vstack((u, v))
|
104 |
+
# print '--->U.shape: ', U.shape
|
105 |
+
# print 'U:\n', U
|
106 |
+
|
107 |
+
# We know that X * r = U
|
108 |
+
if np.linalg.matrix_rank(X) >= 2 * K:
|
109 |
+
r, _, _, _ = np.linalg.lstsq(X, U, rcond=-1)
|
110 |
+
# print(r, X, U, sep="\n")
|
111 |
+
r = np.squeeze(r)
|
112 |
+
else:
|
113 |
+
raise Exception("cp2tform:twoUniquePointsReq")
|
114 |
+
|
115 |
+
sc = r[0]
|
116 |
+
ss = r[1]
|
117 |
+
tx = r[2]
|
118 |
+
ty = r[3]
|
119 |
+
|
120 |
+
Tinv = np.array([[sc, -ss, 0], [ss, sc, 0], [tx, ty, 1]])
|
121 |
+
T = np.linalg.inv(Tinv)
|
122 |
+
T[:, 2] = np.array([0, 0, 1])
|
123 |
+
|
124 |
+
return T, Tinv
|
125 |
+
|
126 |
+
def __findSimilarity(self, uv, xy, options=None):
|
127 |
+
options = {"K": 2}
|
128 |
+
|
129 |
+
# uv = np.array(uv)
|
130 |
+
# xy = np.array(xy)
|
131 |
+
|
132 |
+
# Solve for trans1
|
133 |
+
trans1, trans1_inv = self.__findNonreflectiveSimilarity(uv, xy, options)
|
134 |
+
|
135 |
+
# manually reflect the xy data across the Y-axis
|
136 |
+
xyR = xy
|
137 |
+
xyR[:, 0] = -1 * xyR[:, 0]
|
138 |
+
# Solve for trans2
|
139 |
+
trans2r, trans2r_inv = self.__findNonreflectiveSimilarity(uv, xyR, options)
|
140 |
+
|
141 |
+
# manually reflect the tform to undo the reflection done on xyR
|
142 |
+
TreflectY = np.array([[-1, 0, 0], [0, 1, 0], [0, 0, 1]])
|
143 |
+
trans2 = np.dot(trans2r, TreflectY)
|
144 |
+
|
145 |
+
# Figure out if trans1 or trans2 is better
|
146 |
+
xy1 = self.__tformfwd(trans1, uv)
|
147 |
+
norm1 = np.linalg.norm(xy1 - xy)
|
148 |
+
xy2 = self.__tformfwd(trans2, uv)
|
149 |
+
norm2 = np.linalg.norm(xy2 - xy)
|
150 |
+
|
151 |
+
if norm1 <= norm2:
|
152 |
+
return trans1, trans1_inv
|
153 |
+
else:
|
154 |
+
trans2_inv = np.linalg.inv(trans2)
|
155 |
+
return trans2, trans2_inv
|
156 |
+
|
157 |
+
def __get_similarity_transform(self, src_pts, dst_pts):
|
158 |
+
if self.reflective:
|
159 |
+
trans, trans_inv = self.__findSimilarity(src_pts, dst_pts)
|
160 |
+
else:
|
161 |
+
trans, trans_inv = self.__findNonreflectiveSimilarity(src_pts, dst_pts)
|
162 |
+
return trans, trans_inv
|
163 |
+
|
164 |
+
def __cvt_tform_mat_for_cv2(self, trans):
|
165 |
+
cv2_trans = trans[:, 0:2].T
|
166 |
+
return cv2_trans
|
167 |
+
|
168 |
+
def get_similarity_transform_for_cv2(self, src_pts, dst_pts):
|
169 |
+
trans, trans_inv = self.__get_similarity_transform(src_pts, dst_pts)
|
170 |
+
cv2_trans = self.__cvt_tform_mat_for_cv2(trans)
|
171 |
+
return cv2_trans, trans
|
172 |
+
|
173 |
+
def get_align_image(self, image, lm5_points):
|
174 |
+
assert lm5_points is not None
|
175 |
+
tfm, trans = self.get_similarity_transform_for_cv2(lm5_points, self._std_points)
|
176 |
+
return cv.warpAffine(image, tfm, (112, 112))
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
opencv-python
|
2 |
+
gradio
|
3 |
+
numpy
|
4 |
+
huggingface_hub
|
yunet.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This file is part of OpenCV Zoo project.
|
2 |
+
# It is subject to the license terms in the LICENSE file found in the same directory.
|
3 |
+
#
|
4 |
+
# Copyright (C) 2021, Shenzhen Institute of Artificial Intelligence and Robotics for Society, all rights reserved.
|
5 |
+
# Third party copyrights are property of their respective owners.
|
6 |
+
|
7 |
+
from itertools import product
|
8 |
+
|
9 |
+
import numpy as np
|
10 |
+
import cv2 as cv
|
11 |
+
|
12 |
+
class YuNet:
|
13 |
+
def __init__(self, modelPath, inputSize=[320, 320], confThreshold=0.6, nmsThreshold=0.3, topK=5000, backendId=0, targetId=0):
|
14 |
+
self._modelPath = modelPath
|
15 |
+
self._inputSize = tuple(inputSize) # [w, h]
|
16 |
+
self._confThreshold = confThreshold
|
17 |
+
self._nmsThreshold = nmsThreshold
|
18 |
+
self._topK = topK
|
19 |
+
self._backendId = backendId
|
20 |
+
self._targetId = targetId
|
21 |
+
|
22 |
+
self._model = cv.FaceDetectorYN.create(
|
23 |
+
model=self._modelPath,
|
24 |
+
config="",
|
25 |
+
input_size=self._inputSize,
|
26 |
+
score_threshold=self._confThreshold,
|
27 |
+
nms_threshold=self._nmsThreshold,
|
28 |
+
top_k=self._topK,
|
29 |
+
backend_id=self._backendId,
|
30 |
+
target_id=self._targetId)
|
31 |
+
|
32 |
+
@property
|
33 |
+
def name(self):
|
34 |
+
return self.__class__.__name__
|
35 |
+
|
36 |
+
def setBackendAndTarget(self, backendId, targetId):
|
37 |
+
self._backendId = backendId
|
38 |
+
self._targetId = targetId
|
39 |
+
self._model = cv.FaceDetectorYN.create(
|
40 |
+
model=self._modelPath,
|
41 |
+
config="",
|
42 |
+
input_size=self._inputSize,
|
43 |
+
score_threshold=self._confThreshold,
|
44 |
+
nms_threshold=self._nmsThreshold,
|
45 |
+
top_k=self._topK,
|
46 |
+
backend_id=self._backendId,
|
47 |
+
target_id=self._targetId)
|
48 |
+
|
49 |
+
def setInputSize(self, input_size):
|
50 |
+
self._model.setInputSize(tuple(input_size))
|
51 |
+
|
52 |
+
def infer(self, image):
|
53 |
+
# Forward
|
54 |
+
faces = self._model.detect(image)
|
55 |
+
return np.empty(shape=(0, 5)) if faces[1] is None else faces[1]
|