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

Kinect for Windows SDK 開發入門(七):骨骼追蹤基礎(下)─ 連連看遊戲實作、座標轉換與 SkeletonViewer 自訂控制項

     編輯:關於.NET

前言

承接上一篇骨骼物件模型介紹,本文將透過一個「連連看」小遊戲,完整示範如何將骨骼追蹤資料應用於實際專案:手部追蹤判斷、UI 座標映射、遊戲邏輯與繪圖同步。並補充說明骨骼/深度/彩色/UI 四大座標系轉換細節、鏡像處理,最後封裝可重用的 SkeletonViewer 自訂控制項,便於開發期除錯與視覺化。

一、 專案實作:Kinect 連連看遊戲

1.1 遊戲規則與資料結構

  • 畫面隨機產生 15 個編號點(Point),儲存於 DotPuzzle.Dots List<Point>。
  • 玩家以最近的手(HandLeft 或 HandRight,Z 值最小者)控制游標。
  • 手游標移入點的 25 像素半徑內 → 視為「點擊」,以 Polyline 連線。
  • 依序連完所有點並回到起點即通關。
public class DotPuzzle { public List<Point> Dots { get; set; } = new List<Point>(); }
private DotPuzzle puzzle = new DotPuzzle();
private int puzzleDotIndex = -1; // -1 表示尚未開始

1.2 UI 版面配置:分層 Canvas

<Viewbox>
  <Grid x:Name="LayoutRoot" Width="1920" Height="1200">
    <Polyline x:Name="CrayonElement" Stroke="Black" StrokeThickness="3" />
    <Canvas x:Name="PuzzleBoardElement" />           <!-- 點位圖層 -->
    <Canvas x:Name="GameBoardElement">              <!-- 手游標圖層(最上層) -->
      <Image x:Name="HandCursorElement" Source="Images/hand.png" Width="75" Height="75"
             RenderTransformOrigin="0.5,0.5">
        <Image.RenderTransform>
          <ScaleTransform x:Name="HandCursorScale" ScaleX="1" />
        </Image.RenderTransform>
      </Image>
    </Canvas>
  </Grid>
</Viewbox>
  • Viewbox + 固定 Grid 尺寸:簡化座標轉換,確保手部移動流暢。
  • 分層 Canvas:重設遊戲時僅清除 PuzzleBoardElement.Children,不影響游標與連線。

1.3 手部追蹤與游標同步

private void KinectDevice_SkeletonFrameReady(...)
{
    using (var frame = e.OpenSkeletonFrame())
    {
        if (frame == null) return;
        frame.CopySkeletonDataTo(frameSkeletons);
        var skeleton = GetPrimarySkeleton(frameSkeletons); // 最近的追蹤中骨架
        if (skeleton == null) { HandCursorElement.Visibility = Visibility.Collapsed; return; }

        var hand = GetPrimaryHand(skeleton); // Z 值較小者
        TrackHand(hand);
        TrackPuzzle(hand.Position);
    }
}

private Joint GetPrimaryHand(Skeleton s)
{
    var left = s.Joints[JointType.HandLeft];
    var right = s.Joints[JointType.HandRight];
    if (right.TrackingState == JointTrackingState.NotTracked) return left;
    if (left.TrackingState == JointTrackingState.NotTracked) return right;
    return (left.Position.Z < right.Position.Z) ? left : right; // 較近者
}

private void TrackHand(Joint hand)
{
    if (hand.TrackingState == JointTrackingState.NotTracked)
    { HandCursorElement.Visibility = Visibility.Collapsed; return; }

    HandCursorElement.Visibility = Visibility.Visible;
    var dp = KinectDevice.MapSkeletonPointToDepth(hand.Position, KinectDevice.DepthStream.Format);
    dp.X = (int)(dp.X * LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth - HandCursorElement.ActualWidth / 2);
    dp.Y = (int)(dp.Y * LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight - HandCursorElement.ActualHeight / 2);
    Canvas.SetLeft(HandCursorElement, dp.X);
    Canvas.SetTop(HandCursorElement, dp.Y);
    HandCursorScale.ScaleX = (hand.JointType == JointType.HandRight) ? 1 : -1; // 左手鏡像
}

1.4 連線邏輯與距離判定

private void TrackPuzzle(SkeletonPoint handPos)
{
    if (puzzleDotIndex == puzzle.Dots.Count) return; // 已通關

    // 取得下一個目標點(最後一個點連回第一個)
    Point target = (puzzleDotIndex + 1 < puzzle.Dots.Count)
        ? puzzle.Dots[puzzleDotIndex + 1]
        : puzzle.Dots[0];

    // 手部位置映射至 UI 座標
    var dp = KinectDevice.MapSkeletonPointToDepth(handPos, KinectDevice.DepthStream.Format);
    Point handPt = new Point(
        dp.X * LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth,
        dp.Y * LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight);

    double dist = Math.Sqrt(Math.Pow(target.X - handPt.X, 2) + Math.Pow(target.Y - handPt.Y, 2));
    int lastIdx = CrayonElement.Points.Count - 1;

    if (dist < 25) // 命中半徑
    {
        if (lastIdx > 0) CrayonElement.Points.RemoveAt(lastIdx); // 移除暫時終點
        CrayonElement.Points.Add(target); // 確定終點
        CrayonElement.Points.Add(target); // 新線段起點
        puzzleDotIndex++;
        if (puzzleDotIndex == puzzle.Dots.Count) { /* 通關提示 */ }
    }
    else if (lastIdx > 0) // 未命中:更新暫時終點跟隨手部
    {
        var tempEnd = CrayonElement.Points[lastIdx];
        CrayonElement.Points.RemoveAt(lastIdx);
        tempEnd.X = handPt.X; tempEnd.Y = handPt.Y;
        CrayonElement.Points.Add(tempEnd);
    }
}

遊戲過程 1 遊戲過程 2 遊戲過程 3 遊戲通關

1.5 後續優化方向

  • 手部平滑濾波(移動平均或 Holt 指數平滑)消除抖動。
  • 重新開始機制:ResetPuzzle() 清除連線、重設索引、可加入倒數動畫。
  • 關卡編輯器:從 XML 載入點位、隨機關卡、選單選擇。
  • 提示系統:超時未找到下一點 → 箭頭/文字提示。
  • 離開偵測:骨架消失超過 N 秒自動重置。
  • 通關獎勵:音效、粒子、自由繪圖模式。

二、 座標空間與轉換總整理

2.1 四大座標系對照

座標系單位原點範圍備註
骨骼公尺感測器光心X: ±2.2m, Y: ±1.6m, Z: 0~4m右手制,Y 向上
深度像素/毫米影像左上角640×480, Z: mmZ 為 0 表無效
彩色像素影像左上角1280×960 等與深度不同視角
UI (WPF)裝置獨立像素 (1/96吋)容器左上角依版面而定需自行縮放映射

2.2 核心轉換 API

  • KinectSensor.MapSkeletonPointToDepth(SkeletonPoint, DepthImageFormat) → DepthImagePoint(含 X、Y、Depth)。
  • KinectSensor.MapDepthToSkeletonPoint(DepthImageFormat, x, y, depth) → SkeletonPoint。
  • DepthImageFrame.MapToSkeletonPoint / MapToColorImagePoint:同功能,屬於幀物件。

典型流程:骨骼 → 深度 → UI 縮放

var dp = kinect.MapSkeletonPointToDepth(joint.Position, kinect.DepthStream.Format);
double uiX = dp.X * LayoutRoot.ActualWidth / kinect.DepthStream.FrameWidth;
double uiY = dp.Y * LayoutRoot.ActualHeight / kinect.DepthStream.FrameHeight;

注意: DepthStream.Format 決定解析度與視野,轉換時必須與啟動時一致。DepthStream 不需實際啟用也能轉換,只需格式正確。

2.3 鏡像處理

骨骼資料預設為鏡像(使用者舉左手,骨架顯示右手)。若需「第一人稱視角」(角色模仿真人左右),需反轉 X 軸:

// GetJointPoint 修正版
private Point GetJointPoint(Joint joint)
{
    var dp = KinectDevice.MapSkeletonPointToDepth(joint.Position, KinectDevice.DepthStream.Format);
    dp.X = (int)(-dp.X * LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth); // X 乘 -1
    dp.Y = (int)(dp.Y * LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight);
    return new Point(dp.X, dp.Y);
}

三、 SkeletonViewer 自訂控制項:開發期除錯利器

3.1 控制項結構

<UserControl x:Class="KinectDrawDotsGame.SkeletonViewer" ...>
  <Grid x:Name="LayoutRoot">
    <Grid x:Name="SkeletonsPanel" />      <!-- 繪製骨架線條 -->
    <Canvas x:Name="JointInfoPanel" />    <!-- 顯示關節座標文字 -->
  </Grid>
</UserControl>

3.2 相依性屬性綁定 KinectSensor

public static readonly DependencyProperty KinectDeviceProperty =
    DependencyProperty.Register("KinectDevice", typeof(KinectSensor), typeof(SkeletonViewer),
        new PropertyMetadata(null, OnKinectDeviceChanged));

private static void OnKinectDeviceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var viewer = (SkeletonViewer)d;
    if (e.OldValue is KinectSensor oldSensor)
        oldSensor.SkeletonFrameReady -= viewer.KinectDevice_SkeletonFrameReady;
    if (e.NewValue is KinectSensor newSensor)
    {
        viewer.KinectDevice = newSensor;
        newSensor.SkeletonFrameReady += viewer.KinectDevice_SkeletonFrameReady;
        viewer._FrameSkeletons = new Skeleton[newSensor.SkeletonStream.FrameSkeletonArrayLength];
    }
}

3.3 即時繪製骨架與關節資訊

private void KinectDevice_SkeletonFrameReady(...)
{
    SkeletonsPanel.Children.Clear();
    JointInfoPanel.Children.Clear();
    using (var frame = e.OpenSkeletonFrame())
    {
        if (frame == null || !IsEnabled) return;
        frame.CopySkeletonDataTo(_FrameSkeletons);
        foreach (var sk in _FrameSkeletons)
        {
            if (sk.TrackingState != SkeletonTrackingState.Tracked) continue;
            var brush = _SkeletonBrushes[Array.IndexOf(_FrameSkeletons, sk) % _SkeletonBrushes.Length];
            DrawSkeleton(sk, brush);
            TrackJoint(sk.Joints[JointType.HandLeft],  brush);
            TrackJoint(sk.Joints[JointType.HandRight], brush);
        }
    }
}

private void TrackJoint(Joint joint, Brush brush)
{
    if (joint.TrackingState == JointTrackingState.NotTracked) return;
    var pt = GetJointPoint(joint);
    double z = joint.Position.Z;

    var container = new Canvas();
    var dot = new Ellipse { Width = 15, Height = 15, Fill = brush };
    Canvas.SetLeft(dot, -7.5); Canvas.SetTop(dot, -7.5);
    container.Children.Add(dot);

    var txt = new TextBlock
    {
        Text = $"<{pt.X:0.00}, {pt.Y:0.00}, {z:0.00}>",
        Foreground = brush, FontSize = 24
    };
    txt.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
    Canvas.SetLeft(txt, 35); Canvas.SetTop(txt, 15);
    container.Children.Add(txt);

    Canvas.SetLeft(container, pt.X); Canvas.SetTop(container, pt.Y);
    JointInfoPanel.Children.Add(container);
}

3.4 整合至主視窗

<Window ... xmlns:c="clr-namespace:KinectDrawDotsGame">
  <Viewbox>
    <Grid x:Name="LayoutRoot" Width="1920" Height="1200">
      <c:SkeletonViewer x:Name="SkeletonViewerElement" />
      <!-- 既有遊戲元素 -->
    </Grid>
  </Viewbox>
</Window>

// 後台:KinectSensor 設定器同步指派給 SkeletonViewer
public KinectSensor KinectDevice
{
    set
    {
        if (kinectDevice != null) { /* 釋放舊感測器事件、停止串流 */ }
        kinectDevice = value;
        if (kinectDevice != null)
        {
            kinectDevice.SkeletonStream.Enable();
            frameSkeletons = new Skeleton[kinectDevice.SkeletonStream.FrameSkeletonArrayLength];
            SkeletonViewerElement.KinectDevice = kinectDevice; // 關鍵:傳遞感測器實例
            kinectDevice.Start();
        }
    }
}

SkeletonViewer 效果 1 SkeletonViewer 效果 2

四、 結語

本文以「連連看」遊戲為主軸,串聯了骨骼追蹤的完整應用流程:

  1. 手部主導權判斷(雙手 Z 值比較)。
  2. 骨骼 → 深度 → UI 三階座標映射。
  3. 命中半徑容差設計與 Polyline 動態繪圖技巧。
  4. 四大座標系詳細對照與鏡像修正方法。
  5. 封裝 SkeletonViewer 自訂控制項,支援 DependencyProperty 綁定、事件弱參考防記憶體洩漏、關節即時座標標註。

下一篇將進入骨骼追蹤進階篇,結合 WPF 輸入系統與「西蒙說」遊戲,展示更豐富的互動模式,敬請期待!

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

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