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

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

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

  • 動画の録画: プレビュー用のストリームと、エンコードされてファイルに保存されるストリーム。
  • バーコード スキャン: プレビュー用とバーコード検出用のストリームがあります。
  • 計算写真学: プレビュー用のストリームと顔やシーンの検出用のストリーム。

フレーム処理のパフォーマンス コストはごくわずかであり、並列ストリーム処理やパイプライン処理を行うとコストが乗算されます。

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

リクエストごとに複数のターゲット

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

Kotlin

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 simple 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)

Java

CameraCaptureSession session = …;  // 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
        CaptureRequest.Builder combinedRequest = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

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

// In this simple 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 を使用します。プレビューと録画のシナリオでは、SurfaceViewTextureViewMediaRecorderMediaCodecRenderScript.Allocation を使用する可能性があります。このような場合、画像形式を指定しないでください。互換性のため、内部で使用される実際の形式に関係なく、ImageFormat.PRIVATE としてカウントされます。指定された CameraCharacteristics のデバイスでサポートされている形式をクエリするには、次のコードを使用します。

Kotlin

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

Java

CameraCharacteristics characteristics = …;
        int[] supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputFormats();

出力サイズ

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

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

Kotlin

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

Java

CameraCharacteristics characteristics = …;
        int outputFormat = …;  // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

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

Kotlin

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

Java

CameraCharacteristics characteristics = …;
   int outputFormat = …;  // such as ImageFormat.JPEG
   Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

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

Kotlin

fun <T>getMaximumOutputSize(
    characteristics: CameraCharacteristics, targetClass: Class <T>, format: Int? = null):
    Size {
  val config = 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 }
}

Java

 @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getMaximumOutputSize(CameraCharacteristics characteristics,
                                            Class <T> targetClass,
                                            Integer format) {
        StreamConfigurationMap config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

        // If image format is provided, use it to determine supported sizes; else use target class
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }
        return Arrays.stream(allSizes).max(Comparator.comparing(s -> s.getHeight() * s.getWidth())).get();
    }

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

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

Kotlin

/** 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
}

Java

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
    class SmartSize {
        Size size;
        double longSize;
        double shortSize;

        public SmartSize(Integer width, Integer height) {
            size = new Size(width, height);
            longSize = max(size.getWidth(), size.getHeight());
            shortSize = min(size.getWidth(), size.getHeight());
        }

        @Override
        public String toString() {
            return String.format("SmartSize(%sx%s)", longSize, shortSize);
        }
    }

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

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

    /**
     * Returns the largest available PREVIEW size. For more information, see:
     * https://d.android.com/reference/android/hardware/camera2/CameraDevice
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getPreviewOutputSize(
            Display display,
            CameraCharacteristics characteristics,
            Class <T> targetClass,
            Integer format
    ){

        // Find which is smaller: screen or 1080p
        SmartSize screenSize = getDisplaySmartSize(display);
        boolean hdScreen = screenSize.longSize >= SIZE_1080P.longSize || screenSize.shortSize >= SIZE_1080P.shortSize;
        SmartSize maxSize;
        if (hdScreen) {
            maxSize = SIZE_1080P;
        } else {
            maxSize = screenSize;
        }

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

        // Get available sizes and sort them by area from largest to smallest
        List <Size> sortedSizes = Arrays.asList(allSizes);
        List <SmartSize> validSizes =
                sortedSizes.stream()
                        .sorted(Comparator.comparing(s -> s.getHeight() * s.getWidth()))
                        .map(s -> new SmartSize(s.getWidth(), s.getHeight()))
                        .sorted(Collections.reverseOrder()).collect(Collectors.toList());

        // Then, get the largest output size that is smaller or equal than our max size
        return validSizes.stream()
                .filter(s -> s.longSize <= maxSize.longSize && s.shortSize <= maxSize.shortSize)
                .findFirst().get().size;
    }

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

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

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

Kotlin

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)

Java

CameraCharacteristics characteristics = ...;

// 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
Integer 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 プレビューとアプリ内処理。
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM 静止画撮影とアプリ内処理。

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

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

Kotlin

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)

Java

CameraCharacteristics characteristics = ...;
        Context context = this; // assuming you are inside of an activity

        Size surfaceViewSize = getPreviewOutputSize(
                context, characteristics, SurfaceView.class);
        Size imageReaderSize = getPreviewOutputSize(
                context, characteristics, ImageReader.class, format = ImageFormat.YUV_420_888);

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

Kotlin

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
  }
  ...
})

Java

SurfaceView surfaceView = findViewById <SurfaceView>(...);

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder 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 のカメラサンプルの共通モジュールにある AutoFitSurfaceView と同様のアプローチを取ることもできます。ここでは、アスペクト比と使用可能なスペースの両方を考慮しながら、アクティビティの変更がトリガーされたときに自動的に調整しながら、絶対サイズを設定します。

待機するコールバックがないため、ImageReader から目的のフォーマットでもう一方のサーフェスをセットアップするのは簡単です。

Kotlin

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)

Java

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

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

Kotlin

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

Java

imageReader.setOnImageAvailableListener(listener -> {
            Image frame = listener.acquireNextImage();
            // Do something with "frame" here
            listener.close();
        }, null);

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