新增物理合理性過濾 max_accel_kmh: 擋高信心誤讀(暴增不可能加速)

平滑前先剔除『從上一個有效讀數一秒內暴增超過 max_accel_kmh(25)』的讀數,
這類多為高信心 OCR 誤讀(如 4->70->76 conf 0.76~0.78),會造成假性提早起步。
只擋暴增,煞車減速不受限。實測擋掉 02 的 19:18 70/76 誤讀。

註: 雜訊路口的邊界精度受平滑視窗 ±3s 限制,仍有約 5 秒誤差(OCR 品質的物理極限)。

本次調整由 Claude Opus 4.8 (1M context) 協助處理。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:12:26 +08:00
parent df85116088
commit 9c077284db
3 changed files with 22 additions and 2 deletions
+1
View File
@@ -86,6 +86,7 @@ python auto_remove_redlight.py 01.MOV 03.MOV 05.MOV
| `stop_seconds` | `4.0` | 連續停車超過幾秒判定為紅燈 |
| `speed_threshold` | `0` | 時速 ≤ 此值視為停止 |
| `min_confidence` | `0.7` | OCR 信心低於此值視為「讀不到」(交給平滑用前後判斷)|
| `max_accel_kmh` | `25` | 物理合理加速上限(km/h 每秒):機車不可能一秒從 4 衝到 70,超過此加速度的讀數視為高信心誤讀丟棄(只擋暴增,煞車減速不受限)|
| `smooth_window` | `5` | 中位數濾波視窗(取樣點數) |
| `depart_seconds` | `8.0` | 起步門檻(秒):兩段停車間「持續行駛」需達到(滿)此秒數才算起步成功並保留(含這 8 秒);不足者(起步 N 秒內又停)視為仍在等待,連同短暫蠕動一起剪掉(處理車陣走走停停) |
| `depart_min_speed` | `15` | 真起步速度門檻(km/h):一段移動要「加速到」此速度才算真起步/行駛而保留(連同前面龜速起點一起留);若整段最高速都低於此(只是排隊龜速、從未真正騎走),整段改判停止剪掉 |
+20 -2
View File
@@ -94,6 +94,9 @@ SPEED_THRESHOLD = 0 # 時速 <= 此值視為「停止」。0 =
MIN_CONFIDENCE = 0.70 # OCR 信心低於此值的結果視為「未知」(交給平滑用前後判斷)
# 0.7 可濾掉停車時的低信心誤讀(如把 0 看成 70@0.57)
MAX_SPEED = 300 # 合理時速上限,超過視為誤判(雜訊),當成未知
MAX_ACCEL_KMH = 25 # 物理合理加速上限(km/h 每秒)。機車不可能一秒從 4 衝到 70,
# 超過此加速度的讀數視為誤讀丟棄(擋高信心誤讀,如 4->70)。
# 只限制「暴增」;煞車減速很快,不受此限。
# --- 平滑濾波(關鍵!解決停車時 OCR 在 0 與雜訊間跳動的問題)----
# 停車時 OCR 多數讀到 0,但偶爾蹦出單格雜訊(6、60…)或空值,會把「連續停車」
@@ -323,13 +326,27 @@ def smooth_speeds(samples: List[Sample], window: int) -> List[int]:
if n == 0:
return []
spd: List[Optional[int]] = [s.speed for s in samples]
# 物理合理性過濾: 剔除「從上一個有效讀數一秒內暴增」的不可能加速(多為高信心誤讀,
# 如 4 -> 70)。只擋暴增;煞車減速很快故不限制下降。
last_v: Optional[int] = None
last_i = 0
for i in range(n):
if spd[i] is None:
continue
if last_v is not None and spd[i] - last_v > MAX_ACCEL_KMH * (i - last_i):
spd[i] = None # 不可能的暴增 → 視為誤讀丟棄
continue
last_v, last_i = spd[i], i
# 中位數(只對有效讀數;空白跳過,整個視窗都讀不到才當 0)
half = max(0, window // 2)
smoothed: List[int] = [0] * n
for i in range(n):
lo = max(0, i - half)
hi = min(n, i + half + 1)
vals = sorted(samples[k].speed for k in range(lo, hi)
if samples[k].speed is not None)
vals = sorted(v for v in spd[lo:hi] if v is not None)
smoothed[i] = vals[len(vals) // 2] if vals else 0 # 全讀不到 → 視為停止
return smoothed
@@ -1042,6 +1059,7 @@ _CONFIG_KEYS = {
"stop_seconds": "STOP_SECONDS",
"speed_threshold": "SPEED_THRESHOLD",
"min_confidence": "MIN_CONFIDENCE",
"max_accel_kmh": "MAX_ACCEL_KMH",
"smooth_window": "SMOOTH_WINDOW",
"depart_seconds": "DEPART_SECONDS",
"depart_min_speed": "DEPART_MIN_SPEED",
+1
View File
@@ -10,6 +10,7 @@
"stop_seconds": 4.0,
"speed_threshold": 0,
"min_confidence": 0.7,
"max_accel_kmh": 25,
"smooth_window": 7,
"depart_seconds": 8.0,
"depart_min_speed": 15,