[Flutter] Windowsからdart:ffi経由でOpenCVを呼び出す方法

FlutterでWindowsアプリケーションが作れるようになりましたが、画像処理をするのにDartのImageライブラリを使うと物凄く遅かったので、WindowsのFlutterからdart:ffi経由でOpenCVを使う方法の紹介です。ここに書いたものより、もっといいやり方があると思いますが、FlutterからOpenCVを呼び出す記事があまり見つからなかったので、メモとして置いておきます。
　（iOS/Androidからdart:ffi経由で使うには、そのものズバリのリポジトリ https://github.com/as1605/opencv_flutter_ffi があるので、そちらを参考にどうぞ。クローンして、ちゃっちゃっと書き換えればあっさり動きます。）

## dart:ffiライブラリ
dart:ffiはforeign function interface.の略で、dartから外部のネイティブCライブラリをコールするための仕組みです。DLLを文字通り動的に呼び出すことができます。DLLはCインターフェースであれば、どこで作ったものでもかまいません。
![dartffi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114010/73efbbe2-8f64-9a77-6799-335542af4c8a.png)
左側dart部分がUI部分を担当し、dart:ffiを経由してDLLを呼び出しOpenCVで画像を処理するという流れです。OpenCVに限らずdart:ffiでC/C++のDLLを使うときは、表のUIはFlutter dartが担当して、裏の処理をdart:ffiで行うという使い方になると思います。

### dart側での呼び出し方法
まずDynamicLibraryオブジェクトを作成します。ファイル名を指定してオープンするだけですが、プラットフォームによってファイル名が違ったり初期化の方法が違うのでif文で切り分けて初期化します。
```dart:DynamicLibraryオブジェクトの定義
late DynamicLibrary  dylib ;
...
if(Platform.isWindows){
    dylib = DynamicLibrary.open("OpenCVProc.dll");
}
```
次にDynamicLibraryオブジェクトから関数エントリーを検索します。関数名とシグネチャつまり引数の型のリストを指定すると、該当するエントリーが得られるので、あとはそれを使って通常の関数のように呼び出すことができます。

　関数エントリー検索メソッド　lookupFunction　は次のように書きます。
　　lookupFunction< 
　　　　C関数のシグネチャ, 
　　　　dart関数のシグネチャ>(関数名)

シグネチャに使える型は、整数型、浮動小数型、それらのポインタ、文字列くらいで、複雑なものは使えません。シグネチャの型名はCとdartで微妙に違ってます。dartにポインタがないので、ポインタの取得には特殊なクラスやメソッドを使います。ざっくりと以下の表のようになります。
||  dartシグネチャ  |  Cシグネチャ  |   Cヘッダ  |取得方法 |
|---| ---- | ---- |---- |---- |
|基本型|  void  |  Void  | void||
|基本型|  int  |  Int32  | int||
|基本型|  double  |  Double  | double||
|ポインタ|  Pointer&lt;Int32>  |  Pointer&lt;Int32>  | int*|malloc.allocate&lt;Int32>(128)|
|文字列|  Pointer&lt;Utf8> |  Pointerr&lt;Utf8>  |unsigned char*|xxx.toNativeUtf8()|

使い方の例
```dart:関数エントリー検索メソッド　lookupFunction
// DLL関数の取得
final dllfunc = dylib.lookupFunction<
    Void Function(Pointer<Utf8>, Int32, Pointer<Uint32>),  // C シグネチャ
    void Function(Pointer<Utf8>, int, Pointer<Uint32>)     // dartシグネチャ
    >("RotImg");

// パラメータ設定方法
String filename="img.png";
final inPath = filename.toNativeUtf8();  // 文字列ポインタの取得
int s=0;                                 // 基本型はそのまま使える 
Pointer<Uint32> w = malloc.allocate(128); // ポインタ領域確保

// DLL関数の呼び出し
dllfunc(inPath, s, w);  
```


## Windows側DLLの作成
　Windows側DLLは普通に作れば大丈夫です。ここから先はVisual StudioでOpenCVを使うDLLの作り方の話となります。FlutterをVSCodeで作ってるので、全部VSCodeのCMake一発で済めばいいんですが、知識不足でそこまでできてません。

　なお、Visual Studio と OpenCV のプロジェクト設定は以下の金丸隆志さんのページを参考にしました。
　https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/opencv/index1.html
### Visual StudioでのDLL作成
　Visual Studioの新規作成で「ダイナミック リンク ライブラリ(DLL)」を選びます。
![VS02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114010/e73071dc-f37c-0ef9-2061-1c93f4507c7d.png)
プロジェクトの場所はどこでもいいのですが、Flutterのプロジェクトの下の/windowsの下あたりに作るとFlutterのプロジェクトとGitのリポジトリが共有できて便利です。Visual Studio、Flutterどちらのメニューからでも同じところにコミットできます。まあVisual StudioもVSCodeもGitHubも全部マイクロソフト製ですね。
![VS03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114010/fe422607-498a-e0b5-69d7-8b5b97787936.png)
プロジェクトができたら、以下メニューからプロジェクトのプロパティを設定します。
![VS05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114010/2f70857b-fed9-1859-84ae-e8ce2b2332b9.png)
設定するプロパティは以下の３つです。
・C/C++の追加のインクルードディレクトリ
　　　{OpenCVをインストールしたディレクトリ}\build\include
・リンカーの追加のライブラリディレクトリ
　　　{OpenCVをインストールしたディレクトリ}\build\x64\vc15\lib 
・構成全般の出力ディレクトリ
　　　Flutterの出力先と同じにすると、いちいちコピーしなくていいので便利。
　　　たとえばデバッグモードなら以下の場所に設定する
　　　{Flutterのプロジェクトの格納されたディレクトリ}\build\windows\runner\Debug

さらにCPPのソースの先頭に以下の記述を入れます。
```C++:CPPソースに必要な記述
#ifdef _DEBUG
#pragma comment(lib, "opencv_world460d.lib")
#else
#pragma comment(lib, "opencv_world460.lib")
#endif

#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/flann/flann.hpp>

using namespace cv;
using std::cout; using std::cin;
using std::endl; using std::ifstream;
```
　　#pragma comment(lib, "opencv_world460d.lib")
の最後に引用符でくくられている部分は、インストールされているOpenCVライブラリのファイル名を記述してください。ライブラリファイルは {OpenCVをインストールしたディレクトリ}\build\x64\vc15\lib の下に格納されています。末尾にdが付いてるのがデバッグ用です。
#### 関数の内容とエクスポート定義
　ここまでで準備は完了です。それでは実際に関数を書いてみます。以下のような仕様です。
・パラメータとして入力ファイル名、出力ファイル名、入力パラメータ配列、出力パラメータ配列を受け取る
・入力ファイルから画像を読み取り、画像を入力パラメータで指定された角度だけ回転させる。ただし角度は、0、90、180、270 のいずれかの指定のみ。
・できた画像を出力ファイル名で指定された場所に格納する。
```C++:画像ファイル回転
void RotImg(char* inpath, char* outpath, int angle)
{
    Mat img = cv::imread(inpath);
    if (img.size == 0) {
        return;
    }

    switch (angle) {
    case 0:   break;
    case 90:  cv::rotate(img, img, cv::ROTATE_90_CLOCKWISE); break;
    case 180: cv::rotate(img, img, cv::ROTATE_180);        break;
    case 270: cv::rotate(img, img, cv::ROTATE_90_COUNTERCLOCKWISE); break;
    }

    cv::imwrite(outpath, img);
}
```
※Windowsでcv::imread cv:imwriteを使うと日本語ファイルパスが使えないという問題があります。それについてはこちら https://qiita.com/picpie/items/8a4b865c17ce6dc72c7e

この関数をdart:ffiから呼べるようにヘッダファイルでエクスポート指定するには次のようにします。
```C++:画像ファイル回転
extern "C"  __declspec(dllexport)
void RotImg(char* inpath, char* outpath, int angle);
```
おさらいとなりますが、この関数をFlutterからdart:ffi経由で呼び出すには次のように記述します。inputfile、outputfileにはStutf-8でファイルパスが入ってるものとします。
```C++:画像ファイル回転
// 呼び出し関数の生成
final rotImg = dylib.lookupFunction<
    Void Function(Pointer<Utf8>, Pointer<Utf8>, Int32),  // C シグネチャ
    void Function(Pointer<Utf8>, Pointer<Utf8>, int)     // dartシグネチャ
    >("RotImg");
// パラメータ領域確保
final inPath = inputfile.toNativeUtf8(); 
final outPath = outputfile.toNativeUtf8(); 
// DLL関数の呼び出し
rotImg(inPath, outPath, 180);  
```

## OpenCVを使ったアプリ
FlutterからOpenCVを使った例として、ディレクトリ内の画像ファイルを回転させて、別のディレクトリに書き出すアプリを作ってみました。srcディレクトリにある画像ファイル全てを、ラジオボタンで指定された角度に回転して、dstディレクトリに保存するものです。
![image_batch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114010/4a129a20-bfdc-bb3c-34ac-7c6963ccb833.png)
画面UIはFlutterで作成し、OpenCVが担うのはファイルを読んで回転させて格納するところだけです。

4080x3072の大きさの画像ファイルが61個あるディレクトリで実行してみたところ、Dart Imageライブラリを使ったものと、OpenCVライブラリを使ったもので、ファイル変換にかかった時間は次のようになりました。OpenCVの処理時間はざっくり半分ですね。
|ライブラリ |処理時間      |
|----------|--------------|
|Dart Image|0:01:26.887189|
|OpenCV    |0:00:46.510620|

UI部分をFlutterでサクッと作って、画像処理とか機械学習とか処理時間のかかることは、C/C++のDLLに任せるというのは、いい取り合わせでしょう。DLLは既存のものがほぼ使えるし、プラットフォーム越えて共通化できる部分も多い、呼び出し定義もJNIほどややこしくない。既存の専門的なライブラリを使って、ちょっと実験的なアプリケーションを作ったりするのに向いてると思います。