API multi-caméra

Remarque:Cette page fait référence au package Camera2. Sauf si votre application nécessite des fonctionnalités spécifiques de bas niveau de Camera2, nous vous recommandons d'utiliser CameraX. CameraX et Camera2 sont compatibles avec Android 5.0 (niveau d'API 21) ou version ultérieure.

La fonctionnalité multi-caméra a été introduite avec Android 9 (niveau d'API 28). Depuis sa sortie, des appareils compatibles avec l'API sont commercialisés. De nombreux cas d'utilisation de plusieurs caméras sont étroitement liés à une configuration matérielle spécifique. En d'autres termes, tous les cas d'utilisation ne sont pas compatibles avec tous les appareils,  ce qui fait que les fonctionnalités multicaméras sont idéales pour Play Feature Delivery.

Voici certains cas d'utilisation types :

  • Zoom: permet de passer d'un appareil photo à l'autre en fonction de la zone recadrée ou de la distance focale souhaitée.
  • Profondeur: utiliser plusieurs appareils photo pour créer une carte de profondeur.
  • Bokeh: utilisation d'informations de profondeur inférées pour simuler une plage de mise au point étroite de type DSLR.

Différence entre une caméra logique et une caméra physique

Pour comprendre l'API multicaméra, vous devez comprendre la différence entre une caméra logique et une caméra physique. Pour référence, prenons un appareil doté de trois caméras arrière. Dans cet exemple, chacune des trois caméras arrière est considérée comme une caméra physique. Une caméra logique est alors un regroupement de deux ou plusieurs de ces caméras physiques. La sortie de la caméra logique peut être un flux provenant de l'une des caméras physiques sous-jacentes, ou un flux fusionné provenant simultanément de plusieurs caméras physiques sous-jacentes. Dans tous les cas, le flux est géré par la couche d'abstraction matérielle (HAL) de la caméra.

De nombreux fabricants de téléphones développent des applications d'appareil photo propriétaires, généralement préinstallées sur leurs appareils. Pour exploiter toutes les fonctionnalités du matériel, ils peuvent utiliser des API privées ou masquées, ou recevoir un traitement spécial de la part de l'implémentation du pilote à laquelle les autres applications n'ont pas accès. Certains appareils mettent en œuvre le concept de caméras logiques en fournissant un flux fusionné de trames provenant des différentes caméras physiques, mais uniquement pour certaines applications privilégiées. Souvent, une seule des caméras physiques est exposée au framework. Le schéma suivant illustre le cas de développeurs tiers antérieurs à Android 9:

Figure 1 Les fonctionnalités de la caméra ne sont généralement disponibles que pour les applications privilégiées.

À partir d'Android 9, les API privées ne sont plus autorisées dans les applications Android. En intégrant la prise en charge de plusieurs caméras dans le framework, les bonnes pratiques Android recommandent vivement aux fabricants de téléphones d'exposer un appareil photo logique pour tous les appareils photo physiques orientés dans la même direction. Voici ce à quoi les développeurs tiers doivent s'attendre sur les appareils équipés d'Android 9 ou version ultérieure:

Figure 2 Accès complet des développeurs à tous les appareils photo à partir d'Android 9

Les fonctionnalités fournies par la caméra logique dépendent entièrement de l'implémentation du HAL de la caméra par l'OEM. Par exemple, un appareil comme le Pixel 3 implémente son appareil photo logique de manière à choisir l'une de ses caméras physiques en fonction de la distance focale et de la zone de recadrage demandées.

API multi-caméra

La nouvelle API ajoute les constantes, classes et méthodes suivantes:

En raison des modifications apportées au document de définition de compatibilité (CDD) Android, l'API multi-caméra répond également à certaines attentes de la part des développeurs. Les appareils dotés de deux caméras existaient avant Android 9, mais l'ouverture de plusieurs appareils photo en même temps provoquait des essais et des erreurs. Sur Android 9 et versions ultérieures, la fonctionnalité multicaméra propose un ensemble de règles indiquant quand il est possible d'ouvrir une paire d'appareils photo physiques faisant partie du même appareil photo logique.

Dans la plupart des cas, les appareils équipés d'Android 9 ou version ultérieure exposent toutes les caméras physiques (sauf éventuellement pour des types de capteurs moins courants, tels que les infrarouges), ainsi qu'une caméra logique plus facile à utiliser. Pour chaque combinaison de flux dont le fonctionnement est garanti, un flux appartenant à une caméra logique peut être remplacé par deux flux des caméras physiques sous-jacentes.

Plusieurs flux simultanément

Le fait d'utiliser plusieurs flux d'appareil photo simultanément couvre les règles concernant l'utilisation simultanée de plusieurs flux dans une seule caméra. À noter toutefois que les mêmes règles s'appliquent à plusieurs caméras. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA explique comment remplacer un flux YUV_420_888 logique ou un flux brut par deux flux physiques. Autrement dit, chaque flux de type YUV ou RAW peut être remplacé par deux flux de type et de taille identiques. Vous pouvez commencer avec un flux de caméra de la configuration garantie suivante pour les appareils à caméra unique:

  • Flux 1: type YUV, MAXIMUM taille de l'appareil photo logique id = 0

Ensuite, un appareil compatible avec plusieurs caméras vous permet de créer une session en remplaçant ce flux YUV logique par deux flux physiques:

  • Flux 1: type YUV, MAXIMUM taille de l'appareil photo physique id = 1
  • Flux 2: type YUV, MAXIMUM taille de l'appareil photo physique id = 2

Vous pouvez remplacer un flux YUV ou RAW par deux flux équivalents si et seulement si ces deux appareils photo font partie d'un groupe d'appareils photo logique,  listé sous CameraCharacteristics.getPhysicalCameraIds().

Les garanties fournies par le framework ne représentent que le strict minimum requis pour obtenir simultanément des images à partir de plusieurs caméras physiques. Les flux supplémentaires sont compatibles avec la plupart des appareils, et permettent parfois même d'ouvrir plusieurs appareils photo physiques de manière indépendante. Comme ce n'est pas une garantie stricte de la part du framework, cela nécessite d'effectuer des tests et des réglages par appareil à l'aide d'essais et d'erreurs.

Créer une session avec plusieurs caméras physiques

Lorsque vous utilisez des caméras physiques sur un appareil compatible avec plusieurs caméras, ouvrez un seul CameraDevice (l'appareil photo logique) et interagissez avec lui dans une seule session. Créez la session unique à l'aide de l'API CameraDevice.createCaptureSession(SessionConfiguration config), qui a été ajoutée au niveau d'API 28. La configuration de session comporte un certain nombre de configurations de sortie, chacune étant associée à un ensemble de cibles de sortie et, éventuellement, à un ID d'appareil photo physique souhaité.

Figure 3. Modèle SessionConfiguration et OutputConfiguration

Les requêtes de capture sont associées à une cible de sortie. Le framework détermine l'appareil photo physique (ou logique) vers lequel les requêtes sont envoyées en fonction de la cible de sortie associée. Si la cible de sortie correspond à l'une des cibles de sortie envoyées en tant que configuration de sortie avec un ID d'appareil photo physique, cette caméra reçoit et traite la requête.

Utiliser une paire d'appareils photo physiques

Un autre avantage des API d'appareil photo pour les caméras multi-caméras est la possibilité d'identifier les caméras logiques et de trouver les caméras physiques qui se trouvent derrière. Vous pouvez définir une fonction permettant d'identifier les paires potentielles de caméras physiques que vous pouvez utiliser pour remplacer l'un des flux de caméra logiques:

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

Le traitement des états des caméras physiques est contrôlé par la caméra logique. Pour ouvrir un "double appareil photo", ouvrez l'appareil photo logique correspondant aux caméras physiques:

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

En plus de sélectionner l'appareil photo à ouvrir, le processus est le même que pour les anciennes versions d'Android. La création d'une session de capture à l'aide de la nouvelle API de configuration de session indique au framework d'associer certaines cibles à des ID d'appareil photo physique spécifiques:

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

Pour en savoir plus sur les combinaisons de flux acceptées, consultez la section createCaptureSession. La combinaison de flux s'applique à plusieurs flux sur une même caméra logique. Cette compatibilité s'étend à l'utilisation de la même configuration et au remplacement de l'un de ces flux par deux flux provenant de deux caméras physiques faisant partie de la même caméra logique.

Une fois la session de caméra prête, envoyez les requêtes de capture souhaitées. Chaque cible de la requête de capture reçoit ses données de la caméra physique associée (le cas échéant) ou se rabat sur l'appareil photo logique.

Exemple de cas d'utilisation pour Zoom

Il est possible de fusionner des caméras physiques en un seul flux afin que les utilisateurs puissent basculer entre les différentes caméras physiques pour découvrir un champ de vision différent, capturant ainsi efficacement un "niveau de zoom" différent.

Figure 4 Exemple de remplacement de l'appareil photo par l'utilisation du niveau de zoom (dans l'annonce Pixel 3)

Commencez par sélectionner la paire d'appareils photo physiques pour permettre aux utilisateurs de basculer de l'une à l'autre. Pour un effet maximal, vous pouvez choisir la paire d'appareils photo qui fournissent la distance focale minimale et maximale disponible.

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

Pour cela, il est judicieux d'avoir deux SurfaceViews, un pour chaque flux. Ces SurfaceViews sont échangés en fonction de l'interaction de l'utilisateur, de sorte qu'un seul d'entre eux est visible à la fois.

Le code suivant montre comment ouvrir l'appareil photo logique, configurer les sorties de la caméra, créer une session d'appareil photo et lancer deux flux d'aperçu:

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

Il ne reste plus qu'à fournir une interface utilisateur permettant à l'utilisateur de basculer entre les deux surfaces, par exemple en appuyant deux fois sur la SurfaceView ou en cliquant sur un bouton. Vous pouvez même effectuer une analyse de scène et basculer automatiquement entre les deux flux.

Distorsion de l'objectif

Tous les objectifs produisent un certain degré de distorsion. Dans Android, vous pouvez interroger la distorsion créée par les objectifs à l'aide de CameraCharacteristics.LENS_DISTORTION, qui remplace CameraCharacteristics.LENS_RADIAL_DISTORTION, désormais obsolète. Pour les appareils photo logiques, la distorsion est minimale et votre application peut utiliser plus ou moins les images à mesure qu'elles proviennent de l'appareil photo. Pour les caméras physiques, les configurations d'objectif peuvent être très différentes, en particulier avec les objectifs grand angle.

Certains appareils peuvent implémenter la correction automatique de la distorsion via CaptureRequest.DISTORTION_CORRECTION_MODE. La correction de la distorsion est activée par défaut sur la plupart des appareils.

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

Définir une requête de capture dans ce mode peut avoir une incidence sur la fréquence d'images que la caméra peut produire. Vous pouvez choisir de définir la correction de la distorsion uniquement sur les captures d'images fixes.