首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Kinect for Windows SDK 開發入門(九):骨骼追蹤進階(下)─ 深度互動與姿勢識別實戰

     編輯:關於.NET

前言

承接上一篇「骨骼追蹤進階(上)」,本文將深入探討如何善用骨骼資料中的深度值(Z 軸)打造更立體的互動體驗,並詳細介紹姿勢識別的核心演算法與實作技巧。最後我們將把前一篇的「西蒙說」遊戲升級,改用姿勢識別取代簡單的碰撞測試,展示更自然、更豐富的自然用戶介面(NUI)開發模式。

一、 基於深度資料的使用者互動

1.1 為什麼需要 Z 軸?

過去幾篇文章中,我們僅使用關節點的 X、Y 座標進行二維定位。但 Kinect 提供的每個關節點都包含深度資訊(Z 值),這正是 Kinect 與傳統滑鼠、觸控板最大的差異所在。善用深度值,能讓應用程式呈現偽 3D 效果、根據遠近縮放 UI 元素,甚至實現真正的立體互動邏輯。

1.2 利用 Canvas.ZIndex 打造層次感

WPF 的 Canvas 容器支援 Canvas.ZIndex 屬性,可控制元素在垂直於螢幕方向的堆疊順序。ZIndex 越大,視覺上越接近使用者。我們可以將 Kinect 的深度值(最大約 13.4 英尺)反向映射為 0~1340 的 ZIndex 範圍:

int zIndex = (int)(1340 - depthInFeet * 100);

如此一來,當使用者的手靠近 Kinect 時,對應的手型圖示會浮現在參考圓環的前方;手遠離時則沉入後方,直觀呈現空間深度。

1.3 根據深度動態縮放手型圖示

除了層次,深度值還可驅動 ScaleTransform 縮放手型圖示。使用者手部越靠近感測器,圖示越大,彷彿真實的手在眼前放大;反之則縮小。核心程式碼如下:

private void TrackHand(Joint hand, FrameworkElement cursorElement,
    ScaleTransform cursorScale, FrameworkElement container, bool isLeft)
{
    if (hand.TrackingState != JointTrackingState.NotTracked)
    {
        double z = hand.Position.Z * FeetPerMeters; // 轉換為英尺
        cursorElement.Visibility = Visibility.Visible;

        // 將關節點座標映射至 UI 容器
        Point cursorCenter = new Point(cursorElement.ActualWidth / 2.0,
                                       cursorElement.ActualHeight / 2.0);
        Point jointPoint = GetJointPoint(this.KinectDevice, hand,
                                         container.RenderSize, cursorCenter);
        Canvas.SetLeft(cursorElement, jointPoint.X);
        Canvas.SetTop(cursorElement, jointPoint.Y);

        // 設定 ZIndex:深度越小(越近)ZIndex 越大
        Canvas.SetZIndex(cursorElement, (int)(1200 - (z * 100)));

        // 縮放比例:12 / z 可根據實際測試微調
        cursorScale.ScaleX = 12 / z * (isLeft ? -1 : 1); // 左手鏡像
        cursorScale.ScaleY = 12 / z;

        // 偵錯用:顯示雙手深度
        if (hand.JointType == JointType.HandLeft)
            DebugLeftHand.Text = String.Format("左手: {0:0.00} 英尺", z);
        else
            DebugRightHand.Text = String.Format("右手: {0:0.00} 英尺", z);
    }
    else
    {
        DebugLeftHand.Text = DebugRightHand.Text = String.Empty;
    }
}

深度互動示意圖 1 深度互動示意圖 2

開發提示: 縮放比例(12 / z)需經實測調整,以達到「手型圖示大小 ≈ 使用者實際手掌大小」的最佳體驗。若需更精確的手部尺寸,可考慮搭配感應手套等外部裝置。

二、 姿勢識別核心觀念與演算法

2.1 姿勢 vs. 手勢

  • 姿勢:身體保持特定靜態配置一段時間(如雙臂張開、立正)。
  • 手勢:包含動態軌跡的動作(如揮手、捏合縮放)。

姿勢識別相對單純、穩定,適合作為遊戲指令、選單觸發等離散式互動;手勢識別則適合連續操控。本文聚焦於姿勢識別。

2.2 兩大主流識別策略

策略 A:關節點交叉/距離判斷

判斷兩個關節點(如手腕與髖關節)的歐氏距離是否小於門檻值。適用於「叉腰」、「雙手舉高」等相對位置固定的姿勢。

  • 優點:邏輯直觀,計算量小。
  • 缺點:受雜訊影響大;當關節點靠近(如手遮臉)時,骨骼引擎易誤判,難以區分「手在臉前」、「手摸耳朵」等相似姿勢。

策略 B:關節點夾角計算(三角幾何)

任取三個關節點構成三角形,利用餘弦定理計算夾角。例如「展示肱二頭肌」姿勢:肩─肘─腕形成銳角三角形,夾角特徵明顯。

餘弦定理公式 肱二頭肌姿勢三角形示意

夾角計算公式:

cos θ = (a² + b² - c²) / (2ab)
θ = acos(cos θ) * 180 / π

實作時可採「三點法」(直接用三關節點)或「兩點法」(以肘為原點,另取一水平參考點),視姿勢限制條件而定。

2.3 容差機制:閾值範圍判斷

使用者無法完美複製標準姿勢,因此需設定 Threshold 容許誤差。判斷邏輯:

bool isMatch = (targetAngle - threshold <= actualAngle) 
              && (actualAngle <= targetAngle + threshold);

並處理角度跨越 0°/360° 邊界的情況(如 350° 與 10° 實際僅差 20°)。

三、 實戰:將「西蒙說」遊戲升級為姿勢識別版

3.1 定義姿勢資料結構

public class PoseAngle
{
    public JointType CenterJoint { get; private set; } // 夾角頂點
    public JointType AngleJoint  { get; private set; } // 另一邊關節
    public double Angle { get; private set; }          // 目標夾角
    public double Threshold { get; private set; }      // 容許誤差

    public PoseAngle(JointType center, JointType angle, double a, double t)
    {
        CenterJoint = center; AngleJoint = angle; Angle = a; Threshold = t;
    }
}

public struct Pose
{
    public string Title;
    public PoseAngle[] Angles;
}

3.2 建立姿勢庫

private Pose[] poseLibrary;
private Pose startPose;

private void PopulatePoseLibrary()
{
    poseLibrary = new Pose[4];
    // 起始姿勢:T 型雙臂展開
    startPose = new Pose { Title = "Start Pose", Angles = new PoseAngle[4] };
    startPose.Angles[0] = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    startPose.Angles[1] = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 180, 20);
    startPose.Angles[2] = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    startPose.Angles[3] = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 0, 20);

    // 姿勢 1:雙手舉高
    poseLibrary[0] = new Pose { Title = "舉起手來", Angles = new PoseAngle[4] };
    poseLibrary[0].Angles[0] = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    poseLibrary[0].Angles[1] = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 90, 20);
    poseLibrary[0].Angles[2] = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    poseLibrary[0].Angles[3] = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 90, 20);

    // 姿勢 2:雙手放下
    poseLibrary[1] = new Pose { Title = "把手放下來", Angles = new PoseAngle[4] };
    poseLibrary[1].Angles[0] = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    poseLibrary[1].Angles[1] = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 270, 20);
    poseLibrary[1].Angles[2] = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    poseLibrary[1].Angles[3] = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 270, 20);

    // 姿勢 3:左手舉起、右手放下
    poseLibrary[2] = new Pose { Title = "舉起左手", Angles = new PoseAngle[4] };
    poseLibrary[2].Angles[0] = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    poseLibrary[2].Angles[1] = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 90, 20);
    poseLibrary[2].Angles[2] = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    poseLibrary[2].Angles[3] = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 270, 20);

    // 姿勢 4:右手舉起、左手放下
    poseLibrary[3] = new Pose { Title = "舉起右手", Angles = new PoseAngle[4] };
    poseLibrary[3].Angles[0] = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    poseLibrary[3].Angles[1] = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 270, 20);
    poseLibrary[3].Angles[2] = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    poseLibrary[3].Angles[3] = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 90, 20);
}

3.3 核心判斷邏輯:IsPose()

private bool IsPose(Skeleton skeleton, Pose pose)
{
    bool isPose = true;
    for (int i = 0; i < pose.Angles.Length && isPose; i++)
    {
        double angle = GetJointAngle(skeleton.Joints[pose.Angles[i].CenterJoint],
                                     skeleton.Joints[pose.Angles[i].AngleJoint]);
        double target = pose.Angles[i].Angle;
        double thresh = pose.Angles[i].Threshold;
        double lo = target - thresh, hi = target + thresh;

        // 處理 0/360 邊界
        if (hi >= 360 || lo < 0)
        {
            lo = (lo < 0) ? 360 + lo : lo;
            hi = hi % 360;
            isPose = !(lo > angle && angle > hi); // 角度落在「環狀區間」外才判定失敗
        }
        else
        {
            isPose = (lo <= angle && angle <= hi);
        }
    }
    return isPose;
}

3.4 夾角計算輔助方法:GetJointAngle()

private double GetJointAngle(Joint centerJoint, Joint angleJoint)
{
    // 將關節點投影至 UI 座標系(便於偵錯視覺化)
    Point p1 = GetJointPoint(this.KinectDevice, centerJoint, this.LayoutRoot.RenderSize, new Point());
    Point p2 = GetJointPoint(this.KinectDevice, angleJoint, this.LayoutRoot.RenderSize, new Point());

    // 建構參考點:以 p1 為原點,X 軸正方向
    Point xRef = new Point(p1.X + Math.Abs(p2.X - p1.X), p1.Y);

    double a = Distance(p1, p2);
    double b = Distance(p1, xRef);
    double c = Distance(p2, xRef);

    double rad = Math.Acos((a * a + b * b - c * c) / (2 * a * b));
    double deg = rad * 180 / Math.PI;

    // 依 Y 軸關係判斷象限
    if (p1.Y < p2.Y) deg = 360 - deg;
    return deg;
}

private double Distance(Point a, Point b)
=> Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2));

3.5 遊戲流程控制:計時器與狀態機

使用 DispatcherTimer 限制玩家擺姿勢時間(預設 10 秒),超時即判定失敗。

private DispatcherTimer poseTimer;

public MainWindow()
{
    // ... 既有初始化 ...
    this.poseTimer = new DispatcherTimer();
    this.poseTimer.Interval = TimeSpan.FromSeconds(10);
    this.poseTimer.Tick += (s, e) => ChangePhase(GamePhase.GameOver);
    this.poseTimer.Stop();
}

// 玩家執行階段
private void ProcessPlayerPerforming(Skeleton skeleton)
{
    int seqIndex = this.instructionSequence[this.instructionPosition];
    if (IsPose(skeleton, this.poseLibrary[seqIndex]))
    {
        this.poseTimer.Stop();
        this.instructionPosition++;
        if (this.instructionPosition >= this.instructionSequence.Length)
            ChangePhase(GamePhase.SimonInstructing); // 進入下一輪
        else
        {
            // TODO: 給予正確回饋(音效、動畫)
            this.poseTimer.Start(); // 重置計時器,進入下一指令
        }
    }
}

遊戲畫面 1 遊戲畫面 2 遊戲畫面 3 遊戲畫面 4

四、 使用者體驗優化與架構擴充建議

4.1 即時回饋機制

  • 姿勢判定成功時:播放音效、顯示「✓」動畫、手型圖示變色。
  • 倒數計時視覺化:進度條或數字倒數,增加緊張感。
  • 遊戲結束時:以幻燈片回顧玩家姿勢截圖,增添趣味。

4.2 動態難度調整

  • 關卡越高,指令顯示時間縮短、姿勢序列加長。
  • 提供「簡單/普通/困難」三種指令生成策略供選擇。

4.3 架構重構:打造可復用的 PoseEngine

將姿勢識別邏輯封裝為獨立元件:

  • PoseEngine 類別:內含 PoseDetected 事件、Poses 集合(支援 Add/Remove)、監聽 SkeletonFrameReady 逐幀判斷。
  • 姿勢定義外部化:採用 JSON/XML 序列化儲存,支援執行時動態載入、甚至開發專用「姿勢編輯器」(基於 SkeletonViewer 擴充,即時顯示關節夾角、一鍵匯出設定檔)。

姿勢編輯器原型:顯示關節夾角

五、 結語

本文完整示範了如何:

  1. 善用骨骼資料的 Z 軸,打造具深度感的 UI 互動(ZIndex 分層 + 動態縮放)。
  2. 掌握姿勢識別的兩大核心演算法(關節距離 vs. 夾角計算)與容差處理技巧。
  3. 將「西蒙說」遊戲從碰撞測試升級為姿勢識別版,並建立可擴充的姿勢庫架構。
  4. 規劃 PoseEngine 元件化方向,為未來專案奠定基礎。

骨骼追蹤系列到此告一段落。下一系列將進入手勢識別領域,探討動態軌跡分析、機器學習分類器應用等進階主題,敬請期待!

作者:yangecnu(博客園)|原文連結:https://www.cnblogs.com/yangecnu/

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved