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(); } }
とてもシンプルにできました。こんな風にウィンドウ上で動画の再生が行われます。
が、タスクマネージャでメモリ消費を見てみると、
モリモリとメモリ使用量が増えていきます。
(ある程度増えたところで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(); }); }