LoginSignup
6
4

More than 1 year has passed since last update.

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

Posted at

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
左側dart部分がUI部分を担当し、dart:ffiを経由してDLLを呼び出しOpenCVで画像を処理するという流れです。OpenCVに限らずdart:ffiでC/C++のDLLを使うときは、表のUIはFlutter dartが担当して、裏の処理をdart:ffiで行うという使い方になると思います。

dart側での呼び出し方法

まずDynamicLibraryオブジェクトを作成します。ファイル名を指定してオープンするだけですが、プラットフォームによってファイル名が違ったり初期化の方法が違うのでif文で切り分けて初期化します。

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<Int32> Pointer<Int32> int* malloc.allocate<Int32>(128)
文字列 Pointer<Utf8> Pointerr<Utf8> unsigned char* xxx.toNativeUtf8()

使い方の例

関数エントリー検索メソッド 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
プロジェクトの場所はどこでもいいのですが、Flutterのプロジェクトの下の/windowsの下あたりに作るとFlutterのプロジェクトとGitのリポジトリが共有できて便利です。Visual Studio、Flutterどちらのメニューからでも同じところにコミットできます。まあVisual StudioもVSCodeもGitHubも全部マイクロソフト製ですね。
VS03.png
プロジェクトができたら、以下メニューからプロジェクトのプロパティを設定します。
VS05.png
設定するプロパティは以下の3つです。
・C/C++の追加のインクルードディレクトリ
   {OpenCVをインストールしたディレクトリ}\build\include
・リンカーの追加のライブラリディレクトリ
   {OpenCVをインストールしたディレクトリ}\build\x64\vc15\lib
・構成全般の出力ディレクトリ
   Flutterの出力先と同じにすると、いちいちコピーしなくていいので便利。
   たとえばデバッグモードなら以下の場所に設定する
   {Flutterのプロジェクトの格納されたディレクトリ}\build\windows\runner\Debug

さらにCPPのソースの先頭に以下の記述を入れます。

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ディレクトリに保存するものです。
image_batch.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ほどややこしくない。既存の専門的なライブラリを使って、ちょっと実験的なアプリケーションを作ったりするのに向いてると思います。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4