YuAnTsai commited on
Commit
41ef79e
·
verified ·
1 Parent(s): e902a87

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +85 -154
emotion.py CHANGED
@@ -1,5 +1,5 @@
1
  import time
2
- import cv2 # 確保有安裝 opencv-python
3
  from dataclasses import dataclass, field
4
  from typing import Optional, Dict, Tuple, List
5
  import gradio as gr
@@ -8,7 +8,7 @@ from PIL import Image
8
  from deepface import DeepFace
9
 
10
  # =========================
11
- # 參數(可微調)
12
  # =========================
13
  ANALYZE_EVERY_SEC = 0.8
14
  DOWNSAMPLE_W = 720
@@ -16,7 +16,7 @@ EMA_ALPHA = 0.45
16
  CONF_TH = 40.0
17
  SWITCH_CONFIRM_SEC = 2.5
18
  IOU_TH = 0.25
19
- ZOOM_FACTOR = 1.3 # 微調縮放,讓使用者較容易對準
20
  DETECTOR_BACKEND = "mtcnn"
21
  ALIGN_FACE = True
22
 
@@ -25,8 +25,8 @@ ALIGN_FACE = True
25
  # =========================
26
  EMO_ZH = {"angry": "生氣", "disgust": "厭惡", "fear": "害怕", "happy": "開心", "sad": "難過", "surprise": "驚訝", "neutral": "平靜", "unknown": "未知"}
27
  EMO_TO_SCORE = {"angry": 1, "disgust": 1, "fear": 1, "sad": 2, "neutral": 3, "surprise": 4, "happy": 5, "unknown": 3}
28
- SCORE_LABELS = {1: "1(心情差)", 2: "2(不太好)", 3: "3(普通)", 4: "4(不錯)", 5: "5(超棒)"}
29
- SCORE_CHOICES = [SCORE_LABELS[i] for i in [1, 2, 3, 4, 5]]
30
 
31
  @dataclass
32
  class AppState:
@@ -34,6 +34,8 @@ class AppState:
34
  running: bool = True
35
  finished: bool = False
36
  last_analyze_t: float = 0.0
 
 
37
  track_bbox: Optional[Tuple[int, int, int, int]] = None
38
  ema_scores: Optional[np.ndarray] = None
39
  emo_keys: List[str] = field(default_factory=list)
@@ -45,203 +47,132 @@ class AppState:
45
  final_score: int = 3
46
 
47
  # =========================
48
- # 輔助函式
49
  # =========================
50
  def _hint_html(msg: str) -> str:
51
  return f'<div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;">{msg}</div>'
52
 
53
- def _camera_hint_before_start() -> str:
54
- return _hint_html("請將臉部對準螢幕中央的方框。")
55
-
56
- def _camera_hint_done() -> str:
57
- return _hint_html("已完成辨識。")
58
-
59
  def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
60
  zh = EMO_ZH.get(emo_key, emo_key)
61
  score = EMO_TO_SCORE.get(emo_key, 3)
62
  conf_txt = "" if conf is None else f"{conf:.1f}%"
63
- score_desc = SCORE_LABELS[score].split("(")[1].rstrip(")")
64
- return f"""
65
- <div style="border-radius:16px;padding:20px;border:1px solid #E6E6E6;background:#FFFFFF;color:#000000;">
66
- <div style="font-size:14px;margin-bottom:8px;">辨識結果</div>
67
- <div style="font-size:42px;font-weight:800;line-height:1.1;margin-bottom:14px;">{zh}</div>
68
- <div style="font-size:16px;margin-bottom:6px;">心情分數:{score}({score_desc})</div>
69
- <div style="font-size:14px;">信心值:{conf_txt}</div>
70
- </div>
71
- """
72
-
73
- def _score_radio_value(score: int) -> str:
74
- return SCORE_LABELS[min(5, max(1, int(score)))]
75
-
76
- def _zoom_center(frame: np.ndarray, zoom: float) -> np.ndarray:
77
- if frame is None or zoom <= 1.0: return frame
78
- h, w = frame.shape[:2]
79
- new_w, new_h = int(w / zoom), int(h / zoom)
80
- start_x, start_y = (w - new_w) // 2, (h - new_h) // 2
81
- return frame[start_y : start_y + new_h, start_x : start_x + new_w]
82
 
83
- def _downsample_rgb(frame: np.ndarray, target_w: int) -> np.ndarray:
84
- if frame is None: return None
 
 
 
85
  h, w = frame.shape[:2]
86
- if w <= target_w: return frame
87
- scale = target_w / w
88
- new_h = max(1, int(h * scale))
89
- return np.array(Image.fromarray(frame).resize((target_w, new_h)))
90
 
91
- # 核心修改:繪製固定導引框
92
  def _draw_ui_overlay(frame: np.ndarray, is_aligned: bool):
93
  canvas = frame.copy()
94
  h, w = canvas.shape[:2]
95
-
96
- # 1. 定義「固定導引框」的位置與大小 (置中)
97
- bw, bh = int(w * 0.45), int(h * 0.55) # 框的大小
98
  x1, y1 = (w - bw) // 2, (h - bh) // 2
99
  x2, y2 = x1 + bw, y1 + bh
100
 
101
- # 2. 顏色:對準了變綠色,沒對準是紅色
102
- # (注意:OpenCV 預設是 BGR,但 Gradio 通常是 RGB,所以這裡設 (R, G, B))
103
  color = (0, 255, 0) if is_aligned else (255, 0, 0)
104
- status_text = "SCANNING..." if is_aligned else "PLACE FACE HERE"
105
-
106
- # 3. 繪製半透明遮罩 (讓框以外的地方變暗,更有提示感)
107
- overlay = canvas.copy()
108
- cv2.rectangle(overlay, (0, 0), (w, h), (0, 0, 0), -1)
109
- alpha = 0.3 # 變暗程度
110
- cv2.addWeighted(overlay, alpha, canvas, 1 - alpha, 0, canvas)
111
-
112
- # 4. 把框框的部分「挖亮」(恢復原樣)
113
- canvas[y1:y2, x1:x2] = frame[y1:y2, x1:x2]
114
-
115
- # 5. 繪製導引矩形
116
- cv2.rectangle(canvas, (x1, y1), (x2, y2), color, 3)
117
-
118
- # 6. 繪製角點強化
119
- l = 30 # 角線長度
120
- cv2.line(canvas, (x1, y1), (x1+l, y1), color, 6)
121
- cv2.line(canvas, (x1, y1), (x1, y1+l), color, 6)
122
- cv2.line(canvas, (x2, y2), (x2-l, y2), color, 6)
123
- cv2.line(canvas, (x2, y2), (x2, y2-l), color, 6)
124
-
125
- # 7. 加上提示文字
126
- cv2.putText(canvas, status_text, (x1, y1 - 15), cv2.FONT_HERSHEY_DUPLEX, 0.9, color, 2)
127
-
128
  return canvas
129
 
130
- def _is_face_in_guide(face_bbox, frame_shape):
131
- """ 檢查人臉是否在中央引導框內 """
132
- if face_bbox is None: return False
133
- fx, fy, fw, fh = face_bbox
134
- h, w = frame_shape[:2]
135
-
136
- # 引導框範圍 (需與 _draw_ui_overlay 一致)
137
- bw, bh = int(w * 0.45), int(h * 0.55)
138
- gx1, gy1 = (w - bw) // 2, (h - bh) // 2
139
- gx2, gy2 = gx1 + bw, gy1 + bh
140
-
141
- # 計算人臉中心點
142
- fcx, fcy = fx + fw // 2, fy + fh // 2
143
-
144
- # 判定標準:臉部中心點在引導框內
145
- return (gx1 < fcx < gx2) and (gy1 < fcy < gy2)
146
-
147
- def _pick_face_by_tracking(faces, prev_bbox):
148
- if not faces: return None, None
149
- cand = []
150
- for f in faces:
151
- fa = f.get("facial_area", {})
152
- x, y, w, h = fa.get("x",0), fa.get("y",0), fa.get("w",0), fa.get("h",0)
153
- if w > 0 and h > 0: cand.append((f, (x, y, w, h), w * h))
154
- if not cand: return None, None
155
- return sorted(cand, key=lambda x: x[2], reverse=True)[0][:2]
156
-
157
  # =========================
158
- # Camera streaming
159
  # =========================
160
  def on_stream(frame_rgb: np.ndarray, st: AppState):
161
  if frame_rgb is None or st.finished or (not st.running):
162
  return (None, gr.update(), gr.update(), gr.update(), st, gr.update())
163
 
164
- # 1. 數位變焦
165
- frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
166
  now = time.time()
167
-
168
- # 2. 快速人臉檢測 (判斷位置)
169
- small_check = _downsample_rgb(frame_rgb, 400)
170
- faces_check = DeepFace.extract_faces(img_path=small_check, detector_backend="opencv", enforce_detection=False)
171
- pick_check, bbox_check = _pick_face_by_tracking(faces_check, st.track_bbox)
172
-
173
- # 3. 檢查人臉是否對準中央框
174
- is_aligned = _is_face_in_guide(bbox_check, frame_rgb.shape)
175
-
176
- # 4. 繪製導引畫面 (紅/綠框與遮罩)
177
- frame_with_ui = _draw_ui_overlay(frame_rgb, is_aligned)
178
 
179
- # 5. 分析邏輯 (只有對準時才進行深度分析)
180
- if is_aligned and (now - st.last_analyze_t >= ANALYZE_EVERY_SEC):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  st.last_analyze_t = now
182
- small = _downsample_rgb(frame_rgb, DOWNSAMPLE_W)
183
- faces = DeepFace.extract_faces(img_path=small, detector_backend=DETECTOR_BACKEND, enforce_detection=False, align=ALIGN_FACE)
184
- pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
185
 
186
- if pick:
187
- st.track_bbox = bbox
188
- face = pick["face"]
189
- if face.dtype != np.uint8: face = (face * 255).astype(np.uint8)
 
 
190
 
191
- res = DeepFace.analyze(img_path=face, actions=["emotion"], enforce_detection=False)
192
- emo_dict = res[0].get("emotion") if isinstance(res, list) else res.get("emotion")
193
 
194
- if emo_dict:
195
- top_key = max(emo_dict, key=lambda k: float(emo_dict[k]))
196
- keys = list(emo_dict.keys())
197
- vec = np.array([float(emo_dict[k]) for k in keys])
198
- if st.ema_scores is None: st.ema_scores, st.emo_keys = vec, keys
199
- else: st.ema_scores = EMA_ALPHA * vec + (1.0 - EMA_ALPHA) * st.ema_scores
200
-
201
- stable_key = st.emo_keys[np.argmax(st.ema_scores)]
202
- st.stable_emo = stable_key
203
-
204
- if st.candidate_emo != stable_key:
205
- st.candidate_emo, st.candidate_since = stable_key, now
206
- elif st.candidate_since and (now - st.candidate_since >= SWITCH_CONFIRM_SEC):
207
- st.final_emo, st.final_conf, st.running, st.finished = stable_key, float(emo_dict[top_key]), False, True
208
- return (frame_with_ui, _result_card_html(stable_key, st.final_conf), _score_radio_value(EMO_TO_SCORE.get(stable_key, 3)), _camera_hint_done(), st, gr.update(value="重新攝影機辨識"))
209
-
210
- # 顯示穩定情緒
211
- show_emo = st.stable_emo or "unknown"
212
- return (
213
- frame_with_ui,
214
- gr.update(value=_result_card_html(show_emo, st.final_conf)),
215
- gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show_emo, 3))),
216
- gr.update(value=_camera_hint_before_start()),
217
- st,
218
- gr.update()
219
- )
220
 
 
 
 
221
  def on_restart(st: AppState):
222
- st.running, st.finished, st.last_analyze_t = True, False, 0.0
223
- st.track_bbox, st.ema_scores, st.stable_emo, st.final_emo = None, None, "unknown", None
224
- return (gr.update(visible=True), _hint_html("等待影像..."), _score_radio_value(3), _camera_hint_before_start(), st, "重新攝影機辨識")
225
 
226
  def on_stop(st: AppState):
227
  st.running, st.finished = False, True
228
- return (gr.update(visible=False), _hint_html("已停止"), _score_radio_value(3), _camera_hint_stopped(), st, "重新攝影機辨識")
229
 
230
  # =========================
231
  # UI
232
  # =========================
233
  with gr.Blocks(title="Emotion Detector") as demo:
234
  state = gr.State(AppState())
235
- gr.Markdown("## 情緒辨識系統")
236
- hint = gr.HTML(_camera_hint_before_start())
237
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機預覽")
238
-
239
  with gr.Row():
240
  btn_restart = gr.Button("重新攝影機辨識", variant="primary")
241
  btn_stop = gr.Button("停止", variant="secondary")
242
-
243
  result = gr.HTML(_hint_html("等待串流..."))
244
- mood = gr.Radio(choices=SCORE_CHOICES, value=SCORE_LABELS[3], label="系統判定心情", interactive=False)
245
 
246
  cam.stream(on_stream, [cam, state], [cam, result, mood, hint, state, btn_restart], show_progress="minimal")
247
  btn_restart.click(on_restart, [state], [cam, result, mood, hint, state, btn_restart])
 
1
  import time
2
+ import cv2
3
  from dataclasses import dataclass, field
4
  from typing import Optional, Dict, Tuple, List
5
  import gradio as gr
 
8
  from deepface import DeepFace
9
 
10
  # =========================
11
+ # 參數(效能優化版)
12
  # =========================
13
  ANALYZE_EVERY_SEC = 0.8
14
  DOWNSAMPLE_W = 720
 
16
  CONF_TH = 40.0
17
  SWITCH_CONFIRM_SEC = 2.5
18
  IOU_TH = 0.25
19
+ ZOOM_FACTOR = 1.3
20
  DETECTOR_BACKEND = "mtcnn"
21
  ALIGN_FACE = True
22
 
 
25
  # =========================
26
  EMO_ZH = {"angry": "生氣", "disgust": "厭惡", "fear": "害怕", "happy": "開心", "sad": "難過", "surprise": "驚訝", "neutral": "平靜", "unknown": "未知"}
27
  EMO_TO_SCORE = {"angry": 1, "disgust": 1, "fear": 1, "sad": 2, "neutral": 3, "surprise": 4, "happy": 5, "unknown": 3}
28
+ SCORE_LABELS = {i: f"{i}({desc})" for i, desc in zip(range(1,6), ["心情差","不太好","普通","不錯","超棒"])}
29
+ SCORE_CHOICES = [SCORE_LABELS[i] for i in range(1,6)]
30
 
31
  @dataclass
32
  class AppState:
 
34
  running: bool = True
35
  finished: bool = False
36
  last_analyze_t: float = 0.0
37
+ last_check_t: float = 0.0 # 新增:控制檢查位置的頻率
38
+ is_aligned: bool = False # 新增:快取對準狀態
39
  track_bbox: Optional[Tuple[int, int, int, int]] = None
40
  ema_scores: Optional[np.ndarray] = None
41
  emo_keys: List[str] = field(default_factory=list)
 
47
  final_score: int = 3
48
 
49
  # =========================
50
+ # 介面 HTML
51
  # =========================
52
  def _hint_html(msg: str) -> str:
53
  return f'<div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;">{msg}</div>'
54
 
 
 
 
 
 
 
55
  def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
56
  zh = EMO_ZH.get(emo_key, emo_key)
57
  score = EMO_TO_SCORE.get(emo_key, 3)
58
  conf_txt = "" if conf is None else f"{conf:.1f}%"
59
+ return f'<div style="border-radius:16px;padding:20px;border:1px solid #E6E6E6;background:#FFFFFF;color:#000000;"><div style="font-size:14px;margin-bottom:8px;">辨識結果</div><div style="font-size:42px;font-weight:800;line-height:1.1;margin-bottom:14px;">{zh}</div><div style="font-size:16px;margin-bottom:6px;">心情分數:{score}</div><div style="font-size:14px;">信心值:{conf_txt}</div></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ # =========================
62
+ # 影像處理
63
+ # =========================
64
+ def _zoom_center(frame, zoom):
65
+ if frame is None or zoom <= 1.0: return frame
66
  h, w = frame.shape[:2]
67
+ nw, nh = int(w / zoom), int(h / zoom)
68
+ sx, sy = (w - nw) // 2, (h - nh) // 2
69
+ return frame[sy:sy+nh, sx:sx+nw]
 
70
 
 
71
  def _draw_ui_overlay(frame: np.ndarray, is_aligned: bool):
72
  canvas = frame.copy()
73
  h, w = canvas.shape[:2]
74
+ bw, bh = int(w * 0.45), int(h * 0.55)
 
 
75
  x1, y1 = (w - bw) // 2, (h - bh) // 2
76
  x2, y2 = x1 + bw, y1 + bh
77
 
 
 
78
  color = (0, 255, 0) if is_aligned else (255, 0, 0)
79
+ text = "SCANNING..." if is_aligned else "PLACE FACE HERE"
80
+
81
+ # 效能優化:不再使用 addWeighted 製作全畫面遮罩,改用簡單線條
82
+ cv2.rectangle(canvas, (x1, y1), (x2, y2), color, 2)
83
+ l = 40
84
+ cv2.line(canvas, (x1, y1), (x1+l, y1), color, 5)
85
+ cv2.line(canvas, (x1, y1), (x1, y1+l), color, 5)
86
+ cv2.line(canvas, (x2, y2), (x2-l, y2), color, 5)
87
+ cv2.line(canvas, (x2, y2), (x2, y2-l), color, 5)
88
+ cv2.putText(canvas, text, (x1, y1 - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return canvas
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  # =========================
92
+ # 串流核心
93
  # =========================
94
  def on_stream(frame_rgb: np.ndarray, st: AppState):
95
  if frame_rgb is None or st.finished or (not st.running):
96
  return (None, gr.update(), gr.update(), gr.update(), st, gr.update())
97
 
 
 
98
  now = time.time()
99
+ frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
 
 
 
 
 
 
 
 
 
 
100
 
101
+ # --- 效能優化點:降低檢查頻率 (每 0.3 秒才檢查一次人臉位置) ---
102
+ if now - st.last_check_t > 0.3:
103
+ st.last_check_t = now
104
+ # 極小化影像以利快速檢查
105
+ small_check = np.array(Image.fromarray(frame_rgb).resize((250, 250)))
106
+ try:
107
+ faces = DeepFace.extract_faces(img_path=small_check, detector_backend="opencv", enforce_detection=False)
108
+ if faces and faces[0]['face'] is not None:
109
+ fa = faces[0]['facial_area']
110
+ # 將座標還原到原圖比例
111
+ fx, fy = fa['x'] * (frame_rgb.shape[1]/250), fa['y'] * (frame_rgb.shape[0]/250)
112
+ fw, fh = fa['w'] * (frame_rgb.shape[1]/250), fa['h'] * (frame_rgb.shape[0]/250)
113
+
114
+ # 檢查中心點是否在導引框內 (中央 45%x55%)
115
+ h, w = frame_rgb.shape[:2]
116
+ gx1, gy1 = (w - int(w*0.45))//2, (h - int(h*0.55))//2
117
+ gx2, gy2 = gx1 + int(w*0.45), gy1 + int(h*0.55)
118
+ st.is_aligned = (gx1 < fx + fw/2 < gx2) and (gy1 < fy + fh/2 < gy2)
119
+ else:
120
+ st.is_aligned = False
121
+ except:
122
+ st.is_aligned = False
123
+
124
+ # 繪製畫面 (這個步驟很快)
125
+ frame_with_ui = _draw_ui_overlay(frame_rgb, st.is_aligned)
126
+
127
+ # --- 深度分析邏輯 (只有對準時執行) ---
128
+ if st.is_aligned and (now - st.last_analyze_t >= ANALYZE_EVERY_SEC):
129
  st.last_analyze_t = now
130
+ # 進行高品質偵測與分析
131
+ res = DeepFace.analyze(img_path=frame_rgb, actions=["emotion"], enforce_detection=False, detector_backend="opencv")
132
+ emo_dict = res[0].get("emotion") if isinstance(res, list) else res.get("emotion")
133
 
134
+ if emo_dict:
135
+ top_key = max(emo_dict, key=lambda k: float(emo_dict[k]))
136
+ keys = list(emo_dict.keys())
137
+ vec = np.array([float(emo_dict[k]) for k in keys])
138
+ if st.ema_scores is None: st.ema_scores, st.emo_keys = vec, keys
139
+ else: st.ema_scores = EMA_ALPHA * vec + (1.0 - EMA_ALPHA) * st.ema_scores
140
 
141
+ stable_key = st.emo_keys[np.argmax(st.ema_scores)]
142
+ st.stable_emo = stable_key
143
 
144
+ if st.candidate_emo != stable_key:
145
+ st.candidate_emo, st.candidate_since = stable_key, now
146
+ elif st.candidate_since and (now - st.candidate_since >= SWITCH_CONFIRM_SEC):
147
+ st.final_emo, st.final_conf, st.running, st.finished = stable_key, float(emo_dict[top_key]), False, True
148
+ return (frame_with_ui, _result_card_html(stable_key, st.final_conf), SCORE_LABELS[EMO_TO_SCORE.get(stable_key, 3)], "辨識完成", st, gr.update(value="重新攝影機辨識"))
149
+
150
+ return (frame_with_ui, gr.update(value=_result_card_html(st.stable_emo, st.final_conf)), gr.update(), gr.update(), st, gr.update())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ # =========================
153
+ # 其他 Button 函式 (簡化版以維持穩定)
154
+ # =========================
155
  def on_restart(st: AppState):
156
+ st.running, st.finished, st.ema_scores, st.stable_emo, st.final_emo = True, False, None, "unknown", None
157
+ return (gr.update(visible=True), _hint_html("重新開始..."), SCORE_LABELS[3], "請對準方框", st, "重新攝影機辨識")
 
158
 
159
  def on_stop(st: AppState):
160
  st.running, st.finished = False, True
161
+ return (gr.update(visible=False), _hint_html("已停止"), SCORE_LABELS[3], "停止", st, "重新攝影機辨識")
162
 
163
  # =========================
164
  # UI
165
  # =========================
166
  with gr.Blocks(title="Emotion Detector") as demo:
167
  state = gr.State(AppState())
168
+ gr.Markdown("## 情緒辨識系統 (效能優化版)")
169
+ hint = gr.HTML("請將臉部對準螢幕中央方框")
170
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機預覽")
 
171
  with gr.Row():
172
  btn_restart = gr.Button("重新攝影機辨識", variant="primary")
173
  btn_stop = gr.Button("停止", variant="secondary")
 
174
  result = gr.HTML(_hint_html("等待串流..."))
175
+ mood = gr.Radio(choices=SCORE_CHOICES, value=SCORE_LABELS[3], label="判定心情", interactive=False)
176
 
177
  cam.stream(on_stream, [cam, state], [cam, result, mood, hint, state, btn_restart], show_progress="minimal")
178
  btn_restart.click(on_restart, [state], [cam, result, mood, hint, state, btn_restart])