Augmented Images avec ARCore

ARCore est une plate-forme qui permet de créer des applications de réalité augmentée sur Android. Augmented Images vous permet de créer des applications de RA capables de reconnaître des images préenregistrées et d'y associer des contenus virtuels.

Cet atelier de programmation vous aidera à modifier un exemple d'application ARCore existant afin d'y intégrer des éléments Augmented Images en mouvement ou fixes.

Objectif de cet atelier

Dans cet atelier de programmation, vous allez utiliser un exemple d'application ARCore existant. Voici les tâches que votre application pourra effectuer quand vous aurez terminé :

  • Détecter une image cible et y associer un labyrinthe virtuel (exemple de visualisation ci-dessous)
  • Suivre une cible en mouvement tant qu'elle reste visible

6bc6605df89de525.gif

Est-ce la première fois que vous créez une application ARCore ?

Oui Non

Avez-vous l'intention d'écrire un exemple de code dans cet atelier de programmation ou souhaitez-vous simplement lire ces pages ?

Écrire du code Lire ces pages

Points abordés

  • Utiliser Augmented Images dans ARCore en Java
  • Évaluer la capacité d'une image à être reconnue par ARCore
  • Associer un contenu virtuel à une image et suivre ses déplacements

Prérequis

Assurez-vous de réunir tous les éléments nécessaires avant de démarrer cet atelier de programmation :

  • Un appareil ARCore compatible, connecté via un câble USB à votre ordinateur de développement.
  • ARCore 1.9 ou version ultérieure. Ce fichier APK est normalement installé automatiquement sur l'appareil via le Play Store. Si vous ne disposez pas de la version requise d'ARCore pour votre appareil, vous pouvez toujours l'installer à partir du Play Store.
  • Un ordinateur de développement avec Android Studio (version 3.1 ou ultérieure).
  • Un accès Internet permettant de télécharger des bibliothèques pendant le développement.

Maintenant que tout est prêt, nous pouvons nous lancer.

Nous allons commencer par télécharger le SDK ARCore Java à partir de GitHub : arcore-android-sdk-1.18.1.zip. Décompressez-le à l'emplacement de votre choix. Le dossier d'extraction doit s'appeler "arcore-android-sdk-1.18.1".

Lancez Android Studio, puis cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

5fbf2b21609187cc.png

Accédez à ce dossier décompressé :

arcore-android-sdk-1.18.1/samples/augmented_image_java

Cliquez sur Open (Ouvrir).

Attendez qu'Android Studio effectue la synchronisation du projet. Si des composants requis manquent à Android Studio, le message d'erreur "Install missing platform and sync project" (Installer la plate-forme manquante, puis synchroniser le projet) peut s'afficher. Suivez les instructions pour résoudre le problème.

Maintenant que vous disposez d'un projet d'application ARCore opérationnel, nous allons pouvoir essayer de l'exécuter.

Connectez votre appareil ARCore à l'ordinateur de développement, puis utilisez le menu Run > Run 'app' (Exécuter > Exécuter "application") pour exécuter la version de débogage sur l'appareil. Dans la boîte de dialogue qui vous invite à choisir l'appareil sur lequel exécuter l'application,

sélectionnez l'appareil connecté, puis cliquez sur OK.

1aa2c6faa7ecdbd0.png

92e4c144a632b4ca.png

Cet exemple de projet utilise targetSdkVersion 28. Si vous rencontrez une erreur de build telle que Failed to find Build Tools revision 28.0.3, suivez les instructions fournies dans Android Studio pour télécharger et installer la version requise d'Android Build Tools.

Si tout se déroule correctement, l'exemple d'application se lance sur l'appareil et vous demande d'autoriser Augmented Images à prendre des photos et à enregistrer des vidéos. Appuyez sur ALLOW (Autoriser) pour accorder cette autorisation.

Donnons à notre application une image à traiter.

Dans Android Studio, dans la fenêtre Project (Projet), accédez à app > assets (Application > Éléments), puis double-cliquez sur le fichier default.jpg pour l'ouvrir.

9b333680e7b9f247.jpeg

Orientez l'appareil photo de votre appareil vers l'image de la Terre affichée à l'écran, puis suivez les instructions pour adapter l'image que vous scannez au cadre.

Une image de cadre se superpose sur la photo, comme ci-dessous :

999e05ed35964f6e.png

Apportons quelques petites améliorations à notre application.

Comme nous l'avons mentionné au début de cet atelier de programmation, nous allons placer un petit jeu de labyrinthe sur l'image. Commençons par récupérer un modèle de labyrinthe sur poly.google.com, qui contient de nombreux modèles 3D sous licence CC-BY à usage gratuit.

Pour cet atelier de programmation, nous allons utiliser "Circle Maze - Green" par Evol, sous licence CC-BY 3.0.

832fc0f1b09fea1e.png

Pour télécharger le modèle et l'importer dans Android Studio, procédez comme suit :

  1. Accédez à la page Poly du modèle.
  2. Cliquez sur Télécharger, puis sélectionnez Fichier OBJ.

Un fichier nommé green-maze.zip est alors téléchargé.

  1. Décompressez green-maze.zip, puis copiez son contenu à l'emplacement suivant : arcore-android-sdk-1.18.1/samples/augmented_image_java/app/src/main/assets/models/green-maze
  2. Dans Android Studio, accédez à app > assets > models > green-maze (Application > Éléments > Modèles > green-maze).

Ce dossier doit contenir deux fichiers : GreenMaze.obj et GreenMaze.mtl.

a1f33a2d2d407e03.png

Ensuite, nous allons charger ce fichier OBJ et l'afficher sur l'image détectée.

Maintenant que nous disposons du modèle 3D, GreenMaze.obj, nous allons l'afficher par-dessus notre image.

  1. Dans AugmentedImageRenderer.java, ajoutez une variable de membre appelée mazeRenderer pour afficher le modèle de labyrinthe. Comme le labyrinthe doit se superposer à l'image, il est logique de placer mazeRenderer dans la classe AugmentedImageRenderer.
  2. Dans la fonction createOnGlThread, chargez GreenMaze.ob. Pour plus de simplicité, nous utiliserons la même texture pour l'élément et son cadre.
  3. Dans la fonction draw, ajustez la taille du labyrinthe à celle de l'image détectée, puis exécutez la fonction.

Effectuez la modification dans AugmentedImageRenderer.java.

  // Add a member variable to hold the maze model.
  private final ObjectRenderer mazeRenderer = new ObjectRenderer();

  // Replace the definition of the createOnGlThread function with the
  // following code, which loads GreenMaze.obj.
  public void createOnGlThread(Context context) throws IOException {

    mazeRenderer.createOnGlThread(
        context, "models/green-maze/GreenMaze.obj", "models/frame_base.png");
    mazeRenderer.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);

  }

  // Replace the definition of the draw function with the
  // following code
  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {
    float[] tintColor =
        convertHexToColor(TINT_COLORS_HEX[augmentedImage.getIndex() % TINT_COLORS_HEX.length]);

    final float maze_edge_size = 492.65f; // Magic number of maze size
    final float max_image_edge = Math.max(augmentedImage.getExtentX(), augmentedImage.getExtentZ()); // Get largest detected image edge size

    Pose anchorPose = centerAnchor.getPose();

    float mazsScaleFactor = max_image_edge / maze_edge_size; // scale to set Maze to image size
    float[] modelMatrix = new float[16];

    // OpenGL Matrix operation is in the order: Scale, rotation and Translation
    // So the manual adjustment is after scale
    // The 251.3f and 129.0f is magic number from the maze obj file
    // We need to do this adjustment because the maze obj file
    // is not centered around origin. Normally when you
    // work with your own model, you don't have this problem.
    Pose mozeModelLocalOffset = Pose.makeTranslation(
                                -251.3f * mazsScaleFactor,
                                0.0f,
                                129.0f * mazsScaleFactor);
    anchorPose.compose(mozeModelLocalOffset).toMatrix(modelMatrix, 0);
    mazeRenderer.updateModelMatrix(modelMatrix, mazsScaleFactor, mazsScaleFactor/10.0f, mazsScaleFactor);
    mazeRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);
  }

Parfait, les modifications que nous avons apportées au code ont suffi pour afficher le labyrinthe sur la photo de la Terre default.jpg.

Le code ci-dessous contient des nombres qui peuvent vous sembler mystérieusement sortis de mon chapeau. Pas de panique, ils sont nécessaires, car nous n'avons pas un contrôle total sur ce modèle 3D. J'ai donc analysé manuellement le fichier OBJ pour identifier la position du centre du modèle (x, y, z) et sa taille. Je vous épargne ce travail dans cet atelier de programmation et pour vous livrer les valeurs nécessaires. Les dimensions du modèle de labyrinthe sont de 492,65 × 120 × 492,65, et les coordonnées de son centre sont : (251,3 ; 60 ; -129). Les coordonnées x, y et z minimales et maximales de ses sommets sont : [5,02 ; 497,67], [0 ; 120], [-375,17 ; 117,25]. Nous devons donc effectuer la mise à l'échelle suivante du modèle du labyrinthe : image_size / 492.65. Comme vous le voyez, le modèle 3D du labyrinthe n'est pas centré sur l'origine (0, 0, 0). C'est pourquoi nous devons ajouter manuellement un décalage mozeModelLocalOffset.

De plus, comme le mur de labyrinthe est encore un peu trop élevé pour notre atelier de programmation, nous allons le redimensionner à l'échelle 0,1. Cela permet de réduire la hauteur du mur afin que les passages soient plus visibles. Pour cela, nous devons introduire une fonction d'assistance qui permet de mettre à l'échelle les coordonnées X, Y et Z de façon différenciée.

Effectuez ces modifications dans augmentedimage/rendering/ObjectRenderer.java.

  public void updateModelMatrix(float[] modelMatrix, float scaleFactorX, float scaleFactorY, float scaleFactorZ) {
    float[] scaleMatrix = new float[16];
    Matrix.setIdentityM(scaleMatrix, 0);
    scaleMatrix[0] = scaleFactorX;
    scaleMatrix[5] = scaleFactorY;
    scaleMatrix[10] = scaleFactorZ;
    Matrix.multiplyMM(this.modelMatrix, 0, modelMatrix, 0, scaleMatrix, 0);
  }

Maintenant, essayons d'exécuter le code sur votre appareil compatible avec ARCore. La taille du labyrinthe devrait être identique à celle de l'image.

772cbe2a8baef3ba.png

Ajoutons maintenant un objet qui se déplace à l'intérieur du labyrinthe. Dans cet atelier de programmation, nous allons simplifier et utiliser simplement le fichier de figurine Android andy.obj inclus dans le SDK Android ARCore. Utilisez la même texture pour le cadre et l'image, car elle est différente du labyrinthe vert que nous superposons à l'image.

Ajoutez ce code dans AugmentedImageNode.java.

// Add a private member to render andy
  private final ObjectRenderer andyRenderer = new ObjectRenderer();

  public void createOnGlThread(Context context) throws IOException {

    // Add initialization for andyRenderer at the end of the createOnGlThread function.
    andyRenderer.createOnGlThread(
        context, "models/andy.obj", "models/andy.png");
    andyRenderer.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
  }

  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {

    // In draw() function, at the end add code to display the Andy, standing on top of the maze
    Pose andyModelLocalOffset = Pose.makeTranslation(
        0.0f,
        0.1f,
        0.0f);
    anchorPose.compose(andyModelLocalOffset).toMatrix(modelMatrix, 0);
    andyRenderer.updateModelMatrix(modelMatrix, 0.05f); // 0.05f is a Magic number to scale
    andyRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);

  }

Essayons ensuite d'exécuter le code sur votre appareil. Le résultat devrait être semblable à l'illustration.

cb1e74569d7ace69.png

Déterminer la qualité de l'image cible

Pour reconnaître une image, ARCore se base sur certaines de ses caractéristiques visuelles. Toutes les images n'ont pas la même qualité, et certaines sont moins faciles à identifier.

L'outil "arcoreimg" du SDK Android ARCore vous permet de vérifier la qualité d'une image cible. Nous pouvons exécuter cet outil de ligne de commande pour déterminer avec quelle facilité elle peut être reconnue par ARCore. Cet outil génère un nombre compris entre 0 et 100, où 100 correspond à la plus grande facilité de reconnaissance. Exemple :

arcore-android-sdk-1.18.1/tools/arcoreimg/macos$
$ ./arcoreimg  eval-img --input_image_path=/Users/username/maze.jpg
100

Cette section ne concerne pas directement ARCore, mais elle va nous permettre de jouer avec notre application. N'hésitez pas à sauter cette étape si elle ne vous intéresse pas.

Nous allons utiliser le moteur physique Open Source, jBullet pour gérer la simulation physique.

Voici ce que nous allons faire :

  1. Ajouter GreenMaze.obj au répertoire des éléments du projet afin de pouvoir le charger lors de l'exécution.
  2. Créer la classe PhysicsController pour gérer toutes les fonctions physiques. En interne, elle utilise le moteur physique JBullet.
  3. Lorsqu'une image est reconnue, appeler physicsController et updatePhysics.
  4. Utiliser la gravité réelle pour déplacer la bille dans le labyrinthe. Notez que nous devons réduire légèrement la taille de cette bille pour qu'elle puisse circuler à travers le labyrinthe.

Téléchargez le code PhysicsController.java, puis ajoutez-le à votre projet dans le répertoire arcore-android-sdk-1.18.1/samples/augmented_image_java/app/src/main/java/com/google/ar/core/examples/java/augmentedimage/

Apportez ensuite les modifications souhaitées dans le code Java existant. Comme ci-dessous :

Dans Android Studio, copiez GreenMaze.obj depuis

app > assets > models > green-maze

pour le coller dans :

app > assets

Dans app/build.gradle, ajoutez le code suivant.

    // Add these dependencies.
    implementation 'cz.advel.jbullet:jbullet:20101010-1'

    // Obj - a simple Wavefront OBJ file loader
    // https://github.com/javagl/Obj
    implementation 'de.javagl:obj:0.2.1'

Dans AugmentedImageRenderer.java, ajoutez le code suivant.

// Add this line at the top with the rest of the imports.
  private Pose andyPose = Pose.IDENTITY;

  public void draw(
      float[] viewMatrix,
      float[] projectionMatrix,
      AugmentedImage augmentedImage,
      Anchor centerAnchor,
      float[] colorCorrectionRgba) {

    // Use these code to replace previous code for rendering the Andy object
    // Adjust andy's rendering position
    // Andy's pose is at Maze's vertex's coordinate
    Pose andyPoseInImageSpace = Pose.makeTranslation(
        andyPose.tx() * mazsScaleFactor,
        andyPose.ty() * mazsScaleFactor,
        andyPose.tz() * mazsScaleFactor);

    anchorPose.compose(andyPoseInImageSpace).toMatrix(modelMatrix, 0);
    andyRenderer.updateModelMatrix(modelMatrix, 0.05f);
    andyRenderer.draw(viewMatrix, projectionMatrix, colorCorrectionRgba, tintColor);
  }

  // Add a new utility function to receive Andy pose updates
  public void updateAndyPose(Pose pose) {
    andyPose = pose;
  }

Dans AugmentedImageActivity.java, ajoutez le code suivant.

import com.google.ar.core.Pose;

  // Declare the PhysicsController class.
  private PhysicsController physicsController;

  // Update the case clause for TRACKING as below
  private void drawAugmentedImages(

    ...

        case TRACKING:
          // Have to switch to UI Thread to update View.
          this.runOnUiThread(
              new Runnable() {
                @Override
                public void run() {
                  fitToScanView.setVisibility(View.GONE);
                }
              });

          // Create a new anchor for newly found images.
          if (!augmentedImageMap.containsKey(augmentedImage.getIndex())) {
            Anchor centerPoseAnchor = augmentedImage.createAnchor(augmentedImage.getCenterPose());
            augmentedImageMap.put(
                augmentedImage.getIndex(), Pair.create(augmentedImage, centerPoseAnchor));

            physicsController = new PhysicsController(this);
          } else {
            Pose ballPose = physicsController.getBallPose();
            augmentedImageRenderer.updateAndyPose(ballPose);

            // Use real world gravity, (0, -10, 0) as gravity
            // Convert to Physics world coordinate (because Maze mesh has to be static)
            // Use it as a force to move the ball
            Pose worldGravityPose = Pose.makeTranslation(0, -10f, 0);
            Pose mazeGravityPose = augmentedImage.getCenterPose().inverse().compose(worldGravityPose);
            float mazeGravity[] = mazeGravityPose.getTranslation();
            physicsController.applyGravityToBall(mazeGravity);

            physicsController.updatePhysics();
          }
          break;

Nous pouvons ensuite déplacer la bille ainsi.

2f0df284705d3704.gif

Amusez-vous bien !

Félicitations, vous avez terminé cet atelier de programmation. Récapitulons ce que nous y avons appris.

  • Créer et exécuter un exemple AugmentedImage Java
  • Utiliser le code pour effectuer une mise au point automatique sur les images à proximité, puis modifier le cadre de l'image pour qu'il corresponde à la taille de l'image
  • Mettre à jour le code afin d'utiliser une image spécifiée par l'utilisateur en tant que cible
  • Mettre à jour le code pour afficher un modèle de labyrinthe sur l'image, à l'échelle appropriée
  • Créer un jeu basé sur le positionnement de l'image

Si vous souhaitez accéder au code complet, vous pouvez le télécharger sur cette page.

Est-ce que cet atelier de programmation vous a plu ?

Oui Non

Avez-vous appris des choses utiles dans cet atelier de programmation ?

Oui Non

Avez-vous terminé la création de l'application dans cet atelier de programmation ?

Oui Non

Avez-vous l'intention de créer une application ARCore au cours des six prochains mois ?

Oui Peut-être Non