Partager via


HoloLens (1ère génération) et Azure 302b : Vision personnalisée


Remarque

Les tutoriels Mixed Reality Academy ont été conçus pour les appareils HoloLens (1re génération) et les casques immersifs de réalité mixte. Nous estimons qu’il est important de laisser ces tutoriels à la disposition des développeurs qui recherchent encore des conseils pour développer des applications sur ces appareils. Notez que ces tutoriels ne sont pas mis à jour avec les derniers ensembles d’outils ou interactions utilisés pour HoloLens 2. Ils sont fournis dans le but de fonctionner sur les appareils pris en charge. Il y aura une nouvelle série de tutoriels qui seront publiés à l’avenir qui montreront comment développer pour HoloLens 2. Cet avis sera mis à jour avec un lien vers ces didacticiels lorsqu’ils sont publiés.


Dans ce cours, vous allez apprendre à reconnaître du contenu visuel personnalisé dans une image fournie, à l’aide des fonctionnalités Azure Custom Vision dans une application de réalité mixte.

Ce service vous permet d’entraîner un modèle Machine Learning à l’aide d’images objet. Vous allez ensuite utiliser le modèle entraîné pour reconnaître des objets similaires, comme fourni par la capture de caméra de Microsoft HoloLens ou une caméra connectée à votre PC pour les casques immersifs (VR).

Résultat du cours

Azure Custom Vision est un service cognitif Microsoft qui permet aux développeurs de créer des classifieurs d’images personnalisés. Ces classifieurs peuvent ensuite être utilisés avec de nouvelles images pour reconnaître ou classifier des objets au sein de cette nouvelle image. Le service fournit un portail en ligne simple et facile à utiliser pour simplifier le processus. Pour plus d’informations, consultez la page Du service Azure Custom Vision.

Une fois ce cours terminé, vous aurez une application de réalité mixte qui sera en mesure de fonctionner en deux modes :

  • Mode d’analyse : configuration manuelle du service Custom Vision en chargeant des images, en créant des balises et en formant le service pour reconnaître différents objets (dans ce cas, souris et clavier). Vous allez ensuite créer une application HoloLens qui capturera des images à l’aide de l’appareil photo et essayez de reconnaître ces objets dans le monde réel.

  • Mode d’entraînement : vous allez implémenter du code qui active un « mode d’entraînement » dans votre application. Le mode d’entraînement vous permet de capturer des images à l’aide de l’appareil photo de HoloLens, de charger les images capturées dans le service et d’entraîner le modèle de vision personnalisée.

Ce cours vous apprend à obtenir les résultats du service Custom Vision dans un exemple d’application basé sur Unity. Il vous sera possible d’appliquer ces concepts à une application personnalisée que vous créez peut-être.

Prise en charge des appareils

Cours HoloLens Casques immersifs
MR et Azure 302b : Vision personnalisée ✔️ ✔️

Remarque

Bien que ce cours se concentre principalement sur HoloLens, vous pouvez également appliquer ce que vous apprenez dans ce cours aux casques immersifs Windows Mixed Reality (VR). Étant donné que les casques immersifs (VR) n’ont pas de caméras accessibles, vous aurez besoin d’une caméra externe connectée à votre PC. À mesure que vous suivez le cours, vous verrez des notes sur les modifications que vous devrez peut-être utiliser pour prendre en charge les casques immersifs (VR).

Prérequis

Remarque

Ce tutoriel est conçu pour les développeurs qui ont une expérience de base avec Unity et C#. Sachez également que les conditions préalables et les instructions écrites contenues dans ce document représentent ce qui a été testé et vérifié au moment de l’écriture (juillet 2018). Vous êtes libre d’utiliser le logiciel le plus récent, comme indiqué dans l’article d’installation des outils , bien qu’il ne soit pas supposé que les informations de ce cours correspondent parfaitement à ce que vous trouverez dans les logiciels plus récents que ceux répertoriés ci-dessous.

Nous vous recommandons le matériel et les logiciels suivants pour ce cours :

Avant de commencer

  1. Pour éviter de rencontrer des problèmes lors de la création de ce projet, il est fortement recommandé de créer le projet mentionné dans ce didacticiel dans un dossier racine ou quasi-racine (des chemins de dossier longs peuvent provoquer des problèmes au moment de la génération).
  2. Configurez et testez votre HoloLens. Si vous avez besoin de support pour configurer votre HoloLens, veillez à consulter l’article de configuration de HoloLens.
  3. Il est judicieux d’effectuer l’étalonnage et le réglage des capteurs lors du développement d’une nouvelle application HoloLens (parfois, il peut aider à effectuer ces tâches pour chaque utilisateur).

Pour obtenir de l’aide sur l’étalonnage, suivez ce lien vers l’article d’étalonnage HoloLens.

Pour obtenir de l’aide sur le réglage des capteurs, suivez ce lien vers l’article Paramétrage du capteur HoloLens.

Chapitre 1 - Portail du service Custom Vision

Pour utiliser le service Custom Vision dans Azure, vous devez configurer une instance du service à rendre disponible pour votre application.

  1. Tout d’abord, accédez à la page principale du service Custom Vision.

  2. Cliquez sur le bouton Prise en main .

    Prise en main du service Custom Vision

  3. Connectez-vous au portail du service Custom Vision.

    Se connecter au portail

    Remarque

    Si vous n’avez pas encore de compte Azure, vous devez en créer un. Si vous suivez ce tutoriel dans une situation de salle de classe ou de laboratoire, demandez à votre instructeur ou à l’un des proctoreurs de vous aider à configurer votre nouveau compte.

  4. Une fois que vous êtes connecté pour la première fois, vous serez invité à utiliser le panneau Conditions d’utilisation . Cliquez sur la case à cocher pour accepter les conditions. Cliquez ensuite sur je suis d’accord.

    Conditions d’utilisation du service

  5. Après avoir accepté les termes, vous êtes redirigé vers la section Projets du portail. Cliquez sur Nouveau projet.

    Création d’un projet

  6. Un onglet s’affiche sur le côté droit, ce qui vous invite à spécifier certains champs pour le projet.

    1. Insérez un nom pour votre projet.

    2. Insérez une description pour votre projet (facultatif).

    3. Choisissez un groupe de ressources ou créez-en un. Un groupe de ressources permet de surveiller, de contrôler l’accès, de provisionner et de gérer la facturation d’une collection de ressources Azure. Il est recommandé de conserver tous les services Azure associés à un seul projet (par exemple, ces cours) sous un groupe de ressources commun.

    4. Définir les types de projet sur classification

    5. Définissez les domaines en tant que général.

      Définir les domaines

      Si vous souhaitez en savoir plus sur les groupes de ressources Azure, consultez l’article du groupe de ressources.

  7. Une fois que vous avez terminé, cliquez sur Créer un projet, vous êtes redirigé vers le service Custom Vision, page du projet.

Chapitre 2 - Formation de votre projet Custom Vision

Une fois dans le portail Custom Vision, votre objectif principal est d’entraîner votre projet pour reconnaître des objets spécifiques dans des images. Vous avez besoin d’au moins cinq images (5), bien que dix (10) soient préférées, pour chaque objet que vous souhaitez que votre application reconnaisse. Vous pouvez utiliser les images fournies avec ce cours (une souris d’ordinateur et un clavier).

Pour effectuer l’apprentissage de votre projet Custom Vision Service :

  1. Cliquez sur le + bouton en regard des balises.

    Ajouter une balise

  2. Ajoutez le nom de l’objet que vous souhaitez reconnaître. Cliquez sur Save(Enregistrer).

    Ajouter un nom d’objet et enregistrer

  3. Vous remarquerez que votre balise a été ajoutée (vous devrez peut-être recharger votre page pour qu’elle apparaisse). Cochez la case en regard de votre nouvelle balise, si elle n’est pas déjà cochée.

    Activer une nouvelle balise

  4. Cliquez sur Ajouter des images dans le centre de la page.

    Ajouter des images

  5. Cliquez sur Parcourir les fichiers locaux et recherchez, puis sélectionnez les images que vous souhaitez charger, avec au minimum cinq (5). N’oubliez pas que toutes ces images doivent contenir l’objet que vous entraînez.

    Remarque

    Vous pouvez sélectionner plusieurs images à la fois pour charger.

  6. Une fois que vous pouvez voir les images dans l’onglet, sélectionnez la balise appropriée dans la zone Mes balises .

    Sélectionner des balises

  7. Cliquez sur Charger des fichiers. Les fichiers commencent à être chargés. Une fois que vous avez confirmé le chargement, cliquez sur Terminé.

    Télécharger des fichiers

  8. Répétez le même processus pour créer une balise nommée Clavier et charger les photos appropriées. Veillez à décocher la souris une fois que vous avez créé les nouvelles balises, afin d’afficher la fenêtre Ajouter des images .

  9. Une fois que vous avez configuré les deux balises, cliquez sur Entraîner et la première itération d’entraînement commence à générer.

    Activer l’itération d’entraînement

  10. Une fois qu’il est généré, vous serez en mesure de voir deux boutons appelés Make default and Prediction URL. Cliquez d’abord sur Définir la valeur par défaut , puis sur l’URL de prédiction.

    Définir l’URL par défaut et la prédiction

    Remarque

    L’URL du point de terminaison fournie à partir de ce point de terminaison est définie sur la valeur par défaut de l’itération. Par conséquent, si vous effectuez ultérieurement une nouvelle itération et que vous la mettez à jour comme valeur par défaut, vous n’aurez pas besoin de modifier votre code.

  11. Une fois que vous avez cliqué sur l’URL de prédiction, ouvrez le Bloc-notes, puis copiez et collez l’URL et la clé de prédiction, afin de pouvoir la récupérer lorsque vous en avez besoin plus tard dans le code.

    Copier et coller l’URL et la clé de prédiction

  12. Cliquez sur le Cog en haut à droite de l’écran.

    Cliquez sur l’icône de roue dentée pour ouvrir les paramètres

  13. Copiez la clé d’entraînement et collez-la dans un Bloc-notes pour une utilisation ultérieure.

    Copier la clé d’entraînement

  14. Copiez également votre ID de projet et collez-le dans votre fichier bloc-notes pour une utilisation ultérieure.

    Copier l’ID du projet

Chapitre 3 - Configurer le projet Unity

Voici une configuration classique pour le développement avec la réalité mixte, et en tant que tel, est un bon modèle pour d’autres projets.

  1. Ouvrez Unity , puis cliquez sur Nouveau.

    Créer un projet Unity

  2. Vous devez maintenant fournir un nom de projet Unity. Insérez AzureCustomVision. Vérifiez que le modèle de projet est défini sur 3D. Définissez l’emplacement sur un emplacement approprié pour vous (n’oubliez pas que les répertoires racines sont plus proches). Cliquez ensuite sur Créer un projet.

    Configurer les paramètres du projet

  3. Avec Unity ouvert, il vaut la peine de vérifier que l’éditeur de script par défaut est défini sur Visual Studio. Accédez à Modifier>les préférences, puis à partir de la nouvelle fenêtre, accédez à Outils externes. Remplacez l’éditeur de script externe par Visual Studio 2017. Fermez la fenêtre Préférences.

    Configurer des outils externes

  4. Ensuite, accédez à Paramètres de build de fichier > et sélectionnez plateforme Windows universelle, puis cliquez sur le bouton Basculer la plateforme pour appliquer votre sélection.

    Configurer les paramètres de génération

  5. Tout en restant dans les paramètres de build de fichier > et assurez-vous que :

    1. L’appareil cible est défini sur HoloLens

      Pour les casques immersifs, définissez l’appareil cible sur n’importe quel appareil.

    2. Le type de build est défini sur D3D

    3. Le KIT SDK est défini sur La dernière version installée

    4. La version de Visual Studio est définie sur La dernière version installée

    5. La génération et l’exécution sont définies sur Ordinateur local

    6. Enregistrez la scène et ajoutez-la à la build.

      1. Pour ce faire, sélectionnez Ajouter des scènes ouvertes. Une fenêtre d’enregistrement s’affiche.

        Ajouter une scène ouverte à la liste de build

      2. Créez un dossier pour cela et toute scène future, puis sélectionnez le bouton Nouveau dossier pour créer un dossier, nommez-le Scènes.

        Créer un dossier de scène

      3. Ouvrez votre dossier Scènes nouvellement créé, puis dans le champ Fichier : champ de texte, tapez CustomVisionScene, puis cliquez sur Enregistrer.

        Nom du nouveau fichier de scène

        Sachez que vous devez enregistrer vos scènes Unity dans le dossier Assets , car elles doivent être associées au projet Unity. La création du dossier scènes (et d’autres dossiers similaires) est un moyen typique de structurer un projet Unity.

    7. Les paramètres restants, dans Paramètres de build, doivent être laissés comme valeurs par défaut pour l’instant.

      Paramètres de génération par défaut

  6. Dans la fenêtre Paramètres de build, cliquez sur le bouton Paramètres du lecteur, ce qui ouvre le panneau associé dans l’espace où se trouve l’inspecteur.

  7. Dans ce panneau, quelques paramètres doivent être vérifiés :

    1. Sous l’onglet Autres paramètres :

      1. La version du runtime de script doit être expérimentale (équivalent .NET 4.6), ce qui déclenche un redémarrage de l’éditeur.

      2. Le serveur principal de script doit être .NET

      3. Le niveau de compatibilité des API doit être .NET 4.6

      Définir la compantiblité de l’API

    2. Sous l’onglet Paramètres de publication, sous Fonctionnalités, vérifiez :

      1. InternetClient

      2. Webcam

      3. Microphone

      Configurer les paramètres de publication

    3. Plus loin dans le panneau, dans les paramètres XR (trouvés ci-dessous paramètres de publication), cochez Virtual Reality Pris en charge, vérifiez que le SDK Windows Mixed Reality est ajouté.

    Configurer les paramètres XR

  8. De retour dans les projets Unity C# des paramètres de build n’est plus grisé ; cochez la case en regard de cela.

  9. Fermez la fenêtre Build Settings.

  10. Enregistrez votre scène et votre projet (FILE > SAVE SCENE / FILE > SAVE PROJECT).

Chapitre 4 - Importation de la DLL Newtonsoft dans Unity

Important

Si vous souhaitez ignorer le composant De configuration Unity de ce cours et continuer directement dans le code, n’hésitez pas à télécharger ce package Azure-MR-302b.unity, à l’importer dans votre projet en tant que package personnalisé, puis à partir du chapitre 6.

Ce cours nécessite l’utilisation de la bibliothèque Newtonsoft , que vous pouvez ajouter en tant que DLL à vos ressources. Le package contenant cette bibliothèque peut être téléchargé à partir de ce lien. Pour importer la bibliothèque Newtonsoft dans votre projet, utilisez le package Unity fourni avec ce cours.

  1. Ajoutez le .unitypackage à Unity à l’aide de l’option de menu Package personnalisé d’importationde package> deressources.>

  2. Dans la zone Importer un package Unity qui s’affiche, vérifiez que tous les plug-ins sous (et y compris) sont sélectionnés.

    Importer tous les éléments de package

  3. Cliquez sur le bouton Importer pour ajouter les éléments à votre projet.

  4. Accédez au dossier Newtonsoft sous Plug-ins dans l’affichage du projet et sélectionnez le plug-in Newtonsoft.Json.

    Sélectionner le plug-in Newtonsoft

  5. Une fois le plug-in Newtonsoft.Json sélectionné, vérifiez que n’importe quelle plateforme est désactivée, puis vérifiez que WSAPlayer est également désactivé, puis cliquez sur Appliquer. Il s’agit simplement de confirmer que les fichiers sont correctement configurés.

    Configurer le plug-in Newtonsoft

    Remarque

    Le marquage de ces plug-ins les configure uniquement pour être utilisés dans l’éditeur Unity. Il existe un ensemble différent d’entre eux dans le dossier WSA qui sera utilisé une fois le projet exporté à partir d’Unity.

  6. Ensuite, vous devez ouvrir le dossier WSA dans le dossier Newtonsoft . Vous verrez une copie du même fichier que celui que vous venez de configurer. Sélectionnez le fichier, puis dans l’inspecteur, vérifiez que

    • Toute plateforme n’est pas cochée
    • seul WSAPlayer est activé
    • Le processus n’est pas vérifié

    Configurer les paramètres de la plateforme de plug-in Newtonsoft

Chapitre 5 - Configuration de la caméra

  1. Dans le panneau Hiérarchie, sélectionnez l’appareil photo principal.

  2. Une fois sélectionné, vous pourrez voir tous les composants de la caméra principale dans le panneau Inspecteur.

    1. L’objet caméra doit être nommé Main Camera (notez l’orthographe !)

    2. La balise Main Camera doit être définie sur MainCamera (notez l’orthographe !)

    3. Vérifiez que la position de transformation est définie sur 0, 0, 0

    4. Définissez Clear Flags sur Solid Color (ignorez-le pour le casque immersif).

    5. Définissez la couleur d’arrière-plan du composant caméra sur Black, Alpha 0 (Code hexadécimal : #00000000000) (ignorez ceci pour le casque immersif).

    Configurer les propriétés du composant Caméra

Chapitre 6 : créez la classe CustomVisionAnalyser.

À ce stade, vous êtes prêt à écrire du code.

Vous commencerez par la classe CustomVisionAnalyser .

Remarque

Les appels au service Custom Vision effectués dans le code ci-dessous sont effectués à l’aide de l’API REST Custom Vision. À l’aide de cela, vous verrez comment implémenter et utiliser cette API (utile pour comprendre comment implémenter quelque chose de similaire à votre propre). N’oubliez pas que Microsoft propose un KIT de développement logiciel (SDK) du service Custom Vision qui peut également être utilisé pour effectuer des appels au service. Pour plus d’informations, consultez l’article du Kit de développement logiciel (SDK) du service Custom Vision.

Cette classe est responsable des points suivants :

  • Chargement de la dernière image capturée sous la forme d’un tableau d’octets.

  • Envoi du tableau d’octets à votre instance azure Custom Vision Service à des fins d’analyse.

  • Réception de la réponse sous forme de chaîne JSON.

  • Désérialisation de la réponse et transmission de la prédiction résultante à la classe SceneOrganiser, qui s’occupera de la façon dont la réponse doit être affichée.

Pour créer cette classe :

  1. Cliquez avec le bouton droit dans le dossier de ressources situé dans le volet projet, puis cliquez sur Créer > un dossier. Appelez les scripts du dossier.

    Créer un dossier de scripts

  2. Double-cliquez sur le dossier que vous venez de créer pour l’ouvrir.

  3. Cliquez avec le bouton droit dans le dossier, puis cliquez sur Créer>un script C#. Nommez le script CustomVisionAnalyser.

  4. Double-cliquez sur le nouveau script CustomVisionAnalyser pour l’ouvrir avec Visual Studio.

  5. Mettez à jour les espaces de noms en haut de votre fichier pour qu’ils correspondent aux éléments suivants :

    using System.Collections;
    using System.IO;
    using UnityEngine;
    using UnityEngine.Networking;
    using Newtonsoft.Json;
    
  6. Dans la classe CustomVisionAnalyser , ajoutez les variables suivantes :

        /// <summary>
        /// Unique instance of this class
        /// </summary>
        public static CustomVisionAnalyser Instance;
    
        /// <summary>
        /// Insert your Prediction Key here
        /// </summary>
        private string predictionKey = "- Insert your key here -";
    
        /// <summary>
        /// Insert your prediction endpoint here
        /// </summary>
        private string predictionEndpoint = "Insert your prediction endpoint here";
    
        /// <summary>
        /// Byte array of the image to submit for analysis
        /// </summary>
        [HideInInspector] public byte[] imageBytes;
    

    Remarque

    Veillez à insérer votre clé de prédiction dans la variable predictionKey et votre point de terminaison de prédiction dans la variable predictionEndpoint . Vous les avez copiées dans le Bloc-notes précédemment dans le cours.

  7. Le code pour Awake() doit maintenant être ajouté pour initialiser la variable d’instance :

        /// <summary>
        /// Initialises this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. Supprimez les méthodes Start() et Update().

  9. Ensuite, ajoutez la coroutine (avec la méthode GetImageAsByteArray() statique ci-dessous, qui obtient les résultats de l’analyse de l’image capturée par la classe ImageCapture.

    Remarque

    Dans la coroutine AnalyseImageCapture , il existe un appel à la classe SceneOrganiser que vous n’êtes pas encore à créer. Par conséquent, laissez ces lignes commentées pour l’instant.

        /// <summary>
        /// Call the Computer Vision Service to submit the image.
        /// </summary>
        public IEnumerator AnalyseLastImageCaptured(string imagePath)
        {
            WWWForm webForm = new WWWForm();
            using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(predictionEndpoint, webForm))
            {
                // Gets a byte array out of the saved image
                imageBytes = GetImageAsByteArray(imagePath);
    
                unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream");
                unityWebRequest.SetRequestHeader("Prediction-Key", predictionKey);
    
                // The upload handler will help uploading the byte array with the request
                unityWebRequest.uploadHandler = new UploadHandlerRaw(imageBytes);
                unityWebRequest.uploadHandler.contentType = "application/octet-stream";
    
                // The download handler will help receiving the analysis from Azure
                unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
    
                // Send the request
                yield return unityWebRequest.SendWebRequest();
    
                string jsonResponse = unityWebRequest.downloadHandler.text;
    
                // The response will be in JSON format, therefore it needs to be deserialized    
    
                // The following lines refers to a class that you will build in later Chapters
                // Wait until then to uncomment these lines
    
                //AnalysisObject analysisObject = new AnalysisObject();
                //analysisObject = JsonConvert.DeserializeObject<AnalysisObject>(jsonResponse);
                //SceneOrganiser.Instance.SetTagsToLastLabel(analysisObject);
            }
        }
    
        /// <summary>
        /// Returns the contents of the specified image file as a byte array.
        /// </summary>
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
    
            BinaryReader binaryReader = new BinaryReader(fileStream);
    
            return binaryReader.ReadBytes((int)fileStream.Length);
        }
    
  10. Veillez à enregistrer vos modifications dans Visual Studio avant de revenir à Unity.

Chapitre 7 - Créer la classe CustomVisionObjects

La classe que vous allez créer est maintenant la classe CustomVisionObjects .

Ce script contient un certain nombre d’objets utilisés par d’autres classes pour sérialiser et désérialiser les appels effectués au service Custom Vision.

Avertissement

Il est important de noter le point de terminaison fourni par le service Custom Vision, car la structure JSON ci-dessous a été configurée pour fonctionner avec Custom Vision Prediction v2.0. Si vous avez une autre version, vous devrez peut-être mettre à jour la structure ci-dessous.

Pour créer cette classe :

  1. Cliquez avec le bouton droit dans le dossier Scripts, puis cliquez sur Créer un>script C#. Appelez le script CustomVisionObjects.

  2. Double-cliquez sur le nouveau script CustomVisionObjects pour l’ouvrir avec Visual Studio.

  3. Ajoutez les espaces de noms suivants en haut du fichier :

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. Supprimez les méthodes Start() et Update() à l’intérieur de la classe CustomVisionObjects ; cette classe doit maintenant être vide.

  5. Ajoutez les classes suivantes en dehors de la classe CustomVisionObjects . Ces objets sont utilisés par la bibliothèque Newtonsoft pour sérialiser et désérialiser les données de réponse :

    // The objects contained in this script represent the deserialized version
    // of the objects used by this application 
    
    /// <summary>
    /// Web request object for image data
    /// </summary>
    class MultipartObject : IMultipartFormSection
    {
        public string sectionName { get; set; }
    
        public byte[] sectionData { get; set; }
    
        public string fileName { get; set; }
    
        public string contentType { get; set; }
    }
    
    /// <summary>
    /// JSON of all Tags existing within the project
    /// contains the list of Tags
    /// </summary> 
    public class Tags_RootObject
    {
        public List<TagOfProject> Tags { get; set; }
        public int TotalTaggedImages { get; set; }
        public int TotalUntaggedImages { get; set; }
    }
    
    public class TagOfProject
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public int ImageCount { get; set; }
    }
    
    /// <summary>
    /// JSON of Tag to associate to an image
    /// Contains a list of hosting the tags,
    /// since multiple tags can be associated with one image
    /// </summary> 
    public class Tag_RootObject
    {
        public List<Tag> Tags { get; set; }
    }
    
    public class Tag
    {
        public string ImageId { get; set; }
        public string TagId { get; set; }
    }
    
    /// <summary>
    /// JSON of Images submitted
    /// Contains objects that host detailed information about one or more images
    /// </summary> 
    public class ImageRootObject
    {
        public bool IsBatchSuccessful { get; set; }
        public List<SubmittedImage> Images { get; set; }
    }
    
    public class SubmittedImage
    {
        public string SourceUrl { get; set; }
        public string Status { get; set; }
        public ImageObject Image { get; set; }
    }
    
    public class ImageObject
    {
        public string Id { get; set; }
        public DateTime Created { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public string ImageUri { get; set; }
        public string ThumbnailUri { get; set; }
    }
    
    /// <summary>
    /// JSON of Service Iteration
    /// </summary> 
    public class Iteration
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public bool IsDefault { get; set; }
        public string Status { get; set; }
        public string Created { get; set; }
        public string LastModified { get; set; }
        public string TrainedAt { get; set; }
        public string ProjectId { get; set; }
        public bool Exportable { get; set; }
        public string DomainId { get; set; }
    }
    
    /// <summary>
    /// Predictions received by the Service after submitting an image for analysis
    /// </summary> 
    [Serializable]
    public class AnalysisObject
    {
        public List<Prediction> Predictions { get; set; }
    }
    
    [Serializable]
    public class Prediction
    {
        public string TagName { get; set; }
        public double Probability { get; set; }
    }
    

Chapitre 8 - Créer la classe VoiceRecognizer

Cette classe reconnaît l’entrée vocale de l’utilisateur.

Pour créer cette classe :

  1. Cliquez avec le bouton droit dans le dossier Scripts, puis cliquez sur Créer un>script C#. Appelez le script VoiceRecognizer.

  2. Double-cliquez sur le nouveau script VoiceRecognizer pour l’ouvrir avec Visual Studio.

  3. Ajoutez les espaces de noms suivants au-dessus de la classe VoiceRecognizer :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.Windows.Speech;
    
  4. Ajoutez ensuite les variables suivantes à l’intérieur de la classe VoiceRecognizer , au-dessus de la méthode Start() :

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static VoiceRecognizer Instance;
    
        /// <summary>
        /// Recognizer class for voice recognition
        /// </summary>
        internal KeywordRecognizer keywordRecognizer;
    
        /// <summary>
        /// List of Keywords registered
        /// </summary>
        private Dictionary<string, Action> _keywords = new Dictionary<string, Action>();
    
  5. Ajoutez les méthodes Awake() et Start(), dont la dernière configure les mots clés utilisateur à reconnaître lors de l’association d’une balise à une image :

        /// <summary>
        /// Called on initialization
        /// </summary>
        private void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Runs at initialization right after Awake method
        /// </summary>
        void Start ()
        {
    
            Array tagsArray = Enum.GetValues(typeof(CustomVisionTrainer.Tags));
    
            foreach (object tagWord in tagsArray)
            {
                _keywords.Add(tagWord.ToString(), () =>
                {
                    // When a word is recognized, the following line will be called
                    CustomVisionTrainer.Instance.VerifyTag(tagWord.ToString());
                });
            }
    
            _keywords.Add("Discard", () =>
            {
                // When a word is recognized, the following line will be called
                // The user does not want to submit the image
                // therefore ignore and discard the process
                ImageCapture.Instance.ResetImageCapture();
                keywordRecognizer.Stop();
            });
    
            //Create the keyword recognizer 
            keywordRecognizer = new KeywordRecognizer(_keywords.Keys.ToArray());
    
            // Register for the OnPhraseRecognized event 
            keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        }
    
  6. Supprimez la méthode Update().

  7. Ajoutez le gestionnaire suivant, qui est appelé chaque fois que l’entrée vocale est reconnue :

        /// <summary>
        /// Handler called when a word is recognized
        /// </summary>
        private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
        {
            Action keywordAction;
            // if the keyword recognized is in our dictionary, call that Action.
            if (_keywords.TryGetValue(args.text, out keywordAction))
            {
                keywordAction.Invoke();
            }
        }
    
  8. Veillez à enregistrer vos modifications dans Visual Studio avant de revenir à Unity.

Remarque

Ne vous inquiétez pas du code qui peut sembler avoir une erreur, car vous fournirez bientôt d’autres classes, ce qui les corrigera.

Chapitre 9 - Créer la classe CustomVisionTrainer

Cette classe va chaîner une série d’appels web pour entraîner le service Custom Vision. Chaque appel sera expliqué en détail juste au-dessus du code.

Pour créer cette classe :

  1. Cliquez avec le bouton droit dans le dossier Scripts, puis cliquez sur Créer un>script C#. Appelez le script CustomVisionTrainer.

  2. Double-cliquez sur le nouveau script CustomVisionTrainer pour l’ouvrir avec Visual Studio.

  3. Ajoutez les espaces de noms suivants au-dessus de la classe CustomVisionTrainer :

    using Newtonsoft.Json;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. Ajoutez ensuite les variables suivantes à l’intérieur de la classe CustomVisionTrainer, au-dessus de la méthode Start().

    Remarque

    L’URL de formation utilisée ici est fournie dans la documentation Custom Vision Training 1.2 et a une structure de : https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/{projectId}/
    Pour plus d’informations, consultez l’API de référence Custom Vision Training v1.2.

    Avertissement

    Il est important que vous notiez le point de terminaison que le service Custom Vision vous fournit pour le mode d’entraînement, car la structure JSON utilisée (dans la classe CustomVisionObjects) a été configurée pour fonctionner avec Custom Vision Training v1.2. Si vous avez une autre version, vous devrez peut-être mettre à jour la structure Objects .

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static CustomVisionTrainer Instance;
    
        /// <summary>
        /// Custom Vision Service URL root
        /// </summary>
        private string url = "https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/";
    
        /// <summary>
        /// Insert your prediction key here
        /// </summary>
        private string trainingKey = "- Insert your key here -";
    
        /// <summary>
        /// Insert your Project Id here
        /// </summary>
        private string projectId = "- Insert your Project Id here -";
    
        /// <summary>
        /// Byte array of the image to submit for analysis
        /// </summary>
        internal byte[] imageBytes;
    
        /// <summary>
        /// The Tags accepted
        /// </summary>
        internal enum Tags {Mouse, Keyboard}
    
        /// <summary>
        /// The UI displaying the training Chapters
        /// </summary>
        private TextMesh trainingUI_TextMesh;
    

    Important

    Assurez-vous d’ajouter votre valeur de clé de service (clé d’entraînement) et la valeur ID de projet, que vous avez notée précédemment. Il s’agit des valeurs que vous avez collectées à partir du portail plus haut dans le cours (chapitre 2, étape 10 et versions ultérieures).

  5. Ajoutez les méthodes Start() et Awake() suivantes. Ces méthodes sont appelées lors de l’initialisation et contiennent l’appel pour configurer l’interface utilisateur :

        /// <summary>
        /// Called on initialization
        /// </summary>
        private void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Runs at initialization right after Awake method
        /// </summary>
        private void Start()
        { 
            trainingUI_TextMesh = SceneOrganiser.Instance.CreateTrainingUI("TrainingUI", 0.04f, 0, 4, false);
        }
    
  6. Supprimez la méthode Update(). Cette classe n’en aura pas besoin.

  7. Ajoutez la méthode RequestTagSelection(). Cette méthode est la première à être appelée lorsqu’une image a été capturée et stockée dans l’appareil et est maintenant prête à être envoyée au service Custom Vision pour l’entraîner. Cette méthode affiche, dans l’interface utilisateur d’apprentissage, un ensemble de mots clés que l’utilisateur peut utiliser pour baliser l’image capturée. Il avertit également la classe VoiceRecognizer de commencer à écouter l’utilisateur pour l’entrée vocale.

        internal void RequestTagSelection()
        {
            trainingUI_TextMesh.gameObject.SetActive(true);
            trainingUI_TextMesh.text = $" \nUse voice command \nto choose between the following tags: \nMouse\nKeyboard \nor say Discard";
    
            VoiceRecognizer.Instance.keywordRecognizer.Start();
        }
    
  8. Ajoutez la méthode VerifyTag(). Cette méthode reçoit l’entrée vocale reconnue par la classe VoiceRecognizer et vérifie sa validité, puis démarre le processus d’entraînement.

        /// <summary>
        /// Verify voice input against stored tags.
        /// If positive, it will begin the Service training process.
        /// </summary>
        internal void VerifyTag(string spokenTag)
        {
            if (spokenTag == Tags.Mouse.ToString() || spokenTag == Tags.Keyboard.ToString())
            {
                trainingUI_TextMesh.text = $"Tag chosen: {spokenTag}";
                VoiceRecognizer.Instance.keywordRecognizer.Stop();
                StartCoroutine(SubmitImageForTraining(ImageCapture.Instance.filePath, spokenTag));
            }
        }
    
  9. Ajoutez la méthode SubmitImageForTraining(). Cette méthode démarre le processus d’entraînement du service Custom Vision. La première étape consiste à récupérer l’ID d’étiquette du service associé à l’entrée vocale validée de l’utilisateur. L’ID de balise sera ensuite chargé avec l’image.

        /// <summary>
        /// Call the Custom Vision Service to submit the image.
        /// </summary>
        public IEnumerator SubmitImageForTraining(string imagePath, string tag)
        {
            yield return new WaitForSeconds(2);
            trainingUI_TextMesh.text = $"Submitting Image \nwith tag: {tag} \nto Custom Vision Service";
            string imageId = string.Empty;
            string tagId = string.Empty;
    
            // Retrieving the Tag Id relative to the voice input
            string getTagIdEndpoint = string.Format("{0}{1}/tags", url, projectId);
            using (UnityWebRequest www = UnityWebRequest.Get(getTagIdEndpoint))
            {
                www.SetRequestHeader("Training-Key", trainingKey);
                www.downloadHandler = new DownloadHandlerBuffer();
                yield return www.SendWebRequest();
                string jsonResponse = www.downloadHandler.text;
    
                Tags_RootObject tagRootObject = JsonConvert.DeserializeObject<Tags_RootObject>(jsonResponse);
    
                foreach (TagOfProject tOP in tagRootObject.Tags)
                {
                    if (tOP.Name == tag)
                    {
                        tagId = tOP.Id;
                    }             
                }
            }
    
            // Creating the image object to send for training
            List<IMultipartFormSection> multipartList = new List<IMultipartFormSection>();
            MultipartObject multipartObject = new MultipartObject();
            multipartObject.contentType = "application/octet-stream";
            multipartObject.fileName = "";
            multipartObject.sectionData = GetImageAsByteArray(imagePath);
            multipartList.Add(multipartObject);
    
            string createImageFromDataEndpoint = string.Format("{0}{1}/images?tagIds={2}", url, projectId, tagId);
    
            using (UnityWebRequest www = UnityWebRequest.Post(createImageFromDataEndpoint, multipartList))
            {
                // Gets a byte array out of the saved image
                imageBytes = GetImageAsByteArray(imagePath);           
    
                //unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream");
                www.SetRequestHeader("Training-Key", trainingKey);
    
                // The upload handler will help uploading the byte array with the request
                www.uploadHandler = new UploadHandlerRaw(imageBytes);
    
                // The download handler will help receiving the analysis from Azure
                www.downloadHandler = new DownloadHandlerBuffer();
    
                // Send the request
                yield return www.SendWebRequest();
    
                string jsonResponse = www.downloadHandler.text;
    
                ImageRootObject m = JsonConvert.DeserializeObject<ImageRootObject>(jsonResponse);
                imageId = m.Images[0].Image.Id;
            }
            trainingUI_TextMesh.text = "Image uploaded";
            StartCoroutine(TrainCustomVisionProject());
        }
    
  10. Ajoutez la méthode TrainCustomVisionProject(). Une fois l’image envoyée et étiquetée, cette méthode est appelée. Il crée une itération qui sera entraînée avec toutes les images précédentes envoyées au service, ainsi que l’image juste chargée. Une fois l’entraînement terminé, cette méthode appelle une méthode pour définir l’itération nouvellement créée comme valeur par défaut, afin que le point de terminaison que vous utilisez pour l’analyse soit l’itération entraînée la plus récente.

        /// <summary>
        /// Call the Custom Vision Service to train the Service.
        /// It will generate a new Iteration in the Service
        /// </summary>
        public IEnumerator TrainCustomVisionProject()
        {
            yield return new WaitForSeconds(2);
    
            trainingUI_TextMesh.text = "Training Custom Vision Service";
    
            WWWForm webForm = new WWWForm();
    
            string trainProjectEndpoint = string.Format("{0}{1}/train", url, projectId);
    
            using (UnityWebRequest www = UnityWebRequest.Post(trainProjectEndpoint, webForm))
            {
                www.SetRequestHeader("Training-Key", trainingKey);
                www.downloadHandler = new DownloadHandlerBuffer();
                yield return www.SendWebRequest();
                string jsonResponse = www.downloadHandler.text;
                Debug.Log($"Training - JSON Response: {jsonResponse}");
    
                // A new iteration that has just been created and trained
                Iteration iteration = new Iteration();
                iteration = JsonConvert.DeserializeObject<Iteration>(jsonResponse);
    
                if (www.isDone)
                {
                    trainingUI_TextMesh.text = "Custom Vision Trained";
    
                    // Since the Service has a limited number of iterations available,
                    // we need to set the last trained iteration as default
                    // and delete all the iterations you dont need anymore
                    StartCoroutine(SetDefaultIteration(iteration)); 
                }
            }
        }
    
  11. Ajoutez la méthode SetDefaultIteration(). Cette méthode définit l’itération créée et entraînée précédemment comme valeur par défaut. Une fois terminée, cette méthode devra supprimer l’itération précédente existante dans le service. Au moment de l’écriture de ce cours, il existe une limite de dix (10) itérations maximales autorisées à exister en même temps dans le service.

        /// <summary>
        /// Set the newly created iteration as Default
        /// </summary>
        private IEnumerator SetDefaultIteration(Iteration iteration)
        {
            yield return new WaitForSeconds(5);
            trainingUI_TextMesh.text = "Setting default iteration";
    
            // Set the last trained iteration to default
            iteration.IsDefault = true;
    
            // Convert the iteration object as JSON
            string iterationAsJson = JsonConvert.SerializeObject(iteration);
            byte[] bytes = Encoding.UTF8.GetBytes(iterationAsJson);
    
            string setDefaultIterationEndpoint = string.Format("{0}{1}/iterations/{2}", 
                                                            url, projectId, iteration.Id);
    
            using (UnityWebRequest www = UnityWebRequest.Put(setDefaultIterationEndpoint, bytes))
            {
                www.method = "PATCH";
                www.SetRequestHeader("Training-Key", trainingKey);
                www.SetRequestHeader("Content-Type", "application/json");
                www.downloadHandler = new DownloadHandlerBuffer();
    
                yield return www.SendWebRequest();
    
                string jsonResponse = www.downloadHandler.text;
    
                if (www.isDone)
                {
                    trainingUI_TextMesh.text = "Default iteration is set \nDeleting Unused Iteration";
                    StartCoroutine(DeletePreviousIteration(iteration));
                }
            }
        }
    
  12. Ajoutez la méthode DeletePreviousIteration(). Cette méthode recherche et supprime l’itération précédente non par défaut :

        /// <summary>
        /// Delete the previous non-default iteration.
        /// </summary>
        public IEnumerator DeletePreviousIteration(Iteration iteration)
        {
            yield return new WaitForSeconds(5);
    
            trainingUI_TextMesh.text = "Deleting Unused \nIteration";
    
            string iterationToDeleteId = string.Empty;
    
            string findAllIterationsEndpoint = string.Format("{0}{1}/iterations", url, projectId);
    
            using (UnityWebRequest www = UnityWebRequest.Get(findAllIterationsEndpoint))
            {
                www.SetRequestHeader("Training-Key", trainingKey);
                www.downloadHandler = new DownloadHandlerBuffer();
                yield return www.SendWebRequest();
    
                string jsonResponse = www.downloadHandler.text;
    
                // The iteration that has just been trained
                List<Iteration> iterationsList = new List<Iteration>();
                iterationsList = JsonConvert.DeserializeObject<List<Iteration>>(jsonResponse);
    
                foreach (Iteration i in iterationsList)
                {
                    if (i.IsDefault != true)
                    {
                        Debug.Log($"Cleaning - Deleting iteration: {i.Name}, {i.Id}");
                        iterationToDeleteId = i.Id;
                        break;
                    }
                }
            }
    
            string deleteEndpoint = string.Format("{0}{1}/iterations/{2}", url, projectId, iterationToDeleteId);
    
            using (UnityWebRequest www2 = UnityWebRequest.Delete(deleteEndpoint))
            {
                www2.SetRequestHeader("Training-Key", trainingKey);
                www2.downloadHandler = new DownloadHandlerBuffer();
                yield return www2.SendWebRequest();
                string jsonResponse = www2.downloadHandler.text;
    
                trainingUI_TextMesh.text = "Iteration Deleted";
                yield return new WaitForSeconds(2);
                trainingUI_TextMesh.text = "Ready for next \ncapture";
    
                yield return new WaitForSeconds(2);
                trainingUI_TextMesh.text = "";
                ImageCapture.Instance.ResetImageCapture();
            }
        }
    
  13. La dernière méthode à ajouter dans cette classe est la méthode GetImageAsByteArray(), utilisée sur les appels web pour convertir l’image capturée en tableau d’octets.

        /// <summary>
        /// Returns the contents of the specified image file as a byte array.
        /// </summary>
        static byte[] GetImageAsByteArray(string imageFilePath)
        {
            FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
            BinaryReader binaryReader = new BinaryReader(fileStream);
            return binaryReader.ReadBytes((int)fileStream.Length);
        }
    
  14. Veillez à enregistrer vos modifications dans Visual Studio avant de revenir à Unity.

Chapitre 10 - Créer la classe SceneOrganiser

Cette classe :

  • Créez un objet Cursor à attacher à la caméra principale.

  • Créez un objet Label qui s’affiche lorsque le service reconnaît les objets réels.

  • Configurez l’appareil photo principal en lui attachant les composants appropriés.

  • En mode Analyse, générez les étiquettes au moment de l’exécution, dans l’espace mondial approprié par rapport à la position de la caméra principale et affichez les données reçues du service Custom Vision.

  • Quand vous êtes en mode d’entraînement, générez l’interface utilisateur qui affiche les différentes étapes du processus d’entraînement.

Pour créer cette classe :

  1. Cliquez avec le bouton droit dans le dossier Scripts, puis cliquez sur Créer un>script C#. Nommez le script SceneOrganiser.

  2. Double-cliquez sur le nouveau script SceneOrganiser pour l’ouvrir avec Visual Studio.

  3. Vous n’aurez besoin que d’un espace de noms, supprimez les autres de la classe SceneOrganiser ci-dessus :

    using UnityEngine;
    
  4. Ajoutez ensuite les variables suivantes à l’intérieur de la classe SceneOrganiser , au-dessus de la méthode Start() :

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static SceneOrganiser Instance;
    
        /// <summary>
        /// The cursor object attached to the camera
        /// </summary>
        internal GameObject cursor;
    
        /// <summary>
        /// The label used to display the analysis on the objects in the real world
        /// </summary>
        internal GameObject label;
    
        /// <summary>
        /// Object providing the current status of the camera.
        /// </summary>
        internal TextMesh cameraStatusIndicator;
    
        /// <summary>
        /// Reference to the last label positioned
        /// </summary>
        internal Transform lastLabelPlaced;
    
        /// <summary>
        /// Reference to the last label positioned
        /// </summary>
        internal TextMesh lastLabelPlacedText;
    
        /// <summary>
        /// Current threshold accepted for displaying the label
        /// Reduce this value to display the recognition more often
        /// </summary>
        internal float probabilityThreshold = 0.5f;
    
  5. Supprimez les méthodes Start() et Update().

  6. Juste en dessous des variables, ajoutez la méthode Awake(), qui initialise la classe et configure la scène.

        /// <summary>
        /// Called on initialization
        /// </summary>
        private void Awake()
        {
            // Use this class instance as singleton
            Instance = this;
    
            // Add the ImageCapture class to this GameObject
            gameObject.AddComponent<ImageCapture>();
    
            // Add the CustomVisionAnalyser class to this GameObject
            gameObject.AddComponent<CustomVisionAnalyser>();
    
            // Add the CustomVisionTrainer class to this GameObject
            gameObject.AddComponent<CustomVisionTrainer>();
    
            // Add the VoiceRecogniser class to this GameObject
            gameObject.AddComponent<VoiceRecognizer>();
    
            // Add the CustomVisionObjects class to this GameObject
            gameObject.AddComponent<CustomVisionObjects>();
    
            // Create the camera Cursor
            cursor = CreateCameraCursor();
    
            // Load the label prefab as reference
            label = CreateLabel();
    
            // Create the camera status indicator label, and place it above where predictions
            // and training UI will appear.
            cameraStatusIndicator = CreateTrainingUI("Status Indicator", 0.02f, 0.2f, 3, true);
    
            // Set camera status indicator to loading.
            SetCameraStatus("Loading");
        }
    
  7. Ajoutez maintenant la méthode CreateCameraCursor() qui crée et positionne le curseur de la caméra principale et la méthode CreateLabel(), qui crée l’objet Analysis Label .

        /// <summary>
        /// Spawns cursor for the Main Camera
        /// </summary>
        private GameObject CreateCameraCursor()
        {
            // Create a sphere as new cursor
            GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    
            // Attach it to the camera
            newCursor.transform.parent = gameObject.transform;
    
            // Resize the new cursor
            newCursor.transform.localScale = new Vector3(0.02f, 0.02f, 0.02f);
    
            // Move it to the correct position
            newCursor.transform.localPosition = new Vector3(0, 0, 4);
    
            // Set the cursor color to red
            newCursor.GetComponent<Renderer>().material = new Material(Shader.Find("Diffuse"));
            newCursor.GetComponent<Renderer>().material.color = Color.green;
    
            return newCursor;
        }
    
        /// <summary>
        /// Create the analysis label object
        /// </summary>
        private GameObject CreateLabel()
        {
            // Create a sphere as new cursor
            GameObject newLabel = new GameObject();
    
            // Resize the new cursor
            newLabel.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);
    
            // Creating the text of the label
            TextMesh t = newLabel.AddComponent<TextMesh>();
            t.anchor = TextAnchor.MiddleCenter;
            t.alignment = TextAlignment.Center;
            t.fontSize = 50;
            t.text = "";
    
            return newLabel;
        }
    
  8. Ajoutez la méthode SetCameraStatus(), qui gère les messages destinés au maillage de texte fournissant l’état de l’appareil photo.

        /// <summary>
        /// Set the camera status to a provided string. Will be coloured if it matches a keyword.
        /// </summary>
        /// <param name="statusText">Input string</param>
        public void SetCameraStatus(string statusText)
        {
            if (string.IsNullOrEmpty(statusText) == false)
            {
                string message = "white";
    
                switch (statusText.ToLower())
                {
                    case "loading":
                        message = "yellow";
                        break;
    
                    case "ready":
                        message = "green";
                        break;
    
                    case "uploading image":
                        message = "red";
                        break;
    
                    case "looping capture":
                        message = "yellow";
                        break;
    
                    case "analysis":
                        message = "red";
                        break;
                }
    
                cameraStatusIndicator.GetComponent<TextMesh>().text = $"Camera Status:\n<color={message}>{statusText}..</color>";
            }
        }
    
  9. Ajoutez les méthodes PlaceAnalysisLabel() et SetTagsToLastLabel(), qui génère et affiche les données du service Custom Vision dans la scène.

        /// <summary>
        /// Instantiate a label in the appropriate location relative to the Main Camera.
        /// </summary>
        public void PlaceAnalysisLabel()
        {
            lastLabelPlaced = Instantiate(label.transform, cursor.transform.position, transform.rotation);
            lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>();
        }
    
        /// <summary>
        /// Set the Tags as Text of the last label created. 
        /// </summary>
        public void SetTagsToLastLabel(AnalysisObject analysisObject)
        {
            lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>();
    
            if (analysisObject.Predictions != null)
            {
                foreach (Prediction p in analysisObject.Predictions)
                {
                    if (p.Probability > 0.02)
                    {
                        lastLabelPlacedText.text += $"Detected: {p.TagName} {p.Probability.ToString("0.00 \n")}";
                        Debug.Log($"Detected: {p.TagName} {p.Probability.ToString("0.00 \n")}");
                    }
                }
            }
        }
    
  10. Enfin, ajoutez la méthode CreateTrainingUI(), qui génère l’interface utilisateur affichant les plusieurs étapes du processus d’entraînement lorsque l’application est en mode entraînement. Cette méthode sera également exploitée pour créer l’objet d’état de la caméra.

        /// <summary>
        /// Create a 3D Text Mesh in scene, with various parameters.
        /// </summary>
        /// <param name="name">name of object</param>
        /// <param name="scale">scale of object (i.e. 0.04f)</param>
        /// <param name="yPos">height above the cursor (i.e. 0.3f</param>
        /// <param name="zPos">distance from the camera</param>
        /// <param name="setActive">whether the text mesh should be visible when it has been created</param>
        /// <returns>Returns a 3D text mesh within the scene</returns>
        internal TextMesh CreateTrainingUI(string name, float scale, float yPos, float zPos, bool setActive)
        {
            GameObject display = new GameObject(name, typeof(TextMesh));
            display.transform.parent = Camera.main.transform;
            display.transform.localPosition = new Vector3(0, yPos, zPos);
            display.SetActive(setActive);
            display.transform.localScale = new Vector3(scale, scale, scale);
            display.transform.rotation = new Quaternion();
            TextMesh textMesh = display.GetComponent<TextMesh>();
            textMesh.anchor = TextAnchor.MiddleCenter;
            textMesh.alignment = TextAlignment.Center;
            return textMesh;
        }
    
  11. Veillez à enregistrer vos modifications dans Visual Studio avant de revenir à Unity.

Important

Avant de continuer, ouvrez la classe CustomVisionAnalyser et, dans la méthode AnalyseLastImageCaptured(), supprimez les marques de commentaire des lignes suivantes :

  AnalysisObject analysisObject = new AnalysisObject();
  analysisObject = JsonConvert.DeserializeObject<AnalysisObject>(jsonResponse);
  SceneOrganiser.Instance.SetTagsToLastLabel(analysisObject);

Chapitre 11 - Créer la classe ImageCapture

La classe suivante que vous allez créer est la classe ImageCapture .

Cette classe est responsable des points suivants :

  • Capture d’une image à l’aide de l’appareil photo HoloLens et le stockage dans le dossier de l’application.

  • Gestion des mouvements d’appui de l’utilisateur.

  • Maintien de la valeur Enum qui détermine si l’application s’exécute en mode Analyse ou Mode d’entraînement .

Pour créer cette classe :

  1. Accédez au dossier Scripts que vous avez créé précédemment.

  2. Cliquez avec le bouton droit dans le dossier, puis cliquez sur Créer > un script C#. Nommez le script ImageCapture.

  3. Double-cliquez sur le nouveau script ImageCapture pour l’ouvrir avec Visual Studio.

  4. Remplacez les espaces de noms en haut du fichier par les éléments suivants :

    using System;
    using System.IO;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.XR.WSA.Input;
    using UnityEngine.XR.WSA.WebCam;
    
  5. Ajoutez ensuite les variables suivantes à l’intérieur de la classe ImageCapture , au-dessus de la méthode Start() :

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static ImageCapture Instance;
    
        /// <summary>
        /// Keep counts of the taps for image renaming
        /// </summary>
        private int captureCount = 0;
    
        /// <summary>
        /// Photo Capture object
        /// </summary>
        private PhotoCapture photoCaptureObject = null;
    
        /// <summary>
        /// Allows gestures recognition in HoloLens
        /// </summary>
        private GestureRecognizer recognizer;
    
        /// <summary>
        /// Loop timer
        /// </summary>
        private float secondsBetweenCaptures = 10f;
    
        /// <summary>
        /// Application main functionalities switch
        /// </summary>
        internal enum AppModes {Analysis, Training }
    
        /// <summary>
        /// Local variable for current AppMode
        /// </summary>
        internal AppModes AppMode { get; private set; }
    
        /// <summary>
        /// Flagging if the capture loop is running
        /// </summary>
        internal bool captureIsActive;
    
        /// <summary>
        /// File path of current analysed photo
        /// </summary>
        internal string filePath = string.Empty;
    
  6. Les méthodes Code for Awake() et Start() doivent maintenant être ajoutées :

        /// <summary>
        /// Called on initialization
        /// </summary>
        private void Awake()
        {
            Instance = this;
    
            // Change this flag to switch between Analysis Mode and Training Mode 
            AppMode = AppModes.Training;
        }
    
        /// <summary>
        /// Runs at initialization right after Awake method
        /// </summary>
        void Start()
        {
            // Clean up the LocalState folder of this application from all photos stored
            DirectoryInfo info = new DirectoryInfo(Application.persistentDataPath);
            var fileInfo = info.GetFiles();
            foreach (var file in fileInfo)
            {
                try
                {
                    file.Delete();
                }
                catch (Exception)
                {
                    Debug.LogFormat("Cannot delete file: ", file.Name);
                }
            } 
    
            // Subscribing to the HoloLens API gesture recognizer to track user gestures
            recognizer = new GestureRecognizer();
            recognizer.SetRecognizableGestures(GestureSettings.Tap);
            recognizer.Tapped += TapHandler;
            recognizer.StartCapturingGestures();
    
            SceneOrganiser.Instance.SetCameraStatus("Ready");
        }
    
  7. Implémentez un gestionnaire qui sera appelé lorsqu’un mouvement Tap se produit.

        /// <summary>
        /// Respond to Tap Input.
        /// </summary>
        private void TapHandler(TappedEventArgs obj)
        {
            switch (AppMode)
            {
                case AppModes.Analysis:
                    if (!captureIsActive)
                    {
                        captureIsActive = true;
    
                        // Set the cursor color to red
                        SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red;
    
                        // Update camera status to looping capture.
                        SceneOrganiser.Instance.SetCameraStatus("Looping Capture");
    
                        // Begin the capture loop
                        InvokeRepeating("ExecuteImageCaptureAndAnalysis", 0, secondsBetweenCaptures);
                    }
                    else
                    {
                        // The user tapped while the app was analyzing 
                        // therefore stop the analysis process
                        ResetImageCapture();
                    }
                    break;
    
                case AppModes.Training:
                    if (!captureIsActive)
                    {
                        captureIsActive = true;
    
                        // Call the image capture
                        ExecuteImageCaptureAndAnalysis();
    
                        // Set the cursor color to red
                        SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red;
    
                        // Update camera status to uploading image.
                        SceneOrganiser.Instance.SetCameraStatus("Uploading Image");
                    }              
                    break;
            }     
        }
    

    Remarque

    En mode Analyse , la méthode TapHandler agit comme un commutateur pour démarrer ou arrêter la boucle de capture de photos.

    En mode Entraînement , il capture une image à partir de l’appareil photo.

    Lorsque le curseur est vert, cela signifie que l’appareil photo est disponible pour prendre l’image.

    Lorsque le curseur est rouge, cela signifie que l’appareil photo est occupé.

  8. Ajoutez la méthode utilisée par l’application pour démarrer le processus de capture d’image et stocker l’image.

        /// <summary>
        /// Begin process of Image Capturing and send To Azure Custom Vision Service.
        /// </summary>
        private void ExecuteImageCaptureAndAnalysis()
        {
            // Update camera status to analysis.
            SceneOrganiser.Instance.SetCameraStatus("Analysis");
    
            // Create a label in world space using the SceneOrganiser class 
            // Invisible at this point but correctly positioned where the image was taken
            SceneOrganiser.Instance.PlaceAnalysisLabel();
    
            // Set the camera resolution to be the highest possible
            Resolution cameraResolution = PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
    
            Texture2D targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);
    
            // Begin capture process, set the image format
            PhotoCapture.CreateAsync(false, delegate (PhotoCapture captureObject)
            {
                photoCaptureObject = captureObject;
    
                CameraParameters camParameters = new CameraParameters
                {
                    hologramOpacity = 0.0f,
                    cameraResolutionWidth = targetTexture.width,
                    cameraResolutionHeight = targetTexture.height,
                    pixelFormat = CapturePixelFormat.BGRA32
                };
    
                // Capture the image from the camera and save it in the App internal folder
                captureObject.StartPhotoModeAsync(camParameters, delegate (PhotoCapture.PhotoCaptureResult result)
                {
                    string filename = string.Format(@"CapturedImage{0}.jpg", captureCount);
                    filePath = Path.Combine(Application.persistentDataPath, filename);          
                    captureCount++;              
                    photoCaptureObject.TakePhotoAsync(filePath, PhotoCaptureFileOutputFormat.JPG, OnCapturedPhotoToDisk);              
                });
            });   
        }
    
  9. Ajoutez les gestionnaires qui seront appelés lorsque la photo a été capturée et pour le moment où elle est prête à être analysée. Le résultat est ensuite passé à CustomVisionAnalyser ou CustomVisionTrainer en fonction du mode sur lequel le code est défini.

        /// <summary>
        /// Register the full execution of the Photo Capture. 
        /// </summary>
        void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result)
        {
                // Call StopPhotoMode once the image has successfully captured
                photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
        }
    
    
        /// <summary>
        /// The camera photo mode has stopped after the capture.
        /// Begin the Image Analysis process.
        /// </summary>
        void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result)
        {
            Debug.LogFormat("Stopped Photo Mode");
    
            // Dispose from the object in memory and request the image analysis 
            photoCaptureObject.Dispose();
            photoCaptureObject = null;
    
            switch (AppMode)
            {
                case AppModes.Analysis:
                    // Call the image analysis
                    StartCoroutine(CustomVisionAnalyser.Instance.AnalyseLastImageCaptured(filePath));
                    break;
    
                case AppModes.Training:
                    // Call training using captured image
                    CustomVisionTrainer.Instance.RequestTagSelection();
                    break;
            }
        }
    
        /// <summary>
        /// Stops all capture pending actions
        /// </summary>
        internal void ResetImageCapture()
        {
            captureIsActive = false;
    
            // Set the cursor color to green
            SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.green;
    
            // Update camera status to ready.
            SceneOrganiser.Instance.SetCameraStatus("Ready");
    
            // Stop the capture loop if active
            CancelInvoke();
        }
    
  10. Veillez à enregistrer vos modifications dans Visual Studio avant de revenir à Unity.

  11. Maintenant que tous les scripts ont été terminés, revenez dans l’Éditeur Unity, puis cliquez et faites glisser la classe SceneOrganiser du dossier Scripts vers l’objet Caméra principale dans le panneau Hierarchy.

Chapitre 12 - Avant la construction

Pour effectuer un test approfondi de votre application, vous devez la charger de manière indépendante sur votre HoloLens.

Avant de procéder, assurez-vous que :

  • Tous les paramètres mentionnés dans le chapitre 2 sont correctement définis.

  • Tous les champs de la caméra principale, panneau inspecteur, sont attribués correctement.

  • Le script SceneOrganiser est attaché à l’objet Main Camera .

  • Veillez à insérer votre clé de prédiction dans la variable predictionKey .

  • Vous avez inséré votre point de terminaison de prédiction dans la variable predictionEndpoint .

  • Vous avez inséré votre clé d’entraînement dans la variable trainingKey , de la classe CustomVisionTrainer .

  • Vous avez inséré votre ID de projet dans la variable projectId , de la classe CustomVisionTrainer .

Chapitre 13 - Générer et charger une version test de votre application

Pour commencer le processus de génération :

  1. Accédez aux paramètres de génération de fichier>.

  2. Cochez les projets C# Unity.

  3. Cliquez sur Générer. Unity lance une fenêtre Explorateur de fichiers, où vous devez créer, puis sélectionner un dossier dans lequel générer l’application. Créez maintenant ce dossier et nommez-le App. Ensuite, avec le dossier d’application sélectionné, cliquez sur Sélectionner un dossier.

  4. Unity commence à générer votre projet dans le dossier Application .

  5. Une fois que Unity a terminé la génération (cela peut prendre un certain temps), il ouvre une fenêtre de Explorateur de fichiers à l’emplacement de votre build (vérifiez votre barre des tâches, car elle peut ne pas toujours apparaître au-dessus de vos fenêtres, mais vous informera de l’ajout d’une nouvelle fenêtre).

Pour déployer sur HoloLens :

  1. Vous aurez besoin de l’adresse IP de votre HoloLens (pour le déploiement à distance) et de vous assurer que votre HoloLens est en mode développeur. Pour ce faire :

    1. Tout en portant votre HoloLens, ouvrez les paramètres.

    2. Accéder aux options avancées réseau et Wi-FiInternet>>

    3. Notez l’adresse IPv4 .

    4. Ensuite, revenez aux paramètres, puis à Update &Security>for Developers

    5. Définissez le mode développeur activé.

  2. Accédez à votre nouvelle build Unity (dossier d’application ) et ouvrez le fichier solution avec Visual Studio.

  3. Dans la configuration de la solution, sélectionnez Déboguer.

  4. Dans la plateforme de solutions, sélectionnez x86, Ordinateur distant. Vous serez invité à insérer l’adresse IP d’un appareil distant (HoloLens, dans ce cas, que vous avez noté).

    Configurer l’adresse IP

  5. Accédez au menu Générer , puis cliquez sur Déployer la solution pour charger l’application sur votre HoloLens.

  6. Votre application doit maintenant apparaître dans la liste des applications installées sur votre HoloLens, prête à être lancée !

Remarque

Pour déployer sur un casque immersif, définissez la plateforme de solution sur ordinateur local et définissez la configuration sur Débogage, avec x86 comme plateforme. Ensuite, déployez sur l’ordinateur local, à l’aide de l’élément de menu Générer , en sélectionnant Déployer la solution.

Pour utiliser l’application :

Pour basculer la fonctionnalité d’application entre le mode d’entraînement et le mode prédiction , vous devez mettre à jour la variable AppMode , située dans la méthode Awake() située dans la classe ImageCapture .

        // Change this flag to switch between Analysis mode and Training mode 
        AppMode = AppModes.Training;

or

        // Change this flag to switch between Analysis mode and Training mode 
        AppMode = AppModes.Analysis;

En mode Entraînement :

  • Examinez la souris ou le clavier et utilisez le mouvement Tap.

  • Ensuite, le texte s’affiche pour vous demander de fournir une balise.

  • Dites souris ou clavier.

En mode prédiction :

  • Examinez un objet et utilisez le mouvement Tap.

  • Le texte s’affiche en fournissant l’objet détecté, avec la probabilité la plus élevée (ce qui est normalisé).

Chapitre 14 - Évaluer et améliorer votre modèle Custom Vision

Pour rendre votre service plus précis, vous devez continuer à entraîner le modèle utilisé pour la prédiction. Cela s’effectue à l’aide de votre nouvelle application, avec les modes d’apprentissage et de prédiction , ce qui vous oblige à visiter le portail, qui est ce qui est abordé dans ce chapitre. Préparez-vous à revisiter votre portail plusieurs fois, afin d’améliorer continuellement votre modèle.

  1. Accédez à nouveau à votre portail Azure Custom Vision, puis une fois que vous êtes dans votre projet, sélectionnez l’onglet Prédictions (dans le centre supérieur de la page) :

    Onglet Sélectionner les prédictions

  2. Vous verrez toutes les images qui ont été envoyées à votre service pendant l’exécution de votre application. Si vous pointez sur les images, elles vous fournissent les prédictions effectuées pour cette image :

    Liste des images de prédiction

  3. Sélectionnez l’une de vos images pour l’ouvrir. Une fois ouvert, vous verrez les prédictions effectuées pour cette image à droite. Si les prédictions étaient correctes et que vous souhaitez ajouter cette image au modèle d’entraînement de votre service, cliquez sur la zone d’entrée Mes balises , puis sélectionnez la balise que vous souhaitez associer. Lorsque vous avez terminé, cliquez sur le bouton Enregistrer et fermer en bas à droite, puis passez à l’image suivante.

    Sélectionner une image à ouvrir

  4. Une fois que vous revenez à la grille des images, vous remarquerez que les images auxquelles vous avez ajouté des balises (et enregistrées) seront supprimées. Si vous trouvez des images que vous pensez ne pas avoir votre élément étiqueté dans ces éléments, vous pouvez les supprimer, en cliquant sur la coche de cette image (peut le faire pour plusieurs images), puis en cliquant sur Supprimer dans le coin supérieur droit de la page de grille. Dans la fenêtre contextuelle qui suit, vous pouvez cliquer sur Oui, Supprimer ou Non, pour confirmer la suppression ou l’annuler, respectivement.

    Supprimer des images

  5. Lorsque vous êtes prêt à continuer, cliquez sur le bouton Train vert en haut à droite. Votre modèle de service sera entraîné avec toutes les images que vous avez maintenant fournies (ce qui le rendra plus précis). Une fois l’entraînement terminé, veillez à cliquer une fois de plus sur le bouton Créer la valeur par défaut afin que votre URL de prédiction continue d’utiliser l’itération la plus récente de votre service.

    Démarrer le modèle de service d’entraînementSélectionner l’option Par défaut

Votre application API Custom Vision terminée

Félicitations, vous avez créé une application de réalité mixte qui tire parti de l’API Azure Custom Vision pour reconnaître les objets réels, entraîner le modèle de service et afficher la confiance de ce qui a été vu.

Exemple de projet terminé

Exercices bonus

Exercice 1

Entraîner votre service Custom Vision pour reconnaître d’autres objets.

Exercice 2

Pour développer ce que vous avez appris, effectuez les exercices suivants :

Lire un son lorsqu’un objet est reconnu.

Exercice 3

Utilisez l’API pour réentreîner votre service avec les mêmes images que celles que votre application analyse, afin de rendre le service plus précis (effectuez simultanément la prédiction et l’entraînement).