1、译Kinect for Windows SDK 开发入门( 三):基础知识 下1. 性能改进上文的代码中,对于每一个彩色图像帧,都会创建一个新的 Bitmap 对象。由于 Kinect 视频摄像头默认采集频率为每秒 30 幅,所以应用程序每秒会创建 30 个 bitmap 对象,产生 30 次的 Bitmap 内存创建,对象初始化,填充像素数据等操作。这些对象很快就会变成垃圾等待垃圾回收器进行回收。对数据量小的程序来说可能影响不是很明显,但当数据量很大时,其缺点就会显现出来。改进方法是使用 WriteableBitmap 对象。它位于 System.Windows.Media.Imaging
2、命名空间下面,该对象被用来处理需要频繁更新的像素数据。当创建 WriteableBitmap 时,应用程序需要指定它的高度,宽度以及格式,以使得能够一次性为WriteableBitmap 创建好内存,以后只需根据需要更新像素即可。使用 WriteableBitmap 代码改动地方很小。下面的代码中,首先定义三个新的成员变量,一个是实际的 WriteableBitmap 对象,另外两个用来更新像素数据。每一幅图像的大小都是不变的,因此在创建 WriteableBitmap 时只需计算一次即可。InitializeKinect 方法中加粗的部分是更改的代码。创建 WriteableBitmap 对
3、象,准备接收像素数据,图像的范围同时也计算了。在初始化 WriteableBitmap 的时候,同时也绑定了 UI 元素(名为 ColorImageElement 的 Image 对象) 。此时 WriteableBitmap 中没有像素数据,所以 UI 上是空的。private WriteableBitmap colorImageBitmap;private Int32Rect colorImageBitmapRect;private int colorImageStride;private byte colorImagePixelData;if (kinectSensor != null)
4、 ColorImageStream colorStream=kinectSensor.ColorStream;colorStream.Enable();this.colorImageBitMap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight,96, 96, PixelFormats.Bgr32, null);this.colorImageBitmapRect = new Int32Rect(0, 0, colorStream.FrameWidth, colorStream.FrameHeight);t
5、his.colorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;ColorImageElement.Source = this.colorImageBitMap;kinectSensor.ColorFrameReady += kinectSensor_ColorFrameReady;kinectSensor.Start();还需要进行的一处改动是,对 ColorFrameReady 事件响应的代码。如下图。首先删除之前创建 Bitmap 那部分的代码。调用WriteableBitmap 对象的 Wri
6、tePixels 方法来更新图像。方法使用图像的矩形范围,代码像素数据的数组,图像的 Stride,以及偏移(offset).偏移量通常设置为 0。private void Kinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)using (ColorImageFrame frame = e.OpenColorImageFrame()if (frame != null)byte pixelData = new byteframe.PixelDataLength;frame.CopyPixelDataTo(pi
7、xelData);this.colorImageBitmap.WritePixels(this.colorImageBitmapRect, pixelData, this.colorImageStride, 0);基于 Kinect 的应用程序在无论是在显示 ColorImageStream 数据还是显示 DepthImageStream 数据的时候,都应该使用WriteableBitmap 对象来显示帧影像。在最好的情况下,彩色数据流会每秒产生 30 帧彩色影像,这意味着对内存资源的消耗比较大。WriteableBitmap 能够减少这种内存消耗,减少需要更新影响带来的内存开辟和回收操作。毕
8、竟在应用中显示帧数据不是应用程序的最主要功能,所以在这方面减少内像存消耗显得很有必要。2. 简单的图像处理每一帧 ColorImageFrame 都是以字节序列的方式返回原始的像素数据。应用程序必须以这些数据创建图像。这意味这我们可以对这些原始数据进行一定的处理,然后再展示出来。下面来看看如何对获取的原始数据进行一些简单的处理。void kinectSensor_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)using (ColorImageFrame frame = e.OpenColorImageFrame()
9、if (frame != null)byte pixelData = new byteframe.PixelDataLength;frame.CopyPixelDataTo(pixelData);for (int i = 0; i 0xE5)pixelDatai=0x00; elsepixelDatai=0Xff;If (pixelDatai+10xE5)pixelDatai+1=0x00; elsepixelDatai+1=0Xff;If (pixelDatai+20xE5)pixelDatai+2=0x00; elsepixelDatai+1=0Xff;一下是上面操作后的图像:3. 截图有
10、时候,可能需要从彩色摄像头中截取一幅图像,例如可能要从摄像头中获取图像来设置人物头像。为了实现这一功能,首先需要在界面上设置一个按钮,代码如下:private void TakePictureButton_Click(object sender, RoutedEventArgs e)String fileName = “snapshot.jpg“;if (File.Exists(fileName)File.Delete(fileName);using (FileStream savedSnapshot=new FileStream(fileName,FileMode.CreateNew)Bit
11、mapSource image =(BitmapSource) ColorImageElement.Source;JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();jpgEncoder.QualityLevel = 70;jpgEncoder.Frames.Add(BitmapFrame.Create(image);jpgEncoder.Save(savedSnapshot);savedSnapshot.Flush();savedSnapshot.Close();savedSnapshot.Dispose();为了演示,上面的代码中在
12、当前目录创建了一个文件名。这是一种简单保存文件的方法。我们使用 FileStream 打开一个文件。JpegBitmapEncoder 对象将 UI 上的图像转换为一个标准的 JPEG 文件,保存完后,需要调用对象的 flush 方法,然后关闭,最后释放对象。虽然这三部不需要,因为我们使用了 using 语句,这里是为了演示,所以把这三步加上了。4. ColorImageStream 对象图到此为止,我们讨论了如何发现以及初始化 Kinect 传感器,从 Kinect 的影像摄像头获取图片。现在让我们来看看一些关键的类,以及他们之间的关系。下图展现了 ColorImageStream 的对象模
13、型图。ColorImageStream 是 KinectSensor 对象的一个属性,如同 KinectSensorde 其它流一样,色彩数据流在使用之前需要调用Enable 方法。ColorImageStream 有一个重载的 Enabled 方法,默认的 Eanbled 方法没有参数,重载的方法有一个ColorImageFormat 参数,他是一个枚举类型,可以使用这个参数指定图像格式。下表列出了枚举成员。默认的 Enabled 将ColorImageStream 设置为每秒 30 帧的 640*480 的 RGB 影像数据。一旦调用 Enabled 方法后,就可以通过对象的 Foramt
14、 属性获取到图像的格式了。ColorImageStream 有 5 个属性可以设置摄像头的视场。这些属性都以 Nominal 开头,当 Stream 被设置好后,这些值对应的分辨率就设置好了。一些应用程序可能需要基于摄像头的光学属性比如视场角和焦距的长度来进行计算。ColorImageStream 建议程序员使用这些属性,以使得程序能够面对将来分辨率的变化。ImageStream 是 ColorImageStream 的基类。因此 ColorImageStream 集成了 4 个描述每一帧每一个像素数据的属性。在之前的代码中,我们使用这些属性创建了一个 WriteableBitmap 对象。这
15、些属性与 ColorImageFormat 的设置有关。ImageStream中除了这些属性外还有一个 IsEnabled 属性和 Disable 方法。IsEnabled 属性是一个只读的。当 Stream 打开时返回 true,当调用了Disabled 方法后就返回 false 了。Disable 方法关闭 Stream 流,之后数据帧的产生就会停止,ColorFrameReady 事件的触发也会停止。当 ColorImageStream 设置为可用状态后,就能产生 ColorImageFrame 对象。 ColorImageFrame 对象很简单。他有一个Format 方法,他是父类的
16、ColorImageFormat 值。他只有一个 CopyPixelDataTo 方法,能够将图像的像素数据拷贝到指定的 byte数组中,只读的 PixelDataLength 属性定义了数组的大小 PixelDataLength 属性通过对象的宽度,高度以及每像素多少位属性来获得的。这些属性都继承自 ImageFrame 抽象类。数据流的格式决定了像素的格式,如果数据流是以 ColorImageFormat.RgbResolution640*480Fps30 格式初始化的,那么像素的格式就是 Bgr32,它表示每一个像素占 32 位(4 个字节),第一个字节表示蓝色通道值,第二个表示绿色,第
17、三个表示红色。第四个待用。当像素的格式是 Bgra32 时,第四个字节表示像素的 alpha 或者透明度值。如果一个图像的大小是 640*480,那么对于的字节数组有 122880 个字节 (width*height*BytesPerPixel=640*480*4).在处理影像时有时候也会用到 Stride 这一术语,他表示影像中一行的像素所占的字节数,可以通过图像的宽度乘以每一个像素所占字节数得到。除了描述像素数据的属性外,ColorImageFrame 对象还有一些列描述本身的属性。Stream 会为每一帧编一个号,这个号会随着时间顺序增长。应用程序不要假的每一帧的编号都比前一帧恰好大 1
18、,因为可能出现跳帧现象。另外一个描述帧的属性是Timestamp。他存储自 KinectSensor 开机(调用 Start 方法)以来经过的毫秒数。当每一次 KinectSensor 开始时都会复位为 0。5. 获取数据的方式:事件模式 VS “拉”模式目前为止我们都是使用 KinectSensor 对象的事件来获取数据的。事件在 WPF 中应用很广泛,在数据或者状态发生变化时,事件机制能够通知应用程序。对于大多数基于 Kinect 开发的应用程序来说基于事件的数据获取方式已经足够;但它不是唯一的能从数据流中获取数据的模式。应用程序能够手动的从 Kinect 数据流中获取到新的帧数据。“拉”
19、数据的方式就是应用程序会在某一时间询问数据源是否有新数据,如果有,就加载。每一个 Kinect 数据流都有一个称之为OpenNextFrame 的方法。当调用 OpenNextFrame 的方式时,应用程序可以给定一个超时的值,这个值就是应用程序愿意等待新数据返回的最长时间,以毫秒记。方法试图在超时之前获取到新的数据帧。如果超时,方法将会返回一个 null 值。当使用事件模型时,应用程序注册数据流的 frame-ready 事件,为其指定方法。每当事件触发时,注册方法将会调用事件的属性来获取数据帧。例如,在使用彩色数据流时,方法调用 ColorImageFrameReadyEventArgs
20、对象的 OpenColorImageFrame 方法来获取 ColorImageFrame 对象。程序应该测试获取的 ColorImageFrame 对象是否为空,因为有可能在某些情况下,虽然事件触发了,但是没有产生数据帧。除此之外,事件模型不需要其他的检查和异常处理。相比而言,OpenNextFrame 方法在 KinectSensor 没有运行、Stream 没有初始化或者在使用事件获取帧数据的时候都有可能会产生 InvalidOperationException 异常。应用程序可以自由选择何种数据获取模式,比如使用事件方式获取 ColorImageStream 产生的数据,同时采用“ 拉
21、”的方式从 SkeletonStream 流获取数据。但是不能对同一数据流使用这两种模式。AllFrameReady 事件包括了所有的数据流意味着如果应用程序注册了AllFrameReady 事件。任何试图以拉的方式获取流中的数据都会产生 InvalidOperationException 异常。在展示如何以拉的模式从数据流中获取数据之前,理解使用模式获取数据的场景很有必要。使用“拉” 数据的方式获取数据的最主要原因是性能,只在需要的时候采取获取数据。他的缺点是,实现起来比事件模式复杂。除了性能,应用程序的类型有时候也必须选择“拉” 数据的这种模式。SDK 也能用于 XNA,他不同与 WPF,
22、它不是事件驱动的。当需要使用 XNA 开发游戏时,必须使用拉模式来获取数据。使用 SDK 也能创建没有用户界面的控制台应用程序。设想开发一个使用 Kinect 作为眼睛的机器人应用程序,他通过源源不断的主动从数据流中读取数据然后输入到机器人中进行处理,在这个时候,拉模型是比较好的获取数据的方式。下面的代码展示了如何使用拉模式获取数据:private KinectSensor _Kinect;private WriteableBitmap _ColorImageBitmap;private Int32Rect _ColorImageBitmapRect;private int _ColorIma
23、geStride;private byte _ColorImagePixelData;public MainWindow()InitializeComponent();CompositionTarget.Rendering += CompositionTarget_Rendering;private void CompositionTarget_Rendering(object sender, EventArgs e)DiscoverKinectSensor();PollColorImageStream();代码声明部分和之前的一样。基于“拉”方式获取数据也需要发现和初始化 KinectSen
24、sor 对象。方法使用 WriteBitmap 来创建帧影像。最大的不同是,在构造函数中我们将 Rendering 事件绑定到 CompositionTarget 对象上。ComposationTarget 对象表示应用程序中可绘制的界面。Rendering 事件会在每一个渲染周期上触发。我们需要使用循环来取新的数据帧。有两种方式来创建循环。一种是使用线程,将在下一节中介绍。另一种方式是使用普通的循环语句。使用 CompositionTarget 对象有一个缺点,就是Rendering 事件中如果处理时间过长会导致 UI 线程问题。因为时间处理在主 UI 线程中。所以不应在事件中做一些比较耗时
25、的操作。Redering 事件中的代码需要做四件事情。必须发现一个连接的 KinectSnesor,初始化传感器。响应传感器状态的变化,以及拉取新的数据并对数据进行处理。我们将这四个任务分为两个方法。下面的代码列出了方法的实现。和之前的代码差别不大:private void DiscoverKinectSensor()if(this._Kinect != null if(this._Kinect = null)this._Kinect = KinectSensor.KinectSensors.FirstOrDefault(x = x.Status = KinectStatus.Connecte
26、d);if(this._Kinect != null)this._Kinect.ColorStream.Enable();this._Kinect.Start();ColorImageStream colorStream = this._Kinect.ColorStream;this._ColorImageBitmap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight, 96, 96, PixelFormats.Bgr32, null);this._ColorImageBitmapRect = new I
27、nt32Rect(0, 0, colorStream.FrameWidth, colorStream.FrameHeight);this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;this.ColorImageElement.Source = this._ColorImageBitmap;this._ColorImagePixelData = new bytecolorStream.FramePixelDataLength;下面的代码列出了 PollColorImageStream 方
28、法的实现。代码首先判断是否有 KinectSensor 可用.然后调用 OpneNextFrame方法获取新的彩色影像数据帧。代码获取新的数据后,然后更新 WriteBitmap 对象。这些操作包在 using 语句中,因为调用OpenNextFrame 对象可能会抛出异常。在调用 OpenNextFrame 方法时,将超时时间设置为了 100 毫秒。合适的超时时间设置能够使得程序在即使有一两帧数据跳过时仍能够保持流畅。我们要尽可能的让程序每秒产生 30 帧左右的数据。private void PollColorImageStream()if(this._Kinect = null)/TODO
29、: Display a message to plug-in a Kinect.elsetryusing(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame(100)if(frame != null) frame.CopyPixelDataTo(this._ColorImagePixelData);this._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride,
30、 0); catch(Exception ex)/TODO: Report an error message 总体而言,采用拉模式获取数据的性能应该好于事件模式。上面的例子展示了使用拉方式获取数据,但是它有另一个问题。使用CompositionTarget 对象,应用程序运行在 WPF 的 UI 线程中。任何长时间的数据处理或者在获取数据时超时 时间的设置不当都会使得程序变慢甚至无法响应用户的行为,因为这些操作都执行在 UI 线程上。解决方法是创建一个新的线程,然后在这个线程上执行数据获取和处理操作。 在.net 中使用 BackgroundWorker 类能够简单的解决这个问题。代码如下:p
31、rivate void Worker_DoWork(object sender, DoWorkEventArgs e)BackgroundWorker worker = sender as BackgroundWorker;if(worker != null)while(!worker.CancellationPending)DiscoverKinectSensor(); PollColorImageStream(); 首先,在变量声明中加入了一个 BackgroundWorker 变量 _Worker。在构造函数中,实例化了一个 BackgroundWorker 类,并注册了 DoWork
32、事件,启动了新的线程。当线程开始时就会触发 DoWork 事件。事件不断循环知道被取消。在循环体中,会调用DiscoverKinectSensor 和 PollColorImageStream 方法。如果直接使用之前例子中的这两个方法,你会发现会出现InvalidOperationException 异常,错误提示为“The calling thread cannot access this object because a different thread owns it”。这是由于,拉数据在 background 线程中,但是更新 UI 元素却在另外一个线程中。在 background 线
33、程中更新 UI 界面,需要使用 Dispatch 对象。WPF 中每一个 UI 元素都有一个 Dispathch 对象。下面是两个方法的更新版本:private void DiscoverKinectSensor()if(this._Kinect != null if(this._Kinect = null)this._Kinect = KinectSensor.KinectSensors.FirstOrDefault(x = x.Status = KinectStatus.Connected);if(this._Kinect != null)this._Kinect.ColorStream.
34、Enable();this._Kinect.Start();ColorImageStream colorStream = this._Kinect.ColorStream;this.ColorImageElement.Dispatcher.BeginInvoke(new Action() = this._ColorImageBitmap = new WriteableBitmap(colorStream.FrameWidth, colorStream.FrameHeight, 96, 96, PixelFormats.Bgr32, null);this._ColorImageBitmapRec
35、t = new Int32Rect(0, 0, colorStream.FrameWidth, colorStream.FrameHeight);this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel; this._ColorImagePixelData = new bytecolorStream.FramePixelDataLength;this.ColorImageElement.Source = this._ColorImageBitmap; );private void PollC
36、olorImageStream()if(this._Kinect = null)/TODO: Notify that there are no available sensors.elsetryusing(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame(100)if(frame != null) frame.CopyPixelDataTo(this._ColorImagePixelData);this.ColorImageElement.Dispatcher.BeginInvoke(new Action() = th
37、is._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride, 0););catch(Exception ex)/TODO: Report an error message 到此为止,我们展示了两种采用“拉”方式获取数据的例子,这两个例子都不够健壮。比如说还需要对资源进行清理,比如他们都没有释放 KinectSensor 对象,在构建基于 Kinect 的实际项目中这些都是需要处理的问题。“拉” 模式获取数据跟事件模式相比有很多独特的好
38、处,但它增加了代码量和程序的复杂度。在大多数情况下,事件模式获取数据的方法已经足够,我们应该使用该模式而不是“拉”模式。唯一不能使用事件模型获取数据的情况是在编写非 WPF 平台的应用程序的时候。比如,当编写 XNA 或者其他的采用拉模式架构的应用程序。建议在编写基于 WPF 平台的 Kinect 应用程序时采用事件模式来获取数据。只有在极端注重性能的情况下才考虑使用“拉”的方式。6. 结语本节介绍了采用 WriteableBitmap 改进程序的性能,并讨论了 ColorImageStream 中几个重要对象的对象模型图并讨论了个对象之间的相关关系。最后讨论了在开发基于 Kinect 应用程序时,获取 KinectSensor 数据的两种模式,并讨论了各自的优缺点和应用场合,这些对于之后的 DepthImageSteam 和 SkeletonStream 也是适用的。下一篇文章将会对 KinectSensor 特有的红外传感器产生的 DepthImageStream 进行介绍,敬请期待。