複数のカメラ ストリームを同時に使用する

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

カメラアプリは、複数のフレーム ストリームを同時に使用できます。場合によっては、ストリームごとに異なるフレーム解像度やピクセル形式が必要になることもあります。一般的なユースケースは次のとおりです。

  • 動画撮影: 1 つのストリームはプレビュー用、もう 1 つはエンコードされてファイルに保存されます。
  • バーコード スキャン: 1 つのストリームはプレビュー用、もう 1 つはバーコード検出用。
  • 計算写真学: プレビュー用に 1 つのストリーム、顔またはシーンの検出用にもう 1 つのストリーム。

フレームの処理には、無視できないパフォーマンス コストがかかります。このコストは、並列ストリーム処理やパイプライン処理を行うと、さらに大きくなります。

CPU、GPU、DSP などのリソースは、フレームワークの再処理機能を利用できる場合がありますが、メモリなどのリソースは直線的に増加します。

1 つのリクエストに複数のターゲット

複数のカメラ ストリームを 1 つの CameraCaptureRequest に結合できます。次のコード スニペットは、カメラ プレビュー用のストリームと画像処理用のストリームを使用してカメラ セッションをセットアップする方法を示しています。

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

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD.
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Link the Surface targets with the combined request.
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// In this basic case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request.
session.setRepeatingRequest(combinedRequest.build(), null, null)

ターゲット サーフェスを正しく構成すると、このコードは StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) と StreamComfigurationMap.GetOutputStallDuration(int, Size) で決定される最小 FPS を満たすストリームのみを生成します。実際のパフォーマンスはデバイスによって異なりますが、Android では、出力タイプ、出力サイズ、ハードウェア レベルの 3 つの変数に応じて、特定の組み合わせをサポートすることが保証されています。

サポートされていない変数の組み合わせを使用すると、低いフレームレートで動作する可能性があります。動作しない場合は、エラー コールバックのいずれかがトリガーされます。createCaptureSession のドキュメントには、動作が保証されているものが記載されています。

出力タイプ

出力タイプは、フレームがエンコードされる形式を指します。可能な値は、PRIV、YUV、JPEG、RAW です。これらについては、createCaptureSession のドキュメントで説明されています。

アプリケーションの出力タイプを選択する際、互換性を最大化することが目標である場合は、フレーム分析には ImageFormat.YUV_420_888 を、静止画像には ImageFormat.JPEG を使用します。プレビューや録画のシナリオでは、SurfaceView、TextureView、MediaRecorder、MediaCodec、またはRenderScript.Allocationを使用することになるでしょう。このような場合は、画像形式を指定しないでください。互換性を確保するため、内部で使用される実際の形式に関係なく、ImageFormat.PRIVATE としてカウントされます。CameraCharacteristics を指定してデバイスでサポートされている形式をクエリするには、次のコードを使用します。

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.outputFormats

出力サイズ

利用可能なすべての出力サイズは StreamConfigurationMap.getOutputSizes() にリストされますが、互換性に関連するのは PREVIEW と MAXIMUM の 2 つのみです。サイズは上限として機能します。サイズ PREVIEW のものが機能する場合、PREVIEW より小さいサイズのものも機能します。MAXIMUM の場合も同様です。これらのサイズについては、CameraDevice のドキュメントをご覧ください。

利用可能な出力サイズは、選択した形式によって異なります。CameraCharacteristics とフォーマットを指定すると、次のように、利用可能な出力サイズをクエリできます。

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // such as ImageFormat.JPEG
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.getOutputSizes(outputFormat)

カメラのプレビューと録画のユースケースでは、ターゲット クラスを使用して、サポートされているサイズを決定します。この形式は、カメラ フレームワーク自体によって処理されます。

val characteristics: CameraCharacteristics = ...
val targetClass: Class<T> = ...  // such as SurfaceView::class.java
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.getOutputSizes(targetClass)

MAXIMUM サイズを取得するには、出力サイズを面積で並べ替えて、最も大きいものを返します。

fun <T> getMaximumOutputSize(
    characteristics: CameraCharacteristics,
    targetClass: Class<T>,
    format: Int? = null
): Size {
    val config = checkNotNull(
        characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    )

    // If image format is provided, use it to determine supported sizes; or else use target class.
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)
    return allSizes.maxBy { it.height * it.width }
}

PREVIEW は、デバイスの画面解像度に最適なサイズまたは 1080p(1920x1080)のうち、いずれか小さいほうのサイズを指します。アスペクト比が画面のアスペクト比と完全に一致しない可能性があるため、全画面モードで表示するには、ストリームにレターボックスまたは切り抜きを適用する必要がある場合があります。適切なプレビュー サイズを取得するには、ディスプレイが回転する可能性があることを考慮しながら、利用可能な出力サイズとディスプレイ サイズを比較します。

次のコードは、サイズの比較を少し簡単にするヘルパークラス SmartSize を定義しています。

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
    var size = Size(width, height)
    var long = max(size.width, size.height)
    var short = min(size.width, size.height)
    override fun toString() = "SmartSize(${long}x${short})"
}

/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)

/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
    val outPoint = Point()
    display.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

/**
 * Returns the largest available PREVIEW size. For more information, see:
 * https://d.android.com/reference/android/hardware/camera2/CameraDevice
 */
fun<T> getPreviewOutputSize(
        display: Display,
        characteristics: CameraCharacteristics,
        targetClass: Class<T>,
        format: Int? = null
): Size {

    // Find which is smaller: screen or 1080p
    val screenSize = getDisplaySmartSize(display)
    val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
    val maxSize = if (hdScreen) SIZE_1080P else screenSize

    // If image format is provided, use it to determine supported sizes; else use target class
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    if (format == null)
        assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
    else
        assert(config.isOutputSupportedFor(format))
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // Get available sizes and sort them by area from largest to smallest
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // Then, get the largest output size that is smaller or equal than our max size
    return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}

サポートされているハードウェア レベルを確認する

実行時に利用可能な機能を判断するには、CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL を使用して、サポートされているハードウェア レベルを確認します。

CameraCharacteristics オブジェクトを使用すると、1 つのステートメントでハードウェア レベルを取得できます。

val characteristics: CameraCharacteristics = ...

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
)

すべての要素を組み合わせる

出力タイプ、出力サイズ、ハードウェア レベルを使用して、有効なストリームの組み合わせを決定できます。次の表は、LEGACY ハードウェア レベルの CameraDevice でサポートされている構成のスナップショットです。

目標 1 目標 2 目標 3 サンプルのユースケース
タイプ 最大サイズ タイプ 最大サイズ タイプ 最大サイズ
PRIV MAXIMUM シンプルなプレビュー、GPU 動画処理、プレビューなしの動画録画。
JPEG MAXIMUM ビューファインダーなしの静止画キャプチャ。
YUV MAXIMUM アプリ内での動画 / 画像処理。
PRIV PREVIEW JPEG MAXIMUM 標準の静止画撮影。
YUV PREVIEW JPEG MAXIMUM アプリ内処理と静止画撮影。
PRIV PREVIEW PRIV PREVIEW 標準録画。
PRIV PREVIEW YUV PREVIEW プレビューとアプリ内処理。
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM 静止画キャプチャとアプリ内処理。

LEGACY は、可能な限り低いハードウェア レベルです。この表は、Camera2(API レベル 21 以降)をサポートするすべてのデバイスが、適切な構成を使用し、メモリ、CPU、熱の制約など、パフォーマンスを制限するオーバーヘッドが大きすぎない場合、最大 3 つの同時ストリームを出力できることを示しています。

アプリは、ターゲット出力バッファも構成する必要があります。たとえば、LEGACY ハードウェア レベルのデバイスをターゲットにするには、2 つのターゲット出力サーフェスを設定します。1 つは ImageFormat.PRIVATE を使用し、もう 1 つは ImageFormat.YUV_420_888 を使用します。これは、PREVIEW サイズを使用する場合にサポートされる組み合わせです。このドキュメントで前述した関数を使用して、カメラ ID に必要なプレビュー サイズを取得するには、次のコードが必要です。

val characteristics: CameraCharacteristics = ...
val context = this as Context // assuming you are inside of an activity

val surfaceViewSize = getPreviewOutputSize(
    context, characteristics, SurfaceView::class.java
)
val imageReaderSize = getPreviewOutputSize(
    context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888
)

提供されているコールバックを使用して、SurfaceView の準備が整うまで待機する必要があります。

val surfaceView = findViewById<SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // You do not need to specify image format, and it will be considered of type PRIV.
        // Surface is now ready and you could use it as an output target for CameraSession.
    }
    // ...
})

SurfaceHolder.setFixedSize() を呼び出すことで、SurfaceView をカメラの出力サイズに強制的に一致させることができます。または、GitHub のカメラサンプルの Common モジュールの AutoFitSurfaceView と同様のアプローチを取ることもできます。このアプローチでは、アスペクト比と利用可能なスペースの両方を考慮して絶対サイズを設定し、アクティビティの変更がトリガーされたときに自動調整します。

選択したフォーマットで ImageReader から他のサーフェスを設定する方が簡単です。待機するコールバックがないためです。

val frameBufferCount = 3 // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
    imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
    frameBufferCount
)

ImageReader などのブロッキング ターゲット バッファを使用する場合は、フレームを使用後に破棄します。

imageReader.setOnImageAvailableListener({
    val frame = it.acquireNextImage()
    // Do something with "frame" here.
    it.close()
}, null)

LEGACY ハードウェア レベルは、最大公約数的なデバイスを対象としています。条件分岐を追加して、LIMITED ハードウェア レベルのデバイスでは出力ターゲット サーフェスの 1 つに RECORD サイズを使用し、FULL ハードウェア レベルのデバイスでは MAXIMUM サイズに増やすこともできます。