ML Kitで私物を認識してもらうまで(Kotlin)

こんにちは
東海8期メンターだすです。

突然ですが、流行りの機械学習を駆使したアプリ……作りたいですよね？
画像解析の中でもリアルタイム画像ラベリングいいなと思い、今回はAndroid上にML Kitを使って私物を認識させるまでやってみました。

初めてML Kitを使ってAndroidアプリを作ってみます。
色々調べてわかったことも載せていくので機械学習入門としていいかもです。

#今回の目標
**私物をカメラで撮影してリアルタイムに認識してもらいたい！**

机の上にあった
ちっちゃいマティーニと
ちっちゃいシーブリーズ

それぞれをリアルタイムで識別させようと思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/07ddfb65-dd9a-2741-d987-f23e5238e677.jpeg" width=50%>

#ML Kitとは？
Googleが提供する機械学習のためのAndroid・iOSアプリ用SDK
[公式ガイド](https://developers.google.com/ml-kit/guides?hl=ja)
>ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。その強力で使いやすいパッケージは、機械学習の経験の有無を問わず、わずか数行のコードで実装できます。ニューラル ネットワークやモデルの最適化に関する詳しい知識は必要ありません。経験豊富な ML のデベロッパーの方は、ML Kit の便利な API を利用することで、モバイルアプリにおいてカスタム TensorFlow Lite モデルを簡単に採用できます。
[Firebaseドキュメントより](https://firebase.google.com/docs/ml-kit?hl=ja)

**アプリへの実装は数行でできて、モデルの学習はクラウド上でできちゃうというスゴ過ぎSDK**

とりあえずで実機でどんな感じか知りたかったら公式のサンプルが便利でした！
[ML Kit Vision公式サンプル](https://github.com/googlesamples/mlkit/tree/master/android/vision-quickstart)

#やること
1. カメラ
2. ML KitでImage Labeling
3. 好きな学習済みモデルを使って見る
4. 私物を学習させる

#1. カメラ
カメラを使って撮影しているものをリアルタイムに解析したいので今回は、Androidでめちゃ簡単にカメラを使えるようになるライブラリ**CameraX**を使いました。

##CameraX
[公式のチュートリアル](https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0)に沿って実装しました。これがホントよくできているので手順の記載は省略します。チュートリアルのまんまです。

CameraXは、カメラプレビュー、写真撮影、画像解析機能それぞれをusecaseによって実装することができます。今回はカメラプレビューと画像解析機能を使います。
まずはカメラプレビューと画像解析の途中まで実装しました。
一応コード載せておきますね。

###コード
```gradle:build.gradle
dependencies {
    // 以下を追加
    // CameraX
    implementation "androidx.camera:camera-camera2:1.0.0-beta07"
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta07"
    implementation "androidx.camera:camera-view:1.0.0-alpha14"
}
```

```xml:Manifest.xml
    <!--以下を追加-->
    <uses-permission android:name="android.permission.CAMERA" />
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!--以下を追加-->
    <!--カメラプレビューを表示するView-->
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var cameraExecutor: ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener(Runnable {
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // Preview
            val preview = Preview.Builder()
                    .build()
                    .also {
                        it.setSurfaceProvider(viewFinder.createSurfaceProvider())
                    }
            // imageAnalyzer
            val imageAnalyzer = ImageAnalysis.Builder()
                    .build()
                    .also {
                        // ここ重要
                        // ここに画像解析をセット
                    }

            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                // ここも重要
                // カメラのライフサイクルに画像解析を組み込む
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)

            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    override fun onRequestPermissionsResult(
            requestCode: Int, permissions: Array<String>, grantResults:
            IntArray) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                        "Permissions not granted by the user.",
                        Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    companion object {
        private const val TAG = "CameraXBasic"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }
}
```
ここまででとりあえず実行はできて、カメラプレビューが表示されていると思います。

#2. ML KitでImage Labeling
私物を学習させる前に、まずは標準の学習済みモデルを使ってみます。

[公式ドキュメント](https://developers.google.com/ml-kit/vision/image-labeling/android)に沿って実装してみました。
処理の流れとしては、
1. モデルに渡す画像を用意
2. 画像ラベラーに画像を渡す
3. 解析結果をログに出力

学習済みモデルをアプリに埋め込むか動的にダウンロードするか選べるんですが、今回は手順を省略するために埋め込む方でいきます。
画像解析処理は、CameraXのImageAnalysisを使いました。

###コード
```gradle:build.gradle
dependencies {
    // 追加
    // ML Kit Label Images
    implementation 'com.google.mlkit:image-labeling:17.0.0'
}
```

```kotlin:MainActivity.kt
    // 以下を追加
    // ML Kit Label Images
    private class LabelAnalyzer : ImageAnalysis.Analyzer {
        override fun analyze(imageProxy: ImageProxy) {
            val mediaImage = imageProxy.image
            if (mediaImage != null) {
                val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
                // 画像をラベラーに渡す
                val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
                labeler.process(image)
                        .addOnSuccessListener { labels ->
                            // Task completed successfully
                            // ラベリング結果をログに出力
                            for (label in labels) {
                                val text = label.text
                                val confidence = label.confidence
                                val index = label.index
                                Log.d(TAG, "Label: $text, $confidence, $index")
                            }
                        }
                        .addOnFailureListener { e ->
                            // Task failed with an exception
                            Log.e(TAG, "Label: $e")
                        }
                        // ImageProxyは使い終わったら閉じましょう
                        .addOnCompleteListener { results -> imageProxy.close() }
            }
        }
    }
}
```
```kotlin:MainActivity.kt
    private fun startCamera() {
        // 省略
        // imageAnalyzer
        val imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also {
                    // ここに追加
                    it.setAnalyzer(cameraExecutor, LabelAnalyzer())
                }
        // 省略
    }
```
##動作確認
動かしてみます。
PCを撮影してみると
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/31e6ca1f-47d2-f7be-8605-4514aa93c347.jpeg" width=50%>
こんな結果でした。
楽器とも認識されてますね笑

```:Log
2020-12-10 18:11:53.427 20954-20954/com.norihiro.mllabelimages D/CameraXBasic: Label: Musical instrument, 0.90898794, 251
2020-12-10 18:11:53.427 20954-20954/com.norihiro.mllabelimages D/CameraXBasic: Label: Mobile phone, 0.58168525, 257
2020-12-10 18:11:53.427 20954-20954/com.norihiro.mllabelimages D/CameraXBasic: Label: Computer, 0.5566468, 89
```


##3.好きな学習済みモデルを使って見る
次は標準のじゃなくて別の学習済みモデルを使ってみようと思います。
学習済みモデルの場合は、TensorFlowLite形式の物が使えるみたいです。

[TensorFlow Hub](https://tfhub.dev/s?deployment-format=lite&module-type=image-classification)に色んな学習済みモデルがありました。ちょっと一個試してみます。

[food_v1]:https://tfhub.dev/google/lite-model/aiy/vision/classifier/food_V1/1
[食べ物を学習したモデル][food_v1]がありました。
ウェブページ上で画像認識かけることもものによってはできて、試しに自分で撮ったローストビーフの写真を解析にかけました。
ちゃんと識別されててすごい
![スクリーンショット 2020-12-05 16.57.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/af1dd6f2-aced-d015-9f9a-b356e4b79732.png)
というわけで、このモデルを組み込んでみます。
先までの画像解析とほとんど同じ感じで実装できました。

カスタムモデルを使う際にやることは、
1. モデル追加
2. 画像解析処理
###1. モデル追加
[さっきの食べ物識別モデル][food_v1]をダウンロードします。
プロジェクトにassetsフォルダを作ってその下にダウンロードした.tfliteファイルを配置。
こんな状態です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/047d92ba-6adf-5aac-3a56-e5530a6dddb5.png" width=50%>
###コード
```gradle:build.gradle
dependencies {
    // 以下を追加
    // Image labeling feature with custom classifier support
    implementation 'com.google.mlkit:image-labeling-custom:16.2.1'
}
```

.tfliteファイルが圧縮されるのを防ぐため、この設定がされていることを確認しましょう。

```gradle:build.gradle
android {
    // 省略
    aaptOptions {
        noCompress "tflite"
    }
}
```
###2. 画像解析処理
また同じ様にImageAnalysis.Analyzerを作っていきます。

モデルはAssetsフォルダに配置してあるはずなのでそこから読み込み
ラベラーの設定を決め、画像解析にラベラーを作ります。

| 設定 |  |
|:-----------------------------:|:-:|
|.setConfidenceThreshold(Float) | 一致率の閾値。設定するとこの値以上の一致率のみ結果に出力される。  |
|.setMaxResultCount(Int)        | 結果の最大個数。  |

###コード
```kotlin:MainActivity.kt
    // Food Classifier Image Labeling Model
    private class FoodLabelAnalyzer : ImageAnalysis.Analyzer {
        // モデルの読み込み
        val localModel = LocalModel.Builder()
                .setAssetFilePath("lite-model_aiy_vision_classifier_food_V1_1.tflite")
                .build()
        val customImageLabelerOptions =
                CustomImageLabelerOptions.Builder(localModel)
                        .setConfidenceThreshold(0.1f)
                        .setMaxResultCount(5)
                        .build()

        override fun analyze(imageProxy: ImageProxy) {
            val mediaImage = imageProxy.image
            if (mediaImage != null) {
                val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
                // Pass image to food classifier model
                val imageLabeler = ImageLabeling.getClient(customImageLabelerOptions)
                imageLabeler.process(image)
                        .addOnSuccessListener { labels ->
                            // Task completed successfully
                            // ラベリング結果をログに出力
                            for (label in labels) {
                                val text = label.text
                                val confidence = label.confidence
                                val index = label.index
                                Log.d(TAG, "Food Label: $text, $confidence, $index")
                            }
                        }
                        .addOnFailureListener { e ->
                            // Task failed with an exception
                            Log.e(TAG, "Food Label: $e")
                        }
                        .addOnCompleteListener { results -> imageProxy.close() }
            }
        }
    }
}
```
###動作確認
ちゃんとグミを認識してますね
すごい
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/fd7ee19e-f4f1-6ae6-17fe-895199e102a0.jpeg" width=50%>

```:Log
2020-12-11 15:43:49.935 32454-32454/com.norihiro.mllabelimages D/CameraXBasic: Food Label: Jelly bean, 0.77734375, 232
2020-12-11 15:43:50.203 32454-32454/com.norihiro.mllabelimages D/CameraXBasic: Food Label: Jelly bean, 0.83203125, 232
2020-12-11 15:43:50.486 32454-32454/com.norihiro.mllabelimages D/CameraXBasic: Food Label: Jelly bean, 0.640625, 232
```

#4. 私物を学習させる
さあここまで来ました。
私物を認識してもらうためには、それを学習した**TensorFlow Liteモデル**があればいいということがここまででわかりました。

モデルを作る方法ですが、[TensorFlowLiteモデルメーカー](https://www.tensorflow.org/lite/tutorials/model_maker_image_classification)で作ることができ、GoogleColaboratoryで実行できる様です。

##GoogleColaboratory
https://colab.research.google.com/notebooks/intro.ipynb
GoogleのGPUを使ってPythonを動かすことができるサービスです。
文章中にコードを埋め込みさらに実行ができるという、使いこなせば実行できる資料として最強そう
##TensorFlowliteモデルメーカーでモデル作り
[公式チュートリアル](https://www.tensorflow.org/lite/tutorials/model_maker_image_classification)を元に自分のモデルを作ってみました。
自分のGoogleColaboratoryで実行する形で進めます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/a786d9c8-7dc1-9416-390d-1978f2a7c79b.png" width=50%>
自分のGoogleColaboratoryに移るとこんな画面になっていると思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/f1e88d0d-8e04-2bf6-0826-34761373832e.png" width=50%>
灰色背景のコードを上から順番に実行して行けばモデル学習が終わります。
デフォルト状態では、花の画像を学習する様になっていました。

でも、今回は私物を学習させたいので渡す画像だけ変えて実行しました。

まず、今回学習させたいものの単体の画像をそれぞれ１０枚ずつ撮ります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/4e9ac5e4-3b7e-c2f3-cf20-4981047f70d8.jpeg" width=50%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/04b10b55-512c-8a04-c4d0-1796eb2c8c6b.jpeg" width=50%>
こういう感じで各10枚
フォルダにそれぞれまとめて圧縮
![スクリーンショット 2020-12-11 17.13.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/dc88aa09-9fc1-ee3b-135a-6285f74747f4.png)
GoogleColaboratoryにアップロードして、コードから解凍
アップも解凍もちょっと時間がかかるので焦らず待ちましょう。
![スクリーンショット 2020-12-11 0.02.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/73a2bd98-7eda-a6a0-525c-2536fc8eeda1.png)
学習時に参照される画像のパスimage_pathがアップロードした画像フォルダになっている様に注意しましょう。
![スクリーンショット 2020-12-11 0.03.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/2e0bdfbb-bdaf-5954-087a-46057a7af303.png)
あとは、順番にコードを実行して行けばいいのですが、画像の枚数が少ないとデフォルトだとエラーが出るようです。
自分はbatch_sizeをデフォルトより少ない１８に設定して進めました。
![スクリーンショット 2020-12-11 0.46.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/4add241f-87f8-6a64-2b67-b9ff8fbde13e.png)
model.exportまで実行すると、完成した学習済みモデルが現れます。
これをダウンロードしましょう。
これでモデルの準備は終わりです！
![スクリーンショット 2020-12-11 0.47.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/6cf1e175-7e72-81f4-56a7-ef7ef6d8c86c.png)

##作ったモデルを実装
ここまでで自分のオリジナル学習済みモデルが.tfliteファイルで用意できているはずです！
あとは、同じようにプロジェクトに追加し、画像解析処理を書いていきましょう。
startCamera()でAnalyzerをセットするのもお忘れずに
###コード
![スクリーンショット 2020-12-11 17.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/31ed5e69-0aec-3d14-53fc-9bbb52e3b2d6.png)


```kotlin:MainActivity.kt
    // My Classifier Image Labeling Model
    private class MyLabelAnalyzer : ImageAnalysis.Analyzer {

        val localModel = LocalModel.Builder()
                .setAssetFilePath("model.tflite")
                .build()
        val customImageLabelerOptions =
                CustomImageLabelerOptions.Builder(localModel)
                        .setConfidenceThreshold(0.1f)
                        .setMaxResultCount(5)
                        .build()

        override fun analyze(imageProxy: ImageProxy) {
            val mediaImage = imageProxy.image
            if (mediaImage != null) {
                val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
                // Pass image to food classifier model
                val imageLabeler = ImageLabeling.getClient(customImageLabelerOptions)
                imageLabeler.process(image)
                        .addOnSuccessListener { labels ->
                            // Task completed successfully
                            for (label in labels) {
                                val text = label.text
                                val confidence = label.confidence
                                val index = label.index
                                Log.d(TAG, "My Label: $text, $confidence, $index")
                            }
                        }
                        .addOnFailureListener { e ->
                            // Task failed with an exception
                            Log.e(TAG, "My Label: $e")
                        }
                        .addOnCompleteListener { results -> imageProxy.close() }
            }
        }
    }
}
```
##動作確認
ここまで実装終わった状態でそれぞれを撮影しつつログに出力されるか確認しました。
無事それぞれを映した時に識別されていることがわかります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/1259b3b8-cba1-43f4-388a-e0f83429cf8a.jpeg" width=50%>

```:Log
2020-12-11 17:26:16.399 8822-8822/com.norihiro.mllabelimages D/CameraXBasic: My Label: martini, 0.88119626, 0
2020-12-11 17:26:16.399 8822-8822/com.norihiro.mllabelimages D/CameraXBasic: My Label: seabreeze, 0.11880376, 1
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/c68663b2-b5c1-fe21-6060-4905d6929b39.jpeg" width=50%>

```:Log
2020-12-11 17:26:35.738 8822-8822/com.norihiro.mllabelimages D/CameraXBasic: My Label: seabreeze, 0.7540643, 1
2020-12-11 17:26:35.738 8822-8822/com.norihiro.mllabelimages D/CameraXBasic: My Label: martini, 0.24593572, 0
```

ただ、なんでもないところを映している時も0.6くらいの値が出ているので精度はまだあんまりなのかもです。
学習させた写真の枚数も最低限だったので、もっとたくさん画像を渡せば改善できそうですね。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323458/f9095b34-7645-93be-c996-faaf28ca5757.jpeg" width=50%>

```:Log
2020-12-11 17:51:16.905 10951-10951/com.norihiro.mllabelimages D/CameraXBasic: My Label: martini, 0.6346264, 0
2020-12-11 17:51:16.906 10951-10951/com.norihiro.mllabelimages D/CameraXBasic: My Label: seabreeze, 0.3653737, 1
```
#終わりに
今回、ML Kitで画像解析したさから進めていったことをまとめてみました。
ちょうどこの記事を書いている時に、公式ドキュメントのAutoMLVisionEdge項目がDeprecatedになってびっくりしました。
[AndroidStudio4.1でTensorFlowLiteのインポート](https://developer.android.com/studio/releases/#4.1-tensor-flow-lite-models)が追加されたり、モデルの参照の仕方はまた変わりそうですね。

GoogleColaboratoryを使えばモデルの学習もできたり、機械学習を活用したサービスも作りやすくなりそうです。
自分も何か作ってみようと思います。

#参考
[ML Kit（公式ドキュメント）](https://developers.google.com/ml-kit/vision/image-labeling)
[Androidで始める機械学習(TensorFlow Lite, ML Kit)](https://qiita.com/marchin_1989/items/517885984167e6855c13#fnref1)
[[kotlin]アンドロイドでリアルタイム画像認識アプリをつくる](https://qiita.com/YS-BETA/items/cd412524932dda9ac44c)
[GoogleColaboratory](https://colab.research.google.com/notebooks/intro.ipynb)