다중 카메라 API

참고: 이 페이지에서는 Camera2 패키지에 관해 다룹니다. 앱에 Camera2의 특정 하위 수준 기능이 필요한 경우가 아니라면 CameraX를 사용하는 것이 좋습니다. CameraX와 Camera2는 모두 Android 5.0(API 수준 21) 이상을 지원합니다.

다중 카메라는 Android 9 (API 수준 28)에서 도입되었습니다. API를 지원하는 기기가 출시되면서 출시되었습니다. 다중 카메라 사용 사례는 대부분 특정 하드웨어 구성과 밀접하게 연결되어 있습니다. 즉, 모든 사용 사례가 모든 기기와 호환되는 것은 아니므로 다중 카메라 기능은 Play 기능 제공에 적합합니다.

몇 가지 일반적인 사용 사례는 다음과 같습니다.

  • 확대/축소: 자르기 영역 또는 원하는 초점 거리에 따라 카메라 간에 전환합니다.
  • 깊이: 여러 카메라를 사용하여 깊이 맵을 만듭니다.
  • 빛망울 효과: 추론된 심도 정보를 사용하여 DSLR과 유사한 좁은 초점 범위를 시뮬레이션합니다.

논리 카메라와 물리적 카메라의 차이점

다중 카메라 API를 이해하려면 논리 카메라와 물리적 카메라의 차이점을 이해해야 합니다. 참고로 후면 카메라가 3개인 기기를 생각해 보세요. 이 예에서는 후면 카메라 3대 각각이 물리적 카메라로 간주됩니다. 그러면 논리 카메라는 이러한 물리적 카메라 두 개 이상을 그룹화한 것입니다. 논리 카메라의 출력은 기본 물리적 카메라 중 하나에서 오는 스트림 또는 두 개 이상의 기본 물리적 카메라에서 동시에 유입되는 융합 스트림일 수 있습니다. 어느 쪽이든 스트림은 카메라 하드웨어 추상화 계층 (HAL)에 의해 처리됩니다.

많은 휴대전화 제조업체에서 자사 카메라 애플리케이션을 개발하며, 이 애플리케이션은 일반적으로 기기에 미리 설치되어 있습니다. 하드웨어의 모든 기능을 사용하기 위해 비공개 또는 숨겨진 API를 사용하거나 다른 애플리케이션이 액세스할 수 없는 드라이버 구현으로부터 특별한 처리를 받을 수 있습니다. 일부 기기는 여러 물리적 카메라의 합성 프레임 스트림을 제공하여 논리 카메라의 개념을 구현합니다. 단, 권한이 있는 특정 애플리케이션에만 적용됩니다. 물리적 카메라 중 하나만 프레임워크에 노출되는 경우가 많습니다. Android 9 이전의 서드 파티 개발자의 상황은 다음 다이어그램에 설명되어 있습니다.

그림 1. 카메라 기능은 일반적으로 권한이 있는 애플리케이션에서만 사용할 수 있습니다.

Android 9부터는 Android 앱에서 비공개 API가 더 이상 허용되지 않습니다. Android 권장사항에 따라 프레임워크에 다중 카메라 지원이 포함되면서 휴대전화 제조업체는 같은 방향을 바라보는 모든 물리적 카메라에 논리 카메라를 노출할 것을 적극 권장합니다. Android 9 이상을 실행하는 기기에서 서드 파티 개발자에게 표시되는 내용은 다음과 같습니다.

그림 2. Android 9부터 모든 카메라 기기에 대한 전체 개발자 액세스 권한

논리 카메라가 제공하는 것은 카메라 HAL의 OEM 구현에 따라 완전히 달라집니다. 예를 들어 Pixel 3와 같은 기기는 요청된 초점 거리와 자르기 영역에 따라 물리적 카메라 중 하나를 선택하는 방식으로 논리 카메라를 구현합니다.

다중 카메라 API

새 API에는 다음과 같은 새로운 상수, 클래스, 메서드가 추가됩니다.

Android 호환성 정의 문서 (CDD)의 변경사항으로 인해 다중 카메라 API도 개발자의 특정 기대치를 가지고 제공됩니다. Android 9 이전에는 듀얼 카메라가 장착된 기기가 있었지만 두 개 이상의 카메라를 동시에 열면 시행착오가 발생했습니다. Android 9 이상에서는 다중 카메라가 동일한 논리 카메라의 일부인 물리적 카메라 한 쌍을 열 수 있는 경우를 지정하는 일련의 규칙을 제공합니다.

대부분의 경우 Android 9 이상을 실행하는 기기는 사용하기 쉬운 논리 카메라와 함께 모든 물리적 카메라 (적외선과 같이 덜 일반적인 센서 유형 제외)를 노출합니다. 작동이 보장되는 스트림의 모든 조합에 관해 논리 카메라에 속하는 하나의 스트림을 기본 물리적 카메라의 스트림 두 개로 대체할 수 있습니다.

여러 스트림을 동시에

여러 카메라 스트림을 동시에 사용은 단일 카메라에서 여러 스트림을 동시에 사용하기 위한 규칙을 다룹니다. 주목할 만한 사항이 추가되면 여러 카메라에 동일한 규칙이 적용됩니다. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA는 논리적 YUV_420_888 또는 원시 스트림을 두 개의 물리적 스트림으로 교체하는 방법을 설명합니다. 즉, YUV 또는 RAW 유형의 각 스트림을 동일한 유형과 크기의 두 개의 스트림으로 대체할 수 있습니다. 단일 카메라 기기의 경우 다음과 같은 보장된 구성의 카메라 스트림으로 시작할 수 있습니다.

  • 스트림 1: YUV 유형, 논리 카메라 id = 0MAXIMUM 크기

그런 다음 다중 카메라를 지원하는 기기를 사용하면 이 논리적 YUV 스트림을 두 개의 물리적 스트림으로 대체하는 세션을 만들 수 있습니다.

  • 스트림 1: YUV 유형, 물리적 카메라의 MAXIMUM 크기 id = 1
  • 스트림 2: YUV 유형, 물리적 카메라의 MAXIMUM 크기 id = 2

YUV 또는 RAW 스트림을 동등한 두 스트림으로 교체하려면 두 카메라가 CameraCharacteristics.getPhysicalCameraIds()에 나열된 논리 카메라 그룹의 일부인 경우에만 가능합니다.

프레임워크에서 제공하는 보장은 두 개 이상의 실제 카메라에서 동시에 프레임을 가져오는 데 필요한 최소한의 최소값일 뿐입니다. 추가 스트림은 대부분의 기기에서 지원되며 경우에 따라 여러 물리적 카메라 기기를 독립적으로 열 수도 있습니다. 프레임워크에서 확실하게 보장하지 않으므로 그렇게 하려면 시행착오를 사용하여 기기별 테스트와 조정을 실행해야 합니다.

물리적 카메라 여러 대를 사용하는 세션 만들기

다중 카메라 지원 기기에서 물리적 카메라를 사용하는 경우 단일 CameraDevice (논리 카메라)를 열고 단일 세션 내에서 상호작용합니다. API 수준 28에 추가된 API CameraDevice.createCaptureSession(SessionConfiguration config)를 사용하여 단일 세션을 만듭니다. 세션 구성에는 여러 출력 구성이 있으며 각 구성에는 출력 타겟 집합과 원하는 물리적 카메라 ID(선택사항)가 있습니다.

그림 3. SessionConfiguration 및 OutputConfiguration 모델

캡처 요청에는 출력 대상이 연결되어 있습니다. 프레임워크는 연결된 출력 대상에 따라 요청이 전송되는 실제 (또는 논리) 카메라를 결정합니다. 출력 타겟이 실제 카메라 ID와 함께 출력 구성으로 전송된 출력 타겟 중 하나에 해당하면 이 물리적 카메라가 요청을 수신하여 처리합니다.

물리적 카메라 한 쌍 사용

다중 카메라용 Camera API에 추가된 또 다른 기능은 논리 카메라를 식별하고 카메라 뒤에 있는 물리적 카메라를 찾는 기능입니다. 논리 카메라 스트림 중 하나를 교체하는 데 사용할 수 있는 물리적 카메라 쌍을 식별하는 데 도움이 되는 함수를 정의할 수 있습니다.

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 {
        val dualCameras = MutableList()

        // 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 findDualCameras(CameraManager manager, Integer facing) {
        List dualCameras = new ArrayList<>();

        List 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?, MutableList?>

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 via callback
        it.createCaptureSession(sessionConfiguration)
    }
}

Java

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * 

* 1. Logical camera * 2. First physical camera * 3. Second physical camera */ final class DualCameraOutputs { private final List logicalCamera; private final List firstPhysicalCamera; private final List secondPhysicalCamera; public DualCameraOutputs(List logicalCamera, List firstPhysicalCamera, List third) { this.logicalCamera = logicalCamera; this.firstPhysicalCamera = firstPhysicalCamera; this.secondPhysicalCamera = third; } public List getLogicalCamera() { return logicalCamera; } public List getFirstPhysicalCamera() { return firstPhysicalCamera; } public List 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 outputConfigsLogical = targets.getLogicalCamera().stream() .map(OutputConfiguration::new) .collect(Collectors.toList()); List outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream() .map(s -> { OutputConfiguration outputConfiguration = new OutputConfiguration(s); outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1); return outputConfiguration; }) .collect(Collectors.toList()); List 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 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 via callback c.createCaptureSession(sessionConfiguration)); }

지원되는 스트림 조합에 관한 자세한 내용은 createCaptureSession를 참고하세요. 스트림 결합은 단일 논리 카메라의 여러 스트림을 위한 것입니다. 호환성은 동일한 구성을 사용하고 이러한 스트림 중 하나를 동일한 논리 카메라의 일부인 두 개의 물리적 카메라의 두 스트림으로 교체하는 것으로 확장됩니다.

카메라 세션이 준비되면 원하는 캡처 요청을 전달합니다. 캡처 요청의 각 대상은 연결된 물리적 카메라(사용 중인 경우)로부터 데이터를 수신하거나 논리 카메라로 대체됩니다.

Zoom 사용 사례 예시

실제 카메라를 단일 스트림으로 병합하여 사용자가 여러 실제 카메라 간에 전환하여 다양한 시야를 경험함으로써 다양한 '확대/축소 수준'을 효과적으로 캡처할 수 있습니다.

그림 4. 확대/축소 수준 사용 사례에 맞게 카메라를 전환하는 방법의 예 (Pixel 3 Ad)

먼저 사용자가 전환할 수 있는 물리적 카메라 쌍을 선택합니다. 효과를 극대화하려면 사용 가능한 최소 및 최대 초점 길이를 제공하는 카메라 쌍을 선택하면 됩니다.

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;
    }

이를 위한 적절한 아키텍처는 스트림마다 하나씩, 두 개의 SurfaceViews를 갖는 것입니다. 이러한 SurfaceViews는 사용자 상호작용에 따라 교체되어 주어진 시점에 하나만 표시됩니다.

다음 코드는 논리 카메라를 열고, 카메라 출력을 구성하고, 카메라 세션을 만들고, 두 개의 미리보기 스트림을 시작하는 방법을 보여줍니다.

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();
            }
        });

이제 사용자가 두 노출 영역 간에 전환할 수 있는 UI를 제공하기만 하면 됩니다(예: 버튼을 사용하거나 SurfaceView를 두 번 탭). 일종의 장면 분석을 실행하고 두 스트림 간에 자동으로 전환할 수도 있습니다.

렌즈 왜곡

모든 렌즈는 어느 정도의 왜곡을 일으킵니다. Android에서는 현재 지원 중단된 CameraCharacteristics.LENS_RADIAL_DISTORTION를 대체하는 CameraCharacteristics.LENS_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(), ...);

이 모드에서 캡처 요청을 설정하면 카메라가 생성할 수 있는 프레임 속도에 영향을 미칠 수 있습니다. 스틸 이미지 캡처에만 왜곡 수정을 설정할 수 있습니다.