PawAI
語音開發歷程總整理
姿勢辨識
建立時間: 2026年3月15日 上午11:02 狀態: 專題
專案開發紀錄:基於視覺的即時人體動作感知模組 (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: Google 開源的高效能跨平台機器學習框架,負責提取 33 個人體骨架 3D 關鍵點與臉部特徵。
- 標準函式庫:
time(時間濾波),math(歐式距離計算),collections.deque(動態軌跡記憶)。
🐛 3. 開發歷程與踩坑紀錄 (Troubleshooting)
在建置環境與執行的過程中,遭遇了幾個經典的 Python 環境衝突與 Windows 底層問題,以下為解決方案紀錄:
❌ 問題一:套件已安裝卻出現 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.0 版本大改版,導致舊版或預設版本的 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 不相容。- 解法: 降級 MediaPipe (
pip install mediapipe==0.10.14)。
- 解法: 降級 MediaPipe (
- 原因 3 (Windows 相機 API): OpenCV 預設呼叫相機的模式在部分 Windows 電腦會 Hang 住。
- 解法: 在宣告時強制加上 DirectShow 參數:
cv2.VideoCapture(0, cv2.CAP_DSHOW)。
- 解法: 在宣告時強制加上 DirectShow 參數:
⚠️ 備註:Pylance 的假警報
- VS Code 的 Pylance 語法檢查器會對
mp.solutions報錯(找不到屬性)。這是因為 MediaPipe 底層物件為動態生成,屬於編輯器解析限制,不影響程式實際執行,可直接忽略。
🧠 4. 核心邏輯與演算法設計
本專案採用**「啟發式規則 (Heuristic Rules)」進行動作判定,並加入防抖與抗干擾機制**:
A. 靜態姿勢 (基於瞬間幾何距離)
- 拍手 (Clap): 計算左右手腕 (Wrist) 的直線距離,低於閾值即觸發。
- 舉手 (Raise Hand / Hands_Up): 判斷手腕的 Y 座標是否小於(高於)肩膀 (Shoulder) 的 Y 座標。
- 微笑 (Smile) [抗遮擋升級版]:
- 原始邏輯使用嘴角寬度除以耳朵寬度。但考量到**「頭髮/瀏海遮擋」**會導致 MediaPipe 對耳朵座標進行盲猜而產生數值抖動,進而引發誤判。
- 優化方案: 改用**「雙眼外側 (Eye Outer)」**的距離作為穩定的分母比例尺,大幅提升實用性。
B. 動態動作 (基於時間序列與雙向佇列)
使用 collections.deque 賦予程式短暫記憶力。
- 揮手 (Wave): 條件觸發前提為「手已舉起」。利用 deque 紀錄過去 15 幀手腕的 X 座標,當陣列中的 Max 與 Min X 座標差距大於設定閾值(代表左右擺動),即判定為揮手。
- 跳躍 (Hop): 紀錄過去 10 幀的臀部 (Hip) Y 座標。若最舊的座標與最新座標差距大於設定閾值(瞬間位移),即判定為跳躍。
C. 狀態機與防抖機制 (Debounce)
連續的高頻影像判斷極易產生狀態閃爍(Flickering)。
- 機制: 宣告
pending_action與計時器。任何偵測到的動作都必須連續穩定維持超過0.5秒 (DEBOUNCE_TIME),才會被正式寫入current_state並觸發輸出。
💻 5. 終極完整版程式碼 (posetest.py)
import cv2
import mediapipe as mp
import time
import math
from collections import deque
print("👉 程式開始執行,載入終極動作感知包...")
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils
# 嘗試打開鏡頭 (加入 CAP_DSHOW 防卡死)
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
print("❌ 慘了,Python 讀不到鏡頭!請確認權限或有無被其他程式佔用。")
else:
print("✅ 鏡頭成功打開!準備進入辨識迴圈...")
# 狀態與防抖 (Cooldown) 變數
current_state = "Idle"
pending_action = "Idle"
pending_start_time = time.time()
DEBOUNCE_TIME = 0.5 # 動作必須維持 0.5 秒才會被正式確認
# 動態記憶體:記錄過去 15 幀的手腕 X 座標 (揮手) 與 10 幀的臀部 Y 座標 (跳躍)
right_wrist_x_history = deque(maxlen=15)
hip_y_history = deque(maxlen=10)
with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 轉換色彩空間給 MediaPipe
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
image.flags.writeable = False
results = pose.process(image)
# 轉回給 OpenCV 顯示
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
detected_action = "Idle"
if results.pose_landmarks:
# 畫出骨架
mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
landmarks = results.pose_landmarks.landmark
# --- 抓取身體關鍵點 ---
l_shldr_y = landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y
r_shldr_y = landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y
l_wrist = landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value]
r_wrist = landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value]
l_hip_y = landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y
r_hip_y = landmarks[mp_pose.PoseLandmark.RIGHT_HIP.value].y
l_knee_y = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y
r_knee_y = landmarks[mp_pose.PoseLandmark.RIGHT_KNEE.value].y
# --- 抓取臉部關鍵點 (防瀏海:改用眼睛代替耳朵) ---
mouth_l = landmarks[mp_pose.PoseLandmark.MOUTH_LEFT.value]
mouth_r = landmarks[mp_pose.PoseLandmark.MOUTH_RIGHT.value]
eye_l = landmarks[mp_pose.PoseLandmark.LEFT_EYE_OUTER.value]
eye_r = landmarks[mp_pose.PoseLandmark.RIGHT_EYE_OUTER.value]
# ================= 動作判斷邏輯開始 =================
# 1. 微笑 (Smile):嘴角寬度 vs 雙眼寬度
mouth_width = math.hypot(mouth_l.x - mouth_r.x, mouth_l.y - mouth_r.y)
eye_width = math.hypot(eye_l.x - eye_r.x, eye_l.y - eye_r.y)
if eye_width > 0:
smile_ratio = mouth_width / eye_width
if smile_ratio > 0.8:
detected_action = "Smile"
# 2. 拍手 (Clap)
wrist_dist = math.hypot(l_wrist.x - r_wrist.x, l_wrist.y - r_wrist.y)
if wrist_dist < 0.08:
detected_action = "Clap"
# 3. 舉手系列
elif l_wrist.y < l_shldr_y and r_wrist.y < r_shldr_y:
detected_action = "Hands_Up"
elif l_wrist.y < l_shldr_y and r_wrist.y > r_shldr_y:
detected_action = "Raise_Left_Hand"
elif r_wrist.y < r_shldr_y and l_wrist.y > l_shldr_y:
detected_action = "Raise_Right_Hand"
# 4. 揮手 (Wave)
if r_wrist.y < r_shldr_y:
right_wrist_x_history.append(r_wrist.x)
if len(right_wrist_x_history) == 15 and (max(right_wrist_x_history) - min(right_wrist_x_history) > 0.15):
detected_action = "Wave"
else:
right_wrist_x_history.clear()
# 5. 蹲下 (Squat)
if abs(l_hip_y - l_knee_y) < 0.15 and abs(r_hip_y - r_knee_y) < 0.15:
detected_action = "Squat"
# 6. 向上跳 (Hop)
hip_y_history.append(l_hip_y)
if len(hip_y_history) == 10 and (hip_y_history[0] - l_hip_y > 0.08):
detected_action = "Hop"
# ================= 時間濾波 (防抖機制) =================
if detected_action == pending_action:
if (time.time() - pending_start_time) >= DEBOUNCE_TIME:
if current_state != detected_action:
current_state = detected_action
if current_state != "Idle":
print(f"✅ 系統確認觸發穩定動作:{current_state}")
else:
pending_action = detected_action
pending_start_time = time.time()
cv2.putText(image, f"Status: {current_state}", (10, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('PawAI Pose Testing', image)
# ================= 視窗關閉機制 =================
if cv2.waitKey(10) & 0xFF == ord('q'): break
if cv2.getWindowProperty('PawAI Pose Testing', cv2.WND_PROP_VISIBLE) < 1: break
cap.release()
cv2.destroyAllWindows()終端機測試紀錄顯示,模組已能穩定辨識複雜的動態與靜態動作組合,防抖機制運作正常:
📈 6. 系統執行與測試結果
終端機測試紀錄顯示,模組已能穩定辨識複雜的動態與靜態動作組合,防抖機制運作正常:
(pose_test) PS C:\\Users\\peich\\project\\elder_and_dog> python posetest.py
👉 程式開始執行,載入終極動作感知包...
✅ 鏡頭成功打開!準備進入辨識迴圈...
...
✅ 系統確認觸發穩定動作:Hands_Up
✅ 系統確認觸發穩定動作:Hop
✅ 系統確認觸發穩定動作:Clap
✅ 系統確認觸發穩定動作:Raise_Left_Hand
✅ 系統確認觸發穩定動作:Squat
✅ 系統確認觸發穩定動作:Wave🚀 7. 下一步技術藍圖 (Next Steps)
目前的架構為「啟發式規則 (Heuristic Rules)」主導,雖然輕量且即時,但對於視角變化或不同身型使用者的泛化能力 (Generalization) 較弱。
為接軌更進階的系統應用與機器人感知決策能力,下一步計畫將朝向資料驅動 (Data-driven) 或 端到端 AI 模型 升級:
- 選項 A (中階演進 - 資料驅動與深度學習):
- 棄用寫死的
if-else閾值。收集 MediaPipe 抓取的 33 個特徵點座標數據,利用Scikit-learn訓練 KNN 或隨機森林 (Random Forest) 等分類器。 - 或升級為直接使用 YOLOv8-Pose / YOLOv11 模型,大幅增強在人群擁擠、物體遮擋與複雜光線下的骨架提取強健度。
- 棄用寫死的
- 選項 B (高階演進 - 邁向具身智能 Embodied AI):
- 考量專案最終目標是讓系統與機器人具備自主決策能力,擬評估導入 視覺語言模型 (VLM) 或 視覺語言動作模型 (VLA)。
- 將深度攝影機 (Depth Camera) 獲取的人體動作與空間影像直接作為輸入,運用如 LLaVA-v1.5-7B 或是 MobileVLA-R1 架構。讓模型跳過單純的座標計算,直接「理解」畫面脈絡,做到高階的決策對齊(例如:綜合判斷眼前的人是否跌倒、是否需要協助,並直接輸出對應的機器人動作指令)。