DroidKaigiで展示したファッションチェックアプリについて
こんにちは。ZOZOテクノロジーズ開発部山田(@yshogo87)です。 DroidKaigi 2019ではプラチナスポンサーとして、ブースを出展させていただきました。
そのコンテンツとしてファッションチェックアプリを展示させていただきました。
今回はファッションチェックアプリがどのような仕組みになっているかを説明させていただきます。
ファッションチェックアプリとは
ファッションチェックアプリとは、ユーザーが撮影した全身の写真について、WEARに投稿されたコーディネートを元に作成した学習モデルを使用して採点を行うものになっています。
技術的構成
技術的な構成は下記のようになっています。
フロントエンド: Flutter バックエンド: Firebase(ML Kit、Cloud Firestore)、GCP(Cloud Vision API)
このファッションチェックアプリは、別のイベントでも使う予定があり、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 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から検出されるキーワード
- キーワードにヒットした時の加点数
Firebase ML Kitから返却される数値の平均点と、Cloud Vision APIからよく出力されるキーワードを監視していました。
開発当初、学習済みモデルの出来が悪く全員が同じ点数になることが危惧されたのでCloud Vision APIからキーワードで加点するように対策をしていましたが、結果的に危惧していた事態は起きず、この機能の出番はありませんでした。
まとめ
本記事ではファッションチェックアプリの仕組みと、Firebase ML Kit、 Cloud Vision APIの簡単な使い方について紹介させていただきました。
ZOZOテクノロジーズでは、技術でファッションを盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!