YuAnTsai commited on
Commit
7223a4b
·
verified ·
1 Parent(s): 68007df

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +79 -59
emotion.py CHANGED
@@ -68,7 +68,7 @@ SCORE_CHOICES = [SCORE_LABELS[i] for i in [1, 2, 3, 4, 5]]
68
  # =========================
69
  @dataclass
70
  class AppState:
71
- mode: str = "攝影機辨識"
72
 
73
  running: bool = True
74
  finished: bool = False
@@ -90,14 +90,17 @@ class AppState:
90
  final_emo: Optional[str] = None
91
  final_conf: Optional[float] = None
92
 
93
- final_score: int = 3
 
 
 
 
94
 
95
 
96
  # =========================
97
  # UI HTML (強制黑色文字)
98
  # =========================
99
  def _hint_html(msg: str) -> str:
100
- # 加入 color:#000000 強制黑色
101
  return f"""
102
  <div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;">
103
  {msg}
@@ -126,7 +129,6 @@ def _result_card_html(emo_key: str, conf: Optional[float]) -> str:
126
  conf_txt = "" if conf is None else f"{conf:.1f}%"
127
  score_desc = SCORE_LABELS[score].split("(")[1].rstrip(")")
128
 
129
- # ✅ 強制設定 color: #000000
130
  return f"""
131
  <div style="
132
  border-radius:16px;
@@ -175,25 +177,16 @@ def _downsample_rgb(frame: np.ndarray, target_w: int) -> np.ndarray:
175
  img = img.resize((target_w, new_h))
176
  return np.array(img)
177
 
 
178
  def _zoom_center(frame: np.ndarray, zoom: float) -> np.ndarray:
179
- """
180
- 裁切畫面中心以模擬數位變焦。
181
- zoom: 放大倍率 (例如 1.5)
182
- """
183
  if frame is None or zoom <= 1.0:
184
  return frame
185
-
186
  h, w = frame.shape[:2]
187
-
188
- # 計算新的寬高
189
  new_w = int(w / zoom)
190
  new_h = int(h / zoom)
191
-
192
- # 計算起始點 (讓裁切框置中)
193
  start_x = (w - new_w) // 2
194
  start_y = (h - new_h) // 2
195
-
196
- # 進行裁切
197
  cropped = frame[start_y : start_y + new_h, start_x : start_x + new_w]
198
  return cropped
199
 
@@ -201,12 +194,11 @@ def _zoom_center(frame: np.ndarray, zoom: float) -> np.ndarray:
201
  # =========================
202
  # 引導框(臉要放哪裡)
203
  # =========================
204
- GUIDE_W_RATIO = 0.55 # 框寬佔畫面比例(可調)
205
- GUIDE_H_RATIO = 0.70 # 框高佔畫面比例(可調)
206
- GUIDE_Y_OFFSET = 0.00 # 框中心往上(-)或往下(+)位移比例(可調,例如 -0.05)
207
 
208
  def _guide_box(frame: np.ndarray) -> Tuple[int, int, int, int]:
209
- """回傳引導框 (x1,y1,x2,y2)"""
210
  h, w = frame.shape[:2]
211
  gw = int(w * GUIDE_W_RATIO)
212
  gh = int(h * GUIDE_H_RATIO)
@@ -221,7 +213,6 @@ def _guide_box(frame: np.ndarray) -> Tuple[int, int, int, int]:
221
  return x1, y1, x2, y2
222
 
223
  def _bbox_center_in_box(bbox_xywh: Tuple[float, float, float, float], box_xyxy: Tuple[int, int, int, int]) -> bool:
224
- """判斷臉框中心是否落在引導框內"""
225
  x, y, w, h = bbox_xywh
226
  cx = x + w / 2.0
227
  cy = y + h / 2.0
@@ -229,9 +220,6 @@ def _bbox_center_in_box(bbox_xywh: Tuple[float, float, float, float], box_xyxy:
229
  return (x1 <= cx <= x2) and (y1 <= cy <= y2)
230
 
231
  def _draw_guide_overlay(frame_rgb: np.ndarray, detected_in_box: bool) -> np.ndarray:
232
- """
233
- 在畫面上畫引導框:未偵測到(或不在框內)=紅框;偵測到且在框內=綠框
234
- """
235
  if frame_rgb is None:
236
  return frame_rgb
237
 
@@ -240,27 +228,27 @@ def _draw_guide_overlay(frame_rgb: np.ndarray, detected_in_box: bool) -> np.ndar
240
 
241
  x1, y1, x2, y2 = _guide_box(frame_rgb)
242
 
243
- # 顏色:紅 / 綠(RGB)
244
  color = (0, 200, 0) if detected_in_box else (220, 0, 0)
245
 
246
- # 線寬(跟解析度稍微連動)
247
  h, w = frame_rgb.shape[:2]
248
  thickness = max(3, int(min(w, h) * 0.008))
249
 
250
- # 畫框(加粗)
251
  for t in range(thickness):
252
  draw.rectangle([x1 - t, y1 - t, x2 + t, y2 + t], outline=color)
253
 
254
- # 提示文字
255
- msg = "偵測中…" if detected_in_box else "請把臉放進框內"
256
  tx = x1
257
  ty = max(0, y1 - int(thickness * 6))
258
- draw.rectangle([tx, ty, tx + 240, ty + 32], fill=(255, 255, 255))
259
  draw.text((tx + 6, ty + 6), msg, fill=(0, 0, 0))
260
 
261
  return np.array(img)
262
 
263
 
 
 
 
264
  def _iou(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> float:
265
  ax, ay, aw, ah = a
266
  bx, by, bw, bh = b
@@ -325,6 +313,10 @@ def _reset_state_for_camera(st: AppState) -> AppState:
325
  st.final_emo = None
326
  st.final_conf = None
327
  st.final_score = 3
 
 
 
 
328
  return st
329
 
330
 
@@ -362,20 +354,25 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
362
  st,
363
  gr.update(value="重新攝影機辨識"),
364
  )
365
-
366
- frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
367
 
368
- # 預設:沒偵測��(或不在引導框內)-> 紅框
369
- detected_in_box = False
370
 
371
  now = time.time()
 
 
 
 
372
  if now - st.last_analyze_t < ANALYZE_EVERY_SEC:
373
- show = st.stable_emo or "unknown"
374
- annotated = _draw_guide_overlay(frame_rgb, detected_in_box)
 
 
 
 
375
  return (
376
  gr.update(visible=True, value=annotated),
377
- gr.update(value=_result_card_html(show, st.final_conf)),
378
- gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show, 3))),
379
  gr.update(value=_camera_hint_before_start()),
380
  st,
381
  gr.update(value="重新攝影機辨識"),
@@ -391,20 +388,27 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
391
  enforce_detection=False,
392
  align=ALIGN_FACE,
393
  )
 
394
  pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
 
 
395
  if pick is None or bbox is None or pick.get("face") is None:
396
- show = st.stable_emo or "unknown"
397
- annotated = _draw_guide_overlay(frame_rgb, detected_in_box)
 
 
 
 
398
  return (
399
  gr.update(visible=True, value=annotated),
400
- gr.update(value=_result_card_html(show, st.final_conf)),
401
- gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show, 3))),
402
  gr.update(value=_camera_hint_before_start()),
403
  st,
404
  gr.update(value="重新攝影機辨識"),
405
  )
406
 
407
- # bbox 是 small 座標,換算回 frame_rgb 座標後判斷是否在引導框內
408
  sh, sw = small.shape[:2]
409
  fh, fw = frame_rgb.shape[:2]
410
  sx = fw / max(1, sw)
@@ -412,10 +416,28 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
412
 
413
  x, y, w, h = bbox
414
  bbox_full = (x * sx, y * sy, w * sx, h * sy)
 
415
  guide = _guide_box(frame_rgb)
416
  detected_in_box = _bbox_center_in_box(bbox_full, guide)
417
 
 
 
 
418
  st.track_bbox = bbox
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  face = pick["face"]
420
  if face.dtype != np.uint8:
421
  face = np.clip(face * 255.0, 0, 255).astype(np.uint8)
@@ -429,13 +451,13 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
429
  result = result[0]
430
  emo_dict = result.get("emotion", None)
431
 
 
432
  if not isinstance(emo_dict, dict) or len(emo_dict) == 0:
433
- show = st.stable_emo or "unknown"
434
- annotated = _draw_guide_overlay(frame_rgb, detected_in_box)
435
  return (
436
  gr.update(visible=True, value=annotated),
437
- gr.update(value=_result_card_html(show, st.final_conf)),
438
- gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show, 3))),
439
  gr.update(value=_camera_hint_before_start()),
440
  st,
441
  gr.update(value="重新攝影機辨識"),
@@ -444,13 +466,13 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
444
  top_key = max(emo_dict, key=lambda k: float(emo_dict[k]))
445
  top_conf = float(emo_dict[top_key])
446
 
 
447
  if top_conf < CONF_TH:
448
- show = st.stable_emo or "unknown"
449
- annotated = _draw_guide_overlay(frame_rgb, detected_in_box)
450
  return (
451
  gr.update(visible=True, value=annotated),
452
- gr.update(value=_result_card_html(show, st.final_conf)),
453
- gr.update(value=_score_radio_value(EMO_TO_SCORE.get(show, 3))),
454
  gr.update(value=_camera_hint_before_start()),
455
  st,
456
  gr.update(value="重新攝影機辨識"),
@@ -499,7 +521,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
499
  gr.update(value="重新攝影機辨識"),
500
  )
501
 
502
- annotated = _draw_guide_overlay(frame_rgb, detected_in_box)
503
  return (
504
  gr.update(visible=True, value=annotated),
505
  gr.update(value=_result_card_html(stable_key, top_conf)),
@@ -513,16 +535,15 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
513
  # =========================
514
  # Mode / Buttons
515
  # =========================
516
-
517
  def on_restart(st: AppState):
518
  st = _reset_state_for_camera(st)
519
  return (
520
- gr.update(visible=True),
521
- gr.update(value=_hint_html("等待影像…(尚未開始串流)")),
522
- gr.update(value=_score_radio_value(3)),
523
- gr.update(value=_camera_hint_before_start()),
524
  st,
525
- gr.update(value="重新攝影機辨識"),
526
  )
527
 
528
 
@@ -552,13 +573,12 @@ def on_stop(st: AppState):
552
  # =========================
553
  # Gradio UI
554
  # =========================
555
-
556
  if __name__ == "__main__":
557
  with gr.Blocks(title="Emotion Detector", css=css) as demo:
558
  st = gr.State(AppState())
559
 
560
  gr.Markdown("## 情緒辨識")
561
-
562
  hint = gr.HTML(_camera_hint_before_start())
563
 
564
  cam = gr.Image(
 
68
  # =========================
69
  @dataclass
70
  class AppState:
71
+ mode: str = "攝影機辨識"
72
 
73
  running: bool = True
74
  finished: bool = False
 
90
  final_emo: Optional[str] = None
91
  final_conf: Optional[float] = None
92
 
93
+ final_score: int = 3
94
+
95
+ # ✅ 新增:最近有沒有臉(避免節流期間一直紅框)
96
+ last_face_t: float = 0.0
97
+ face_in_box: bool = False
98
 
99
 
100
  # =========================
101
  # UI HTML (強制黑色文字)
102
  # =========================
103
  def _hint_html(msg: str) -> str:
 
104
  return f"""
105
  <div style="border-radius:14px;padding:14px;border:1px dashed #DADADA;background:#FAFAFA;color:#000000;">
106
  {msg}
 
129
  conf_txt = "" if conf is None else f"{conf:.1f}%"
130
  score_desc = SCORE_LABELS[score].split("(")[1].rstrip(")")
131
 
 
132
  return f"""
133
  <div style="
134
  border-radius:16px;
 
177
  img = img.resize((target_w, new_h))
178
  return np.array(img)
179
 
180
+
181
  def _zoom_center(frame: np.ndarray, zoom: float) -> np.ndarray:
 
 
 
 
182
  if frame is None or zoom <= 1.0:
183
  return frame
184
+
185
  h, w = frame.shape[:2]
 
 
186
  new_w = int(w / zoom)
187
  new_h = int(h / zoom)
 
 
188
  start_x = (w - new_w) // 2
189
  start_y = (h - new_h) // 2
 
 
190
  cropped = frame[start_y : start_y + new_h, start_x : start_x + new_w]
191
  return cropped
192
 
 
194
  # =========================
195
  # 引導框(臉要放哪裡)
196
  # =========================
197
+ GUIDE_W_RATIO = 0.55
198
+ GUIDE_H_RATIO = 0.70
199
+ GUIDE_Y_OFFSET = 0.00
200
 
201
  def _guide_box(frame: np.ndarray) -> Tuple[int, int, int, int]:
 
202
  h, w = frame.shape[:2]
203
  gw = int(w * GUIDE_W_RATIO)
204
  gh = int(h * GUIDE_H_RATIO)
 
213
  return x1, y1, x2, y2
214
 
215
  def _bbox_center_in_box(bbox_xywh: Tuple[float, float, float, float], box_xyxy: Tuple[int, int, int, int]) -> bool:
 
216
  x, y, w, h = bbox_xywh
217
  cx = x + w / 2.0
218
  cy = y + h / 2.0
 
220
  return (x1 <= cx <= x2) and (y1 <= cy <= y2)
221
 
222
  def _draw_guide_overlay(frame_rgb: np.ndarray, detected_in_box: bool) -> np.ndarray:
 
 
 
223
  if frame_rgb is None:
224
  return frame_rgb
225
 
 
228
 
229
  x1, y1, x2, y2 = _guide_box(frame_rgb)
230
 
231
+ # 紅/綠框
232
  color = (0, 200, 0) if detected_in_box else (220, 0, 0)
233
 
 
234
  h, w = frame_rgb.shape[:2]
235
  thickness = max(3, int(min(w, h) * 0.008))
236
 
 
237
  for t in range(thickness):
238
  draw.rectangle([x1 - t, y1 - t, x2 + t, y2 + t], outline=color)
239
 
240
+ msg = "已偵測到人臉,開始辨識情緒中…" if detected_in_box else "未偵測到人臉,請把臉放進框內"
 
241
  tx = x1
242
  ty = max(0, y1 - int(thickness * 6))
243
+ draw.rectangle([tx, ty, tx + 360, ty + 32], fill=(255, 255, 255))
244
  draw.text((tx + 6, ty + 6), msg, fill=(0, 0, 0))
245
 
246
  return np.array(img)
247
 
248
 
249
+ # =========================
250
+ # Tracking / IoU
251
+ # =========================
252
  def _iou(a: Tuple[int, int, int, int], b: Tuple[int, int, int, int]) -> float:
253
  ax, ay, aw, ah = a
254
  bx, by, bw, bh = b
 
313
  st.final_emo = None
314
  st.final_conf = None
315
  st.final_score = 3
316
+
317
+ # ✅ 新增欄位也要 reset
318
+ st.last_face_t = 0.0
319
+ st.face_in_box = False
320
  return st
321
 
322
 
 
354
  st,
355
  gr.update(value="重新攝影機辨識"),
356
  )
 
 
357
 
358
+ frame_rgb = _zoom_center(frame_rgb, ZOOM_FACTOR)
 
359
 
360
  now = time.time()
361
+
362
+ # ✅ 節流時沿用最近一次「臉是否在框內」狀態(避免一直紅框)
363
+ detected_in_box_recent = st.face_in_box and (now - st.last_face_t) < 1.0
364
+
365
  if now - st.last_analyze_t < ANALYZE_EVERY_SEC:
366
+ annotated = _draw_guide_overlay(frame_rgb, detected_in_box_recent)
367
+ if detected_in_box_recent:
368
+ result_html = _hint_html("已偵測到人臉,開始辨識情緒中…")
369
+ else:
370
+ result_html = _hint_html("未偵測到人臉,請把臉放進框內")
371
+
372
  return (
373
  gr.update(visible=True, value=annotated),
374
+ gr.update(value=result_html),
375
+ gr.update(value=_score_radio_value(3)),
376
  gr.update(value=_camera_hint_before_start()),
377
  st,
378
  gr.update(value="重新攝影機辨識"),
 
388
  enforce_detection=False,
389
  align=ALIGN_FACE,
390
  )
391
+
392
  pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
393
+
394
+ # ✅ (A) 完全沒臉:紅框 + 不顯示情緒(避免「沒臉也顯示害怕」)
395
  if pick is None or bbox is None or pick.get("face") is None:
396
+ st.track_bbox = None
397
+ st.face_in_box = False
398
+ st.last_face_t = now
399
+ st.stable_emo = "unknown"
400
+
401
+ annotated = _draw_guide_overlay(frame_rgb, False)
402
  return (
403
  gr.update(visible=True, value=annotated),
404
+ gr.update(value=_hint_html("未偵測到人臉,請把臉放進框內")),
405
+ gr.update(value=_score_radio_value(3)),
406
  gr.update(value=_camera_hint_before_start()),
407
  st,
408
  gr.update(value="重新攝影機辨識"),
409
  )
410
 
411
+ # bbox 是 small 座標,換算回 frame_rgb
412
  sh, sw = small.shape[:2]
413
  fh, fw = frame_rgb.shape[:2]
414
  sx = fw / max(1, sw)
 
416
 
417
  x, y, w, h = bbox
418
  bbox_full = (x * sx, y * sy, w * sx, h * sy)
419
+
420
  guide = _guide_box(frame_rgb)
421
  detected_in_box = _bbox_center_in_box(bbox_full, guide)
422
 
423
+ # ✅ 更新「臉狀態」
424
+ st.last_face_t = now
425
+ st.face_in_box = detected_in_box
426
  st.track_bbox = bbox
427
+
428
+ # ✅ (B) 有臉但不在框內:紅框,不做情緒辨識
429
+ if not detected_in_box:
430
+ annotated = _draw_guide_overlay(frame_rgb, False)
431
+ return (
432
+ gr.update(visible=True, value=annotated),
433
+ gr.update(value=_hint_html("已偵測到人臉,但請把臉放進框內")),
434
+ gr.update(value=_score_radio_value(3)),
435
+ gr.update(value=_camera_hint_before_start()),
436
+ st,
437
+ gr.update(value="重新攝影機辨識"),
438
+ )
439
+
440
+ # ✅ (C) 有臉且在框內:綠框,才開始 analyze
441
  face = pick["face"]
442
  if face.dtype != np.uint8:
443
  face = np.clip(face * 255.0, 0, 255).astype(np.uint8)
 
451
  result = result[0]
452
  emo_dict = result.get("emotion", None)
453
 
454
+ # 若分析結果不完整:仍顯示綠框 + 辨識中提示
455
  if not isinstance(emo_dict, dict) or len(emo_dict) == 0:
456
+ annotated = _draw_guide_overlay(frame_rgb, True)
 
457
  return (
458
  gr.update(visible=True, value=annotated),
459
+ gr.update(value=_hint_html("已偵測到人臉,開始辨識情緒中…")),
460
+ gr.update(value=_score_radio_value(3)),
461
  gr.update(value=_camera_hint_before_start()),
462
  st,
463
  gr.update(value="重新攝影機辨識"),
 
466
  top_key = max(emo_dict, key=lambda k: float(emo_dict[k]))
467
  top_conf = float(emo_dict[top_key])
468
 
469
+ # 信心不足:仍綠框 + 辨識中
470
  if top_conf < CONF_TH:
471
+ annotated = _draw_guide_overlay(frame_rgb, True)
 
472
  return (
473
  gr.update(visible=True, value=annotated),
474
+ gr.update(value=_hint_html("已偵測到人臉,開始辨識情緒中…")),
475
+ gr.update(value=_score_radio_value(3)),
476
  gr.update(value=_camera_hint_before_start()),
477
  st,
478
  gr.update(value="重新攝影機辨識"),
 
521
  gr.update(value="重新攝影機辨識"),
522
  )
523
 
524
+ annotated = _draw_guide_overlay(frame_rgb, True)
525
  return (
526
  gr.update(visible=True, value=annotated),
527
  gr.update(value=_result_card_html(stable_key, top_conf)),
 
535
  # =========================
536
  # Mode / Buttons
537
  # =========================
 
538
  def on_restart(st: AppState):
539
  st = _reset_state_for_camera(st)
540
  return (
541
+ gr.update(visible=True),
542
+ gr.update(value=_hint_html("等待影像…(尚未開始串流)")),
543
+ gr.update(value=_score_radio_value(3)),
544
+ gr.update(value=_camera_hint_before_start()),
545
  st,
546
+ gr.update(value="重新攝影機辨識"),
547
  )
548
 
549
 
 
573
  # =========================
574
  # Gradio UI
575
  # =========================
 
576
  if __name__ == "__main__":
577
  with gr.Blocks(title="Emotion Detector", css=css) as demo:
578
  st = gr.State(AppState())
579
 
580
  gr.Markdown("## 情緒辨識")
581
+
582
  hint = gr.HTML(_camera_hint_before_start())
583
 
584
  cam = gr.Image(