カメラのキャプチャ セッションとリクエスト

注: このページでは、Camera2 パッケージについて言及しています。アプリで Camera2 の特定の低レベル機能が必要な場合を除き、CameraX を使用することをおすすめします。CameraX と Camera2 は、どちらも Android 5.0(API レベル 21)以降に対応しています。

1 台の Android 搭載デバイスに複数のカメラを搭載できます。各カメラは CameraDevice であり、CameraDevice は複数のストリームを同時に出力できます。

これを行う理由の一つは、CameraDevice からの連続するカメラフレームである 1 つのストリームを、ビューファインダーの表示などの特定のタスク用に最適化し、他のストリームを写真撮影や動画撮影に使用できるようにするためです。ストリームは、カメラから出力される RAW フレームを 1 フレームずつ処理する並列パイプラインとして機能します。

カメラデバイスからの未加工フレームを異なる出力形式に処理する並列ストリーム処理
図 1. ユニバーサル カメラアプリの作成(Google I/O '18)の図

並列処理では、CPU、GPU、またはその他のプロセッサから利用可能な処理能力に応じて、パフォーマンスに制限が生じる可能性があります。パイプラインが受信フレームに追いつけない場合、フレームのドロップが開始されます。

各パイプラインには独自の出力形式があります。受信した元データは、各パイプラインに関連付けられた暗黙的なロジックによって、適切な出力形式に自動的に変換されます。このページのコードサンプル全体で使用されている CameraDevice は、特定のカメラを指すものではありません。そのため、まず利用可能なすべてのカメラを列挙してから、処理を進めます。

CameraDevice を使用して、その CameraDevice に固有の CameraCaptureSession を作成できます。CameraDevice は、CameraCaptureSession を使用して、各 RAW フレームのフレーム構成を受け取る必要があります。この構成では、オートフォーカス、絞り、エフェクト、露出などのカメラ属性を指定します。ハードウェアの制約により、カメラセンサーで一度に有効にできる構成は 1 つのみです。これをアクティブな構成と呼びます。

ただし、Stream のユースケースは、CameraDevice を使用してキャプチャ セッションをストリーミングする従来の方法を強化、拡張したものであり、特定のユースケースに合わせてカメラ ストリームを最適化できます。たとえば、ビデオ通話を最適化する際にバッテリー駆動時間を延ばすことができます。

CameraCaptureSession は、CameraDevice にバインドされているすべての可能なパイプラインを記述します。セッションの作成後にパイプラインを追加または削除することはできません。CameraCaptureSession は、アクティブな構成となる CaptureRequest のキューを維持します。

CaptureRequest は、キューに構成を追加し、CameraDevice からフレームを受信する 1 つ、複数、またはすべての利用可能なパイプラインを選択します。キャプチャ セッションの存続期間中に、多くのキャプチャ リクエストを送信できます。各リクエストは、アクティブな構成と、未加工画像を受信する出力パイプラインのセットを変更できます。

ストリームのユースケースを使用してパフォーマンスを向上させる

ストリーム ユースケースは、Camera2 キャプチャ セッションのパフォーマンスを向上させる方法です。これにより、ハードウェア デバイスにパラメータを調整するための追加情報が提供され、特定のタスクでより優れたカメラ エクスペリエンスが実現します。

これにより、カメラデバイスは、各ストリームのユーザー シナリオに基づいてカメラのハードウェアとソフトウェアのパイプラインを最適化できます。ストリームのユースケースの詳細については、setStreamUseCase をご覧ください。

ストリーム ユースケースを使用すると、CameraDevice.createCaptureRequest() でテンプレートを設定するだけでなく、特定のカメラ ストリームの使用方法をより詳細に指定できます。これにより、カメラ ハードウェアは、特定のユースケースに適した品質またはレイテンシのトレードオフに基づいて、チューニング、センサーモード、カメラセンサー設定などのパラメータを最適化できます。

ストリームのユースケースには、次のようなものがあります。

  • DEFAULT: 既存のすべてのアプリケーション動作をカバーします。これは、ストリームのユースケースを設定しない場合と同じです。

  • PREVIEW: ビューファインダーまたはアプリ内画像分析におすすめです。

  • STILL_CAPTURE: 高品質、高解像度のキャプチャ用に最適化されており、プレビューのようなフレームレートを維持することは想定されていません。

  • VIDEO_RECORD: 高品質の動画キャプチャ用に最適化されています。デバイスでサポートされ、アプリケーションで有効になっている場合は、高品質の手ぶれ補正も含まれます。このオプションでは、最高品質の手ぶれ補正などの処理を可能にするため、出力フレームがリアルタイムから大幅に遅れることがあります。

  • VIDEO_CALL: 電力の消耗が懸念される、長時間のカメラの使用におすすめです。

  • PREVIEW_VIDEO_STILL: ソーシャル メディア アプリや単一ストリームのユースケースにおすすめです。これは多目的ストリームです。

  • VENDOR_START: OEM 定義のユースケースに使用されます。

CameraCaptureSession を作成する

カメラ セッションを作成するには、アプリが出力フレームを書き込める 1 つ以上の出力バッファをセッションに提供します。各バッファはパイプラインを表します。フレームワークがデバイスの内部パイプラインを構成し、必要な出力ターゲットにフレームを送信するためのメモリバッファを割り当てられるように、カメラの使用を開始する前にこの処理を行う必要があります。

次のコード スニペットは、2 つの出力バッファ(1 つは SurfaceView に属し、もう 1 つは ImageReader に属する)を使用してカメラ セッションを準備する方法を示しています。PREVIEW ストリーム ユースケースを previewSurface に追加し、STILL_CAPTURE ストリーム ユースケースを imReaderSurface に追加すると、デバイス ハードウェアはこれらのストリームをさらに最適化できます。

// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
// reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(
    // ...
)
val imageReader = ImageReader.newInstance(
    // ...
)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated().
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready.
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>) {
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    // ...
    device.createCaptureSession(session)
}

この時点では、カメラのアクティブな構成は定義されていません。セッションが構成されたら、キャプチャ リクエストを作成してディスパッチし、キャプチャを実行できます。

入力がバッファに書き込まれるときに適用される変換は、各ターゲットのタイプによって決まります。このタイプは Surface である必要があります。Android フレームワークは、アクティブな構成の未加工画像を各ターゲットに適した形式に変換する方法を認識しています。変換は、特定の Surface のピクセル形式とサイズによって制御されます。

フレームワークは最善を尽くしますが、Surface 構成の組み合わせによっては、セッションが作成されない、リクエストのディスパッチ時にランタイム エラーがスローされる、パフォーマンスが低下するなどの問題が発生する可能性があります。フレームワークは、デバイス、サーフェス、リクエスト パラメータの特定の組み合わせを保証します。詳しくは、createCaptureSession() のドキュメントをご覧ください。

単一の CaptureRequest

各フレームに使用される構成は CaptureRequest でエンコードされ、カメラに送信されます。キャプチャ リクエストを作成するには、事前定義されたテンプレートのいずれかを使用するか、TEMPLATE_MANUAL を使用して完全に制御します。テンプレートを選択するときは、リクエストで使用する 1 つ以上の出力バッファを指定する必要があります。使用できるのは、使用するキャプチャ セッションですでに定義されているバッファのみです。

キャプチャ リクエストはビルダー パターンを使用し、デベロッパーは自動露出、オートフォーカス、レンズ絞りなど、さまざまなオプションを設定できます。フィールドを設定する前に、CameraCharacteristics.getAvailableCaptureRequestKeys() を呼び出して、デバイスで特定のオプションが利用可能であることを確認します。また、利用可能な自動露出モードなど、適切なカメラ特性をチェックして、選択した値がサポートされていることを確認します。

変更を加えずにプレビュー用に設計されたテンプレートを使用して SurfaceView のキャプチャ リクエストを作成するには、CameraDevice.TEMPLATE_PREVIEW を使用します。

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

キャプチャ リクエストを定義したら、カメラ セッションにディスパッチできます。

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null.
session.capture(captureRequest.build(), null, null)

出力フレームが特定のバッファに格納されると、キャプチャ コールバックがトリガーされます。多くの場合、含まれているフレームが処理されると、ImageReader.OnImageAvailableListener などの追加のコールバックがトリガーされます。この時点で、指定されたバッファから画像データを取得できます。

CaptureRequest を繰り返す

単一カメラのリクエストは簡単に実行できますが、ライブ プレビューや動画を表示する場合にはあまり役に立ちません。その場合は、1 つのフレームだけでなく、フレームの連続ストリームを受信する必要があります。次のコード スニペットは、繰り返しリクエストをセッションに追加する方法を示しています。

val session: CameraCaptureSession = ... // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ... // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the session is torn down or session.stopRepeating() is called.
session.setRepeatingRequest(captureRequest.build(), null, null)

反復キャプチャ リクエストにより、カメラデバイスは、指定された CaptureRequest の設定を使用して画像を継続的にキャプチャします。Camera2 API では、GitHub の Camera2 サンプル リポジトリに示されているように、CaptureRequests を繰り返し送信することで、カメラから動画をキャプチャすることもできます。また、GitHub の Camera2 スローモーション動画サンプルアプリに示されているように、繰り返しバースト CaptureRequests を使用して高速(スローモーション)動画をキャプチャすることで、スローモーション動画をレンダリングすることもできます。

CaptureRequest をインターリーブする

ビューファインダーを表示してユーザーが写真を撮影できるようにするなど、繰り返しキャプチャ リクエストがアクティブなときに 2 番目のキャプチャ リクエストを送信する場合、進行中の繰り返しリクエストを停止する必要はありません。代わりに、繰り返しリクエストの実行を継続しながら、繰り返しなしのキャプチャ リクエストを発行します。

使用する出力バッファは、セッションが最初に作成されるときに、カメラ セッションの一部として構成する必要があります。繰り返しリクエストは、シングルフレーム リクエストやバースト リクエストよりも優先度が低いため、次のサンプルを動作させることができます。

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it.
val repeatingRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW
)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it.
// NOTE: This can disrupt the ongoing repeating request momentarily.
val singleRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE
)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

ただし、この方法には、単一のリクエストがいつ発生するかが正確にわからないという欠点があります。次の図では、A が繰り返しキャプチャ リクエスト、B が単一フレーム キャプチャ リクエストの場合に、セッションがリクエスト キューを処理する方法を示しています。

アクティブな繰り返しキャプチャ リクエスト キューが、単一のキャプチャ リクエストによって中断された
図 2. 進行中のカメラ セッションのリクエスト キューの図

リクエスト B が有効になる前の A からの最後の繰り返しリクエストと、A が再び使用される次のタイミングとの間のレイテンシは保証されないため、フレームがスキップされることがあります。この問題を軽減するには、次の方法があります。

  • リクエスト A の出力ターゲットをリクエスト B に追加します。そうすることで、B のフレームの準備が整うと、A の出力ターゲットにコピーされます。たとえば、安定したフレームレートを維持するために動画スナップショットを作成する場合に不可欠です。上記のコードでは、リクエストを作成する前に singleRequest.addTarget(previewSurface) を追加しています。

  • ゼロシャッターラグなど、この特定のシナリオで機能するように設計されたテンプレートの組み合わせを使用します。