Kinect尝鲜(1)——第一个程序
曾经微软宣传Kinect宣传的很火,但一直没有舍得买一台。第一次接触是在某个hackathon上,想做一个空气鼠标的项目,借助Kinect实现的,感觉这个产品挺惊艳。最近想方设法借到一台一代的Kinect for Windows,还有微软官方的开发书籍(《Kinect应用开发实战——用最自然的方式与机器对话》),略研究了下Kinect的开发。
一、环境配置
关于Kinect的介绍网上有很多资料,这里不再赘述。既然是开发微软自家的产品,肯定要上微软全家桶,VS2015(C#)+SDK V1.8+Developer toolkit V1.8。其中SDK可以直接在微软官网上下载,除了官方SDK,还有其它的SDK,我不是很了解,所以不敢妄言介绍。一代Kinect有windows和xbox 360两个版本,windows版本的Kinect前面写着“Kinect”,而xbox 360版本前面写着“xbox 360”,xbox版的连接电脑需要有转接线,但是很诡异的是我曾经直接用xbox版的连接电脑也成功了。并且我最开始安装的SDK是V2.0,也能成功跑起来Kinect V1……虽说SDK V2.0只能驱动二代Kinect,但也许微软还是照顾了旧版本的硬件吧。不过为了稳妥,还是安装SDK V1.8,并且使用Kinect for Windows。
将Kinect连接上电脑之后,可以打开Developer toolkit browser,运行其中某一个demo,来检验Kinect是否正常工作。一般情况下,正常工作是Kinect正面绿灯一直亮。在这里不得不吐槽下Kinect的电源线质量问题,两次接触Kinect都是电源线有问题。这时只有USB供电,电压不足,状态是红灯一直亮,这种情况下更换电源线就好了。
二、正式开发
环境配好之后,打开VS2015,新建一个WPF窗体工程的解决方案,然后在引用里面添加Kinect v1.8,然后在程序中using Microsoft.Kinect即可。Kinect视频方面主要包括采集彩色数据、采集深度数据、追踪骨骼三个功能,此外还有通过麦克风阵列采集声音数据。
三、第一个程序
Kinect有两个摄像头,分别是彩色摄像头和深度摄像头,所以第一个程序就是实现获取两个摄像头采集到的彩色视频流和深度视频流。在MainWindow.xaml文件里,在工具箱中选中Image,向窗体中添加两个大小为640*480的Image,不重叠,分别命名为depthImage和colorImage;在Window标签中添加属性Loaded="Window_Loaded" Closed=Window_Closed,最终Xaml文件代码如下:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:KinectWpfApplication1" xmlns:WpfViewers="clr-namespace:Microsoft.Samples.Kinect.WpfViewers;assembly=Microsoft.Samples.Kinect.WpfViewers" x:Class="KinectWpfApplication1.MainWindow" mc:Ignorable="d" Title="MainWindow" Height="590" Width="1296" Loaded="Window_Loaded" Closed="Window_Closed"> <Grid> <Image x:Name="depthImage" HorizontalAlignment="Left" Height="480" Margin="650,0,-0.4,0" VerticalAlignment="Top" Width="640"/> <Image x:Name="colorImage" HorizontalAlignment="Left" Height="480" VerticalAlignment="Top" Width="640"/> </Grid> </Window>
Kinect的调用是使用已经封装好的KinectSensor类,用于管理Kinect资源。该类同样支持多个Kinect同时工作,因为我只弄到一台,所以多台Kinect的情况不予考虑。定义KinectSensor _kinect;在Window_Load()中添加函数StartKinect(),然后定义StartKinect函数如下:
private void StartKinect() { if (KinectSensor.KinectSensors.Count <= 0) { MessageBox.Show("No Kinect device foound!"); return; } _kinect = KinectSensor.KinectSensors[0]; //MessageBox.Show("Status:" + _kinect.Status); _kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30); _kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30); _kinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(KinectColorFrameReady); _kinect.DepthFrameReady += new EventHandler<DepthImageFrameReadyEventArgs>(KinectDepthFrameReady); //_kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(_kinect_AllFrameReady); _kinect.Start(); }
第一个if可以判断有几台Kinect工作,如果没有就提示,然后获取第一台Kinect设备。定义彩色视频流和深度视频流的格式,包括颜色格式、视频大小和帧速。一代Kinect只有640*480FPS30和1280*720FPS12,二代的图像分辨率和帧速都比一代优秀。接下来注册事件,这里要介绍一下Kinect的两种模型——事件模型和轮询模型,事件模型就如同上述代码中,彩色视频采集到一帧之后会触发事件ColorFrameReady,然后在事件属性ColorFrameReadyArgs中处理数据,深度视频和骨骼追踪也是如此,除了分别处理事件,还有三种帧都采集完毕后触发的AllFramesReady,但是集中处理的代码运行后十分卡顿,所以我没有使用这一事件;另一种是轮询模型,与事件模型的“等待Kinect给数据”不同,该模型是去向Kinect“主动要数据”,这种方法更快,也更适合多线程,这一模型以后会介绍。事件模型的优点在于代码可读性好,对编程语言来说显得更加优雅。我们注册的处理深度数据和彩色视频数据的方法代码如下:
private void KinectColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { using (ColorImageFrame colorImageFrame = e.OpenColorImageFrame()) { if (colorImageFrame == null) return; byte[] pixels = new byte[colorImageFrame.PixelDataLength]; colorImageFrame.CopyPixelDataTo(pixels); int stride = colorImageFrame.Width * 4; colorImage.Source = BitmapSource.Create(colorImageFrame.Width, colorImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride); } } private void KinectDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame()) { if (depthImageFrame == null) return; short[] depthPixelData = new short[depthImageFrame.PixelDataLength]; depthImageFrame.CopyPixelDataTo(depthPixelData); byte[] pixels = ConvertDepthFrameToColorFrame(depthPixelData, ((KinectSensor)sender).DepthStream); int stride = depthImageFrame.Width * 4; depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride); } }
ConvertDepthFrameToColorFrame()是将深度数据流转为彩色数据,以便在Image控件上显示。
/// <summary> /// 将16位灰阶深度图转为32位彩色深度图 /// </summary> /// <param name="depthImageFrame">16位灰阶深度图</param> /// <param name="depthImageStream">用于获得深度数据流的相关属性</param> /// <returns></returns> private byte[] ConvertDepthFrameToColorFrame(short[] depthImageFrame, DepthImageStream depthImageStream) { byte[] depthFrame32 = new byte[depthImageStream.FrameWidth * depthImageStream.FrameHeight * bgr32BytesPerPixel]; //通过常量获取有效视距,不用硬编码 int tooNearDepth = depthImageStream.TooNearDepth; int tooFarDepth = depthImageStream.TooFarDepth; int unknowDepth = depthImageStream.UnknownDepth; for (int i16 = 0, i32 = 0; i16 < depthImageFrame.Length && i32 < depthFrame32.Length; i16++, i32 += 4) { int player = depthImageFrame[i16] & DepthImageFrame.PlayerIndexBitmask; int realDepth = depthImageFrame[i16] >> DepthImageFrame.PlayerIndexBitmaskWidth; //通过位运算,将13位的深度图裁剪位8位 byte intensity = (byte)(~(realDepth >> 4)); if (player == 0 && realDepth == 0) { depthFrame32[i32 + redIndex] = 255; depthFrame32[i32 + greenIndex] = 255; depthFrame32[i32 + blueIndex] = 255; } else if (player == 0 && realDepth == tooFarDepth) { //深紫色 depthFrame32[i32 + redIndex] = 66; depthFrame32[i32 + greenIndex] = 0; depthFrame32[i32 + blueIndex] = 66; } else if (player == 0 && realDepth == unknowDepth) { //深棕色 depthFrame32[i32 + redIndex] = 66; depthFrame32[i32 + greenIndex] = 66; depthFrame32[i32 + blueIndex] = 33; } else { depthFrame32[i32 + redIndex] = (byte)(intensity >> intensityShiftByPlayerR[player]); depthFrame32[i32 + greenIndex] = (byte)(intensity >> intensityShiftByPlayerG[player]); depthFrame32[i32 + blueIndex] = (byte)(intensity >> intensityShiftByPlayerB[player]); } } return depthFrame32; }
其中player是Kinect通过深度数据判断出视野内有多少人,人体区域用鲜艳的颜色标记。
最后,在 Window_Closed 中关闭Kinect:
private void Window_Closed(object sender, EventArgs e) { if (_kinect != null) { if (_kinect.Status == KinectStatus.Connected) { _kinect.Stop(); } } }
完成代码后,就可以生成并运行了。这里暂时不贴图了,等以后有图了再贴。文章末尾会附上完整代码。
四、一点感想
Kinect SDK支持用C++和C#开发,因为C#比较简单再加上VS2015的足够智能化,许多方法直接看函数名就知道用处,所以我选择使用C#。微软在那本书中介绍了NUI的概念,再加上对Kinect开发的了解,以及最近 HoloLens 发行,我感觉Kinect + HoloLens 才是绝配——一个负责处理数据和显示,一个负责人机交互连接现实世界和虚拟世界。NUI必然是未来的趋势,而实现NUI 90%会依靠Kinect或者其它功能类似 Kinect 的设备来实现。虽然 Kinect 市场占有率很小,应用也非常少,但不得不令我猜测微软在下很大的一盘棋,藉此来定义未来的操作系统。
PS. 完整代码(XAML完整代码在上面):
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using Microsoft.Kinect; using System.Threading; namespace KinectWpfApplication1 { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { private KinectSensor _kinect; const float maxDepthDistance = 4095; const float minDepthDistance = 850; const float maxDepthDistancOddset = maxDepthDistance - minDepthDistance; private const int redIndex = 2; private const int greenIndex = 1; private const int blueIndex = 0; private static readonly int[] intensityShiftByPlayerR = { 1, 2, 0, 2, 0, 0, 2, 0 }; private static readonly int[] intensityShiftByPlayerG = { 1, 2, 2, 0, 2, 0, 0, 1 }; private static readonly int[] intensityShiftByPlayerB = { 1, 0, 2, 2, 0, 2, 0, 2 }; private static readonly int bgr32BytesPerPixel = (PixelFormats.Bgr32.BitsPerPixel + 7) / 8; public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { StartKinect(); } private void StartKinect() { if (KinectSensor.KinectSensors.Count <= 0) { MessageBox.Show("No Kinect device foound!"); return; } _kinect = KinectSensor.KinectSensors[0]; //MessageBox.Show("Status:" + _kinect.Status); _kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30); _kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30); _kinect.ColorFrameReady += new EventHandler<ColorImageFrameReadyEventArgs>(KinectColorFrameReady); _kinect.DepthFrameReady += new EventHandler<DepthImageFrameReadyEventArgs>(KinectDepthFrameReady); //_kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(_kinect_AllFrameReady); _kinect.Start(); } private void KinectColorFrameReady(object sender, ColorImageFrameReadyEventArgs e) { using (ColorImageFrame colorImageFrame = e.OpenColorImageFrame()) { if (colorImageFrame == null) return; byte[] pixels = new byte[colorImageFrame.PixelDataLength]; colorImageFrame.CopyPixelDataTo(pixels); int stride = colorImageFrame.Width * 4; colorImage.Source = BitmapSource.Create(colorImageFrame.Width, colorImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride); } } private void KinectDepthFrameReady(object sender, DepthImageFrameReadyEventArgs e) { using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame()) { if (depthImageFrame == null) return; short[] depthPixelData = new short[depthImageFrame.PixelDataLength]; depthImageFrame.CopyPixelDataTo(depthPixelData); byte[] pixels = ConvertDepthFrameToColorFrame(depthPixelData, ((KinectSensor)sender).DepthStream); int stride = depthImageFrame.Width * 4; depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride); } /* using (DepthImageFrame depthImageFrame = e.OpenDepthImageFrame()) { if (depthImageFrame == null) return; byte[] pixels = ConvertDepthFrameToGrayFrame(depthImageFrame); int stride = depthImageFrame.Width * 4; depthImage.Source = BitmapSource.Create(depthImageFrame.Width, depthImageFrame.Height, 96, 96, PixelFormats.Bgr32, null, pixels, stride); }*/ } /// <summary> /// 单色直方图计算公式,返回256色灰阶,颜色越黑越远。 /// </summary> /// <param name="dis">深度值,有效值为......</param> /// <returns></returns> private static byte CalculateIntensityFromDepth(int dis) { return (byte)(255 - (255 * Math.Max(dis - minDepthDistance, 0) / maxDepthDistancOddset)); } /// <summary> /// 生成BGR32格式的图片字节数组 /// </summary> /// <param name="depthImageFrame"></param> /// <returns></returns> private byte[] ConvertDepthFrameToGrayFrame(DepthImageFrame depthImageFrame) { short[] rawDepthData = new short[depthImageFrame.PixelDataLength]; depthImageFrame.CopyPixelDataTo(rawDepthData); byte[] pixels = new byte[depthImageFrame.Height * depthImageFrame.Width * 4]; for (int depthIndex = 0, colorIndex = 0; depthIndex < rawDepthData.Length && colorIndex < pixels.Length; depthIndex++, colorIndex += 4) { int player = rawDepthData[depthIndex] & DepthImageFrame.PlayerIndexBitmask; int depth = rawDepthData[depthIndex] >> DepthImageFrame.PlayerIndexBitmaskWidth; if (depth <= 900) { //离Kinect很近 pixels[colorIndex + blueIndex] = 255; pixels[colorIndex + greenIndex] = 0; pixels[colorIndex + redIndex] = 0; } else if (depth > 900 && depth < 2000) { pixels[colorIndex + blueIndex] = 0; pixels[colorIndex + greenIndex] = 255; pixels[colorIndex + redIndex] = 0; } else if (depth >= 2000) { //离Kinect超过2米 pixels[colorIndex + blueIndex] = 0; pixels[colorIndex + greenIndex] = 0; pixels[colorIndex + redIndex] = 255; } //单色直方图着色 byte intensity = CalculateIntensityFromDepth(depth); pixels[colorIndex + blueIndex] = intensity; pixels[colorIndex + greenIndex] = intensity; pixels[colorIndex + redIndex] = intensity; //如果是人体区域,用亮绿色标记 /*if (player > 0) { pixels[colorIndex + blueIndex] = Colors.LightGreen.B; pixels[colorIndex + greenIndex] = Colors.LightGreen.G; pixels[colorIndex + redIndex] = Colors.LightGreen.R; }*/ } return pixels; } /// <summary> /// 将16位灰阶深度图转为32位彩色深度图 /// </summary> /// <param name="depthImageFrame">16位灰阶深度图</param> /// <param name="depthImageStream">用于获得深度数据流的相关属性</param> /// <returns></returns> private byte[] ConvertDepthFrameToColorFrame(short[] depthImageFrame, DepthImageStream depthImageStream) { byte[] depthFrame32 = new byte[depthImageStream.FrameWidth * depthImageStream.FrameHeight * bgr32BytesPerPixel]; //通过常量获取有效视距,不用硬编码 int tooNearDepth = depthImageStream.TooNearDepth; int tooFarDepth = depthImageStream.TooFarDepth; int unknowDepth = depthImageStream.UnknownDepth; for (int i16 = 0, i32 = 0; i16 < depthImageFrame.Length && i32 < depthFrame32.Length; i16++, i32 += 4) { int player = depthImageFrame[i16] & DepthImageFrame.PlayerIndexBitmask; int realDepth = depthImageFrame[i16] >> DepthImageFrame.PlayerIndexBitmaskWidth; //通过位运算,将13位的深度图裁剪位8位 byte intensity = (byte)(~(realDepth >> 4)); if (player == 0 && realDepth == 0) { depthFrame32[i32 + redIndex] = 255; depthFrame32[i32 + greenIndex] = 255; depthFrame32[i32 + blueIndex] = 255; } else if (player == 0 && realDepth == tooFarDepth) { //深紫色 depthFrame32[i32 + redIndex] = 66; depthFrame32[i32 + greenIndex] = 0; depthFrame32[i32 + blueIndex] = 66; } else if (player == 0 && realDepth == unknowDepth) { //深棕色 depthFrame32[i32 + redIndex] = 66; depthFrame32[i32 + greenIndex] = 66; depthFrame32[i32 + blueIndex] = 33; } else { depthFrame32[i32 + redIndex] = (byte)(intensity >> intensityShiftByPlayerR[player]); depthFrame32[i32 + greenIndex] = (byte)(intensity >> intensityShiftByPlayerG[player]); depthFrame32[i32 + blueIndex] = (byte)(intensity >> intensityShiftByPlayerB[player]); } } return depthFrame32; } private void Window_Closed(object sender, EventArgs e) { if (_kinect != null) { if (_kinect.Status == KinectStatus.Connected) { _kinect.Stop(); } } } } }