Android Camera X

#CameraX
※ まだアルファのライブラリーです。

[![CameraX: a Jetpack support library for camera app development](http://img.youtube.com/vi/QYkTXJ2TuiA/0.jpg)](https://www.youtube.com/watch?v=QYkTXJ2TuiA)

[CameraXの公式ドキュメント](https://developer.android.com/training/camerax)
最新のCameraXを確認をするならこちらのリンクを参考

# 特徴
・ Camera2を利用するので Android5.0(API level21) からサポート
・ ユースケース基盤の設計で Preview、Image analysis、 Image Captureのユースケースを同時サポート
・ ライフサイクル対応
・ デバイスの互換性問題を解決して、機器別の分岐コードを簡略化
・ 特定デバイスの従属される Bokeh、HDR などと同じエフェクトをサポート

# 最低要求事項
CameraXライブラリーを利用するためには次の条件が必要。
・ Android5.0(API Level21)
・ Android Architecture Components v1.1.1
・ lifecycle-awareアクティビティでは FragmentActivity または AppCompatActivity 利用　

# Cameraサンプルでやってみる
CameraXを利用して、Preview、Image Capture、Image analysis ３つの機能があるサンプルアプリを作る

## 依存関係追加
CameraXを利用するためには Google Maven レポジトリをプロジェクトに追加する
プロジェクトレベルの build.gradle に google() を次のように追加する

```kotlin

allprojects {
    repositories {
        // 追加
        google()
        jcenter()
    }
}
```

モジュールレベルの build.gradle 次のように追加する

```kotlin
dependencies {
    // coreライブラリー
    implementation "androidx.camera:camera-core:1.0.0-alpha01"
    // Camera2 extensionsを利用するならこちらも追加
    implementation "androidx.camera:camera-camera2:1.0.0-alpha01"

}
```

## Permission追加
カメラを使うためにはPermissionの追加が必要です。AndroidManifestに次のように追加

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

## ビューファインダーを作る（ViewFinder）
ビューファインダーを作る View は TextureView にする
* TexttureViewを使う理由はカメラからのコンテンツストリームを表現するには適切なビューである。 Androidでのストリームを SurfaceTexture で扱う。

<img width="600" alt="スクリーンショット 2019-05-28 22.31.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269837/7cc7b1e4-df7a-0c90-df75-f82e4b2641e0.png">

```kotlin:activity_main
<?xml version="1.0" encoding="utf-8"?>
<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">
 
    <TextureView
            android:id="@+id/textureView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
 
</androidx.constraintlayout.widget.ConstraintLayout>
```

## MainActivity実装
MainActivityには次のような定数を追加


```kotlin:MainActivity
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
```

MainActivityの内部には次のようなフィルドとメソッドを追加する。カメラの権限が要請され、権限が承認されたら startCamera()を呼び出す。

```kotlin:MainActivity
class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
 
    private lateinit var viewFinder: TextureView
 
    private fun startCamera() {
        ...
    }
 
    private fun updateTransform() {
        ...
    }
 
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<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 {
        for (permission in REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(
                    this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false
            }
        }
        return true
    }
}
```

```kotlin:onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
    textureView = findViewById(R.id.textureView)

    // パーミッションチェック
    if (allPermissionsGranted()) {
        textureView.post { startCamera() }
    } else {
        ActivityCompat.requestPermissions(
            this,
            REQUIRED_PERMISSIONS,
            REQUEST_CODE_PERMISSIONS
        )
    }

    // [TextureView]が変更されるたびにレイアウトを再読込する
    textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
        updateTransform()
    }
}

```
これでアプリを起動するとカメラのパーミッションをチェックする。 カメラのパーミッションが既に許可されていれば startCamera() を呼び出す。パーミッションが許可されてなければパーミッションをチェックし、許可された時に startCamera() を呼び出す。

# ビューファインダー実装
starCamera() メソッドに CameraXライブラリーを使って実装する。

```kotlin:onCreate()
    private fun startCamera() {
        // プレビューを確認するための設定のオブジェクトを生成
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetAspectRatio(Rational(1, 1))
            setTargetResolution(Size(640, 640))
        }.build()

        // プレビューのオブジェクトを生成する
        val preview = Preview(previewConfig)

        // ビューファインダーが更新されるたびにレイアウトを再設定する
        preview.setOnPreviewOutputUpdateListener {
            textureView.surfaceTexture = it.surfaceTexture
            updateTransform()
        }

        // ライフサイクルにカメラをバインディングする
        CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
    }

```

```kotlin:updateTransform()
    private fun updateTransform() {
        val matrix = Matrix()

        // ビューファインダーの中心を計算
        val centerX = textureView.width / 2f
        val centerY = textureView.height / 2f
        
        // 画面回転を考慮してプレビューを出力
        val rotationDegrees = when(textureView.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.setTransform(matrix)
    }

```
updateTransform() を実装して、端末の画面回転によってカメラの映像も合わせて出力する。


#写真を撮る処理を実装
```kotlin:activity_main
<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" />
```
activity_main.xml に写真を撮るためのボタンを追加する。


```kotlin:startCamera（）
    private fun startCamera() {
        val previewConfig = ...
        val preview = ...

        preview.setOnPreviewOutputUpdateListener {
            ....
        }

        // 写真を撮る設定のために ImageCaptureConfig を生成
        val imageCaptureConfig = ImageCaptureConfig.Builder()
            .apply {
                setTargetAspectRatio(Rational(1, 1))
                // 写真の解像度を設定しない代わりに画面の比率とキャプチャーモードで
                // CameraXライブラリーが解像度を決める
                setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
            }.build()

        // ImageCapture を利用してボタンがクリックされた時に写真を撮る
        val imageCapture = ImageCapture(imageCaptureConfig)
        findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
            val file = File(externalMediaDirs.first(),
                "${System.currentTimeMillis()}.jpg")
            imageCapture.takePicture(file,
                object : ImageCapture.OnImageSavedListener {
                    override fun onError(error: ImageCapture.UseCaseError,
                                         message: String, exc: Throwable?) {
                        val msg = "Photo capture failed: $message"
                        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        Log.e("CameraXApp", msg)
                        exc?.printStackTrace()
                    }

                    override fun onImageSaved(file: File) {
                        val msg = "PATH : ${file.absolutePath}"
                        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        Log.d("CameraXApp", msg)
                    }
                })
        }

        CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
    }
```

# イメージプロセシングを実装
CameraX の面白い機能は ImageAnalysis クラスである。カメラフレームと一緒に ImageAnalysis.Analyzer インタフェースが実装できる。カメラセッションを管理したりイメージを廃棄する心配はいらない。

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

        /**
         * Image Buffer を Byte 配列に抽出する
         */
        private fun ByteBuffer.toByteArray(): ByteArray {
            rewind()    // Buffer の Position を0に戻す
            val data = ByteArray(remaining())
            get(data)   // Byte Buffer を Byte 配列にコピーする
            return data // Byte 配列に返す
        }

        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:ByteArray = buffer.toByteArray()
                val pixels:List<Int> = data.map { it.toInt() and 0xFF }
                val luma:Double = pixels.average()
                Log.d("CameraXApp", "Average luminosity: $luma")
                lastAnalyzedTimestamp = currentTimestamp
            }
        }
    }
```

LuminosityAnalyzer をインスタンス化して、 startCamera() に設定処理をする

```kotlin:startCamera
private fun startCamera() {
        val previewConfig = ...

        // 写真を撮る設定処理
        val analyzerConfig = ImageAnalysisConfig.Builder().apply {
            val analyzerThread = HandlerThread("LuminosityAnalysis").apply { start() }
            setCallbackHandler(Handler(analyzerThread.looper))
            setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
        }.build()

        // イメージプロセシングのオブジェクトを生成
        val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
            analyzer = LuminosityAnalyzer()
        }

        // ライフサイクルにカメラをバインディングする
        CameraX.bindToLifecycle(this, preview, imageCapture, analyzerUseCase)
    }
```

これでログが１秒ごとに出力されることが確認できる。

#最後に
Firebase の ML Kit との組み合わせも良さそう。