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


Notes

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 et qui montreront comment développer pour HoloLens 2. Cet avis sera mis à jour avec un lien vers ces tutoriels lorsqu’ils seront publiés.


Dans ce cours, vous allez apprendre à reconnaître du contenu visuel personnalisé dans une image fournie, à l’aide des fonctionnalités d’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 utiliserez ensuite le modèle entraîné pour reconnaître des objets similaires, comme fourni par la capture d’appareil photo 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 Azure Custom Vision Service.

À l’issue de ce cours, vous disposerez d’une application de réalité mixte qui pourra fonctionner dans deux modes :

  • Mode d’analyse : configuration manuelle du service Custom Vision en chargeant des images, en créant des balises et en entraînant le service à 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 essayera de reconnaître ces objets dans le monde réel.

  • Mode d’entraînement : vous allez implémenter du code qui activera un « mode d’entraînement » dans votre application. Le mode d’entraînement vous permet de capturer des images à l’aide de la caméra HoloLens, de charger les images capturées sur 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 Unity. Il vous appartient d’appliquer ces concepts à une application personnalisée que vous pouvez créer.

Prise en charge des appareils

Cours HoloLens Casques immersifs
Réalité mixte - Azure - Cours 302b : Vision personnalisée ✔️ ✔️

Notes

Bien que ce cours se concentre principalement sur HoloLens, vous pouvez également appliquer ce que vous apprenez dans ce cours à Windows Mixed Reality casques immersifs (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

Notes

Ce tutoriel est conçu pour les développeurs qui ont une expérience de base avec Unity et C#. Notez également que les prérequis et les instructions écrites dans ce document représentent ce qui a été testé et vérifié au moment de la rédaction (juillet 2018). Vous êtes libre d’utiliser les logiciels les plus récents, comme indiqué dans l’article installer les outils , bien qu’il ne soit pas supposé que les informations de ce cours correspondront parfaitement à ce que vous trouverez dans les logiciels plus récents que ce qui est listé ci-dessous.

Nous vous recommandons d’utiliser 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 proche de la racine (les chemins de dossier longs peuvent entraîner 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, consultez l’article sur la configuration de HoloLens.
  3. Il est judicieux d’effectuer l’étalonnage et le réglage des capteurs lorsque vous commencez à développer une nouvelle application HoloLens (parfois, cela peut aider à effectuer ces tâches pour chaque utilisateur).

Pour obtenir de l’aide sur l’étalonnage, suivez ce lien vers l’article É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 - Le portail de service Custom Vision

Pour utiliser le service Custom Vision dans Azure, vous devez configurer une instance du service à mettre à la disposition de votre application.

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

  2. Cliquez sur le bouton Prise en main .

    Bien démarrer avec Custom Vision Service

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

    Se connecter au portail

    Notes

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

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

    Conditions d’utilisation

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

    Créer un projet

  6. Un onglet s’affiche sur le côté droit, qui vous invite à spécifier des 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éfinissez les types de projets sur Classification

    5. Définissez les domaines sur Général.

      Définir les domaines

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

  7. Une fois que vous avez terminé, cliquez sur Créer un projet. Vous serez redirigé vers la page Custom Vision Service, 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 à reconnaître des objets spécifiques dans des images. Vous avez besoin d’au moins cinq (5) images, 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 entraîner votre projet Custom Vision Service :

  1. Cliquez sur le + bouton en regard de Balises.

    Ajouter une balise

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

    Ajouter le nom de l’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 à côté de votre nouvelle balise, si elle n’est pas déjà cochée.

    Activer la nouvelle balise

  4. Cliquez sur Ajouter des images au centre de la page.

    Ajouter des images

  5. Cliquez sur Parcourir les fichiers locaux, puis 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.

    Notes

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

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

    Sélectionner des étiquettes

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

    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écocherla souris une fois que vous avez créé les nouvelles étiquettes, afin d’afficher la fenêtre Ajouter des images .

  9. Une fois que vous avez configuré les deux balises, cliquez sur Entraîner pour que la première itération d’entraînement commence à être créée.

    Activer l’itération d’entraînement

  10. Une fois qu’il est généré, vous pouvez voir deux boutons appelés Effectuer la valeur par défaut et URL de prédiction. Cliquez d’abord sur Définir la valeur par défaut , puis cliquez sur URL de prédiction.

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

    Notes

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

  11. Une fois que vous avez cliqué sur URL de prédiction, ouvrez le Bloc-notes, puis copiez-collez l’URL et la clé de prédiction, afin de pouvoir la récupérer quand vous en avez besoin plus loin 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 d’engrenage 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 également dans votre fichier Bloc-notes , pour une utilisation ultérieure.

    Copier l’ID de projet

Chapitre 3 - Configurer le projet Unity

Ce qui suit est 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é (n’oubliez pas qu’il est préférable de se rapprocher des répertoires racines). Cliquez ensuite sur Créer un projet.

    Configurer les paramètres du projet

  3. Avec Unity ouvert, il est utile 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, dans la nouvelle fenêtre, accédez à Outils externes. Remplacez É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 > génération de fichiers, sélectionnez plateforme Windows universelle, puis cliquez sur le bouton Changer de plateforme pour appliquer votre sélection.

    Configurer les paramètres de build

  5. Toujours dans 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 Appareil cible sur N’importe quel appareil.

    2. Type de build défini sur D3D

    3. Le Kit de développement logiciel (SDK) est défini sur Dernier installé

    4. La version de Visual Studio est définie sur Dernière installation

    5. Générer et exécuter est défini 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 génération

      2. Créez un dossier pour cette scène et toute autre 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 Scenes nouvellement créé, puis dans le champ Nom de fichier : texte, tapez CustomVisionScene, puis cliquez sur Enregistrer.

        Nommer un nouveau fichier de scène

        N’oubliez pas 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 classique de structurer un projet Unity.

    7. Les autres paramètres, dans Paramètres de build, doivent être conservés par défaut pour l’instant.

      Paramètres de build par défaut

  6. Dans la fenêtre Paramètres de build, cliquez sur le bouton Paramètres du lecteur. Le panneau associé s’ouvre 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 la nécessité de redémarrer l’éditeur.

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

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

      Définir la compatibilité de l’API

    2. Sous l’onglet Paramètres de publication, sous Fonctionnalités, case activée :

      1. InternetClient

      2. Webcam

      3. Microphone

      Configurer les paramètres de publication

    3. Plus loin dans le panneau, dans Paramètres XR (ci-dessous Paramètres de publication), cochez Réalité virtuelle prise en charge, vérifiez que le Windows Mixed Reality SDK est ajouté.

    Configurer les paramètres XR

  8. Retour dans Paramètres de buildUnity C# Projects n’est plus grisé ; cochez la case à côté de cette case.

  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 Unity Set up de ce cours et continuer directement dans le code, n’hésitez pas à télécharger ce package Azure-MR-302b.unitypackage, à 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 .unitypackage à Unity à l’aide de l’option de menu Importer> le packagepersonnaliséderessources>.

  2. Dans la zone Importer un package Unity qui s’affiche, vérifiez que tout ce qui se trouve sous (et y compris) Plug-ins est sélectionné.

    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 la vue projet, puis 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écochée, vérifiez que WSAPlayer est également décoché, puis cliquez sur Appliquer. Il s’agit simplement de vérifier que les fichiers sont configurés correctement.

    Configurer le plug-in Newtonsoft

    Notes

    Le marquage de ces plug-ins les configure pour qu’ils soient utilisés uniquement dans l’éditeur Unity. Il existe un ensemble différent d’entre eux dans le dossier WSA qui sera utilisé après l’exportation du projet à partir d’Unity.

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

    • Toute plateformeest décochée
    • seulWSAPlayer est coché
    • Le processus non est coché

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

Chapitre 5 : Configuration de l’appareil photo

  1. Dans le panneau Hiérarchie, sélectionnez la caméra principale.

  2. Une fois cette option sélectionnée, vous pourrez voir tous les composants de la caméra principale dans le panneau de l’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 Couleur unie (ignorez cela pour le casque immersif).

    5. Définissez la couleur d’arrière-plan du composant de l’appareil photo sur Noir, Alpha 0 (code hexadécimal : #00000000) (ignorez cela 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 allez commencer par la classe CustomVisionAnalyser .

Notes

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

Cette classe est responsable des opérations suivantes :

  • 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’occupe 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 panneau Projet, puis cliquez sur Créer un > dossier. Appelez le dossier Scripts.

    Créer un dossier 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;
    

    Notes

    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és dans le Bloc-notes plus tôt 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 statique GetImageAsByteArray() en dessous), qui obtiendra les résultats de l’analyse de l’image capturée par la classe ImageCapture .

    Notes

    Dans la coroutine AnalyseImageCapture , il existe un appel à la classe SceneOrganiser que vous n’avez pas encore créé. 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 maintenant créer est 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 prendre note du 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 dans 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(), qui configurent les mots clés utilisateur pour qu’ils soient reconnus 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.

Notes

Ne vous inquiétez pas du code qui peut sembler avoir une erreur, car vous fournirez bientôt d’autres classes, qui corrigeront ces problèmes.

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 dans la classe CustomVisionTrainer, au-dessus de la méthode Start().

    Notes

    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 de prendre note du 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

    Veillez à ajouter votre valeur de clé de service (clé d’entraînement) et votre valeur d’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 ultérieurement).

  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 qu’elle est maintenant prête à être envoyée au service Custom Vision pour l’entraîner. Cette méthode affiche, dans l’interface utilisateur de formation, un ensemble de mots clés que l’utilisateur peut utiliser pour étiqueter l’image qui a été 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 de balise du service associé à l’entrée vocale validée de l’utilisateur. L’ID de balise est 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 marqué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 que vous venez de charger. 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 la dernière itération entraînée.

        /// <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 l’opération terminée, cette méthode devra supprimer l’itération précédente existante dans le service. Au moment de la rédaction de ce cours, il existe une limite de dix (10) itérations autorisées à exister simultanément 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 autre que celle 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 apparaîtra lorsque le service reconnaîtra les objets réels.

  • Configurez la caméra principale en y attachant les composants appropriés.

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

  • En mode 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 au-dessus de la classe SceneOrganiser :

    using UnityEngine;
    
  4. Ajoutez ensuite les variables suivantes dans 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 sous les 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 Main Camera, 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 la status 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èreront et afficheront 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 différentes é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 status 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 opérations suivantes :

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

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

  • Conservation de la valeur Enum qui détermine si l’application s’exécutera en mode Analyse ou En mode 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 dans 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. Le code pour les méthodes Awake() et Start() doit maintenant être ajouté :

        /// <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 d’appui 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;
            }     
        }
    

    Notes

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

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

    Lorsque le curseur est vert, cela signifie que la caméra est disponible pour prendre l’image.

    Lorsque le curseur est rouge, cela signifie que la caméra est occupée.

  8. Ajoutez la méthode que l’application utilise 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 une fois la photo capturée et quand elle sera prête à être analysée. Le résultat est ensuite transmis au CustomVisionAnalyser ou au 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 sont terminés, revenez dans l’éditeur Unity, puis cliquez et faites glisser la classe SceneOrganiser du dossier Scripts vers l’objet Main Camera dans le panneau de hiérarchie.

Chapitre 12 - Avant de construire

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

Avant de le faire, 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 d’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 à Paramètres de génération de fichiers>.

  2. Cochez Les projets Unity C#.

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

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

  5. Une fois la génération terminée (cela peut prendre un certain temps), Unity ouvre une fenêtre Explorateur de fichiers à l’emplacement de votre build (case activée votre barre des tâches, car elle n’apparaît peut-être pas toujours au-dessus de vos fenêtres, mais vous informe 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. Lorsque vous portez votre HoloLens, ouvrez les Paramètres.

    2. Accédez à Network & Internet>Wi-Fi>Advanced Options

    3. Notez l’adresse IPv4 .

    4. Ensuite, revenez à Paramètres, puis à Mettre à jour & sécurité>pour les développeurs

    5. Définissez Mode développeur activé.

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

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

  4. Dans Plateforme de solution, 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 et cliquez sur Déployer la solution pour charger une version test de 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 !

Notes

Pour effectuer un déploiement sur un casque immersif, définissez La plateforme de solution sur Ordinateur local et définissez la configuration sur Déboguer, avec x86 comme plateforme. Déployez ensuite 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é de l’application entre le mode 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;

ou

        // 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 d’appui.

  • Ensuite, un texte vous invitant à fournir une balise s’affiche.

  • Dites souris ou clavier.

En mode prédiction :

  • Examinez un objet et utilisez le mouvement Appuyez.

  • Le texte s’affiche à condition que l’objet détecté, avec la probabilité la plus élevée (ceci 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. Pour ce faire, utilisez votre nouvelle application, avec les modes d’entraînement et de prédiction , ce qui vous oblige à visiter le portail, ce qui est abordé dans ce chapitre. Préparez-vous à revenir plusieurs fois sur votre portail afin d’améliorer continuellement votre modèle.

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

    Onglet Sélectionner des 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 qui ont été faites pour cette image :

    Liste des images de prédiction

  3. Sélectionnez l’une de vos images pour l’ouvrir. Une fois ouverte, 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 étiquettes , 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 l’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 qui, selon vous, ne contiennent pas votre élément balisé, 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 vert Entraîner 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 Définir 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 des 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înez votre service Custom Vision pour qu’il reconnaisse d’autres objets.

Exercice 2

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

Diffuser un son lorsqu’un objet est reconnu.

Exercice 3

Utilisez l’API pour réentrêter votre service avec les mêmes images que celles que votre application analyse, afin de rendre le service plus précis (effectuer simultanément des prédictions et des entraînements).