• C#使用EmguCV实现视频读取和播放,及多个视频一起播放的问题


    大家知道WPF中多线程访问UI控件时会提示UI线程的数据不能直接被其他线程访问或者修改,该怎样来做呢?

    分下面两种情况

    1.WinForm程序

    1)第一种方法,使用委托:
    private delegate void SetTextCallback(string text);
            private void SetText(string text)
            {
                // InvokeRequired需要比较调用线程ID和创建线程ID
                // 如果它们不相同则返回true
                if (this.txt_Name.InvokeRequired)
                {
                    SetTextCallback d = new SetTextCallback(SetText);
                    this.Invoke(d, new object[] { text });
                }
                else
                {
                    this.txt_Name.Text = text;
                }
            }
    2)第二种方法,使用匿名委托
            private void SetText(Object obj)
            {
                if (this.InvokeRequired)
                {
                    this.Invoke(new MethodInvoker(delegate
                    {
                        this.txt_Name.Text = obj;
                    }));
                }
                else
                {
                    this.txt_Name.Text = obj;
                }
            }
    这里说一下BeginInvoke和Invoke和区别:BeginInvoke会立即返回,Invoke会等执行完后再返回。

    2.WPF程序

    1)可以使用Dispatcher线程模型来修改

    如果是窗体本身可使用类似如下的代码:

    this.lblState.Dispatcher.Invoke(new Action(delegate
    {
         this.lblState.Content = "状态:" + this._statusText;
    }));

    那么假如是在一个公共类中弹出一个窗口、播放声音等呢?这里我们可以使用:System.Windows.Application.Current.Dispatcher,如下所示

     System.Windows.Application.Current.Dispatcher.Invoke(new Action(() =>
     {
         if (path.EndsWith(".mp3") || path.EndsWith(".wma") || path.EndsWith(".wav"))
         {
            _player.Open(new Uri(path));
            _player.Play();
        }
     }));
     
    关键问题:多个视频同时播放,以上几种方法不足以解决,多个视频播放中主界面卡死和播放显示刷新不了的问题。
    目前笔者的解决方法是
     pinturebox.CreateGraphics().DrawImage(imgSrc.Bitmap, new System.Drawing.Rectangle(0, 0, pinturebox.Width, pinturebox.Height));

    EmguCV中的Capture类可以完成视频文件的读取,并捕捉每一帧,可以利用Capture类完成实现WinForm中视频检测跟踪环境的搭建。本文只实现最简陋的WinForm + EmguCV上的avi文件读取和播放框架,复杂的检测和跟踪算法在之后添加进去。

            这里使用WinForm实现视频的播放,主要是PictureBox类,它是支持基于事件的异步模式的典型组件,不使用EmguCV自带的UI控件等。

    图1.效果图

            直接在UI线程中完成视频的播放的话整个程序只有一个线程,由于程序只能同步执行,播放视频的时候UI将停止响应用户的输入,造成界面的假死。所以视频的播放需要实现异步模式。主要有三种方法:第一是使用异步委托;第二种是使用BackgroundWorker组件;最后一种就是使用多线程(不使用CheckForIllegalCrossThreadCalls =false的危险做法)。

            Windows窗体控件,唯一可以从创建它的线程之外的线程中调用的是Invoke()、BegionInvoke()、EndInvoke()方法和InvokeRequired属性。其中BegionInvoke()、EndInvoke()方法是Invoke()方法的异步版本。这些方法会切换到创建控件的线程上,以调用赋予一个委托参数的方法,该委托参数可以传递给这些方法。

            (一)   使用多线程
            首先定义监控的类及其对应的事件参数类和异常类:
            判断是否继续执行的布尔型成员会被调用线程改变,因此声名为volatile,不进行优化。

    /// <summary>
    /// 红外检测子。
    /// </summary>
    public class ThermalSurveillant
    {
        #region Private Fields
    
        /// <summary>
        /// 是否停止线程,此变量供多个线程访问。
        /// </summary>
        private volatile bool shouldStop = false;
    
        #endregion
        #region Public Properties
    
        #endregion
        #region Public Events
    
        /// <summary>
        /// 帧刷新事件。
        /// </summary>
        public EventHandler<FrameRefreshEventArgs> FrameRefresh;
    
        /// <summary>
        /// 播放完成。
        /// </summary>
        public EventHandler<CompletedEventArgs> Completed;
    
        #endregion
        #region Protected Methods
    
        /// <summary>
        /// 处理帧刷新事件。
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnFrameRefresh(FrameRefreshEventArgs e)
        {
            if (this.FrameRefresh != null)
            {
                this.FrameRefresh(this, e);
            }
        }
    
        /// <summary>
        /// 处理视频读完事件。
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnCompleted(CompletedEventArgs e)
        {
            if (this.Completed != null)
            {
                this.Completed(this, e);
            }
        }
    
        #endregion
        #region Public Methods
    
        /// <summary>
        /// 视频监控。
        /// </summary>
        /// <param name="capture">捕捉。</param>
        public void DoSurveillance(Object oCapture)
        {
            Capture capture = oCapture as Capture;
            int id = 1;
            if (capture == null)
            {
                throw new InvalidCaptureObjectException("传递的Capture类型无效。");
            }
            while (!shouldStop)
            {
                Image<Bgr, byte> frame = capture.QueryFrame();
                if (frame != null)
                {
                    FrameRefreshEventArgs e = new FrameRefreshEventArgs(frame.ToBitmap(), id++);
                    // 触发刷新事件
                    this.OnFrameRefresh(e);
                }
                else
                {
                    break;
                }
            }
            // 触发完成事件
            this.OnCompleted(new CompletedEventArgs(id));
        }
    
        /// <summary>
        /// 请求停止线程。
        /// </summary>
        public void Cancel()
        {
            this.shouldStop = true;
        }
    
        #endregion
    }
    

      

            UI线程中启动播放线程:

    声明:

    /// <summary>
    /// 监控线程。
    /// </summary>
    private Thread threadSurveillance = null;
    
    /// <summary>
    /// 捕获视频帧。
    /// </summary>
    private Capture captureSurveillance;
    
    /// <summary>
    /// 监控子。
    /// </summary>
    private ThermalSurveillant surveillant = new ThermalSurveillant();
     

    读入视频文件:

    captureSurveillance = new Capture(this.videoFilePath);
    captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, this.width);
    captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT, this.height);
    Image<Bgr, byte> frame = captureSurveillance.QueryFrame();
    this.pictureBox.Image = frame.ToBitmap();
     

      

    播放视频文件:

    // 启动播放线程
    threadSurveillance = new Thread(new ParameterizedThreadStart(surveillant.DoSurveillance));
    threadSurveillance.Name = “Surveillance”;
    threadSurveillance.IsBackground = true;
    threadSurveillance.Start(captureSurveillance);

      

            UI线程中响应监控类的事件:

    定义异步调用的委托:

    #region Delegate
    
    /// <summary>
    /// 为了调用UI线程的方法声明的委托。
    /// </summary>
    /// <param name=”frame”>要刷新的帧。</param>
    public delegate void FrameRefreshDelegate(Bitmap frame);
    
    /// <summary>
    /// 为了调用UI线程的方法声明的委托。
    /// </summary>
    /// <param name=”frame”>要刷新的帧。</param>
    /// <param name=”id”>帧序号。</param>
    public delegate void FrameStatusRefreshDelegate(Bitmap frame, int id);
    
    /// <summary>
    /// 为了调用UI线程的方法声明的委托。
    /// </summary>
    /// <param name=”message”>要输出的信息。</param>
    public delegate void CompletedDelegate(string message);
    
    #endregion

      

    添加事件委托:

    this.surveillant.FrameRefresh += OnRefreshFrame;
    this.surveillant.Completed += OnCompleted;
     

            以下方法中都是由监控线程中的事件委托方法,应该使用BeginInvoke方法,这样可以优雅的结束线程,如果使用Invoke方法,则调用方式为同步调用,此时如果使用Thread.Join()方法终止线程将引发死锁(正常播放没有问题),Thread.Join()方法的使用使调用线程阻塞等待当前线程完成,在这里即UI线程阻塞等待监控线程完成,而监控线程中又触发UI线程中pictureBox的刷新,使用Invoke方法就造成了监控线程等待UI线程刷新结果,而UI线程已经阻塞,形成了死锁。死锁时只能用Thread.Abort()方法才能结束线程。或者直接强制结束应用程序。

            使用BeginInvoke方法时为异步调用,监控线程不等待刷新结果直接继续执行,可以正常结束。结束后UI才进行刷新,不会造成死锁。

    图2.线程关系

    /// <summary>
    /// 刷新UI线程的pixtureBox的方法。
    /// </summary>
    /// <param name="frame">要刷新的帧。</param>
    private void RefreshFrame(Bitmap frame)
    {
        this.pictureBox.Image = frame;
        // 这里一定不能刷新!2012年8月2日1:50:16
        //this.pictureBox.Refresh();
    }
    
    
    /// <summary>
    /// 响应pictureBox刷新。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnRefreshFrame(object sender, FrameRefreshEventArgs e)
    {
        // 判断是否需要跨线程调用
        if (this.pictureBox.InvokeRequired == true)
        {
            FrameRefreshDelegate fresh = this.RefreshFrame;
            this.BeginInvoke(fresh, e.Frame);
        }
        else
        {
            this.RefreshFrame(e.Frame);
        }
    }
    
    
    /// <summary>
    /// 响应Label刷新信息。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnCompleted(object sender, CompletedEventArgs e)
    {
        // 判断是否需要跨线程调用
        CompletedDelegate fresh = this.RefreshStatus;
        string message = "视频结束,共 " + e.FrameCount + " 帧。";
        this.BeginInvoke(fresh, message);
    }
      
    
            关闭时需要中止播放线程之后再退出:
    
    /// <summary>
    /// 关闭窗体时发生。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnFormClosed(object sender, FormClosedEventArgs e)
    {
        // 检测子算法请求终止
        surveillant.Cancel();
    
        // 阻塞调用线程直到检测子线程终止
        if (threadSurveillance != null)
        {
            if (threadSurveillance.IsAlive == true)
            {
                threadSurveillance.Join();
            }
        }
    }

      

     

            (二)   使用异步委托

            创建线程的一个更简单的方法是定义一个委托,并异步调用它。委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。

    // asynchronous by using a delegate
    PlayVideoDelegate play = this.PlayVideoFile;
    IAsyncResult status = play.BeginInvoke(null, null);
    
    /// <summary>
    /// 播放视频文件。
    /// </summary>
    private void PlayVideoFile()
    {
        while (true)
        {
            Image<Bgr, byte> frame = capture.QueryFrame();
            if (frame != null)
            {
                Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
                grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
                RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
                try
                {
                    this.BeginInvoke(fresh, grayFrame.ToBitmap());
                }
                catch (ObjectDisposedException ex)
                {
                    Thread.CurrentThread.Abort();
                }
            }
            else
            {
                break;
            }
        }
    }
    
    /// <summary>
    /// 刷新UI线程的pixtureBox的方法。
    /// </summary>
    /// <param name="frame">要刷新的帧。</param>
    private void RefreshPictureBox(Bitmap frame)
    {
        this.pictureBox.Image = frame;
    }
    

      

            (三)   使用BackgroundWorker组件

            BackgroundWorker类是异步事件的一种实现方案,异步组件可以选择性的支持取消操作,并提供进度信息。RunWorkerAsync()方法启动异步调用。CancelAsync()方法取消。

    图3.BackgroundWorker组件

    /// <summary>

    /// 播放视频文件。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void detectItemPlay_Click(object sender, EventArgs e)
    {
        if (this.videoFilePath != null)
        {
            // run async
            this.backgroundWorker.RunWorkerAsync(capture);
        }
    }
    
    /// <summary>
    /// 异步调用。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnDoWork(object sender, DoWorkEventArgs e)
    {
        Emgu.CV.Capture capture = e.Argument as Emgu.CV.Capture;
        while (!e.Cancel)
        {
            Image<Bgr, byte> frame = capture.QueryFrame();
            if (frame != null)
            {
                Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
                grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
                if (this.backgroundWorker.CancellationPending == true)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    if (this.pictureBox.InvokeRequired == true)
                    {
                        RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
                        this.BeginInvoke(fresh, grayFrame.ToBitmap());
                    }
                    else
                    {
                        this.RefreshPictureBox(grayFrame.ToBitmap());
                    }
                }
            }
            else
            {
                break;
            }
        }
    }
    
    /// <summary>
    /// 关闭窗体时发生。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
        if (this.backgroundWorker.IsBusy)
        {
            this.backgroundWorker.CancelAsync();
        }
    }

    转自http://blog.csdn.net/azkabannull/article/details/7827673
     
  • 相关阅读:
    python定义函数的三种形式
    python函数的返回值
    python函数的调用
    python函数的定义
    python文件操作
    Python2和3字符编码的区别
    python的字符编码
    python异常处理
    python深浅拷贝
    python色彩缤纷的python(改变字体颜色以及样式)
  • 原文地址:https://www.cnblogs.com/Daywei/p/4936600.html
Copyright © 2020-2023  润新知