読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

OpenCvSharpで動画再生

OpenCvSharpネタの続きです。
今度はOpenCvSharpを使って、動画ファイルの再生をしてみます。

動画ファイルの再生手順

1.動画ファイルの読み込み

動画ファイルを再生する時にはCvCaptureというクラスを使います。
↓のように、CvCaputureクラスのコンストラクタに、ファイルのパスを渡してインスタンスを生成します。

var capture = new CvCapture([再生する動画ファイルのパス])
2.動画内のフレームを取得

動画の各フレームは、QueryFrameメソッドを呼ぶと取得できます。
このメソッドは動画の最後まで行くとnullを返すようになります。

var image = capture.QueryFrame();
3.WriteableBitmapへの変換

QueryFrame()メソッドではIplImage型の画像が取得できるので、これをWPFのウィンドウで表示できるように変換します。
変換処理はWriteableBitmapConverterクラスに色々と用意されています。
今回は以下の二つの方法を使ってみました。

  • ToWriteableBitmap拡張メソッド
    • static WriteableBitmap ToWriteableBitmap(this IplImage src);
    • IplImage型の拡張メソッドとして用意されている
  • WriteableBitmapConverterのstaticメソッド
    • static void ToWriteableBitmap(IplImage src, WriteableBitmap dst);
    • このメソッドでは、既存のWriteableBitmap上にIplImageの内容をコピーすることができます。

BackgroundWorkerを使ってシンプルに実装

とりあえず、BackgroundWorkerを使ってシンプルにやってみます。
OpenCV側の画像をWPF用の画像クラスに変換する処理は、IplImageに用意されているToWriteableBitmap拡張メソッドを使っています。
コードは以下の通り。

<Window x:Class="OpenCVSharpTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="525"
        Height="350" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Image x:Name="imgResult"
               Margin="10" />
    </Grid>
</Window>
    public partial class MainWindow : Window
    {
        private BackgroundWorker worker;

        public MainWindow()
        {
            InitializeComponent();

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

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            worker.RunWorkerAsync();
        }

        void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            var bw = sender as BackgroundWorker;

            using (var capture = new CvCapture(@"H:\Test\CIMG1672.MOV"))
            {
                int interval = (int)(1000 / capture.Fps);
                IplImage image;
                while((image = capture.QueryFrame()) != null)
                {
                    bw.ReportProgress(0, image);
                    Thread.Sleep(interval);
                }
            }
        }

        void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            var image = e.UserState as IplImage;
            imgResult.Source = image.ToWriteableBitmap();
        }
    }

とてもシンプルにできました。こんな風にウィンドウ上で動画の再生が行われます。
f:id:minami_SC:20141005015538j:plain:w300
が、タスクマネージャでメモリ消費を見てみると、
モリモリとメモリ使用量が増えていきます。
(ある程度増えたところでGCされてますが。。。)
毎フレームごとに、ToWriteableBitmap()メソッドで、新規WriteableBitmapインスタンスを生成しているためですね。

WriteableBitmapConverter.ToWriteableBitmapメソッドを使う

無駄なメモリ消費をしないように、一度作成したWriteableBitmapを使いまわすように修正します。

WriteableBitmapConverterのToWriteableBitmapメソッドを使うと、既存のWriteableBitmapに上書きすることができます。
引数にコピー元のIplImageとコピー先のWriteableBitmapを渡して、↓こんな風に使います。

WriteableBitmapConverter.ToWriteableBitmap(image, wb);

ちょっと手抜きですが、WriteableBitmapのメンバ変数を用意して、各フレームのコピーの際に使いまわすようにしてみました。

        private WriteableBitmap wb;
        void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            var image = e.UserState as IplImage;
            if (wb == null)
            {
                // キャプチャした画像のコピー先となるWriteableBitmapを作成
                wb = new WriteableBitmap(image.Width, image.Height, 96, 96, PixelFormats.Bgr24, null);
            }
            WriteableBitmapConverter.ToWriteableBitmap(image, wb);
            imgResult.Source = wb;
        }

こうすると、動画のフレームを更新していってもメモリ消費が増えなくなります。

async/awaitを使ってもう少しモダンに書いてみる

時代はasync/awaitでしょ!!
ってことで、BackgroundWorkerを使わずにasync/await構文を使って書いてみました。
とてもスッキリかけました。

// ToWriteableBitmap拡張メソッドを使うために↓を追加
using OpenCvSharp.Extensions;

namespace OpenCVSharpTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Window_Loaded(object sender, RoutedEventArgs e)
        {
            await CaptureAsync();
        }

        private WriteableBitmap wb;
        private async Task CaptureAsync()
        {
            using (var capture = new CvCapture(@"H:\Test\CIMG1672.MOV"))
            {
                // キャプチャした画像のコピー先となるWriteableBitmapを作成
                wb = new WriteableBitmap(capture.FrameWidth, capture.FrameHeight, 96, 96, PixelFormats.Bgr24, null);

                int interval = (int)(1000 / capture.Fps);
                IplImage image;
                while ((image = capture.QueryFrame()) != null)
                {
                    WriteableBitmapConverter.ToWriteableBitmap(image, wb);
                    imgResult.Source = wb;
                    await Task.Delay(interval);
                }
            }
        }
    }
}

適当にasync/awaitを使って書いてみましたが、よくよく見なおしてみると、UIスレッド上でフレームのキャプチャ処理とかすべてを行っています。
UIスレッドにあまり負荷をかけるのは良くないだろうということで、ちょっと修正してみました。
QueryFrameを呼び出してIplImageを取得する部分をAsyncなメソッドにしてます。
各フレームの画像に画像処理とかする場合は、この非同期メソッド中で行えば、UIスレッドに負荷をかけずに複雑な処理ができます。

        private WriteableBitmap wb;
        private async Task CaptureAsync()
        {
            using (var capture = new CvCapture(@"H:\Test\CIMG1672.MOV"))
            {
                // キャプチャした画像のコピー先となるWriteableBitmapを作成
                wb = new WriteableBitmap(capture.FrameWidth, capture.FrameHeight, 96, 96, PixelFormats.Bgr24, null);

                int interval = (int)(1000 / capture.Fps);
                while (true)
                {
                    // フレーム画像を非同期に取得
                    var image = await QueryFrameAsync(capture);
                    if (image == null) break;

                    WriteableBitmapConverter.ToWriteableBitmap(image, wb);
                    imgResult.Source = wb;
                    await Task.Delay(interval);
                }
            }
        }

        private async Task<IplImage> QueryFrameAsync(CvCapture capture)
        {
            // awaitできる形で、非同期にフレームの取得を行います。
            return await Task.Run(() =>
                {
                    return capture.QueryFrame();
                });
        }