다중 카메라 API

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

멀티 카메라는 Android 9 (API 수준 28)에서 도입되었습니다. 출시 이후 이 API를 지원하는 기기가 시장에 출시되었습니다. 많은 다중 카메라 사용 사례는 특정 하드웨어 구성과 긴밀하게 결합되어 있습니다. 즉, 모든 사용 사례가 모든 기기와 호환되는 것은 아니므로  멀티 카메라 기능은 Play Feature Delivery에 적합합니다.

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

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

논리적 카메라와 실제 카메라의 차이점

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

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

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

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

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 또는 RAW 스트림을 두 개의 물리적 스트림으로 대체하는 방법을 설명합니다. 즉, YUV 또는 RAW 유형의 각 스트림을 동일한 유형 및 크기의 두 스트림으로 대체할 수 있습니다. 단일 카메라 기기의 경우 다음과 같은 보장된 구성의 카메라 스트림으로 시작할 수 있습니다.

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

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

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

두 카메라가 CameraCharacteristics.getPhysicalCameraIds()에 나열된 논리 카메라 그룹에 속하는 경우에만 YUV 또는 RAW 스트림을 두 개의 동일한 스트림으로 대체할 수 있습니다.

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

여러 물리적 카메라로 세션 만들기

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

SessionConfiguration 및 OutputConfiguration 모델
그림 3. SessionConfiguration 및 OutputConfiguration 모델

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

물리적 카메라 한 쌍 사용

다중 카메라용 카메라 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<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
}

자바

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

자바

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

자바

/**
 * 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을 참고하세요. 스트림 결합은 단일 논리 카메라의 여러 스트림에 사용됩니다. 호환성은 동일한 구성을 사용하고 이러한 스트림 중 하나를 동일한 논리 카메라에 속한 두 개의 물리적 카메라에서 가져온 두 개의 스트림으로 대체하는 것으로 확장됩니다.

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

Zoom 사용 사례 예시

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

확대/축소 수준 사용 사례를 위한 카메라 전환 예시
그림 4. 확대/축소 수준 사용 사례를 위한 카메라 전환 예 (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
}

자바

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

자바

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_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(), ...)

자바

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(), ...);

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