前面十三篇文章介绍了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进行近距离探测的一些方法,限于篇幅原因,本文仅仅介绍了近距离探测的三种方式。本文所有代码点击此处下载,希望以上这些内容对于您了解Kinect SDK有所帮助。
下文将继续介绍近距离探测中如何探测运动,如何获取并保存产生的影像数据;然后将会介绍如何进行脸部识别,以及介绍全息图(Holograme)的一些知识,最后还会介绍其他一些需要关注的类库,敬请期待!