OpenCvSharpをつかう その7 (WindowsFormsで動画の再生)

OpenCVのhighguiではなくWindowsFormsのウィンドウ上で動画を再生したいということは多いようです。OpenCvSharpに限っても、以下のページなどで試みられています。

http://d.hatena.ne.jp/Guernsey/20081207/1228649067
http://blog.livedoor.jp/embed_life/archives/267250.html

C#GUIアプリケーションの開発の容易さや強力さが大きなウリだと思うので、当然のことです。しかしサンプルには入れてなかった気がするので、ここで様々な方法を試してみます。

前提

「Windows フォーム アプリケーション」でプロジェクトを新規作成し、Form1にPictureBoxを貼り付けます。この時点でのForm1.csのコードはこのようになります。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using OpenCvSharp;

namespace OpenCvSharpTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

シングルスレッドのループ

何も考えず、表示された瞬間からループを回していくコードです。

public partial class Form1 : Form
{
    private CvCapture capture;

    public Form1()
    {
        InitializeComponent();

        capture = new CvCapture("test.avi");
    }

    private void Form1_Shown(object sender, EventArgs e)
    {
        int interval = (int)(1000 / capture.Fps);
        IplImage image;
        while ((image = capture.QueryFrame()) != null)
        {
            pictureBox1.Image = image.ToBitmap();

            System.Threading.Thread.Sleep(interval);
            Application.DoEvents();
        }
    }
}

Application.DoEventsがミソです。これを入れないと固まります。

QueryFrameは動画の最後まで行くとnullを返すので、これを利用してループを停止させています。フレーム数をカウントしても良いと思います。

欠点としては、動画の再生以外何もできなくなることでしょうか。

Timerを使う

System.Windows.Forms.Timerクラスを使い、定期的にPictureBoxの画像を入れ替えます。

public partial class Form2 : Form
{
    private CvCapture capture;
    private Timer timer;

    public Form2()
    {
        InitializeComponent();

        capture = new CvCapture("test.avi");

        timer = new Timer();
        timer.Interval = (int)(1000 / capture.Fps);
        timer.Tick += new EventHandler(timer_Tick);            
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        timer.Start();
    }

    private void timer_Tick(object sender, EventArgs e)
    {
        IplImage image = capture.QueryFrame();
        if (image != null)
        {
            pictureBox1.Image = image.ToBitmap();
        }
        else
        {
            timer.Stop();
        }
    }
}

簡単な割に使い勝手も良く、悪くない方法です。処理が追いつかない際の挙動にやや不安材料がありますが、最近のPCならば普通は大丈夫でしょう。

BackgroundWorkerによるマルチスレッド

System.ComponentModel.BackgroundWorkerを用いて、動画からの毎フレームの画像の取得を別スレッドで行います。

WindowsFormsのコントロールは基本的にシングルスレッドで扱うようにできており、普通のThreadを用いる際はInvokeを使ったりしなければならずそのあたりがやや面倒になります。BackgroundWorkerはReportProgressを使うとそのあたりが楽に記述できます。

public partial class Form3 : Form
{
    private BackgroundWorker worker;

    public Form3()
    {
        InitializeComponent();

        worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        worker.DoWork += new DoWorkEventHandler(worker_DoWork);
        worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
    }

    private void Form3_Load(object sender, EventArgs e)
    {
        worker.RunWorkerAsync();
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bw = (BackgroundWorker)sender;
        using (CvCapture capture = new CvCapture("test.avi"))
        {
            int interval = (int)(1000 / capture.Fps);
            IplImage image;
            while ( (image = capture.QueryFrame()) != null )
            {
                bw.ReportProgress(0, image);
                System.Threading.Thread.Sleep(interval);
            }
        }
    }

    private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        IplImage image = (IplImage)e.UserState;
        pictureBox1.Image = image.ToBitmap();
    }
}


他にもまだ方法はあると思いますが、この辺で。



また、OpenCvSharp.UserInterface.PictureBoxIplを使うと、画像の更新処理は以下のように書けます。

pictureBoxIpl1.RefreshIplImage(image);

この方法を使うと、毎フレームごとのBitmapオブジェクトの再生成を行わなくなるので、若干効率が良いと思います。


OpenCVで始める簡単動画プログラミング

OpenCVで始める簡単動画プログラミング