2.4 获取数据的方式:事件模式VS拉(Polling)模式
目前为止我们都是使用KinectSensor对象的事件来获取数据的。事件在WPF中应用很广泛,在数据或者状态发生变化时,事件机制能够通知应用程序。对于大多数基于Kinect开发的应用程序来说基于事件的数据获取方式已经足够;但它不是唯一的能从数据流中获取数据的模式。应用程序能够手动地从Kinect数据流中获取到新的帧数据。
“拉”数据的方式就是应用程序会在某一时间询问数据源是否有新数据,如果有,就加载。每一个Kinect数据流都有一个称之为OpenNextFrame的方法。当调用OpenNextFrame的方式时,应用程序可以给定一个超时的值,这个值就是应用程序愿意等待新数据返回的最长时间,以毫秒记。方法试图在超时之前获取到新的数据帧。如果超时,方法将会返回一个null值。
当使用事件模型时,应用程序注册数据流的frame-ready事件,为其指定方法。每当事件触发时,注册方法将会调用事件的属性来获取数据帧。例如,在使用彩色数据流时,方法调用ColorImageFrameReadyEventArgs对象的OpenColorImageFrame方法来获取ColorImageFrame对象。程序应该测试获取的ColorImageFrame对象是否为空,因为有可能在某些情况下,虽然事件触发了,但是没有产生数据帧。除此之外,事件模型不需要其他的检查和异常处理。
相比而言,OpenNextFrame方法在KinectSensor没有运行、Stream没有初始化或者在使用事件获取帧数据的时候都有可能会产生InvalidOperationException异常。应用程序可以自由选择何种数据获取模式,比如使用事件方式获取ColorImageStream产生的数据,同时采用“拉”的方式从SkeletonStream流获取数据。但是不能对同一数据流使用这两种模式。AllFrameReady事件包括了所有的数据流——这意味着如果应用程序注册了AllFrameReady事件,任何试图以拉的方式获取流中的数据都会产生InvalidOperationException异常。
在展示如何以拉的模式从数据流中获取数据之前,理解使用模式获取数据的场景很有必要。使用“拉”数据的方式获取数据的最主要原因是性能,只在需要的时候采取获取数据。它的缺点是,实现起来比事件模式复杂。除了性能,应用程序的类型有时候也必须选择“拉”数据的这种模式。SDK也能用于XNA,它不同于WPF,它不是事件驱动的。当需要使用XNA开发游戏时,必须使用拉模式来获取数据。使用SDK也能创建没有用户界面的控制台应用程序。设想开发一个使用Kinect作为眼睛的机器人应用程序,它通过源源不断的主动从数据流中读取数据然后输入到机器人中进行处理,在这个时候,拉模型是比较好的获取数据的方式。
下面的代码展示了如何使用拉模式获取数据:
public partial class MainWindow : Window { private KinectSensor _Kinect; private WriteableBitmap _ColorImageBitmap; private Int32Rect _ColorImageBitmapRect; private int _ColorImageStride; private byte[] _ColorImagePixelData; public MainWindow() { InitializeComponent(); CompositionTarget.Rendering += CompositionTarget_Rendering; } private void CompositionTarget_Rendering(object sender, EventArgs e) { DiscoverKinectSensor(); PollColorImageStream(); } }
代码声明部分和之前的一样。基于“拉”方式获取数据也需要发现和初始化KinectSensor对象。方法使用WriteableBitmap来创建帧影像。最大的不同是,在构造函数中我们将Rendering事件绑定到CompositionTarget对象上。ComposationTarget对象表示应用程序中可绘制的界面。Rendering事件会在每一个渲染周期上触发。我们需要使用循环来取新的数据帧。有两种方式来创建循环。一种是使用线程,将在下一个代码中介绍。另一种方式是使用普通的循环语句。使用CompositionTarget对象有一个缺点,就是Rendering事件中如果处理时间过长会导致UI线程问题。因为时间处理在主UI线程中。所以不应在事件中做一些比较耗时的操作。Redering事件中的代码需要做四件事情。必须发现一个连接的KinectSnesor,初始化传感器,响应传感器状态的变化,以及拉取新的数据并对数据进行处理。我们将这四个任务分为两个方法。下面的代码列出了方法的实现。和之前的代码差别不大:
private void DiscoverKinectSensor() { if(this._Kinect != null && this._Kinect.Status != KinectStatus.Connected) { this._Kinect = null; } if(this._Kinect == null) { this._Kinect = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected); 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 Int32Rect(0, 0, colorStream.FrameWidth, colorStream.FrameHeight); this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel; this.ColorImageElement.Source = this._ColorImageBitmap; this._ColorImagePixelData = new byte[colorStream.FramePixelDataLength]; } } }
下面的代码列出了PollColorImageStream方法的实现。代码首先判断是否有KinectSensor可用,然后调用OpneNextFrame方法获取新的彩色影像数据帧。代码获取新的数据后,然后更新WriteableBitmap对象。这些操作包在using语句中,因为调用OpenNextFrame对象可能会抛出异常。在调用OpenNextFrame方法时,将超时时间设置为了100毫秒。合适的超时时间设置能够使得程序在即使有一两帧数据跳过时仍能够保持流畅。我们要尽可能的让程序每秒产生30帧左右的数据。
private void PollColorImageStream() { if(this._Kinect == null) { //TODO: Display a message to plug-in a Kinect. } else { try { using(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame(100)) { if(frame != null) { frame.CopyPixelDataTo(this._ColorImagePixelData); this._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride, 0); } } } catch(Exception ex) { //TODO: Report an error message } } } }
总体而言,采用拉模式获取数据的性能应该好于事件模式。上面的例子展示了使用拉方式获取数据,但是它有另一个问题。使用CompositionTarget对象,应用程序运行在WPF的UI线程中。任何长时间的数据处理或者在获取数据时超时时间的设置不当都会使得程序变慢甚至无法响应用户的行为,因为这些操作都执行在UI线程上。解决方法是创建一个新的线程,然后在这个线程上执行数据获取和处理操作。
在.net中使用BackgroundWorker类能够简单的解决这个问题,要使用BackgroundWorker类,需要在MainWindow.xaml.cs中添加System.ComponentModel命名空间。代码如下:
public partial class MainWindow : Window { private KinectSensor _Kinect; private WriteableBitmap _ColorImageBitmap; private Int32Rect _ColorImageBitmapRect; private int _ColorImageStride; private byte[] _ColorImagePixelData; private BackgroundWorker _Worker; public MainWindow() { InitializeComponent(); this._Worker = new BackgroundWorker(); this._Worker.DoWork += Worker_DoWork; this._Worker.RunWorkerAsync(); this.Unloaded += (s, e) => { this._Worker.CancelAsync(); }; } private void Worker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; if(worker != null) { while(!worker.CancellationPending) { DiscoverKinectSensor(); PollColorImageStream(); } } } }
首先,在变量声明中加入了一个BackgroundWorker变量_Worker。在构造函数中,实例化了一个BackgroundWorker类,并注册了DoWork事件,启动了新的线程。
当线程开始时就会触发DoWork事件。事件不断循环直到被取消。在循环体中,会调用DiscoverKinectSensor和PollColorImageStream方法。如果直接使用之前例子中的这两个方法,你会发现会出现InvalidOperationException异常,错误提示为“The calling thread cannot access this object because a different thread owns it”。
这是由于拉数据在background线程中,但是更新UI元素却在另外一个线程中。在background线程中更新UI界面,需要使用Dispatch对象。WPF中每一个UI元素都有一个Dispathch对象。下面是两个方法的更新版本:
private void DiscoverKinectSensor() { if(this._Kinect != null && this._Kinect.Status != KinectStatus.Connected) { this._Kinect = null; } if(this._Kinect == null) { this._Kinect = KinectSensor.KinectSensors .FirstOrDefault(x => x.Status == KinectStatus.Connected); if(this._Kinect != null) { this._Kinect.ColorStream.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._ColorImageBitmapRect = new Int32Rect(0, 0, colorStream.FrameWidth, colorStream.FrameHeight); this._ColorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel; this._ColorImagePixelData = new byte[colorStream.FramePixelDataLength]; this.ColorImageElement.Source = this._ColorImageBitmap; })); } } } private void PollColorImageStream() { if(this._Kinect == null) { //TODO: Notify that there are no available sensors. } else { try { using(ColorImageFrame frame = this._Kinect.ColorStream.OpenNextFrame(100)) { if(frame != null) { frame.CopyPixelDataTo(this._ColorImagePixelData); this.ColorImageElement.Dispatcher.BeginInvoke(new Action(() => { this._ColorImageBitmap.WritePixels(this._ColorImageBitmapRect, this._ColorImagePixelData, this._ColorImageStride, 0); })); } } } catch(Exception ex) { //TODO: Report an error message } } }
到此为止,我们展示了两种采用“拉”方式获取数据的例子,这两个例子都不够健壮。比如说还需要对资源进行清理,比如他们都没有释放KinectSensor对象,在构建基于Kinect的实际项目中这些都是需要处理的问题。
“拉”模式获取数据跟事件模式相比有很多独特的好处,但它增加了代码量和程序的复杂度。在大多数情况下,事件模式获取数据的方法已经足够,我们应该使用该模式而不是“拉”模式。唯一不能使用事件模型获取数据的情况是在编写非WPF平台的应用程序的时候。比如,当编写XNA或者其他的采用拉模式架构的应用程序。建议在编写基于WPF平台的Kinect应用程序时采用事件模式来获取数据。只有在极端注重性能的情况下才考虑使用“拉”的方式。
结语
本章介绍了采用WriteableBitmap改进程序的性能,并讨论了ColorImageStream中几个重要对象的对象模型图并讨论了个对象之间的相关关系。
最后讨论了在开发基于Kinect应用程序时,获取KinectSensor数据的两种模式,并讨论了各自的优缺点和应用场合,这些知识对于之后的DepthImageSteam和SkeletonStream也是适用的。
文件下载(已下载 1746 次)发布时间:2013/2/15 下午8:56:25 阅读次数:5468