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

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +64 -63
emotion.py CHANGED
@@ -16,12 +16,12 @@ EMA_ALPHA = 0.45
16
  CONF_TH = 40.0
17
  SWITCH_CONFIRM_SEC = 2.5
18
  IOU_TH = 0.25
19
- ZOOM_FACTOR = 1.5
20
  DETECTOR_BACKEND = "mtcnn"
21
  ALIGN_FACE = True
22
 
23
  # =========================
24
- # 狀態與映射 (保持原本邏輯)
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}
@@ -51,13 +51,10 @@ 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("請先按「Click to access webcam」開啟攝影機,接著按「錄製 (Record)」開始。")
55
 
56
  def _camera_hint_done() -> str:
57
- return _hint_html("已完成辨識,可按「重新攝影機辨識」再次進行。")
58
-
59
- def _camera_hint_stopped() -> str:
60
- return _hint_html("已停止辨識。")
61
 
62
  def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
63
  zh = EMO_ZH.get(emo_key, emo_key)
@@ -91,43 +88,61 @@ def _downsample_rgb(frame: np.ndarray, target_w: int) -> np.ndarray:
91
  new_h = max(1, int(h * scale))
92
  return np.array(Image.fromarray(frame).resize((target_w, new_h)))
93
 
94
- # 這是關鍵:新增繪圖函式,確保顏色與座標正確
95
- def _draw_ui_overlay(frame: np.ndarray, is_detected: bool):
96
  canvas = frame.copy()
97
  h, w = canvas.shape[:2]
98
 
99
- # 框的大小 (寬50%, 高60%)
100
- bw, bh = int(w * 0.5), int(h * 0.6)
101
  x1, y1 = (w - bw) // 2, (h - bh) // 2
102
  x2, y2 = x1 + bw, y1 + bh
103
 
104
- # 顏色判定 (RGB 格式)
105
- # 如果抓到臉 -> 綠色 (0, 255, 0);沒抓到 -> 紅色 (255, 0, 0)
106
- color = (0, 255, 0) if is_detected else (255, 0, 0)
107
- text = "DETECTED" if is_detected else "ALIGN FACE"
108
-
109
- # 1. 畫外框矩形
110
- cv2.rectangle(canvas, (x1, y1), (x2, y2), color, 4)
 
 
 
111
 
112
- # 2. 畫四個角落增強視覺感 (可選)
113
- length = 30
114
- cv2.line(canvas, (x1, y1), (x1 + length, y1), color, 8)
115
- cv2.line(canvas, (x1, y1), (x1, y1 + length), color, 8)
 
116
 
117
- # 3. 加上文字
118
- cv2.putText(canvas, text, (x1, y1 - 15), cv2.FONT_HERSHEY_DUPLEX, 1.0, color, 2)
 
 
 
 
 
 
 
119
 
120
  return canvas
121
 
122
- def _iou(a, b):
123
- ax, ay, aw, ah = a
124
- bx, by, bw, bh = b
125
- ix1, iy1 = max(ax, bx), max(ay, by)
126
- ix2, iy2 = min(ax + aw, bx + bw), min(ay + ah, by + bh)
127
- iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)
128
- inter = iw * ih
129
- if inter <= 0: return 0.0
130
- return inter / (aw * ah + bw * bh - inter)
 
 
 
 
 
 
 
131
 
132
  def _pick_face_by_tracking(faces, prev_bbox):
133
  if not faces: return None, None
@@ -137,39 +152,33 @@ def _pick_face_by_tracking(faces, prev_bbox):
137
  x, y, w, h = fa.get("x",0), fa.get("y",0), fa.get("w",0), fa.get("h",0)
138
  if w > 0 and h > 0: cand.append((f, (x, y, w, h), w * h))
139
  if not cand: return None, None
140
- if prev_bbox:
141
- best = max(cand, key=lambda c: _iou(prev_bbox, c[1]), default=(None, None, 0))
142
- if best[0] and _iou(prev_bbox, best[1]) >= IOU_TH: return best[0], best[1]
143
  return sorted(cand, key=lambda x: x[2], reverse=True)[0][:2]
144
 
145
  # =========================
146
  # Camera streaming
147
  # =========================
148
  def on_stream(frame_rgb: np.ndarray, st: AppState):
149
- if frame_rgb is None:
150
  return (None, gr.update(), gr.update(), gr.update(), st, gr.update())
151
 
152
- # 如果已經結束,回傳空影像讓攝像頭不再更新
153
- if st.finished or (not st.running):
154
- return (None, gr.update(), gr.update(), gr.update(), st, gr.update())
155
-
156
- # 1. 處理影像
157
  frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
158
  now = time.time()
159
 
160
- # 2. 快速檢測人臉 (每一幀都跑,確保框框顏色反應即時)
161
- small_check = _downsample_rgb(frame_rgb, 400) # 用更小的圖加快速度
162
  faces_check = DeepFace.extract_faces(img_path=small_check, detector_backend="opencv", enforce_detection=False)
163
- pick_check, _ = _pick_face_by_tracking(faces_check, st.track_bbox)
164
- is_detected = (pick_check is not None)
 
 
165
 
166
- # 3. 繪製導引框 (這張圖會回傳給 UI)
167
- frame_with_box = _draw_ui_overlay(frame_rgb, is_detected)
168
 
169
- # 4. 深度辨識邏輯 (每 ANALYZE_EVERY_SEC 秒執行一次)
170
- if is_detected and (now - st.last_analyze_t >= ANALYZE_EVERY_SEC):
171
  st.last_analyze_t = now
172
- # 正式分析
173
  small = _downsample_rgb(frame_rgb, DOWNSAMPLE_W)
174
  faces = DeepFace.extract_faces(img_path=small, detector_backend=DETECTOR_BACKEND, enforce_detection=False, align=ALIGN_FACE)
175
  pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
@@ -184,9 +193,6 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
184
 
185
  if emo_dict:
186
  top_key = max(emo_dict, key=lambda k: float(emo_dict[k]))
187
- top_conf = float(emo_dict[top_key])
188
-
189
- # EMA & Stable 邏輯
190
  keys = list(emo_dict.keys())
191
  vec = np.array([float(emo_dict[k]) for k in keys])
192
  if st.ema_scores is None: st.ema_scores, st.emo_keys = vec, keys
@@ -195,18 +201,16 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
195
  stable_key = st.emo_keys[np.argmax(st.ema_scores)]
196
  st.stable_emo = stable_key
197
 
198
- # 確認穩定度
199
  if st.candidate_emo != stable_key:
200
  st.candidate_emo, st.candidate_since = stable_key, now
201
  elif st.candidate_since and (now - st.candidate_since >= SWITCH_CONFIRM_SEC):
202
- # 完成辨識
203
- st.final_emo, st.final_conf, st.running, st.finished = stable_key, top_conf, False, True
204
- return (frame_with_box, _result_card_html(stable_key, top_conf), _score_radio_value(EMO_TO_SCORE.get(stable_key, 3)), _camera_hint_done(), st, gr.update(value="重新攝影機辨識"))
205
 
206
- # 持續回傳帶有 UI 框的畫面
207
  show_emo = st.stable_emo or "unknown"
208
  return (
209
- frame_with_box,
210
  gr.update(value=_result_card_html(show_emo, st.final_conf)),
211
  gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show_emo, 3))),
212
  gr.update(value=_camera_hint_before_start()),
@@ -230,8 +234,6 @@ with gr.Blocks(title="Emotion Detector") as demo:
230
  state = gr.State(AppState())
231
  gr.Markdown("## 情緒辨識系統")
232
  hint = gr.HTML(_camera_hint_before_start())
233
-
234
- # 這裡的 cam 元件接收輸出的影像
235
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機預覽")
236
 
237
  with gr.Row():
@@ -241,7 +243,6 @@ with gr.Blocks(title="Emotion Detector") as demo:
241
  result = gr.HTML(_hint_html("等待串流..."))
242
  mood = gr.Radio(choices=SCORE_CHOICES, value=SCORE_LABELS[3], label="系統判定心情", interactive=False)
243
 
244
- # 串流綁���
245
  cam.stream(on_stream, [cam, state], [cam, result, mood, hint, state, btn_restart], show_progress="minimal")
246
  btn_restart.click(on_restart, [state], [cam, result, mood, hint, state, btn_restart])
247
  btn_stop.click(on_stop, [state], [cam, result, mood, hint, state, btn_restart])
 
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
+ # 狀態與映射
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}
 
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)
 
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
 
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)
 
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
 
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()),
 
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():
 
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])
248
  btn_stop.click(on_stop, [state], [cam, result, mood, hint, state, btn_restart])