新增起步門檻 depart_seconds: 處理車陣走走停停

- 兩段停車間的行駛(GPS>0)若不超過 depart_seconds(預設5秒),視為仍在等待,
  連同那段短暫蠕動一起剪掉;唯有行駛超過5秒才算真正起步而保留(含整段)
- 合併改在原始停止段層級進行,取代舊的 merge_gap(post-padding 合併)
- 短暫走停的多個小停(各不到 STOP_SECONDS)合併後可一起構成紅燈被移除
- 已用合成情境驗證: 3秒走停被剪、13秒行駛保留、結尾停車砍到底

本次調整由 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-02 21:01:01 +08:00
parent ca69c04ccb
commit c015061267
3 changed files with 48 additions and 34 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ python auto_remove_redlight.py 01.MOV 03.MOV 05.MOV
| `speed_threshold` | `0` | 時速 ≤ 此值視為停止 |
| `min_confidence` | `0.5` | OCR 信心低於此值視為雜訊 |
| `smooth_window` | `5` | 中位數濾波視窗(取樣點數) |
| `merge_gap` | `3.0` | 相鄰紅燈段間隔小於此秒數即合併 |
| `depart_seconds` | `5.0` | 起步門檻(秒):兩段停車間「持續行駛」需超過此秒數才算真正起步而保留;否則(起步 N 秒內又停)視為仍在等待,連同短暫蠕動一起剪掉(處理車陣走走停停),也順便吸收 OCR 雜訊斷點 |
| `cut_before_stop` | `2.0` | 進入紅燈端(速度到 0)移除起點再往前幾秒,連減速進站一起砍 |
| `keep_after_stop` | `2.0` | 綠燈起步端(速度從 0 開始跑)移除終點提早幾秒,多留卡達起步畫面 |
| `min_keep` | `0.8` | 保留片段短於此秒數即丟棄 |
+46 -32
View File
@@ -101,7 +101,10 @@ MAX_SPEED = 300 # 合理時速上限,超過視為誤判(雜
# - 孤立的雜訊讀數會被周圍的 0 吃掉(中位數)
# - 辨識失敗的空值會用前後最近的有效讀數補上
SMOOTH_WINDOW = 5 # 中位數濾波視窗(取樣點數,奇數)。5 = 看前後各 2 秒
MERGE_GAP = 3.0 # 兩個紅燈移除段若間隔小於此秒數,視為同一段並合併 (秒)
DEPART_SECONDS = 5.0 # 起步門檻(秒): 兩段停車中間「持續行駛(GPS>0)」需超過此
# 秒數,才算真正起步而保留;否則(起步 N 秒內又停)視為
# 仍在等待,連同那段短暫蠕動一起剪掉(處理車陣走走停停)。
# 也順便吸收 OCR 雜訊造成的短暫斷點。
# --- 剪輯 / 輸出參數 ----------------------------------------
# 紅燈移除段的頭尾「不對稱」微調:
@@ -347,48 +350,59 @@ def find_keep_intervals(samples: List[Sample], duration: float, interval: float
smoothed = smooth_speeds(samples, SMOOTH_WINDOW)
stopped = [v <= SPEED_THRESHOLD for v in smoothed]
removals: List[Tuple[float, float]] = []
# 1) 先抓出所有「原始連續停止」區段(不論長短)
raw_runs: List[List[float]] = []
i = 0
while i < n:
if stopped[i]:
# 找出這一段連續停止的範圍 i..j
j = i
while j + 1 < n and stopped[j + 1]:
j += 1
run_start = samples[i].t
# 視為持續到「下一個取樣點」才結束,所以 +interval
run_end = min(samples[j].t + interval, duration)
run_dur = run_end - run_start
# 是否一路停到「影片結尾」(結尾就是 GPS 0)
at_end = run_end >= duration - 1e-3
# 列為移除的條件: 達到紅燈門檻,或停到影片結尾(結尾停車一律砍掉)
if run_dur >= STOP_SECONDS or at_end:
# 進入紅燈端往前多砍 CUT_BEFORE_STOP 秒(連減速進站一起砍)。
rstart = max(0.0, run_start - CUT_BEFORE_STOP)
if at_end:
# 停到影片結尾: 沒有起步要留 → 砍到底,不保留 KEEP_AFTER_STOP
rend = run_end
else:
# 一般紅燈: 綠燈起步端提早 KEEP_AFTER_STOP 秒結束(多留卡達起步)
rend = run_end - KEEP_AFTER_STOP
if rend > rstart:
removals.append((rstart, rend))
run_end = min(samples[j].t + interval, duration) # 持續到個取樣點
raw_runs.append([run_start, run_end])
i = j + 1
else:
i += 1
# 合併「間隔很短」的相鄰移除段(平滑後仍可能殘留 1~2 格雜訊把一個紅燈切兩半)
if removals:
merged: List[Tuple[float, float]] = [removals[0]]
for (rs, re) in removals[1:]:
if rs - merged[-1][1] < MERGE_GAP:
merged[-1] = (merged[-1][0], re) # 接起來
# 2) 合併「中間行駛(GPS>0)不超過 DEPART_SECONDS 秒」的相鄰停止段:
# 起步後 N 秒內又停 → 視為仍在等待(車陣走走停停),連那段短暫蠕動一起算停止。
# 只有行駛「超過」DEPART_SECONDS 才算真正起步,該行駛段才會被保留。
merged_runs: List[List[float]] = []
for run in raw_runs:
if merged_runs and (run[0] - merged_runs[-1][1]) <= DEPART_SECONDS:
merged_runs[-1][1] = run[1] # 行駛 <= 門檻 → 併入前一段停止
else:
merged_runs.append(list(run))
# 3) 套用門檻與頭尾偏移 → 產生移除區段
removals: List[Tuple[float, float]] = []
for (run_start, run_end) in merged_runs:
run_dur = run_end - run_start
at_end = run_end >= duration - 1e-3 # 是否一路停到影片結尾
# 列為移除: 達到紅燈門檻,或停到影片結尾(結尾停車一律砍掉)
if run_dur >= STOP_SECONDS or at_end:
# 進入紅燈端往前多砍 CUT_BEFORE_STOP 秒(連減速進站一起砍)。
rstart = max(0.0, run_start - CUT_BEFORE_STOP)
if at_end:
# 停到影片結尾: 沒有起步要留 → 砍到底,不保留 KEEP_AFTER_STOP
rend = run_end
else:
merged.append((rs, re))
removals = merged
# 一般紅燈: 綠燈起步端提早 KEEP_AFTER_STOP 秒結束(多留卡達起步)
rend = run_end - KEEP_AFTER_STOP
if rend > rstart:
removals.append((rstart, rend))
# 4) 頭尾偏移後若仍有相鄰移除段重疊/相接,合併之
if removals:
merged: List[List[float]] = [list(removals[0])]
for (rs, re) in removals[1:]:
if rs <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], re)
else:
merged.append([rs, re])
removals = [(a, b) for a, b in merged]
# keeps = 全片 [0, duration] 扣掉所有 removals
keeps: List[Tuple[float, float]] = []
@@ -951,7 +965,7 @@ _CONFIG_KEYS = {
"speed_threshold": "SPEED_THRESHOLD",
"min_confidence": "MIN_CONFIDENCE",
"smooth_window": "SMOOTH_WINDOW",
"merge_gap": "MERGE_GAP",
"depart_seconds": "DEPART_SECONDS",
"cut_before_stop": "CUT_BEFORE_STOP",
"keep_after_stop": "KEEP_AFTER_STOP",
"min_keep": "MIN_KEEP",
+1 -1
View File
@@ -11,7 +11,7 @@
"speed_threshold": 0,
"min_confidence": 0.5,
"smooth_window": 5,
"merge_gap": 3.0,
"depart_seconds": 5.0,
"cut_before_stop": 2.0,
"keep_after_stop": 2.0,
"min_keep": 0.8,