http://www.ituring.com.cn/article/196144
作者/ 吳國斌
博士,PMP,微軟亞洲研究院學術合作經理。負責中國高校及科研機構Kinect for Windows學術合作計划及微軟精英大挑戰Kinect主題項目。曾擔任微軟TechEd2011 Kinect論壇講師,微軟亞洲教育高峰會Kinect分論壇主席,中國計算機學會學科前沿講習班Kinect主題學術主任。
骨骼追蹤技術是Kinect的核心技術,它可以准確標定人體的20個關鍵點,並能對這20個點的位置進行實時追蹤。利用這項技術,可以開發出各種基於體感人機交互的有趣應用。
骨骼追蹤數據的結構
目前,Kinect for Windows SDK中的骨骼API可以提供位於Kinect前方至多兩個人的位置信息,包括詳細的姿勢和骨骼點的三維坐標信息。另外,Kinect for Windows SDK最多可以支持20個骨骼點。數據對象類型以骨骼幀的形式提供,每一幀最多可以保存20個點,如圖1所示。

圖1 20個骨骼點示意圖
在SDK中每個骨骼點都是用Joint類型來表示的,每一幀的20個骨骼點組成基於Joint類型的集合。此類型包含3個屬性,具體內容如下所示。
-
JointType:骨骼點的類型,這是一種枚舉類型,列舉出了20個骨骼點的特定名稱,比如“HAND_LEFT”表示該骨骼點是左手節點。 -
Position:SkeletonPoint類型表示骨骼點的位置信息。SkeletonPoint是一個結構體,包含X、Y、Z三個數據成員,用以存儲骨骼點的三維坐標。 -
TrackingState:JointTrackingState類型也是一種枚舉類型,表示該骨骼點的追蹤狀態。其中,Tracked表示正確捕捉到該骨骼點,NotTracked表示沒有捕捉到骨骼點,Inferred表示狀態不確定。
半身模式
如果應用程序只需要捕捉上半身的姿勢動作,就可以采用Kinect for Windows SDK提供的半身模式(Seated Mode)。在半身模式下,系統只捕捉人體上半身10個骨骼點的信息,而忽略下半身另外10個骨骼點的位置信息,這樣就解決了用戶坐在椅子上時無法被Kinect識別的問題,即使下半身骨骼點的數據不穩定或是不存在也不會對上半身的骨骼數據造成影響。而且當用戶距離Kinect設備只有0.4米時,應用程序仍能正常地進行骨骼追蹤,這就大幅提高了骨骼追蹤的性能。
半身模式定義在枚舉類型SkeletonTrackingMode中,該類型包含兩個枚舉值:Default和Seated。前者為默認的骨骼追蹤模式,會正常捕捉20個骨骼點;后者為半身模式,選擇該值則只捕捉上半身的10個骨骼點。
開發者可以通過改變SkeletonStream對象的TrackingMode屬性來設置骨骼追蹤的模式,代碼如下:
kinectSensor.SkeletonStream.TrackingMode = SkeletonTrackingMode.Seated;
骨骼追蹤數據的獲取方式
應用程序獲取下一幀骨骼數據的方式同獲取彩色圖像和深度圖像數據的方式一樣,都是通過調用回調函數並傳遞一個緩存實現的,獲取骨骼數據調用的是OpenSkeletonFrame()函數。如果最新的骨骼數據已經准備好了,那么系統就會將其復制到緩存中;但如果應用程序發出請求時,新的骨骼數據還未准備好,此時可以選擇等待下一個骨骼數據直至其准備完畢,或者立即返回稍后再發送請求。對於NUI骨骼API而言,相同的骨骼數據只會提供一次。
NUI骨骼API提供了兩種應用模型,分別是輪詢模型和時間模型,簡要介紹如下。
-
輪詢模型是讀取骨骼事件最簡單的方式,通過調用
SkeletonStream類的OpenNextFrame()函數即可實現。OpenNextFrame()函數的聲明如下所示。public SkeletonFrame OpenNextFrame ( int millisecondsWait )可以傳遞參數指定等待下一幀骨骼數據的時間。當新的數據准備好或是超出等待時間時,
OpenNextFrame()函數才會返回。 -
時間模型以事件驅動的方式獲取骨骼數據,更加靈活、准確。應用程序傳遞一個事件處理函數給
SkeletonFrameReady事件,該事件定義在KinectSensor類中。當下一幀的骨骼數據准備好時,會立即調用該事件回調函數。因此Kinect應用應該通過調用OpenSkeletonFrame()函數來實時獲取骨骼數據。
實例——調用API獲取骨骼數據並實時繪制
本實例程序將實現獲取骨骼數據,然后將骨骼點的坐標作為Ellipse控件的20個位置坐標,同時用線段將相應的點連接起來,最后將繪制出的骨架映射到彩色圖像上。讀者可以在實例1的基礎上開始本實例,具體操作步驟如下所示。
1. 在Window_Loaded()函數中添加下列骨骼數據流的啟動函數,並添加kinectSensor_SkeletonFrameReady事件處理函數相應的SkeletonFrameReady事件。
kinectSensor.SkeletonStream.Enable(); kinectSensor.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(kinectSensor_SkeletonFrameReady);
2. 准備WPF界面。通過以下代碼在界面上添加20個小圓點,分別跟蹤由Kinect for Windows SDK獲取到的人體的20個關鍵點,並將這20個點標記為不同的顏色。
<Canvas Name="SkeletonCanvas" Visibility="Visible"> <Ellipse Canvas.Left="0" Canvas.Top="0" Height="10" Name="headPoint" Width="10" Fill="Red" /> <Ellipse Canvas.Left="10" Canvas.Top="0" Height="10" Name="shouldercenterPoint" Width="10" Fill="Blue" /> <Ellipse Canvas.Left="20" Canvas.Top="0" Height="10" Name="shoulderrightPoint" Width="10" Fill="Orange" /> …省略中間的Ellipse定義 <Image Canvas.Left="303" Canvas.Top="161" Height="150" Name="image1" Stretch="Fill" Width="200" /> </Canvas>
此時,設計窗口如圖2所示。

圖2 WPF設計界面
3. 編寫kinectSensor_SkeletonFrameReady()事件處理函數。正確連接Kinect后,當用戶站在Kinect前並且Kinect能夠正確識別人體時,將觸發該事件處理函數,其代碼如下:
private void kinectSensor_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e) { using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame()) { if (skeletonFrame != null) { skeletonData = new Skeleton[kinectSensor.SkeletonStream.FrameSkeletonArrayLength]; skeletonFrame.CopySkeletonDataTo(this.skeletonData); Skeleton skeleton = (from s in skeletonData where s.TrackingState == SkeletonTrackingState.Tracked select s).FirstOrDefault(); if (skeleton!=null) { SetAllPointPosition(skeleton); } } } }
上述代碼使用LINQ語句來獲取TrackingState等於Tracked的骨骼數據。目前SDK最多可以追蹤兩幅骨骼。為了簡化起見,本實例只對捕捉到的第一幅骨骼進行追蹤和顯示。
4. 在Skeleton對象的Joints屬性集合中保存了所有骨骼點的信息,每個骨骼點的信息都是一個Joint對象。為了得到特定的骨骼點,同樣使用LINQ語句對Joint的JointType屬性進行篩選,相關代碼如下:
Joint headJoint = (from j in skeleton.Joints where j.JointType == JointType.Head select j).FirstOrDefault();
在本實例程序中,需要遍歷每個骨骼點,並分別對其進行處理。這里使用foreach語句來實現,並根據JointType屬性進行處理。在SetAllPointPosition()函數中可以看到具體的實現細節。
foreach (Joint joint in skeleton.Joints) { Point jointPoint = GetDisplayPosition(joint); switch (joint.JointType) { case JointType.Head: SetPointPosition(headPoint, joint); headPolyline.Points.Add(jointPoint); break; ... } }
5. 前面提到,Joint的Position屬性的X、Y、Z表示該骨骼點的三維位置,其中X和Y的范圍都是-1~1,而Z是Kinect到識別物體的距離。
為了能更好地將這20個點顯示出來,需要對Position的X值和Y值進行縮放,可以通過以下函數實現。
private Point GetDisplayPosition(Joint joint) { var scaledJoint = joint.ScaleTo(640, 480); return new Point(scaledJoint.Position.X, scaledJoint.Position.Y); }
上面語句中,ScaleTo函數的最后兩個參數640和480分別代表原始數據X和Y的最大值,通過該語句可以將X坐標放大到0~640范圍內的任意值,將Y坐標放大到0~480范圍內的任意值。該坐標是相對於應用程序窗口的左上角(0,0)而言的,窗口的寬和高分別是640和480,以保證彩色圖像和骨骼繪制的結果相匹配。
其中,ScaleTo()函數是Coding4Fun的Help類中的方法。Coding4Fun是一個Kinect開發輔助類庫。讀者可以從http://c4fkinect.codeplex.com/下載該類庫,並通過“Add Reference”菜單項將Coding4Fun.Kinect.Wpf.dll添加到項目中。
6. 編寫一個函數,將每個骨骼點轉換后的(X,Y)坐標值分別映射到相應的Ellipse控件的Left和Top屬性上,其代碼如下:
private void SetPointPosition(FrameworkElement ellipse, Joint joint) { var scaledJoint = joint.ScaleTo(640, 480); Canvas.SetLeft(ellipse, scaledJoint.Position.X); Canvas.SetTop(ellipse, scaledJoint.Position.Y); SkeletonCanvas.Children.Add(ellipse); }
使用Polyline類表示骨架線,顯而易見,骨架由5條多段線組成,分別定義它們,並在遍歷所有骨骼點時分類存儲相應的點。詳見SetAllPointPosition()函數,相關代碼如下:
Polyline headPolyline = new Polyline(); Polyline handleftPolyline = new Polyline(); Polyline handrightPolyline = new Polyline(); Polyline footleftPolyline = new Polyline(); Polyline footrightPolyline = new Polyline(); private void SetAllPointPosition(Skeleton skeleton) { SkeletonCanvas.Children.Clear(); headPolyline.Points.Clear(); handleftPolyline.Points.Clear(); handrightPolyline.Points.Clear(); footleftPolyline.Points.Clear(); footrightPolyline.Points.Clear(); foreach (Joint joint in skeleton.Joints) { Point jointPoint = GetDisplayPosition(joint); switch (joint.JointType) { case JointType.Head: SetPointPosition(headPoint, joint); headPolyline.Points.Add(jointPoint); break; case JointType.ShoulderCenter: SetPointPosition(shouldercenterPoint, joint); headPolyline.Points.Add(jointPoint); handleftPolyline.Points.Add(jointPoint); handrightPolyline.Points.Add(jointPoint); break; case JointType.ShoulderLeft: SetPointPosition(shoulderleftPoint, joint); handleftPolyline.Points.Add(jointPoint); break; ... case JointType.FootRight: SetPointPosition(footrightPoint, joint); footrightPolyline.Points.Add(jointPoint); break; default: 