Files
momentry_core/docs/CHARADE_FACE_MATCHING_EXPERIENCE.md
Accusys e3066c3f49 Add Charade face matching experience report
Documents the journey from Rust pipeline snowball bug through
5 iterations of pgvector-based matching to the final 11-identity
centroid approach with dual-gate and ambiguity cleanup.
2026-06-02 05:01:56 +08:00

8.5 KiB
Raw Blame History

Charade 臉部匹配經驗總結

背景

Charade (1963) 影片 a6fb22eebefaef17e62af874997c5944 有 62,298 個人臉偵測結果,分布在 4,378 個 trace 中TKG face tracker 輸出)。目標是將每張臉匹配到正確的 TMDb 演員 identity。

問題

1. Rust Pipeline (face_agent.rs) 的 Snowball 效應

原始 pipeline 透過多輪 propagation 來匹配:

  • Seed embedding 匹配 → propagation rounds (2-10 輪)
  • 每輪把已匹配的 face 當作新 seed 繼續擴散
  • 結果:Antonio Passalia 被匹配 18,821 張臉(實際應 < 50
  • 原因propagation 會放大初始匹配中的假陽性

2. Dev 資料庫污染

dev schema 的 identity_bindings 表:

  • 所有 trace-type binding 的 file_uuid 都是 NULL12,828 行)
  • 這些 binding 只對應已刪除的 CCBN 檔案 (63acd3bb)
  • 完全無法用於 sync 到 public schema

3. TMDb Seed Embedding 品質不均

22/23 個 TMDb identity 有 face_embeddingThomas Chelimsky 因無 TMDb 照片而缺少)。但這些 seed 來自單一 TMDb 照片,品質差異大:

Identity Seed 品質 問題
Audrey Hepburn 特徵明顯,易區分
Cary Grant 但 Charade 造型與 seed 照片有差異
Walter Matthau Seed 照片與 Charade 形象差異大
Bernard Musson 泛用 「典型白人男性」— seed 太泛用
Antonio Passalia 泛用 同上

解決方案演進

V1直接 pgvector 比對 (threshold 0.50)

CROSS JOIN LATERAL (
  SELECT i.id FROM identities i
  WHERE 1 - (embedding <=> i.face_embedding) >= 0.50
  ORDER BY 1 - (embedding <=> i.face_embedding) DESC LIMIT 1
)

結果17,066 匹配 (27.4%)

  • Audrey 9,550 (正確)
  • Antonio 降為 151 (不再 snowball)
  • Bernard Musson 847Paul Bonifas 273 — generic seed 假陽性
  • trace-level 衝突(同一 trace 多個 identity
  • Walter Matthau 僅 535seed 不準導致 recall 低)

V2Trace Conflict Cleanup

在 V1 之後,對每個 conflict trace 做多數決 → 清除 minority identity。

結果:移除 836 個污染臉

  • trace-level 衝突降為 0
  • Bernard Musson 仍保留 847trace 內獨佔)
  • 無法解決 generic seed 的根本問題

V3雙階段 Centroid Matching

設計:

Phase 1: Seed matching @ 0.55 (stricter) → 乾淨 base set
Phase 2: Centroid matching @ 0.45 → 用電影內平均臉擴張 recall

結果27,375 匹配 (43.9%) → trace cleanup → 24,286 (39.0%)

  • Audrey 11,347 (+19%)
  • Cary Grant 3,107 (+56%)
  • Walter Matthau 1,200 (+124%) — centroid 修正 seed!
  • Bernard Musson 2,903 (+243%) — centroid 放大 generic seed
  • Antonio Passalia 898 (+642%) — 同上

教訓Generic seed 的 centroid 更泛用。Phase 2 的低 threshold 讓問題惡化。

V4雙重驗證 (Dual Gate)

在 V3 的 Phase 2 加上 seed_sim >= 0.40 條件:

centroid_sim >= 0.45 AND seed_sim >= 0.40

結果23,023 匹配 → gap cleanup → trace cleanup → 22,548 (36.2%)

  • Bernard / Paul / Antonio / Michel / Clément / Raoul / Roger 仍偏高但 avg_seed_sim 改善

V5最終版排除 7 個 Generic Identity

核心洞察:與其過濾假陽性,不如不讓 generic seed 參賽

只保留 11 個可靠的 TMDb identity排除 7 個:

  • 排除Bernard Musson · Paul Bonifas · Michel Thomass · Antonio Passalia · Clément Harari · Raoul Delfosse · Roger Trapp
  • 保留Audrey · Cary · James Coburn · Jacques Marin · Walter Matthau · George Kennedy · Dominique Minot · Monte Landis · Stanley Donen · Ned Glass · Louis Viret

流程:

1. Clear all assignments
2. Phase 1 @ 0.55 — only against 11 identities
3. Compute centroids
4. Phase 2 — centroid>=0.45 AND seed>=0.40 (11 centroids)
5. Ambiguity gate (top2 gap < 0.04 → NULL)
6. Trace conflict cleanup

最終結果

Identity 最終 faces traces fpt avg_sim
Audrey Hepburn 11,325 438 25.9 0.608
Cary Grant 5,101 ≪ 大幅增加 269 19.0 0.497
James Coburn 1,508 92 16.4 0.588
Jacques Marin 1,438 84 17.1 0.631
Walter Matthau 1,250 55 22.7 0.494
George Kennedy 869 60 14.5 0.590
排除的 7 個 0
Unassigned 39,750

Cary Grant 從 3,107→5,101 (+64%):之前被 Bernard/Antonio 攔截的臉全部釋放。

關鍵教訓

1. Generic Seed 辨識

可以透過以下指標辨識 generic seed

  • Phase 1 faces / traces 比例低< 5 fpt
  • 被分配到大量短 trace(表示非連續場景)
  • avg_seed_sim 偏低但 face count 異常高

2. Propagation 是雙面刃

Rust pipeline 的 propagation 可以增加 recall但前提是 seed 要夠純。Generic seed + propagation = snowball。

3. Seed 數量 vs 品質

不是 identity 越多越好。11 個好 seed 勝過 22 個(含 7 個壞的)。

壞 seed 會攔截好 seed 的配對。排除壞 seed 後,那些臉自然會配到正確的人。

4. Centroid Matching 的適用條件

Centroid matching 只有在以下情況才有效:

  • Centroid 來自高信賴的 Phase 1 配對threshold >= 0.55
  • Centroid 的 Phase 1 base set > 200 faces
  • 搭配 seed_sim dual gate 防止 centroid 飄移

5. Trace Context 的重要性

  • 一個 trace = 同一人face tracker 保證)
  • Trace-level conflict cleanup 是必要的後處理
  • 但無法解決 trace 層級以下(同一 trace 內)的 contamination

可改進的方向

短期

  1. 手動檢查 Cary Grant 的 5,101 facesavg_sim 0.497 偏低,部分可能是假陽性
  2. 補回已被排除的 identity:對 Bernard Musson 等用更高 threshold如 0.60 seed只看能否 match 到少數高信賴臉
  3. 降低 Ambiguity Gate threshold:從 0.04 降到 0.03 可再清除一批邊緣配對

中期

  1. 多 seed 策略:對每個 identity 用 3-5 張 TMDb 照片,取 centroid 作為 seed
  2. 場景約束:利用 shot boundary 資訊限制跨場景的 identity 分配
  3. 雙向驗證:同時用 face→identity 和 identity→trace 兩種方向互相驗證

長期

  1. 取代 pgvector face-level matching:改用 trace-level embedding同一 trace 的所有 face 取平均),再對 trace 做 identity 匹配,減少 single-frame noise

SQL 核心語法

pgvector Nearest Neighbor

SELECT fd.id, m.identity_id
FROM eligible fd
CROSS JOIN LATERAL (
  SELECT i.id FROM identities i
  WHERE 1 - (fd.embedding::vector <=> i.face_embedding) >= {threshold}
  ORDER BY 1 - (fd.embedding::vector <=> i.face_embedding) DESC
  LIMIT 1
) m

Centroid 計算

CREATE TABLE centroids AS
SELECT identity_id, AVG(embedding::vector) as centroid
FROM face_detections
WHERE file_uuid = '{uuid}' AND identity_id IS NOT NULL
GROUP BY identity_id
HAVING COUNT(*) >= 5;

Trace Conflict Cleanup

WITH conflict_traces AS (
  SELECT trace_id FROM face_detections
  WHERE file_uuid = '{uuid}' AND identity_id IS NOT NULL
  GROUP BY trace_id HAVING COUNT(DISTINCT identity_id) > 1
),
trace_majority AS (
  SELECT DISTINCT ON (ct.trace_id) ct.trace_id, fd.identity_id
  FROM conflict_traces ct
  JOIN face_detections fd ON fd.trace_id = ct.trace_id
  WHERE fd.file_uuid = '{uuid}' AND fd.identity_id IS NOT NULL
  GROUP BY ct.trace_id, fd.identity_id
  ORDER BY ct.trace_id, COUNT(*) DESC
)
UPDATE face_detections fd SET identity_id = NULL
FROM trace_majority tm
WHERE fd.file_uuid = '{uuid}' AND fd.trace_id = tm.trace_id
  AND fd.identity_id != tm.identity_id;

Ambiguity Gate

WITH all_sims AS (
  SELECT fd.id, c.identity_id,
         1 - (fd.embedding::vector <=> c.centroid) as sim
  FROM face_detections fd
  CROSS JOIN centroids c
  WHERE fd.file_uuid = '{uuid}' AND fd.identity_id IS NOT NULL
),
ranked AS (
  SELECT id, sim, LEAD(sim) OVER (PARTITION BY id ORDER BY sim DESC) as sim2
  FROM all_sims
),
ambiguous AS (
  SELECT id FROM ranked
  WHERE rn = 1 AND sim - COALESCE(sim2, 0) < 0.04
)
UPDATE face_detections fd SET identity_id = NULL
FROM ambiguous a WHERE fd.id = a.id;

資料庫備份

每次關鍵操作都有備份:

Backup Rows 內容
fd_charade_bak 62,298 原始無 identity 的 Charade face_detections
fd_state_bak2 24,286 V5 執行前的 assignment snapshot
wp_snippets_backup_20260601_11940.sql WordPress snippets 備份