Orientations de la caméra

Si votre application Android utilise des caméras, vous devez tenir compte de certains aspects spécifiques lors de la gestion des orientations. Ce document suppose que vous comprenez les concepts de base de l'API camera2 d'Android. Pour obtenir une présentation de camera2, vous pouvez consulter notre article de blog ou notre résumé. Nous vous recommandons également d'essayer d'écrire une application d'appareil photo avant de consulter ce document.

Arrière-plan

La gestion des orientations dans les applications d'appareil photo Android est délicate et doit tenir compte des facteurs suivants :

  • Orientation naturelle : orientation de l'écran lorsque l'appareil est en position "normale" pour sa conception (généralement l'orientation portrait pour les téléphones mobiles et l'orientation paysage pour les ordinateurs portables).
  • Orientation du capteur : orientation du capteur physiquement monté sur l'appareil.
  • Rotation de l'écran : rotation physique de l'appareil par rapport à son orientation naturelle.
  • Taille du viseur : taille du viseur utilisé pour afficher l'aperçu de l'appareil photo.
  • Taille de l'image produite par la caméra.

Ces facteurs combinés introduisent un grand nombre de configurations possibles pour l'UI et l'aperçu des applis d'appareil photo. Ce document vise à montrer aux développeurs comment parcourir ces orientations et gérer correctement les orientations de l'appareil photo dans les applications Android.

Pour simplifier les choses, supposons que tous les exemples impliquent une caméra orientée vers l'arrière, sauf indication contraire. De plus, toutes les photos suivantes sont simulées pour rendre les illustrations plus claires.

À propos des orientations

Orientation naturelle

L'orientation naturelle est définie comme l'orientation de l'écran lorsque l'appareil est dans la position dans laquelle il est normalement censé se trouver. Pour les téléphones, l'orientation naturelle est souvent le mode Portrait. En d'autres termes, les téléphones ont une largeur plus petite et une hauteur plus grande. Pour les ordinateurs portables, l'orientation naturelle est le mode paysage, ce qui signifie que leur largeur est plus importante que leur hauteur. Les tablettes sont un peu plus complexes : elles peuvent être en mode portrait ou paysage.

Illustration de l'orientation naturelle avec un téléphone, un ordinateur portable et un objet du côté de l'observateur

Orientation du capteur

Pour être plus précis, l'orientation du capteur est mesurée en degrés de rotation dans le sens des aiguilles d'une montre nécessaires pour que l'image de sortie du capteur corresponde à l'orientation naturelle de l'appareil. En d'autres termes, l'orientation du capteur correspond au nombre de degrés de rotation du capteur dans le sens inverse des aiguilles d'une montre avant son montage sur l'appareil. Lorsque vous regardez l'écran, la rotation semble se faire dans le sens des aiguilles d'une montre, car le capteur de la caméra arrière est installé à l'arrière de l'appareil.

Selon la Définition de compatibilité Android 10 7.5.5 Orientation de la caméra, les caméras avant et arrière "DOIVENT être orientées de sorte à faire correspondre la dimension longue de la caméra à la dimension longue de l'écran".

Les tampons de sortie des caméras sont au format paysage. Étant donné que l'orientation naturelle des téléphones est généralement portrait, l'orientation du capteur est généralement de 90 ou 270 degrés par rapport à l'orientation naturelle afin que le côté long du tampon de sortie corresponde au côté long de l'écran. L'orientation du capteur est différente pour les appareils dont l'orientation naturelle est le mode paysage, comme les Chromebooks. Sur ces appareils, les capteurs d'image sont à nouveau placés de sorte que le côté long du tampon de sortie corresponde au côté long de l'écran. Comme les deux sont au format Paysage, les orientations correspondent et l'orientation du capteur est de 0 ou 180 degrés.

Illustration de l'orientation naturelle avec un téléphone, un ordinateur portable et un objet du côté de l'observateur

Les illustrations suivantes montrent comment les éléments s'affichent du point de vue d'un observateur qui regarde l'écran de l'appareil :

Illustration de l'orientation du capteur avec un téléphone, un ordinateur portable et un objet du côté de l'observateur

Prenons l'exemple suivant :

Scène avec une jolie figurine Android (bugdroid)

Téléphone Ordinateur portable
Illustration d'une image vue à travers le capteur de la caméra arrière d'un téléphone Illustration d'une image vue à travers le capteur de la caméra arrière d'un ordinateur portable

Comme l'orientation du capteur est généralement de 90 ou 270 degrés sur les téléphones, sans tenir compte de l'orientation du capteur, les images que vous obtiendriez ressembleraient à ceci :

Téléphone Ordinateur portable
Illustration d'une image vue à travers le capteur de la caméra arrière d'un téléphone Illustration d'une image vue à travers le capteur de la caméra arrière d'un ordinateur portable

Supposons que l'orientation du capteur dans le sens inverse des aiguilles d'une montre soit stockée dans la variable sensorOrientation. Pour compenser l'orientation du capteur, vous devez faire pivoter les tampons de sortie de `sensorOrientation` dans le sens des aiguilles d'une montre pour réaligner l'orientation sur l'orientation naturelle de l'appareil.

Dans Android, les applications peuvent utiliser TextureView ou SurfaceView pour afficher l'aperçu de leur caméra. Les deux peuvent gérer l'orientation du capteur si les applications les utilisent correctement. Nous vous expliquerons comment tenir compte de l'orientation du capteur dans les sections suivantes.

Rotation de l'écran

La rotation de l'écran est formellement définie par la rotation des éléments graphiques dessinés sur l'écran, qui est dans le sens opposé à la rotation physique de l'appareil par rapport à son orientation naturelle. Les sections suivantes supposent que les rotations d'écran sont toutes des multiples de 90. Si vous récupérez la rotation de l'écran en degrés absolus, arrondissez-la à la valeur la plus proche parmi {0, 90, 180, 270}.

Dans les sections suivantes, "orientation de l'écran" fait référence à la position physique de l'appareil (paysage ou portrait) et est différente de la "rotation de l'écran".

Supposons que vous fassiez pivoter les appareils de 90 degrés dans le sens inverse des aiguilles d'une montre par rapport à leur position précédente, comme illustré dans la figure suivante :

Illustration de la rotation de l'écran à 90° avec un téléphone, un ordinateur portable et un objet du côté de l'observateur

En supposant que les tampons de sortie sont déjà pivotés en fonction de l'orientation du capteur, vous disposeriez alors des tampons de sortie suivants :

Téléphone Ordinateur portable
Illustration d'une image vue à travers le capteur de la caméra arrière d'un téléphone Illustration d'une image vue à travers le capteur de la caméra arrière d'un ordinateur portable

Si la rotation de l'écran est stockée dans la variable displayRotation, pour obtenir l'image correcte, vous devez faire pivoter les tampons de sortie de displayRotation dans le sens inverse des aiguilles d'une montre.

Pour les caméras avant, la rotation de l'écran agit sur les tampons d'image dans le sens opposé à celui de l'écran. Si vous utilisez une caméra avant, vous devez faire pivoter les tampons dans le sens des aiguilles d'une montre en fonction de la rotation de l'écran.

Mises en garde

La rotation de l'écran mesure la rotation de l'appareil dans le sens inverse des aiguilles d'une montre. Cela ne s'applique pas à toutes les API d'orientation/de rotation.

Par exemple,

Il est important de noter que la rotation de l'écran est relative à l'orientation naturelle. Par exemple, si vous faites pivoter physiquement un téléphone de 90 ou 270 degrés, l'écran s'affiche en mode Paysage. En comparaison, vous obtiendriez un écran au format portrait si vous faisiez pivoter un ordinateur portable du même angle. Les applications doivent toujours garder cela à l'esprit et ne jamais faire d'hypothèses sur l'orientation naturelle d'un appareil.

Exemples

Utilisons les chiffres précédents pour illustrer les orientations et les rotations.

Illustration combinée de l'orientation avec un téléphone et un ordinateur portable non pivotés, et un objet

Téléphone Ordinateur portable
Orientation naturelle = Portrait Orientation naturelle = Paysage
Orientation du capteur = 90 Orientation du capteur = 0
Rotation de l'écran = 0 Rotation de l'écran = 0
Orientation de l'écran = Portrait Orientation de l'écran = Paysage

Illustration combinée de l'orientation avec un téléphone et un ordinateur portable non pivotés, et un objet

Téléphone Ordinateur portable
Orientation naturelle = Portrait Orientation naturelle = Paysage
Orientation du capteur = 90 Orientation du capteur = 0
Rotation de l'écran = 90 Rotation de l'écran = 90
Orientation de l'écran = Paysage Orientation de l'écran = Portrait

Taille du viseur

Les applications doivent toujours redimensionner le viseur en fonction de l'orientation, de la rotation et de la résolution de l'écran. En règle générale, les applications doivent faire en sorte que l'orientation du viseur soit identique à l'orientation actuelle de l'écran. En d'autres termes, les applications doivent aligner le bord long du viseur sur le bord long de l'écran.

Taille de sortie des images par caméra

Lorsque vous choisissez la taille de sortie de l'image pour l'aperçu, vous devez choisir une taille égale ou légèrement supérieure à celle du viseur, dans la mesure du possible. En général, vous ne souhaitez pas que les tampons de sortie soient mis à l'échelle, car cela entraînerait une pixellisation. Vous ne devez pas non plus choisir une taille trop grande, car cela pourrait réduire les performances et consommer plus de batterie.

Orientation JPEG

Commençons par une situation courante : la capture d'une photo au format JPEG. Dans l'API camera2, vous pouvez transmettre JPEG_ORIENTATION dans la requête de capture pour spécifier le degré de rotation dans le sens des aiguilles d'une montre de vos fichiers JPEG de sortie.

Voici un bref récapitulatif de ce que nous avons mentionné :

  • Pour gérer l'orientation du capteur, vous devez faire pivoter le tampon d'image de sensorOrientation dans le sens des aiguilles d'une montre.
  • Pour gérer la rotation de l'écran, vous devez faire pivoter un tampon de displayRotation dans le sens inverse des aiguilles d'une montre pour les caméras arrière et dans le sens des aiguilles d'une montre pour les caméras avant.

En additionnant les deux facteurs, le montant de la rotation dans le sens des aiguilles d'une montre est

  • sensorOrientation - displayRotation pour les caméras arrière.
  • sensorOrientation + displayRotation pour les caméras avant.

Vous trouverez un exemple de code pour cette logique dans la documentation JPEG_ORIENTATION. Notez que deviceOrientation dans l'exemple de code de la documentation utilise la rotation de l'appareil dans le sens des aiguilles d'une montre. Les signes de la rotation de l'écran sont donc inversés.

Aperçu

Qu'en est-il de l'aperçu de la caméra ? Une application peut afficher un aperçu de l'appareil photo de deux manières principales : SurfaceView et TextureView. Chacun d'eux nécessite une approche différente pour gérer correctement l'orientation.

SurfaceView

SurfaceView est généralement recommandé pour les aperçus de caméra, à condition que vous n'ayez pas besoin de traiter ni d'animer les tampons d'aperçu. Il est plus performant et moins gourmand en ressources que TextureView.

SurfaceView est également relativement plus facile à organiser. Vous n'avez qu'à vous soucier du format de la SurfaceView sur laquelle vous affichez l'aperçu de l'appareil photo.

Source

Sous SurfaceView, la plate-forme Android fait pivoter les tampons de sortie pour qu'ils correspondent à l'orientation de l'écran de l'appareil. En d'autres termes, il tient compte à la fois de l'orientation du capteur et de la rotation de l'écran. En d'autres termes, lorsque notre écran est en mode paysage, l'aperçu est également en mode paysage, et inversement pour le mode portrait.

Cela est illustré dans le tableau suivant. Il est important de noter que la rotation de l'écran à elle seule ne détermine pas l'orientation de la source.

Rotation de l'écran Téléphone (orientation naturelle = portrait) Ordinateur portable (orientation naturelle = paysage)
0 Image au format portrait avec la tête du Bugdroid pointant vers le haut Image au format paysage avec la tête du Bugdroid pointant vers le haut
90 Image au format paysage avec la tête du Bugdroid pointant vers le haut Image au format portrait avec la tête du Bugdroid pointant vers le haut
180 Image au format portrait avec la tête du Bugdroid pointant vers le haut Image au format paysage avec la tête du Bugdroid pointant vers le haut
270 Image au format paysage avec la tête du Bugdroid pointant vers le haut Image au format portrait avec la tête du Bugdroid pointant vers le haut

Mise en page

Comme vous pouvez le voir, SurfaceView gère déjà certaines des choses délicates pour nous. Vous devez maintenant tenir compte de la taille du viseur ou de la taille de l'aperçu que vous souhaitez afficher à l'écran. SurfaceView met automatiquement à l'échelle la mémoire tampon source pour l'adapter à ses dimensions. Vous devez vous assurer que les proportions du viseur sont identiques à celles du sourcebuffer. Par exemple, si vous essayez d'adapter un aperçu au format portrait dans une SurfaceView au format paysage, vous obtiendrez une image déformée comme celle-ci :

Illustration montrant un bugdroid étiré en raison de l'adaptation d'un aperçu au format portrait dans un viseur au format paysage

En général, le format (c'est-à-dire largeur/hauteur) du viseur doit être identique à celui de la source. Si vous ne souhaitez pas rogner l'image dans le viseur (c'est-à-dire couper certains pixels pour corriger l'affichage), vous devez tenir compte de deux cas : lorsque aspectRatioActivity est supérieur à aspectRatioSource et lorsqu'il est inférieur ou égal à aspectRatioSource.

aspectRatioActivity > aspectRatioSource

Vous pouvez considérer que le boîtier est "plus large" que l'activité. Prenons l'exemple d'une activité au format 16:9 et d'une source au format 4:3.

aspectRatioActivity = 16/9 ≈ 1.78
aspectRatioSource = 4/3 ≈ 1.33

Tout d'abord, vous devez également définir le format 4:3 pour votre viseur. Vous devez ensuite ajuster la source et le viseur dans l'activité comme suit :

Illustration d'une activité dont le format est supérieur à celui du viseur

Dans ce cas, vous devez faire correspondre la hauteur du viseur à celle de l'activité, tout en veillant à ce que le format du viseur soit identique à celui de la source. Le pseudo-code est le suivant :

viewfinderHeight = activityHeight;
viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource

L'autre cas se produit lorsque l'activité est "plus étroite" ou "plus haute". Nous pouvons réutiliser l'exemple précédent, sauf que dans l'exemple suivant, vous faites pivoter l'appareil de 90 degrés, ce qui donne une activité de 9:16 et une source de 3:4.

aspectRatioActivity = 9/16 = 0.5625
aspectRatioSource = 3/4 = 0.75

Dans ce cas, vous devez ajuster la source et le viseur à l'activité comme suit :

Illustration d'une activité dont le format est inférieur à celui du viseur à l'intérieur

Vous devez faire correspondre la largeur du viseur à celle de l'activité (par opposition à la hauteur dans le cas précédent), tout en rendant le format du viseur identique à celui de la source. Pseudo-code :

viewfinderWidth = activityWidth;
viewfinderHeight = activityWidth / aspectRatioSource;
Écrêtages

AutoFitSurfaceView.kt (github) des exemples Camera2 remplace SurfaceView et gère les proportions non concordantes en utilisant une image qui est égale ou "juste plus grande" que l'activité dans les deux dimensions, puis en découpant le contenu qui dépasse. Cela est utile pour les applications qui souhaitent que l'aperçu couvre l'intégralité de l'activité ou remplisse complètement une vue de dimensions fixes, sans déformer l'image.

Avertissement

L'exemple précédent tente de maximiser l'espace à l'écran en rendant l'aperçu légèrement plus grand que l'activité afin qu'aucun espace ne soit laissé vide. Cela repose sur le fait que les parties qui dépassent sont coupées par la mise en page parente (ou ViewGroup) par défaut. Ce comportement est cohérent avec RelativeLayout et LinearLayout, mais PAS avec ConstraintLayout. Un ConstraintLayout peut redimensionner les vues enfants pour les faire tenir dans la mise en page, ce qui casserait l'effet "center-crop" prévu et entraînerait des aperçus étirés. Vous pouvez utiliser ce commit comme référence.

TextureView

TextureView offre un contrôle maximal sur le contenu de l'aperçu de la caméra, mais cela a un coût en termes de performances. Il faut également plus de travail pour que l'aperçu de la caméra s'affiche correctement.

Source

Sous TextureView, la plate-forme Android fait pivoter les tampons de sortie en fonction de l'orientation du capteur pour correspondre à l'orientation naturelle de l'appareil. Bien que TextureView gère l'orientation du capteur, il ne gère pas les rotations de l'écran. Il aligne les tampons de sortie sur l'orientation naturelle de l'appareil, ce qui signifie que vous devrez gérer vous-même les rotations de l'écran.

Cela est illustré dans le tableau suivant. Si vous essayez de faire pivoter les chiffres selon la rotation de l'écran correspondante, vous obtiendrez les mêmes chiffres dans SurfaceView.

Rotation de l'écran Téléphone (orientation naturelle = portrait) Ordinateur portable (orientation naturelle = paysage)
0 Image au format portrait avec la tête du Bugdroid pointant vers le haut Image au format paysage avec la tête du Bugdroid pointant vers le haut
90 Image au format portrait avec la tête du bugdroid pointant vers la droite Image au format paysage avec la tête du bugdroid pointant vers la droite
180 Image au format paysage avec la tête du Bugdroid pointant vers le haut Image au format portrait avec la tête du Bugdroid pointant vers le haut
270 Image au format paysage avec la tête du Bugdroid pointant vers le haut Image au format portrait avec la tête du Bugdroid pointant vers le haut

Mise en page

La mise en page est un peu délicate dans le cas de TextureView. Il a déjà été suggéré d'utiliser une matrice de transformation pour TextureView, mais cette méthode ne fonctionne pas pour tous les appareils. Nous vous conseillons plutôt de suivre les étapes décrites ici.

Voici la procédure en trois étapes pour mettre en page correctement les aperçus sur une TextureView :

  1. Définissez la taille de TextureView pour qu'elle soit identique à la taille d'aperçu choisie.
  2. Remettez à l'échelle la TextureView potentiellement étirée pour qu'elle retrouve les dimensions d'origine de l'aperçu.
  3. Faites pivoter TextureView de displayRotation dans le sens inverse des aiguilles d'une montre.

Supposons que vous ayez un téléphone dont l'écran est orienté à 90 degrés.

Illustration d'un téléphone avec une rotation de l'écran de 90 degrés et un objet

1. Définissez la taille de TextureView pour qu'elle soit identique à la taille de l'aperçu choisie.

Supposons que la taille de l'aperçu que vous avez choisie soit previewWidth × previewHeight, où previewWidth > previewHeight (la sortie du capteur est naturellement au format paysage). Lors de la configuration d'une session de capture, vous devez appeler SurfaceTexture#setDefaultBufferSize(int width, height) pour spécifier la taille de l'aperçu (previewWidth × previewHeight).

Avant d'appeler setDefaultBufferSize, il est important que vous définissiez également la taille de TextureView sur `previewWidth × previewHeight` avec View#setLayoutParams(android.view.ViewGroup.LayoutParams). En effet, TextureView appelle SurfaceTexture#setDefaultBufferSize(int width, height) avec sa largeur et sa hauteur mesurées. Si la taille de TextureView n'est pas définie explicitement au préalable, cela peut entraîner une condition de concurrence. Pour éviter cela, définissez explicitement la taille de TextureView en premier.

Il est possible que TextureView ne corresponde pas aux dimensions de la source. Dans le cas des téléphones, la source est au format portrait, mais la TextureView est au format paysage en raison des layoutParams que vous venez de définir. Les aperçus seraient alors étirés, comme illustré ci-dessous :

Illustration d'un aperçu au format portrait étiré pour s'adapter à une TextureView de la même taille que celle de l'aperçu choisi

2. Remettre à l'échelle la TextureView potentiellement étirée pour qu'elle retrouve les dimensions d'origine de l'aperçu

Pour redimensionner l'aperçu étiré aux dimensions de la source, tenez compte des éléments suivants.

Les dimensions de la source (sourceWidth × sourceHeight) sont les suivantes :

  • previewHeight × previewWidth, si l'orientation naturelle est portrait ou portrait inversé (l'orientation du capteur est de 90 ou 270 degrés)
  • previewWidth × previewHeight, si l'orientation naturelle est paysage ou paysage inversé (l'orientation du capteur est de 0 ou 180 degrés)

Correction de l'étirement à l'aide de View#setScaleX(float) et View#setScaleY(float)

  • setScaleX(sourceWidth / previewWidth)
  • setScaleY(sourceHeight / previewHeight)

Illustration montrant la procédure de redimensionnement de l'aperçu étiré à ses dimensions d'origine

3. Faire pivoter l'aperçu de `displayRotation` dans le sens inverse des aiguilles d'une montre

Comme mentionné précédemment, vous devez faire pivoter l'aperçu de displayRotation dans le sens inverse des aiguilles d'une montre pour compenser la rotation de l'écran.

Pour ce faire, View#setRotation(float).

  • setRotation(-displayRotation), car elle effectue une rotation dans le sens des aiguilles d'une montre.

Illustration montrant la procédure de rotation de l'aperçu pour qu'il corresponde à l'orientation de l'écran de l'appareil

Exemples

Remarque : Si vous avez déjà utilisé une matrice de transformation pour TextureView dans votre code, il est possible que l'aperçu ne s'affiche pas correctement sur un appareil naturellement en mode paysage, comme un Chromebook. Il est probable que votre matrice de transformation suppose à tort que l'orientation du capteur est de 90 ou 270 degrés. Vous pouvez vous référer à ce commit sur GitHub pour trouver une solution de contournement, mais nous vous recommandons vivement de migrer votre application pour utiliser la méthode décrite ici.