Cloud AutoML VisionとFlutterを組み合わせてサクッと画像認識アプリを作りたかったが作れなかった

こんにちは、この記事は、~~[Flutter Advent Calendar](https://qiita.com/advent-calendar/2019/flutter) 21日目の記事です。（2時間遅れてしまって、上書きされてしまいました。。。とほほ。）~~
[Flutter Advent Calendar #2](https://qiita.com/advent-calendar/2019/flutter-2) 23日目が空いていたので、埋めさせていただきました。


## 概要
以下の構成で画像認識アプリを作ります。
![flutter_automl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/950faae3-c02c-3fb6-91d3-7779f543fb23.png)

ポイントとしては、Cloud AutoML Visionから吐き出したTensorFlow Liteの形式のモデルを
Flutterで読み込ませて推論を行います。そのため、**オフラインでも画像認識が行うことができます**。

ただ、現状、Flutter内で、推論を行おうとすると、アプリがクラッシュして落ちてしまいます...
(本記事の最後に原因の推測と対応策をまとめています。)

<img width="500" alt="データセット_–_Vision_–_NullSuck_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/395a2afe-734d-6bc4-f0c5-f5eb2cbe2a2a.gif">

## Cloud AutoML Visionで画像認識モデルをつくる
早速Flutterではなく、GCPをいじります。

### AutoML APIの有効化

GCPのプロジェクト作成や請求先アカウントは作成済みの前提で進めます。
右のメニューから「Vision -> ダッシュボード」を選択すると、AutoML Visionのダッシュボードが開きます。

まず、ここでAutoML APIを有効化しましょう。

<img width="878" alt="Vision_–_NullSuck_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/b2aa444e-3369-34b8-6afb-fbe134700f53.png">

### データセットのラベリング

AutoML APIを有効化するとデータセットの画面が開きます。

<img width="862" alt="データセット_–_Vision_–_NullSuck_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/9ae57202-e3b8-7ded-304d-e11ad72dc12c.png">

ここで、画像を含んだデータセットを登録することができます。
今回は、以下のペットのデータセットを使ってみましょう。

[The Oxford-IIIT Pet Dataset](http://www.robots.ox.ac.uk/~vgg/data/pets/)

データセットをダウンロードすると以下のようなディレクトリ構成になっています。
一つのディレクトリにファイル名でラベリングしている形式です。

```
images
├── Abyssinian_1.jpg
├── Abyssinian_10.jpg
├── Abyssinian_100.jpg
...
```

AutoMLはファイル名ではなく、ディレクトリ名を元にラベリングを行うので、これだと一つのラベルしか付与されません。
そのため、ファイル名を元に以下のような形でディレクトリ毎にファイルを分けましょう。

自分は、人力でやりましたが、bash scriptなどを使うのがよいかと思います。

```
pets
├── Abyssinian
│   ├── Abyssinian_1.jpg
│   ├── Abyssinian_10.jpg
│   ├── Abyssinian_100.jpg
│    ...
├── American_bulldog
│   ├── american_bulldog_10.jpg
│   ├── american_bulldog_100.jpg
│   ├── american_bulldog_101.jpg
│    ...
├── Basset_Hound
│   ├── basset_hound_1.jpg
│   ├── basset_hound_10.jpg
│   ├── basset_hound_100.jpg
...
```

これをzipで固めてアップロードします。

### データセットのアップロード
Vision -> データセット から新しいデータセットを選択します。
<img width="373" alt="データセット_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/95affc35-7a0e-43d3-345b-ec525606bd9b.png">

データセット名には適当な名前を、モデルの目的は「単一ラベル分類」を使いましょう。
<img width="1164" alt="データセット_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/5bc0425d-42c8-c893-4221-322eaa16a1db.png">

「パソコンから画像をアップロード」で先程作成したzipファイルを選択。
画像はGoogle Cloud Storageに上がるので、バケットがない場合は、プロンプトに従ってバケットを作成しましょう。
既にGoogle Cloud Storageのバケットがある方はそれを選択します。

<img width="1012" alt="pets_1576730901896_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/62777098-afba-2a77-945e-b3d816cf2638.png">

画像のインポートは少々時間がかかるので他のことをして待ちましょう。
インポートが終わると、メールでお知らせしてくれます。
<img width="1027" alt="データセット_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/267160ac-92a2-125a-c797-02cf464fc07c.png">

インポートが終わるとステータスが変わります。
<img width="1030" alt="データセット_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/ecf36b16-7752-8675-4b14-4c904ae06782.png">
「警告: 画像のインポート」となったら取り込みが完了しています。
警告は一部の画像フォーマットが対応していないためで、特に気にしなくてよいです。

データセットをクリックして開くと、取り込まれた画像およびラベリングが確認できます。
<img width="911" alt="pets_1576636901735_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/44484692-ab10-2e66-69f5-401193266243.png">

### トレーニング
データセットを取り込み終わると学習を開始できます。
トレーニングから学習を開始しましょう。
<img width="823" alt="pets_1576730901896_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/26235870-1744-b07b-0826-867928221eb1.png">

モデルの定義以下のように選択します。

- モデルの定義: Edge
- モデル最適化のオプション: Best Trade-Off(精度と速さのバランスをとったもの)
- Node Hour Budget(トレーニングの上限時間): デフォルトで推測時間をセットしてくれてるのでそれを利用

ちなみに、Node Hour Budgetは最初の15時間は無料とのことです。

<img width="543" alt="新しいモデルのトレーニング_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/91d047d7-e0b3-3908-f2ee-c55b4a8d0b6e.png">

トレーニング開始を押すと、学習を開始します。
これも時間がかかるのでしばらく待ちましょう。
（自分は、2,3時間ほどで学習が完了しました。）

<img width="1286" alt="データセット_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/e19b53bc-fa82-346d-0480-10a8e61e6bb7.png">

モデルのトレーニングが完了するとモデルの評価も見ることができます。
<img width="1006" alt="pets_1576730901896_–_Vision_–_cloud-vision_–_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/e74ce5b9-7464-109b-7b70-66475c836ac3.png">

正答率は100%-90%を出せていて優秀ですね。

### TensorFlow Liteモデルのダウンロード
モデルができたので、TensorFlow Liteのモデルをダウンロードしましょう。
「テストと使用」をクリックすると様々な種類のモデルをダウンロードできます。

![pets_1576730901896 – Vision – cloud-vision – Google Cloud Platform - Google Chrome 2019-12-21 12.37.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/704dd513-73f3-92f0-e801-3496fec482c1.png)

この中から「TF Lite」を選択しましょう。クリックするとGoogle Cloud Storageにエクスポートするダイアログが出るので、エクスポートしましょう。
エクスポートすると、GCSの中に、tfliteとラベルのtxtファイルがあるのでそれをダウンロードします。（jsonファイルは利用しません。）
![バケットの詳細 - cloud-vision - Google Cloud Platform - Google Chrome 2019-12-21 13.04.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125528/b214a59b-6c59-81bd-76b2-70eb7c652367.png)

## Flutterで画像認識アプリをつくる
### モデルを配置する
ダウンロードしてきたTensorFlow Liteを`assets`フォルダに配置します。

```
assets
└── custom
    ├── dict.txt
    └── model.tflite
```

`pubspec.yml`にも以下の通り記載します。

```yml:pubspec.yml
# To add assets to your application, add an assets section, like this:
assets:
  - assets/custom/model.tflite
  - assets/custom/dict.txt
```

### 必要なプラグインを読み込む
FlutterでTensorFlow Liteのモデルを読み込めるプラグインがあるので、それを使ってみます。
https://github.com/shaqian/flutter_tflite

プロジェクトを作成して、`pubspec.yml`に以下のとおり、プラグインを追加しましょう。
(image_pickerは、画像を選択できるピッカーのプラグインです。)

```yml:pubspec.yml
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  tflite: 1.0.4
+  image_picker: 0.6.2+3
```

### 画面をつくる
`main.dart`で画面を作っていきます。
非常にシンプルなので、説明は省略します。

```dart:main.dart
import 'dart:async';
import 'dart:io';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Image Picker Example',
      theme: new ThemeData(
        primarySwatch: Colors.red,
      ),
      home: new MyHomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _HomePageState extends State<HomePage> {
  File _image;

  @override
  void initState() {
    super.initState();
  }

  Future getImage() async {
     var image = await ImagePicker.pickImage(source: ImageSource.gallery);

     setState(() {
       _image = image;
     });
   }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Picker Example'),
      ),
      body: Center(
        child: _image == null
            ? Text('No image selected.')
            : Image.file(_image),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: getImage,
        tooltip: 'Pick Image',
        child: Icon(Icons.add_a_photo),
      ),
    );
  }
}
```

### 立ち上げ時にモデルを読み込む
`_HomePageState`の`initState`時にモデルを読み込むように処理を追加します。
`Tflite`の`loadModel`メソッドを呼び出すと、モデルを読み込むことができます。

```dart:main.dart
 import 'package:flutter/material.dart';
 import 'package:image_picker/image_picker.dart';
+import 'package:tflite/tflite.dart';

 class MyHomePage extends StatefulWidget {
 (省略...)
    @override
    void initState() {
      super.initState();
+     loadModel();
   }

+  Future loadModel() async {
+    String res = await Tflite.loadModel(
+        model: "assets/custom/model.tflite",
+        labels: "assets/custom/dict.txt",
+    );
+    print(res);
+  }
 (省略...)
 }
```

### 画像選択時に推論するようにする
画像選択したときにモデルから推論を行うようにします。

```dart:main.dart
+  Future detectImage(File in_image) async {
+    var predictions = await Tflite.runModelOnImage(path: in_image.path);
+    print(predictions);
+  }

   Future getImage() async {
     var image = await ImagePicker.pickImage(source: ImageSource.gallery);
+    detectImage(image);

     setState(() {
       _image = image;
     });
   }
```

これで、アプリは完成しました。
ただし前述の通り、現状だと画像を読み込ませて、`runModelOnImage`時に**なにもログを吐かずに**アプリが終了します。

## 動かない原因の推測

現状、flutterの`tflite`プラグインがCloud AutoML Visionのモデルに対応していない可能性が高いです。
実際、モデルを`tflite`のリポジトリ内のサンプルモデルに差し替えて動かすと、動きます。

実際、[このIssue](https://github.com/shaqian/flutter_tflite/issues/78)でも「Cloud AutoML VisionのモデルはQuantized Modelのため利用できない可能性が高い」という旨の言及があります。


## 対応策
以下のような対応策が考えられます。

- `tflite`プラグインのコードを読んで、修正・プルリクを出す
- Cloud AutoML VisionのモデルをEdgeではなく、APIにして呼び出す
- [Firebase AutoML Vision Edge](https://firebase.google.com/docs/ml-kit/automl-image-labeling)と[firebase_ml_vision](https://pub.dev/packages/firebase_ml_vision)を利用する

最初からやり直すなら、最後のFirebase AutoML Vision Edgeを使うのがいいかな、と思いました。
Firebaseといっても、ほぼほぼGoogle Cloud版と機能は同じですし、Firebaseで勝手にモデルの更新もやってくれるみたいですし、非常に便利そうです。
次回はこれで、成功させてみたいな、と思ってます。

## 参考資料
[Real-Time Object Detection with Flutter, TensorFlow Lite and Yolo -Part 1](https://blog.francium.tech/real-time-object-detection-on-mobile-with-flutter-tensorflow-lite-and-yolo-android-part-a0042c9b62c6)
[Cross-Platform On-Device ML Inference TensorFlow Lite ft. Flutter](https://towardsdatascience.com/cross-platform-on-device-ml-inference-a55b67e306b3)