YuAnTsai commited on
Commit
08629ea
·
verified ·
1 Parent(s): 854943e

Update emotion.py

Browse files
Files changed (1) hide show
  1. emotion.py +21 -29
emotion.py CHANGED
@@ -24,14 +24,12 @@ SWITCH_CONFIRM_SEC = 2.5
24
  IOU_TH = 0.25
25
  ZOOM_FACTOR = 1.5
26
 
27
- DETECTOR_BACKEND = "mtcnn" # 想快一點可改 "opencv"
28
  ALIGN_FACE = True
29
 
30
- # ✅ 新增:防止背景誤判為臉(你要的核心)
31
- # 臉框面積佔整張畫面比例下限(太小通常是誤判)
32
- MIN_FACE_AREA_RATIO = 0.05 # 5%(可調:0.03~0.08)
33
- # 偵測器信心值下限(MTCNN/RetinaFace 通常有 confidence)
34
- MIN_DET_CONF = 0.90 # 可調:0.80~0.95
35
 
36
 
37
  # =========================
@@ -59,9 +57,6 @@ EMO_TO_SCORE = {
59
  "unknown": 3,
60
  }
61
 
62
- def get_emotion_score(emo: str) -> int:
63
- return EMO_TO_SCORE.get(emo, 3)
64
-
65
  SCORE_LABELS = {
66
  1: "1(心情差)",
67
  2: "2(不太好)",
@@ -83,7 +78,7 @@ class AppState:
83
  finished: bool = False
84
  last_analyze_t: float = 0.0
85
 
86
- # tracking
87
  track_bbox: Optional[Tuple[int, int, int, int]] = None
88
 
89
  # smoothing
@@ -98,10 +93,9 @@ class AppState:
98
  # final
99
  final_emo: Optional[str] = None
100
  final_conf: Optional[float] = None
101
-
102
  final_score: int = 3
103
 
104
- # 最近有沒有臉(避免節流期間一直紅框)
105
  last_face_t: float = 0.0
106
  face_in_box: bool = False
107
 
@@ -313,14 +307,13 @@ def _reset_state_for_camera(st: AppState) -> AppState:
313
  st.final_emo = None
314
  st.final_conf = None
315
  st.final_score = 3
316
-
317
  st.last_face_t = 0.0
318
  st.face_in_box = False
319
  return st
320
 
321
 
322
  # =========================
323
- # ✅ 判斷「這個 face box 算不算真的臉」
324
  # =========================
325
  def _is_real_face(
326
  pick: Dict,
@@ -330,13 +323,10 @@ def _is_real_face(
330
  fh, fw = frame_hw
331
  x, y, w, h = bbox_full_xywh
332
 
333
- # 1) 面積門檻(避免小塊背景誤判)
334
  area_ratio = (w * h) / max(1.0, float(fw * fh))
335
  if area_ratio < MIN_FACE_AREA_RATIO:
336
  return False
337
 
338
- # 2) 偵測 confidence(如果偵測器有提供)
339
- # DeepFace.extract_faces 的 dict 常見會有 "confidence"(依 backend 而異)
340
  det_conf = pick.get("confidence", None)
341
  if det_conf is not None:
342
  try:
@@ -352,9 +342,9 @@ def _is_real_face(
352
  # Camera streaming
353
  # =========================
354
  def on_stream(frame_rgb: np.ndarray, st: AppState):
 
355
  if st.finished or (not st.running):
356
  cam_visible = False
357
-
358
  if st.final_emo:
359
  result_html = _result_card_html(st.final_emo, st.final_conf)
360
  mood_val = _score_radio_value(EMO_TO_SCORE.get(st.final_emo, 3))
@@ -387,7 +377,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
387
 
388
  now = time.time()
389
 
390
- # 節流:沿用最近一次「真的有人臉在框內」狀態
391
  detected_in_box_recent = st.face_in_box and (now - st.last_face_t) < 1.0
392
 
393
  if now - st.last_analyze_t < ANALYZE_EVERY_SEC:
@@ -406,12 +396,12 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
406
 
407
  small = _downsample_rgb(frame_rgb, DOWNSAMPLE_W)
408
 
409
- # ✅ 把「沒臉」當作 faces=[](避免 backend 誤回傳奇怪結果)
410
  try:
411
  faces = DeepFace.extract_faces(
412
  img_path=small,
413
  detector_backend=DETECTOR_BACKEND,
414
- enforce_detection=True, # ✅ 變嚴格:沒偵測到臉就丟例外
415
  align=ALIGN_FACE,
416
  )
417
  except Exception:
@@ -419,7 +409,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
419
 
420
  pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
421
 
422
- # (A) 完全沒臉:紅框 + 清掉狀態
423
  if pick is None or bbox is None or pick.get("face") is None:
424
  st.track_bbox = None
425
  st.face_in_box = False
@@ -445,7 +435,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
445
  x, y, w, h = bbox
446
  bbox_full = (x * sx, y * sy, w * sx, h * sy)
447
 
448
- # ✅ 新增:先檢查是不是「真的臉」(面積+confidence)
449
  if not _is_real_face(pick, bbox_full, (fh, fw)):
450
  st.track_bbox = None
451
  st.face_in_box = False
@@ -469,7 +459,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
469
  st.face_in_box = detected_in_box
470
  st.track_bbox = bbox
471
 
472
- # (B) 有臉但不在框內:紅框,不做情緒辨識
473
  if not detected_in_box:
474
  annotated = _draw_guide_overlay(frame_rgb, False)
475
  return (
@@ -481,12 +471,12 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
481
  gr.update(value="重新攝影機辨���"),
482
  )
483
 
484
- # (C) 有臉且在框內:綠框,才開始 analyze
485
  face = pick["face"]
486
  if face.dtype != np.uint8:
487
  face = np.clip(face * 255.0, 0, 255).astype(np.uint8)
488
 
489
- # ✅ analyze 也變嚴格:如果這塊其實不是臉,就當沒臉(避免誤判一路走到底)
490
  try:
491
  result = DeepFace.analyze(
492
  img_path=face,
@@ -551,6 +541,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
551
  stable_key = st.emo_keys[stable_idx]
552
  st.stable_emo = stable_key
553
 
 
554
  if st.candidate_emo != stable_key:
555
  st.candidate_emo = stable_key
556
  st.candidate_since = now
@@ -597,7 +588,7 @@ def on_stream(frame_rgb: np.ndarray, st: AppState):
597
  def on_restart(st: AppState):
598
  st = _reset_state_for_camera(st)
599
  return (
600
- gr.update(visible=True),
601
  gr.update(value=_hint_html("等待影像…(尚未開始串流)")),
602
  gr.update(value=_score_radio_value(3)),
603
  gr.update(value=_camera_hint_before_start()),
@@ -608,9 +599,10 @@ def on_restart(st: AppState):
608
  def on_stop(st: AppState):
609
  st.running = False
610
  st.finished = True
 
611
  if st.final_emo is None:
612
  return (
613
- gr.update(visible=False),
614
  gr.update(value=_hint_html("已停止辨識。")),
615
  gr.update(value=_score_radio_value(3)),
616
  gr.update(value=_camera_hint_stopped()),
@@ -619,7 +611,7 @@ def on_stop(st: AppState):
619
  )
620
 
621
  return (
622
- gr.update(visible=False),
623
  gr.update(value=_result_card_html(st.final_emo, st.final_conf)),
624
  gr.update(value=_score_radio_value(EMO_TO_SCORE.get(st.final_emo, 3))),
625
  gr.update(value=_camera_hint_done()),
 
24
  IOU_TH = 0.25
25
  ZOOM_FACTOR = 1.5
26
 
27
+ DETECTOR_BACKEND = "mtcnn" # 想快一點可改 "opencv"
28
  ALIGN_FACE = True
29
 
30
+ # ✅ 防止背景誤判為臉(重要)
31
+ MIN_FACE_AREA_RATIO = 0.05 # 臉框面積佔整張畫面比例下限(3%~8%可調)
32
+ MIN_DET_CONF = 0.90 # 偵測器信心值下限(若 backend 提供 confidence)
 
 
33
 
34
 
35
  # =========================
 
57
  "unknown": 3,
58
  }
59
 
 
 
 
60
  SCORE_LABELS = {
61
  1: "1(心情差)",
62
  2: "2(不太好)",
 
78
  finished: bool = False
79
  last_analyze_t: float = 0.0
80
 
81
+ # tracking(用於連續幀追蹤)
82
  track_bbox: Optional[Tuple[int, int, int, int]] = None
83
 
84
  # smoothing
 
93
  # final
94
  final_emo: Optional[str] = None
95
  final_conf: Optional[float] = None
 
96
  final_score: int = 3
97
 
98
+ # ✅ 新增:最近有沒有臉(避免節流期間框顏色亂跳)
99
  last_face_t: float = 0.0
100
  face_in_box: bool = False
101
 
 
307
  st.final_emo = None
308
  st.final_conf = None
309
  st.final_score = 3
 
310
  st.last_face_t = 0.0
311
  st.face_in_box = False
312
  return st
313
 
314
 
315
  # =========================
316
+ # ✅ 判斷這個 bbox 是否真的像「人臉」
317
  # =========================
318
  def _is_real_face(
319
  pick: Dict,
 
323
  fh, fw = frame_hw
324
  x, y, w, h = bbox_full_xywh
325
 
 
326
  area_ratio = (w * h) / max(1.0, float(fw * fh))
327
  if area_ratio < MIN_FACE_AREA_RATIO:
328
  return False
329
 
 
 
330
  det_conf = pick.get("confidence", None)
331
  if det_conf is not None:
332
  try:
 
342
  # Camera streaming
343
  # =========================
344
  def on_stream(frame_rgb: np.ndarray, st: AppState):
345
+ # 已結束/停止
346
  if st.finished or (not st.running):
347
  cam_visible = False
 
348
  if st.final_emo:
349
  result_html = _result_card_html(st.final_emo, st.final_conf)
350
  mood_val = _score_radio_value(EMO_TO_SCORE.get(st.final_emo, 3))
 
377
 
378
  now = time.time()
379
 
380
+ # 節流:沿用最近一次臉狀態(避免顏色亂跳)
381
  detected_in_box_recent = st.face_in_box and (now - st.last_face_t) < 1.0
382
 
383
  if now - st.last_analyze_t < ANALYZE_EVERY_SEC:
 
396
 
397
  small = _downsample_rgb(frame_rgb, DOWNSAMPLE_W)
398
 
399
+ # ✅ extract_faces 變嚴格:沒臉就當 faces=[]
400
  try:
401
  faces = DeepFace.extract_faces(
402
  img_path=small,
403
  detector_backend=DETECTOR_BACKEND,
404
+ enforce_detection=True,
405
  align=ALIGN_FACE,
406
  )
407
  except Exception:
 
409
 
410
  pick, bbox = _pick_face_by_tracking(faces, st.track_bbox)
411
 
412
+ # (A) 完全沒臉
413
  if pick is None or bbox is None or pick.get("face") is None:
414
  st.track_bbox = None
415
  st.face_in_box = False
 
435
  x, y, w, h = bbox
436
  bbox_full = (x * sx, y * sy, w * sx, h * sy)
437
 
438
+ # ✅ 防背景誤判:不夠大/信心不足 -> 視為沒臉
439
  if not _is_real_face(pick, bbox_full, (fh, fw)):
440
  st.track_bbox = None
441
  st.face_in_box = False
 
459
  st.face_in_box = detected_in_box
460
  st.track_bbox = bbox
461
 
462
+ # (B) 有臉但不在框內
463
  if not detected_in_box:
464
  annotated = _draw_guide_overlay(frame_rgb, False)
465
  return (
 
471
  gr.update(value="重新攝影機辨���"),
472
  )
473
 
474
+ # (C) 有臉且在框內:綠框,開始 analyze
475
  face = pick["face"]
476
  if face.dtype != np.uint8:
477
  face = np.clip(face * 255.0, 0, 255).astype(np.uint8)
478
 
479
+ # ✅ analyze 也嚴格:避免誤判一路走到底
480
  try:
481
  result = DeepFace.analyze(
482
  img_path=face,
 
541
  stable_key = st.emo_keys[stable_idx]
542
  st.stable_emo = stable_key
543
 
544
+ # 延遲確認
545
  if st.candidate_emo != stable_key:
546
  st.candidate_emo = stable_key
547
  st.candidate_since = now
 
588
  def on_restart(st: AppState):
589
  st = _reset_state_for_camera(st)
590
  return (
591
+ gr.update(visible=True, value=None), # ✅ 清掉上一張畫面(你問的重點)
592
  gr.update(value=_hint_html("等待影像…(尚未開始串流)")),
593
  gr.update(value=_score_radio_value(3)),
594
  gr.update(value=_camera_hint_before_start()),
 
599
  def on_stop(st: AppState):
600
  st.running = False
601
  st.finished = True
602
+
603
  if st.final_emo is None:
604
  return (
605
+ gr.update(visible=False, value=None), # ✅ 順便清空畫面(可選,但我先幫你加上)
606
  gr.update(value=_hint_html("已停止辨識。")),
607
  gr.update(value=_score_radio_value(3)),
608
  gr.update(value=_camera_hint_stopped()),
 
611
  )
612
 
613
  return (
614
+ gr.update(visible=False, value=None), # ✅ 順便清空
615
  gr.update(value=_result_card_html(st.final_emo, st.final_conf)),
616
  gr.update(value=_score_radio_value(EMO_TO_SCORE.get(st.final_emo, 3))),
617
  gr.update(value=_camera_hint_done()),