マルチカメラ API

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

マルチカメラは Android 9(API レベル 28)で導入されました。リリース以降、この API をサポートするデバイスが市場に登場しています。マルチカメラのユースケースの多くは、特定のハードウェア構成と密結合されています。つまり、すべてのユースケースがすべてのデバイスに対応しているわけではないため、マルチカメラ機能は Play Feature Delivery の有力な候補となります。

一般的なユースケースは次のとおりです。

  • ズーム: クロップ領域または選択したレンズ焦点距離に応じてカメラを切り替えます。
  • 深度: 複数のカメラを使用して深度マップを作成します。
  • ボケ: 推定された深度情報を使用して、デジタル一眼レフカメラのような狭い被写界深度をシミュレートします。

論理カメラと物理カメラの違い

マルチカメラ API を理解するには、論理カメラと物理カメラの違いを理解する必要があります。参考までに、3 つの背面カメラを搭載したデバイスを考えてみましょう。この例では、3 つの背面カメラのそれぞれが物理カメラと見なされます。論理カメラは、これらの物理カメラを 2 つ以上グループ化したものです。論理カメラの出力は、基盤となる物理カメラの 1 つからのストリーム、または複数の基盤となる物理カメラからの融合ストリームになります。いずれの場合も、ストリームはカメラの Hardware Abstraction Layer(HAL)によって処理されます。

多くのスマートフォン メーカーは、ファーストパーティのカメラアプリを開発しており、通常はデバイスにプリインストールされています。ハードウェアのすべての機能を使用するために、非公開 API や非表示 API を使用したり、他のアプリがアクセスできないドライバ実装から特別な処理を受けたりすることがあります。一部のデバイスでは、異なる物理カメラからのフレームの融合ストリームを提供することで論理カメラのコンセプトを実装していますが、これは特定の特権アプリにのみ提供されます。多くの場合、フレームワークに公開されるのは物理カメラの 1 つだけです。Android 9 以前のサードパーティ デベロッパーの状況を次の図に示します。

通常は特権アプリのみが利用できるカメラ機能
図 1. 通常は特権アプリでのみ利用可能なカメラ機能

Android 9 以降、Android アプリで非公開 API を使用することはできなくなりました。フレームワークにマルチカメラ サポートが組み込まれたことで、Android のベスト プラクティスでは、同じ方向を向いたすべての物理カメラに対して論理カメラを公開することが、スマートフォン メーカーに強く推奨されています。Android 9 以降を搭載したデバイスでサードパーティ デベロッパーが想定すべき動作は次のとおりです。

Android 9 以降のすべてのカメラデバイスへの完全なデベロッパー アクセス
図 2. Android 9 以降、デベロッパーはすべてのカメラデバイスにフルアクセス可能

論理カメラが提供するものは、カメラ HAL の OEM 実装によって異なります。たとえば、Google Pixel 3 などのデバイスは、リクエストされたレンズ焦点距離と切り抜き領域に基づいて物理カメラの 1 つを選択するように論理カメラを実装しています。

マルチカメラ API

新しい API では、次の新しい定数、クラス、メソッドが追加されています。

Android 互換性定義ドキュメント(CDD)の変更により、マルチカメラ API にはデベロッパーからの一定の期待も寄せられています。デュアルカメラを搭載したデバイスは Android 9 より前から存在していましたが、複数のカメラを同時に開くには試行錯誤が必要でした。Android 9 以降では、マルチカメラに一連のルールが用意されており、同じ論理カメラの一部である物理カメラのペアを開くことができるタイミングを指定できます。

ほとんどの場合、Android 9 以降を搭載したデバイスは、すべての物理カメラ(赤外線などの一般的でないセンサータイプを除く)と、より使いやすい論理カメラを公開します。動作が保証されているストリームの組み合わせごとに、論理カメラに属する 1 つのストリームを、構成要素である物理カメラからの 2 つのストリームに置き換えることができます。

複数のストリームを同時に

複数のカメラ ストリームを同時に使用するでは、1 台のカメラで複数のストリームを同時に使用するためのルールについて説明しています。1 つの注目すべき追加機能により、複数のカメラに同じルールが適用されます。CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA では、1 つの論理 YUV_420_888 または RAW ストリームを 2 つの物理ストリームで置き換える方法について説明しています。つまり、YUV または RAW タイプの各ストリームを、同じタイプとサイズの 2 つのストリームに置き換えることができます。シングルカメラ デバイスの場合、次の保証された構成のカメラ ストリームから開始できます。

  • ストリーム 1: YUV タイプ、論理カメラ id = 0 からの MAXIMUM サイズ

次に、マルチカメラをサポートするデバイスで、その論理 YUV ストリームを 2 つの物理ストリームに置き換えるセッションを作成します。

  • ストリーム 1: YUV タイプ、物理カメラ id = 1 からの MAXIMUM サイズ
  • ストリーム 2: YUV タイプ、物理カメラ id = 2 からの MAXIMUM サイズ

YUV または RAW ストリームを 2 つの同等のストリームに置き換えることができるのは、それら 2 つのカメラが CameraCharacteristics.getPhysicalCameraIds() にリストされている論理カメラ グループの一部である場合に限られます。

フレームワークが提供する保証は、複数の物理カメラから同時にフレームを取得するために必要な最小限のものです。ほとんどのデバイスでは追加のストリームがサポートされており、複数の物理カメラデバイスを個別に開くことができる場合もあります。フレームワークによる厳密な保証ではないため、これを行うには、デバイスごとのテストと、試行錯誤によるチューニングが必要です。

複数の物理カメラを使用したセッションの作成

マルチカメラ対応デバイスで物理カメラを使用する場合は、1 つの CameraDevice(論理カメラ)を開き、1 つのセッション内で操作します。API レベル 28 で追加された API CameraDevice.createCaptureSession(SessionConfiguration config) を使用して、単一のセッションを作成します。セッション構成には複数の出力構成があり、それぞれに出力ターゲットのセットと、必要に応じて選択された物理カメラ ID があります。

SessionConfiguration モデルと OutputConfiguration モデル
図 3. SessionConfiguration モデルと OutputConfiguration モデル

キャプチャ リクエストには、出力ターゲットが関連付けられています。フレームワークは、アタッチされている出力ターゲットに基づいて、リクエストの送信先となる物理(または論理)カメラを決定します。出力ターゲットが、物理カメラ ID とともに出力構成として送信された出力ターゲットのいずれかに対応している場合、その物理カメラがリクエストを受信して処理します。

物理カメラのペアを使用する

マルチカメラ用の Camera API に追加されたもう 1 つの機能は、論理カメラを識別し、その背後にある物理カメラを見つける機能です。論理カメラ ストリームの 1 つを置き換えるために使用できる物理カメラのペアを特定する関数を定義できます。

Kotlin

/**
 * Helper class used to encapsulate a logical camera and two underlying
 * physical cameras
 */
data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

fun findDualCameras(manager: CameraManager, facing: Int? = null): List<DualCamera> {
    val dualCameras = MutableList<DualCamera>()

    // Iterate over all the available camera characteristics
    manager.cameraIdList.map {
        Pair(manager.getCameraCharacteristics(it), it)
    }.filter {
        // Filter by cameras facing the requested direction
        facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
    }.filter {
        // Filter by logical cameras
        // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
        it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
    }.forEach {
        // All possible pairs from the list of physical cameras are valid results
        // NOTE: There could be N physical cameras as part of a logical camera grouping
        // getPhysicalCameraIds() requires API >= 28
        val physicalCameras = it.first.physicalCameraIds.toTypedArray()
        for (idx1 in 0 until physicalCameras.size) {
            for (idx2 in (idx1 + 1) until physicalCameras.size) {
                dualCameras.add(DualCamera(
                    it.second, physicalCameras[idx1], physicalCameras[idx2]))
            }
        }
    }

    return dualCameras
}

Java

/**
 * Helper class used to encapsulate a logical camera and two underlying
 * physical cameras
 */
final class DualCamera {
    final String logicalId;
    final String physicalId1;
    final String physicalId2;

    DualCamera(String logicalId, String physicalId1, String physicalId2) {
        this.logicalId = logicalId;
        this.physicalId1 = physicalId1;
        this.physicalId2 = physicalId2;
    }
}
List<DualCamera> findDualCameras(CameraManager manager, Integer facing) {
    List<DualCamera> dualCameras = new ArrayList<>();

    List<String> cameraIdList;
    try {
        cameraIdList = Arrays.asList(manager.getCameraIdList());
    } catch (CameraAccessException e) {
        e.printStackTrace();
        cameraIdList = new ArrayList<>();
    }

    // Iterate over all the available camera characteristics
    cameraIdList.stream()
            .map(id -> {
                try {
                    CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
                    return new Pair<>(characteristics, id);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                    return null;
                }
            })
            .filter(pair -> {
                // Filter by cameras facing the requested direction
                return (pair != null) &&
                        (facing == null || pair.first.get(CameraCharacteristics.LENS_FACING).equals(facing));
            })
            .filter(pair -> {
                // Filter by logical cameras
                // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
                IntPredicate logicalMultiCameraPred =
                        arg -> arg == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA;
                return Arrays.stream(pair.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES))
                        .anyMatch(logicalMultiCameraPred);
            })
            .forEach(pair -> {
                // All possible pairs from the list of physical cameras are valid results
                // NOTE: There could be N physical cameras as part of a logical camera grouping
                // getPhysicalCameraIds() requires API >= 28
                String[] physicalCameras = pair.first.getPhysicalCameraIds().toArray(new String[0]);
                for (int idx1 = 0; idx1 < physicalCameras.length; idx1++) {
                    for (int idx2 = idx1 + 1; idx2 < physicalCameras.length; idx2++) {
                        dualCameras.add(
                                new DualCamera(pair.second, physicalCameras[idx1], physicalCameras[idx2]));
                    }
                }
            });
    return dualCameras;
}

物理カメラの状態処理は、論理カメラによって制御されます。「デュアルカメラ」を開くには、物理カメラに対応する論理カメラを開きます。

Kotlin

fun openDualCamera(cameraManager: CameraManager,
                       dualCamera: DualCamera,
                       // AsyncTask is deprecated beginning API 30
                       executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                       callback: (CameraDevice) -> Unit) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(
            dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {
                override fun onOpened(device: CameraDevice) = callback(device)
                // Omitting for brevity...
                override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
                override fun onDisconnected(device: CameraDevice) = device.close()
            })
    }

Java

void openDualCamera(CameraManager cameraManager,
                        DualCamera dualCamera,
                        Executor executor,
                        CameraDeviceCallback cameraDeviceCallback
    ) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(dualCamera.logicalId, executor, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) {
               cameraDeviceCallback.callback(cameraDevice);
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                cameraDevice.close();
            }

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) {
                onDisconnected(cameraDevice);
            }
        });
    }

開くカメラを選択する以外は、以前の Android バージョンでカメラを開くプロセスと同じです。新しいセッション構成 API を使用してキャプチャ セッションを作成すると、特定のターゲットを特定の物理カメラ ID に関連付けるようフレームワークに指示します。

Kotlin

/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
typealias DualCameraOutputs =
        Triple<MutableList<Surface>?, MutableList<Surface>?, MutableList<Surface>?>

fun createDualCameraSession(cameraManager: CameraManager,
                            dualCamera: DualCamera,
                            targets: DualCameraOutputs,
                            // AsyncTask is deprecated beginning API 30
                            executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                            callback: (CameraCaptureSession) -> Unit) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }
    val outputConfigsPhysical1 = targets.second?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }
    val outputConfigsPhysical2 = targets.third?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }

    // Put all the output configurations into a single flat array
    val outputConfigsAll = arrayOf(
        outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2)
        .filterNotNull().flatMap { it }

    // Instantiate a session configuration that can be used to create a session
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) = callback(session)
            // Omitting for brevity...
            override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
        })

    // Open the logical camera using the previously defined function
    openDualCamera(cameraManager, dualCamera, executor = executor) {

        // Finally create the session and return using the callback
        it.createCaptureSession(sessionConfiguration)
    }
}

Java

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * <p>
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
final class DualCameraOutputs {
    private final List<Surface> logicalCamera;
    private final List<Surface> firstPhysicalCamera;
    private final List<Surface> secondPhysicalCamera;

    public DualCameraOutputs(List<Surface> logicalCamera, List<Surface> firstPhysicalCamera, List<Surface> third) {
        this.logicalCamera = logicalCamera;
        this.firstPhysicalCamera = firstPhysicalCamera;
        this.secondPhysicalCamera = third;
    }

    public List<Surface> getLogicalCamera() {
        return logicalCamera;
    }

    public List<Surface> getFirstPhysicalCamera() {
        return firstPhysicalCamera;
    }

    public List<Surface> getSecondPhysicalCamera() {
        return secondPhysicalCamera;
    }
}

interface CameraCaptureSessionCallback {
    void callback(CameraCaptureSession cameraCaptureSession);
}

void createDualCameraSession(CameraManager cameraManager,
                             DualCamera dualCamera,
                             DualCameraOutputs targets,
                             Executor executor,
                             CameraCaptureSessionCallback cameraCaptureSessionCallback) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    List<OutputConfiguration> outputConfigsLogical = targets.getLogicalCamera().stream()
            .map(OutputConfiguration::new)
            .collect(Collectors.toList());
    List<OutputConfiguration> outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream()
            .map(s -> {
                OutputConfiguration outputConfiguration = new OutputConfiguration(s);
                outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1);
                return outputConfiguration;
            })
            .collect(Collectors.toList());
    List<OutputConfiguration> outputConfigsPhysical2 = targets.getSecondPhysicalCamera().stream()
            .map(s -> {
                OutputConfiguration outputConfiguration = new OutputConfiguration(s);
                outputConfiguration.setPhysicalCameraId(dualCamera.physicalId2);
                return outputConfiguration;
            })
            .collect(Collectors.toList());

    // Put all the output configurations into a single flat array
    List<OutputConfiguration> outputConfigsAll = Stream.of(
                    outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2
            )
            .filter(Objects::nonNull)
            .flatMap(Collection::stream)
            .collect(Collectors.toList());

    // Instantiate a session configuration that can be used to create a session
    SessionConfiguration sessionConfiguration = new SessionConfiguration(
            SessionConfiguration.SESSION_REGULAR,
            outputConfigsAll, executor, new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
            cameraCaptureSessionCallback.callback(cameraCaptureSession);
        }
        // Omitting for brevity...
        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
            cameraCaptureSession.getDevice().close();
        }
    });

    // Open the logical camera using the previously defined function
    openDualCamera(cameraManager, dualCamera, executor, (CameraDevice c) ->
            // Finally create the session and return using the callback
            c.createCaptureSession(sessionConfiguration));
}

サポートされているストリームの組み合わせについては、createCaptureSession をご覧ください。ストリームの結合は、1 つの論理カメラ上の複数のストリームを対象としています。この互換性は、同じ構成を使用し、それらのストリームの 1 つを、同じ論理カメラの一部である 2 つの物理カメラからの 2 つのストリームに置き換える場合にも適用されます。

カメラ セッションの準備ができたら、選択したキャプチャ リクエストをディスパッチします。キャプチャ リクエストの各ターゲットは、関連付けられた物理カメラ(使用中の場合)からデータを受信するか、論理カメラにフォールバックします。

Zoom のユースケースの例

物理カメラを 1 つのストリームに統合することで、ユーザーは異なる物理カメラを切り替えて、異なる画角を体験し、異なる「ズームレベル」を効果的にキャプチャできます。

ズームレベルのユースケースでカメラを切り替える例
図 4. ズームレベルのユースケースでカメラを切り替える例(Google Pixel 3 の広告より)

まず、ユーザーが切り替えられるようにする物理カメラのペアを選択します。効果を最大にするには、利用可能な最小レンズ焦点距離と最大レンズ焦点距離を提供するカメラのペアを選択します。

Kotlin

fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

    return findDualCameras(manager, facing).map {
        val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
        val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

        // Query the focal lengths advertised by each physical camera
        val focalLengths1 = characteristics1.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
        val focalLengths2 = characteristics2.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)

        // Compute the largest difference between min and max focal lengths between cameras
        val focalLengthsDiff1 = focalLengths2.maxOrNull()!! - focalLengths1.minOrNull()!!
        val focalLengthsDiff2 = focalLengths1.maxOrNull()!! - focalLengths2.minOrNull()!!

        // Return the pair of camera IDs and the difference between min and max focal lengths
        if (focalLengthsDiff1 < focalLengthsDiff2) {
            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
        } else {
            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
        }

        // Return only the pair with the largest difference, or null if no pairs are found
    }.maxByOrNull { it.second }?.first
}

Java

// Utility functions to find min/max value in float[]
    float findMax(float[] array) {
        float max = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            max = Math.max(max, cur);
        return max;
    }
    float findMin(float[] array) {
        float min = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            min = Math.min(min, cur);
        return min;
    }

DualCamera findShortLongCameraPair(CameraManager manager, Integer facing) {
        return findDualCameras(manager, facing).stream()
                .map(c -> {
                    CameraCharacteristics characteristics1;
                    CameraCharacteristics characteristics2;
                    try {
                        characteristics1 = manager.getCameraCharacteristics(c.physicalId1);
                        characteristics2 = manager.getCameraCharacteristics(c.physicalId2);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                        return null;
                    }

                    // Query the focal lengths advertised by each physical camera
                    float[] focalLengths1 = characteristics1.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
                    float[] focalLengths2 = characteristics2.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);

                    // Compute the largest difference between min and max focal lengths between cameras
                    Float focalLengthsDiff1 = findMax(focalLengths2) - findMin(focalLengths1);
                    Float focalLengthsDiff2 = findMax(focalLengths1) - findMin(focalLengths2);

                    // Return the pair of camera IDs and the difference between min and max focal lengths
                    if (focalLengthsDiff1 < focalLengthsDiff2) {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId1, c.physicalId2), focalLengthsDiff1);
                    } else {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId2, c.physicalId1), focalLengthsDiff2);
                    }

                }) // Return only the pair with the largest difference, or null if no pairs are found
                .max(Comparator.comparing(pair -> pair.second)).get().first;
    }

この場合の適切なアーキテクチャは、2 つの SurfaceViews(ストリームごとに 1 つ)を使用することです。これらの SurfaceViews は、ユーザー操作に基づいて入れ替えられ、一度に 1 つだけ表示されます。

次のコードは、論理カメラを開き、カメラ出力を構成し、カメラ セッションを作成して、2 つのプレビュー ストリームを開始する方法を示しています。

Kotlin

val cameraManager: CameraManager = ...

// Get the two output targets from the activity / fragment
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(manager)!!
val outputTargets = DualCameraOutputs(
    null, mutableListOf(surface1), mutableListOf(surface2))

// Here you open the logical camera, configure the outputs and create a session
createDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->

  // Create a single request which has one target for each physical camera
  // NOTE: Each target receive frames from only its associated physical camera
  val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
  val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
    arrayOf(surface1, surface2).forEach { addTarget(it) }
  }.build()

  // Set the sticky request for the session and you are done
  session.setRepeatingRequest(captureRequest, null, null)
}

Java

CameraManager manager = ...;

        // Get the two output targets from the activity / fragment
        Surface surface1 = ...;  // from SurfaceView
        Surface surface2 = ...;  // from SurfaceView

        DualCamera dualCamera = findShortLongCameraPair(manager, null);
                DualCameraOutputs outputTargets = new DualCameraOutputs(
                null, Collections.singletonList(surface1), Collections.singletonList(surface2));

        // Here you open the logical camera, configure the outputs and create a session
        createDualCameraSession(manager, dualCamera, outputTargets, null, (session) -> {
            // Create a single request which has one target for each physical camera
            // NOTE: Each target receive frames from only its associated physical camera
            CaptureRequest.Builder captureRequestBuilder;
            try {
                captureRequestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                Arrays.asList(surface1, surface2).forEach(captureRequestBuilder::addTarget);

                // Set the sticky request for the session and you are done
                session.setRepeatingRequest(captureRequestBuilder.build(), null, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        });

あとは、ボタンや SurfaceView のダブルタップなど、ユーザーが 2 つのサーフェスを切り替えるための UI を提供するだけです。なんらかのシーン分析を行い、2 つのストリームを自動的に切り替えることもできます。

レンズの歪み

すべてのレンズには、ある程度の歪みが生じます。Android では、CameraCharacteristics.LENS_DISTORTION を使用して、レンズによって生じる歪みをクエリできます。これは、非推奨となった CameraCharacteristics.LENS_RADIAL_DISTORTION に代わるものです。論理カメラの場合、歪みは最小限に抑えられ、アプリはカメラから取得したフレームをほぼそのまま使用できます。物理カメラの場合、特に広角レンズでは、レンズ構成が大きく異なる可能性があります。

一部のデバイスでは、CaptureRequest.DISTORTION_CORRECTION_MODE を使用して自動歪み補正を実装している場合があります。歪み補正は、ほとんどのデバイスでデフォルトでオンになっています。

Kotlin

val cameraSession: CameraCaptureSession = ...

        // Use still capture template to build the capture request
        val captureRequest = cameraSession.device.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE
        )

        // Determine if this device supports distortion correction
        val characteristics: CameraCharacteristics = ...
        val supportsDistortionCorrection = characteristics.get(
            CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
        )?.contains(
            CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
        ) ?: false

        if (supportsDistortionCorrection) {
            captureRequest.set(
                CaptureRequest.DISTORTION_CORRECTION_MODE,
                CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            )
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequest.build(), ...)

Java

CameraCaptureSession cameraSession = ...;

        // Use still capture template to build the capture request
        CaptureRequest.Builder captureRequestBuilder = null;
        try {
            captureRequestBuilder = cameraSession.getDevice().createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        // Determine if this device supports distortion correction
        CameraCharacteristics characteristics = ...;
        boolean supportsDistortionCorrection = Arrays.stream(
                        characteristics.get(
                                CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
                        ))
                .anyMatch(i -> i == CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY);
        if (supportsDistortionCorrection) {
            captureRequestBuilder.set(
                    CaptureRequest.DISTORTION_CORRECTION_MODE,
                    CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            );
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequestBuilder.build(), ...);

このモードでキャプチャ リクエストを設定すると、カメラで生成できるフレームレートに影響する可能性があります。歪み補正を静止画画像キャプチャのみに設定することもできます。