PawAI
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-pythonopencv-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)。
  • 原因 3 (Windows 相機 API): OpenCV 預設呼叫相機的模式在部分 Windows 電腦會 Hang 住。
    • 解法: 在宣告時強制加上 DirectShow 參數:cv2.VideoCapture(0, cv2.CAP_DSHOW)

⚠️ 備註: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 模型 升級:

  1. 選項 A (中階演進 - 資料驅動與深度學習)
    • 棄用寫死的 if-else 閾值。收集 MediaPipe 抓取的 33 個特徵點座標數據,利用 Scikit-learn 訓練 KNN 或隨機森林 (Random Forest) 等分類器。
    • 或升級為直接使用 YOLOv8-Pose / YOLOv11 模型,大幅增強在人群擁擠、物體遮擋與複雜光線下的骨架提取強健度。
  2. 選項 B (高階演進 - 邁向具身智能 Embodied AI)
    • 考量專案最終目標是讓系統與機器人具備自主決策能力,擬評估導入 視覺語言模型 (VLM)視覺語言動作模型 (VLA)
    • 將深度攝影機 (Depth Camera) 獲取的人體動作與空間影像直接作為輸入,運用如 LLaVA-v1.5-7B 或是 MobileVLA-R1 架構。讓模型跳過單純的座標計算,直接「理解」畫面脈絡,做到高階的決策對齊(例如:綜合判斷眼前的人是否跌倒、是否需要協助,並直接輸出對應的機器人動作指令)。

On this page