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
Elm --- 階層化
下記記事にてElmを用いてカウンタを実装しました。
Elm --- Model、View、Update - 何でもプログラミング
今回はこのカウンタを再利用して、複数のカウンタを配置してみます。
内容はElmのTutorialにあるものとほとんど同じです。
作成するアプリケーション
カウンタが2つあり、一番下に合計値が出力されるアプリケーションになります。
Counter.elm
新たにCounter.elmファイルを作成し、下記コードを記述します。
内容は上記記事のものとほぼ同じです。
VisualStudioCodeを利用しているのですが、関数の型宣言をしないと警告が出るようになっていました。
module Counter exposing (..) import Html exposing (Html, div, button, text) import Html.Events exposing (onClick) type alias Model = { count : Int } initialModel : Model initialModel = { count = 0 } type Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case msg of Increment -> { model | count = model.count + 1 } Decrement -> { model | count = model.count - 1 } view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (toString model.count) ] , button [ onClick Increment ] [ text "+" ] ]
Main.elm
CounterのModel、update、viewをそのまま利用しています。
Html.App.mapを利用して、CounterのメッセージをMainの方に伝搬しています。
beginnerProgramがいつの間にかHtml.Appに移動していました。
import Html exposing (Html, div, text) import Html.App exposing (beginnerProgram, map) import Counter type alias Model = { counter1 : Counter.Model , counter2 : Counter.Model } initialModel : Model initialModel = { counter1 = Counter.initialModel , counter2 = Counter.initialModel } type Msg = Counter1Msg Counter.Msg | Counter2Msg Counter.Msg update : Msg -> Model -> Model update msg model = case msg of Counter1Msg x -> { model | counter1 = Counter.update x model.counter1 } Counter2Msg x -> { model | counter2 = Counter.update x model.counter2 } view : Model -> Html Msg view model = div [] [ map Counter1Msg (Counter.view model.counter1) , map Counter2Msg (Counter.view model.counter2) , div [] [ text (toString (model.counter1.count + model.counter2.count)) ] ] main : Program Never main = beginnerProgram { model = initialModel , view = view , update = update }
主要部分
// CounterのModelを保持 type alias Model = { counter1 : Counter.Model // メッセージの一つをCounterのMsg型に type Msg = Counter1Msg Counter.Msg // CounterのMsg型が来たらCounterのupdateを実行 update msg model = case msg of Counter1Msg x -> { model | counter1 = Counter.update x model.counter1 } // Counterのviewを利用し、mapにてメッセージを受け取り view model = div [] [ map Counter1Msg (Counter.view model.counter1)
リフレクション 逆引き (F#)
F#でリフレクションを利用するときに、やり方を忘れていることがよくあるため、ここを備忘録にしたいと思います。
今後適宜追加していこうと思います。
Type取得
let t = typeof<int>
TypeDefinition取得
let t = typedefof<List<_>>
TypeDefinitionからType作成
let t = typedefof<List<_>>.MakeGenericType(typeof<int>) // List<int>
TypeからTypeDefinition取得
typeof<List<int>>.GetGenericTypeDefinition() = typedefof<List<_>> // true
Cast可能かどうか
typeof<IEnumerable<int>>.IsAssignableFrom(typeof<List<int>>) // true
static classかどうか
let t = typeof<Enumerable> t.IsAbstract && t.IsSealed // true
ロードされているAssembly全て取得
let assemblies = System.AppDomain.CurrentDomain.GetAssemblies()
オーバーロードかつジェネリックな関数を取得
現状GetMethods()して絞り込む方法しかなさそうです。
下記はSystem.Linq.Enumerable.Selectの一つを取得しています。
let selectMethod = typeof<Enumerable>.GetMethods(BindingFlags.Static ||| BindingFlags.Public) |> Array.find (fun x -> let parameters = x.GetParameters() |> Array.map (fun x -> x.ParameterType.GetGenericTypeDefinition()) |> Array.toList x.Name = "Select" && parameters = [ typedefof<IEnumerable<_>>; typedefof<Func<_, _>> ])
拡張メソッドかどうか
let method = typeof<Enumerable>.GetMethod("All") method.IsDefined(typeof<ExtensionAttribute>, true) // true
Cast可能な型一覧取得
ベースクラスはBaseTypeで、インターフェースはGetInterfaces()で取得します。
インターフェースの継承関係はツリー構造になるため、flatTreeを用意しています。
let assignableTypes (type_ : Type) = let flatTree getChildren root = [ root ] |> List.unfold (function | [] -> None | h::t -> Some (h, t @ getChildren h)) type_ |> List.unfold (Option.ofObj >> Option.map (fun x -> x, x.BaseType)) |> List.collect (flatTree (fun x -> x.GetInterfaces() |> Array.toList)) |> List.distinct
UnionCaseの名前取得
let caseName (x : obj) = FSharpValue.GetUnionFields(x, x.GetType()) |> fst |> (fun x -> x.Name)