3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

からくりAdvent Calendar 2019

Day 20

Camera Xを試す

Last updated at Posted at 2019-12-24

Camera Xとは

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

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

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

完成図↓
mojikyo45_640-2.gif

環境

  • Kotlin
  • minSdkVersion 21
  • Android Studio 3.3(以上)

1. 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ブロックの最下部に以下を追加します。

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

2. レイアウト作成

プレビュー画面表示用のTextureViewを設定します。

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パーミッションを追加します。

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

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

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クラスを使用します。

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. 画像キャプチャ

撮影用のボタンを設置します。

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

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

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クラスでちょっとした解析を行ってみましょう。
ここでは画像の平均輝度を求めてみます。

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()メソッド内でインスタンスの生成を行い、それをバインドします。

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:

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?