CameraX のスタートガイド

この Codelab では、CameraX を使用してビューファインダーを表示し、写真を撮影し、カメラからの画像ストリームを分析するカメラアプリの作成方法を学びます。

これを実現するために、CameraX にはユースケースのコンセプトが導入されます。これは、ビューファインダーの表示からリアルタイムでのフレーム分析まで、さまざまなカメラ操作に使用できます。

学習内容

  • CameraX の依存関係を追加する方法
  • アクティビティでカメラ プレビューを表示する方法。(プレビュー ユースケース)
  • 写真を撮影してストレージに保存する方法(ImageCapture のユースケース)
  • カメラからのフレームをリアルタイムで分析する方法(ImageAnalysis のユースケース)

必要なハードウェア

  • Android デバイス。ただし、Android Studio のエミュレータでは問題ありません。サポートされている最小 API レベルは 21 です。

必要なソフトウェア

  • Android Studio 3.3 以降

Android Studio のメニューを使用して新しいプロジェクトを開始し、プロンプトが表示されたら [Empty Activity] を選択します。

名前は自由自在に決められます。そこで「CameraX アプリ」を選びました。言語が Kotlin に設定されており、最小 API レベルが 21(CameraX に必要な最小レベル)であり、AndroidX アーティファクトを使用していることを確認します。

まず、アプリの Gradle ファイル内の dependencies セクションで、CameraX の依存関係を追加してみましょう。

// Use the most recent version of CameraX, currently that is alpha04
def camerax_version = "1.0.0-alpha04"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

メッセージが表示されたら、[今すぐ同期] をクリックすると、アプリで CameraX を使用できるようになります。

SurfaceTexture を使用して、カメラのビューファインダーを表示します。この Codelab では、固定サイズの正方形形式でビューファインダーを表示します。レスポンシブ ビューファインダーのより包括的な例については、公式サンプルをご覧ください。

activity_main レイアウト ファイルを res > layout > activity_main.xml の下で編集してみましょう。

<?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/view_finder"
            android:layout_width="640px"
            android:layout_height="640px"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

プロジェクトにカメラを使用する機能を追加するには、適切な CAMERA 権限をリクエストする必要があります。まず、マニフェスト内で Application タグの前に宣言する必要があります。

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

次に、MainActivity 内で実行時に権限をリクエストする必要があります。MainActivity ファイルの変更を java > com.example.cameraxapp > MainActivity.kt で行います。

ファイルの先頭にある MainActivity クラス定義のほかに、次の定数とインポートを追加します。

// Your IDE likely can auto-import these classes, but there are several
// different implementations so we list them here to disambiguate
import android.Manifest
import android.util.Size
import android.graphics.Matrix
import java.util.concurrent.TimeUnit

// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10

// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

MainActivity クラス内に、次のフィールドとヘルパー メソッドを追加します。これらを使用して権限を要求し、すべての権限が付与されていることが判明したらコードをトリガーします。

class MainActivity : AppCompatActivity(), LifecycleOwner {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    // Add this after onCreate

    private lateinit var viewFinder: TextureView

    private fun startCamera() {
        // TODO: Implement CameraX operations
    }

    private fun updateTransform() {
        // TODO: Implement camera viewfinder transformations
    }

    /**
     * Process result from permission request dialog box, has the request
     * been granted? If yes, start Camera. Otherwise display a toast
     */
    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()
            }
        }
    }

    /**
     * Check if all permission specified in the manifest have been granted
     */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
               baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
}

最後に、すべてを onCreate 内に記述して、必要に応じて権限リクエストをトリガーします。

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Add this at the end of onCreate function

    viewFinder = findViewById(R.id.view_finder)

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

    // Every time the provided texture view changes, recompute layout
    viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
        updateTransform()
    }
}

これで、アプリが起動すると、適切なカメラ権限が付与されているかどうかが確認されます。存在する場合は、startCamera() が直接呼び出されます。それ以外の場合は、権限をリクエストして「startCamera()」を呼び出します。

ほとんどのカメラアプリでは、ユーザーにビューファインダーを表示することが非常に重要であり、そうでなければ、ユーザーがカメラを正しい位置に向けることは非常に困難です。ビューファインダーは、CameraX の「Preview」クラスを使用して実装できます。

プレビューを使用するには、まず構成を定義してから、ユースケースのインスタンスの作成に使用します。結果として得られるインスタンスは、CameraX のライフサイクルにバインドするために必要なものです。これは `startCamera()` メソッド内で行います。次のコードで実装を記入します。

private fun startCamera() {

    // Create configuration object for the viewfinder use case
    val previewConfig = PreviewConfig.Builder().apply {
        setTargetAspectRatio(Rational(1, 1))
        setTargetResolution(Size(640, 640))
    }.build()

    // Build the viewfinder use case
    val preview = Preview(previewConfig)

    // Every time the viewfinder is updated, recompute layout
    preview.setOnPreviewOutputUpdateListener {

        // To update the SurfaceTexture, we have to remove it and re-add it
        val parent = viewFinder.parent as ViewGroup
        parent.removeView(viewFinder)
        parent.addView(viewFinder, 0)

        viewFinder.surfaceTexture = it.surfaceTexture
        updateTransform()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

この時点で、不可解な「updateTransform()」メソッドを実装する必要があります。「updateTransform()」内での目標は、デバイスの向きの変化を補正して、ビューファインダーを縦向きに回転させることです。

private fun updateTransform() {
    val matrix = Matrix()

    // Compute the center of the view finder
    val centerX = viewFinder.width / 2f
    val centerY = viewFinder.height / 2f

    // Correct preview output to account for display rotation
    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)

    // Finally, apply transformations to our TextureView
    viewFinder.setTransform(matrix)
}

本番環境に対応したアプリを実装するには、公式サンプルを参照して、他に処理する必要があるものをご確認ください。この Codelab を簡潔にするために、いくつかのショートカットをご紹介します。たとえば、180 度回転するなど、レイアウト変更リスナーがトリガーされない設定変更はトラッキングされていません。正方形以外のビューファインダーでは、デバイスの回転時のアスペクト比の変化も補正する必要があります。

アプリをビルドして実行すると、ライフ プレビューが表示されます。成功です。

ユーザーが画像を取得できるように、レイアウトの残りの部分である > layout > 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" />

他のユースケースも、プレビューと非常に似た方法で動作します。まず、実際のユースケース オブジェクトのインスタンス化に使用する構成オブジェクトを定義する必要があります。写真を撮るには、キャプチャ ボタンが押されたときに `startCamera()` メソッドを更新し、CameraX.bindToLifecycle を呼び出す前に最後にコードを数行追加する必要があります。

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Create configuration object for the image capture use case
    val imageCaptureConfig = ImageCaptureConfig.Builder()
        .apply {
            setTargetAspectRatio(Rational(1, 1))
            // We don't set a resolution for image capture; instead, we
            // select a capture mode which will infer the appropriate
            // resolution based on aspect ration and requested mode
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
    }.build()

    // Build the image capture use case and attach button click listener
    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 = "Photo capture succeeded: ${file.absolutePath}"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d("CameraXApp", msg)
            }
        })
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

次に、CameraX.bindToLifecycle の呼び出しを更新して新しいユースケースを含めます。

CameraX.bindToLifecycle(this, preview, imageCapture)

それに伴い、機能的な写真撮影ボタンを実装しています。

CameraX の非常に興味深い機能は ImageAnalysis クラスです。受信したカメラフレームで呼び出される ImageAnalysis.Analyzer インターフェースを実装するカスタムクラスを定義できます。CameraX の主要なビジョンに沿って、カメラのセッション状態の管理や画像の廃棄について心配する必要はありません。他のライフサイクル対応コンポーネントと同様に、アプリの望ましいライフサイクルにバインドするだけで十分です。

まず、カスタム画像解析ツールを実装します。アナライザは非常にシンプルで、画像の平均輝度(輝度)をログに記録するだけでなく、任意の複雑なユースケースで何をする必要があるかを示しています。必要な作業は、ImageAnalysis.Analyzer インターフェースを実装するクラスの「analyze」関数をオーバーライドすることだけです。この実装は、MainActivity の内部クラスとして定義できます。

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

    /**
     * Helper extension function used to extract a byte array from an
     * image plane buffer
     */
    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // Calculate the average luma no more often than every second
        if (currentTimestamp - lastAnalyzedTimestamp >=
            TimeUnit.SECONDS.toMillis(1)) {
            // Since format in ImageAnalysis is YUV, image.planes[0]
            // contains the Y (luminance) plane
            val buffer = image.planes[0].buffer
            // Extract image data from callback object
            val data = buffer.toByteArray()
            // Convert the data into an array of pixel values
            val pixels = data.map { it.toInt() and 0xFF }
            // Compute average luminance for the image
            val luma = pixels.average()
            // Log the new luma value
            Log.d("CameraXApp", "Average luminosity: $luma")
            // Update timestamp of last analyzed frame
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

このクラスで ImageAnalysis.Analyzer インターフェースを実装すると、ほかのユースケースと同様に ImageAnalysis をインスタンス化し、CameraX.bindToLifecycle を呼び出す前に `startCamera()` 関数を再度更新します。

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Setup image analysis pipeline that computes average pixel luminance
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        // Use a worker thread for image analysis to prevent glitches
        val analyzerThread = HandlerThread(
            "LuminosityAnalysis").apply { start() }
        setCallbackHandler(Handler(analyzerThread.looper))
        // In our analysis, we care more about the latest image than
        // analyzing *every* image
        setImageReaderMode(
            ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // Build the image analysis use case and instantiate our analyzer
    val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
        analyzer = LuminosityAnalyzer()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview, imageCapture)
}

また、CameraX.bindtoLifecycle の呼び出しを更新して、新しいユースケースをバインドします。

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

アプリを実行すると、ほぼ毎秒、logcat に次のようなメッセージが表示されます。

D/CameraXApp: Average luminosity: ...

アプリをテストするために、Android Studio で [Run] ボタンをクリックするだけで、選択したデバイスまたはエミュレータでプロジェクトがビルド、デプロイ、起動されます。アプリが読み込まれると、ビューファインダーが表示されます。ビューファインダーは、先ほど追加した向きの処理コードのおかげでデバイスを回転させた後も縦向きのままで、ボタンを使って写真を撮ることもできます。

Codelab を修了し、正常に完了しました。振り返ってみると、以下を新しい Android アプリにゼロから実装しました。

  • CameraX の依存関係をプロジェクトに含める。
  • カメラのビューファインダーを表示(プレビューのユースケースを使用)
  • 写真キャプチャを実装し、画像をストレージに保存(ImageCapture ユースケースを使用)
  • カメラからのフレーム分析をリアルタイムで実装(ImageAnalysis ユースケースを使用)

CameraX とその機能について詳しくは、こちらのドキュメントをご覧いただくか、公式サンプルのクローンを作成してください。