Kinect for Windows SDK開(kāi)發(fā)入門(六):骨骼追蹤基礎(chǔ) 上
掃描二維碼
隨時(shí)隨地手機(jī)看文章
Kinect產(chǎn)生的景深數(shù)據(jù)作用有限,要利用Kinect創(chuàng)建真正意義上交互,有趣和難忘的應(yīng)用,還需要除了深度數(shù)據(jù)之外的其他數(shù)據(jù)。這就是骨骼追蹤技術(shù)的初衷,骨骼追蹤技術(shù)通過(guò)處理景深數(shù)據(jù)來(lái)建立人體各個(gè)關(guān)節(jié)的坐標(biāo),骨骼追蹤能夠確定人體的各個(gè)部分,如那部分是手,頭部,以及身體。骨骼追蹤產(chǎn)生X,Y,Z數(shù)據(jù)來(lái)確定這些骨骼點(diǎn)。在上文中,我們討論了景深圖像處理的一些技術(shù)。骨骼追蹤系統(tǒng)采用的景深圖像處理技術(shù)使用更復(fù)雜的算法如矩陣變換,機(jī)器學(xué)習(xí)及其他方式來(lái)確定骨骼點(diǎn)的坐標(biāo)。
本文首先用一個(gè)例子展示骨骼追蹤系統(tǒng)涉及的主要對(duì)象,然后在此基礎(chǔ)上詳細(xì)討論骨骼追蹤中所涉及的對(duì)象模型。
1. 獲取骨骼數(shù)據(jù)本節(jié)將會(huì)創(chuàng)建一個(gè)應(yīng)用來(lái)將獲取到的骨骼數(shù)據(jù)繪制到UI界面上來(lái)。在開(kāi)始編碼前,首先來(lái)看看一些基本的對(duì)象以及如何從這些對(duì)象中如何獲取骨骼數(shù)據(jù)。在進(jìn)行數(shù)據(jù)處理之前了解數(shù)據(jù)的格式也很有必要。這個(gè)例子很簡(jiǎn)單明了,只需要骨骼數(shù)據(jù)對(duì)象然后將獲取到的數(shù)據(jù)繪制出來(lái)。
彩色影像數(shù)據(jù),景深數(shù)據(jù)分別來(lái)自ColorImageSteam和DepthImageStream,同樣地,骨骼數(shù)據(jù)來(lái)自SkeletonStream。訪問(wèn)骨骼數(shù)據(jù)和訪問(wèn)彩色影像數(shù)據(jù)、景深數(shù)據(jù)一樣,也有事件模式和 “拉”模式兩種方式。在本例中我們采用基于事件的方式,因?yàn)檫@種方式簡(jiǎn)單,代碼量少,并且是一種很普通基本的方法。KinectSensor對(duì)象有一個(gè)名為SkeletonFrameReady事件。當(dāng)SkeletonStream中有新的骨骼數(shù)據(jù)產(chǎn)生時(shí)就會(huì)觸發(fā)該事件。通過(guò)AllFramesReady事件也可以獲取骨骼數(shù)據(jù)。在下一節(jié)中,我們將會(huì)詳細(xì)討論骨骼追蹤對(duì)象模型,現(xiàn)在我們只展示如何從SkeletonStream流中獲取骨骼數(shù)據(jù)。SkeletonStream產(chǎn)生的每一幀數(shù)據(jù)都是一個(gè)骨骼對(duì)象集合。每一個(gè)骨骼對(duì)象包含有描述骨骼位置以及骨骼關(guān)節(jié)的數(shù)據(jù)。每一個(gè)關(guān)節(jié)有一個(gè)唯一標(biāo)示符如頭(head)、肩(shoulder)、肘(dlbow)等信息和3D向量數(shù)據(jù)。
現(xiàn)在來(lái)寫(xiě)代碼。首先創(chuàng)建一個(gè)新的wpf工程文件,添加Microsoft.Kinect.dll。添加基本查找和初始化傳感器的代碼,這些代碼參考之前的文章。在開(kāi)始啟動(dòng)傳感器之前,初始化SkeletonStream數(shù)據(jù)流,并注冊(cè)KinectSensor對(duì)象的SkeletonFrameReady事件,這個(gè)例子沒(méi)有使用彩色攝像機(jī)和紅外攝像機(jī)產(chǎn)生的數(shù)據(jù),所以不需要初始化這些數(shù)據(jù)流。UI界面采用默認(rèn)的,將Grid的名稱改為L(zhǎng)ayoutRoot,之后就再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>
后臺(tái)邏輯代碼如下:
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數(shù)組以及該數(shù)組如何在流初始化時(shí)進(jìn)行內(nèi)存分配的。Kinect能夠追蹤到的骨骼數(shù)量是一個(gè)常量。這使得我們?cè)谡麄€(gè)應(yīng)用程序中能夠一次性的為數(shù)組分配內(nèi)存。為了方便,Kinect SDK在SkeletonStream對(duì)象中定義了一個(gè)能夠追蹤到的骨骼個(gè)數(shù)常量FrameSkeletonArrayLength,使用這個(gè)常量可以方便的對(duì)數(shù)組進(jìn)行初始化。代碼中也定義了一個(gè)筆刷數(shù)組,這些筆刷在繪制骨骼時(shí)對(duì)多個(gè)游戲者可以使用不同的顏色進(jìn)行繪制。也可以將筆刷數(shù)組中的顏色設(shè)置為自己喜歡的顏色。
下面的代碼展示了SkeletonFrameReady事件的響應(yīng)方法,每一次事件被激發(fā)時(shí),通過(guò)調(diào)用事件參數(shù)的OpenSkeletonFrame方法就能夠獲取當(dāng)前的骨骼數(shù)據(jù)幀。剩余的代碼遍歷骨骼數(shù)據(jù)幀的Skeleton數(shù)組frameSkeletons,在UI界面通過(guò)關(guān)節(jié)點(diǎn)將骨骼連接起來(lái),用一條直線代表一根骨骼。UI界面簡(jiǎn)單,將Grid元素作為根結(jié)點(diǎn),并將其背景設(shè)置為白色。
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); } } } }}
循環(huán)遍歷frameSkeletons對(duì)象,每一次處理一個(gè)骨骼,在處理之前需要判斷是否是一個(gè)追蹤好的骨骼,可以使用Skeleton對(duì)象的TrackingState屬性來(lái)判斷,只有骨骼追蹤引擎追蹤到的骨骼我們才進(jìn)行繪制,忽略哪些不是游戲者的骨骼信息即過(guò)濾掉那些TrackingState不等于SkeletonTrackingState.Tracked的骨骼數(shù)據(jù)。Kinect能夠探測(cè)到6個(gè)游戲者,但是同時(shí)只能夠追蹤到2個(gè)游戲者的骨骼關(guān)節(jié)位置信息。在后面我們將會(huì)詳細(xì)討論TrackingState這一屬性。
處理骨骼數(shù)據(jù)相對(duì)簡(jiǎn)單,首先,我們根Kinect追蹤到的游戲者的編號(hào),選擇一種顏色筆刷。然后利用這只筆刷繪制曲線。CreateFigure方法為每一根骨骼繪制一條直線。GetJointPoint方法在繪制骨骼曲線中很關(guān)鍵。該方法以關(guān)節(jié)點(diǎn)的三維坐標(biāo)作為參數(shù),然后調(diào)用KinectSensor對(duì)象的MapSkeletonPointToDepth方法將骨骼坐標(biāo)轉(zhuǎn)換到深度影像坐標(biāo)上去。后面我們將會(huì)討論為什么需要這樣轉(zhuǎn)換以及如何定義坐標(biāo)系統(tǒng)。現(xiàn)在我們只需要知道的是,骨骼坐標(biāo)系和深度坐標(biāo)及彩色影像坐標(biāo)系不一樣,甚至和UI界面上的坐標(biāo)系不一樣。在開(kāi)發(fā)Kinect應(yīng)用程序中,從一個(gè)坐標(biāo)系轉(zhuǎn)換到另外一個(gè)坐標(biāo)系這樣的操作非常常見(jiàn),GetJointPoint方法的目的就是將骨骼關(guān)節(jié)點(diǎn)的三維坐標(biāo)轉(zhuǎn)換到UI繪圖坐標(biāo)系統(tǒng),返回該骨骼關(guān)節(jié)點(diǎn)在UI上的位置。下面的代碼展示了CreateFigure和GetJointPoint這兩個(gè)方法。
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);}
值得注意的是,骨骼關(guān)節(jié)點(diǎn)的三維坐標(biāo)中我們舍棄了Z值,只用了X,Y值。Kinect好不容易為我們提供了每一個(gè)節(jié)點(diǎn)的深度數(shù)據(jù)(Z值)而我們卻沒(méi)有使用,這看起來(lái)顯得很浪費(fèi)。其實(shí)不是這樣的,我們使用了節(jié)點(diǎn)的Z值,只是沒(méi)有直接使用,沒(méi)有在UI界面上展現(xiàn)出來(lái)而已。在坐標(biāo)空間轉(zhuǎn)換中是需要深度數(shù)據(jù)的??梢栽囋囋贕etJointPoint方法中,將joint的Position中的Z值改為0,然后再調(diào)用MapSkeletonPointToDepth方法,你會(huì)發(fā)現(xiàn)返回的對(duì)象中x和y值均為0,可以試試,將圖像以Z值進(jìn)行等比縮放,可以發(fā)現(xiàn)圖像的大小是和Z值(深度)成反的。也就是說(shuō),深度值越小,圖像越大,即人物離Kinect越近,骨骼數(shù)據(jù)越大。
運(yùn)行程序,會(huì)得到如下骨骼圖像,這個(gè)是手握鍵盤準(zhǔn)備截圖的姿勢(shì)。一開(kāi)始可能需要調(diào)整一些Form窗體的大小。程序會(huì)為每一個(gè)游戲者以一種顏色繪制骨骼圖像,可以試著在Kinect前面移動(dòng),可以看到骨骼圖像的變化,也可以走進(jìn)然后走出圖像以觀察顏色的變化。仔細(xì)觀察有時(shí)候可以看到繪圖出現(xiàn)了一些奇怪的圖案,在討論完骨骼追蹤相關(guān)的API之后,就會(huì)明白這些現(xiàn)象出現(xiàn)的原因了。
2. 骨骼對(duì)象模型Kinect SDK中骨骼追蹤有一些和其他對(duì)象不一樣的對(duì)象結(jié)構(gòu)和枚舉。在SDK中骨骼追蹤相關(guān)的內(nèi)容幾乎占據(jù)了三分之一的內(nèi)容,可見(jiàn)Kinect中骨骼追蹤技術(shù)的重要性。下圖展示了骨骼追蹤系統(tǒng)中涉及到的一些主要的對(duì)象模型。有四個(gè)最主要的對(duì)象,他們是SkeletonStream,SkeletonFrame,Skeleton和Joint。下面將詳細(xì)介紹這四個(gè)對(duì)象。
2.1 SkeletonStream對(duì)象SkeletonStream對(duì)象產(chǎn)生SkeletonFrame。從SkeletonStream獲取骨骼幀數(shù)據(jù)和從ColorStream及DepthStream中獲取數(shù)據(jù)類似??梢宰?cè)SkeletonFrameReady事件或者AllFramesReady事件通過(guò)事件模型來(lái)獲取數(shù)據(jù),或者是使用OpenNextFrame方法通過(guò)“拉”模型來(lái)獲取數(shù)據(jù)。不能對(duì)同一個(gè)SkeletonStream同時(shí)使用這兩種模式。如果注冊(cè)了SkeletonFrameReady事件然后又調(diào)用OpenNextFrame方法將會(huì)返回一個(gè)InvalidOperationException異常。
SkeletonStream的啟動(dòng)和關(guān)閉
除非啟動(dòng)了SkeletonStream對(duì)象,否則,不會(huì)產(chǎn)生任何數(shù)據(jù),默認(rèn)情況下,SkeletonStream對(duì)象是關(guān)閉的。要使SkeletonStream產(chǎn)生數(shù)據(jù),必須調(diào)用對(duì)象的Enabled方法。相反,調(diào)用Disable方法能夠使SkeletonStream對(duì)象暫停產(chǎn)生數(shù)據(jù)。SkeletonStream有一個(gè)IsEnabled方法來(lái)描述當(dāng)前SkeletonStream對(duì)象的狀態(tài)。只有SkeletonStream對(duì)象啟動(dòng)了,KinectSensor對(duì)象的SkeletonFrameReady事件才能被激活。如果要使用“拉”模式來(lái)獲取數(shù)據(jù)SkeletonStream也必須啟動(dòng)后才能調(diào)用OpenNextFrame方法。否則也會(huì)拋出InvalidOperationException異常。
一般地在應(yīng)用程序的聲明周期中,一旦啟動(dòng)了SkeletonStream對(duì)象,一般會(huì)保持啟動(dòng)狀態(tài)。但是在有些情況下,我們希望關(guān)閉SkeletonStream對(duì)象。比如在應(yīng)用程序中使用多個(gè)Kinect傳感器時(shí)。只有一個(gè)Kinect傳感器能夠產(chǎn)生骨骼數(shù)據(jù),這也意味著,即使使用多個(gè)Kinect傳感器,同時(shí)也只能追蹤到兩個(gè)游戲者的骨骼數(shù)據(jù)信息。在應(yīng)用程序執(zhí)行的過(guò)程中,有可能會(huì)關(guān)閉某一個(gè)Kinect傳感器的SkeletonStream對(duì)象而開(kāi)啟另一個(gè)Kinect傳感器的SkeletonStream對(duì)象。
另一個(gè)有可能關(guān)閉骨骼數(shù)據(jù)產(chǎn)生的原因是出于性能方面的考慮,骨骼數(shù)據(jù)處理是很耗費(fèi)計(jì)算性能的操作。打開(kāi)骨骼追蹤是可以觀察的到CPU的占用率明顯增加。當(dāng)不需要骨骼數(shù)據(jù)時(shí),關(guān)閉骨骼追蹤很有必要。例如,在有些游戲場(chǎng)景中可能在展現(xiàn)一些動(dòng)畫(huà)效果或者播放視頻,在這個(gè)動(dòng)畫(huà)效果或者視頻播放時(shí),停止骨骼追蹤可能可以使得游戲更加流暢。
當(dāng)然關(guān)閉SkeletonStream也有一些副作用。當(dāng)SkeletonStream的狀態(tài)發(fā)生改變時(shí),所有的數(shù)據(jù)產(chǎn)生都會(huì)停止和從新開(kāi)始。SkeletonStream的狀態(tài)改變會(huì)使傳感器重新初始化,將TimeStamp和FrameNumber重置為0。在傳感器重新初始化時(shí)也有幾毫秒的延遲。
平滑化
在前面的例子中,會(huì)注意到,骨骼運(yùn)動(dòng)會(huì)呈現(xiàn)出跳躍式的變化。有幾個(gè)原因會(huì)導(dǎo)致出現(xiàn)這一問(wèn)題,可能是應(yīng)用程序的性能,游戲者的動(dòng)作不夠連貫,也有可能是Kinect硬件的性能問(wèn)題。骨骼關(guān)節(jié)點(diǎn)的相對(duì)位置可能在幀與幀之間變動(dòng)很大,這回對(duì)應(yīng)用程序產(chǎn)生一些負(fù)面的影像。除了會(huì)影像用戶體驗(yàn)和不愉快意外,也可能會(huì)導(dǎo)致用戶的形象或者手的顫動(dòng)抽搐而使用戶感到迷惑。
SkeletonStream對(duì)象有一種方法能夠解決這個(gè)問(wèn)題。他通過(guò)將骨骼關(guān)節(jié)點(diǎn)的坐標(biāo)標(biāo)準(zhǔn)化來(lái)減少幀與幀之間的關(guān)節(jié)點(diǎn)位置差異。當(dāng)初始化SkeletonStream對(duì)象調(diào)用重載的Enable方法時(shí)可以傳入一個(gè)TransformSmoothParameters參數(shù)。SkeletonStream對(duì)象有兩個(gè)與平滑有關(guān)只讀屬性:IsSmoothingEnabled和SmoothParameters。當(dāng)調(diào)用Enable方法傳入了TransformSmoothParameters是IsSmoothingEnabled返回true而當(dāng)使用默認(rèn)的不帶參數(shù)的Enable方法初始化時(shí),IsSmoothingEnabled對(duì)象返回false。SmoothParameters屬性用來(lái)存儲(chǔ)定義平滑參數(shù)。TransformSmoothParameters這個(gè)結(jié)構(gòu)定義了一些屬性:
修正值(Correction)屬性,接受一個(gè)從0-1的浮點(diǎn)型。值越小,修正越多。抖動(dòng)半徑(JitterRadius)屬性,設(shè)置修正的半徑,如果關(guān)節(jié)點(diǎn)“抖動(dòng)”超過(guò)了設(shè)置的這個(gè)半徑,將會(huì)被糾正到這個(gè)半徑之內(nèi)。該屬性為浮點(diǎn)型,單位為米。最大偏離半徑(MaxDeviationRadius)屬性,用來(lái)和抖動(dòng)半徑一起來(lái)設(shè)置抖動(dòng)半徑的最大邊界。任何超過(guò)這一半徑的點(diǎn)都不會(huì)認(rèn)為是抖動(dòng)產(chǎn)生的,而被認(rèn)定為是一個(gè)新的點(diǎn)。該屬性為浮點(diǎn)型,單位為米。預(yù)測(cè)幀大小(Prediction)屬性,返回用來(lái)進(jìn)行平滑需要的骨骼幀的數(shù)目。平滑值(Smoothing)屬性,設(shè)置處理骨骼數(shù)據(jù)幀時(shí)的平滑量,接受一個(gè)0-1的浮點(diǎn)值,值越大,平滑的越多。0表示不進(jìn)行平滑。對(duì)骨骼關(guān)節(jié)點(diǎn)進(jìn)行平滑處理會(huì)產(chǎn)生性能開(kāi)銷。平滑處理的越多,性能消耗越大。設(shè)置平滑參數(shù)沒(méi)有經(jīng)驗(yàn)可以遵循。需要不斷的測(cè)試和調(diào)試已達(dá)到最好的性能和效果。在程序運(yùn)行的不同階段,可能需要設(shè)置不同的平滑參數(shù)。
Note:SDK使用霍爾特指數(shù)平滑(Holt Double Exponential Smoothing)來(lái)對(duì)減少關(guān)節(jié)點(diǎn)的抖動(dòng)。指數(shù)平滑數(shù)據(jù)處理與時(shí)間有關(guān)。骨骼數(shù)據(jù)是時(shí)間序列數(shù)據(jù),因?yàn)楣趋酪鏁?huì)以某一時(shí)間間隔不斷產(chǎn)生一幀一幀的骨骼數(shù)據(jù)。平滑處理使用統(tǒng)計(jì)方法進(jìn)行滑動(dòng)平均,這樣能夠減少時(shí)間序列數(shù)據(jù)中的噪聲和極值。類似的處理方法最開(kāi)始被用于金融市場(chǎng)和經(jīng)濟(jì)數(shù)據(jù)的預(yù)測(cè)。
骨骼追蹤對(duì)象選擇
默認(rèn)情況下,骨骼追蹤引擎會(huì)對(duì)視野內(nèi)的所有活動(dòng)的游戲者進(jìn)行追蹤。但只會(huì)選擇兩個(gè)可能的游戲者產(chǎn)生骨骼數(shù)據(jù),大多數(shù)情況下,這個(gè)選擇過(guò)程不確定。如果要自己選擇追蹤對(duì)象,需要使用AppChoosesSkeletons屬性和ChooseSkeletons方法。 默認(rèn)情況下AppChoosesSkeleton屬性為false,骨骼追蹤引擎追蹤所有可能的最多兩個(gè)游戲者。要手動(dòng)選擇追蹤者,需要將AppChoosesSkeleton設(shè)置為true,并調(diào)用ChooseSkeletons方法,傳入TrackingIDs已表明需要追蹤那個(gè)對(duì)象。ChooseSkeletons方法接受一個(gè),兩個(gè)或者0個(gè)TrackingIDs。當(dāng)ChooseSkeletons方法傳入0個(gè)參數(shù)時(shí),引擎停止追蹤骨骼信息。有一些需要注意的地方:
如果調(diào)用ChooseSkeletons方法時(shí)AppChoosesSkeletons的屬性為false,就會(huì)引發(fā)InvalidOperationExcepthion的異常。如果在SkeletonStream開(kāi)啟前,經(jīng)AppChoosesSkeletons設(shè)置為true,只有手動(dòng)調(diào)用ChooseSkeleton方法后才會(huì)開(kāi)始骨骼追蹤。在AppChoosesSkeletons設(shè)置為 true之前,骨骼引擎自動(dòng)選擇追蹤的游戲者,并且繼續(xù)保持這些該游戲者的追蹤,直到用戶手動(dòng)指定需要追蹤的游戲者。如果自動(dòng)選擇追蹤的游戲者離開(kāi)場(chǎng)景,骨骼引擎不會(huì)自動(dòng)更換追蹤者。將AppChoosesSkeletons沖新設(shè)置為false后,骨骼引擎會(huì)繼續(xù)對(duì)之前手動(dòng)設(shè)置的游戲者進(jìn)行追蹤,直到這些游戲者離開(kāi)視野。當(dāng)游戲這離開(kāi)視野時(shí)骨骼引擎才會(huì)選擇其他的可能的游戲者進(jìn)行追蹤。2.2 SkeletonFrameSkeletonStream產(chǎn)生SkeletonFrame對(duì)象??梢允褂檬录P蛷氖录?shù)中調(diào)用OpenSkeletonFrame方法來(lái)獲取SkeletonFrame對(duì)象,或者采用”拉”模型調(diào)用SkeletonStream的OpenNextFrame來(lái)獲取SkeletonFrame對(duì)象。SkeletonFrame對(duì)象會(huì)存儲(chǔ)骨骼數(shù)據(jù)一段時(shí)間。同以通過(guò)調(diào)用SkeletonFrame對(duì)象的CopySkeletonDataTo方法將其保存的數(shù)據(jù)拷貝到骨骼對(duì)象數(shù)組中。SkeletonFrame對(duì)象有一個(gè)SkeletonArrayLength的屬性,這個(gè)屬性表示追蹤到的骨骼信息的個(gè)數(shù)。
時(shí)間標(biāo)記字段
SkeletonFrame的FrameNumber和Timestamp字段表示當(dāng)前記錄中的幀序列信息。FrameNumber是景深數(shù)據(jù)幀中的用來(lái)產(chǎn)生骨骼數(shù)據(jù)幀的幀編號(hào)。幀編號(hào)通常是不連續(xù)的,但是之后的幀編號(hào)一定比之前的要大。骨骼追蹤引擎在追蹤過(guò)程中可能會(huì)忽略某一幀深度數(shù)據(jù),這跟應(yīng)用程序的性能和每秒產(chǎn)生的幀數(shù)有關(guān)。例如,在基于事件獲取骨骼幀信息中,如果事件中處理幀數(shù)據(jù)的時(shí)間過(guò)長(zhǎng)就會(huì)導(dǎo)致這一幀數(shù)據(jù)還沒(méi)有處理完就產(chǎn)生了新的數(shù)據(jù),那么這些新的數(shù)據(jù)就有可能被忽略了。如果采用“拉”模型獲取幀數(shù)據(jù),那么取決于應(yīng)用程序設(shè)置的骨骼引擎產(chǎn)生數(shù)據(jù)的頻率,即取決于深度影像數(shù)據(jù)產(chǎn)生骨骼數(shù)據(jù)的頻率。
Timestap字段記錄字Kinect傳感器初始化以來(lái)經(jīng)過(guò)的累計(jì)毫秒時(shí)間。不用擔(dān)心FrameNumber或者Timestamp字段會(huì)超出上限。FrameNumber是一個(gè)32位的整型,Timestamp是64位整型。如果應(yīng)用程序以每秒30幀的速度產(chǎn)生數(shù)據(jù),應(yīng)用程序需要運(yùn)行2.25年才會(huì)達(dá)到FrameNumber的限,此時(shí)Timestamp離上限還很遠(yuǎn)。另外在Kinect傳感器每一次初始化時(shí),這兩個(gè)字段都會(huì)初始化為0??梢哉J(rèn)為FrameNumber和Timestamp這兩個(gè)值是唯一的。
這兩個(gè)字段在分析處理幀序列數(shù)據(jù)時(shí)很重要,比如進(jìn)行關(guān)節(jié)點(diǎn)值的平滑,手勢(shì)識(shí)別操作等。在多數(shù)情況下,我們通常會(huì)處理幀時(shí)間序列數(shù)據(jù),這兩個(gè)字段就顯得很有用。目前SDK中并沒(méi)有包含手勢(shì)識(shí)別引擎。在未來(lái)SDK中加入手勢(shì)引擎之前,我們需要自己編寫(xiě)算法來(lái)對(duì)幀時(shí)間序列進(jìn)行處理來(lái)識(shí)別手勢(shì),這樣就會(huì)大量依賴這兩個(gè)字段。
幀描述信息
FloorClipPlane字段是一個(gè)有四個(gè)元素的元組Tuple<int,int,int,int>,每一個(gè)都是Ax+By+Cz+D=0地面平面(floor plane)表達(dá)式里面的系數(shù)項(xiàng)。元組中第一個(gè)元素表示A,即x前面的系數(shù),一次類推,最后一個(gè)表示常數(shù)項(xiàng),通常為負(fù)數(shù),是Kinect距離地面高度。在可能的情況下SDK會(huì)利用圖像處理技術(shù)來(lái)確定這些系數(shù)。但是有時(shí)候這些系數(shù)不肯能能夠確定下來(lái),可能需要預(yù)估。當(dāng)?shù)孛娌荒艽_定時(shí)FloorClipPlane中的所有元素均為0.
2.3 SkeletonSkeleton類定義了一系列字段來(lái)描述骨骼信息,包括描述骨骼的位置以及骨骼中關(guān)節(jié)可能的位置信息。骨骼數(shù)據(jù)可以通過(guò)調(diào)用SkeletonFrame對(duì)象的CopySkeletonDataTo方法獲得Skeleton數(shù)組。CopySkeletonDataTo方法有一些不可預(yù)料的行為,可能會(huì)影響內(nèi)存使用和其引用的骨骼數(shù)組對(duì)象。產(chǎn)生的每一個(gè)骨骼數(shù)組對(duì)象數(shù)組都是唯一的。以下面代碼為例:
Skeleton[] skeletonA = new Skeleton[frame.SkeletonArrayLength];Skeleton[] skeletonB = new Skeleton[frame.SkeletonArrayLength];frame.CopySkeletonDataTo(skeletonA);frame.CopySkeletonDataTo(skeletonB);Boolean resultA = skeletonA[0] == skeletonB[0];//falseBoolean resultB = skeletonA[0].TrackingId == skeletonB[0].TrackingId;//true
上面的代碼可以看出,使用CopySkeletonDataTo是深拷貝對(duì)象,會(huì)產(chǎn)生兩個(gè)不同的Skeleton數(shù)組對(duì)象。
TrackingID
骨骼追蹤引擎對(duì)于每一個(gè)追蹤到的游戲者的骨骼信息都有一個(gè)唯一編號(hào)。這個(gè)值是整型,他會(huì)隨著新的追蹤到的游戲者的產(chǎn)生添加增長(zhǎng)。和之前幀序號(hào)一樣,這個(gè)值并不是連續(xù)增長(zhǎng)的,但是能保證的是后面追蹤到的對(duì)象的編號(hào)要比之前的編號(hào)大。另外,這個(gè)編號(hào)的產(chǎn)生是不確定的。如果骨骼追蹤引擎失去了對(duì)游戲者的追蹤,比如說(shuō)游戲者離開(kāi)了Kinect的視野,那么這個(gè)對(duì)應(yīng)的唯一編號(hào)就會(huì)過(guò)期。當(dāng)Kinect追蹤到了一個(gè)新的游戲者,他會(huì)為其分配一個(gè)新的唯一編號(hào),編號(hào)值為0表示這個(gè)骨骼信息不是游戲者的,他在集合中僅僅是一個(gè)占位符。應(yīng)用程序使用TrackingID來(lái)指定需要骨骼追蹤引擎追蹤那個(gè)游戲者。調(diào)用SkeletonStream對(duì)象的ChooseSkeleton能以初始化對(duì)指定游戲這的追蹤。
TrackingState
該字段表示當(dāng)前的骨骼數(shù)據(jù)的狀態(tài)。下表展示了SkeletonTrackingState枚舉的可能值機(jī)器含義:
Position
Position一個(gè)SkeletonPoint類型的字段,代表所有骨骼的中間點(diǎn)。身體的中間點(diǎn)和脊柱關(guān)節(jié)的位置相當(dāng)。改字段提供了一個(gè)最快且最簡(jiǎn)單的所有視野范圍內(nèi)的游戲者位置的信息,而不管其是否在追蹤狀態(tài)中。在一些應(yīng)用中,如果不用關(guān)心骨骼中具體的關(guān)節(jié)點(diǎn)的位置信息,那么該字段對(duì)于確定游戲者的位置狀態(tài)已經(jīng)足夠。該字段對(duì)于手動(dòng)選擇要追蹤的游戲者(SkeletonStream.ChooseSkeleton)也是一個(gè)參考。例如,應(yīng)用程序可能需要追蹤距離Kinect最近的且處于追蹤狀態(tài)的游戲者,那么該字段就可以用來(lái)過(guò)濾掉其他的游戲者。
ClippedEdges
ClippedEdges字段用來(lái)描述追蹤者的身體哪部分位于Kinect的視野范圍外。他大體上提供了一個(gè)追蹤這的位置信息。使用這一屬性可以通過(guò)程序調(diào)整Kinect攝像頭的俯仰角或者提示游戲者讓其返回到視野中來(lái)。該字段類型為FrameEdges,他是一個(gè)枚舉并且有一個(gè)FlagsAtrribute自定義屬性修飾。這意味著ClippedEdges字段可以一個(gè)或者多個(gè)FrameEdges值。下面列出了FrameEdges的所有可能的值。
當(dāng)游戲者身體的某一部分超出Kinect視場(chǎng)范圍時(shí),就需要對(duì)骨骼追蹤產(chǎn)生的數(shù)據(jù)進(jìn)行某些改進(jìn),因?yàn)槟承┎课坏臄?shù)據(jù)可能追蹤不到或者不準(zhǔn)確。最簡(jiǎn)單的解決辦法就是提示游戲者身體超出了Kinect的某一邊界范圍讓游戲者回到視場(chǎng)中來(lái)。例如,有時(shí)候應(yīng)用程序可能不關(guān)心游戲者超出Kinect視場(chǎng)下邊界的情況,但是如果超出了左邊界或者右邊界時(shí)就會(huì)對(duì)應(yīng)用產(chǎn)生影響,這是可以針對(duì)性的給游戲者一些提示。另一個(gè)解決辦法是調(diào)整Kinect設(shè)備的物理位置。Kinect底座上面有一個(gè)小的馬達(dá)能夠調(diào)整Kinect的俯仰角度。俯仰角度可以通過(guò)更改KinectSensor對(duì)象的ElevationAnagle屬性來(lái)進(jìn)行調(diào)整。如果應(yīng)用程序?qū)τ谟螒蛘吣_部動(dòng)作比較關(guān)注,那么通過(guò)程序調(diào)整Kinect的俯仰角能夠決絕腳部超出視場(chǎng)下界的情況。
ElevationAnagle以度為單位。KinectSensor的MaxElevationAngle和MinElevationAngle確定了可以調(diào)整角度的上下界。任何將ElevationAngle設(shè)置超出上下界的操作將會(huì)掏出ArgumentOutOfRangeExcepthion異常。微軟建議不要過(guò)于頻繁重復(fù)的調(diào)整俯仰角以免損壞馬達(dá)。為了使得開(kāi)發(fā)這少犯錯(cuò)誤和保護(hù)馬達(dá),SDK限制了每秒能調(diào)整的俯仰角的值。SDK限制了在連續(xù)15次調(diào)整之后要暫停20秒。
Joints
每一個(gè)骨骼對(duì)象都有一個(gè)Joints字段。該字段是一個(gè)JointsCollection類型,它存儲(chǔ)了一些列的Joint結(jié)構(gòu)來(lái)描述骨骼中可追蹤的關(guān)節(jié)點(diǎn)(如head,hands,elbow等等)。應(yīng)用程序使用JointsCollection索引獲取特定的關(guān)節(jié)點(diǎn),并通過(guò)節(jié)點(diǎn)的JointType枚舉來(lái)過(guò)濾指定的關(guān)節(jié)點(diǎn)。即使Kinect視場(chǎng)中沒(méi)有游戲者Joints對(duì)象也被填充。
2.4 Joint骨骼追蹤引擎能夠跟蹤和獲取每個(gè)用戶的近20個(gè)點(diǎn)或者關(guān)節(jié)點(diǎn)信息。追蹤的數(shù)據(jù)以關(guān)節(jié)點(diǎn)數(shù)據(jù)展現(xiàn),它有三個(gè)屬性。JointType屬性是一個(gè)枚舉類型。下圖描述了可追蹤的所有關(guān)節(jié)點(diǎn)。
每一個(gè)關(guān)節(jié)點(diǎn)都有類型為SkeletonPoint的Position屬性,他通過(guò)X,Y,Z三個(gè)值來(lái)描述關(guān)節(jié)點(diǎn)的控件位置。X,Y值是相對(duì)于骨骼平面空間的位置,他和深度影像,彩色影像的空間坐標(biāo)系不一樣。KinectSnesor對(duì)象有一些列的坐標(biāo)轉(zhuǎn)換方法,可以將骨骼坐標(biāo)點(diǎn)轉(zhuǎn)換到對(duì)應(yīng)的深度數(shù)據(jù)影像中去。最后每一個(gè)Skeleton對(duì)象還有一個(gè)JointTrackingState屬性,他描述了該關(guān)節(jié)點(diǎn)的跟蹤狀態(tài)及方式,下面列出了所有的可能值。
3. 結(jié)語(yǔ)本文首先通過(guò)一個(gè)例子展示骨骼追蹤系統(tǒng)所涉及的主要對(duì)象,并將骨骼數(shù)據(jù)在UI界面上進(jìn)行了繪制,在此基礎(chǔ)上詳細(xì)介紹了骨骼追蹤對(duì)象模型中涉及到的主要對(duì)象,方法和屬性。SDK中骨骼追蹤占了大概三分之一的內(nèi)容,所以熟悉這些對(duì)象對(duì)于開(kāi)發(fā)基于Kinect應(yīng)用程序至關(guān)重要。