zoukankan      html  css  js  c++  java
  • Kinect开发 —— 基础知识

    转自:http://www.cnblogs.com/yangecnu/archive/2012/04/02/KinectSDK_Application_Fundamentals_Part2.html


    1,性能改进

    如果使用Bitmap对象,对于每一个彩色图像帧,都会创建一个新的Bitmap对象。由于Kinect视频摄像头默认采集频率为每秒30幅,所以应用程序每秒会创建30个bitmap对象,产生30次的Bitmap内存创建,对象初始化,填充像素数据等操作。这些对象很快就会变成垃圾等待垃圾回收器进行回收。对数据量小的程序来说可能影响不是很明显,但当数据量很大时,其缺点就会显现出来。

        改进方法是使用WriteableBitmap对象。它位于System.Windows.Media.Imaging命名空间下面,该对象被用来处理需要频繁更新的像素数据。当创建WriteableBitmap时,应用程序需要指定它的高度,宽度以及格式,以使得能够一次性为WriteableBitmap创建好内存,以后只需根据需要更新像素即可。

    private WriteableBitmap colorImageBitmap;
    private Int32Rect colorImageBitmapRect;
    private int colorImageStride;
    private byte[] colorImagePixelData;
    
    if (kinectSensor != null)
    {   
        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);
        this.colorImageStride = colorStream.FrameWidth * colorStream.FrameBytesPerPixel;
        ColorImageElement.Source = this.colorImageBitMap;
    
        kinectSensor.ColorFrameReady += kinectSensor_ColorFrameReady;
        kinectSensor.Start();
    }
    private void Kinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
    {
       using (ColorImageFrame frame = e.OpenColorImageFrame())
      {
         if (frame != null)
         {
            byte[] pixelData = new byte[frame.PixelDataLength];
            frame.CopyPixelDataTo(pixelData);
            this.colorImageBitmap.WritePixels(this.colorImageBitmapRect, pixelData, this.colorImageStride, 0);
         }
       }
    }

    基于Kinect的应用程序在无论是在显示ColorImageStream数据还是显示DepthImageStream数据的时候,都应该使用WriteableBitmap对象来显示帧影像。在最好的情况下,彩色数据流会每秒产生30帧彩色影像,这意味着对内存资源的消耗比较大。WriteableBitmap能够减少这种内存消耗,减少需要更新影响带来的内存开辟和回收操作。毕竟在应用中显示帧数据不是应用程序的最主要功能,所以在这方面减少内像存消耗显得很有必要。


    2,简单的图像处理

    每一帧ColorImageFrame都是以字节序列的方式返回原始的像素数据。应用程序必须以这些数据创建图像。这意味这我们可以对这些原始数据进行一定的处理,然后再展示出来。

    void kinectSensor_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
    {
        using (ColorImageFrame frame = e.OpenColorImageFrame())
        {
            if (frame != null)
            {
                byte[] pixelData = new byte[frame.PixelDataLength];
                frame.CopyPixelDataTo(pixelData);
                for (int i = 0; i < pixelData.Length; i += frame.BytesPerPixel)
                {
                   pixelData[i] = 0x00;//蓝色
                   pixelData[i + 1] = 0x00;//绿色
                 }
               this.colorImageBitMap.WritePixels(this.colorImageBitmapRect, pixelData,this.colorImageStride,0);
            }
        }
    }

    for循环遍历每个像素,使得i的起始位置重视该像素的第一个字节。由于数据的格式是Bgr32,即RGB32位(一个像素共占4个字节,每个字节8位),所以第一个字节是蓝色通道,第二个是绿色,第三个是红色。循环体类,将第一个和第二个通道设置为0.所以输出的代码中只用红色通道的信息。这类操作通常很消耗计算资源。像素着色通常是GPU上的一些很基础的操作。

    Inverted Color 反色

    pixelData[i]=(byte)~pixelData[i];

    pixelData[i+1]=(byte)~pixelData[i+1];

    pixelData[i+2]=(byte)~pixelData[i+2];

    Apocalyptic Zombie

    pixelData[i]= pixelData[i+1];

    pixelData[i+1]= pixelData[i];

    pixelData[i+2]=(byte)~pixelData[i+2];

    Gray scale

    byte gray=Math.Max(pixelData[i],pixelData[i+1])

    gray=Math.Max(gray,pixelData[i+2]);

    pixelData[i]=gray;

    pixelData[i+1]=gray;

    pixelData[i+2]=gray;

    Grainy black and white movie

    byte gray=Math.Min(pixelData[i],pixelData[i+1]);

    gray=Math.Min(gray,pixelData[i+2]);

    pixelData[i]=gray;

    pixelData[i+1]=gray;

    pixelData[i+2] =gray;

    Washed out color

    double gray=(pixelData[i]*0.11)+(pixelData[i+1]*0.59)+(pixelData[i+2]*0.3);

    double desaturation=0.75;

    pixelData[i]=(byte)(pixelData[i]+desaturation*(gray-pixelData[i]));

    pixelData[i+1]=(byte)(pixelData[i+1]+desaturation*(gray-pixelData[i+1]));

    pixelData[i+2]=(byte)(pixelData[i+2]+desatuation*(gray-pixelData[i+2]));

    High saturation

    If (pixelData[i]<0x33||pixelData[i]>0xE5)
    
    {
    
    pixelData[i]=0x00;
    
    } else
    
    {
    
      pixelData[i]=0Xff;
    
    }
    
    If (pixelData[i+1]<0x33||pixelData[i+1]>0xE5)
    
    {
    
      pixelData[i+1]=0x00;
    
    } else
    
    {
    
      pixelData[i+1]=0Xff;
    
    }
    
    If (pixelData[i+2]<0x33||pixelData[i+2]>0xE5)
    
    {
    
      pixelData[i+2]=0x00;
    
    } else
    
    {
    
      pixelData[i+1]=0Xff;
    
    }

    3,ColorImageStream 对象图

    ColorImageStream是KinectSensor对象的一个属性,如同KinectSensorde其它流一样,色彩数据流在使用之前需要调用Enable方法。ColorImageStream有一个重载的Enabled方法,默认的Eanbled方法没有参数,重载的方法有一个ColorImageFormat参数,他是一个枚举类型,可以使用这个参数指定图像格式。下表列出了枚举成员。默认的Enabled将ColorImageStream设置为每秒30帧的640*480的RGB影像数据。一旦调用Enabled方法后,就可以通过对象的Foramt属性获取到图像的格式了。

    image

    ColorImageStream 有5个属性可以设置摄像头的视场。这些属性都以Nominal开头,当Stream被设置好后,这些值对应的分辨率就设置好了。一些应用程序可能需要基于摄像头的光学属性比如视场角和焦距的长度来进行计算。ColorImageStream建议程序员使用这些属性,以使得程序能够面对将来分辨率的变化。

    ImageStream是ColorImageStream的基类。因此ColorImageStream集成了4个描述每一帧每一个像素数据的属性。在之前的代码中,我们使用这些属性创建了一个WriteableBitmap对象。这些属性与ColorImageFormat的设置有关。ImageStream中除了这些属性外还有一个IsEnabled属性和Disable方法。IsEnabled属性是一个只读的。当Stream打开时返回true,当调用了Disabled方法后就返回false了。Disable方法关闭Stream流,之后数据帧的产生就会停止,ColorFrameReady事件的触发也会停止。当ColorImageStream设置为可用状态后,就能产生ColorImageFrame对象。ColorImageFrame对象很简单。他有一个Format方法,他是父类的ColorImageFormat值。他只有一个CopyPixelDataTo方法,能够将图像的像素数据拷贝到指定的byte数组中,只读的PixelDataLength属性定义了数组的大小PixelDataLength属性通过对象的宽度,高度以及每像素多少位属性来获得的。这些属性都继承自ImageFrame抽象类。

    数据流的格式决定了像素的格式,如果数据流是以ColorImageFormat.RgbResolution640*480Fps30格式初始化的,那么像素的格式就是Bgr32,它表示每一个像素占32位(4个字节),第一个字节表示蓝色通道值,第二个表示绿色,第三个表示红色。第四个待用。当像素的格式是Bgra32时,第四个字节表示像素的alpha或者透明度值。如果一个图像的大小是640*480,那么对于的字节数组有122880个字节(width*height*BytesPerPixel=640*480*4).在处理影像时有时候也会用到Stride这一术语,他表示影像中一行的像素所占的字节数,可以通过图像的宽度乘以每一个像素所占字节数得到。

    除了描述像素数据的属性外,ColorImageFrame对象还有一些列描述本身的属性。Stream会为每一帧编一个号,这个号会随着时间顺序增长。应用程序不要假的每一帧的编号都比前一帧恰好大1,因为可能出现跳帧现象。另外一个描述帧的属性是Timestamp。他存储自KinectSensor开机(调用Start方法)以来经过的毫秒数。当每一次KinectSensor开始时都会复位为0。


    4,获取数据的方式: 事件 VS “拉”

    事件在WPF中应用很广泛,在数据或者状态发生变化时,事件机制能够通知应用程序。对于大多数基于Kinect开发的应用程序来说基于事件的数据获取方式已经足够

    当使用事件模型时,应用程序注册数据流的frame-ready事件,为其指定方法。每当事件触发时,注册方法将会调用事件的属性来获取数据帧。例如,在使用彩色数据流时,方法调用ColorImageFrameReadyEventArgs对象的OpenColorImageFrame方法来获取ColorImageFrame对象。程序应该测试获取的ColorImageFrame对象是否为空,因为有可能在某些情况下,虽然事件触发了,但是没有产生数据帧。除此之外,事件模型不需要其他的检查和异常处理

    “拉”数据的方式就是应用程序会在某一时间询问数据源是否有新数据,如果有,就加载。每一个Kinect数据流都有一个称之为OpenNextFrame的方法。当调用OpenNextFrame的方式时,应用程序可以给定一个超时的值,这个值就是应用程序愿意等待新数据返回的最长时间,以毫秒记。方法试图在超时之前获取到新的数据帧。如果超时,方法将会返回一个null值。

    OpenNextFrame方法在KinectSensor没有运行、Stream没有初始化或者在使用事件获取帧数据的时候都有可能会产生InvalidOperationException异常。应用程序可以自由选择何种数据获取模式,比如使用事件方式获取ColorImageStream产生的数据,同时采用“拉”的方式从SkeletonStream流获取数据。但是不能对同一数据流使用这两种模式。AllFrameReady事件包括了所有的数据流—意味着如果应用程序注册了AllFrameReady事件。任何试图以拉的方式获取流中的数据都会产生InvalidOperationException异常。

    在展示如何以拉的模式从数据流中获取数据之前,理解使用模式获取数据的场景很有必要。使用“拉”数据的方式获取数据的最主要原因是性能,只在需要的时候采取获取数据。他的缺点是,实现起来比事件模式复杂。除了性能,应用程序的类型有时候也必须选择“拉”数据的这种模式。SDK也能用于XNA,他不同与WPF,它不是事件驱动的。当需要使用XNA开发游戏时,必须使用拉模式来获取数据。使用SDK也能创建没有用户界面的控制台应用程序。设想开发一个使用Kinect作为眼睛的机器人应用程序,他通过源源不断的主动从数据流中读取数据然后输入到机器人中进行处理,在这个时候,拉模型是比较好的获取数据的方式。

    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();
    }

    在构造函数中我们将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方法获取新的彩色影像数据帧。代码获取新的数据后,然后更新WriteBitmap对象。这些操作包在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类能够简单的解决这个问题。代码如下:

    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
            }   
        }
    }

    “拉”模式获取数据跟事件模式相比有很多独特的好处,但它增加了代码量和程序的复杂度。在大多数情况下,事件模式获取数据的方法已经足够,我们应该使用该模式而不是“拉”模式。唯一不能使用事件模型获取数据的情况是在编写非WPF平台的应用程序的时候。比如,当编写XNA或者其他的采用拉模式架构的应用程序。建议在编写基于WPF平台的Kinect应用程序时采用事件模式来获取数据。只有在极端注重性能的情况下才考虑使用“拉”的方式。

    完整代码:

    namespace TestBasic_Poll
    {
        /// <summary>
        /// MainWindow.xaml 的交互逻辑
        /// </summary>
        public partial class MainWindow : Window
        {
            private KinectSensor _kinect;
            private WriteableBitmap _colorImageBitmap;
            private Int32Rect _colorImageBitmapRect;
            private int _colorImageStride;
            private byte[] _colorImagePixelData;
            private BackgroundWorker bWorker;
    
            public MainWindow()
            {
                InitializeComponent();
                bWorker = new BackgroundWorker();
                bWorker.DoWork += new DoWorkEventHandler(Worker_DoWork);
                bWorker.RunWorkerAsync(this);
            }
    
            private void Worker_DoWork(Object sender, DoWorkEventArgs e)
            {
                BackgroundWorker worker = sender as BackgroundWorker;
                if (worker!=null)
                {
                    while (!worker.CancellationPending)
                    {
                        DiscoverKinectSensor();
                        PollColorImageStream();
                    }
                }
            }
    
            private void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
            {
                if (null != this._kinect)
                {
                    this._kinect.Stop();
                }
            }
    
            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)
                {
                }
                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 (System.Exception ex)
                    {
                        
                    }
                }
            }
    
    
        }
    }
     
  • 相关阅读:
    Oracle中有大量的sniped会话
    Error 1130: Host '127.0.0.1' is not allowed to connect to this MySQL server
    汉字转换为拼音以及缩写(javascript)
    高效率随机删除数据(不重复)
    vs2010 舒服背景 优雅字体 配置
    mvc中的ViewData用到webfrom中去
    jquery ajax return值 没有返回 的解决方法
    zShowBox (图片放大展示jquery版 兼容性好)
    动感效果的TAB选项卡 jquery 插件
    loading 加载提示······
  • 原文地址:https://www.cnblogs.com/sprint1989/p/3832145.html
Copyright © 2011-2022 走看看