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インターフェースであれば、どこで作ったものでもかまいません。
左側dart部分がUI部分を担当し、dart:ffiを経由してDLLを呼び出しOpenCVで画像を処理するという流れです。OpenCVに限らずdart:ffiでC/C++のDLLを使うときは、表のUIはFlutter dartが担当して、裏の処理をdart:ffiで行うという使い方になると思います。
dart側での呼び出し方法
まずDynamicLibraryオブジェクトを作成します。ファイル名を指定してオープンするだけですが、プラットフォームによってファイル名が違ったり初期化の方法が違うのでif文で切り分けて初期化します。
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<Int32> | Pointer<Int32> | int* | malloc.allocate<Int32>(128) |
文字列 | Pointer<Utf8> | Pointerr<Utf8> | unsigned char* | xxx.toNativeUtf8() |
使い方の例
// 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)」を選びます。
プロジェクトの場所はどこでもいいのですが、Flutterのプロジェクトの下の/windowsの下あたりに作るとFlutterのプロジェクトとGitのリポジトリが共有できて便利です。Visual Studio、Flutterどちらのメニューからでも同じところにコミットできます。まあVisual StudioもVSCodeもGitHubも全部マイクロソフト製ですね。
プロジェクトができたら、以下メニューからプロジェクトのプロパティを設定します。
設定するプロパティは以下の3つです。
・C/C++の追加のインクルードディレクトリ
{OpenCVをインストールしたディレクトリ}\build\include
・リンカーの追加のライブラリディレクトリ
{OpenCVをインストールしたディレクトリ}\build\x64\vc15\lib
・構成全般の出力ディレクトリ
Flutterの出力先と同じにすると、いちいちコピーしなくていいので便利。
たとえばデバッグモードなら以下の場所に設定する
{Flutterのプロジェクトの格納されたディレクトリ}\build\windows\runner\Debug
さらに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 のいずれかの指定のみ。
・できた画像を出力ファイル名で指定された場所に格納する。
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から呼べるようにヘッダファイルでエクスポート指定するには次のようにします。
extern "C" __declspec(dllexport)
void RotImg(char* inpath, char* outpath, int angle);
おさらいとなりますが、この関数をFlutterからdart:ffi経由で呼び出すには次のように記述します。inputfile、outputfileにはStutf-8でファイルパスが入ってるものとします。
// 呼び出し関数の生成
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ディレクトリに保存するものです。
画面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ほどややこしくない。既存の専門的なライブラリを使って、ちょっと実験的なアプリケーションを作ったりするのに向いてると思います。