程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Kinect for Windows SDK開發入門(九)骨骼追蹤進階 下

Kinect for Windows SDK開發入門(九)骨骼追蹤進階 下

編輯:關於.NET

1. 基於景深數據的用戶交互

到目前為止我們只用了骨骼數據中關節點的X,Y值。然而Kinect產生的關節點數據除了X,Y值外還有一個深度值。基於Kinect的應用程序應該利用好這個深度值。下面的部分將會介紹如何在Kinect應用程序中使用深度值。

除了使用WPF的3D特性外,在布局系統中可以根據深度值來設定可視化元素的尺寸大小來達到某種程序的立體效果。下面的例子使用Canvas.ZIndex屬性來設置元素的層次,手動設置控件的大小並使用ScaleTransform來根據深度值的改變來進行縮放。用戶界面包括一些圓形,每一個圓代表一定的深度。應用程序跟蹤用戶的手部關節點,以手形圖標顯示,圖標會根據用戶手部關節點的深度值來進行縮放,用戶離Kinect越近,手形圖表越大,反之越小。

創建一個新的WPF項目,主界面的XAML如下。主要的布局容器為Cnavas容器。它包含5個Ellipses及對應的TextBlock控件,TextBlock用來對圓形進行說明。這幾個圓形隨機分布在屏幕上,但是圓形的Canvas.ZIndex是確定的。Canvas容器也包含了兩個圖像控件,用來代表兩只手。每一個手部圖標都定義了一個ScaleTransform對象。手形圖標是和右手方向一致的,將ScaleTransform的ScaleX設置為-1可以將其反轉,看起來像左手。

<Window x:Class="KinectDepthBasedInteraction.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Depth UI Target" Height="1080" Width="1920" WindowState="Maximized" Background="White">
    <Window.Resources>
        <Style x:Key="TargetLabel" TargetType="TextBlock" >
            <Setter Property="FontSize" Value="40" />
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="FontWeight" Value="Bold" />
            <Setter Property="IsHitTestVisible" Value="False" />
        </Style>
    </Window.Resources>
    <Viewbox>
            
        <Grid x:Name="LayoutRoot" Width="1920" Height="1280">
            <Image x:Name="DepthImage"/>
            <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
                <TextBlock x:Name="DebugLeftHand" Style="{StaticResource TargetLabel}" Foreground="Black" />
                <TextBlock x:Name="DebugRightHand" Style="{StaticResource TargetLabel}" Foreground="Black" />
            </StackPanel>
            <Canvas>
                <Ellipse x:Name="Target3" Fill="Orange" Height="200" Width="200" Canvas.Left="776" Canvas.Top="162" Canvas.ZIndex="1040" />
                <TextBlock Text="3" Canvas.Left="860" Canvas.Top="206" Panel.ZIndex="1040" Style="{StaticResource TargetLabel}" />
    
                <Ellipse x:Name="Target4" Fill="Purple" Height="150" Width="150" Canvas.Left="732" Canvas.Top="320" Canvas.ZIndex="940" />
                <TextBlock Text="4" Canvas.Left="840" Canvas.Top="372" Panel.ZIndex="940" Style="{StaticResource TargetLabel}" />
    
                <Ellipse x:Name="Target5" Fill="Green" Height="120" Width="120" Canvas.Left="880" Canvas.Top="592" Canvas.ZIndex="840" />
                <TextBlock Text="5" Canvas.Left="908" Canvas.Top="590" Panel.ZIndex="840" Style="{StaticResource TargetLabel}" />
    
                <Ellipse x:Name="Target6" Fill="Blue" Height="100" Width="100" Canvas.Left="352" Canvas.Top="544" Canvas.ZIndex="740" />
                <TextBlock Text="6" Canvas.Left="368" Canvas.Top="582" Panel.ZIndex="740" Style="{StaticResource TargetLabel}" />
    
                <Ellipse x:Name="Target7" Fill="Red" Height="85" Width="85" Canvas.Left="378" Canvas.Top="192" Canvas.ZIndex="640" />
                <TextBlock Text="7" Canvas.Left="422" Canvas.Top="226" Panel.ZIndex="640" Style="{StaticResource TargetLabel}" />
    
                <Image x:Name="LeftHandElement" Source="Images/hand.png" Width="80" Height="80" RenderTransformOrigin="0.5,0.5">
                    <Image.RenderTransform>
                        <ScaleTransform x:Name="LeftHandScaleTransform" ScaleX="1" CenterY="-1" />
                    </Image.RenderTransform>
                </Image>
                    
                <Image x:Name="RightHandElement" Source="Images/hand.png" Width="80" Height="80" RenderTransformOrigin="0.5,0.5">
                    <Image.RenderTransform>
                        <ScaleTransform x:Name="RightHandScaleTransform" CenterY="1" ScaleX="1" />
                    </Image.RenderTransform>
                </Image>
            </Canvas>
        </Grid>
    </Viewbox>
</Window

不同顏色的圓形代表不同的深度,例如名為Target3的元素代表距離為3英尺。Target3的長寬比Target7要大,這簡單的通過縮放可以實現。在我們的實例程序中,我們將其大小進行硬編碼,實際的程序中,應該根據特定要求可以進行縮放。Canvas容器會根據子元素的Canvas.ZIndex的值對元素在垂直於計算機屏幕的方向上進行排列,例如最上面的元素,其Canvas.ZIndex最大。如果兩個元素有相同的ZIndex值,那麼會根據其在XAML中聲明的順序進行顯示,在XAML中,後面聲明的元素在之前聲明的元素的前面。對於Canvas的所有子元素,ZIndex值越大,離屏幕越近,越小離屏幕越遠。將深度值取反剛好能達到想要的效果。這意味這我們不能直接使用深度值來給ZIndex來賦值,而要對它進行一點轉換。Kinect能夠產生的最大深度值為13.4英尺,相應的,我們將Canvas.Zindex的取值范圍設置為0-1340,將深度值乘以100能獲得更好的精度。因此Target5的Canvas.ZIndex設置為840(13.5-5=8.4*100=840)。

XAML文件中,包含兩個名為DebugLeftHand和DebugRightHand的TextBlocks控件。這兩個控件用來顯示兩只手的關節點數據的深度值。因為調試Kinect程序比較麻煩,這個值是用來調試程序的。

下面的代碼用來處理骨骼數據。SkeletonFrameReady事件處理代碼除了TrackHand方法之外和之前的例子沒有區別。TrackHand方法對手形圖標使用深度值進行縮放。方法將手所在點的坐標轉換到UI界面上後,使用Canvas.SetLeft和Canvas.SetTop方法進行賦值。Cnavas.ZIndex的使用前面討論的計算方法。

設置好Canvas.ZIndex對於可視化元素的布局已經足夠,但是不能夠根據深度視覺效果對物體進行縮放。對於Kinect應用程序,Z值其他輸入設備不能提供的,如果沒有根據節點深度數據進行的縮放,那麼這以獨特的Z值就浪費了。縮放比例可能需要測試以後才能確定下來。如果想要達到最好的用戶體驗效果。手形圖標的大小應該和用戶手的實際大小一致,目前從Kinect數據不能直接獲取到用戶手的大小信息。有一種方法時讓用戶戴上類似感應手套這一類產品以提供另外一些額外的信息,這樣可以產生更加自然的交互體驗。

private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    using (SkeletonFrame frame = e.OpenSkeletonFrame())
    {
        if (frame != null)
        {
            frame.CopySkeletonDataTo(this.frameSkeletons);
            Skeleton skeleton = GetPrimarySkeleton(this.frameSkeletons);
            if (skeleton != null)
            {
                TrackHand(skeleton.Joints[JointType.HandLeft], LeftHandElement, LeftHandScaleTransform, LayoutRoot, true);
                TrackHand(skeleton.Joints[JointType.HandRight], RightHandElement, RightHandScaleTransform, LayoutRoot, false);
    
            }
        }
    }
}
    
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 = System.Windows.Visibility.Visible;
        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);
        Canvas.SetZIndex(cursorElement, (int)(1200- (z * 100)));
    
        cursorScale.ScaleX = 12 / z * (isLeft ? -1 : 1);
        cursorScale.ScaleY = 12 / z;
        if (hand.JointType == JointType.HandLeft)
        {
            DebugLeftHand.Text = String.Format("Left Hand:{0:0.00} feet", z);
        }
        else
        {
            DebugRightHand.Text = String.Format("Right Hand:{0:0.00} feet", z);
        }
    }
    else
    {
        DebugLeftHand.Text = String.Empty;
        DebugRightHand.Text = String.Empty;
    }
}

編譯並運行程序,將手距Kinect不同距離,界面上的手形圖標會隨著距離的大小進行縮放;同時界面上用於調試的信息也在變化,還可以注意到,隨著遠近的不同,參考深度標注圖案,手形圖標在界面上的深度值也會相應的發生變化,有時候在圖標在某些標簽的前面,有時候在某些標簽後面。

以上例子展示了骨骼數據信息中Z值的用處,一般在開發基於Kinect應用程序時,除了用到關節點的X,Y點數據來進行定位之外,還可以用上面講的方法來使用Z值數據。有時候Z值數據可以增加應用程序的應用體驗。

2. 姿勢

姿勢(Pose)是人和其他物體的重要區別。在日常生活中,人們通過姿勢來表達感情。姿勢是一段個動作的停頓,他能傳達一些信息息 。例如在體育運動中, 裁判員會使用各種各樣的姿勢來向運動員傳遞信息。姿勢和手勢通常會混淆,但是他們是兩個不同的概念。當一個人擺一個姿勢時,他會保持身體的位置和樣子一段時間。但是手勢包含有動作,例如用戶通過手勢在觸摸屏上,放大圖片等操作。

在Kinect開發的早期,更多的經歷會放在手勢的識別上而不是姿勢的識別上。 這有點不對,但是可以理解。Kinect的賣點在於運動識別。Kinect這個名字本身就來源於單詞kinetic,他是“運動的”的意思。Kinect 做為Xbox的一個外設,使得可以使用游戲者的肢體動作,或者說是手勢,來控制游戲。手勢識別給開發者帶來了很多機遇以全新的用戶界面設計。在後面的文章中,我們將會看到,手勢並非都很簡單,有些手勢很復雜,使得應用程序很難識別出來。相對而言,姿勢是用戶有意做的動作,它通常有一定的形式。

雖然姿勢識別沒有手勢識別那樣受開發者關注,但即使在現在,很多游戲中都大量使用姿勢識別。通常,游戲者很容易模仿指定姿勢並且比較容易編寫算法來識別指定的姿勢。例如,如果開發一個用戶在天上飛的游戲。 一種控制游戲的方式是,游戲者像鳥一樣揮動手臂。揮動的頻率越快游戲角色飛的越快,這是一個手勢。還有一種方法是,展開雙臂,雙臂張得越快開,飛的越快。雙臂離身體越近,飛的越慢。在Simon Says游戲中游戲者必須伸開雙臂將雙手放到指定的位置才能開始游戲,也可以將這個改為,當用戶伸開雙臂時即可開始游戲。問題是,如何識別這一姿勢呢?

2.1 姿勢識別

身體以及各個關節點的位置定義了一個姿勢。更具體的來說,是某些關節點相對於其他關節點的位置定義了一個姿勢。姿勢的類型和復雜度決定了識別算法的復雜度。通過關節點位置的交叉或者關節點之間的角度都可以進行姿勢識別。

通過關節點交叉進行姿勢識別就是對關節點進行命中測試。在前一篇文章中,我們可以確定某一個關節點的位置是否在UI界面上某一個可視化元素的有效范圍內。我們可以對關節點做同樣的測試。但是需要的工作量要少的多,因為所有的關節點都是在同一個坐標空間中,這使得計算相對容易。例如叉腰動作(hand-on-hip),可以從骨骼追蹤的數據獲取左右髋關節和左右手的位置。然後計算左手和左髋關節的位置。如果這個距離小於某一個阈值,就認為這兩個點相交。這個阈值可以很小,對一個確定的相交點進行命中測試,就像我們對界面上可視化元素進行命中測試那樣,可能會有比較不好的用戶界面。即使通過一些平滑參數設置,從Kinect中獲取的關節點數據要完全匹配也不太現實。另外,不可能期望用戶做出一些連貫一致的動作,或者保持一個姿勢一段時間。簡而言之,用戶運動的精度以及數據的精度使得這種簡單計算不適用。因此,計算兩個點的長度,並測試長度是否在一個阈值內是唯一的選擇。

當兩個關節點比較接近時,會導致關節點位置精度進一步下降,這使得使用骨骼追蹤引擎判斷一個關節點的開始是否是另一個關節點的結束點變得困難。例如,如果將手放在臉的位置上,那麼頭的位置大致就在鼻子那個地方,手的關節點位置和頭的關節點位置就不能匹配起來。這使得難以區分某些相似的姿勢,比如,很難將手放在臉的前面,手放在頭上,和手捂住耳朵這幾個姿勢區分開來。這些還不是所有應用設計和開發者可能遇到的問題。有時候會,要擺出一個確切的姿勢也很困難,用戶是否會按照程序顯示的姿勢來做也是一個問題。

節點交叉並不需要使用X,Y的所有信息。一些姿勢只需要使用一個坐標軸信息。例如:立正姿勢,在這個姿勢中,手臂和肩膀近乎在一個垂直坐標軸內而不用考慮用戶的身體的大小和形狀。在這個姿勢中,邏輯上只需要測試手和肩部節點的X坐標的差值,如果在一個阈值內就可以判斷這些關節點在一個平面內。但是這並不能保證用戶是立正姿勢。應用程序還需要判斷手在Y坐標軸上應該低於肩部。這能提高識別精度,但仍然不夠完美。沒有一個簡單的方法能夠判定用戶所處的站立姿勢。如果用戶只是稍微將膝蓋彎曲有點,那麼這種識別方法就不是很科學。

並不是所有的姿勢識別都適合使用節點交叉法,一些姿勢使用其他方法識別精度會更高。例如,用戶伸開雙臂和肩膀在一條線上這個姿勢,稱之為T姿勢。可以使用節點相交技術,判斷手、肘、以及肩膀是否在Y軸上處於近乎相同的位置。另一種方法是計算某些關節點連線之間的角度。骨骼追蹤引擎能夠識別多達20個關節點數據。任何三個關節點就可以組成一個三角形。使用三角幾何就可以計算出他們之間的角度。

實際上我們只需要根據兩個關節點即可繪制一個三角形,第三個點有時候可以這兩個關節點來決定的。知道每個節點的坐標就可以計算每個邊長的值。然後使用余弦定理就可以計算出角度了。公式如下圖:

為了演示使用關節點三角形方法來識別姿勢,我們考慮在健美中常看到了展示肱二頭肌姿勢。用戶肩部和肘在一條線上並且和地面平行,手腕與肘部與胳膊垂直。在這個姿勢中,可以很容易看到有一個直角或者銳角三角形。我們可以使用上面所說的方法來計算三角形的每一個角度,如下圖所示:

上圖中,組成三角形的三個關節點為。肩膀,軸和手腕。根據這三個關節點的坐標可以計算三個角度。

有兩種使用節點三角形的方法。最明顯的如上面的例子那樣,使用三個節點來構造一個三角形。另一個方法就是使用兩個節點,第三個節點手動指定一個點。這種方法取決於姿勢的限制和復雜度。在上面的例子中,我們使用三個及節點的方法,因為需要的角度可以由手腕-肘-肩部構成。不論其他部位如何變化,這三者所構成的三角形相形狀相對不變。

使用兩個節點來識別這一動作只需要肘部和手腕關節點信息。將肘部作為整個坐標系統的中心或者零點。以肘部為基准點,隨便找一個水平的X軸上的點。然後就可以由這三點組成一個三角形。在兩點方法中,用戶在直立和有點傾斜姿勢下所計算得到的結果是不一樣的。

2.2 響應識別到的姿勢

了解了姿勢識別後,使得我們可以在Kinect開發中使用的姿勢信息。應用程序如何處理這些信息以及如何和用戶交互對於功能完整的應用程序來說同樣重要。識別姿勢的目的是觸發一些操作。最簡單的方法是當探測到某一姿勢後立即響應一些類似鼠標點擊之類的事件。

Kinect應用程序比較酷的一點是可以使用人體作為輸入設備,但這也帶來了一新的問題。對於應用程序設計和開發者來說,用戶通常不會如我們設想的那樣按照設定好的,或者指定的姿勢來進行運動。近十年來,應用設計者和開發者一直在關注如如何改進鍵盤及鼠標驅動的應用程序,使得這些應用程序能夠正確,健壯的處理任何用戶的鍵盤或者鼠標操作。可惜的是這些方面的經驗並不適用於Kinect。當使用鼠標時,用戶需要有意的去按下鼠標左鍵或者右鍵去進行操作。大多數鼠標點擊事件時有意的,如果是無意中按下的,應用程序也不能判斷用戶是否無意按下。但是因為需要按下按鈕,通常無意按中的情況極少。但在識別姿勢時,這種情況就不同了,因為用戶一直在擺pose。

應用程序要使用姿勢識別必須知道什麼時候該忽略什麼時候該響應特定的姿勢。如前所述,最簡單的方法是當識別到某一姿勢時立即響應。如果這是應用程序的功能,需要選擇一個用戶不可能會在休息或者放松時會產生的姿勢。選擇一個姿勢很容易,但是這個姿勢不能是戶自然而然或者大多數情況下都會產生的姿勢。這意味著姿勢必須是有意識的,就像是鼠標點擊那樣,用戶需要進行某項操作才會去做某種特定的姿勢。除了馬上響應識別到的某個姿勢外,另一種方法是觸發一個計時器。只有用戶保持這一姿勢一段時間,應用程序才會觸發相應的操作。這個和手勢有點類似。在以後的文章中我會詳細討論。

另一種方法是當用戶擺出某一系列的姿勢時才觸發某一動作。這需要用戶按照特定的序列擺出一些列的姿勢,才會執行某一操作。使用系列姿勢和一些不常用的姿勢可以使得應用程序知道用戶有意想進行某一項操作,而不是誤操作。換句話說,這能夠幫助用戶減少誤操作。

3. Simon Says 游戲中使用姿勢識別

使用上一篇文章介紹的游戲項目,我們重新實現一些功能,比如我們使用姿勢而不是可視化元素的命中測試來進行指令執行判斷。在這個版本中,Simon指令時讓用戶按照順序做一系列的姿勢,而不是觸摸那四個矩形。使用關節點角度進行姿勢識別可以給予應用程序更多的姿勢選擇。更多的和更瘋狂的姿勢可以使得游戲更加有趣和好玩。

使用姿勢替代可視化元素需要對代碼做出較大改動,但幸好的是識別姿勢的代碼比命中測試和判斷手是否在指定可視化元素有效范圍內的代碼要少。姿勢識別主要是使用三角幾何。改動代碼的同時也改變了用提體驗和游戲的玩法。所有界面上的矩形塊都會移除,只保留TextBlocks和手形圖標。我們還需要用一定的方式提示用戶擺出某種姿勢。最好的方式是顯示要擺出姿勢的圖片。為了簡便,我們這裡使用一個TextBlock,顯示姿勢的名稱,讓用戶來做指定的姿勢。

游戲的玩法也變了,去掉了所有用來進行命中測試的可視化元素後,將使用擺出某種姿勢來開始游戲。Simon Says游戲的開始姿勢和之前的一樣。前面是游戲者伸開胳膊,將雙手放到指定的區域內就開始游戲。現在是用戶擺出一個T型的姿勢。

在之前的游戲中,Simon Says指令序列指針對用戶觸摸到正確的可視化元素時移到下一個地方。在現在的版本中,游戲只需要在指定的時間內擺出某種要求的姿勢,如果在規定的時間不能擺出姿勢的話,游戲就結束了。如果識別了指定的姿勢,游戲繼續下一個姿勢,計時器歸零。

在寫代碼之前,必須把架子搭起來。為了讓游戲好玩,需要盡可能多的選擇可識別的姿勢。另外,還要能比較容易的將新的姿勢添加進來。為了創建一個姿勢庫,需要創建一個新的PoseAngle類和名為Pose的結構。如下面的代碼所示。Pose存儲了一個姿勢的名稱和一個PoseAngle數組。PoseAngle的有兩個JointType類型的成員變量用來計算角度,Angle為期望角度,Threshold 阈值。 我們並不期望用戶關節點之間的夾角和期望的角度完全吻合,這也是不可能的。就像命中測試那樣,只要關節點夾角在一定的阈值范圍內即可。

public class PoseAngle
{
    public PoseAngle(JointType centerJoint, JointType angleJoint, double angle, double threshold)
    {
        CenterJoint = centerJoint;
        AngleJoint  = angleJoint;
        Angle       = angle;
        Threshold   = threshold;
    }
    
    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 struct Pose
{
    public string Title;
    public PoseAngle[] Angles;
}

在MainWindows.xaml.cs中添加poseLibrary和startPose變量,以及一個PopulatePoseLibrary方法。代碼如下。PopulatePoseLibrary方法定義了開始姿勢(T姿勢),以及游戲中需要的四個姿勢。第一個姿勢稱之為“舉起手來”姿勢,就是雙手舉起來,第二個姿勢和第一個姿勢相反,將雙手放下來,第三個和第四個分別為只舉起左手或者右手姿勢。

private Pose[] poseLibrary;
private Pose startPose;
private void PopulatePoseLibrary()
{
    this.poseLibrary = new Pose[4];
    
    //游戲開始 Pose - 伸開雙臂 Arms Extended
    this.startPose             = new Pose();
    this.startPose.Title       = "Start Pose";
    this.startPose.Angles      = new PoseAngle[4];
    this.startPose.Angles[0]   = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    this.startPose.Angles[1]   = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 180, 20);
    this.startPose.Angles[2]   = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    this.startPose.Angles[3]   = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 0, 20);             
    
    
    //Pose 1 -舉起手來 Both Hands Up
    this.poseLibrary[0]            = new Pose();
    this.poseLibrary[0].Title      = "舉起手來(Arms Up)";
    this.poseLibrary[0].Angles     = new PoseAngle[4];
    this.poseLibrary[0].Angles[0]  = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    this.poseLibrary[0].Angles[1]  = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 90, 20);
    this.poseLibrary[0].Angles[2]  = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    this.poseLibrary[0].Angles[3]  = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 90, 20);
    
    
    //Pose 2 - 把手放下來 Both Hands Down
    this.poseLibrary[1]            = new Pose();
    this.poseLibrary[1].Title = "把手放下來(Arms Down)";
    this.poseLibrary[1].Angles     = new PoseAngle[4];            
    this.poseLibrary[1].Angles[0]  = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    this.poseLibrary[1].Angles[1]  = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 270, 20);            
    this.poseLibrary[1].Angles[2]  = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    this.poseLibrary[1].Angles[3]  = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 270, 20);
    
    
    //Pose 3 - 舉起左手 Left Up and Right Down
    this.poseLibrary[2]            = new Pose();
    this.poseLibrary[2].Title = "(舉起左手)Left Up and Right Down";
    this.poseLibrary[2].Angles     = new PoseAngle[4];
    this.poseLibrary[2].Angles[0]  = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    this.poseLibrary[2].Angles[1]  = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 90, 20);
    this.poseLibrary[2].Angles[2]  = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    this.poseLibrary[2].Angles[3]  = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 270, 20);
    
    
    //Pose 4 - 舉起右手 Right Up and Left Down
    this.poseLibrary[3]            = new Pose();
    this.poseLibrary[3].Title = "(舉起右手)Right Up and Left Down";
    this.poseLibrary[3].Angles     = new PoseAngle[4];
    this.poseLibrary[3].Angles[0]  = new PoseAngle(JointType.ShoulderLeft, JointType.ElbowLeft, 180, 20);
    this.poseLibrary[3].Angles[1]  = new PoseAngle(JointType.ElbowLeft, JointType.WristLeft, 270, 20);
    this.poseLibrary[3].Angles[2]  = new PoseAngle(JointType.ShoulderRight, JointType.ElbowRight, 0, 20);
    this.poseLibrary[3].Angles[3]  = new PoseAngle(JointType.ElbowRight, JointType.WristRight, 90, 20);
}

開始姿勢和姿勢庫定義好了之後,下面來開始改寫游戲的邏輯代碼。當游戲GameOver時,會調用ProcessGameOver方法。在前篇文章中,這個方法用來判斷用戶的雙手是否在指定的對象上,現在替換為識別用戶的姿勢是否是指定的姿勢。如下代碼展示了如何處理游戲開始和姿勢識別,IsPose方法判斷是否和指定的姿勢匹配,這個方法在多個地方都可能會用到。IsPost方法遍歷一個姿勢中的所有PoseAngle,如果任何一個關節點角度和定義的不一致,方法就返回false,表示不是指定的姿勢。方法中的if語句用來判斷角度是否在360度范圍內,如果不在,則轉換到該范圍內。

private void ProcessGameOver(Skeleton skeleton)
{
    if(IsPose(skeleton, this.startPose))
    {
        ChangePhase(GamePhase.SimonInstructing);
    }         
}
    
private bool IsPose(Skeleton skeleton, Pose pose)
{
    bool isPose = true;
    double angle;
    double poseAngle;
    double poseThreshold;
    double loAngle;
    double hiAngle;
    
    for(int i = 0; i < pose.Angles.Length && isPose; i++)
    {
        poseAngle       = pose.Angles[i].Angle;
        poseThreshold   = pose.Angles[i].Threshold;
        angle           = GetJointAngle(skeleton.Joints[pose.Angles[i].CenterJoint], skeleton.Joints[pose.Angles[i].AngleJoint]);
    
        hiAngle = poseAngle + poseThreshold;
        loAngle = poseAngle - poseThreshold;
    
        if(hiAngle >= 360 || loAngle < 0)
        {
            loAngle = (loAngle < 0) ? 360 + loAngle : loAngle;
            hiAngle = hiAngle % 360;
    
            isPose = !(loAngle > angle && angle > hiAngle);
        }
        else
        {
            isPose = (loAngle <= angle && hiAngle >= angle);
        }
    }
    return isPose;
}

IsPost方法調用GetJointAngle方法來計算兩個關節點之間的角度。GetJointAngle調用GetJointPoint方法來獲取每一個節點在主UI布局空間中的坐標。這一步其實沒有太大必要,原始的位置信息也可以用來計算角度。但是,將關節點的坐標轉換到主UI界面上來能夠幫助我們進行調試。獲得了節點的位置後,使用余弦定理計算節點間的角度。Math.Acos返回的值是度,將其轉換到角度值。If語句處理角度值在180-360的情況。余弦定理返回的角度在0-180度內,if語句將在第三和第四象限的值調整到第一第二象限中來。

private double GetJointAngle(Joint centerJoint, Joint angleJoint)
{
    Point primaryPoint  = GetJointPoint(this.KinectDevice, centerJoint, this.LayoutRoot.RenderSize, new Point());
    Point anglePoint    = GetJointPoint(this.KinectDevice, angleJoint, this.LayoutRoot.RenderSize, new Point());
    Point x             = new Point(primaryPoint.X + anglePoint.X, primaryPoint.Y);
    
    double a;
    double b;
    double c;
    
    a = Math.Sqrt(Math.Pow(primaryPoint.X - anglePoint.X, 2) + Math.Pow(primaryPoint.Y - anglePoint.Y, 2));
    b = anglePoint.X;
    c = Math.Sqrt(Math.Pow(anglePoint.X - x.X, 2) + Math.Pow(anglePoint.Y - x.Y, 2));
    
    double angleRad = Math.Acos((a * a + b * b - c * c) / (2 * a * b));
    double angleDeg = angleRad * 180 / Math.PI;
    
    if(primaryPoint.Y < anglePoint.Y)
    {
        angleDeg = 360 - angleDeg;                            
    }
    
    return angleDeg;
}

程序還必須識別姿勢並啟動程序。當程序識別到啟動的姿勢是,將游戲的狀態切換到SimonInstructing。這部分代碼和GenerateInstructions及DisplayInstructions是分開的。將GenerateInstructions產生的指令改為隨機的從姿勢庫中選取某一個姿勢。然後使用選擇的姿勢填充指令集合。DisplayInstructions方法可以使用自己的方法比如圖片來給用戶以提示。一旦游戲顯示完指令,游戲轉入PlayerPerforming階段。這個階段給了游戲者一定的時間來擺出特定的姿勢,當程序識別到需要的姿勢時,轉到下一個姿勢,並重啟計時器。如果超過給定時間仍然沒有給出指定的姿勢,游戲結束。WPF中System.Windows.Threading命名空間下的DispatcherTimer類可以簡單的完成計時器的功能。下面的代碼顯示了如何使用DispatcherTimer,代碼首先實例化了一個類,然後設定間隔時間。添加PoseTimer局部變量,然後將下面的代碼添加到主窗體的構造函數中。

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();
    ……………………
}

最後一部分更新是ProcessPlayerPerforming方法,代碼如下。每一次方法調用,都會判斷當前的姿勢是否在姿勢庫中匹配,如果匹配正確,那麼停止計時器,進入到下一個姿勢指令。當用戶到了姿勢序列中的末尾時,游戲更改姿勢指令。否則,刷新到下一個姿勢。

private void ProcessPlayerPerforming(Skeleton skeleton)
{           
    int instructionSeq = this.instructionSequence[this.instructionPosition];
    if(IsPose(skeleton, this.poseLibrary[instructionSeq]))
    {     
        this.poseTimer.Stop();           
        this.instructionPosition++;
    
        if(this.instructionPosition >= this.instructionSequence.Length)
        {
            ChangePhase(GamePhase.SimonInstructing);
        }
        else
        {
            //TODO: Notify the user of correct pose
            this.poseTimer.Start();
        }
    }
}

將以上的這些代碼添加到項目中區之後,Simon Says現在就是用姿勢識別取代可視化元素的命中測試來進行判別用戶是否完成了指定的指令了。運行游戲,為了截圖,下面都是我端著鍵盤的姿勢,大家可以下載本文的代碼回去自己玩哈。

這個例子展示了在實際應用中,如何使用姿勢識別。可以試著在姿勢庫中添加其他姿勢,然後測試。你會發現並不是所有的姿勢都是那麼容易就能夠識別的。

對於任何程序,尤其是Kinect應用程序,用戶體驗對於應用的成功與否至關重要。運行Simon Says游戲,你會感覺游戲缺少了很多東西。游戲界面缺少一些使得游戲交互更加有趣的元素。這個游戲缺少對用戶動作的反饋,這對用戶體驗很重要。要使得Simon Say變成一個真正意義上的Kienct驅動的游戲,在游戲開始或者結束時必須給予一些提示信息。當游戲者正確的做了一個指定的姿勢時應該給予一定的鼓勵。有以下幾個方面可以增加游戲的趣味性和可玩性:

添加更多的姿勢。使用Pose類可以很容易的添加新的姿勢。需要做的只是定義姿勢裡一些關節點之間的夾角。

可以調整計時器,通關後,下一個姿勢的計時時間可以加短一些,這樣可以使得游戲更加緊張刺激。

將計時器的時間顯示在UI界面上可以增加游戲的緊張感。將計時器時間顯示在UI界面上,並使用一些動畫效果,這是游戲中常用的做法。

在游戲過程中,對游戲者的姿勢進行截屏,在游戲結束是可以幻燈片播放用戶的姿勢,這樣可以使得游戲更加有趣。

查看本欄目

4. 擴展與代碼重構

本例中,使用的最多的地方是姿勢識別部分代碼。在Simon Says游戲中,我們寫了很多代碼來啟動開始姿勢識別引擎。在未來Kinect SDK中可能會增加姿勢識別引擎,但是目前的SDK版本中沒有這一功能。考慮到Kinect SDK是否會在未來增加這一功能,所以很有必要創建這麼一個工具。Kinect開發社區有一些類似的工具,但都部是標准的。

可以考慮創建一個PoseEngine類,他有一個PoseDetected事件。當引擎識別到骨骼數據擺出了一個姿勢時,觸發該事件。默認地,PoseEngine類監聽SkeletonFrameReady事件,他能夠一幀一幀的使用某種方法測試骨骼數據幀,這使得能夠支持“拉”數據模型。PosEngine類有一個Pose集合,他定義了一些能夠識別的姿勢合集。可以就像.Net中的List那樣使用Add和Remove方法進行添加或者刪除,開發者可以為應用程序定義一個姿勢庫。

為了能夠動態的添加和刪除姿勢,姿勢定義那部分代碼不能像我們之前的Simon Says游戲中的那樣硬編碼。最簡單的方法是使用序列化。序列化姿勢數據有兩個好處,一是姿勢很容易從應用程序中添加和移除。應用程序可以在運行時動態對添加到配置文件中的姿勢進行讀取。更進一步的,我們可以將這些姿勢配置持久化,使得我們可以創建一個專門的工具來捕捉或者定義姿勢。

開發一個能夠捕捉用戶姿勢,並將數據序列化成應用程序直接使用的數據源不是太難。這個程序可以使用前面我們所講到的知識開發出來。可以在SkeletonView自定義控件的基礎上,添加關節點之間角度計算邏輯。然後顯示在SkeletonVeiw的輸出信息中,將角度信息顯示在關節點位置。姿勢捕捉工具使用函數來對這用戶的姿勢進行截圖,這截圖實際上是一系列關節點之間的角度信息,截圖可以序列化,使得能夠很容易的添加到其他應用程序中去。

將SkeletonView根據上面的想法進行改進後,可以顯示關節點夾角信息。下圖展示了可能的輸出。使得能夠很容易的看出各個關節點之間的夾角。可以根據這個夾角來手動的定義一些姿勢。甚至可以開發出一些工具根據這些夾角來生成姿勢配置文件。將夾角顯示在UI上也能提供很多有用的調試信息。

5. 結語

本文首先介紹了如何使用骨骼節點數據中的Z值來創建更好的體驗,然後討論了姿勢識別的常用方法,並結合上文中Simon Says的游戲,把它改造為了使用姿勢識別來判斷指令執行是否正確,最後討論了該游戲可以改進的一些地方和創建姿勢識別引擎的一些設想。本文是骨骼追蹤的最後一節內容,從下篇文章開始將會介紹手勢識別,敬請關注。

本文所有代碼點擊此處下載,希望本文對您了解Kinect SDK有所幫助!

作者:   yangecnu(yangecnu's Blog on 博客園)

出處:http://www.cnblogs.com/yangecnu/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved