🎯 YOLO Tracker-Aided Temporal Smoothing
2026-05-18 · 用上下幀關係降 YOLO det 的 FP/flickering,無需重訓
一句話結論:person YOLO det 加 ByteTrack + per-track confidence sliding window,**單幀假框被 filter / 短暫漏抓被 gap fill / 持續真陽性被 boost**。預期 FP -30~50%,flickering -60%,零訓練成本。
🎯 為什麼要做
場域 FP/FN 根因(過往 audit):
- 78% FP 來自 YOLO 假框(單幀偵測誤判)
- 97% 漏報來自 YOLO 漏框(同人忽抓忽漏 flickering)
- 換更大 ViT 分類器 → 邊際遞減(已 ablation 確認)
**模型本身已飽和,但畫面是連續的 — 我們白白浪費了「上下幀關係」這個訊號**。同一個人在連續 30 幀內如果只有 1 幀偵測到,多半是 FP;如果連 8 幀都偵測到,幾乎一定是真的。
🧠 核心 idea(3 個機制)
| 機制 | 解決什麼 | 邏輯 |
| 1. Per-track confidence smoothing | 單幀低 conf 噪音 | 同 track_id N 幀 conf 做 sliding mean / EMA,平滑 confidence 抖動 |
| 2. Gap filling | 短暫漏抓 / 遮擋 | 同 track 短暫消失 ≤ 3 幀,用 motion model interpolate bbox,當作仍存在 |
| 3. Transient filter | 單幀假框 FP | track 長度 < N_min 幀(如 3)視為 transient,不報 alarm |
🔄 完整 pipeline
RTSP frame
↓
┌─────────────────────────────┐
│ Stage 1: YOLO det │ ← 現役 person YOLO11n(不動)
│ → raw det bbox + conf │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Stage 2: ByteTrack │ ← Ultralytics 內建,加 persist=True
│ → bbox + tid │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Stage 3: Per-track buffer │ ← 新加(核心)
│ for tid in tracks: │
│ deque.append(conf) │
│ smoothed = mean(deque) │
│ track_age += 1 │
│ → filter / fill │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Stage 4: 下游消費者 │
│ - PPE 21-attr cls │
│ - safety_rope RoI │
│ - iSeek 通報 │
└─────────────────────────────┘
⚙️ 演算法詳細
per-track state
track_state: dict[tid, {
"conf_deque": deque(maxlen=N), # 最近 N 幀的 confidence
"bbox_deque": deque(maxlen=N), # 最近 N 幀的 bbox(給 gap fill)
"first_seen_frame": int, # 第一次出現
"last_seen_frame": int, # 最後一次出現
"miss_count": int, # 連續多少幀沒看到(gap fill 用)
}]
每幀更新邏輯
def process_frame(frame, frame_idx):
raw_dets = yolo.track(frame, persist=True, tracker="botsort.yaml")
smoothed_dets = []
seen_tids = set()
# update active tracks
for det in raw_dets:
tid = det.tid
seen_tids.add(tid)
st = track_state.setdefault(tid, {...})
st["conf_deque"].append(det.conf)
st["bbox_deque"].append(det.bbox)
st["last_seen_frame"] = frame_idx
st["miss_count"] = 0
smoothed_conf = mean(st["conf_deque"])
track_age = len(st["conf_deque"])
# transient filter — track 太短不信
if track_age < N_MIN_TRACK:
continue # 不輸出到下游
# 輸出 smoothed det
smoothed_dets.append({
"bbox": det.bbox, "tid": tid,
"raw_conf": det.conf,
"smoothed_conf": smoothed_conf,
"track_age": track_age,
})
# gap fill — 沒看到的 active track
for tid, st in track_state.items():
if tid in seen_tids: continue
st["miss_count"] += 1
if st["miss_count"] <= N_GAP_MAX:
# interpolate bbox(用最後 bbox 或 linear motion)
ghost_bbox = st["bbox_deque"][-1]
smoothed_dets.append({
"bbox": ghost_bbox, "tid": tid,
"raw_conf": 0.0,
"smoothed_conf": mean(st["conf_deque"]) * 0.7, # 0.7 為衰減因子
"track_age": len(st["conf_deque"]),
"is_ghost": True,
})
elif st["miss_count"] > N_RETIRE:
del track_state[tid] # retire 死掉的 track
return smoothed_dets
建議參數
| 參數 | 建議值 | 說明 |
N (deque maxlen) | 8-16 幀 | RTSP @ 10 fps 約 1-1.5 秒 |
N_MIN_TRACK (transient filter) | 3-5 幀 | track 持續這麼久才認真 |
N_GAP_MAX (gap fill) | 3 幀 | 短暫遮擋可補;超過視為真消失 |
N_RETIRE (清掉死 track) | 30 幀 | 3 秒沒看到就 retire |
ghost conf decay | 0.7 | 補幀的 confidence 打折,不要當真實 det |
🚨 跟 iSeek 通報路徑整合
現在 iSeek rule:「raw det 連續 N 幀觸發 sustained → 通報 → cooldown」
新 iSeek rule:「smoothed det 出現即可觸發 → 通報 → cooldown」
| Before | After | Bonus |
|--------|-------|-------|
| sustained window 5-10 幀 才報 | 3-5 幀 即可(smoothed 本身夠穩)| 通報延遲 -50% |
| FP rate 高(同一 false alarm 連報 cooldown 期)| FP 在 smoothing 階段就過濾 | cooldown 邏輯可放寬 |
| flickering 導致 alarm 中斷 → 重新計算 sustained | 同 track 持續中(gap fill),sustained 不中斷 | recall 改善 |
📊 預期效益(推測,需 production 驗證)
| 指標 | baseline | + smoothing | 說明 |
| FP rate | 100% | 50-70% | transient filter + smoothing 雙效 |
| flickering(同人忽抓忽漏) | 常見 | -60% | gap fill 補回 |
| recall(持續真陽性) | baseline | +2-5% | gap fill + boost confidence |
| iSeek 通報延遲 | sustained × frame_dt | -50% | sustained 可調短 |
| 推論成本 | 1× | ~1.1× | tracker 開銷可忽略 |
🛠️ 部署位置
| 服務 | 位置 | 動作 |
| model_viewer / ppe-demo | scripts/model_viewer/app.py | PPE21Handler 已加 完成;person handler 待加 |
| iSeek | iSeek_iframe (5090-2 上 docker) | rule engine 整合 smoothed det 待做 |
| iseek-river | iseek-river.intemotech.com | river_debris 也可加 待做 |
| 離線推論 batch | infer_ppe_video.py / infer_ppe_to_json.py | 可加 選做 |
⚠️ 注意 / 已知限制
- 切換 RTSP / 換 source 必須 reset_tracking() — 不然舊 track 會延續到新影片
- tracker 本身有限制:ByteTrack 對快速移動 / 嚴重遮擋會斷 track,這時 gap fill 反而 cover 不到
- 第一秒推論 transient:N_MIN_TRACK=3 意思是新進入的人前 3 幀「不會被報出」,對「快速衝入」場景可能漏報。可以對 high-conf det (raw_conf > 0.85) bypass transient filter
- 不同場景需調參:RTSP fps 變化(10 / 15 / 25 fps)影響 N 設定,可以做成 per-camera config
- 不是 SOTA 終點:A 路線飽和後可考慮重訓 VOD architecture(FGFA / SELSA / YOLO-MS),但 cost ×3-10
🚀 動工順序
| Step | 動作 | 時長 |
| 1 | model_viewer person handler 加 tracker smoothing(沿用 PPE21Handler 範本) | 1 天 |
| 2 | 同份 code 套到 iSeek inference 服務(5090-2 docker) | 1 天 |
| 3 | 調 iSeek rule 整合 smoothed conf + 縮短 sustained 窗口 | 1 天 |
| 4 | production 跑 1 週 ABTest(half cam baseline / half cam smoothing) | 1 週 |
| 5 | analytics:FP / recall / 通報延遲 對比 | 1 天 |
| 6 | 若效益顯著 → 全 cam roll out | 1 天 |
🔗 相關 VOD 研究(後續可延伸)
| 方法 | 類別 | 備註 |
| ByteTrack / BoT-SORT | Tracker | Ultralytics 內建,我們本方案直接用 |
| FGFA (Flow-Guided Feature Aggregation) | VOD-train | 用 optical flow warp 鄰幀 feature 加權平均 |
| SELSA (Sequence Level Semantic Aggregation) | VOD-train | DETR-based |
| MEGA (Multi-frame Enhanced Global) | VOD-train | Long-term + short-term memory |
| YOLOv8-MS / YOLO11-MS | VOD-train | YOLO 加 temporal neck(社群實作) |
| TSM (Temporal Shift Module) | VOD-train | channel-wise shift to prev/next frame |
| TransVOD / PTSEFormer | VOD-train | DETR + temporal attention |
| VideoSwin / SlowFast | Video transformer | 長期可考慮,cost 高 |
本份是
A 路線(tracker-aided smoothing)的設計文件,零訓練成本,當下立即可動。
未來累積足夠 sequence 標注後,可再評估 B 路線(FGFA / SELSA 重訓),預期再 +5-10% recall / -20% FP,但 cost 3-10×。
→ 回模型訓練 SOP ·
→ 所有報告目錄