【Android】CameraX試してみた

# はじめに
2019/5月の[Google I/O](https://io.google/2019/)で紹介された `CameraX` が気になったので、試して見ました。 :camera_with_flash: 
（実際にはチュートリアルをちょっと改造しただけです・・・。:innocent:）

# CameraXとは

>CameraX は、カメラアプリの開発を容易にすることを目的とした Jetpack ライブラリです。新しいアプリの場合は、CameraX から始めることをおすすめします。ほとんどの Android デバイスで機能する一貫性のある使いやすい API を提供するとともに、Android 5.0（API レベル 21）への下位互換性も備えています。

[公式](https://developer.android.com/training/camerax)より引用

要するに、今までのCameraやCamera2を使いやすくして、さらに古いAndroid(5.0)でも使えるようにしたよ、というライブラリになります。

# チュートリアル

公式がチュートリアルを出しています。
https://codelabs.developers.google.com/codelabs/camerax-getting-started/

今回はこれを進めていきました。

## 対応環境

- AndroidAPI21以上の端末 or エミュレータ
- Android Studio ArcticFox2020.3.1以降

## 1. プロジェクトの作成

### Gradleへの記述
KTSで書いているので、groovyをお使いの方は変換して記述してください :bow: 

以下を記述します。CameraX は Camera2 の機能を使用しているので、Camera2も含めます。
`1.1.0-beta01`からはすべてのライブラリが同じバージョンで導入出来る様になったようです。（今まではそれぞれ違うバージョンが必要だったとか）

```Kotlin:build.gradle.kts
val cameraxVersion = "1.1.0-beta03"
implementation("androidx.camera:camera-core:${cameraxVersion}")
implementation("androidx.camera:camera-camera2:${cameraxVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraxVersion}")
implementation("androidx.camera:camera-video:${cameraxVersion}")

implementation("androidx.camera:camera-view:${cameraxVersion}")
implementation("androidx.camera:camera-extensions:${cameraxVersion}")
```

### レイアウト作成
`androidx.camera.view.PreviewView` のViewにプレビューが表示されます。
自分は画面いっぱいに画面が表示されて欲しかったので、縦横を`0dp`で指定しています。

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

    <androidx.camera.view.PreviewView
            android:id="@+id/camera"
            android:layout_height="0dp"
            android:layout_width="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
```

### Activityクラスの作成
ベースとなるActivityクラスを作成します。

```kotlin:MainActivity.kt
class MainActivity {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)
    }
}
```

## 2. 必要なパーミッションを制御する
`ActivityManifest.xml`に以下を追記して、必要なpermissionを設定します。

```xml:ActivityManifest.xml
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />
```

次に、定数群を作成します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    companion object {
        private val TAG = MainActivity::class.java.simpleName
        private const val REQUEST_CODE_PERMISSIONS = 10
        // 必要なpermissionのリスト
        private val REQUIRED_PERMISSIONS =
            mutableListOf (
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                // WRITE_EXTERNAL_STORAGEはPie以下で必要
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }
}
```

そして、必要なpermissionがすべて反映されているか確認するメソッドを実装

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
    //...
}
```

permissionの許可結果を受け取るメソッドをオーバーライド

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults:
        IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                // カメラ開始処理
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    //...
}
```

最後にonCreate内でpermission許可要求を出します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    override fun onCreate(savedInstanceState: Bundle?) {

        // ViewBindingの設定など...

        if (allPermissionsGranted()) {
            // permissionは得られているので、カメラ始動
        } else {
            // permission許可要求
            ActivityCompat.requestPermissions(
                this, 
                REQUIRED_PERMISSIONS, 
                REQUEST_CODE_PERMISSIONS
            )
        }
    }
    //...
}
```

このpermission要求の書き方、スマートな感じで良いですね(語彙力)

## 3. カメラが映るようにする

ついにカメラの映像を画面に反映させます。

まずは、カメラを始動させるための`startCamera`メソッドを実装します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            // ライフサイクルにバインドするために利用する
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // PreviewのUseCase
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.camera.surfaceProvider)
                }

            // アウトカメラを設定
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // バインドされているカメラを解除
                cameraProvider.unbindAll()
                // カメラをライフサイクルにバインド
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview
                )

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

あとはカメラを起動したいタイミングで`startCamera`を呼び出します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    override fun onCreate(savedInstanceState: Bundle?) {

        // ViewBindingの設定など...

        if (allPermissionsGranted()) {
            // permissionは得られているので、カメラ始動
            startCamera()
        } else {
            // permission許可要求
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }
    //...
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults:
        IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                // カメラ開始処理
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    //...
}
```

これだけでカメラが映るようになるのか・・・(感動)
これで、ライフサイクルに紐付いてくれているので、スリープしたときにも勝手にカメラがOFFになってくれます。めっちゃ便利。 :upside_down: 

## 4. 写真を撮れるようにする

先ほどのはプレビューだけですので、写真を撮れるようにします。

まずは写真を撮るためのボタンを追加します。

```xml:activity_main.xml
<ImageButton
        android:id="@+id/capture_button"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
```

ImageCaptureというUseCaseを使う事で写真を撮ります。
まずはメンバーとしてImageCaptureを持たせます。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private var imageCapture: ImageCapture? = null
    //...
}
```

定数として保存するファイル名のフォーマットを定義します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    companion object {
        //...
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        //...
    }
}
```

写真を撮るための`takePhoto`メソッドを実装します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private fun takePhoto() {
        // imageCaptureがまだセットされていないときは早期リターン。
        // セットされていない時はプレビューもされていないはず。
        val imageCapture = imageCapture ?: return

        // 写真の名前の設定（タイムスタンプ）とMediaStoreの設定
        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
            .format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
            }
        }

        // 保存するオプションを作成
        val outputOptions = ImageCapture.OutputFileOptions
            .Builder(
                contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues
            )
            .build()

        // 画像のキャプチャ
        // 結果はImageCapture.OnImageSavedCallbackのコールバックで返ってくる
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }
            }
        )
    }
    //...
}
```

次に、ImageCaptureを設定するために、`startCamera`でライフサイクルにバインドするときにImageCaptureも渡す様にします。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private fun startCamera() {
        //...
        cameraProviderFuture.addListener({
            //...
            imageCapture = ImageCapture.Builder()
                .build()
            //...
            try {
                //...
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture
                )

            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }
    //...
}
```

最後にボタンを押したタイミングで写真を撮る用にします。

```kotlin:MainAcitivity.kt
class MainActivity : AppCompatActivity() {
    //...
    override fun onCreate(savedInstanceState: Bundle?) {

        // カメラ始動処理など

        viewBinding.captureButton.setOnClickListener {
            takePhoto()
        }
    }
    //...
}
```

カメラボタンを押すと写真が撮られて、Toastで表示されたディレクトリへ保存されます。
便利！！！！

## 5. 解析
解析と書きましたが「写っているものが何か」という解析ではなく、`輝度の平均値`を求めるという解析です。
ML Kit等と組み合わせることで画像解析も可能になるとは思いますが、今回は触れません。

リスナー用のtypealiaceを作成します。

```kotlin:MainAcitivity.kt
//...

typealias LumaListener = (luma: Double) -> Unit

//...
```

分析に利用する用の`ExecutorService`を設定します。
onCreateでExecutorが起動するようにし、onDestroyでシャットダウンする用にします。

```kotlin:MainAcitivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private lateinit var cameraExecutor: ExecutorService
    //...
    override fun onCreate(savedInstanceState: Bundle?) {
        //...
        cameraExecutor = Executors.newSingleThreadExecutor()
    }
    //...
    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
    //...
}
```

`ImageAnalysis.Analyzer`を実装した`LuminosityAnalyzer`クラスをMainActivityのインナークラスとして作成します。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private class LuminosityAnalyzer(
        private val listener: LumaListener
    ) : ImageAnalysis.Analyzer {

        private fun ByteBuffer.toByteArray(): ByteArray {
            rewind()    // バッファを0に戻す
            val data = ByteArray(remaining())
            get(data)   // バッファをバイト配列へコピー
            return data // バイト配列の取得
        }

        override fun analyze(image: ImageProxy) {

            val buffer = image.planes[0].buffer
            val data = buffer.toByteArray()
            val pixels = data.map { it.toInt() and 0xFF }
            val luma = pixels.average()

            listener(luma)

            image.close()
        }
    }
    //...
}
```

最後に、`LuminosityAnalyzer`を設定するために、`startCamera`でライフサイクルにバインドするときに`LuminosityAnalyzer`も渡す様にします。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    //...
    private fun startCamera() {
        //...
        cameraProviderFuture.addListener({
            //...
            val imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                        Log.d(TAG, "Average luminosity: $luma")
                    })
                }
            //...
            try {
                //...
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture,
                    imageAnalyzer
                )

            } catch (exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }
    //...
}
```

これで輝度の平均値がログに出力されるようになります！
これは夢が広がりますね！！！

```
D/MainActivity: Average luminosity: 121.706884765625
```

## チュートリアルは以上

`7. 動画の撮影` と `8. 動画の撮影と別のUseCaseを組み合わせる` は今回は扱いません。
（チュートリアルのコードが動かなかったので、一旦ここまでにします。）

リファクタリングの余地はありそうですが、最後に動作テストをして終わりです :+1:

# CameraXを試してみて

Cameraライブラリはちょっとかじったことがあるのですが、段違いなぐらい楽になっていると感じました！（Camera2は未経験）
あと、チュートリアルがかなり親切で、英語がわからない自分でも理解しながら進める事ができました。ありがとうGoogle。

途中でも記載しましたが、ML Kitとかと組み合わせて画像解析を行うのも楽しそうだなと思いました。 :camera_with_flash: