2026年10月17日 星期六

CNC 錄片影像與狀態文字融合:方式、步驟與注意事項

CNC 錄片「影像 + 狀態文字」融合:方式、步驟與注意事項

一、要做的事

把 CNC 執行時的控制面板狀態(log)疊在 webcam 錄片上,變成單一支「畫面 + 即時狀態字幕」的影片。
狀態文字包含:Run / Idle / Alarm、MPos 座標、FS 進給速度、SD 目前指令、Ov 倍率等,每 0.4 秒更新一次。

二、融合方式總覽

                 ┌──────────────────────────────────────────┐
  webcam ──RTSP──►  Pi4 (mediamtx) ──RTSP──► Pi4 (monitor)
                 │                                        │
  ESP32 FluidNC ──WebSocket(即時狀態)─► monitor 同步記 log
                 │                                        │
                 └────────► 收檔 run_YYYYMMDD_HHMMSS.mp4
                                 + .mp4.log (每 0.4s 一行的狀態)
  • 錄製端(Pi4):monitor.py 同時抓兩路——RTSP 影像串流寫 MP4,WebSocket 狀態寫成時間軸 log。
  • 收檔觸發:偵測到 Run/Hold/Alarm 開始錄,回 Idle 8 秒確認後收檔。
  • 回傳/疊字端(GX10):Pi4 的 sync timer 每 2 分鐘掃一次,把新錄片 + log scp 到 GX10 /home/gx/cnc_records/,校驗大小一致後 Pi4 端直接刪除(不留檔,檔案只存在 GX10),並自動跑 stitch.py 疊字。


三、stitch.py 如何把 log 融進影片(技術)

  1. 不用「切段再拼」:ffmpeg -ss 切段 drawtext → concat 的方式,seek 是跳到關鍵幀,每段最多漂 1~2 秒,字幕會「跟不上畫面」。
  2. 改用 ASS 字幕單次 pass:每行 log → 一條 Dialogue(起始/结束時間戳),ffmpeg -i in.mp4 -vf ass=subs.ass 一次解碼一次編碼就疊完,逐幀精確。
  3. 每行 log 都是字幕:161 行 log = 161 條字幕,MPos 數字隨機器逐行跳動,不會被「同狀態 5 秒合併」規則吃掉的訊息。
  4. 時基校正 (L2V_OFF = 2.7s):影片走 webcam → mediamtx → RTSP → ffmpeg,有固定的串流累積延遲(本環境實測 2.7 秒,用圓弧起點兩點對齊 + 像素運動法交叉驗證)。所有字幕時間戳統一 +2.7s。
  5. 結尾裁定 (TAIL = 0.5s):錄片尾段有 mediamtx 的凍住緩衝幀(收檔瞬間 ffmpeg 還在吐尾),cut = min(log_tstop + L2V_OFF + TAIL, 影片長度),ffmpeg -t cut 直接裁掉凍尾。
  6. ASS 時間戳格式:HH:MM:SS.cc(ASS 用 1/100 秒的兩位小數)。65 秒 = 00:01:05.00,不是 00:65.00——寫錯的話 ASS 會把「分」和「秒」搞亂,整支片時間軸錯位。

四、一次完整流程

[1] 執行工: ssh Pi4 → 透過 curl 對 FluidNC 下 $LocalFS/Run=<檔>.gcode 指令要包含$跟gcode要對
[2] Pi4 monitor 偵測到 Run,自動起錄 webcam + WebSocket 狀態 log
[3] 機器回 Idle → 8s 後收檔 recordings/run_*.mp4 + .mp4.log
[4] cnc-sync.timer (每 2 min) 掃 recordings/ → 校驗+回傳 GX10
[5] 回傳成功 → Pi4 端刪除; 失敗 → 保留等待下輪
[6] GX10 自動跑 stitch.py,產出 run_*_annotated.mp4 (同目錄)
[7] 收片於 GX10,需要就自取,不要就手動刪

五、注意事項 (踩過的坑)

1. 時基 (最重要)

  • 串流是慢的,log 是即時的——兩個時基不對齊,字幕就會「跟不上」或「超前」。
  • 換硬體/換路由器/改 mediamtx 設定後,L2V_OFF 要重新實測。
  • 實測方法:在 log 挑一個 MPos 已知的點(例如圓弧起點),用 ffmpeg -filter:v select='gt(scene,0.03)' 或逐 1 秒抽幀比像素差,找到畫面第一次出現該位置對應的時間,差值就是偏移。

2. ASS 字幕格式

  • 每一行 log = 一條 Dialogue,[start, end) 半開區間,相切不重疊。
  • 文字中的 : 是 ASS 分隔符,若文字本身要出現冒號要轉義;我們直接用半形冒號無事,遇到 : 分隔符要留意。
  • 過長行 (>64 char) 會被截斷加 …,避免單行跨兩行顯示。

3. 不要切段拼

  • drawtext -ss 切段 → concat demuxer → -c copy 看似省事,seek 會跳到關鍵幀,每段都會飄。
  • 用 ass 單次 pass,一次解碼一次編碼,零飄移,還比較快。

4. Pi4 端不要留錄片

  • 回傳校驗 size 一致 + 已落 GX10 → Pi4 端直接 rm。
  • 校驗不符 / scp 失敗 → 保留 Pi4 原檔,標記下輪重傳,絕不能雙邊都刪。
  • 收檔剛完成的檔案 (<30s) timer 會跳過(避免抓到寫入中)——這是 monitor 端 recorder_active flag 與 sync 端的約定。

5. 結尾裁定

  • mediamtx 在收檔瞬間還會吐 3~6 秒緩衝凍幀,不做 ffmpeg -t cut 會看到「畫面停了、文字還在跳」的殘影。
  • cut 用 min(log_tstop + L2V_OFF + 0.5s, 影片長度),不要超過影片實際長度(ffmpeg 會報錯)。

6. log 格式

  • START epoch=<t> / <offset> <state> <detail> / END 三行結構。
  • 解析 regex ^([\d.]+)\s+(\S+)\s*(.*)$,第一欄是「距 START 的偏移秒」不是絕對時間。
  • 若改 log 寫入格式,stitch.py 的 parse_log 要同步改。

7. 不要雙邊都跑 stitch

  • stitch 只在 GX10 跑一次,Pi4 只負責同步回傳。
  • 若 Pi4 也跑了 stitch,雙邊會產生同一個 _annotated.mp4,回傳時以 GX10 版本為準。

8. 時間戳顯示

  • 65 秒是 00:01:05,不是 00:65——ASS 的 HH:MM:SS.cc 是「小時:分鐘:秒.百分秒」,把 65 秒當秒寫進去會被解讀成 64 分鐘 + 5 秒,整支片時間軸錯位 63 分鐘以上。
  • 寫 ts(sec) 工具時:h=sec//3600, m=(sec%3600)//60, s=sec%60,不要用 %60 套在 m 上(會把 65 秒寫成 00:65.00,ASS 解讀成 0 分 65 秒,等於 1 分 5 秒,跟預期差 53 秒)。

9. 收片位置

  • 原版 run_*.mp4、log run_*.mp4.log、疊字版 run_*_annotated.mp4 三者都在 /home/gx/cnc_records/,自取即可。
  • Pi4 端 recordings/ 不應留任何 mp4(只有 trace.log 是正常)。

六、驗證清單 (每支片收工前)

項 驗法
機器跑完整支 gcode log 最後有 Idle MPos:0.000,0.000,0.000
字幕不重疊 awk 掃 ass 檔,end_i < start_{i+1}
結尾 = Idle MPos:0,0,0 抽最後 3 幀看 MPos
結尾 = 圓完整 抽最後 1 幀看圓弧
中途不跳 抽 30% / 60% / 90% 三幀,每幀 MPos 跟 log 對應行一致
Pi4 端乾淨 ls ~/cnc-monitor/recordings/*.mp4 無結果
GX10 三件齊 run_*.mp4 + run_*.mp4.log + run_*_annotated.mp4 都在

七、文件位置

檔 位置
monitor.py (錄製 + log) Pi4 ~/cnc-monitor/monitor.py
stitch.py (疊字) GX10 /home/gx/cnc_records/stitch.py
sync_rec.sh (回傳 + 清理) Pi4 ~/cnc-monitor/sync_rec.sh
錄片 + log + annotated GX10 /home/gx/cnc_records/
本文 WP http://localhost:8082/ (區網)

沒有留言:

張貼留言