FlutterとFirebase ML Kitを使ってカンファレンス用デモアプリを作った話

DroidKaigiで展示したファッションチェックアプリについて

こんにちは。ZOZOテクノロジーズ開発部山田(@yshogo87)です。 DroidKaigi 2019ではプラチナスポンサーとして、ブースを出展させていただきました。

DroidKaigi 2019

f:id:vasilyjp:20190307173118j:plain

そのコンテンツとしてファッションチェックアプリを展示させていただきました。

f:id:vasilyjp:20190307173352j:plain

f:id:vasilyjp:20190307173443j:plain

今回はファッションチェックアプリがどのような仕組みになっているかを説明させていただきます。

ファッションチェックアプリとは

ファッションチェックアプリとは、ユーザーが撮影した全身の写真について、WEARに投稿されたコーディネートを元に作成した学習モデルを使用して採点を行うものになっています。

f:id:vasilyjp:20190307173514p:plain

技術的構成

技術的な構成は下記のようになっています。

フロントエンド: Flutter
バックエンド: Firebase(ML Kit、Cloud Firestore)、GCP(Cloud Vision API)

f:id:vasilyjp:20190307173533p:plain

このファッションチェックアプリは、別のイベントでも使う予定があり、iOSでも動かせるようにする必要があったため、クロスプラットフォームで開発できるFlutterを選択しました。 また、バックエンドもTensorFlow Liteで作成した学習済みモデルをFirebaseにアップロードすることで特別なAPIを作成することなく、SDK経由で簡単に使うことができるためFirebase ML Kitを選択しました。

Cloud Firestoreは検出された結果をログとして保存しています。

FlutterからFirebase ML Kitを使う

Firebaseの導入

FirebaseはFlutterから使用することができます。 導入手順はこちらの公式ページをご参照ください。

Firebase ML KitをFlutterで使う

Firebase ML KitをFlutterで使うためのプラグインもOSSで公開されています。

今回使用するFirebase ML Kit Custom Modelもこのプラグインに内包されています。このプラグインを使うと、AndroidとiOSを同時に一つのコードで動かすことができるので非常に便利です。

ただしCustom ModelをFlutterから使用する場合には一部状況で注意が必要です。今回はこのプラグインを使用せず実装しました。

FlutterからFirebase ML Kit Custom Modelを使うときの注意点

Custom ModelをFlutterから使用する場合、返却される型に注意が必要です。Dartではfloat型が存在しないので、Custom Modelからの結果がfloat型の場合うまく受け取れません。そこで、KotlinでFirebase ML Kitとのやりとり部分を書いてFlutter側に結果を返すコードを書きました。

実装

FlutterからKotlinのコードを実行する

次のように invokeMethod によってFlutterからKotlinのコードを呼び出し、その実行結果が result に返ってきます。

    const platform = const MethodChannel("firebaseCustomModel#run");
    try {
      final String result =
          await platform.invokeMethod("getResult", <String, dynamic>{
        'imageFile': cameraImage.path,
        'gender': widget.gender,
      }).catchError((err) {
        print("エラーが発生しました");
        setState(() {
          _isError = true;
        });
      });

次にKotlin側でFlutterからきたデータを受け取ります。 MethodChannel クラスを使ってコールバッククラスを設定することでFlutterから getResult という文字列でリクエストがくるとKotlin側で受け取れます。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)

        MethodChannel(flutterView, "firebaseCustomModel#run").setMethodCallHandler(object : MethodCallHandler {
            override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
                val camera = call.argument<String>("imageFile")
                val bitmap = BitmapFactory.decodeFile(camera)
                val gender = call.argument<Int>("gender")

                startCustomModel(bitmap, gender, result)
            }
        })
    }

Flutterから設定されたパラメータは下記のコードで取得しています。

val camera = call.argument<String>("imageFile")
val bitmap = BitmapFactory.decodeFile(camera)
val gender = call.argument<Int>("gender")

Flutterからネイティブのコードを呼び出す方法については公式ページがあるので詳しくはこちらをご覧ください。

Firebase ML Kitからデータを取得する

ここからはFirebase ML KitからCustom Modelをダウンロードし、結果を取得します。 下記のコードはFirebase ML Kitのドキュメントを参考に実装しています。

    fun startCustomModel(bitmap: Bitmap, gender: Int?, result: MethodChannel.Result) {

        var conditionsBuilder: FirebaseModelDownloadConditions.Builder = FirebaseModelDownloadConditions.Builder().requireWifi()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            conditionsBuilder = conditionsBuilder
                    .requireCharging()
                    .requireDeviceIdle()
        }
        val conditions = conditionsBuilder.build()

        val cloudSource = FirebaseCloudModelSource.Builder("batai")
                .enableModelUpdates(true)
                .setInitialDownloadConditions(conditions)
                .setUpdatesDownloadConditions(conditions)
                .build()
        FirebaseModelManager.getInstance().registerCloudModelSource(cloudSource)

        val modelName = if (gender == 0) {
            "wear_model"
        } else {
            "wear_model_women"
        }

        val options = FirebaseModelOptions.Builder()
                .setCloudModelName(modelName)
                .build()
        val firebaseInterpreter = FirebaseModelInterpreter.getInstance(options)

        val inputOutputOptions = FirebaseModelInputOutputOptions.Builder()
                .setInputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1, 224, 224, 3))
                .setOutputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1, 1))
                .build()

        val batchNum = 0
        val input = Array(1) { Array(224) { Array(224) { FloatArray(3) } } }
        for (x in 0..223) {
            for (y in 0..223) {
                val pixel = bitmap.getPixel(x, y)
                input[batchNum][x][y][0] = ((Color.red(pixel) - 128) / 128).toFloat()
                input[batchNum][x][y][1] = ((Color.green(pixel) - 128) / 128).toFloat()
                input[batchNum][x][y][2] = ((Color.blue(pixel) - 128) / 128).toFloat()
            }
        }

        val inputs = FirebaseModelInputs.Builder()
                .add(input)  // add() as many input arrays as your model requires
                .build()

        firebaseInterpreter!!.run(inputs, inputOutputOptions)
                .addOnFailureListener { e ->
                    // Task failed with an exception
                    // ...
                    e.printStackTrace()
                }.continueWith { task ->
                    val labelProbArray = task.result!!.getOutput<Array<FloatArray>>(0)
                    result.success(getTopLabels(labelProbArray)[0])
                }
    }

    private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> {
        val data = labelProbArray[0][0]
        val list = ArrayList<String>()
        list.add("$data")
        return list
    }

下記のコードでは、先ほどの説明した通りFlutterではfloat型を受け取れなかったので、KotlinでString型に変換してFlutter側に返却しています。

val labelProbArray = task.result!!.getOutput<Array<FloatArray>>(0)
                    result.success(getTopLabels(labelProbArray)[0])


 private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> {
        val data = labelProbArray[0][0]
        val list = ArrayList<String>()
        list.add("$data")
        return list
 }

Cloud Vision APIを叩く

開発期間内では学習済みモデルの精度が高められるか不安があったので、Cloud Vision APIも叩いて加点することにしました。

Cloud Vision APIでは写真の情報から写っている画像に対して得られる情報を返すGCPのサービスの1つです。

cloud.google.com

ファッションチェックアプリでは、Cloud Visionから「cool」や「fashion」などのファッションチェックとしてプラスになりそうなキーワードが検出されると加点しています。 加点するキーワードについてはCloud Firestoreで管理しています。

Cloud Vision APIを叩くコードは下記になります。

  _requestCloudVision(File cameraImage, String result) async {
    String url = "https://vision.googleapis.com/v1/images:annotate";
    String apiKey = "api key";

    List<int> imageBytes = cameraImage.readAsBytesSync();

    Map json = {
      "requests": [
        {
          "image": {"content": base64Encode(imageBytes)},
          "features": [
            {
              "type": "LABEL_DETECTION",
              "maxResults": 100,
              "model": "builtin/stable"
            }
          ],
          "imageContext": {"languageHints": []}
        }
      ]
    };

    Response response = await http.post(url + "?key=" + apiKey,
        body: jsonEncode(json), headers: {"Content-Type": "application/json"});

    var body = response.body;
    print(body);
    var bodyJson = jsonDecode(response.body);
    List<dynamic> responces = bodyJson["responses"];
    if (responces == null || responces.length == 0) {
      _showErrorDialog("Label not found");
      return;
    }
    Map<String, dynamic> labelAnnotations = responces[0];

    if (labelAnnotations != null && labelAnnotations.length != 0) {
      List<LabelAnnotationModel> list = [];
      for (dynamic label in labelAnnotations["labelAnnotations"]) {
        LabelAnnotationModel model = LabelAnnotationModel.fromJson(label);
        list.add(model);
      }
    }
  }

このコードではlistに検出されたラベル一覧が格納される方法になります。

結果はログとしてCloud Firestoreに保存

撮影した写真以外のデータはCloud Firestoreに保存していてリアルタイムでログを見れるようにするためのアプリも実装しました。 こちらのアプリでは、Flutterだけで実装しているため、AndroidでもiOSの動かすことができます。

ログとして保存したのは下記です。

  • 男女ごとのスコア
  • Cloud Visionから検出されるキーワード
  • キーワードにヒットした時の加点数

f:id:vasilyjp:20190307173617p:plain

Firebase ML Kitから返却される数値の平均点と、Cloud Vision APIからよく出力されるキーワードを監視していました。

開発当初、学習済みモデルの出来が悪く全員が同じ点数になることが危惧されたのでCloud Vision APIからキーワードで加点するように対策をしていましたが、結果的に危惧していた事態は起きず、この機能の出番はありませんでした。

f:id:vasilyjp:20190307173634p:plain

まとめ

本記事ではファッションチェックアプリの仕組みと、Firebase ML Kit、 Cloud Vision APIの簡単な使い方について紹介させていただきました。

ZOZOテクノロジーズでは、技術でファッションを盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

www.wantedly.com

www.wantedly.com

カテゴリー