OpenCV(Emgu)入门系列(9):在C#中,使用Emgu+PictureBox实现一个简易的视频播放器

有了之前使用Emgu读取图片并显示在C#的PictureBox中的实践,今天使用相同的思路实现一个视频播放器。

任务

使用C#与Emgu实现一个简单的视频播放器,有以下功能:

可播放avi,rmvb等格式视频

有暂停、继续、停止、上一帧、下一帧等功能

有刻度条显示播放进度,并且可通过拖动刻度条来改来视频进度

要求:

VS 2012 RC,代码及库全部采用64位

使用C#的PictureBox,而不是Emgu的ImageBox :我想实际体验一下PictureBox与ImageBox之间的性能差距

采用System.Windows.Forms.Timer取帧:据说当间隔小于100ms时Timer会很不准确。在实际运行时,发现打开一个正常的视频文件(帧频为30/s)时,播放很慢,像在慢放一样,可证明的确有这个问题。不过我这么做是为了熟悉C#的API,下一次将换用更高效的方法。

使用自定义事件来控制按钮与刻度条的状态(如文字)

效果图

OpenCV(Emgu)入门系列(9):在C#中,使用Emgu+PictureBox实现一个简易的视频播放器_第1张图片

布局

布局还是比较简单的,主要是要用好以下几点:

AutoSize

Dock

Anchor

MinimumSize

就可以设计出一个窗口可随视频大小自动变化,按钮位置不会错位的布局出来。

Emgu相关API

使用Emgu封装好的方法读取视频文件,及获取相关信息:

// 读取视频文件(可以为AVI,rmvb等,只要系统安装了解码器)IntPtr capture = CvInvoke.cvCreateFileCapture(file);// 得到总帧数CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_COUNT);// 视频宽度CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_WIDTH);// 视频高度CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT);// 当前帧位置CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES);// 帧频CvInvoke.cvGetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FPS);

使用以下方法,将Emgu取得的图片变为PictureBox认识的Bitmap:

// 读取下一帧var frame = CvInvoke.cvQueryFrame(capture);// 生成一个新的Image容器Image dest =newImage(movieInfo.width, movieInfo.height);// 把数据复制过去CvInvoke.cvCopy(frame, dest, IntPtr.Zero);// 转换为Bitmapdest.ToBitmap();

如果想让视频定位到某一帧,使用以下方法:

intnewPos = 23;CvInvoke.cvSetCaptureProperty(capture, Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_POS_FRAMES, newPos);

Emgu的代码就是以上这些,其它的都是C#代码了。

视频信息

通过定义一个叫MovieInfo的struct,来保存与视频相关的信息供使用:

structMovieInfo {publicString filename;publicintframeCount;publicintwidth;publicintheight;publicintcurrentFrame;publicintfps;}

Timer

创建Timer,根据帧频设好Interval,以及Tick对应的操作。如下:

Timer myTimer =newTimer();myTimer.Interval = 1000 / Convert.ToInt32(movieInfo.fps);myTimer.Tick +=newEventHandler(MyTimer_Tick);myTimer.Start();

在MyTimer_Tick方法中读取下一帧画面,显示在PictureBox中

事件

各按钮与刻度条的状态,与视频的播放状态,是互相对应的。比如只有打开了一个视频文件,“开始/停止/前一帧/后一帧”按钮才可用,并且刻度条上的指针也随着播放向右移动;同时拖动刻度指针,视频也会随之变化。点击“前一帧/后一帧”时,画面也会跳动。它们之间是通过自定义的事件来协调的。

关于事件,可参考这篇文件:C#事件(event)解析

首先自定义了一个MovieEvent,它有多种不同的状态,用以区分不同的事件:

classMovieEvent : EventArgs {publicState EventState { get; set; }publicenumState {        NewMovie, Started, Stopped, Paused, Playing, Scroll, PrevNext    }publicMovieEvent(State state) :base() {this.EventState = state;    }}

然后在Form1中定义了一个delegate和一个event handler:

delegatevoidMovieHandler(objectsender, MovieEvent e);eventMovieHandler MovieHandlers;

再写一个handler方法,用于捕获事件并处理。这里写的有点乱,不知道有没有更好的做法:

voidhandler(objectsender, MovieEvent e) {switch(e.EventState) {caseMovieEvent.State.NewMovie:            btnStart.Enabled =true;            btnStop.Enabled =true;            btnPrev.Enabled =true;            btnNext.Enabled =true;            btnStart.Text ="暂停";break;caseMovieEvent.State.Paused:            btnStart.Enabled =true;            btnStop.Enabled =true;            btnPrev.Enabled =true;            btnNext.Enabled =true;            btnStart.Text ="开始";break;// 更多}

在Form1的构造函数中,把handler注册上去:

this.MovieHandlers +=newMovieHandler(this.handler);

然后就是在程序的不同地方,执行了不同操作时,创建事件并散播出去:

MovieHandlers(this,newMovieEvent(MovieEvent.State.Stopped));MovieHandlers(this,newMovieEvent(MovieEvent.State.Playing));

项目源代码

放在了github上:https://github.com/freewind/opencv_emgu_learning/tree/master/AviPlayer_PictureBox

这是一个VS 2012 RC的项目,应该也可以在低版本运行。如果要运行,需要手动安装OpenCV/Emgu等,具体做法可参考我之前的文章。

改进

下一步将对该项目进行如下改进:

使用Emgu的ImageBox,测一下两者的性能差距

使用其它方式代替PictureBox

寻找进一步优化代码的方法

后记

(2012-08-09)

该程序有一个严重的问题:无法按帧频精准的播放视频。比如当帧频为30时,timer的间隔时间为1000/30=33,而准确的应该是33.3333,这样实际上就慢了一点。另外,如果视频比较大,解析一幅画面比较费时的时候,Timer就更加不准确了,播放起来像在慢放。

这应该是一个难以解决的问题,精准的控制时间很难做到,参见How to use c# to write a simple video player which plays the video with accurate fps?

由于我的目的是通过这个例子来学习OpenCV,所以就不往下钻了,做到现在的程度已经达到了目的。

你可能感兴趣的:(OpenCV(Emgu)入门系列(9):在C#中,使用Emgu+PictureBox实现一个简易的视频播放器)