Camera Xを試す

# Camera Xとは
>CameraX は、カメラアプリの開発を簡単に行うための Jetpack サポート ライブラリです。ほとんどの Android デバイスで機能する、使いやすく一貫性のある API サーフェスを提供するほか、Android 5.0（API レベル 21）への下位互換性も備えています。

これまでの[camera2](https://developer.android.com/reference/kotlin/android/hardware/camera2/package-summary.html)よりもシンプルに実装でき、Android 5.0でも使用することができるので嬉しいですね。
また、拡張機能を利用することでポートレート、HDR、夜景などの効果も利用することができるようです :camera_with_flash: 
https://developer.android.com/training/camerax

公式でCodelabが用意されているので、今回はそれを進めていきたいと思います。
https://codelabs.developers.google.com/codelabs/camerax-getting-started

完成図↓
<img width="300" alt="mojikyo45_640-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172329/e786b663-d324-8cec-dc53-bf6eca447ff7.png">

# 環境
- Kotlin
- minSdkVersion 21
- Android Studio 3.3(以上)

# 1. Gradleへ依存関係を追加
```gradle:build.gradle
def camerax_version = '1.0.0-alpha06'
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
```
現時点では`implementation 'androidx.appcompat:appcompat:1.1.0'`が必要でした。

また、Camera XのいくつかのメソッドはJava 8が必要なので、コンパイルオプションを設定する必要があります。 
androidブロックの最下部に以下を追加します。

```gradle
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
```

# 2. レイアウト作成
プレビュー画面表示用のTextureViewを設定します。

```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">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
# 3. カメラのパーミッションをリクエスト
`AndroidManifest.xml`のapplicationタグの前にCameraパーミッションを追加します。

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

次に、カメラへのアクセス許可の要求を実装していきます。

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

    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }

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

        viewFinder = findViewById(R.id.view_finder)

        if (allPermissionsGranted()) {
            viewFinder.post { startCamera() }
        } else {
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }

    // パーミッションダイアログの結果を取得
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

    // マニフェストで指定されたすべての権限が付与されているか
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it
        ) == PackageManager.PERMISSION_GRANTED
    }
}
```

# 4. ViewFinderの実装
`Preview`クラスを使用します。

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        ...
        // TextureViewが変更される度にレイアウトを再計算
        viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
    }

    private fun startCamera() {
        // PreviewConfigオブジェクトを生成
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetResolution(Size(640, 480))
        }.build()

        // PreviewConfigからPreviewインスタンスを生成
        val preview = Preview(previewConfig)
        preview.setOnPreviewOutputUpdateListener {
            val parent = viewFinder.parent as ViewGroup
            parent.removeView(viewFinder)
            parent.addView(viewFinder, 0)

            viewFinder.surfaceTexture = it.surfaceTexture
            updateTransform()
        }
        // Camera Xのライフサイクルにバインド
        CameraX.bindToLifecycle(this, preview)
    }

    private fun updateTransform() {
        val matrix = Matrix()
        // ViewFinderの中心を計算
        val centerX = viewFinder.width / 2f
        val centerY = viewFinder.height / 2f
        // ディスプレイの回転を考慮してプレビューを出力
        val rotationDegrees = when (viewFinder.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> return
        }
        matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
        // TextureViewに変換をを適用
        viewFinder.setTransform(matrix)
    }
}
```
ここまでで、カメラが表示されるようになったとおもいます :iphone: 

# 5. 画像キャプチャ
撮影用のボタンを設置します。

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

カメラボタンを押した時の保存処理を追加します。

```kotlin:MainActivity.kt
    private fun startCamera() {
        ・・・
        ・・・
        // ImageCaptureConfigオブジェクトを生成
        val imageCaptureConfig = ImageCaptureConfig.Builder().apply {
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
        }.build()

        // ImageCaptureConfigからImageCaptureインスタンスを生成
        val imageCapture = ImageCapture(imageCaptureConfig)
        findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
            val file = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")

            // 画像をキャプチャしてファイルに保存
            imageCapture.takePicture(file, executor,
                object : ImageCapture.OnImageSavedListener {
                    override fun onError(
                        imageCaptureError: ImageCapture.ImageCaptureError,
                        message: String,
                        cause: Throwable?
                    ) {
                        val msg = "Photo capture failed: $message"
                        Log.e("Camera X App", msg, cause)
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                                .show()
                        }
                    }

                    override fun onImageSaved(file: File) {
                        val msg = "Photo capture succeeded: ${file.absolutePath}"
                        Log.d("Camera X App", msg)
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                                .show()
                        }
                    }
                })
        }
        // 引数に"imageCapture"を追加
        CameraX.bindToLifecycle(this, preview, imageCapture)
    }
```
ボタンを押すと撮影した画像が保存されているはずです :file_folder: 

# 6. 画像解析
最後にImageAnalysisクラスでちょっとした解析を行ってみましょう。
ここでは画像の平均輝度を求めてみます。

```kotlin:MainActivity.kt
private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()
        val data = ByteArray(remaining())
        get(data)
        return data
    }

    // ImageAnalysis.Analyzerインターフェースにある"analyze"メソッドをオーバーライド
    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // 輝度計算を毎秒以上の間隔で行う
        if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
            val buffer = image.planes[0].buffer
            val data = buffer.toByteArray()
            val pixels = data.map { it.toInt() and 0xFF }
            val luma = pixels.average()
            Log.d("Camera X App", "Average luminosity: $luma")
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}
```

startCamera()メソッド内でインスタンスの生成を行い、それをバインドします。

```kotlin:MainActivity.kt
private fun startCamera() {
    ・・・
    ・・・
    // ImageAnalysisConfigオブジェクトを生成
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // ImageAnalysisConfigからImageAnalysisインスタンスを生成
    val analyzerUsecase = ImageAnalysis(analyzerConfig).apply {
        setAnalyzer(executor, LuminosityAnalyzer())
    }
    // 引数に"analyzerUsecase"を追加
    CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUsecase)
}
```

これで完了です。
以下のようなログが出力されるようになりましたね :tada: 

```
D/Camera X App: Average luminosity: 86.584970703125
D/Camera X App: Average luminosity: 86.58956380208333
D/Camera X App: Average luminosity: 91.70888997395834
D/Camera X App: Average luminosity: 91.86529947916667
D/Camera X App: Average luminosity: 92.1453125
```

# 最後に
カメラ機能の実装がとても楽になり、開発工数の削減にも繋がるかもしれませんね！
より詳細な解析や、ポートレート効果なども実装してみたいと思いました :relieved: