YuAnTsai commited on
Commit
9709828
·
verified ·
1 Parent(s): 31cf7c5

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +150 -100
emotion.py CHANGED
@@ -9,23 +9,22 @@ from PIL import Image, ImageDraw
9
  from deepface import DeepFace
10
 
11
  # =========================
12
- # 參數優化(平衡效能與準度)
13
  # =========================
14
- ANALYZE_EVERY_SEC = 1.0 # 辨識頻率:1秒一次
15
- DOWNSAMPLE_W = 320 # 再次降解析度,大幅提升速度
16
- EMA_ALPHA = 0.45
17
- CONF_TH = 35.0 # 稍微調低門檻,增加反應靈敏度
18
- SWITCH_CONFIRM_SEC = 2.0 # 縮短確認時間,體感變快
19
- ZOOM_FACTOR = 1.2
20
-
21
- DETECTOR_BACKEND = "opencv" # 最速模式
22
- ALIGN_FACE = False # 關閉對齊以節省運算
23
-
24
- # 全域狀態:防止運算堆積
25
  _is_processing = False
26
 
27
  # =========================
28
- # 狀態與工具
29
  # =========================
30
  @dataclass
31
  class AppState:
@@ -43,6 +42,38 @@ class AppState:
43
  last_face_t: float = 0.0
44
  face_in_box: bool = False
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def _downsample_rgb(frame: np.ndarray, target_w: int) -> np.ndarray:
47
  if frame is None: return None
48
  h, w = frame.shape[:2]
@@ -59,121 +90,140 @@ def _zoom_center(frame: np.ndarray, zoom: float) -> np.ndarray:
59
  return frame[sy:sy + nh, sx:sx + nw]
60
 
61
  # =========================
62
- # UI 組件 (情緒映射)
63
- # =========================
64
- EMO_ZH = {"angry": "生氣", "disgust": "厭惡", "fear": "害怕", "happy": "開心", "sad": "難過", "surprise": "驚訝", "neutral": "平靜", "unknown": "未知"}
65
- EMO_TO_SCORE = {"angry": 1, "disgust": 1, "fear": 1, "sad": 2, "neutral": 3, "surprise": 4, "happy": 5, "unknown": 3}
66
- SCORE_LABELS = {1: "1(心情差)", 2: "2(不太好)", 3: "3(普通)", 4: "4(不錯)", 5: "5(超棒)"}
67
-
68
- def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
69
- zh = EMO_ZH.get(emo_key, "分析中...")
70
- score = EMO_TO_SCORE.get(emo_key, 3)
71
- conf_txt = f"{conf:.1f}%" if conf else "--"
72
- return f"""
73
- <div style="border-radius:12px;padding:15px;border:1px solid #EEE;background:#FFF;color:#333;">
74
- <div style="font-size:32px;font-weight:bold;">{zh}</div>
75
- <div style="font-size:16px;">心情分數:{score} | 信心值:{conf_txt}</div>
76
- </div>
77
- """
78
-
79
- # =========================
80
- # 核心 Stream 邏輯
81
  # =========================
82
  def on_stream(frame_rgb: np.ndarray, st: AppState):
83
  global _is_processing
84
  now = time.time()
85
 
86
- if not st.running or st.finished or frame_rgb is None:
87
- return (gr.update(), gr.update(), gr.update(), gr.update(), st, gr.update())
 
88
 
89
- # 1. 基礎處理 (輕量)
90
- frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
91
-
92
- # 2. 判斷是否要跳過重度運算 (防止阻塞關鍵)
93
- # 如果正在處理中,或距離上次處理不到時間,只回傳畫框影像
94
  if _is_processing or (now - st.last_analyze_t < ANALYZE_EVERY_SEC):
95
- # 即使不運算,也回傳當前畫面讓 UI 不會卡死
96
- is_detected = (now - st.last_face_t) < 1.0
97
- # 此處省略畫框 draw 邏輯以極大化流暢度,或可沿用你的 _draw_guide_overlay
98
- return (frame_rgb, gr.update(), gr.update(), gr.update(), st, gr.update())
99
 
100
- # 3. 開始重度運算
101
  _is_processing = True
102
  try:
103
  st.last_analyze_t = now
104
- small = _downsample_rgb(frame_rgb, DOWNSAMPLE_W)
105
 
106
- # 快速臉部偵測
107
  results = DeepFace.analyze(
108
- img_path=small,
109
  actions=['emotion'],
110
  detector_backend=DETECTOR_BACKEND,
111
  enforce_detection=False,
112
  align=ALIGN_FACE
113
  )
114
 
115
- if results and results[0].get('face_confidence', 0) > 0.4:
116
  res = results[0]
117
- st.last_face_t = now
118
- emo_dict = res['emotion']
119
- top_key = max(emo_dict, key=emo_dict.get)
120
- top_conf = emo_dict[top_key]
121
-
122
- # EMA 平滑處理
123
- keys = list(emo_dict.keys())
124
- vec = np.array([emo_dict[k] for k in keys])
125
- if st.ema_scores is None:
126
- st.ema_scores = vec
127
- st.emo_keys = keys
128
- else:
129
- st.ema_scores = EMA_ALPHA * vec + (1.0 - EMA_ALPHA) * st.ema_scores
130
-
131
- stable_key = st.emo_keys[np.argmax(st.ema_scores)]
132
- st.stable_emo = stable_key
133
-
134
- # 確認邏輯
135
- if st.candidate_emo != stable_key:
136
- st.candidate_emo = stable_key
137
- st.candidate_since = now
138
- elif st.candidate_since and (now - st.candidate_since) >= SWITCH_CONFIRM_SEC:
139
- st.final_emo = stable_key
140
- st.final_conf = top_conf
141
- st.finished = True
142
- st.running = False
143
-
144
- return (
145
- frame_rgb,
146
- _result_card_html(stable_key, top_conf),
147
- SCORE_LABELS[EMO_TO_SCORE.get(stable_key, 3)],
148
- "偵測中...",
149
- st,
150
- gr.update()
151
- )
152
-
153
  except Exception as e:
154
- print(f"Analysis Error: {e}")
155
  finally:
156
- _is_processing = False # 釋放鎖定,允許下一波分析
157
 
158
- return (frame_rgb, gr.update(), gr.update(), gr.update(), st, gr.update())
159
 
160
  # =========================
161
- # Gradio 介面省略 (沿用原架構但簡化)
162
  # =========================
163
- with gr.Blocks(title="Fast Emotion Detector") as demo:
164
- st = gr.State(AppState())
165
- cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="Live")
166
- result_html = gr.HTML(_result_card_html("unknown", 0))
167
- mood_radio = gr.Radio(choices=list(SCORE_LABELS.values()), label="心情分數")
168
 
169
- # 串流設定
170
- cam.stream(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  fn=on_stream,
172
- inputs=[cam, st],
173
- outputs=[cam, result_html, mood_radio, gr.HTML(), st, gr.Button()],
174
- show_progress="hidden", # 隱藏進度條可以減少 UI 負擔
175
- queue=False # 關鍵:不進入隊列,直接拋棄過期請求
176
  )
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  if __name__ == "__main__":
179
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
9
  from deepface import DeepFace
10
 
11
  # =========================
12
+ # 1. 核心參數優化
13
  # =========================
14
+ ANALYZE_EVERY_SEC = 1.0 # 辨識頻率:1秒1次 (降低 CPU 負荷)
15
+ DOWNSAMPLE_W = 320 # 解析度降至 320px (大幅提升速度)
16
+ EMA_ALPHA = 0.40 # 平滑係數
17
+ CONF_TH = 35.0 # 信心值門檻
18
+ SWITCH_CONFIRM_SEC = 2.0 # 情緒穩定多久才算最終結果
19
+ ZOOM_FACTOR = 1.25 # 稍微放大臉部區域
20
+ DETECTOR_BACKEND = "opencv" # 最速辨識器
21
+ ALIGN_FACE = False # 關閉對齊以節省資源
22
+
23
+ # 全域狀態:防止 DeepFace 重疊執行導致阻塞
 
24
  _is_processing = False
25
 
26
  # =========================
27
+ # 2. 狀態管理
28
  # =========================
29
  @dataclass
30
  class AppState:
 
42
  last_face_t: float = 0.0
43
  face_in_box: bool = False
44
 
45
+ # =========================
46
+ # 3. UI 輔助函數 (修正 AttributeError)
47
+ # =========================
48
+ def _hint_html(msg: str) -> str:
49
+ return f"""
50
+ <div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;">
51
+ {msg}
52
+ </div>
53
+ """
54
+
55
+ def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
56
+ EMO_ZH = {"angry": "生氣", "disgust": "厭惡", "fear": "害怕", "happy": "開心", "sad": "難過", "surprise": "驚訝", "neutral": "平靜", "unknown": "未知"}
57
+ zh = EMO_ZH.get(emo_key, "分析中...")
58
+ score_map = {"angry": 1, "disgust": 1, "fear": 1, "sad": 2, "neutral": 3, "surprise": 4, "happy": 5, "unknown": 3}
59
+ score = score_map.get(emo_key, 3)
60
+ conf_txt = f"{conf:.1f}%" if conf else "--"
61
+ return f"""
62
+ <div style="border-radius:16px;padding:20px;border:1px solid #E6E6E6;background:#FFFFFF;color:#000000;">
63
+ <div style="font-size:14px;color:#666;margin-bottom:8px;">即時辨識結果</div>
64
+ <div style="font-size:42px;font-weight:800;line-height:1.1;margin-bottom:14px;">{zh}</div>
65
+ <div style="font-size:16px;margin-bottom:6px;">心情分數:{score}</div>
66
+ <div style="font-size:14px;color:#999;">信心值:{conf_txt}</div>
67
+ </div>
68
+ """
69
+
70
+ def _score_to_label(score_int: int) -> str:
71
+ labels = {1: "1(心情差)", 2: "2(不太好)", 3: "3(普通)", 4: "4(不錯)", 5: "5(超棒)"}
72
+ return labels.get(score_int, "3(普通)")
73
+
74
+ # =========================
75
+ # 4. 影像處理工具
76
+ # =========================
77
  def _downsample_rgb(frame: np.ndarray, target_w: int) -> np.ndarray:
78
  if frame is None: return None
79
  h, w = frame.shape[:2]
 
90
  return frame[sy:sy + nh, sx:sx + nw]
91
 
92
  # =========================
93
+ # 5. 核心串流邏輯 (防止阻塞)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  # =========================
95
  def on_stream(frame_rgb: np.ndarray, st: AppState):
96
  global _is_processing
97
  now = time.time()
98
 
99
+ # 如果已經完成或沒影像,直接結束
100
+ if st.finished or not st.running or frame_rgb is None:
101
+ return (gr.update(), gr.update(), gr.update(), gr.update(), st)
102
 
103
+ # A. 基礎���像處理 (輕量級,每影格都做)
104
+ processed_frame = _zoom_center(frame_rgb, ZOOM_FACTOR)
105
+
106
+ # B. 判斷是否要進行 DeepFace 分析 (防止堆積)
 
107
  if _is_processing or (now - st.last_analyze_t < ANALYZE_EVERY_SEC):
108
+ # 忙碌中或時間未到,只回傳影像,確保 UI 不會卡死
109
+ return (processed_frame, gr.update(), gr.update(), gr.update(), st)
 
 
110
 
111
+ # C. 開始分析 (加鎖)
112
  _is_processing = True
113
  try:
114
  st.last_analyze_t = now
115
+ small_img = _downsample_rgb(processed_frame, DOWNSAMPLE_W)
116
 
117
+ # 使用 DeepFace 分析 (enforce_detection=False 避免沒臉時噴 Error)
118
  results = DeepFace.analyze(
119
+ img_path=small_img,
120
  actions=['emotion'],
121
  detector_backend=DETECTOR_BACKEND,
122
  enforce_detection=False,
123
  align=ALIGN_FACE
124
  )
125
 
126
+ if results and len(results) > 0:
127
  res = results[0]
128
+ # 只有在真的偵測到臉的情況下才更新 (OpenCV 信心值較低)
129
+ if res.get('face_confidence', 0) > 0.3:
130
+ st.last_face_t = now
131
+ emo_dict = res['emotion']
132
+ top_key = max(emo_dict, key=emo_dict.get)
133
+
134
+ # EMA 平滑
135
+ keys = list(emo_dict.keys())
136
+ vec = np.array([float(emo_dict[k]) for k in keys])
137
+ if st.ema_scores is None:
138
+ st.ema_scores, st.emo_keys = vec, keys
139
+ else:
140
+ st.ema_scores = EMA_ALPHA * vec + (1.0 - EMA_ALPHA) * st.ema_scores
141
+
142
+ stable_key = st.emo_keys[np.argmax(st.ema_scores)]
143
+ st.stable_emo = stable_key
144
+
145
+ # 確認邏輯
146
+ if st.candidate_emo != stable_key:
147
+ st.candidate_emo, st.candidate_since = stable_key, now
148
+ elif st.candidate_since and (now - st.candidate_since) >= SWITCH_CONFIRM_SEC:
149
+ st.final_emo, st.final_conf = stable_key, float(emo_dict[stable_key])
150
+ st.finished = True
151
+
152
+ score_val = {"angry": 1, "disgust": 1, "fear": 1, "sad": 2, "neutral": 3, "surprise": 4, "happy": 5, "unknown": 3}.get(stable_key, 3)
153
+
154
+ return (
155
+ processed_frame,
156
+ _result_card_html(stable_key, float(emo_dict[top_key])),
157
+ _score_to_label(score_val),
158
+ _hint_html("正在辨識情緒,請保持表情穩定..."),
159
+ st
160
+ )
161
+
 
 
162
  except Exception as e:
163
+ print(f"DeepFace 分析異常: {e}")
164
  finally:
165
+ _is_processing = False
166
 
167
+ return (processed_frame, gr.update(), gr.update(), gr.update(), st)
168
 
169
  # =========================
170
+ # 6. Gradio 介面佈局
171
  # =========================
172
+ with gr.Blocks(title="Emotion AI Detector", css=".gradio-container {background-color: #f9f9f9}") as demo:
173
+ st_state = gr.State(AppState())
174
+
175
+ gr.Markdown("# 🎭 AI 情緒即時偵測系統")
 
176
 
177
+ with gr.Row():
178
+ with gr.Column(scale=2):
179
+ # 串流組件
180
+ input_cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="即時視訊")
181
+
182
+ with gr.Row():
183
+ btn_reset = gr.Button("🔄 重新辨識", variant="secondary")
184
+ btn_stop = gr.Button("⏹ 停止", variant="stop")
185
+
186
+ with gr.Column(scale=1):
187
+ # 結果顯示組件
188
+ hint_box = gr.HTML(_hint_html("請開啟攝影機,並按「Record」開始辨識。"))
189
+ result_box = gr.HTML(_result_card_html("unknown", None))
190
+ mood_radio = gr.Radio(
191
+ choices=["1(心情差)", "2(不太好)", "3(普通)", "4(不錯)", "5(超棒)"],
192
+ label="系統判定心情等級",
193
+ interactive=False,
194
+ value="3(普通)"
195
+ )
196
+
197
+ # --- 事件綁定 ---
198
+
199
+ # 串流主邏輯: queue=False 與 show_progress="hidden" 是流暢關鍵
200
+ input_cam.stream(
201
  fn=on_stream,
202
+ inputs=[input_cam, st_state],
203
+ outputs=[input_cam, result_box, mood_radio, hint_box, st_state],
204
+ show_progress="hidden",
205
+ queue=False
206
  )
207
 
208
+ # 重新辨識按鈕
209
+ def restart_app():
210
+ return (None, _result_card_html("unknown", None), "3(普通)", _hint_html("系統已重置,請重新開始。"), AppState())
211
+
212
+ btn_reset.click(
213
+ fn=restart_app,
214
+ outputs=[input_cam, result_box, mood_radio, hint_box, st_state]
215
+ )
216
+
217
+ # 停止按鈕
218
+ def stop_app(curr_st: AppState):
219
+ curr_st.running = False
220
+ return _hint_html("偵測已停止。"), curr_st
221
+
222
+ btn_stop.click(fn=stop_app, inputs=[st_state], outputs=[hint_box, st_state])
223
+
224
+ # =========================
225
+ # 7. 啟動伺服器
226
+ # =========================
227
  if __name__ == "__main__":
228
+ # 使用 share=False 減少網路延遲造成的卡頓
229
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)