PawAI
姿勢辨識開發歷程總整理
專案開發紀錄:基於視覺的即時人體動作感知模組 (Real-time Pose & Action Detection)
1. 專案目標與背景
本模組旨在透過一般 WebCam 攝影機,即時擷取人體的骨架關鍵點(Landmarks),並透過幾何運算與時間序列過濾,辨識出使用者的「靜態姿勢」與「動態動作」。
此感知模組為前端輸入節點,當成功捕捉到穩定動作後,未來預計將狀態打包成 JSON 格式,透過 ROS2 傳遞給後端的系統或決策大腦(如 VLM/VLA 模型),賦予機器人或系統視覺理解能力。
2. 使用技術棧 (Tech Stack)
- 語言: Python 3.9+
- 環境管理: Conda (Virtual Environment), pip
- 核心套件:
opencv-python: 負責存取硬體攝影機、影像色彩轉換、視窗縮放控制與 UI 畫面繪製。mediapipe(v0.10.14): Google 開源的高效能跨平台機器學習框架,負責提取 33 個人體骨架 3D 關鍵點。numpy(v1.26.4): 負責底層關鍵點座標的矩陣與幾何向量運算。
- 標準函式庫:
time(時間濾波),math(歐氏距離與三角函數計算),collections.deque(時間平滑滑動視窗)。
3. 目前功能與限制
A. 支援姿勢與狀態映射 (Pose Emoji Mapping)
系統目前定義並分類以下 8 種行為狀態:
standing: 🧍 (站立)sitting: 🪑 (坐下)crouching: 🏋️ (蹲下)bending: 🙇 (彎腰)hands_on_hips: 🦸 (手插腰)kneeling_one_knee: 🧎 (單膝跪地)fallen: ⚠️ (跌倒)unknown: 🧍 (未知)
B. 模組穩定度評估
- 🟢 高穩定度狀態:
standing(站立)、crouching(蹲下)、sitting(坐下)、bending( 彎腰)、fallen(跌倒)。此類動作幾何特徵鮮明,判定率高。 - 🟡 待優化/不穩定狀態:
hands_on_hips(手插腰)、kneeling_one_knee(單膝跪地)。易受正面視角壓縮或特殊衣物遮擋干擾,仍有小幅波動風險。
C. 現階段系統限制
- 視角依賴性:受限於 2D 影像投影,身體轉向側面(Side Profile) 時,關節間的幾何角度與深度落差最為清晰,系統會比較容易且精準地偵測到動作。
- 時間特徵延遲:為了過濾環境雜訊與極短暫的晃動,系統設有時間平滑與防抖機制,使用者必須維持該姿勢數秒(數幀)以上,系統才會正式確認並變更當前狀態。
- 風險控管機制(跌倒安全鎖):目前因為自然活動中(如深度彎腰、突發大動作)引發「跌倒 (Fallen)」狀態的誤觸頻率仍然偏高。為避免造成後端系統的警報疲勞,目前偵測到跌倒後先不觸發後續的自動警示反應,僅於畫面上做視覺 log 紀錄,待後續結合大腦(VLM)進行多模態複驗。
4. 開發歷程與踩坑紀錄 (Troubleshooting)
問題一:套件已安裝卻出現 ModuleNotFoundError
- 原因: 終端機(執行環境)與 VS Code 右下角的 Python 解譯器(Interpreter)不一致。套件安裝在 Conda 的虛擬環境中,但執行時卻使用了全域 (base) 的 Python。
- 解法: 確保在終端機執行
conda activate pose_test,並確認指令行前方有出現(pose_test)標籤;若使用 PowerShell 無法啟動,則切換至 Command Prompt (cmd)。
問題二:numpy.core.multiarray failed to import (底層 C 語言報錯)
- 原因: 依賴衝突 (Dependency Conflict)。最新的 NumPy 2.x 版本進行了底層 C API 大改版,導致舊版或預設版本的 OpenCV 與 MediaPipe 底層 C++ 引擎無法相容。
- 解法: 將 NumPy 強制降級回 1.x 的最後穩定版本:
pip install numpy==1.26.4。
問題三:Windows 系統下攝影機卡死或程式無聲崩潰 (Silent Crash)
- 原因 1 (套件衝突): 環境中同時存在
opencv-python與opencv-contrib-python。兩者在呼叫import cv2時產生 Namespace 衝突,導致系統崩潰且不輸出任何報錯。- 解法:
pip uninstall opencv-python opencv-contrib-python -y,然後僅透過安裝mediapipe讓其自動拉取正確的相依套件。
- 解法:
- 原因 2 (MediaPipe 版本): MediaPipe 版本太新 (如 0.10.32+),其預設依賴與降級後的 NumPy 1.x 產生二次衝突。
- 解法: 將 MediaPipe 降級至相容版本:
pip install mediapipe==0.10.14。
- 解法: 將 MediaPipe 降級至相容版本:
- 原因 3 (Windows 相機 API): OpenCV 預設呼叫相機的模式在部分 Windows 電腦上會因為硬體抽象層等待而 Hang 住。
- 解法: 在宣告時強制加上 DirectShow 參數:
cv2.VideoCapture(0, cv2.CAP_DSHOW)。
- 解法: 在宣告時強制加上 DirectShow 參數:
問題四:正面視角投影壓縮,導致「插腰 (Hands on hips)」頻繁落入「站立 (Standing)」
- 原因: 舊版邏輯依賴手肘(Elbow)在 X 軸方向的外移距離來判斷插腰。當使用者正對鏡頭時,3D 空間的深度在 2D 畫面上被嚴重壓縮,導致 X 軸特徵極不明顯而觸發失敗。
- 解法: 放棄 X 軸手肘位移判斷。改用 「手腕高度接近髖部 Y 座標」 結合 「雙手肘彎曲角度限制(elbow_angle < 145°)」。由於自然垂手時手肘接近打直(≈ 160–180°),加入手肘彎曲角能精準區分垂手與插腰。
問題五:單膝跪地(Kneeling on one knee)與插腰、蹲姿發生嚴重「搶判斷」
- 原因 1: 做單膝跪姿時,使用者雙手會自然靠近髖部或大腿,導致插腰(Hands on hips)的幾何條件先被觸發,且原本插腰優先順序排在跪姿前面,導致誤判。
- 原因 2: 跪地側的腳踝(Ankle)極易因為小腿重疊、地面阻擋或褲子而產生遮擋(Occlusion),導致 MediaPipe 座標盲猜或產生雜訊,造成原本只靠兩膝 Y 軸高低差的邏輯在「蹲下」時也會誤觸。
- 解法:
- 調整優先權:將
kneeling_one_knee的判斷順序調至hands_on_hips之前。 - 全局蹲姿預判:在進入這兩個狀態判斷前,先計算
is_crouching_likely(髖角 < 145 且 膝角 < 130)。只要判定極可能是蹲姿,就直接跳過跪地與插腰的檢查。 - 多條件聯防:針對跪地腿引入三選一觸發機制:
- 跪地側腳踝可見度(visibility)低於 0.45(代表被遮擋)。
- 腳背躺平貼地(腳踝 Y 座標與膝蓋 Y 座標高度差小於 0.08)。
- 該側膝角明顯彎曲(< 140°),且與站立腳的膝角差距大於 25°(徹底排除蹲姿)。
- 調整優先權:將
5. 核心邏輯與演算法設計
本專案採用「啟發式規則 (Heuristic Rules)」進行多階層姿勢判定,具體架構與排除邏輯如下:
A. 幾何特徵計算
- 三點夾角 (
angle_between): 利用 NumPy 進行向量點積與範數運算,計算關節彎曲度(如手肘、膝蓋、髖部)。 - 軀幹傾角 (
trunk_angle_deg): 計算左右肩膀中點與左右髖部中點連線,與絕對垂直線(Vector[0, -1])的夾角,用以判斷前傾或跌倒。
B. 姿勢分類器狀態機優先順序
程式在每幀(Frame)會依序掃描以下條件,一旦滿足即跳出:
- Fallen (跌倒): 軀幹傾角大於 60°(接近水平)。
- Kneeling on one knee (單膝跪地):
- 前置排除:非蹲姿。
- 特徵:兩膝 Y 軸高低差 > 0.07,站立腳挺直(> 130°),跪地側滿足「腳踝被遮擋」、「腳背貼地」或「膝角極度彎曲」之一,且兩腳膝角有顯著代數差(> 25°)。
- Hands on hips (雙手叉腰):
- 前置排除:非蹲姿。
- 特徵:雙手腕 Y 軸逼近同側髖部(落差 < 0.10),手腕 X 軸縮在肩膀與髖部合理框內,雙手肘皆有明顯彎曲(< 145°),雙膝打直(> 145°,排除坐姿手放腿上)。
- Standing (站立): 髖角 > 155°、膝角 > 155° 且軀幹直立(傾角 < 30°)。
- Sitting (坐姿): 臀部 Y 軸高度明顯高於踝部(落差 > 0.10),臀膝高度相近(落差 < 0.12)或膝蓋高於臀部(矮凳),軀幹直立且膝角彎曲(< 145°)。
- Crouching (蹲姿): 髖角 < 145° 且膝角 < 125°。
- Bending (彎腰): 軀幹前傾(傾角 > 30°),但雙膝保持相對挺直(> 130°),明確排除蹲姿。
C. 時間序列平滑處理 (Smoothing)
- 滑動多數決 (Sliding Window Majority Vote): 為了防止影像環境雜訊導致狀態高頻閃爍(Flickering),使用
collections.deque(maxlen=5)儲存最近 5 幀的原始辨識結果,並取出現次數最多(Mode)的狀態作為最終輸出的穩定狀態。