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

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

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

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

フレームを処理する際には、無視できないパフォーマンス コストが発生します。並列ストリームまたはパイプライン処理を行う場合は、このコストが乗算されます。

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

リクエストあたりの複数のターゲット

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

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 JPEG MAXIMUM キャプチャ プラスのアプリ内処理はまだ行われています。

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

アプリでは、ターゲティング出力バッファも構成する必要があります。たとえば、LEGACY ハードウェア レベルのデバイスをターゲットにするには、ImageFormat.PRIVATE を使用するターゲット出力サーフェスと ImageFormat.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 のカメラ サンプルの Common モジュール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 サイズに増やすこともできます。