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

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

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

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

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

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

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 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 を指定してデバイスでサポートされている形式をクエリするには、次のコードを使用します。

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() にリストされますが、互換性に関連するのは PREVIEW と MAXIMUM の 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 JPEG MAXIMUM 静止画キャプチャとアプリ内処理。

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

アプリは、ターゲット出力バッファも構成する必要があります。たとえば、LEGACY ハードウェア レベルのデバイスをターゲットにするには、2 つのターゲット出力サーフェスを設定します。1 つは ImageFormat.PRIVATE を使用し、もう 1 つは ImageFormat.YUV_420_888 を使用します。これは、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 サイズに増やしたりすることもできます。