カメラ映像を出さずに画像データを取得&画像解析

本記事は[Android Advent Calendar 2021](https://qiita.com/advent-calendar/2021/android)の2021/12/01分です。

# はじめに
最近流行りの**AI[^1]を用いスマホのカメラから取得した映像に対して処理するアプリ**を作りたいときに、サンプルを探すと基本的にカメラで撮影した映像に処理結果をオーバーレイした画面が表示されたものばかりでてきます。
しかし、スマホのディスプレイにメインコンテンツ(≠カメラ映像)を映しつつスマホのカメラで撮影した映像をAIで処理したいというニーズもそこそこあると思います。
あやふやな記憶なんですが、5,6年ほど前はカメラを使用する際にはカメラから取得した映像を画面に表示することが必須で、表示しない場合はカメラ機能は使えず仕方なく画面に1pxだけ表示する`SurfaceView`を使ったりしたような記憶があります。
しかしこのAI[^1]全盛時代においてさすがにカメラ映像を出さずにも使えるだろうと思い試してみました。

# 結論
CameraX APIの`ImageAnalysis.Analyzer`を使うことでできます。


# 実際にやってみる
それでは実際にやってみます。
今回は[TensorFlow Lite](https://www.tensorflow.org/lite?hl=ja)を用いてUI上のボタンを押すとカメラに写った物体を判別してみます。
AIモデルに関してはTensorFlow Liteの[Example](https://www.tensorflow.org/lite/examples/object_detection/overview)で公開されているものを使用しました。
TensorFlow Lite及びモデルともに今回の本題ではないので詳しい説明はないです。


## 1.TensorFlow LiteとCameraXのインポート
モジュールのbuild.gradleに追加するだけです。
ついでにパッケージング時にモデルまで圧縮されると当然動かなくなってしまうので、モデルファイルを圧縮しない設定もしておきます。

```gradle:build.gradle
~~~~ 略 ~~~~~
android {
~~~~ 略 ~~~~~
    aaptOptions {
        noCompress "tflite"
    }

}

~~~~ 略 ~~~~~
dependencies {

~~~~ 略 ~~~~~

    implementation 'org.tensorflow:tensorflow-lite:2.7.0'
    implementation 'org.tensorflow:tensorflow-lite-gpu:2.7.0'
    implementation 'org.tensorflow:tensorflow-lite-support:0.3.0'

    def camerax_version = "1.0.2"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
}
```



## 2.TensorFlow Lite用のモデルの取り込み
[TensorFlow Lite](https://www.tensorflow.org/lite?hl=ja)では、学習済みモデルがいくつか公開されています。
今回はその中の[Object Detection](https://www.tensorflow.org/lite/examples/object_detection/overview)(物体検出)をやってみます。

まずはモデルが[公開](https://tfhub.dev/tensorflow/lite-model/ssd_mobilenet_v1/1/metadata/2?lite-format=tflite)されているのでダウンロードします。

次にダウンロードしたモデル（lite-model_ssd_mobilenet_v1_1_metadata_2.tflite）をAndroidプロジェクトにインポートします。
インポート方法はAndroidStudioでインポートしたいmoduleを右クリック->`New`->`Other`->`TensorFlow Lite Model`をクリック。


![スクリーンショット 2021-11-30 15.55.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/ef63f66d-877e-44f6-635a-5afdcc8b0e81.png)

クリックするとどのモデルをインポートするか聞かれるので、先程ダウンロードしたファイルを選択します。

![スクリーンショット 2021-11-30 20.09.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/ce03aa0e-c59b-cc14-25cb-1ecaa70ba7df.png)


`Finish`を押した後下記のようにモデルの詳細が出てくれば取り込み成功です。
![スクリーンショット 2021-11-30 20.10.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/2ae694e0-27d4-6667-7f37-fa5d37f206c4.png)

## 3.CameraXを用いてデータを取得する
あとは、CameraXを用いてデータを取得しAIモデルに流し込むという流れになります。
まずはCameraXで画像データを取得するところをやってみます。

### まずは意外と忘れるAndroidManifestにカメラのPermissionを記載

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.CAMERA" />
```

### アプリを起動したタイミング等でカメラのPermissionをユーザーから取得
今回は[PermissionsDispatcher]()を用いて、起動したら直ぐにユーザーから取得します。（挙動確認アプリなので特に取得できなかったときなどは考えません)

```kotlin:MainActivity.kt
@RuntimePermissions
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        hogehogeWithPermissionCheck() <- ここで取得してしまう
~~~~ 略 ~~~~
    }

    @NeedsPermission(Manifest.permission.CAMERA)
    fun hogehoge() {
        // DO nothing
    }
```



### Camera Providerの取得
此処から先は~~おまじない~~CameraXを使うボイラープレートの意味合いが結構強いです。

```kotlin:MainActivity.kt

    private val cameraExecutor by lazy {
        Executors.newSingleThreadExecutor()
    }

    private suspend fun getCameraProvider(): ProcessCameraProvider =
        suspendCoroutine { continuation ->
            ProcessCameraProvider.getInstance(this).apply {
                processCameraProvider = this
                addListener({
                    continuation.resume(get())
                }, cameraExecutor)
            }
        }
```

### 使用するカメラの指定
今回はカメラは背面固定

```kotlin:MainActivity.kt

    private fun buildCameraSelector(): CameraSelector = CameraSelector.Builder()
        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
        .build()
```

### CameraXにデータの利用目的を画像解析に設定
今回はカメラデータの利用目的が画像解析(Analyze)なのでImageAnalysisを使います。
CameraX APIでは、カメラで取得した画像映像データをどのような目的で使用するか(UseCaseと呼ぶ)を指定する必要があります。
現在は下記の3つのUseCaseが用意されています。

- プレビュー: プレビュー表示用
- 画像解析: 画像解析用
- 画像キャプチャ: 写真保存用

これらのUseCaseは複数個組み合わせることができますが今回はあえてここでImageAnalysis(画像解析)だけを使用することでプレビュー無しでのカメラ利用ができるというわけです。
~~ある意味この記事はここ以外・・・~~

また、ここでどの程度のクオリティの画像を取得するかの指定なども行います。

```kotlin:MainActivity.kt

    private val imageAnalyzer by lazy {
        ImageAnalysis.Builder()
            .setTargetResolution(Size(640, 480))
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build()
    }
```

### カメラの画像データの取得
上記で作成したImageAnalysisインスタンスに対して、setAnalyzerメソッドを呼ぶことでカメラから画像データを取得します。
コールバックを登録し、取得できたら呼ばれるタイプなのでメソッドを呼んだ瞬間のデータが取れるわけではないというところは注意です。

```kotlin:MainActivity.kt

    private suspend fun setAnalyzer(executor: Executor): ImageProxy {
        return suspendCoroutine { continuation ->

            imageAnalyzer.setAnalyzer(executor, { image ->
                continuation.resume(image)
            })
        }
    }
```

### 上記をまとめて実行
CameraXまわりのAPIはコールバックが多く、そのまま実装するとコールバック地獄になりがちなので、KotlinCoroutineのsuspendCoroutineを使って単純なメソッドっぽくラップしています。
コードは載せていませんが、適当なボタンを作って押されたらこのメソッドを呼べばいいと思います。

```kotlin:MainActivity.kt

    @SuppressLint("UnsafeOptInUsageError")
    private fun doJudge() {
        var resultImage: ImageProxy? = null
        lifecycleScope.launch {
            try {
                cameraProvider = getCameraProvider()
                val selector = buildCameraSelector()
                val camera = cameraProvider?.bindToLifecycle(
                    this@MainActivity,
                    selector,
                    imageAnalyzer
                ) ?: return@launch

                resultImage = setAnalyzer(cameraExecutor)
                // ここで取れた resultImage の中に画像データが入っている

                resultImage!!.close()
                resultImage = null
                cameraProvider?.unbindAll()
                cameraProvider = null
            } catch (e: Exception) {
            } finally {
                cameraProvider?.unbindAll()
                cameraProvider = null
                resultImage?.close()
            }
        }
    }

```

## 4.TensorFlow Liteを使って画像解析
やっとカメラから画像データが取れたのでこれの中に何が写っているかを解析したいと思います。

### モデルが受け取れる形式に変換
CameraXから取得できる画像データは*Image*(android.media.Imageクラス)形式なのですが、これをそのまま先程取り込んだモデルに入力として渡すことができません。
そこでまずはモデルに入力できる形式に変換します。
また、今回のモデルは、300px x 300px以下の画像サイズでのインプットになるので、それに合わせた縮小処理も必要となります。
AndroidのTensorFlow LiteのサポートライブラリにはBitmapからの入力をサポートしてくれるメソッドがあるので今回は300px x 300px以下のBitmap画像に変換する処理を行います。

```kotlin:MainActivity.kt

    private fun toBitmap(image: Image): Bitmap? {
        val planes: Array<Image.Plane> = image.planes
        val yBuffer: ByteBuffer = planes[0].buffer
        val uBuffer: ByteBuffer = planes[1].buffer
        val vBuffer: ByteBuffer = planes[2].buffer
        val ySize: Int = yBuffer.remaining()
        val uSize: Int = uBuffer.remaining()
        val vSize: Int = vBuffer.remaining()
        val nv21 = ByteArray(ySize + uSize + vSize)
        yBuffer.get(nv21, 0, ySize)
        vBuffer.get(nv21, ySize, vSize)
        uBuffer.get(nv21, ySize + vSize, uSize)
        val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
        val out = ByteArrayOutputStream()
        yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 75, out)
        val imageBytes: ByteArray = out.toByteArray()
        val croppedBitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
        val src = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
        // 直接縮小したBitmapを作る方法がなさそうなのでCanvas経由で作る
        val canvas = Canvas(croppedBitmap)
        val matrix = getTransformationMatrix(src.width, src.height, 300, 300)
        canvas.drawBitmap(src, matrix, null)
        return croppedBitmap
    }

    // 縮小率を求めてCanvasに書き込むときの倍率指定Matrixを作る
    private fun getTransformationMatrix(
        srcWidth: Int,
        srcHeight: Int,
        dstWidth: Int,
        dstHeight: Int
    ): Matrix {
        val matrix = Matrix()
        val scaleFactorX = dstWidth.toFloat() / srcWidth.toFloat()
        val scaleFactorY = dstHeight.toFloat() / srcHeight.toFloat()
        val scaleFactor = min(scaleFactorX, scaleFactorY)
        matrix.postScale(scaleFactor, scaleFactor)
        return matrix
    }
```

### モデルにデータを入力&出力取得
モデルにデータが入れれるようになったので、そのまま渡します。
AndroidでTensorFlow Liteのモデルを使用する場合、AndroidStudioの機能でモデルを取り込んでおくと取り込んだモデルの使用Sampleコードを見ることができます。
モデル取り込み時に表示された詳細部分の下部の方に記載があり、閉じてしまっても再度モデルをダブルクリックすることで見ることができます。
モデルは取り込んだモジュールの`srd`->`main`(ここは取り込み時の設定で変わる)->`ml`フォルダに入っています。
今回使用したモデルのSampleコードは下記のようです。

```kotlin:sample.kt
val model = LiteModelSsdMobilenetV11Metadata2.newInstance(context)

// Creates inputs for reference.
val image = TensorImage.fromBitmap(bitmap)

// Runs model inference and gets result.
val outputs = model.process(image)
val detectionResult = outputs.detectionResultList.get(0)

// Gets result from DetectionResult.
val location = detectionResult.locationAsRectF;
val category = detectionResult.categoryAsString;
val score = detectionResult.scoreAsFloat;

// Releases model resources if no longer used.
model.close()

```

これを参考に実際のコードを作ります。

```kotlin:MainActivity.kt
    private fun analyze(image: Image) {
        var model: LiteModelSsdMobilenetV11Metadata2? = null
        try {
            val bitmap = toBitmap(image)
            model = LiteModelSsdMobilenetV11Metadata2.newInstance(applicationContext)
            val tensorImage = TensorImage.fromBitmap(bitmap)
            val outputs = model.process(tensorImage)
            val list = outputs.detectionResultList
            list.filter {
                it.scoreAsFloat > 0.5f
            }.map {
                // ここに判別結果が流れてくる
            }
        } catch (e: Exception) {
        } finally {
            model?.close()
            model = null
        }
    }
```

あとはこのanalyzeメソッドをカメラのデータ取得部につなげ、カメラから流れてきたImageデータを入れてやると完成です。
モデルから出てきたデータに対してフィルターを掛けているのは、スコア（解析結果の信用度）が低いものを弾くためです。これを入れておかないと人のいない部屋で何故か人がいることになってしまったりします。

# 実機動作
適当に呼び出しボタンと結果表示用TextViewだけつけたアプリで試しましたが、カメラ映像を表示することなくカメラからの画像データを使えています。

https://twitter.com/Gazyu/status/1465677916289732608?s=20

ちなみに、カメラ使用時はAndroid12では下図のように右上にカメラ使用中マークが出るのでユーザーに知られずにということは無理です。
![Screenshot_20211130-223241.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/e5ef0e61-2847-78fb-2e9b-fd9b2cddc563.png)

# まとめ
CameraX APIの`ImageAnalysis.Analyzer`を使いましょう。

[^1]: 個人的にはこういう場合にAIというワードを使うのは嫌いなのですが、わかりやすさのためにAIとします。