MediaFoundation --- 動画の読み込み
下記記事にてOpenCVで動画を読み書きする方法を調べました。
OpenCVで動画読み込み&書き込み - 何でもプログラミング
今回はWindows特有のMediaFoundationを利用して動画を読み込んでみます。
全体の流れの把握重視の為、エラー処理は全て省いてあります。
アプリケーションコード
動画を読み込んで、そのまま書き出しています。
今回は書き出しにはOpenCVを利用しました。
IMFSourceReaderを作成、デコーダ設定、読み込み、の流れになります。
動画はOpenCVのサンプルである、vtest.aviを利用しています。
MediaFoundationを利用するにあたり、MFStartupを忘れないようにしてください。(CoInitializeも、されていなければ呼び出してください。)
ConfigureVideoDecoder、GetVideoInfo、Captureは後程記載します。
#include <mfapi.h> #include <mfidl.h> #include <mfreadwrite.h> #pragma comment(lib, "mfplat.lib") #pragma comment(lib, "mfuuid.lib") #pragma comment(lib, "mfreadwrite.lib") #include <opencv2\opencv.hpp> #pragma comment(lib, "opencv_world320.lib") int main() { CoInitialize(NULL); MFStartup(MF_VERSION); IMFSourceReader* reader; MFCreateSourceReaderFromURL(L"c:\\lib\\opencv3.2\\sources\\samples\\data\\vtest.avi", NULL, &reader); ConfigureVideoDecoder(reader, MFVideoFormat_RGB24); UINT32 width, height; double fps; GetVideoInfo(reader, &width, &height, &fps); auto writer = cv::VideoWriter("output.avi", cv::VideoWriter::fourcc('X', 'V', 'I', 'D'), fps, cv::Size(width, height)); while (true) { auto data = Capture(reader); if (data.empty()) break; cv::Mat frame(height, width, CV_8UC3, data.data()); writer << frame; } reader->Release(); MFShutdown(); CoUninitialize(); return 0; }
ConfigureVideoDecoder
SourceReaderにどのフォーマットで出力するか設定します。
上記ではMFVideoFormat_RGB24を指定していますが、デコーダによってはMFVideoFormat_YUY2しか出力しない等ありますので、利用の際は注意してください。
対応出力は下記ページのVideo Codecs、Decoderのリンク先を参照してください。
Supported Media Formats in Media Foundation (Windows)
void ConfigureVideoDecoder(IMFSourceReader* reader, GUID format) { IMFMediaType* mediaType; MFCreateMediaType(&mediaType); mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); mediaType->SetGUID(MF_MT_SUBTYPE, format); reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, mediaType); mediaType->Release(); }
GetVideoInfo
SourceReaderからサイズとfpsを取得しています。
void GetVideoInfo(IMFSourceReader* reader, UINT32* width, UINT32* height, double* fps) { IMFMediaType* mediaType; reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &mediaType); MFGetAttributeSize(mediaType, MF_MT_FRAME_SIZE, width, height); UINT32 nume, denom; MFGetAttributeRatio(mediaType, MF_MT_FRAME_RATE, &nume, &denom); *fps = (double)nume / denom; mediaType->Release(); }
Capture
ReadSampleにてIMFSampleを取得し、そこからIMFMediaBufferを取得、データ取り出しを行っています。
ReadSample時にflagsを渡さないとsampleがnullptrになってしまいます。
std::vector<BYTE> Capture(IMFSourceReader* reader) { DWORD flags; IMFSample* sample; reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, NULL, &flags, NULL, &sample); if (sample == nullptr) return std::vector<BYTE>(); IMFMediaBuffer* buffer; sample->GetBufferByIndex(0, &buffer); BYTE* p; DWORD size; buffer->Lock(&p, NULL, &size); std::vector<BYTE> data(size); memcpy(data.data(), p, size); buffer->Unlock(); buffer->Release(); sample->Release(); return data; }
シーク
SourceReaderにはSetCurrentPositionがあり、100ns単位で指定します。
ただしその位置に一番近いキーフレームに移動します。
void SeekToKeyframe(IMFSourceReader* reader, int _100ns) { PROPVARIANT var; InitPropVariantFromInt64(_100ns, &var); auto hr = reader->SetCurrentPosition(GUID_NULL, var); PropVariantClear(&var); }
動画時間取得
durationは100ns単位の値になります。
PROPVARIANT var; reader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, MF_PD_DURATION, &var); LONGLONG duration; PropVariantToInt64(var, &duration); PropVariantClear(&var);
Fourcc取得
IMFMediaType* mediaType; reader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &mediaType); GUID subtype; mediaType->GetGUID(MF_MT_SUBTYPE, &subtype); char fourcc[] = { (char) (subtype.Data1 & 0XFF), (char)((subtype.Data1 & 0XFF00) >> 8), (char)((subtype.Data1 & 0XFF0000) >> 16), (char)((subtype.Data1 & 0XFF000000) >> 24), };
OpenCVで動画読み込み&書き込み --- C++/CLIでラップ
下記記事にてOpenCVで動画を読み書きする方法を調べました。
OpenCVで動画読み込み&書き込み - 何でもプログラミング
今回は.Netで利用できるよう、C++/CLIでラップしてみたいと思います。
尚、Matクラス(Mat8UC1クラス)は下記記事のものを利用しています。
OpenCVをC++/CLIでラップ - 何でもプログラミング
VideoCapture
FourccはStringで返すようにしてみました。
Mat8UC1を利用しているため、Captureの中で8UC1に変換しています。ここは適宜変更してください。
using namespace msclr::interop; using namespace System; public ref class VideoCapture { public: property int Width { int get() { return (int)_capture->get(CV_CAP_PROP_FRAME_WIDTH); } } property int Height { int get() { return (int)_capture->get(CV_CAP_PROP_FRAME_HEIGHT); } } property double Fps { double get() { return _capture->get(CV_CAP_PROP_FPS); } } property int Count { int get() { return (int)_capture->get(CV_CAP_PROP_FRAME_COUNT); } } property String^ Fourcc { String^ get() { int i = (int)_capture->get(CV_CAP_PROP_FOURCC); char fourcc[] = { (char)(i & 0XFF), (char)((i & 0XFF00) >> 8), (char)((i & 0XFF0000) >> 16), (char)((i & 0XFF000000) >> 24), 0 }; return gcnew String(fourcc); } } property bool IsOpened { bool get() { return _capture->isOpened(); } } VideoCapture(String^ path) { _capture = new cv::VideoCapture(marshal_as<std::string>(path)); } !VideoCapture() { if (_capture != nullptr) { delete _capture; _capture = nullptr; } } ~VideoCapture() { this->!VideoCapture(); } Mat8UC1^ Capture() { cv::Mat src; *_capture >> src; auto dst = gcnew Mat8UC1(src.rows, src.cols); if (src.channels() == 1) src.copyTo(*dst->_mat); else if (src.channels() == 3 || src.channels() == 4) cv::cvtColor(src, *dst->_mat, CV_RGB2GRAY); return dst; } void Seek(int frame) { _capture->set(CV_CAP_PROP_POS_FRAMES, frame); } private: cv::VideoCapture* _capture; };
VideoWriter
Fourcc文字列からIntに変換するにはcv::VideoWriter::fourccを利用します。
Write関数内でカラー化していますが、ここは適宜変更してください。
public ref class VideoWriter { public: property bool IsOpened { bool get() { return _writer->isOpened(); } } VideoWriter(String^ path, int width, int height, String^ fourcc, double fps) { _writer = new cv::VideoWriter( marshal_as<std::string>(path), cv::VideoWriter::fourcc(fourcc[0], fourcc[1], fourcc[2], fourcc[3]), fps, cv::Size(width, height)); } !VideoWriter() { if (_writer != nullptr) { delete _writer; _writer = nullptr; } } ~VideoWriter() { this->!VideoWriter(); } void Write(Mat8UC1^ mat) { cv::Mat color; cv::cvtColor(*mat->_mat, color, CV_GRAY2BGR); *_writer << color; } private: cv::VideoWriter* _writer; };
F#で利用
動画はopenCVのサンプルMegamind.aviを利用しています。
動画をそのまま保存しているだけですが、Mat8UC1を介しているため出力はグレースケールになっています。
[<EntryPoint>] let main argv = let capture = new CV.VideoCapture(@"C:\lib\opencv3.2\sources\samples\data\megamind.avi") let writer = new CV.VideoWriter(@"output.avi", capture.Width, capture.Height, capture.Fourcc, capture.Fps); capture |> Seq.unfold (fun x -> Some(x.Capture(), x)) |> Seq.takeWhile (fun x -> not x.IsEmpty) |> Seq.iter writer.Write 0
OpenCVで動画読み込み&書き込み
OpenCVのダウンロードは下記記事を参照してください。
OpenCVをC++/CLIでラップ - 何でもプログラミング
VideoCapture
動画を読み込むにはVideoCaptureクラスを利用します。
パスを指定して開き、>>オペレータでフレームを取得します。
cv::VideoCapture capture("c:\\..."); cv::Mat frame; capture >> frame;
VideoWriter
動画を出力するにはVideoWriterクラスを利用します。
パス、コーデック、fps、サイズを指定して作成し、<<オペレータでフレームを出力します。
cv::VideoWriter writer("c:\\...", fourcc, fps, cv::Size(width, height)); writer << frame;
エッジ化して出力する例
Win32コンソールアプリケーション(64bit)を想定しています。
動画は、opencvのサンプルとして付属しているMegamind.aviを利用します。(sources/samples/data/Megamind.avi)
開いた動画の情報は、VideoCaptureのget関数で色々取得できます。
作成したexeと同じフォルダにopencv_world320.dllとopencv_ffmpeg320_64.dllをコピーするのを忘れないようにしてください。
#include <opencv2\opencv.hpp> #pragma comment(lib, "opencv_world320.lib") int main() { cv::VideoCapture capture("c:\\lib\\opencv3.2\\sources\\samples\\data\\megamind.avi"); int width = (int)capture.get(CV_CAP_PROP_FRAME_WIDTH); int height = (int)capture.get(CV_CAP_PROP_FRAME_HEIGHT); int count = (int)capture.get(CV_CAP_PROP_FRAME_COUNT); int fourcc = (int)capture.get(CV_CAP_PROP_FOURCC); double fps = capture.get(CV_CAP_PROP_FPS); cv::VideoWriter writer("out.avi", fourcc, fps, cv::Size(width, height), false); while (true) { cv::Mat frame; capture >> frame; if (frame.empty()) break; cv::Mat edge; cv::Canny(frame, edge, 50, 150); writer << edge; } return 0; }
シーク
CV_CAP_PROP_POS_FRAMESでフレーム位置を指定できます。(その他ミリ秒で設定できるCV_CAP_PROP_POS_MSECなどもあります)
capture.set(CV_CAP_PROP_POS_FRAMES, 100);
H.264
上記のアプリケーションでは、H.264の読込は出来ても、書き込みはできません。
書き込みをしたい場合は、下記からopenh264-1.6.0-win64msvc.dllをダウンロードしてexeと同じフォルダに配置してください。
Releases · cisco/openh264 · GitHub
F#でWPF --- BitmapImageで画像ロード
下記記事にてOpenCVと連携して画像を表示しました。
画像を読み込むだけであればWPFの標準セットで可能であるため、今回はWPFで画像をロードして表示してみます。
ついでにグレースケール化も行ってみます。
作成するアプリケーション
ファイルダイアログで画像を選び、表示するアプリケーションを作成します。
アプリケーションコード
読込にはBitmapImageを利用します。(読込失敗の場合は例外が発生します。)
グレースケール化は、FormatConvertedBitmapを利用します。
その他ColorConvertedBitmap、TransformedBitmapなど色々あります。
Xamlは上記記事のものをそのまま使います。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:local="clr-namespace:Actions;assembly=OpenCVSample" Title="MainWindow" Height="250" Width="400"> <Grid> <Button Content="画像を開く" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <local:OpenFileDialogAction Command="{Binding LoadImage}" /> </i:EventTrigger> </i:Interaction.Triggers> </Button> <UniformGrid Margin="10,40,10,10" Columns="2"> <Image Source="{Binding SrcImage}" /> <Image Source="{Binding DstImage}" /> </UniformGrid> </Grid> </Window>
F#もupdateModel以外は上記記事のものをそのまま使います。
open System open System.Windows open System.Windows.Media open System.Windows.Media.Imaging type Model = { SrcImage : BitmapSource DstImage : BitmapSource } let initialModel = { SrcImage = null DstImage = null } type Msg = LoadImage of string let updateModel model msg = match msg with | LoadImage x -> let bmp = try BitmapImage(Uri(x)) with | _ -> failwith "not supported" let gray = FormatConvertedBitmap(bmp, PixelFormats.Gray8, null, 0.0) { model with SrcImage = bmp DstImage = gray } [<STAThread>] [<EntryPoint>] let main argv = let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window window.DataContext <- DataContext(initialModel, updateModel, id) Application().Run(window) |> ignore 0
F#でWPF --- OpenCV連携
下記記事にてOpenCVを.netから利用できるようC++/CLIでラップする方法を紹介しました。
OpenCVをC++/CLIでラップ - 何でもプログラミング
今回はWPFと連携させて画像を表示してみたいと思います。
作成するアプリケーション
ファイルダイアログで画像を選び、表示するアプリケーションを作成します。折角なのでエッジ化した画像も表示してみます。
Canny追加
C++/CLI側のコードは上記記事のものを利用します。
追加してCanny関数を定義します。
[ExtensionAttribute] static Mat8UC1^ Canny(Mat8UC1^ src, double threshold1, double threshold2) { auto dst = gcnew Mat8UC1(src->Cols, src->Rows); cv::Canny(*src->_mat, *dst->_mat, threshold1, threshold2); return dst; }
アプリケーションコード
WPFのImageコントロールはImageSourceクラスを受け取ります。
そのため、BitmapSource.Createを利用してMat8UC1クラスからBitmapSourceを作成しています。
Xaml内のOpenFileDialogActionは下記記事を参照してください。
F#でWPF --- ファイルダイアログCommand - 何でもプログラミング
F#内のDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:local="clr-namespace:Actions;assembly=OpenCVSample" Title="MainWindow" Height="250" Width="400"> <Grid> <Button Content="画像を開く" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <local:OpenFileDialogAction Command="{Binding LoadImage}" /> </i:EventTrigger> </i:Interaction.Triggers> </Button> <UniformGrid Margin="10,40,10,10" Columns="2"> <Image Source="{Binding SrcImage}" /> <Image Source="{Binding DstImage}" /> </UniformGrid> </Grid> </Window>
F#
open System open System.Windows open System.Windows.Media open System.Windows.Media.Imaging open CV type Model = { SrcImage : BitmapSource DstImage : BitmapSource } let initialModel = { SrcImage = null DstImage = null } type Msg = LoadImage of string let updateModel model msg = match msg with | LoadImage x -> let mat = Processor.Load(x) let toBitmapSource (x : Mat8UC1) = BitmapSource.Create(x.Cols, x.Rows, 96.0, 96.0, PixelFormats.Gray8, null, x.Data, x.Cols * x.Rows, x.Cols) { model with SrcImage = toBitmapSource mat DstImage = toBitmapSource <| mat.Canny(50.0, 200.0) } [<STAThread>] [<EntryPoint>] let main argv = let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window window.DataContext <- DataContext(initialModel, updateModel, id) Application().Run(window) |> ignore 0
C++/CLIラッピング --- 逆引き
C++を.Netで利用する際、C++/CLIで必要になりそうなことをまとめていきます。
随時更新予定です。
クラスラップ基本構造
public ref class NetClass { public: NetClass() { _cppClass = new CppClass(); } ~NetClass() { this->!NetClass(); } // Dispose() !NetClass() { delete _cppClass; } // ファイナライザ private: CppClass* _cppClass; };
System::String → std::string (copy)
#include <msclr\marshal_cppstd.h> System::String^ net; std::string cpp = msclr::interop::marshal_as<std::string>(net);
std::string → System::String (copy)
std::string cpp; System::String^ net = gcnew String(cpp.c_str());
System::String → const wchar_t* (no copy)
System::StringはUnicodeのため、wchar_t*しか利用できません。
System::String^ net; pin_ptr<const wchar_t> pin = PtrToStringChars(net); const wchar_t* p = pin;
array<int> → int* (no copy)
array<int>^ net; pin_ptr<int> pin = &net[0]; int* p = pin;
std::vector<int> → array<int> (copy)
std::vector<int> cpp; auto net = gcnew array<int>(cpp.size()); System::Runtime::InteropServices::Marshal::Copy(System::IntPtr(cpp.data()), net, 0, net->Length);
プロパティ
デフォルトのgetter、setterでよいならproperty int Value;のみでもOKです。
public ref class NetClass { public: property int Value { int get() { return 10; } int set(int value) { Console::WriteLine(value); } } };
インターフェース
public interface class INetClass { public: virtual int GetValue(); }; public ref class NetClass : public INetClass { public: virtual int GetValue() { return 10; } };
抽象クラス
public ref class NetClassBase abstract { public: virtual int GetValue() abstract; }; public ref class NetClass : public NetClassBase { public: virtual int GetValue() override { return 10; } };
拡張メソッド
using namespace System; using namespace System::Runtime::CompilerServices; [ExtensionAttribute] public ref class Extension abstract sealed { public: [ExtensionAttribute] static String^ AddSpace(String^ src) { return src + " "; } };
GCにメモリ使用量通知
C++のnew等でアンマネッジドなメモリを確保した場合、GCはそれを感知しません。
GCに通知しておくと、適宜Collectが行われるようになります。
0より大きい値でないと例外が発生します。
using namespace System; GC::AddMemoryPressure(10); // 追加 GC::RemoveMemoryPressure(10); // 削除
ref classをスコープ脱出時にデストラクト
gcnewを用いずにインスタンス化すると、通常のcppのクラスのように、スコープを抜けるとデストラクタが呼ばれます。
ref class NetClass { public: ~NetClass() { Console::WriteLine("destructed"); } }; { NetClass c; } // "destructed"
メンバのDisposeを自動で呼ぶ
スタックセマンティック(gcnewで作成されていない)のメンバが存在すると、親のDispose時にメンバのDisposeも呼んでくれます。
また、親のDisposeは宣言しなくても自動で作成されます。(宣言した場合は、親のDisposeが先に処理されます。)
ref class Child { public: ~Child() { Console::WriteLine("child disposed"); } }; ref class Parent { Child _child; }; // C#側 Parent parent; parent.Dispose(); // "child disposed"
OpenCVをC++/CLIでラップ
今回はOpenCVをF#から扱えるようにしてみたいと思います。
OpenCVはC++のライブラリであるため、C++/CLIを用いてF#から扱えるようにします。
OpenCV取得
下記サイトから、バイナリとソースがセットになったファイルをダウンロードします。
解凍(exe実行)して適当なフォルダに配置してください。
今回は既にビルドされているopencv_world320.dllを利用していきます。
(opencv/build/x64/vc14/binフォルダにあります。)
C++/CLIプロジェクトの準備
VisualC++ / CLR / Class Libraryを選択します。
opencv_world320.dllが64bitでビルドされているので、64bitビルドに変更します。
プロジェクトの設定で、OpenCVのincludeとlibの場所を設定します。
Matクラス(CV_8UC1限定)
内部でcv::Matポインタを保持し、propertyで適宜メンバを公開しています。
GCにメモリが確保されたことを知らせるため、AddMemoryPressureとRemoveMemoryPressureを利用しています。
~Mat8UC1()は.Net側ではDispose()に変わります。
!Mat8UC1()は.Netのファイナライザになります。
public ref class Mat8UC1 { public: property int Cols { int get() { return _mat->cols; } } property int Rows { int get() { return _mat->rows; } } property IntPtr Data { IntPtr get() { return IntPtr(_mat->data); } } Mat8UC1(int rows, int cols) { _mat = new cv::Mat(rows, cols, CV_8UC1); if (_mat->empty() == false) GC::AddMemoryPressure(_mat->total() * _mat->elemSize()); } !Mat8UC1() { if (_mat != nullptr) { if (_mat->empty() == false) GC::RemoveMemoryPressure(_mat->total() * _mat->elemSize()); delete _mat; _mat = nullptr; } } ~Mat8UC1() { this->!Mat8UC1(); } internal: cv::Mat* _mat; };
Prosseorクラス
動作確認のため、画像のロードとセーブを行う関数を作成しました。
ExtensionAttributeを付与することにより、Saveは拡張メソッドとして定義されます。
System::Stringからstd::stringへの変換は"msclr/marshal_cppstd.h"を利用しています。
imreadの代に引数を0にすることにより、グレースケールで読み込むようにしてあります。
[ExtensionAttribute] public ref class Processor abstract sealed { public: static Mat8UC1^ Load(String^ path) { cv::Mat src = cv::imread(msclr::interop::marshal_as<std::string>(path), 0); Mat8UC1^ dst = gcnew Mat8UC1(src.rows, src.cols); src.copyTo(*dst->_mat); return dst; } [ExtensionAttribute] static void Save(Mat8UC1^ src, String^ path) { cv::imwrite(msclr::interop::marshal_as<std::string>(path), *src->_mat); } };
コード全体(ヘッダファイル)
警告が発生するので、"opencv2\opencv.hpp"はunmanagedでコンパイルされるようにしてあります。
#pragma once #pragma unmanaged #include <opencv2\opencv.hpp> #pragma managed #include <msclr\marshal_cppstd.h> #ifdef _DEBUG #pragma comment(lib, "opencv_world320d.lib") #else #pragma comment(lib, "opencv_world320.lib") #endif using namespace System; using namespace System::Runtime::CompilerServices; namespace CV { public ref class Mat8UC1 { public: property int Cols { int get() { return _mat->cols; } } property int Rows { int get() { return _mat->rows; } } property IntPtr Data { IntPtr get() { return IntPtr(_mat->data); } } Mat8UC1(int rows, int cols) { _mat = new cv::Mat(rows, cols, CV_8UC1); if (_mat->empty() == false) GC::AddMemoryPressure(_mat->total() * _mat->elemSize()); } !Mat8UC1() { if (_mat != nullptr) { if (_mat->empty() == false) GC::RemoveMemoryPressure(_mat->total() * _mat->elemSize()); delete _mat; _mat = nullptr; } } ~Mat8UC1() { this->!Mat8UC1(); } internal: cv::Mat* _mat; }; [ExtensionAttribute] public ref class Processor abstract sealed { public: static Mat8UC1^ Load(String^ path) { cv::Mat src = cv::imread(msclr::interop::marshal_as<std::string>(path), 0); Mat8UC1^ dst = gcnew Mat8UC1(src.rows, src.cols); src.copyTo(*dst->_mat); return dst; } [ExtensionAttribute] static void Save(Mat8UC1^ src, String^ path) { cv::imwrite(msclr::interop::marshal_as<std::string>(path), *src->_mat); } }; }
動作確認
単に画像を読み込んで保存するだけです。
exeと同じフォルダにopencv_world320.dllを配置するのを忘れないようにしてください。
(面倒であればリソースに追加して"copy if newer"にするのもアリです。)
また、64bitターゲットにするのを忘れないようにしてください。
open CV [<EntryPoint>] let main argv = let image = Processor.Load(@"image.jpg") image.Save(@"image2.jpg") 0