Abhishek Gola commited on
Commit
a58bc70
·
1 Parent(s): 4b9f394

Added facial expression recognition to opencv spaces

Browse files
Files changed (5) hide show
  1. README.md +7 -0
  2. app.py +64 -0
  3. facial_fer_model.py +176 -0
  4. requirements.txt +4 -0
  5. 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]