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