前幾篇文章已系統介紹了 Kinect for Windows SDK 的基礎開發知識,涵蓋影像資料流、景深攝影機、骨骼追蹤、麥克風陣列與語音辨識等核心功能。然而,掌握這些基礎後,若要開發出具備優質使用者體驗的應用程式,往往仍感力有未逮。Kinect SDK 提供的功能與其他類庫大同小異,真正的應用潛力在於整合其他技術。本文將介紹協助處理 Kinect 感測器資料的第三方類庫與進階技術,幫助開發者突破基礎框架的限制。
在 .NET 架構下,System.Windows.Media (WPF) 與 System.Drawing (WinForms/GDI+) 兩大圖形命名空間擁有各自獨立的形狀與顏色類別,彼此不相容。引入 Kinect 後,情況更複雜:Kinect 專有的 ImageFrame 必須轉為 WPF 的 ImageSource 才能顯示,而第三方影像處理庫 (如 Emgu CV) 又只認識 System.Drawing 類型。這導致開發者必須在多種類型間繞彎轉換:
Kinect (Microsoft.Kinect) → System.Drawing → Emgu CV → System.Drawing → System.Windows.Media (WPF)
Coding4Fun Kinect Toolkit 專為簡化此類轉換而設計,包含三個獨立 DLL:
Coding4Fun.Kinect.Wpf.dll:提供擴充方法,在 Microsoft.Kinect 與 System.Windows.Media 間雙向轉換。Coding4Fun.Kinect.WinForm.dll:提供擴充方法,在 Microsoft.Kinect 與 System.Drawing 間轉換。可惜它缺少 System.Drawing 與 System.Windows.Media 直接互轉的方法,且部分深度影像處理邏輯被封裝在簡單轉換方法中,有時行為不如預期。但它有兩大優點:開源可參考原始碼、皆為擴充方法,方便開發者自行擴充。
為了獲得更穩定、符合預期的轉換行為,建議建立專屬的擴充方法類別。以下示範核心轉換邏輯:
Microsoft.Kinect.dll、System.Drawing.dllusing Media = System.Windows.Media; 避免類別名稱衝突namespace ImageManipulationExtensionMethods {
public static class ImageExtensions {
// byte[] → System.Drawing.Bitmap
public static Bitmap ToBitmap(this byte[] data, int width, int height, PixelFormat format) {
var bitmap = new Bitmap(width, height, format);
var bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, format);
Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
bitmap.UnlockBits(bmpData);
return bitmap;
}
// short[] (深度資料) → Bitmap
public static Bitmap ToBitmap(this short[] data, int width, int height, PixelFormat format) {
var bitmap = new Bitmap(width, height, format);
var bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, format);
Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
bitmap.UnlockBits(bmpData);
return bitmap;
}
// byte[] → WPF BitmapSource
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);
}
// short[] → WPF BitmapSource
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);
}
// ColorImageFrame → BitmapSource (Bgr32)
public static Media.Imaging.BitmapSource ToBitmapSource(this ColorImageFrame frame) {
if (frame == null || frame.PixelDataLength == 0) return null;
var data = new byte[frame.PixelDataLength];
frame.CopyPixelDataTo(data);
return data.ToBitmapSource(Media.PixelFormats.Bgr32, frame.Width, frame.Height);
}
// DepthImageFrame → BitmapSource (Bgr555)
public static Media.Imaging.BitmapSource ToBitmapSource(this DepthImageFrame frame) {
if (frame == null || frame.PixelDataLength == 0) return null;
var data = new short[frame.PixelDataLength];
frame.CopyPixelDataTo(data);
return data.ToBitmapSource(Media.PixelFormats.Bgr555, frame.Width, frame.Height);
}
// Bitmap ↔ BitmapSource 互轉 (含記憶體釋放)
[DllImport("gdi32")] private static extern int DeleteObject(IntPtr o);
public static Media.Imaging.BitmapSource ToBitmapSource(this Bitmap bitmap) {
if (bitmap == null) return null;
IntPtr hBitmap = bitmap.GetHbitmap();
var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
DeleteObject(hBitmap); // 防止記憶體洩漏
return source;
}
public static Bitmap ToBitmap(this Media.Imaging.BitmapSource source) {
using (var ms = new MemoryStream()) {
var encoder = new Media.Imaging.PngBitmapEncoder();
encoder.Frames.Add(Media.Imaging.BitmapFrame.Create(source));
encoder.Save(ms);
return new Bitmap(ms);
}
}
}
}
void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) {
this.depthImage.Source = e.OpenDepthImageFrame().ToBitmapSource();
}
void ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) {
this.rgbImage.Source = e.OpenColorImageFrame().ToBitmapSource();
}
自建類庫的好處在於:邏輯透明、參數可控、易於除錯,避免第三方封裝的「黑盒子」行為差異。
近距離偵測不只是互動的觸發器,更能讓應用程式「感知環境」。例如:數位看板在無人時播放廣告,有人靠近切換互動模式;安防系統偵測移動物體自動錄影。Kinect 的深度與骨骼資料是實現此功能的關鍵。
利用 SkeletonFrameReady 事件搭配 DispatcherTimer 判斷追蹤中斷:
private void Pulse(object sender, SkeletonFrameReadyEventArgs e) {
using (var frame = e.OpenSkeletonFrame()) {
if (frame == null) return;
var skeletons = new Skeleton[frame.SkeletonArrayLength];
frame.CopySkeletonDataTo(skeletons);
foreach (var s in skeletons) {
if (s.TrackingState == SkeletonTrackingState.Tracked) {
OnDetection(); // 觸發偵測到事件
_lastTrackTime = DateTime.Now;
if (!_timer.IsEnabled) _timer.Start();
break;
}
}
}
}
void _timer_Tick(object sender, EventArgs e) {
if (DateTime.Now.Subtract(_lastTrackTime).TotalMilliseconds > _threshold) {
_timer.Stop();
OnDetectionStopped(); // 觸發離開事件
}
}
優點:精準鎖定人體;缺點:非人體物體 (寵物、機器人) 會被忽略,且需額外計時器處理中斷邏輯。
直接分析 DepthImageFrame 的像素深度值,設定距離閾值區間 (如 1.0m~1.2m):
void DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) {
bool inRange = false;
using (var frame = e.OpenDepthImageFrame()) {
if (frame == null) return;
var bits = new short[frame.PixelDataLength];
frame.CopyPixelDataTo(bits);
// 深度值儲存於高位元,需右移 PlayerIndexBitmaskWidth
for (int i = 0; i < bits.Length; i += frame.BytesPerPixel) {
int depth = bits[i] >> DepthImageFrame.PlayerIndexBitmaskWidth;
if (depth > 1000 && depth < 1200) { // 1000~1200 毫米
inRange = true; OnDetection(); break;
}
}
}
if (!inRange) OnDetectionStopped();
}
優點:資料連續無需計時器、可偵測非人體物體、演算法簡單速度快;缺點:靜止物體也會觸發、需精確設定深度閾值。
結合骨骼追蹤的「人體辨識」與深度資料的「距離篩選」,僅當 player > 0 (偵測到玩家) 且深度在範圍內才觸發:
var depth = bits[i] >> DepthImageFrame.PlayerIndexBitmaskWidth;
var player = bits[i] & DepthImageFrame.PlayerIndexBitmask;
if (player > 0 && depth > 1700 && depth < 2000) { ... }
此法最適合公開展示場域:劃定互動區域,只有真人進入特定距離才觸發,兼具精準度與彈性。
本文介紹了進階開發的兩大基石:影像類型轉換的自主掌控與近距離偵測的多種策略。下一篇將深入探討:
敬請期待《Kinect for Windows SDK 進階開發指南(下)》!