API de várias câmeras

Observação:esta página se refere ao pacote Camera2. A menos que seu app exija recursos específicos e de baixo nível do Camera2, recomendamos o uso do CameraX. CameraX e Camera2 oferecem suporte ao Android 5.0 (nível 21 da API) e versões mais recentes.

O recurso de várias câmeras foi lançado no Android 9 (API de nível 28). Desde o lançamento, entraram no mercado dispositivos com suporte à API. Muitos casos de uso de várias câmeras estão fortemente associados a uma configuração de hardware específica. Em outras palavras, nem todos os casos de uso são compatíveis com todos os dispositivos,  o que torna os recursos de várias câmeras um bom candidato para o Play Feature Delivery.

Veja alguns casos de uso comuns:

  • Zoom: alterna entre câmeras, dependendo da região de corte ou do comprimento focal desejado.
  • Profundidade: use várias câmeras para criar um mapa de profundidade.
  • Bokeh: usa informações de profundidade inferidas para simular um intervalo de foco estreito semelhante a uma DSLR.

A diferença entre câmeras lógicas e físicas.

Para entender a API de várias câmeras, é preciso entender a diferença entre câmeras lógicas e físicas. Como referência, considere um dispositivo com três câmeras traseiras. Neste exemplo, cada uma das três câmeras traseiras é considerada uma câmera física. Uma câmera lógica é, então, um agrupamento de duas ou mais dessas câmeras físicas. A saída da câmera lógica pode ser um stream que vem de uma das câmeras físicas subjacentes ou um stream fundido que vem de mais de uma câmera física simultaneamente. De qualquer forma, o stream é processado pela camada de abstração de hardware (HAL) da câmera.

Muitos fabricantes de smartphones desenvolvem aplicativos de câmera próprios, que geralmente vêm pré-instalados nos dispositivos. Para usar todos os recursos do hardware, eles podem usar APIs privadas ou ocultas ou receber tratamento especial da implementação do driver a que outros aplicativos não têm acesso. Alguns dispositivos implementam o conceito de câmeras lógicas fornecendo um stream de frames de diferentes câmeras físicas, mas apenas para determinados aplicativos com privilégios. Muitas vezes, apenas uma das câmeras físicas é exposta ao framework. A situação de desenvolvedores terceirizados anteriores ao Android 9 é ilustrada no diagrama a seguir:

Figura 1. Recursos de câmera normalmente disponíveis apenas para aplicativos privilegiados

A partir do Android 9, APIs privadas não são mais permitidas em aplicativos Android. Com a inclusão do suporte a várias câmeras no framework, as práticas recomendadas do Android recomendam que os fabricantes de smartphones exponham uma câmera lógica para todas as câmeras físicas voltadas para a mesma direção. Confira a seguir o que os desenvolvedores terceirizados vão encontrar em dispositivos com o Android 9 e versões mais recentes:

Figura 2. Acesso total do desenvolvedor a todos os dispositivos de câmera a partir do Android 9

O que a câmera lógica oferece depende totalmente da implementação do OEM da HAL da câmera. Por exemplo, um dispositivo como o Pixel 3 implementa a câmera lógica de modo a escolher uma das câmeras físicas com base na distância focal e na região de corte solicitadas.

A API multicâmera

A nova API adiciona estas novas constantes, classes e métodos:

Devido a mudanças no Documento de definição de compatibilidade (CDD) do Android, a API multicâmera também vem com algumas expectativas dos desenvolvedores. Dispositivos com duas câmeras existiam antes do Android 9, mas a abertura de mais de uma câmera envolveva tentativa e erro. No Android 9 e versões mais recentes, o recurso de várias câmeras oferece um conjunto de regras para especificar quando é possível abrir um par de câmeras físicas que fazem parte da mesma câmera lógica.

Na maioria dos casos, os dispositivos com o Android 9 e versões mais recentes expõem todas as câmeras físicas (exceto possivelmente tipos de sensores menos comuns, como infravermelho), além de uma câmera lógica mais fácil de usar. Para cada combinação de streams com garantia de funcionamento, um stream pertencente a uma câmera lógica pode ser substituído por dois streams das câmeras físicas subjacentes.

Várias transmissões simultâneas

A opção Usar simultaneamente vários streams de câmera abrange as regras para vários streams simultâneos em uma única câmera. Com uma adição importante, as mesmas regras se aplicam a várias câmeras. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA explica como substituir um YUV_420_888 lógico ou um stream bruto por dois fluxos físicos. Ou seja, cada fluxo do tipo YUV ou RAW pode ser substituído por dois fluxos de tipo e tamanho idênticos. É possível começar com um stream da câmera da seguinte configuração garantida para dispositivos de câmera única:

  • Stream 1: tipo YUV, tamanho MAXIMUM da câmera lógica id = 0

Em seguida, um dispositivo que oferece suporte a várias câmeras permite criar uma sessão substituindo esse stream YUV lógico por dois streams físicos:

  • Fluxo 1: tipo YUV, tamanho MAXIMUM da câmera física id = 1
  • Fluxo 2: tipo YUV, tamanho MAXIMUM da câmera física id = 2

Você poderá substituir um stream YUV ou RAW por dois streams equivalentes somente se essas duas câmeras fizerem parte de um agrupamento lógico de câmeras,  listado em CameraCharacteristics.getPhysicalCameraIds().

As garantias fornecidas pelo framework são o mínimo necessário para receber frames de mais de uma câmera física simultaneamente. A maioria dos dispositivos oferece suporte a mais streams. Às vezes, é possível abrir vários dispositivos de câmera física de maneira independente. Como não é uma garantia dura do framework, fazer isso exige a execução de testes e ajustes por dispositivo usando tentativa e erro.

Como criar uma sessão com várias câmeras físicas

Ao usar câmeras físicas em um dispositivo com várias câmeras, abra uma única CameraDevice (a câmera lógica) e interaja com ela em uma única sessão. Crie a sessão única usando a API CameraDevice.createCaptureSession(SessionConfiguration config), que foi adicionada no nível 28 da API. A configuração da sessão tem diversas configurações de saída, cada uma com um conjunto de destinos de saída e, opcionalmente, um ID de câmera física desejado.

Figura 3. Modelo "SessionConfiguration e OutputConfiguration"

As solicitações de captura têm um destino de saída associado a elas. O framework determina para qual câmera física (ou lógica) as solicitações são enviadas com base no destino de saída anexado. Se o destino de saída corresponder a um dos destinos de saída enviados como uma configuração de saída, junto com um ID da câmera física, essa câmera receberá e processará a solicitação.

Usar um par de câmeras físicas

Outra adição às APIs de câmera para várias câmeras é a capacidade de identificar câmeras lógicas e encontrar as físicas atrás delas. É possível definir uma função para identificar possíveis pares de câmeras físicas que podem ser usadas para substituir um dos streams de câmera lógicos:

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

O manuseio do estado das câmeras físicas é controlado pela câmera lógica. Para abrir uma "câmera dupla", abra a câmera lógica correspondente às físicas:

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

Além de selecionar qual câmera abrir, o processo é o mesmo que abrir uma câmera nas versões anteriores do Android. A criação de uma sessão de captura usando a nova API de configuração de sessão instrui o framework a associar determinados destinos a IDs de câmera física específicos:

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

Consulte createCaptureSession para ver informações sobre qual combinação de streams tem suporte. Combinar streams é para vários streams em uma única câmera lógica. A compatibilidade se estende ao uso da mesma configuração e à substituição de um desses streams por dois streams de duas câmeras físicas que fazem parte da mesma câmera lógica.

Com a sessão da câmera pronta, envie as solicitações de captura desejadas. Cada destino da solicitação de captura recebe os dados da câmera física associada, se houver alguma em uso, ou retorna para a câmera lógica.

Exemplo de caso de uso do Zoom

É possível usar a combinação de câmeras físicas em um único stream para que os usuários possam alternar entre as diferentes câmeras físicas e ter um campo de visão diferente, capturando efetivamente um "nível de zoom" diferente.

Figura 4. Exemplo de troca de câmeras para caso de uso de nível de zoom (do anúncio do Pixel 3)

Comece selecionando o par de câmeras físicas para permitir que os usuários alternem. Para o efeito máximo, você pode escolher o par de câmeras que fornecem a distância focal mínima e máxima disponível.

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

Uma arquitetura adequada para isso seria ter dois SurfaceViews, um para cada stream. Essas SurfaceViews são trocadas com base na interação do usuário para que apenas uma fique visível a qualquer momento.

O código a seguir mostra como abrir a câmera lógica, configurar as saídas da câmera, criar uma sessão de câmera e iniciar dois streams de visualização:

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

Tudo o que resta a fazer é fornecer uma IU para que o usuário alterne entre as duas plataformas, como um botão ou um toque duplo na SurfaceView. É possível até mesmo realizar alguma forma de análise de cena e alternar entre os dois streams automaticamente.

Distorção da lente

Todas as lentes produzem uma certa quantidade de distorção. No Android, é possível consultar a distorção criada pelas lentes usando CameraCharacteristics.LENS_DISTORTION, que substitui o CameraCharacteristics.LENS_RADIAL_DISTORTION descontinuado. Para câmeras lógicas, a distorção é mínima, e o aplicativo pode usar os frames mais ou menos à medida que eles vêm da câmera. Para câmeras físicas, há configurações de lente muito diferentes, especialmente em lentes amplas.

Alguns dispositivos podem implementar a correção automática de distorção usando CaptureRequest.DISTORTION_CORRECTION_MODE. A correção de distorção é ativada por padrão na maioria dos dispositivos.

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

Definir uma solicitação de captura nesse modo pode afetar o frame rate que pode ser produzido pela câmera. Você pode definir a correção da distorção apenas em capturas de imagens estáticas.