前面十三篇文章介紹了Kinect SDK開發中的各個方面的最基礎的知識。正如本系列博聞標題那樣,這些知識只是Kinect for windows SDK開發的入門知識。本文將會介紹Kinect進階開發需要了解一些知識(beyond the basic)。
讀者可能會注意到,在學習了前面十三篇文章中關於Kinect開發的方方面面,如影像數據流、景深攝像機、骨骼追蹤、麥克風陣列、語音識別等這些知識後,離開發出一些我們在網上看到的那些具有良好用戶體驗的Kinect應用程序還是顯得捉襟見肘。Kinect SDK在某種意義上只是提供了一些其它Kinect類庫的相同或者更好的功能。為了進一步提高Kinect for Windows應用程序的應用體驗,我們需要了解一些其他的和Kinect有關的開發技術和類庫。Kinect的真正應用潛力是和其他技術進行整合。
本文將會介紹一些第三方類庫如何來幫助處理Kinect傳感器提供的數據。使用不同的技術進行Kinect開發,可以發掘出Kinect應用的強大功能。另一方面如果不使用這些為了特定處理目的而開發的一些類庫,而是自己實現這些邏輯的話,代碼可能會比較混亂和不穩定。本文只是簡單的介紹這些第三方類庫並給以適當的引導。Kinect開發最大的困難不是什麼技術,而是知道什麼樣技術能夠被用到Kinect開發中。一旦了解了什麼技術能夠使用,Kinect可能開發的應用就會出現巨大的潛力。
本文中介紹的一些技術可能覆蓋面不夠廣,有可能有些類庫很重要但是本文沒有涉及到,Kinect開發方面的技術變化的非常快,這一點也難以避免,歡迎大家能夠補充。但是通過介紹一些基本的幫助類庫,圖像處理類庫等,希望大家了解到這些技術的重要性以及對Kinect開發的作用。可能隨著技術的發展,這些類庫可能有些變化,但是相關的技術領域相信還是一樣的,本文只是起到一個拋磚引玉的作用。
本文及下篇文章將會介紹幾個有用的工具及類庫,包括Coding4Fun Kinect Toolkit,Emgu(OpenCV計算機視覺庫的C#版本)和Blender。只是非常簡單的介紹了Unity 3D游戲框架,FAAST手勢識別中間件以及Microsoft Robotics Developer Studio。這些知識如果大家感興趣的話可能要花一定的精力去了解和掌握,這裡只是簡單介紹。
1. 影像處理幫助類
有很多影像處理相關的類庫可以使用。單單在.NET Framework中,就有PresentationCore.dll中的System.Windows.Media.Drawing抽象類以及System.Drawing.dll中System.Drawing命名空間下的類可以使用。更復雜的是,在System.Windows和System.Drawing命名空間下有一些相互獨立的處理形狀(shape)和顏色(color)的類。有時候一個類庫中的方法能夠進行一些圖像處理而其它類庫中卻沒有類似的方法。為了方便,各種圖形對象之間的轉換顯得很有必要。
當引入Kinect後,情況變得更加復雜。Kinect有自己的影像數據流,如ImageFrame。為了能夠使Kinect這些專有的影像對象能夠和WPF一同使用,ImageFrame對象必須轉換為ImageSource類型,該對象在System.Windows.Media.Imaging命名空間中。第三方影像處理庫並不知道System.Windows.Media命名空間中的對象,但是知道System.Drawing命名空間,為了能夠使用Emgu處理Kinect中產生的數據,需要將Microsoft.Kinect中的某些數據類型轉化為System.Drawing類型,然後將System.Drawing類型轉換到Emgu中的類型,在Emgu中處理完之後,再轉換回System.Drawing類型,最後再轉換為System.Windows.Media類型來共WPF使用。
1.1 Coding4Fun Kinect工具類
Coding4Fun Kinect Toolkit為將一些類型從一種類庫轉換到其他類庫中的對應類型的提供了一些便利。這個工具集可以從該開源工具集官網 http://c4fkinect.codeplex.com/ 上下載。它包括3個獨立的dll。其中Coding4Fun.Kinect.Wpf.dll提供了一系列擴展方法來在Microsoft.Kinect和System.Windows.Media之間進行轉換。而Coding4Fun.Kinect.WinForm.dll提供了一系列擴展方法來在Microsoft.Kinect和System.Drawing之間進行轉換。System.Drawing是.NET圖形庫中的dll。他包含了WinForm中用來進行繪圖和展現所需的元素,而WPF中所需要的展現元素包含在System.Windows.Media中。
遺憾的是Coding4Fun Kinect Toolkit並沒有提供在System.Drawing命名空間和System.Windows.Media命名空間之間對應對象的轉換方法。這是因為Toolkit的最初目的是方便簡單的編寫Kinect Demo程序而不是提供一個通用的在不同的圖像類型之間進行轉換的類庫。所以,一些可能在WPF中要用到的方法可能存在於WinForm的dll中。一些非常有用的,復雜的處理景深數據流中景深影像數據的方法被封裝到了一些簡單的將Kinect圖像類型轉換為WPF ImageSource的對象中去了。
但是Coding4Fun Kinect ToolKit有兩個比較好的地方可以取消上面的疑慮。一個就是,他是開源的,源代碼可以下載並查看。可以通過源碼查看Coding4Fun團隊是如何在圖像處理內部使用字節數組的。你可以在這些代碼中看到前面博文中代碼的影子,類庫中的一些小的技巧非常有幫助。第二就是這些方法都是擴展方法,可以很方便的進行擴充。
擴展方法是一種語法糖,它使得一個獨立的方法看起來像是被附加到一個類型上一樣。比如,有一個方法AddOne可以將當前的值加1。這個方法可以改寫為一個擴展方法。只需要簡單的將該方法設置為靜態的,並在Int32類型前加this即可。代碼如下, 然後調用AddOne(3)這個方法可以簡單的改寫為3. AddOne()
public int AddOne(int i) { return i + 1; } public static class myExtensions { public static int AddOne(this int i) { return i + 1; } }
為了使用擴展方法庫,必須引用這個方法所在的類庫。包含擴展方法的靜態類(myExtensions類)實際是被忽略的。使用擴展方法將一種image類型轉換到另外一種image類型,可以簡單的使用如下類似的代碼進行操作。
var bitmapSource = imageFrame.ToBitmapSource(); image1.Source=bitmapSource;
下圖是Coding4Fun Kinect Toolkit中的一些擴展方法,使用這些方法可以簡化我們的Kinect開發。但是在開發實踐中,我們應該考慮建立我們自己的幫助方法類庫。這可以擴充Coding4Fun類庫中所沒有提供的功能。更重要的是,因為Coding4Fun的一些方法隱藏了處理深度影像數據的復雜性,有時候可能並不像你所期望的那樣工作。隱藏復雜性是這些類庫設計的初衷,但是當你使用時可能會感到困惑,比如當你使用Coding4Fun Toolkit中提供的方法來處理景深數據流時,e.ImageFrame.ToBitmapSource()返回的值可能和e.ImageFrame.Bits.ToBitmapSource(e.ImageFrame.Image.With,e.ImageFrame.Image.Height)產生的返回值不同。可以建立自己的擴展方法類庫來方便Kinect開發,可以使你明確的使用自己的擴展方法來達到自己想要的結果。
1.2 創建自己的擴展方法庫
我們可以建立自己的擴展方法。在前面的文章中我們講述了如何建立一個圖像操作項目的擴展方法。這些方法的目的是幫助我們從經常用的System.Drawing命名空間的類型轉換到WPF中的System.Windows.Media命名空間中去,這能夠為第三方圖像處理類庫和WPF應用程序之間提供橋梁。這些擴展方法是一些標准的處理Bitmap和BitmapSource對象的方法。一些方法也可以在Coding4Fun Kinect Toolkit中找到。
為了演示方便,我們在這裡建立一個類而不是類庫來包含我們想要的擴展方法,在其他項目中如果需要使用這個類中的擴展方法,只需要將該類拷貝過去即可。
1.2.1 創建一個WPF項目
現在我們創建一個簡單的WPF項目來建立對擴展方法類的測試。項目的MainWindows.xaml前台代碼如下,頁面上包含有名為rgbImage和depthImage的兩個Image對象。
<Window x:Class="ImageLibrarySamples.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Image Library Samples" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Image Name="rgbImage" Stretch="Uniform" Grid.Column="0"/> <Image Name="depthImage" Stretch="Uniform" Grid.Column="1"/> </Grid> </Window>
現在來編寫後台代碼,與之前的類似,首先添加對Microsoft.Kinect.dll引用,然後聲明KinectSensor對象並在MainWindows的構造函數中實例化,代碼如下所示。實例化KinectSensor對象,處理ColorFrameReady和DepthFrameReady事件,然後打開color和depth數據流。
Microsoft.Kinect.KinectSensor _kinectSensor; public MainWindow() { InitializeComponent(); this.Unloaded += delegate { _kinectSensor.ColorStream.Disable(); _kinectSensor.DepthStream.Disable(); }; this.Loaded += delegate { _kinectSensor = KinectSensor.KinectSensors[0]; _kinectSensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30); _kinectSensor.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30); _kinectSensor.ColorFrameReady += ColorFrameReady; _kinectSensor.DepthFrameReady += DepthFrameReady; _kinectSensor.Start(); }; } void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { } void ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { }
1.2.2 創建類及擴展方法
在項目中創建一個新的名為ImageExtensions.cs的類來包含擴展方法。這個類的實際名字並不重要,需要用到的只是命名空間。在下面的代碼中,我們使用的命名空間是ImageManipulationExtensionMethods。另外,還需要添加對System.Drawing.dll的引用。如前所述,System.Drawing和System.Windows.Media中有些類的名稱是一樣的。為了消除命名空間的沖突,我們必須選取一個作為默認的命名空間。在下面的代碼中,我們將System.Drawing作為默認的命名空間,而給System.Windows.Media起了一個名為Media的別名。最後,我們為最重要的兩個圖像轉換創建了擴展方法。一個是將字節序列(byte array)轉換為Bitmap對象,另一個是將字節序列(byte array)轉換為BitmapSource對象。這兩個擴展方法會用到彩色圖像的顯示中。我們還創建了另外兩個擴展方法,通過這些方法中的字節序列替換短字節序列(short array) 來對深度影像進行變換,因為深度影像數據是由short類型而不是byte類型組成的。
namespace ImageManipulationExtensionMethods { public static class ImageExtensions { public static Bitmap ToBitmap(this byte[] data, int width, int height , PixelFormat format) { var bitmap = new Bitmap(width, height, format); var bitmapData = bitmap.LockBits( new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat); Marshal.Copy(data, 0, bitmapData.Scan0, data.Length); bitmap.UnlockBits(bitmapData); return bitmap; } public static Bitmap ToBitmap(this short[] data, int width, int height , PixelFormat format) { var bitmap = new Bitmap(width, height, format); var bitmapData = bitmap.LockBits( new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat); Marshal.Copy(data, 0, bitmapData.Scan0, data.Length); bitmap.UnlockBits(bitmapData); return bitmap; } public static Media.Imaging.BitmapSource ToBitmapSource(this byte[] data , Media.PixelFormat format, int width, int height) { return Media.Imaging.BitmapSource.Create(width, height, 96, 96 , format, null, data, width * format.BitsPerPixel / 8); } public static Media.Imaging.BitmapSource ToBitmapSource(this short[] data , Media.PixelFormat format, int width, int height) { return Media.Imaging.BitmapSource.Create(width, height, 96, 96 , format, null, data, width * format.BitsPerPixel / 8); } } }
1.2.3 創建其它的擴展方法
現在影像流和深度流數據的字節序列都可以通過從ColorImageFrame和DepthImageFrame類型獲取。我們還可以創建一些額外的擴展方法來處理這些類型而不是字節序列。
獲取bit序列數據並將其轉換為Bitmap或者BitmapSource類型過程中,最重要的因素是考慮像元的格式。影像數據流返回的是一系列32位的RGB影像,深度數據流返回的是一系列16位的RGB影像。在下面的代碼中,我們使用沒有透明值的32個字節的影像數據作為默認的數據類型,也就是說,影像數據流可以簡單的調用ToBitmap或者ToBitmapSource方法,其他擴展方法的名稱應該給予影像數據格式一些提示。
// bitmap methods public static Bitmap ToBitmap(this ColorImageFrame image, PixelFormat format) { if (image == null || image.PixelDataLength == 0) return null; var data = new byte[image.PixelDataLength]; image.CopyPixelDataTo(data); return data.ToBitmap(image.Width, image.Height , format); } public static Bitmap ToBitmap(this DepthImageFrame image, PixelFormat format) { if (image == null || image.PixelDataLength == 0) return null; var data = new short[image.PixelDataLength]; image.CopyPixelDataTo(data); return data.ToBitmap(image.Width, image.Height , format); } public static Bitmap ToBitmap(this ColorImageFrame image) { return image.ToBitmap(PixelFormat.Format32bppRgb); } public static Bitmap ToBitmap(this DepthImageFrame image) { return image.ToBitmap(PixelFormat.Format16bppRgb565); } // bitmapsource methods public static Media.Imaging.BitmapSource ToBitmapSource(this ColorImageFrame image) { if (image == null || image.PixelDataLength == 0) return null; var data = new byte[image.PixelDataLength]; image.CopyPixelDataTo(data); return data.ToBitmapSource(Media.PixelFormats.Bgr32, image.Width, image.Height); } public static Media.Imaging.BitmapSource ToBitmapSource(this DepthImageFrame image) { if (image == null || image.PixelDataLength == 0) return null; var data = new short[image.PixelDataLength]; image.CopyPixelDataTo(data); return data.ToBitmapSource(Media.PixelFormats.Bgr555, image.Width, image.Height); } public static Media.Imaging.BitmapSource ToTransparentBitmapSource(this byte[] data , int width, int height) { return data.ToBitmapSource(Media.PixelFormats.Bgra32, width, height); }
注意到上面的代碼中有三種不同的像元格式。Bgr32格式就是32位彩色影像,它由RGB三個通道。Bgra32也是32位,但是她使用了第四個稱之為alpha的通道,用來表示透明度。最後Bgr555是16位影像格式。在之前的深度影像處理那篇文章中,深度影像中的每一個像元都代表2個字節,555代表紅綠藍三個通道每一個通道占用5位。在深度影像處理中,也可以使用Bgr565格式,這種格式中綠色通道占用6位,你也可以添加一個使用這種格式的擴展方法。
1.2.4 調用擴展方法
為了在之前的MainWindows的後台代碼中使用之前寫的擴展方法,我們首先需要添加ImageManipulationExtensionMethods命名空間,這是擴展方法所在的命名空間。現在利用這些擴展方法,我們能夠方便的將影像和深度數據轉換為字節數組了,如下代碼:
void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { this.depthImage.Source = e.OpenDepthImageFrame().ToBitmap().ToBitmapSource(); } void ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { this.rgbImage.Source = e.OpenColorImageFrame().ToBitmapSource(); }
1.2.5 編寫轉換方法
我們需要一些方法在不同的類型之間進行轉換。如果能夠從System.Windows.Media.Imaging.BitmapSource對象轉換到System.Drawing.Bitmap對象,或者相反方向轉換,這對我們的開發將會很有用處。下面的代碼展示了這兩個類型之間的轉換。這些很有用處的幫助方法添加到了自己的類庫中,就能夠測試他們了。例如,可以將depthImage.Source賦值為e.Image.Frame.Image.ToBitmapSource().ToBitmap().ToBitmapSource()這樣來進行測試。
// conversion between bitmapsource and bitmap [DllImport("gdi32")] private static extern int DeleteObject(IntPtr o); public static Media.Imaging.BitmapSource ToBitmapSource(this Bitmap bitmap) { if (bitmap == null) return null; IntPtr ptr = bitmap.GetHbitmap(); var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( ptr, IntPtr.Zero, Int32Rect.Empty, Media.Imaging.BitmapSizeOptions.FromEmptyOptions()); DeleteObject(ptr); return source; } public static Bitmap ToBitmap(this Media.Imaging.BitmapSource source) { Bitmap bitmap; using (MemoryStream outStream = new MemoryStream()) { var enc = new Media.Imaging.PngBitmapEncoder(); enc.Frames.Add(Media.Imaging.BitmapFrame.Create(source)); enc.Save(outStream); bitmap = new Bitmap(outStream); } return bitmap; }
DeleteObject方法稱之為PInvoke調用,這使得我們可以使用操作系統的內建方法來進行內存管理。我們在ToBitmapSource中使用該方法來保證不會發生內存洩漏。
2. 近距離探測(Proximity Detection)
多虧了Kinect sensor for Xbox360的成功,他使得人們開始將Kinect應用程序作為一個全新的用戶體驗手段。但是,Kinect也可以對以一些標准的使用鼠標鍵盤,觸控板作為輸入設備的應用程序進行簡單的用戶體驗增強。例如,我們可以只使用Kinect的麥克風陣列來作為傳統語音輸入設備的一種替代品,或者我們可以僅僅使用Kinect的視覺分析功能來進行一些簡單的識別,這些識別可能並不需要深度或者骨骼數據。
在本節,我們將探索如何將Kinect設備作為一個近距離探測傳感器。為了演示這一點,我們處理的場景可能在以前看到過。就是某一個人是否站在Kinect前面,在Kinect前面移動的是人還是什麼其他的物體。當我們設置的觸發器超過一定的阈值,我們就發起另一個處理線程。類似的觸發器如當用戶走進房間時,我們打開房間裡面的燈。對於商業廣告系統,當展示牌前面沒有人時,可以以“attract”模式展現內容,而當人靠近展示牌時,則展現一些更多的可供交互的內容。和僅僅編寫交互性強的應用程序不同,我們可以編寫一些能夠感知周圍環境的應用程序。
Kinect甚至可以被我們改造成一個安全的攝像頭,當某些重要的特征發生時,我們可以記錄下Kinect看到的景象。在晚上,我在門前放了一些食物,住在哪兒的貓可以吃到。最近我開始懷疑有其他的動物在偷吃我們家貓的食物。我將Kinect作為一個運動探測和視頻錄像機放在門口,這樣就可以知道真實發生的情況了。如果你喜歡一些自然景像,你可以通過簡單的設置來實現長時間的錄像來獲取其他動物的出現情況。雖然在探測到有動物靠近時開啟視頻影像錄制可以節省磁盤空間,但是識別有動物靠近可能需要花費很長的時間。如果像我這樣,你可以打開影像錄制功能,這樣你能夠看到長時間的景象變化,比如風吹葉落的聲音。Kinect作為一種顯示增強的工具,其不僅僅可以作為應用程序的輸入設備,一些新的Kinect的可能應用正在迅速發掘出來。
2.1 簡單的近距離探測
為了說明這一概念,我們將要建立一個近距離探測應用,當有人站在Kinect設備前面時,打開視頻影像錄制。自然,當有用戶進入到Kinect的視野范圍時需要觸發一些列的操作。最簡單的實現近距離探測的方法是使用KinectSDK中的骨骼探測功能。
首先創建一個名為KinectProximityDetectionUsingSkeleton的WPF應用程序,添加Microsoft.Kinect.dll和對System.Drawing命名空間的引用。將ImageExtensions.cs類文件拷貝到項目中,並添加對ImageManipulationExtensionMethods命名空間的引用。主界面元素非常簡單,我們只是添加了一個名為rgbImage的Image對象來從Kinect影像數據中獲取並顯示數據。
<Grid >
<Image Name="rgbImage" Stretch="Fill"/>
</Grid>
下面的代碼顯示了一些初始化代碼。大部分的代碼都是在為Image提供數據源。在MainWindows的構造函數中,對_kinectSensor對象進行了初始化,並注冊影像數據流和骨骼數據流響應事件。這部分代碼和以前我們寫的代碼類似。所不同的是,我們添加了一個布爾型的_isTracking來表示是否我們的近距離探測算法識別到了有人進入視野。如果有,則更新影像數據流,更新image對象。如果沒有,我們略過影像數據流,給Image控件的Source屬性賦null值。
Microsoft.Kinect.KinectSensor _kinectSensor; bool _isTracking = false; public MainWindow() { InitializeComponent(); this.Unloaded += delegate{ _kinectSensor.ColorStream.Disable(); _kinectSensor.SkeletonStream.Disable(); }; this.Loaded += delegate { _kinectSensor = Microsoft.Kinect.KinectSensor.KinectSensors[0]; _kinectSensor.ColorFrameReady += ColorFrameReady; _kinectSensor.ColorStream.Enable(); _kinectSensor.Start(); }; } void ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { if (_isTracking) { using (var frame = e.OpenColorImageFrame()) { if (frame != null) rgbImage.Source = frame.ToBitmapSource(); }; } else rgbImage.Source = null; } private void OnDetection() { if (!_isTracking) _isTracking = true; } private void OnDetectionStopped() { _isTracking = false; }
為了能夠處理_isTracking標簽,我們需要注冊KienctSensor.SkeletonFrameReady事件。SkeletonFrameReady事件類似心髒跳動一樣驅動程序的運行。只要有物體在Kinect前面,SkeletonFrameReady事件就會觸發。在我們的代碼中,我們需要做的是檢查骨骼數據數組,判斷數組中是否有骨骼數據處在追蹤狀態中。代碼如下。
有時候我們不需要拋出事件。我們有一個內建的機制能夠通知我們有人體進入到了Kinect視野范圍內,但是,我們沒有一個機制能夠告訴在什麼時候人走出了視野或者不在追蹤狀態。為了實現這一功能,不管是否探測到了用戶,我們開啟一個計時器,這個計時器的功能是存儲最後一次追蹤到的事件的時間,我們檢查當前時間和這一時間的時間差,如果時差超過某一個阈值,就認為我們失去了對物體的追蹤,我們應該結束當前的近距探測。
int _threshold = 100; DateTime _lastSkeletonTrackTime; DispatcherTimer _timer = new DispatcherTimer(); public MainWindow() { InitializeComponent(); this.Unloaded += delegate{ _kinectSensor.ColorStream.Disable(); _kinectSensor.SkeletonStream.Disable(); }; this.Loaded += delegate { _kinectSensor = Microsoft.Kinect.KinectSensor.KinectSensors[0]; _kinectSensor.ColorFrameReady += ColorFrameReady; _kinectSensor.ColorStream.Enable(); _kinectSensor.SkeletonFrameReady += Pulse; _kinectSensor.SkeletonStream.Enable(); _timer.Interval = new TimeSpan(0, 0, 1); _timer.Tick += new EventHandler(_timer_Tick); _kinectSensor.Start(); }; } void _timer_Tick(object sender, EventArgs e) { if (DateTime.Now.Subtract(_lastSkeletonTrackTime).TotalMilliseconds > _threshold) { _timer.Stop(); OnDetectionStopped(); } } private void Pulse(object sender, SkeletonFrameReadyEventArgs e) { using (var skeletonFrame = e.OpenSkeletonFrame()) { if (skeletonFrame == null || skeletonFrame.SkeletonArrayLength == 0) return; Skeleton[] skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength]; skeletonFrame.CopySkeletonDataTo(skeletons); for (int s = 0; s < skeletons.Length; s++) { if (skeletons[s].TrackingState == SkeletonTrackingState.Tracked) { OnDetection(); _lastSkeletonTrackTime = DateTime.Now; if (!_timer.IsEnabled) { _timer.Start(); } break; } } } }
查看本欄目
2.2 使用景深數據進行近距離探測
使用骨骼追蹤進行近距離探測是近距離探測的基礎,當沒有一個人進入到視野中,並進行交互時,電子廣告牌進入“StandBy”模式,在這種情況下,我們只是簡單的播放一些視頻。不幸的是,骨骼追蹤不能很好的捕捉類似在我家後面的門廊上的偷食物的浣熊或者是在曠野中的大腳野人的圖像。這是因為骨骼追蹤的算法是針對人類的關鍵特征以及特定的人體類型進行設計的。超出人體的范圍,在Kinect鏡頭前骨骼追蹤會失敗或者是追蹤會變的時斷時續。
為了處理這一情況,我們可以使用Kinect的深度影像數據,而不能依靠骨骼追蹤。深度影像數據也是近距離探測的一種基本類型。如下代碼所示,程序運行中必須配置或者獲取彩色影像和深度影像數據流,而不是骨骼數據流。
_kinectSensor.ColorFrameReady += ColorFrameReady;
_kinectSensor.DepthFrameReady += DepthFrameReady;
_kinectSensor.ColorStream.Enable();
_kinectSensor.DepthStream.Enable();
相比骨骼追蹤數據,使用景深數據作為近距探測算法的基礎數據有一些優點。首先只要傳感器在運行,那麼深度影像數據就是連續的。這避免了需要另外設置一個計時器來監控在探測過程是否意外終止。另外,我們可以對我們要探測的對象離Kinect的距離設置一個最小和最大的距離阈值。當物體離Kinect的距離比這個最小的阈值還要小,或者超過最大阈值的范圍時,將_isTracking設置為false。下面的代碼中,我們探測距離Kinect 1米至1.2米的對象。通過分析深度影像數據的每一個像素來判斷是否有像元落在該距離范圍內。如果有一個像元落在該范圍內,那麼停止對影像的繼續分析,將isTracking設置為true。ColorFrameReady事件處理探測到物體的事件,然後使用彩色影像數據來更新image對象。
void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { bool isInRange = false; using (var imageData = e.OpenDepthImageFrame()) { if (imageData == null || imageData.PixelDataLength == 0) return; short[] bits = new short[imageData.PixelDataLength]; imageData.CopyPixelDataTo(bits); int minThreshold = 1000; int maxThreshold = 1200; for (int i = 0; i < bits.Length; i += imageData.BytesPerPixel) { var depth = bits[i] >> DepthImageFrame.PlayerIndexBitmaskWidth; if (depth > minThreshold && depth < maxThreshold) { isInRange = true; OnDetection(); break; } } } if (!isInRange) OnDetectionStopped(); }
相比骨骼數據,使用深度影像數據進行近距離探測的最後一個好處是速度較快。即使在比我們對景深數據處理更低的級別上進行骨骼追蹤,骨骼追蹤仍需要有完整的人體出現在Kinect視野中。同時Kinect SDK需要利用決策樹分析整個人體影像數據,並將識別出來的結果和骨骼識別預設的一些特征參數進行匹配,以判斷是否是人體而不是其他物體。使用景深影像數據算法,我們只需要查找是否有一個像元點落在指定的深度值范圍內,而不用分析整個人體。和之前的骨骼追中算法不同,代碼中深度值探測算法只要有物體落在Kinect傳感器的視野范圍內,都會觸發OnDetection方法。
2.3 對近距離探測的改進
當然,使用景深數據進行近距離探測也有一些缺點。最小和最大深度阈值必須明確定義,這樣才能避免_isTracking永遠為true的情況。深度影像允許我們放松只能對人體進行近距離探測的這一限制,但是這一放松有點過了,使得即使一些靜止不動的物體可能也會觸發近距離探測。在實現一個運動測試來解決這一問題之前,我們可以實現一個探測條件不緊也不松的近距離探測。
下面的代碼展示了如何結合深度影像數據中的深度值數據和游戲者索引位數據來實現一個近距離探測器。如果骨骼追蹤算法符合你的需求,同時你又想將探測的對象限定在距離傳感器的最大最小距離阈值范圍內,這種方法是最好的選擇。這在露天的廣告牌中也很有用,比如可以在廣告牌前設置一個區域范圍。當有人進入到這一范圍時觸發交互。當人進入到距離裝有Kinect的廣告牌1米至1.5時觸發另一種交互,當人離廣告牌夠近以至於可以觸摸到時,觸發另外一種交互。要建立這種類型的近距離探測,需要在MainWindows的構造函數中開啟骨骼追蹤功能,使得能夠使用景深影像的深度數據和游戲者索引位數據。這些都做好了之後,可以改寫之前例子中的DepthFrameReady事件,來對距離阈值進行判斷,並檢查是否有游戲者索引位數據存在。代碼如下:
void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { bool isInRange = false; using (var imageData = e.OpenDepthImageFrame()) { if (imageData == null || imageData.PixelDataLength == 0) return; short[] bits = new short[imageData.PixelDataLength]; imageData.CopyPixelDataTo(bits); int minThreshold = 1700; int maxThreshold = 2000; for (int i = 0; i < bits.Length; i += imageData.BytesPerPixel) { var depth = bits[i] >> DepthImageFrame.PlayerIndexBitmaskWidth; var player = bits[i] & DepthImageFrame.PlayerIndexBitmask; if (player > 0 && depth > minThreshold && depth < maxThreshold) { isInRange = true; OnDetection(); break; } } } if(!isInRange) OnDetectionStopped(); }
3. 結語
本文之前的十三篇博文介紹了Kinect for Windows SDK開發的基礎知識,但是Kinect開發涉及到的技術很多,有時候,這些基本的知識並不能滿足我們的實際需求,本文介紹了Kinect for Windows SDK開發進階需要了解的一些內容,包括影像處理幫助類庫Coding4Fun Kinect工具類庫以及如何建立自己的擴展方法類庫來方便開發,接下來介紹了利用Kinect進行近距離探測的一些方法,限於篇幅原因,本文僅僅介紹了近距離探測的三種方式。
下文將繼續介紹近距離探測中如何探測運動,如何獲取並保存產生的影像數據;然後將會介紹如何進行臉部識別,以及介紹全息圖(Holograme)的一些知識,最後還會介紹其他一些需要關注的類庫,敬請期待!
作者: yangecnu(yangecnu's Blog on 博客園)
出處:http://www.cnblogs.com/yangecnu/