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

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +77 -84
emotion.py CHANGED
@@ -15,10 +15,7 @@ DOWNSAMPLE_W = 720
15
  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
 
23
  # =========================
24
  # 狀態與映射
@@ -30,13 +27,11 @@ SCORE_CHOICES = [SCORE_LABELS[i] for i in range(1,6)]
30
 
31
  @dataclass
32
  class AppState:
33
- mode: str = "攝影機辨識"
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)
42
  stable_emo: str = "unknown"
@@ -44,29 +39,25 @@ class AppState:
44
  candidate_since: Optional[float] = None
45
  final_emo: Optional[str] = None
46
  final_conf: Optional[float] = None
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()
@@ -74,109 +65,111 @@ def _draw_ui_overlay(frame: np.ndarray, is_aligned: bool):
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])
179
- btn_stop.click(on_stop, [state], [cam, result, mood, hint, state, btn_restart])
180
 
181
  if __name__ == "__main__":
182
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
15
  EMA_ALPHA = 0.45
16
  CONF_TH = 40.0
17
  SWITCH_CONFIRM_SEC = 2.5
 
18
  ZOOM_FACTOR = 1.3
 
 
19
 
20
  # =========================
21
  # 狀態與映射
 
27
 
28
  @dataclass
29
  class AppState:
 
30
  running: bool = True
31
  finished: bool = False
32
  last_analyze_t: float = 0.0
33
+ last_check_t: float = 0.0
34
+ is_aligned: bool = False
 
35
  ema_scores: Optional[np.ndarray] = None
36
  emo_keys: List[str] = field(default_factory=list)
37
  stable_emo: str = "unknown"
 
39
  candidate_since: Optional[float] = None
40
  final_emo: Optional[str] = None
41
  final_conf: Optional[float] = None
 
42
 
43
  # =========================
44
+ # UI 介面
45
  # =========================
46
  def _hint_html(msg: str) -> str:
47
+ return f'<div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;text-align:center;">{msg}</div>'
48
 
49
  def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
50
  zh = EMO_ZH.get(emo_key, emo_key)
51
  score = EMO_TO_SCORE.get(emo_key, 3)
52
+ conf_txt = f"{conf:.1f}%" if conf else "--%"
53
+ return f"""
54
+ <div style="border-radius:16px;padding:30px;border:2px solid #4CAF50;background:#FFFFFF;color:#000000;text-align:center;box-shadow: 0 4px 10px rgba(0,0,0,0.1);">
55
+ <div style="font-size:18px;margin-bottom:10px;color:#666;">✨ 辨識完成 ✨</div>
56
+ <div style="font-size:56px;font-weight:800;color:#2E7D32;margin-bottom:15px;">{zh}</div>
57
+ <div style="font-size:20px;margin-bottom:10px;">心情分數:<strong>{score}</strong></div>
58
+ <div style="font-size:16px;color:#888;">系統信心值:{conf_txt}</div>
59
+ </div>
60
+ """
 
 
 
61
 
62
  def _draw_ui_overlay(frame: np.ndarray, is_aligned: bool):
63
  canvas = frame.copy()
 
65
  bw, bh = int(w * 0.45), int(h * 0.55)
66
  x1, y1 = (w - bw) // 2, (h - bh) // 2
67
  x2, y2 = x1 + bw, y1 + bh
 
68
  color = (0, 255, 0) if is_aligned else (255, 0, 0)
69
  text = "SCANNING..." if is_aligned else "PLACE FACE HERE"
 
 
70
  cv2.rectangle(canvas, (x1, y1), (x2, y2), color, 2)
71
  l = 40
72
+ cv2.line(canvas, (x1, y1), (x1+l, y1), color, 5); cv2.line(canvas, (x1, y1), (x1, y1+l), color, 5)
73
+ cv2.line(canvas, (x2, y2), (x2-l, y2), color, 5); cv2.line(canvas, (x2, y2), (x2, y2-l), color, 5)
 
 
74
  cv2.putText(canvas, text, (x1, y1 - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)
75
  return canvas
76
 
77
  # =========================
78
+ # 核心串流邏輯
79
  # =========================
80
  def on_stream(frame_rgb: np.ndarray, st: AppState):
81
+ if frame_rgb is None or st.finished or not st.running:
82
+ # 當 finished 為 True 時,這裡會被觸發,返回關閉攝影機的指令
83
+ return (gr.update(visible=False), gr.update(), gr.update(), gr.update(), st)
84
 
85
  now = time.time()
86
+ # 數位變焦
87
+ h, w = frame_rgb.shape[:2]
88
+ nw, nh = int(w / ZOOM_FACTOR), int(h / ZOOM_FACTOR)
89
+ sx, sy = (w - nw) // 2, (h - nh) // 2
90
+ frame_rgb = frame_rgb[sy:sy+nh, sx:sx+nw]
91
 
92
+ # 每 0.3 秒檢查一次臉部位置
93
  if now - st.last_check_t > 0.3:
94
  st.last_check_t = now
 
 
95
  try:
96
+ small = np.array(Image.fromarray(frame_rgb).resize((200, 200)))
97
+ faces = DeepFace.extract_faces(img_path=small, detector_backend="opencv", enforce_detection=False)
98
  if faces and faces[0]['face'] is not None:
99
  fa = faces[0]['facial_area']
100
+ fx, fy = fa['x'] * (frame_rgb.shape[1]/200), fa['y'] * (frame_rgb.shape[0]/200)
101
+ fw, fh = fa['w'] * (frame_rgb.shape[1]/200), fa['h'] * (frame_rgb.shape[0]/200)
102
+ gx1, gy1 = (frame_rgb.shape[1] - int(frame_rgb.shape[1]*0.45))//2, (frame_rgb.shape[0] - int(frame_rgb.shape[0]*0.55))//2
103
+ gx2, gy2 = gx1 + int(frame_rgb.shape[1]*0.45), gy1 + int(frame_rgb.shape[0]*0.55)
 
 
 
 
104
  st.is_aligned = (gx1 < fx + fw/2 < gx2) and (gy1 < fy + fh/2 < gy2)
105
  else:
106
  st.is_aligned = False
107
  except:
108
  st.is_aligned = False
109
 
 
110
  frame_with_ui = _draw_ui_overlay(frame_rgb, st.is_aligned)
111
 
112
+ # 分析邏輯
113
  if st.is_aligned and (now - st.last_analyze_t >= ANALYZE_EVERY_SEC):
114
  st.last_analyze_t = now
115
+ try:
116
+ res = DeepFace.analyze(img_path=frame_rgb, actions=["emotion"], enforce_detection=False, detector_backend="opencv")
117
+ emo_dict = res[0].get("emotion") if isinstance(res, list) else res.get("emotion")
118
+ if emo_dict:
119
+ keys = list(emo_dict.keys())
120
+ vec = np.array([float(emo_dict[k]) for k in keys])
121
+ if st.ema_scores is None: st.ema_scores, st.emo_keys = vec, keys
122
+ else: st.ema_scores = EMA_ALPHA * vec + (1.0 - EMA_ALPHA) * st.ema_scores
123
+
124
+ stable_key = st.emo_keys[np.argmax(st.ema_scores)]
125
+ st.stable_emo = stable_key
126
+
127
+ if st.candidate_emo != stable_key:
128
+ st.candidate_emo, st.candidate_since = stable_key, now
129
+ elif st.candidate_since and (now - st.candidate_since >= SWITCH_CONFIRM_SEC):
130
+ # 【關鍵點】辨識成功,設定 finished 並關閉攝像頭
131
+ st.final_emo, st.final_conf, st.running, st.finished = stable_key, float(emo_dict[stable_key]), False, True
132
+ return (
133
+ gr.update(visible=False), # 隱藏攝像頭
134
+ _result_card_html(stable_key, st.final_conf), # 跳出大卡片
135
+ SCORE_LABELS[EMO_TO_SCORE.get(stable_key, 3)], # 更新分數按鈕
136
+ _hint_html("辨識完成!"), # 更新提示文字
137
+ st
138
+ )
139
+ except:
140
+ pass
141
+
142
+ return (frame_with_ui, gr.update(value=_result_card_html(st.stable_emo, None)), gr.update(), gr.update(), st)
143
 
144
  # =========================
145
+ # 按鈕動作
146
  # =========================
147
  def on_restart(st: AppState):
148
+ st.running, st.finished, st.ema_scores, st.stable_emo = True, False, None, "unknown"
149
+ return (gr.update(visible=True), _hint_html("請將臉部對準方框"), SCORE_LABELS[3], st)
 
 
 
 
150
 
151
  # =========================
152
+ # UI 配置
153
  # =========================
154
  with gr.Blocks(title="Emotion Detector") as demo:
155
  state = gr.State(AppState())
156
+ gr.Markdown("## 情緒辨識系統")
157
+
158
+ hint = gr.HTML(_hint_html("請點擊下方錄製按鈕開始辨識"))
159
+
160
+ with gr.Column(elem_id="main-container"):
161
+ cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機預覽")
162
+ result_area = gr.HTML(_hint_html("辨識結果將在此顯示"))
163
+
164
  with gr.Row():
165
+ btn_restart = gr.Button("重新辨識 / 開啟攝影機", variant="primary")
166
  btn_stop = gr.Button("停止", variant="secondary")
167
+
168
  mood = gr.Radio(choices=SCORE_CHOICES, value=SCORE_LABELS[3], label="判定心情", interactive=False)
169
 
170
+ cam.stream(on_stream, [cam, state], [cam, result_area, mood, hint, state], show_progress="minimal")
171
+ btn_restart.click(on_restart, [state], [cam, hint, mood, state])
172
+ btn_stop.click(lambda st: (gr.update(visible=False), _hint_html("已停止"), st), [state], [cam, hint, state])
173
 
174
  if __name__ == "__main__":
175
  demo.launch(server_name="0.0.0.0", server_port=7860)