1. 程式人生 > >Kinect for Windows SDK開發入門(六):骨骼追蹤基礎 上

Kinect for Windows SDK開發入門(六):骨骼追蹤基礎 上

 Kinect產生的景深資料作用有限,要利用Kinect建立真正意義上互動,有趣和難忘的應用,還需要除了深度資料之外的其他資料。這就是骨骼追蹤技術的初衷,骨骼追蹤技術通過處理景深資料來建立人體各個關節的座標,骨骼追蹤能夠確定人體的各個部分,如那部分是手,頭部,以及身體。骨骼追蹤產生X,Y,Z資料來確定這些骨骼點。在上文中,我們討論了景深影象處理的一些技術。骨骼追蹤系統採用的景深影象處理技術使用更復雜的演算法如矩陣變換,機器學習及其他方式來確定骨骼點的座標。

    本文首先用一個例子展示骨骼追蹤系統涉及的主要物件,然後在此基礎上詳細討論骨骼追蹤中所涉及的物件模型。

1. 獲取骨骼資料

     本節將會建立一個應用來將獲取到的骨骼資料繪製到UI介面上來。在開始編碼前,首先來看看一些基本的物件以及如何從這些物件中如何獲取骨骼資料。在進行資料處理之前瞭解資料的格式也很有必要。這個例子很簡單明瞭,只需要骨骼資料物件然後將獲取到的資料繪製出來。

    彩色影像資料,景深資料分別來自ColorImageSteam和DepthImageStream,同樣地,骨骼資料來自SkeletonStream。訪問骨骼資料和訪問彩色影像資料、景深資料一樣,也有事件模式和 “拉”模式兩種方式。在本例中我們採用基於事件的方式,因為這種方式簡單,程式碼量少,並且是一種很普通基本的方法。KinectSensor物件有一個名為SkeletonFrameReady事件。當SkeletonStream中有新的骨骼資料產生時就會觸發該事件。通過AllFramesReady事件也可以獲取骨骼資料。在下一節中,我們將會詳細討論骨骼追蹤物件模型,現在我們只展示如何從SkeletonStream流中獲取骨骼資料。SkeletonStream產生的每一幀資料都是一個骨骼物件集合。每一個骨骼物件包含有描述骨骼位置以及骨骼關節的資料。每一個關節有一個唯一標示符如頭(head)、肩(shoulder)、肘(dlbow)等資訊和3D向量資料。

    現在來寫程式碼。首先建立一個新的wpf工程檔案,新增Microsoft.Kinect.dll。新增基本查詢和初始化感測器的程式碼,這些程式碼參考之前的文章。在開始啟動感測器之前,初始化SkeletonStream資料流,並註冊KinectSensor物件的SkeletonFrameReady事件,這個例子沒有使用彩色攝像機和紅外攝像機產生的資料,所以不需要初始化這些資料流。UI介面採用預設的,將Grid的名稱改為LayoutRoot,之後就再Grid裡面繪製。程式碼如下:

<Window x:Class="KinectSkeletonTracking.MainWindow"
        
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid x:Name="LayoutRoot" Background="White"> </Grid> </Window>

後臺邏輯程式碼如下:

private KinectSensor kinectDevice;
private readonly Brush[] skeletonBrushes;//繪圖筆刷
private Skeleton[] frameSkeletons;

public MainWindow()
{
    InitializeComponent();
    skeletonBrushes = new Brush[] { Brushes.Black, Brushes.Crimson, Brushes.Indigo, Brushes.DodgerBlue, Brushes.Purple, Brushes.Pink };
    KinectSensor.KinectSensors.StatusChanged += KinectSensors_StatusChanged;
    this.KinectDevice = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected);

}

public KinectSensor KinectDevice
{
    get { return this.kinectDevice; }
    set
    {
        if (this.kinectDevice != value)
        {
            //Uninitialize
            if (this.kinectDevice != null)
            {
                this.kinectDevice.Stop();
                this.kinectDevice.SkeletonFrameReady -= KinectDevice_SkeletonFrameReady;
                this.kinectDevice.SkeletonStream.Disable();
                this.frameSkeletons = null;
            }

            this.kinectDevice = value;

            //Initialize
            if (this.kinectDevice != null)
            {
                if (this.kinectDevice.Status == KinectStatus.Connected)
                {
                    this.kinectDevice.SkeletonStream.Enable();
                    this.frameSkeletons = new Skeleton[this.kinectDevice.SkeletonStream.FrameSkeletonArrayLength];
                    this.kinectDevice.SkeletonFrameReady += KinectDevice_SkeletonFrameReady;
                    this.kinectDevice.Start();
                }
            }
        }
    }
}

private void KinectSensors_StatusChanged(object sender, StatusChangedEventArgs e)
{
    switch (e.Status)
    {
        case KinectStatus.Initializing:
        case KinectStatus.Connected:
        case KinectStatus.NotPowered:
        case KinectStatus.NotReady:
        case KinectStatus.DeviceNotGenuine:
            this.KinectDevice = e.Sensor;
            break;
        case KinectStatus.Disconnected:
            //TODO: Give the user feedback to plug-in a Kinect device.                    
            this.KinectDevice = null;
            break;
        default:
            //TODO: Show an error state
            break;
    }
}

    以上程式碼中,值得注意的是frameSkeletons陣列以及該陣列如何在流初始化時進行記憶體分配的。Kinect能夠追蹤到的骨骼數量是一個常量。這使得我們在整個應用程式中能夠一次性的為陣列分配記憶體。為了方便,Kinect SDK在SkeletonStream物件中定義了一個能夠追蹤到的骨骼個數常量FrameSkeletonArrayLength,使用這個常量可以方便的對陣列進行初始化。程式碼中也定義了一個筆刷陣列,這些筆刷在繪製骨骼時對多個遊戲者可以使用不同的顏色進行繪製。也可以將筆刷陣列中的顏色設定為自己喜歡的顏色。

     下面的程式碼展示了SkeletonFrameReady事件的響應方法,每一次事件被激發時,通過呼叫事件引數的OpenSkeletonFrame方法就能夠獲取當前的骨骼資料幀。剩餘的程式碼遍歷骨骼資料幀的Skeleton陣列frameSkeletons,在UI介面通過關節點將骨骼連線起來,用一條直線代表一根骨骼。UI介面簡單,將Grid元素作為根結點,並將其背景設定為白色。

private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    using (SkeletonFrame frame = e.OpenSkeletonFrame())
    {
        if (frame != null)
        {
            Polyline figure;
            Brush userBrush;
            Skeleton skeleton;

            LayoutRoot.Children.Clear();
            frame.CopySkeletonDataTo(this.frameSkeletons);


            for (int i = 0; i < this.frameSkeletons.Length; i++)
            {
                skeleton = this.frameSkeletons[i];

                if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
                {
                    userBrush = this.skeletonBrushes[i % this.skeletonBrushes.Length];

                    //繪製頭和軀幹
                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.Head, JointType.ShoulderCenter, JointType.ShoulderLeft, JointType.Spine,
                                                                JointType.ShoulderRight, JointType.ShoulderCenter, JointType.HipCenter
                                                                });
                    LayoutRoot.Children.Add(figure);

                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.HipLeft, JointType.HipRight });
                    LayoutRoot.Children.Add(figure);

                    //繪製作腿
                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.HipCenter, JointType.HipLeft, JointType.KneeLeft, JointType.AnkleLeft, JointType.FootLeft });
                    LayoutRoot.Children.Add(figure);

                    //繪製右腿
                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.HipCenter, JointType.HipRight, JointType.KneeRight, JointType.AnkleRight, JointType.FootRight });
                    LayoutRoot.Children.Add(figure);

                    //繪製左臂
                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.ShoulderLeft, JointType.ElbowLeft, JointType.WristLeft, JointType.HandLeft });
                    LayoutRoot.Children.Add(figure);

                    //繪製右臂
                    figure = CreateFigure(skeleton, userBrush, new[] { JointType.ShoulderRight, JointType.ElbowRight, JointType.WristRight, JointType.HandRight });
                    LayoutRoot.Children.Add(figure);
                }
            }
        }
    }
}

    迴圈遍歷frameSkeletons物件,每一次處理一個骨骼,在處理之前需要判斷是否是一個追蹤好的骨骼,可以使用Skeleton物件的TrackingState屬性來判斷,只有骨骼追蹤引擎追蹤到的骨骼我們才進行繪製,忽略哪些不是遊戲者的骨骼資訊即過濾掉那些TrackingState不等於SkeletonTrackingState.Tracked的骨骼資料。Kinect能夠探測到6個遊戲者,但是同時只能夠追蹤到2個遊戲者的骨骼關節位置資訊。在後面我們將會詳細討論TrackingState這一屬性。

     處理骨骼資料相對簡單,首先,我們根Kinect追蹤到的遊戲者的編號,選擇一種顏色筆刷。然後利用這隻筆刷繪製曲線。CreateFigure方法為每一根骨骼繪製一條直線。GetJointPoint方法在繪製骨骼曲線中很關鍵。該方法以關節點的三維座標作為引數,然後呼叫KinectSensor物件的MapSkeletonPointToDepth方法將骨骼座標轉換到深度影像座標上去。後面我們將會討論為什麼需要這樣轉換以及如何定義座標系統。現在我們只需要知道的是,骨骼座標系和深度座標及彩色影像座標系不一樣,甚至和UI介面上的座標系不一樣。在開發Kinect應用程式中,從一個座標系轉換到另外一個座標系這樣的操作非常常見,GetJointPoint方法的目的就是將骨骼關節點的三維座標轉換到UI繪圖座標系統,返回該骨骼關節點在UI上的位置。下面的程式碼展示了CreateFigure和GetJointPoint這兩個方法。

private Polyline CreateFigure(Skeleton skeleton, Brush brush, JointType[] joints)
{
    Polyline figure = new Polyline();

    figure.StrokeThickness = 8;
    figure.Stroke = brush;

    for (int i = 0; i < joints.Length; i++)
    {
        figure.Points.Add(GetJointPoint(skeleton.Joints[joints[i]]));
    }

    return figure;
}

private Point GetJointPoint(Joint joint)
{

    DepthImagePoint point = this.KinectDevice.MapSkeletonPointToDepth(joint.Position, this.KinectDevice.DepthStream.Format);

    point.X *= (int)this.LayoutRoot.ActualWidth / KinectDevice.DepthStream.FrameWidth;
    point.Y *= (int)this.LayoutRoot.ActualHeight / KinectDevice.DepthStream.FrameHeight;

    return new Point(point.X, point.Y);
} 

    值得注意的是,骨骼關節點的三維座標中我們捨棄了Z值,只用了X,Y值。Kinect好不容易為我們提供了每一個節點的深度資料(Z值)而我們卻沒有使用,這看起來顯得很浪費。其實不是這樣的,我們使用了節點的Z值,只是沒有直接使用,沒有在UI介面上展現出來而已。在座標空間轉換中是需要深度資料的。可以試試在GetJointPoint方法中,將joint的Position中的Z值改為0,然後再呼叫MapSkeletonPointToDepth方法,你會發現返回的物件中x和y值均為0,可以試試,將影象以Z值進行等比縮放,可以發現影象的大小是和Z值(深度)成反的。也就是說,深度值越小,影象越大,即人物離Kinect越近,骨骼資料越大。

     執行程式,會得到如下骨骼影象,這個是手握鍵盤準備截圖的姿勢。一開始可能需要調整一些Form窗體的大小。程式會為每一個遊戲者以一種顏色繪製骨骼影象,可以試著在Kinect前面移動,可以看到骨骼影象的變化,也可以走進然後走出影象以觀察顏色的變化。仔細觀察有時候可以看到繪圖出現了一些奇怪的圖案,在討論完骨骼追蹤相關的API之後,就會明白這些現象出現的原因了。

skeleton-1

2. 骨骼物件模型

    Kinect SDK中骨骼追蹤有一些和其他物件不一樣的物件結構和列舉。在SDK中骨骼追蹤相關的內容幾乎佔據了三分之一的內容,可見Kinect中骨骼追蹤技術的重要性。下圖展示了骨骼追蹤系統中涉及到的一些主要的物件模型。有四個最主要的物件,他們是SkeletonStream,SkeletonFrame,Skeleton和Joint。下面將詳細介紹這四個物件。

Beginning.Kinect.Programming.with.the.Microsoft.Kinect

2.1 SkeletonStream物件

    SkeletonStream物件產生SkeletonFrame。從SkeletonStream獲取骨骼幀資料和從ColorStream及DepthStream中獲取資料類似。可以註冊SkeletonFrameReady事件或者AllFramesReady事件通過事件模型來獲取資料,或者是使用OpenNextFrame方法通過“拉”模型來獲取資料。不能對同一個SkeletonStream同時使用這兩種模式。如果註冊了SkeletonFrameReady事件然後又呼叫OpenNextFrame方法將會返回一個InvalidOperationException異常。

SkeletonStream的啟動和關閉

    除非啟動了SkeletonStream物件,否則,不會產生任何資料,預設情況下,SkeletonStream物件是關閉的。要使SkeletonStream產生資料,必須呼叫物件的Enabled方法。相反,呼叫Disable方法能夠使SkeletonStream物件暫停產生資料。SkeletonStream有一個IsEnabled方法來描述當前SkeletonStream物件的狀態。只有SkeletonStream物件啟動了,KinectSensor物件的SkeletonFrameReady事件才能被啟用。如果要使用“拉”模式來獲取資料SkeletonStream也必須啟動後才能呼叫OpenNextFrame方法。否則也會丟擲InvalidOperationException異常。

    一般地在應用程式的宣告週期中,一旦啟動了SkeletonStream物件,一般會保持啟動狀態。但是在有些情況下,我們希望關閉SkeletonStream物件。比如在應用程式中使用多個Kinect感測器時。只有一個Kinect感測器能夠產生骨骼資料,這也意味著,即使使用多個Kinect感測器,同時也只能追蹤到兩個遊戲者的骨骼資料資訊。在應用程式執行的過程中,有可能會關閉某一個Kinect感測器的SkeletonStream物件而開啟另一個Kinect感測器的SkeletonStream物件。

    另一個有可能關閉骨骼資料產生的原因是出於效能方面的考慮,骨骼資料處理是很耗費計算效能的操作。開啟骨骼追蹤是可以觀察的到CPU的佔用率明顯增加。當不需要骨骼資料時,關閉骨骼追蹤很有必要。例如,在有些遊戲場景中可能在展現一些動畫效果或者播放視訊,在這個動畫效果或者視訊播放時,停止骨骼追蹤可能可以使得遊戲更加流暢。

    當然關閉SkeletonStream也有一些副作用。當SkeletonStream的狀態發生改變時,所有的資料產生都會停止和從新開始。SkeletonStream的狀態改變會使感測器重新初始化,將TimeStamp和FrameNumber重置為0。在感測器重新初始化時也有幾毫秒的延遲。

平滑化

    在前面的例子中,會注意到,骨骼運動會呈現出跳躍式的變化。有幾個原因會導致出現這一問題,可能是應用程式的效能,遊戲者的動作不夠連貫,也有可能是Kinect硬體的效能問題。骨骼關節點的相對位置可能在幀與幀之間變動很大,這回對應用程式產生一些負面的影像。除了會影像使用者體驗和不愉快意外,也可能會導致使用者的形象或者手的顫動抽搐而使使用者感到迷惑。

    SkeletonStream物件有一種方法能夠解決這個問題。他通過將骨骼關節點的座標標準化來減少幀與幀之間的關節點位置差異。當初始化SkeletonStream物件呼叫過載的Enable方法時可以傳入一個TransformSmoothParameters引數。SkeletonStream物件有兩個與平滑有關只讀屬性:IsSmoothingEnabled和SmoothParameters。當呼叫Enable方法傳入了TransformSmoothParameters是IsSmoothingEnabled返回true而當使用預設的不帶引數的Enable方法初始化時,IsSmoothingEnabled物件返回false。SmoothParameters屬性用來儲存定義平滑引數。TransformSmoothParameters這個結構定義了一些屬性:

  • 修正值(Correction)屬性,接受一個從0-1的浮點型。值越小,修正越多。
  • 抖動半徑(JitterRadius)屬性,設定修正的半徑,如果關節點“抖動”超過了設定的這個半徑,將會被糾正到這個半徑之內。該屬性為浮點型,單位為米。
  • 最大偏離半徑(MaxDeviationRadius)屬性,用來和抖動半徑一起來設定抖動半徑的最大邊界。任何超過這一半徑的點都不會認為是抖動產生的,而被認定為是一個新的點。該屬性為浮點型,單位為米。
  • 預測幀大小(Prediction)屬性,返回用來進行平滑需要的骨骼幀的數目。
  • 平滑值(Smoothing)屬性,設定處理骨骼資料幀時的平滑量,接受一個0-1的浮點值,值越大,平滑的越多。0表示不進行平滑。

    對骨骼關節點進行平滑處理會產生效能開銷。平滑處理的越多,效能消耗越大。設定平滑引數沒有經驗可以遵循。需要不斷的測試和除錯已達到最好的效能和效果。在程式執行的不同階段,可能需要設定不同的平滑引數。

Note:SDK使用霍爾特指數平滑(Holt Double Exponential Smoothing)來對減少關節點的抖動。指數平滑資料處理與時間有關。骨骼資料是時間序列資料,因為骨骼引擎會以某一時間間隔不斷產生一幀一幀的骨骼資料。平滑處理使用統計方法進行滑動平均,這樣能夠減少時間序列資料中的噪聲和極值。類似的處理方法最開始被用於金融市場和經濟資料的預測。

骨骼追蹤物件選擇

    預設情況下,骨骼追蹤引擎會對視野內的所有活動的遊戲者進行追蹤。但只會選擇兩個可能的遊戲者產生骨骼資料,大多數情況下,這個選擇過程不確定。如果要自己選擇追蹤物件,需要使用AppChoosesSkeletons屬性和ChooseSkeletons方法。 預設情況下AppChoosesSkeleton屬性為false,骨骼追蹤引擎追蹤所有可能的最多兩個遊戲者。要手動選擇追蹤者,需要將AppChoosesSkeleton設定為true,並呼叫ChooseSkeletons方法,傳入TrackingIDs已表明需要追蹤那個物件。ChooseSkeletons方法接受一個,兩個或者0個TrackingIDs。當ChooseSkeletons方法傳入0個引數時,引擎停止追蹤骨骼資訊。有一些需要注意的地方:

  • 如果呼叫ChooseSkeletons方法時AppChoosesSkeletons的屬性為false,就會引發InvalidOperationExcepthion的異常。
  • 如果在SkeletonStream開啟前,經AppChoosesSkeletons設定為true,只有手動呼叫ChooseSkeleton方法後才會開始骨骼追蹤。
  • 在AppChoosesSkeletons設定為 true之前,骨骼引擎自動選擇追蹤的遊戲者,並且繼續保持這些該遊戲者的追蹤,直到使用者手動指定需要追蹤的遊戲者。如果自動選擇追蹤的遊戲者離開場景,骨骼引擎不會自動更換追蹤者。
  • 將AppChoosesSkeletons衝新設定為false後,骨骼引擎會繼續對之前手動設定的遊戲者進行追蹤,直到這些遊戲者離開視野。當遊戲這離開視野時骨骼引擎才會選擇其他的可能的遊戲者進行追蹤。

2.2 SkeletonFrame

    SkeletonStream產生SkeletonFrame物件。可以使用事件模型從事件引數中呼叫OpenSkeletonFrame方法來獲取SkeletonFrame物件,或者採用”拉”模型呼叫SkeletonStream的OpenNextFrame來獲取SkeletonFrame物件。SkeletonFrame物件會儲存骨骼資料一段時間。同以通過呼叫SkeletonFrame物件的CopySkeletonDataTo方法將其儲存的資料拷貝到骨骼物件陣列中。SkeletonFrame物件有一個SkeletonArrayLength的屬性,這個屬性表示追蹤到的骨骼資訊的個數。

時間標記欄位

    SkeletonFrame的FrameNumber和Timestamp欄位表示當前記錄中的幀序列資訊。FrameNumber是景深資料幀中的用來產生骨骼資料幀的幀編號。幀編號通常是不連續的,但是之後的幀編號一定比之前的要大。骨骼追蹤引擎在追蹤過程中可能會忽略某一幀深度資料,這跟應用程式的效能和每秒產生的幀數有關。例如,在基於事件獲取骨骼幀資訊中,如果事件中處理幀資料的時間過長就會導致這一幀資料還沒有處理完就產生了新的資料,那麼這些新的資料就有可能被忽略了。如果採用“拉”模型獲取幀資料,那麼取決於應用程式設定的骨骼引擎產生資料的頻率,即取決於深度影像資料產生骨骼資料的頻率。

Timestap欄位記錄字Kinect感測器初始化以來經過的累計毫秒時間。不用擔心FrameNumber或者Timestamp欄位會超出上限。FrameNumber是一個32位的整型,Timestamp是64位整型。如果應用程式以每秒30幀的速度產生資料,應用程式需要執行2.25年才會達到FrameNumber的限,此時Timestamp離上限還很遠。另外在Kinect感測器每一次初始化時,這兩個欄位都會初始化為0。可以認為FrameNumber和Timestamp這兩個值是唯一的。

這兩個欄位在分析處理幀序列資料時很重要,比如進行關節點值的平滑,手勢識別操作等。在多數情況下,我們通常會處理幀時間序列資料,這兩個欄位就顯得很有用。目前SDK中並沒有包含手勢識別引擎。在未來SDK中加入手勢引擎之前,我們需要自己編寫演算法來對幀時間序列進行處理來識別手勢,這樣就會大量依賴這兩個欄位。

幀描述資訊

    FloorClipPlane欄位是一個有四個元素的元組Tuple<int,int,int,int>,每一個都是Ax+By+Cz+D=0地面平面(floor plane)表示式裡面的係數項。元組中第一個元素表示A,即x前面的係數,一次類推,最後一個表示常數項,通常為負數,是Kinect距離地面高度。在可能的情況下SDK會利用影象處理技術來確定這些係數。但是有時候這些係數不肯能能夠確定下來,可能需要預估。當地面不能確定時FloorClipPlane中的所有元素均為0.

2.3 Skeleton

    Skeleton類定義了一系列欄位來描述骨骼資訊,包括描述骨骼的位置以及骨骼中關節可能的位置資訊。骨骼資料可以通過呼叫SkeletonFrame物件的CopySkeletonDataTo方法獲得Skeleton陣列。CopySkeletonDataTo方法有一些不可預料的行為,可能會影響記憶體使用和其引用的骨骼陣列物件。產生的每一個骨骼陣列物件陣列都是唯一的。以下面程式碼為例:

Skeleton[] skeletonA = new Skeleton[frame.SkeletonArrayLength];
Skeleton[] skeletonB = new Skeleton[frame.SkeletonArrayLength];

frame.CopySkeletonDataTo(skeletonA);
frame.CopySkeletonDataTo(skeletonB);

Boolean resultA = skeletonA[0] == skeletonB[0];//false
Boolean resultB = skeletonA[0].TrackingId == skeletonB[0].TrackingId;//true

上面的程式碼可以看出,使用CopySkeletonDataTo是深拷貝物件,會產生兩個不同的Skeleton陣列物件。

TrackingID

    骨骼追蹤引擎對於每一個追蹤到的遊戲者的骨骼資訊都有一個唯一編號。這個值是整型,他會隨著新的追蹤到的遊戲者的產生新增增長。和之前幀序號一樣,這個值並不是連續增長的,但是能保證的是後面追蹤到的物件的編號要比之前的編號大。另外,這個編號的產生是不確定的。如果骨骼追蹤引擎失去了對遊戲者的追蹤,比如說遊戲者離開了Kinect的視野,那麼這個對應的唯一編號就會過期。當Kinect追蹤到了一個新的遊戲者,他會為其分配一個新的唯一編號,編號值為0表示這個骨骼資訊不是遊戲者的,他在集合中僅僅是一個佔位符。應用程式使用TrackingID來指定需要骨骼追蹤引擎追蹤那個遊戲者。呼叫SkeletonStream物件的ChooseSkeleton能以初始化對指定遊戲這的追蹤。

TrackingState

    該欄位表示當前的骨骼資料的狀態。下表展示了SkeletonTrackingState列舉的可能值機器含義:

image

Position

Position一個SkeletonPoint型別的欄位,代表所有骨骼的中間點。身體的中間點和脊柱關節的位置相當。改欄位提供了一個最快且最簡單的所有視野範圍內的遊戲者位置的資訊,而不管其是否在追蹤狀態中。在一些應用中,如果不用關心骨骼中具體的關節點的位置資訊,那麼該欄位對於確定遊戲者的位置狀態已經足夠。該欄位對於手動選擇要追蹤的遊戲者(SkeletonStream.ChooseSkeleton)也是一個參考。例如,應用程式可能需要追蹤距離Kinect最近的且處於追蹤狀態的遊戲者,那麼該欄位就可以用來過濾掉其他的遊戲者。

ClippedEdges

    ClippedEdges欄位用來描述追蹤者的身體哪部分位於Kinect的視野範圍外。他大體上提供了一個追蹤這的位置資訊。使用這一屬性可以通過程式調整Kinect攝像頭的俯仰角或者提示遊戲者讓其返回到視野中來。該欄位型別為FrameEdges,他是一個列舉並且有一個FlagsAtrribute自定義屬性修飾。這意味著ClippedEdges欄位可以一個或者多個FrameEdges值。下面列出了FrameEdges的所有可能的值。

image

    當遊戲者身體的某一部分超出Kinect視場範圍時,就需要對骨骼追蹤產生的資料進行某些改進,因為某些部位的資料可能追蹤不到或者不準確。最簡單的解決辦法就是提示遊戲者身體超出了Kinect的某一邊界範圍讓遊戲者回到視場中來。例如,有時候應用程式可能不關心遊戲者超出Kinect視場下邊界的情況,但是如果超出了左邊界或者右邊界時就會對應用產生影響,這是可以針對性的給遊戲者一些提示。另一個解決辦法是調整Kinect裝置的物理位置。Kinect底座上面有一個小的馬達能夠調整Kinect的俯仰角度。俯仰角度可以通過更改KinectSensor物件的ElevationAnagle屬性來進行調整。如果應用程式對於遊戲者腳部動作比較關注,那麼通過程式調整Kinect的俯仰角能夠決絕腳部超出視場下界的情況。

    ElevationAnagle以度為單位。KinectSensor的MaxElevationAngle和MinElevationAngle確定了可以調整角度的上下界。任何將ElevationAngle設定超出上下界的操作將會掏出ArgumentOutOfRangeExcepthion異常。微軟建議不要過於頻繁重複的調整俯仰角以免損壞馬達。為了使得開發這少犯錯誤和保護馬達,SDK限制了每秒能調整的俯仰角的值。SDK限制了在連續15次調整之後要暫停20秒。

Joints

每一個骨骼物件都有一個Joints欄位。該欄位是一個JointsCollection型別,它儲存了一些列的Joint結構來描述骨骼中可追蹤的關節點(如head,hands,elbow等等)。應用程式使用JointsCollection索引獲取特定的關節點,並通過節點的JointType列舉來過濾指定的關節點。即使Kinect視場中沒有遊戲者Joints物件也被填充。

2.4 Joint

骨骼追蹤引擎能夠跟蹤和獲取每個使用者的近20個點或者關節點資訊。追蹤的資料以關節點資料展現,它有三個屬性。JointType屬性是一個列舉型別。下圖描述了可追蹤的所有關節點。

image

     每一個關節點都有型別為SkeletonPoint的Position屬性,他通過X,Y,Z三個值來描述關節點的控制元件位置。X,Y值是相對於骨骼平面空間的位置,他和深度影像,彩色影像的空間座標系不一樣。KinectSnesor物件有一些列的座標轉換方法,可以將骨骼座標點轉換到對應的深度資料影像中去。最後每一個Skeleton物件還有一個JointTrackingState屬性,他描述了該關節點的跟蹤狀態及方式,下面列出了所有的可能值。

image

3. 結語

    本文首先通過一個例子展示骨骼追蹤系統所涉及的主要物件,並將骨骼資料在UI介面上進行了繪製,在此基礎上詳細介紹了骨骼追蹤物件模型中涉及到的主要物件,方法和屬性。SDK中骨骼追蹤佔了大概三分之一的內容,所以熟悉這些物件對於開發基於Kinect應用程式至關重要。限於篇幅,下一篇文章將會演示一個使用Kinect骨骼追蹤系統開發的小遊戲,然後討論控制元件座標變換,敬請期待。

    本文程式碼點選此處下載,希望以上內容對您熟悉Kinect SDK有所幫助!