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)
F#でWPF --- Resource、Content、EmbeddedResource
F#でWPFプログラミングをする際に、XamlファイルのBuild Actionをいくつか選ぶことができます。
今回はResource、Content、EmbeddedResource各々でのXamlの読み込み方を記述していきます。
Resource
WPFにのみ提供されるBuild Actionです。
対象のリソースを実行ファイルに埋め込みます。
リソースを読み込む際は、GetResourceStreamを利用します。
Application.GetResourceStream(Uri("Resource.xaml", UriKind.Relative)).Stream
Xamlファイルを読み込む際は、LoadComponentが利用できます。
Application.LoadComponent(new Uri("Resource.xaml", UriKind.Relative))
Content
WPFにのみ提供されるBuild Actionです。
対象のリソースを外部ファイルのまま利用します。
"Copy to Output Directory"を"Copy always"か"Copy if newer"にしておくと、ビルドの際に出力ディレクトリにコピーしてくれます。
F#で利用する際には、AssemblyInfo.fsに対象ファイル分、下記を追加する必要があります。(C#では勝手に追加してくれるようです。)
open System.Windows.Resources [<assembly: AssemblyAssociatedContentFile("Content.xaml")>]
リソースを読み込む際は、GetContentStreamを利用します。
Application.GetContentStream(Uri("Content.xaml", UriKind.Relative)).Stream
Xamlファイルを読み込む際は、LoadComponentが利用できます。
Application.LoadComponent(new Uri("Content.xaml", UriKind.Relative))
EmbeddedResource
対象のファイルを実行ファイルに埋め込みます。
リソースを読み込む際は、GetManifestResourceStreamを利用します。
Assembly.GetExecutingAssembly().GetManifestResourceStream("Embedded.xaml")
Xamlファイルを読み込む際は、LoadComponentが使えないため、XamlReader.Loadを利用します。
XamlReader.Loadを利用するため、System.Xmlを参照に追加します。
open System.Windows.Markup let stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Embedded.xaml"); XamlReader.Load(stream)
格納場所
GetManifestResourceNamesを利用することで、アセンブリ内のリソース名一覧が取得できます。
EmbeddedResourceでビルドされたものは、この中に格納されています。
Assembly.GetExecutingAssembly().GetManifestResourceNames()
このリソースの中に、"アセンブリ名.g.resources"という名前のものがあります。
Resourceでビルドされたものは、さらにこの中に格納されています。
let asm = Assembly.GetExecutingAssembly() let resources = asm.GetManifestResourceStream(asm.GetName().Name + ".g.resources") let names = new ResourceReader(resources) |> Seq.cast<DictionaryEntry> |> Seq.iter (fun x -> x.Key)
Visual Studioと通信(F#)
通常、VisualStudioに機能を追加する際は、VSIXを作成してインストールします。
今回は外部からVisualStudioと通信してみます。
VisualStudioを見つける(DTEの取得)
RunningObjectTableからDTEのオブジェクトを取得しています。
DTEはIDE以外のも取れることがあります。
open System open System.Runtime.InteropServices open System.Runtime.InteropServices.ComTypes open EnvDTE [<DllImport("ole32.dll")>] extern int GetRunningObjectTable(int reserved, IRunningObjectTable& pprot) let mutable runningObjectTable = null if GetRunningObjectTable(0, &runningObjectTable) <> 0 then failwith "GetRunningObjectTable failed" let mutable enumMoniker = null runningObjectTable.EnumRunning(&enumMoniker) let monikers = [ let moniker = [| null |] while enumMoniker.Next(1, moniker, IntPtr.Zero) = 0 do yield moniker.[0] ] let getDte (moniker : IMoniker) = let mutable dte = null if runningObjectTable.GetObject(moniker, &dte) <> 0 then failwith "GetObject failed" match dte with | :? DTE as x -> Some x | _ -> None let dtes = monikers |> List.choose getDte
テキストを送ってみる
"Target"ソリューションの現在のカーソルの位置に"hello"を埋め込んでいます。
DTEのリストから対象のIDEを取得する方法は自由に変更してください。
let dte = dtes |> List.find (fun x -> IO.Path.GetFileNameWithoutExtension(x.Solution.FileName) = "Target") let document = dte.ActiveDocument.Object() :?> TextDocument document.Selection.ActivePoint.CreateEditPoint().Insert("hello")