上篇文章介紹了Kinect開發的環境配置,這篇文章和下一篇文章將介紹Kinect開發的基本知識,為深入研究Kinect for Windows SDK做好基礎。
每一個Kinect應用都有一些基本元素。應用程序必須探測和發現鏈接到設備上的Kinect傳感器。在使用這些傳感器之前,必須進行初始化,一旦初始化成功後,就能產生數據,我們的程序就能處理這些數據。最後當應用程序關閉是,必須合理的釋放這些傳感器。
本文第一部分將會介紹如何探測初始化幾釋放傳感器,這是非常基礎的話題,但是對於基於Kinect開發的應用程序非常重要。一旦初始化好了之後,Kinect的各種傳感器就能夠產生數據。我們的程序可以讀取這些數據流。Kinect產生的數據流類類似於System.IO命名空間下面的IO數據流。
第二部分將詳細介紹數據流的基礎,並演示如何從Kinect中使用ColorImageStream獲取彩色攝像頭產生的數據。數據流能夠生產基於像素的數據,使得能夠像從相機或者基本的相片那樣生產彩色圖像。可以對這些數據進行各種有趣的處理。
本文是整個Kinect SDK開發的基礎部分,了解了這些之後,對於熟悉SDK中其他部分比較有幫助。
1. Kinect傳感器
基於Kinect開發的應用程序最開始需要用到的對象就是KinectSensor對象,該對象直接表示Kinect硬件設備。KinectSensor對象是我們想要獲取數據,包括彩色影像數據,景深數據和骨骼追蹤數據的源頭。本文將詳細介紹ColorImageStream,後面的文章將詳細討論DepthImageStream和SkeletonStream。
從KinectSensor獲取數據最常用的方式是通過監聽該對象的一系列事件。每一種數據流都有對應的事件,當改類型數據流可用時,就會觸發改時間。每一個數據流以幀(frame)為單位。例如:ColorImageStream當獲取到了新的數據時就會觸發ColorFrameReady事件。當在討論各個具體的傳感器數據流是我們將會詳細討論這些事件。
每一種數據流(Color,Depth,Skeleton)都是以數據點的方式在不同的坐標系中顯示的,在後面的討論中我們能夠清楚的看到這一點。將一個數據流中的點數據轉換到另一個數據流中是一個很常見的操作,在本文的後面將會討論如何轉換以及為什麼這種轉換很有必要。KinectSensor對象有一些列的方法能夠進行數據流到數據點陣的轉換,他們是MapDepthToColorImagePoint,MapDepthToSkeletonPoint以及MapSkeletonPointToDepth。在獲取Kinect數據前,我們必須先發現連接的Kinect設備。發現Kinect設備很簡單,但是也有需要主注意的地方。
1.1 發現連接的Kinect設備
KinectObject對象沒有公共的構造器,應用程序不能直接創建它。相反,該對象是SDK在探測到有連接的Kinect設備時創建的。當有Kinect設備連接到計算機上時,應用程序應該得到通知或者提醒。KinectSeneor對象有一個靜態的屬性KinectSensors,該屬性是一個KinectSensorCollection集合,該集合繼承自ReadOnlyCollection,ReadOnlyCollection集合很簡單,他只有一個索引器和一個稱之為StatusChanged的事件。
使用集合中的索引器來獲取KinectSensor對象。集合中元素的個數就是Kinect設備的個數。也就是說,一台電腦上可以連接多個Kinect設備來從不同的方向獲取數據。應用程序可以使用多個Kinect設備來獲取多方面的數據,Kinect個數的限制 只有電腦配置的限制。由於每個Kinect是通過USB來進行數據傳輸的,所以每一個Kinect設備需要一條USB線與電腦相連。此外,更多的Kinect設備需要更多的CPU和內存消耗。
查找Kinect設備可以通過簡單的遍歷集合找到;但是KinectSensor集合中的設備不是都能直接使用,所以KinectSensor對象有一個Status屬性,他是一個枚舉類型,標識了當前Kinect設備的狀態。下表中列出了傳感器的狀態及其含義:
只有設備在Connected狀態下時,KinectSensor對象才能初始化。在應用的整個生命周期中,傳感器的狀態可能會發生變化,這意味著我們開發的應用程序必須監控設備的連接狀態,並且在設備連接狀態發生變化時能夠采取相應的措施來提高用戶體驗。例如,如果連接Kinect的USB線從電腦拔出,那麼傳感器的連接狀態就會變為Disconnected,通常,應用程序在這種情況下應該暫停,並提示用戶將Kinect設備插入到電腦上。應用程序不應該假定在一開始時Kinect設備就處於可用狀態,也不應該假定在整個程序運行的過程中,Kinect設備會一直與電腦連接。
下面,首先創建一個WPF應用程序來展示如何發現,獲取Kinect傳感器的狀態。先建按一個WPF項目,並添加Microsoft.Kinect.dll。在MainWindows.xaml.cs中寫下如下代碼:
public partial class MainWindow : Window { //私有Kinectsensor對象 private KinectSensor kinect; public KinectSensor Kinect { get { return this.kinect;} set { //如果帶賦值的傳感器和目前的不一樣 if (this.kinect!=value) { //如果當前的傳感對象不為null if (this.kinect!=null) { //uninitailize當前對象 this.kinect=null; } //如果傳入的對象不為空,且狀態為連接狀態 if (value!=null&&value.Status==KinectStatus.Connected) { this.kinect=value; } } } } public MainWindow() { InitializeComponent(); this.Loaded += (s, e) => DiscoverKinectSensor(); this.Unloaded += (s, e) => this.kinect = null; } private void DiscoverKinectSensor() { KinectSensor.KinectSensors.StatusChanged += KinectSensors_StatusChanged; this.Kinect = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected); } private void KinectSensors_StatusChanged(object sender, StatusChangedEventArgs e) { switch (e.Status) { case KinectStatus.Connected: if (this.kinect == null) this.kinect = e.Sensor; break; case KinectStatus.Disconnected: if (this.kinect == e.Sensor) { this.kinect = null; this.kinect = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected); if (this.kinect == null) { //TODO:通知用於Kinect已拔出 } } break; //TODO:處理其他情況下的狀態 } } }
上面的代碼注釋很詳細,首先定義了一個私有變量kinect,應用程序應該定義一個私有的變量來存儲對獲取到的KincectSensor對象的引用,當應用程序不在需要KinectSensor產生數據時,可以使用這個局部變量來釋放對KinectSensor對象的引用從而釋放資源。我們還定義了一個Kinect屬性來對這個私有變量進行包裝,使用屬性的目的是保證能夠以正確的方式初始化和反初始化KinectSensor對象。在Set方法中我們可以看到,自由待賦值的對象的組航太是Connected的時候我們才進行賦值操作,任何將沒有處在Connected狀態的傳感器對象復制給KinectSensor對象時都會拋出InvalidOperationException異常。
在構造函數中有兩個匿名方法,一個用來監聽Loaded事件,一個用來監聽Unloaded事件。當卸載時應該將Kinect屬性置為空。在窗口的Loaded事件中程序通過DiscoverKinectSensor方法試圖調用一個連接了的傳感器。在窗體的Loaded和Unloaded事件中注冊這兩個事件用來初始化和釋放Kinect對象,如果應用程序沒有找到Kinect對象,將會通知用戶。
DiscoverKinectSensor方法只有兩行代碼,第一行代碼注冊StatusChanged事件,第二行代碼通過lambda表達式查詢集合中第一個處在Connected狀態的傳感器對象,並將該對象復制給Kinect屬性。Kinect屬性的set方法確保能都賦值一個合法的Kinect對象。
StatusChanged事件中值得注意的是,當狀態為KinectSensor.Connected的時候,if語句限制了應用程序只能有一個kinect傳感器,他忽略了電腦中可能連接的其他Kinect傳感器。
以上代碼展示了用於發現和引用Kinect設備的最精簡的代碼,隨著應用的復雜,可能需要更多的代碼來保證線程安全以及能讓垃圾回收器及時釋放資源以防止內存洩露。
1.2 打開傳感器
一旦發現了傳感器,在應用程序能夠使用傳感器之前必須對其進行初始化。傳感器的初始化包括三個步驟。首先,應用程序必須設置需要使用的數據流,並將其狀態設為可用。每一中類型的數據流都有一個Enable方法,該方法可以初始化數據流。每一種數據流都完全不同,在使用之前需要進行一些列的設置。在一些情況下這些設置都在Enable方法中處理了。在下面,我們將會討論如何初始化ColorImageStream數據流,在以後的文章中還會討論如何初始化DepthImageStream數據流和SkeletonStream數據流。
初始化之後,接下來就是要確定應用程序如何使用產生的數據流。最常用的方式是使用Kinect對象的一些列事件,每一種數據流都有對應的事件,他們是:ColorImageStream對應ColorFrameReady事件、DepthImageStream對應DepthFrameReady事件、SkeletonStream對象對應SkeletonFrameReady事件。以及AllFramesReady事件。各自對應的事件只有在對應的數據流enabled後才能使用,AllFramesReady事件在任何一個數據流狀態enabled時就能使用。
最後,應用程序調用KinectSensor對象的Start方法後,frame-ready事件就會觸發從而產生數據。
1.3 停止傳感器
一旦傳感器打開後,可以使用KinectSensor對象的Stop方法停止。這樣所有的數據產生都會停止,因此在監聽frameready事件時要先檢查傳感器是否不為null。
KinectSensor對象以及數據流都會使用系統資源,應用程序在不需要使用KinectSensor對象時必須能夠合理的釋放這些資源。在這種情況下,程序不僅要停止傳單器,還用注銷frameready事件。注意,不要去調用KinectSensor對象的Dispose方法。這將會阻止應用程序再次獲取傳感器。應用程序必須從啟或者將Kinect從新拔出然後插入才能再次獲得並使用對象。
2. 彩色影像數據流
Kinect有兩類攝像頭,近紅外攝像頭和普通的視頻攝像頭。視頻攝像頭提供了一般攝像頭類似的彩色影像。這種數據流是三中數據流中使用和設置最簡單的。因此我將他作為Kinect數據流介紹的例子。
使用Kinect數據流也有三部。首先是數據流必須可用。一旦數據流可用,應用程序就可以從數據量中讀取數據並對數據進行處理和展現。一旦有新的數據幀可用,這兩個步驟就會一直進行,下面的代碼展現了如何初始化ColorImage對象。
public KinectSensor Kinect { get { return this.kinect;} set { //如果帶賦值的傳感器和目前的不一樣 if (this.kinect!=value) { //如果當前的傳感對象不為null if (this.kinect!=null) { UninitializeKinectSensor(this.kinect); //uninitailize當前對象 this.kinect=null; } //如果傳入的對象不為空,且狀態為連接狀態 if (value!=null&&value.Status==KinectStatus.Connected) { this.kinect=value; InitializeKinectSensor(this.kinect); } } } } private void InitializeKinectSensor(KinectSensor kinectSensor) { if (kinectSensor != null) { kinectSensor.ColorStream.Enable(); kinectSensor.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(kinectSensor_ColorFrameReady); kinectSensor.Start(); } } private void UninitializeKinectSensor(KinectSensor kinectSensor) { if (kinectSensor != null) { kinectSensor.Stop(); kinectSensor.ColorFrameReady -= new EventHandler<ColorImageFrameReadyEventArgs>(kinectSensor_ColorFrameReady); } }
查看本欄目
上面的代碼對之前Kinect屬性進行了修改,加粗為修改部分。新添加的兩行調用了兩個方法,分別初始化和釋放KinectSensor和ColorImageStream對象。InitializeKinectSensor對象調用ColorImageStream的Enable方法,注冊ColorFrameReady事件並調用start方法。一旦打開了傳感器,當新數據幀大道是就會觸發frameready事件,該事件觸發頻率是每秒30次。
在實現Kinect_ColorFrameReady方法前,我們先在XAML窗體中添加一些空間來展現獲取到的數據,代碼如下:
<Window x:Class="KinectApplicationFoundation.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ColorImageStreamFromKinect" Height="350" Width="525"> <Grid> <Image x:Name="ColorImageElement"></Image> </Grid> </Window>
然後,在Kinect_ColorFrameReady方法中,我們首先通過打開或者獲取一個frame來提取獲Frame數據。ColorImageFrameReadyEventArgs對象的OpenColorImageFrame屬性返回一個當前的ColorImageFrame對象。這個對象實現了IDisposable接口。所以可以將這個對象抱在using語句中的原因,在提取像素數據之前需要使用一個Byte數組保存獲取到的數據。FrameObject對象的PixelDataLength對象返回數據和序列的具體大小。調用CopyPixelDataTo方法可以填充像素數據,然後將數據展示到image控件上,具體代碼如下:
void kinectSensor_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { using (ColorImageFrame frame = e.OpenColorImageFrame()) { if (frame != null) { byte[] pixelData = new byte[frame.PixelDataLength]; frame.CopyPixelDataTo(pixelData); ColorImageElement.Source = BitmapImage.Create(frame.Width, frame.Height, 96, 96, PixelFormats.Bgr32, null, pixelData, frame.Width * frame.BytesPerPixel); } } }
運行程序,就能得到從Kinect獲取的視頻信息,如下圖所示這是從Kinect彩色攝像頭獲取的我房間的照片。和一般的視頻沒什麽兩樣,只不過這個是從Kinect的視頻攝像頭產生的。
3. 結語
本文簡要介紹了Kinect開發會遇到的基本對象,Kinect物理設備的發現,KinectSensor對象的初始化,打開KinectSensor對象以及如何獲取數據流,最後以ColorImageStream對象為例展示了如何從Kinect獲取數據並展現出來。
由於Kinect的彩色攝像頭默認每秒產生30副ColorImageFrame,所以上面的應用程序會產生30個Bitmap對象,而且這些對象初始化後很快將變成垃圾等待垃圾回收器進行收集,當采集的數據量很大時,將會對性能產生影響。限於篇幅原因,下篇文章將會介紹如何對這一點進行改進,並將討論獲取Kinect傳感器產生數據的兩種編程模式:基於事件的模式和輪詢的模式。
本文示例代碼:http://files.cnblogs.com/yangecnu/KinectSDK_Application_Fundamentals_Part1_DisplayImageViaBitmap.rar
作者: yangecnu(yangecnu's Blog on 博客園)
出處:http://www.cnblogs.com/yangecnu/