HoloLens (1.ª geração) e Azure 302b: Visão personalizada


Nota

Os tutoriais Mixed Reality Academy foram concebidos com o HoloLens (1.ª geração) e Mixed Reality Headsets Envolventes em mente. Como tal, consideramos importante deixar estes tutoriais em vigor para os programadores que ainda estão à procura de orientação no desenvolvimento desses dispositivos. Estes tutoriais não serão atualizados com os conjuntos de ferramentas ou interações mais recentes que estão a ser utilizados para HoloLens 2. Serão mantidas para continuarem a trabalhar nos dispositivos suportados. Haverá uma nova série de tutoriais que serão publicados no futuro que demonstrarão como desenvolver para HoloLens 2. Este aviso será atualizado com uma ligação para esses tutoriais quando forem publicados.


Neste curso, irá aprender a reconhecer conteúdos de elementos visuais personalizados numa imagem fornecida, com as capacidades do Azure Visão Personalizada numa aplicação de realidade mista.

Este serviço permite-lhe preparar um modelo de machine learning com imagens de objetos. Em seguida, irá utilizar o modelo preparado para reconhecer objetos semelhantes, conforme fornecido pela captura da câmara de Microsoft HoloLens ou uma câmara ligada ao PC para auscultadores envolventes (VR).

resultado do curso

O Azure Visão Personalizada é um Serviço Cognitivo da Microsoft que permite aos programadores criar classificadores de imagens personalizados. Estes classificadores podem então ser utilizados com novas imagens para reconhecer ou classificar objetos nessa nova imagem. O Serviço fornece um portal online simples e fácil de utilizar para simplificar o processo. Para obter mais informações, visite a página Azure Visão Personalizada Service.

Após a conclusão deste curso, terá uma aplicação de realidade mista que poderá funcionar em dois modos:

  • Modo de Análise: configurar manualmente o Serviço de Visão Personalizada ao carregar imagens, criar etiquetas e preparar o Serviço para reconhecer diferentes objetos (neste caso, rato e teclado). Em seguida, irá criar uma aplicação HoloLens que irá capturar imagens com a câmara e tentar reconhecer esses objetos no mundo real.

  • Modo de Preparação: irá implementar código que ativará um "Modo de Preparação" na sua aplicação. O modo de preparação irá permitir-lhe capturar imagens com a câmara do HoloLens, carregar as imagens capturadas para o Serviço e preparar o modelo de visão personalizada.

Este curso irá ensiná-lo a obter os resultados do Serviço de Visão Personalizada para uma aplicação de exemplo baseada no Unity. Cabe-lhe a si aplicar estes conceitos a uma aplicação personalizada que possa estar a criar.

Suporte de dispositivos

Curso HoloLens Headsets envolventes
MR e Azure 302b: Visão personalizada ✔️ ✔️

Nota

Embora este curso se centre principalmente no HoloLens, também pode aplicar o que aprende neste curso para Windows Mixed Reality headsets envolventes (VR). Uma vez que os auscultadores envolventes (VR) não têm câmaras acessíveis, precisará de uma câmara externa ligada ao PC. À medida que acompanha o curso, verá notas sobre quaisquer alterações que possa ter de utilizar para suportar headsets envolventes (VR).

Pré-requisitos

Nota

Este tutorial foi concebido para programadores com experiência básica com o Unity e C#. Tenha também em atenção que os pré-requisitos e as instruções escritas neste documento representam o que foi testado e verificado no momento da escrita (julho de 2018). Pode utilizar o software mais recente, conforme listado no artigo Instalar as ferramentas , embora não se deva presumir que as informações neste curso corresponderão perfeitamente ao que irá encontrar no software mais recente do que o que está listado abaixo.

Recomendamos o seguinte hardware e software para este curso:

Antes de começar

  1. Para evitar problemas ao criar este projeto, é altamente sugerido que crie o projeto mencionado neste tutorial numa pasta raiz ou de raiz próxima (os caminhos de pasta longos podem causar problemas no tempo de compilação).
  2. Configure e teste o HoloLens. Se precisar de suporte para configurar o HoloLens, veja o artigo de configuração do HoloLens.
  3. Recomendamos que execute a Calibragem e o Ajuste do Sensor quando começar a desenvolver uma nova aplicação HoloLens (por vezes, pode ajudar a executar essas tarefas para cada utilizador).

Para obter ajuda sobre a Calibragem, siga esta ligação para o artigo Calibragem do HoloLens.

Para obter ajuda sobre a Otimização do Sensor, siga esta ligação para o artigo Otimização do Sensor do HoloLens.

Capítulo 1 - O Portal do Serviço Visão Personalizada

Para utilizar o Serviço Visão Personalizada no Azure, terá de configurar uma instância do Serviço para ser disponibilizada à sua aplicação.

  1. Primeiro, navegue para a página principal do Serviço Visão Personalizada.

  2. Clique no botão Começar .

    Introdução ao Serviço Visão Personalizada

  3. Inicie sessão no Portal do Serviço Visão Personalizada.

    Iniciar sessão no portal

    Nota

    Se ainda não tiver uma conta do Azure, terá de criar uma. Se estiver a seguir este tutorial numa situação de sala de aula ou laboratório, peça ajuda ao seu instrutor ou a um dos tutores para configurar a sua nova conta.

  4. Depois de iniciar sessão pela primeira vez, ser-lhe-á pedido no painel Termos de Serviço . Clique na caixa de verificação para concordar com os termos. Em seguida, clique em Concordo.

    Termos de serviço

  5. Depois de ter concordado com os Termos, será navegado para a secção Projetos do Portal. Clique em Novo Projeto.

    Criar novo projeto

  6. Será apresentado um separador no lado direito, que lhe pedirá para especificar alguns campos para o projeto.

    1. Insira um Nome para o seu projeto.

    2. Insira uma Descrição para o seu projeto (opcional).

    3. Escolha um Grupo de Recursos ou crie um novo. Um grupo de recursos fornece uma forma de monitorizar, controlar o acesso, aprovisionar e gerir a faturação de uma coleção de recursos do Azure. Recomenda-se manter todos os Serviços do Azure associados a um único projeto (por exemplo, estes cursos) num grupo de recursos comum.

    4. Definir os Tipos de Projeto como Classificação

    5. Defina os Domínios como Gerais.

      Definir os domínios

      Se quiser ler mais sobre os Grupos de Recursos do Azure, veja o artigo do grupo de recursos.

  7. Quando tiver terminado, clique em Criar projeto, será redirecionado para a página Visão Personalizada Serviço, projeto.

Capítulo 2 - Preparar o seu projeto de Visão Personalizada

Uma vez no portal Visão Personalizada, o seu principal objetivo é preparar o projeto para reconhecer objetos específicos em imagens. Precisa de, pelo menos, cinco (5) imagens, embora dez (10) seja preferível, para cada objeto que pretende que a sua aplicação reconheça. Pode utilizar as imagens fornecidas com este curso (um rato de computador e um teclado).

Para preparar o projeto Visão Personalizada Service:

  1. Clique no + botão junto a Etiquetas.

    Adicionar etiqueta nova

  2. Adicione o nome do objeto que pretende reconhecer. Clique em Guardar.

    Adicionar o nome do objeto e guardar

  3. Irá reparar que a etiqueta foi adicionada (poderá ter de recarregar a página para que apareça). Clique na caixa de verificação juntamente com a nova etiqueta, se ainda não estiver selecionada.

    Ativar nova etiqueta

  4. Clique em Adicionar Imagens no centro da página.

    Adicionar imagens

  5. Clique em Procurar ficheiros locais e procure e, em seguida, selecione as imagens que pretende carregar, sendo que no mínimo cinco (5). Lembre-se de que todas estas imagens devem conter o objeto que está a preparar.

    Nota

    Pode selecionar várias imagens de cada vez para carregar.

  6. Assim que conseguir ver as imagens no separador, selecione a etiqueta adequada na caixa As Minhas Etiquetas .

    Selecionar etiquetas

  7. Clique em Carregar ficheiros. Os ficheiros começarão a ser carregados. Assim que tiver a confirmação do carregamento, clique em Concluído.

    Carregar ficheiros

  8. Repita o mesmo processo para criar uma nova Etiqueta com o nome Teclado e carregar as fotografias adequadas para o mesmo. Certifique-se de que desmarca o Rato depois de criar as novas etiquetas, para mostrar a janela Adicionar imagens.

  9. Depois de configurar ambas as Etiquetas, clique em Preparar e a primeira iteração de preparação começará a ser criada.

    Ativar a iteração de preparação

  10. Assim que estiver criado, poderá ver dois botões chamados Tornar predefinido e URL de Predição. Clique em Predefinir primeiro e, em seguida, clique em URL de Predição.

    Tornar o URL predefinido e de predição

    Nota

    O URL do ponto final que é fornecido a partir deste, está definido para a Iteração que tiver sido marcada como predefinição. Como tal, se posteriormente criar uma nova Iteração e atualizá-la como predefinição , não terá de alterar o código.

  11. Depois de clicar no URL de Predição, abra o Bloco de Notas e copie e cole o URL e a Chave de Predição, para que possa recuperá-lo quando precisar mais tarde no código.

    Copiar e colar URL e Chave de Predição

  12. Clique na Engrenagem no canto superior direito do ecrã.

    Clique no ícone de engrenagem para abrir as definições

  13. Copie a Chave de Preparação e cole-a num Bloco de Notas, para utilização posterior.

    Copiar chave de preparação

  14. Copie também o ID do Projeto e cole-o no ficheiro do Bloco de Notas para utilização posterior.

    Copiar id do projeto

Capítulo 3 - Configurar o projeto unity

Segue-se uma configuração típica para desenvolver com realidade mista e, como tal, é um bom modelo para outros projetos.

  1. Abra o Unity e clique em Novo.

    Criar novo projeto do Unity

  2. Terá agora de fornecer um nome de projeto do Unity. Inserir AzureCustomVision. Certifique-se de que o modelo de projeto está definido como 3D. Defina a Localização para um local adequado para si (lembre-se de que é melhor estar mais perto dos diretórios de raiz). Em seguida, clique em Criar projeto.

    Configurar as definições do projeto

  3. Com o Unity aberto, vale a pena verificar se o Editor de Scripts predefinido está definido como Visual Studio. Aceda a Editar>Preferências e, em seguida, a partir da nova janela, navegue para Ferramentas Externas. Altere o Editor de Scripts Externos para o Visual Studio 2017. Feche a janela Preferências .

    Configurar ferramentas externas

  4. Em seguida, aceda a Definições de Compilação de Ficheiros > e selecione Plataforma Universal do Windows e, em seguida, clique no botão Mudar de Plataforma para aplicar a sua seleção.

    Configurar definições de compilação

  5. Ainda em Definições de Compilação de Ficheiros > e certifique-se de que:

    1. O Dispositivo de Destino está definido como HoloLens

      Para os headsets envolventes, defina Dispositivo de Destino como Qualquer Dispositivo.

    2. O Tipo de Compilação está definido como D3D

    3. O SDK está definido como Instalado mais recentemente

    4. A Versão do Visual Studio está definida como Mais Recente instalada

    5. A compilação e a execução estão definidas como Máquina Local

    6. Guarde a cena e adicione-a à compilação.

      1. Faça-o ao selecionar Adicionar Cenas Abertas. Será apresentada uma janela guardar.

        Adicionar cena aberta à lista de compilação

      2. Crie uma nova pasta para este cenário e, em seguida, selecione o botão Nova pasta , para criar uma nova pasta, atribua-lhe o nome Cenas.

        Criar nova pasta de cenas

      3. Abra a pasta Cenas recentemente criada e, em seguida, no campo Nome do ficheiro : texto, escreva CustomVisionScene e, em seguida, clique em Guardar.

        Atribuir um nome ao novo ficheiro de cena

        Tenha em atenção que tem de guardar as cenas do Unity na pasta Recursos, uma vez que têm de estar associadas ao projeto unity. Criar a pasta cenas (e outras pastas semelhantes) é uma forma típica de estruturar um projeto do Unity.

    7. As restantes definições, em Definições de Compilação, devem ser deixadas como predefinição por enquanto.

      Predefinições de compilação

  6. Na janela Definições de Compilação , clique no botão Definições do Leitor . Esta ação abrirá o painel relacionado no espaço onde o Inspetor está localizado.

  7. Neste painel, é necessário verificar algumas definições:

    1. No separador Outras Definições :

      1. A Versão do Runtime de Scripts deve ser Experimental (.NET 4.6 Equivalente), o que irá acionar a necessidade de reiniciar o Editor.

      2. O Back-end de Scripts deve ser .NET

      3. O Nível de Compatibilidade da API deve ser .NET 4.6

      Definir a compantiblity da API

    2. No separador Definições de Publicação , em Capacidades, verifique:

      1. InternetClient

      2. Câmara Web

      3. Microfone

      Configurar definições de publicação

    3. Mais abaixo no painel, em Definições XR (encontradas abaixo de Definições de Publicação), marque a Realidade Virtual Suportada, certifique-se de que o SDK Windows Mixed Reality é adicionado.

    Configurar definições XR

  8. Novamente em Definições de Compilação, o Unity C# Projects já não está a cinzento; marque a caixa de verificação junto a esta.

  9. Feche a janela Definições de Compilação.

  10. Guarde a cena e o projeto (CENA DE GUARDAR FICHEIRO>/PROJETO GUARDAR FICHEIROS>).

Capítulo 4 - Importar a DLL newtonsoft em Unity

Importante

Se quiser ignorar o componente Configurar o Unity deste curso e continuar diretamente para o código, pode transferir este Azure-MR-302b.unitypackage, importá-lo para o seu projeto como um Pacote Personalizado e, em seguida, continuar a partir do Capítulo 6.

Este curso requer a utilização da biblioteca Newtonsoft , que pode adicionar como DLL aos seus recursos. O pacote que contém esta biblioteca pode ser transferido a partir desta Ligação. Para importar a biblioteca Newtonsoft para o seu projeto, utilize o Pacote unity que veio com este curso.

  1. Adicione o .unitypackage ao Unity com a opção de menu PacotePersonalizado Importar Pacote> deImportação de Recursos>.

  2. Na caixa Importar Pacote do Unity que é apresentada, certifique-se de que está selecionado tudo em (e incluindo) Plug-ins .

    Importar todos os itens de pacote

  3. Clique no botão Importar para adicionar os itens ao projeto.

  4. Aceda à pasta Newtonsoft em Plug-ins na vista do projeto e selecione o plug-in Newtonsoft.Json.

    Selecione Plug-in Newtonsoft

  5. Com o plug-in Newtonsoft.Json selecionado, certifique-se de que Qualquer Plataforma está desmarcada e, em seguida, certifique-se de que o WSAPlayer também está desmarcado e, em seguida, clique em Aplicar. Isto é apenas para confirmar que os ficheiros estão configurados corretamente.

    Configurar o plug-in Newtonsoft

    Nota

    Marcar estes plug-ins configura-os para serem utilizados apenas no Editor do Unity. Existe um conjunto diferente deles na pasta WSA que será utilizado depois de o projeto ser exportado do Unity.

  6. Em seguida, tem de abrir a pasta WSA , na pasta Newtonsoft . Verá uma cópia do mesmo ficheiro que acabou de configurar. Selecione o ficheiro e, em seguida, no inspetor, certifique-se de que

    • Qualquer Plataforma está desmarcada
    • apenasWSAPlayer está selecionado
    • O processo não está selecionado

    Configurar as definições da plataforma de plug-in Newtonsoft

Capítulo 5 - Configuração da câmara

  1. No Painel hierarquia, selecione a Câmara Principal.

  2. Depois de selecionado, poderá ver todos os componentes da Câmara Principal no Painel de Inspetores.

    1. O objeto da câmara tem de ter o nome Câmara Principal (anote a ortografia!)

    2. A Etiqueta da Câmara Principal tem de ser definida como MainCamera (anote a ortografia!)

    3. Certifique-se de que a Posição da Transformação está definida como 0, 0, 0

    4. Defina Limpar Sinalizadores como Cor Sólida (ignore-o para auscultadores envolventes).

    5. Defina a Cor de Fundo do Componente da câmara como Preto, Alfa 0 (Código Hexadecim: #00000000) (ignore-o para auscultadores envolventes).

    Configurar as propriedades do componente Câmara

Capítulo 6 - Criar a classe CustomVisionAnalyser.

Neste momento, está pronto para escrever algum código.

Começará com a classe CustomVisionAnalyser .

Nota

As chamadas para o Serviço de Visão Personalizada efetuadas no código apresentado abaixo são efetuadas com a API REST Visão Personalizada. Através desta utilização, verá como implementar e utilizar esta API (útil para compreender como implementar algo semelhante por conta própria). Tenha em atenção que a Microsoft oferece um SDK do Serviço Visão Personalizada que também pode ser utilizado para efetuar chamadas para o Serviço. Para obter mais informações, veja o artigo do SDK do Serviço Visão Personalizada.

Esta classe é responsável por:

  • Carregar a imagem mais recente capturada como uma matriz de bytes.

  • Enviar a matriz de bytes para a instância do Azure Visão Personalizada Service para análise.

  • A receber a resposta como uma cadeia JSON.

  • Anular a serialização da resposta e transmitir a Predição resultante para a classe SceneOrganiser , que tratará da forma como a resposta deve ser apresentada.

Para criar esta classe:

  1. Clique com o botão direito do rato na Pasta de Recursos localizada no Painel de Projeto e, em seguida, clique em Criar > Pasta. Chame a pasta Scripts.

    Criar pasta de scripts

  2. Faça duplo clique na pasta que acabou de criar para abri-la.

  3. Clique com o botão direito do rato dentro da pasta e, em seguida, clique em Criar>Script C#. Atribua o nome CustomVisionAnalyser ao script.

  4. Faça duplo clique no novo script CustomVisionAnalyser para o abrir com o Visual Studio.

  5. Atualize os espaços de nomes na parte superior do ficheiro para que correspondam ao seguinte:

    using System.Collections;
    using System.IO;
    using UnityEngine;
    using UnityEngine.Networking;
    using Newtonsoft.Json;
    
  6. Na classe CustomVisionAnalyser , adicione as seguintes variáveis:

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

    Nota

    Certifique-se de que insere a Chave de Predição na variável predictionKey e no Ponto Final de Predição na variável predictionEndpoint . Copiou-os para o Bloco de Notas anteriormente no curso.

  7. O código para Awake() tem agora de ser adicionado para inicializar a variável Instance:

        /// <summary>
        /// Initialises this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. Elimine os métodos Start() e Update().

  9. Em seguida, adicione o coroutine (com o método estático GetImageAsByteArray() abaixo do mesmo), que obterá os resultados da análise da imagem capturada pela classe ImageCapture .

    Nota

    No coroutine AnalysisImageCapture , existe uma chamada para a classe SceneOrganiser que ainda não criou. Portanto, deixe estas linhas comentadas por enquanto.

        /// <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. Certifique-se de que guarda as alterações no Visual Studio antes de regressar ao Unity.

Capítulo 7 - Criar a classe CustomVisionObjects

A classe que irá criar agora é a classe CustomVisionObjects .

Este script contém vários objetos utilizados por outras classes para serializar e anular a serialização das chamadas efetuadas ao Serviço Visão Personalizada.

Aviso

É importante que tome nota do ponto final que o Serviço Visão Personalizada lhe fornece, uma vez que a estrutura JSON abaixo foi configurada para funcionar com o Visão Personalizada Prediction v2.0. Se tiver uma versão diferente, poderá ter de atualizar a estrutura abaixo.

Para criar esta classe:

  1. Clique com o botão direito do rato dentro da pasta Scripts e, em seguida, clique em Criar>Script C#. Chame o script CustomVisionObjects.

  2. Faça duplo clique no novo script CustomVisionObjects para o abrir com o Visual Studio.

  3. Adicione os seguintes espaços de nomes à parte superior do ficheiro:

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. Elimine os métodos Start() e Update() na classe CustomVisionObjects ; esta classe deve estar agora vazia.

  5. Adicione as seguintes classes fora da classe CustomVisionObjects . Estes objetos são utilizados pela biblioteca Newtonsoft para serializar e anular a serialização dos dados de resposta:

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

Capítulo 8 - Criar a classe VoiceRecognizer

Esta classe reconhecerá a entrada de voz do utilizador.

Para criar esta classe:

  1. Clique com o botão direito do rato dentro da pasta Scripts e, em seguida, clique em Criar>Script C#. Chame o script VoiceRecognizer.

  2. Faça duplo clique no novo script VoiceRecognizer para o abrir com o Visual Studio.

  3. Adicione os seguintes espaços de nomes acima da classe VoiceRecognizer :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.Windows.Speech;
    
  4. Em seguida, adicione as seguintes variáveis dentro da classe VoiceRecognizer , acima do método 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. Adicione os métodos Awake() e Start(), os quais configurarão as palavras-chave do utilizador para serem reconhecidas ao associar uma etiqueta a uma imagem:

        /// <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. Elimine o método Update( ).

  7. Adicione o seguinte processador, que é chamado sempre que a entrada de voz é reconhecida:

        /// <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. Certifique-se de que guarda as alterações no Visual Studio antes de regressar ao Unity.

Nota

Não se preocupe com o código que poderá parecer ter um erro, uma vez que irá fornecer mais classes em breve, o que irá corrigi-las.

Capítulo 9 - Criar a classe CustomVisionTrainer

Esta classe irá encadear uma série de chamadas Web para preparar o Serviço de Visão Personalizada. Cada chamada será explicada em detalhe diretamente acima do código.

Para criar esta classe:

  1. Clique com o botão direito do rato dentro da pasta Scripts e, em seguida, clique em Criar>Script C#. Chame o script CustomVisionTrainer.

  2. Faça duplo clique no novo script CustomVisionTrainer para o abrir com o Visual Studio.

  3. Adicione os seguintes espaços de nomes acima da classe CustomVisionTrainer :

    using Newtonsoft.Json;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. Em seguida, adicione as seguintes variáveis dentro da classe CustomVisionTrainer , acima do método Start( ).

    Nota

    O URL de preparação utilizado aqui é fornecido na documentação do Visão Personalizada Training 1.2 e tem uma estrutura de:https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/{projectId}/
    Para obter mais informações, visite a API de referência da Preparação do Visão Personalizada v1.2.

    Aviso

    É importante que tome nota do ponto final que o Serviço Visão Personalizada lhe fornece para o modo de preparação, uma vez que a estrutura JSON utilizada (na classe CustomVisionObjects) foi configurada para funcionar com o Visão Personalizada Training v1.2. Se tiver uma versão diferente, poderá ter de atualizar a estrutura Objetos .

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

    Importante

    Certifique-se de que adiciona o valor da Chave de Serviço (Chave de Preparação) e o valor do ID do Projeto , que anotou anteriormente; estes são os valores que recolheu do portal anteriormente no curso (Capítulo 2, passo 10 em diante).

  5. Adicione os seguintes métodos Start() e Awake( ). Esses métodos são chamados na inicialização e contêm a chamada para configurar a IU:

        /// <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. Elimine o método Update( ). Esta classe não vai precisar dela.

  7. Adicione o método RequestTagSelection( ). Este método é o primeiro a ser chamado quando uma imagem foi capturada e armazenada no dispositivo e está agora pronta para ser submetida ao Serviço Visão Personalizada, para a preparar. Este método apresenta, na IU de preparação, um conjunto de palavras-chave que o utilizador pode utilizar para etiquetar a imagem capturada. Também alerta a classe VoiceRecognizer para começar a ouvir o utilizador para obter entrada de voz.

        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. Adicione o método VerifyTag( ). Este método receberá a entrada de voz reconhecida pela classe VoiceRecognizer , verificará a sua validade e, em seguida, iniciará o processo de preparação.

        /// <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. Adicione o método SubmitImageForTraining( ). Este método iniciará o processo de preparação do serviço Visão Personalizada. O primeiro passo é obter o ID da Etiqueta do Serviço que está associado à entrada de voz validada do utilizador. Em seguida, o ID da Etiqueta será carregado juntamente com a imagem.

        /// <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. Adicione o método TrainCustomVisionProject( ). Assim que a imagem tiver sido submetida e marcada, este método será chamado. Irá criar uma nova Iteração que será preparada com todas as imagens anteriores submetidas para o Serviço e a imagem que acabou de carregar. Assim que a preparação estiver concluída, este método chamará um método para definir a Iteração recentemente criada como Predefinição, para que o ponto final que está a utilizar para análise seja a iteração preparada mais recente.

        /// <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. Adicione o método SetDefaultIteration( ). Este método definirá a iteração criada e preparada anteriormente como Predefinição. Depois de concluído, este método terá de eliminar a iteração anterior existente no Serviço. No momento da escrita deste curso, existe um limite máximo de dez (10) iterações permitidas ao mesmo tempo no Serviço.

        /// <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. Adicione o método DeletePreviousIteration( ). Este método irá localizar e eliminar a iteração anterior não predefinida:

        /// <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. O último método a adicionar nesta classe é o método GetImageAsByteArray(), utilizado nas chamadas Web para converter a imagem capturada numa matriz de bytes.

        /// <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. Certifique-se de que guarda as alterações no Visual Studio antes de regressar ao Unity.

Capítulo 10 - Criar a classe SceneOrganiser

Esta classe irá:

  • Crie um objeto Cursor para anexar à Câmara Principal.

  • Crie um objeto Etiqueta que será apresentado quando o Serviço reconhecer os objetos do mundo real.

  • Configure a Câmara Principal ao anexar os componentes adequados à mesma.

  • Quando estiver no Modo de Análise, desova as Etiquetas no runtime, no espaço mundial adequado em relação à posição da Câmara Principal e apresente os dados recebidos do Serviço Visão Personalizada.

  • Quando estiver no Modo de Preparação, desova a IU que irá apresentar as diferentes fases do processo de preparação.

Para criar esta classe:

  1. Clique com o botão direito do rato dentro da pasta Scripts e, em seguida, clique em Criar>Script C#. Atribua o nome SceneOrganiser ao script.

  2. Faça duplo clique no novo script SceneOrganiser para o abrir com o Visual Studio.

  3. Só precisará de um espaço de nomes, remova os outros de cima da classe SceneOrganiser :

    using UnityEngine;
    
  4. Em seguida, adicione as seguintes variáveis dentro da classe SceneOrganiser , acima do método 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. Elimine os métodos Iniciar() e Atualizar( ).

  6. Mesmo por baixo das variáveis, adicione o método Awake(), que inicializará a classe e configurará a cena.

        /// <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. Agora, adicione o método CreateCameraCursor() que cria e posiciona o cursor câmara principal e o método CreateLabel(), que cria o objeto 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. Adicione o método SetCameraStatus(), que processará as mensagens destinadas à malha de texto que fornece o estado da câmara.

        /// <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. Adicione os métodos PlaceAnalysisLabel() e SetTagsToLastLabel(), que irão gerar e apresentar os dados do Serviço Visão Personalizada no local.

        /// <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. Por fim, adicione o método CreateTrainingUI(), que irá gerar a IU que apresenta as múltiplas fases do processo de preparação quando a aplicação estiver no Modo de Preparação. Este método também será aproveitado para criar o objeto de estado da câmara.

        /// <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. Certifique-se de que guarda as alterações no Visual Studio antes de regressar ao Unity.

Importante

Antes de continuar, abra a classe CustomVisionAnalyser e, no método AnalysisLastImageCaptured(),anule o comentário das seguintes linhas:

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

Capítulo 11 - Criar a classe ImageCapture

A próxima classe que vai criar é a classe ImageCapture .

Esta classe é responsável por:

  • Capturar uma imagem com a câmara holoLens e armazená-la na Pasta de Aplicações .

  • Processar gestos de Toque do utilizador.

  • Manter o valor Enum que determina se a aplicação será executada no Modo de Análise ou no Modo de Preparação .

Para criar esta classe:

  1. Aceda à pasta Scripts que criou anteriormente.

  2. Clique com o botão direito do rato dentro da pasta e, em seguida, clique em Criar > Script C#. Atribua o nome ImageCapture ao script.

  3. Faça duplo clique no novo script ImageCapture para o abrir com o Visual Studio.

  4. Substitua os espaços de nomes na parte superior do ficheiro pelo seguinte:

    using System;
    using System.IO;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.XR.WSA.Input;
    using UnityEngine.XR.WSA.WebCam;
    
  5. Em seguida, adicione as seguintes variáveis dentro da classe ImageCapture , acima do método Iniciar( ):

        /// <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. O código para métodos Awake() e Start() tem agora de ser adicionado:

        /// <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. Implemente um processador que será chamado quando ocorrer um gesto de Toque.

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

    Nota

    No Modo de análise , o método TapHandler funciona como um comutador para iniciar ou parar o ciclo de captura de fotografias.

    No modo de preparação , irá capturar uma imagem da câmara.

    Quando o cursor está verde, significa que a câmara está disponível para tirar a imagem.

    Quando o cursor está vermelho, significa que a câmara está ocupada.

  8. Adicione o método que a aplicação utiliza para iniciar o processo de captura de imagens e armazenar a imagem.

        /// <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. Adicione os processadores que serão chamados quando a fotografia tiver sido capturada e quando estiver pronta para ser analisada. Em seguida, o resultado é transmitido para o CustomVisionAnalyser ou para o CustomVisionTrainer consoante o modo em que o código está definido.

        /// <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. Certifique-se de que guarda as alterações no Visual Studio antes de regressar ao Unity.

  11. Agora que todos os scripts foram concluídos, volte ao Editor do Unity e, em seguida, clique e arraste a classe SceneOrganiser da pasta Scripts para o objeto Câmara Principal no Painel de Hierarquia.

Capítulo 12 - Antes do edifício

Para realizar um teste minucioso da sua aplicação, terá de carregá-la lateralmente no HoloLens.

Antes de o fazer, certifique-se de que:

  • Todas as definições mencionadas no Capítulo 2 estão definidas corretamente.

  • Todos os campos na Câmara Principal, Painel de Inspetor, são atribuídos corretamente.

  • O script SceneOrganiser está anexado ao objeto Câmara Principal .

  • Certifique-se de que insere a Chave de Predição na variável predictionKey .

  • Inseriu o Ponto Final de Predição na variável predictionEndpoint .

  • Inseriu a Chave de Preparação na variável trainingKey da classe CustomVisionTrainer .

  • Inseriu o ID do Projeto na variável projectId da classe CustomVisionTrainer .

Capítulo 13 – Compilar e sideload da sua aplicação

Para iniciar o processo de compilação :

  1. Aceda a Definições de Compilação de Ficheiros>.

  2. Marque Projetos C# do Unity.

  3. Clique em Compilar. O Unity iniciará uma janela de Explorador de Ficheiros, onde tem de criar e, em seguida, selecionar uma pasta para criar a aplicação. Crie essa pasta agora e dê-lhe o nome Aplicação. Em seguida, com a pasta Aplicação selecionada, clique em Selecionar Pasta.

  4. O Unity começará a criar o seu projeto para a pasta Aplicação .

  5. Assim que o Unity terminar a criação (poderá demorar algum tempo), abrirá uma janela de Explorador de Ficheiros na localização da compilação (verifique a barra de tarefas, uma vez que pode nem sempre aparecer acima das janelas, mas irá notificá-lo da adição de uma nova janela).

Para implementar no HoloLens:

  1. Precisará do Endereço IP do HoloLens (para Implementação Remota) e para garantir que o HoloLens está no Modo de Programador. Para efetuar este procedimento:

    1. Enquanto estiver a usar o HoloLens, abra as Definições.

    2. Aceda aOpções Avançadasde Wi-Fi> de Rede & Internet>

    3. Repare no endereço IPv4 .

    4. Em seguida, navegue de volta para Definições e, em seguida, para Atualizar Segurança> &para Programadores

    5. Defina o Modo de Programador ativado.

  2. Navegue para a nova compilação do Unity (a pasta Aplicação ) e abra o ficheiro de solução com o Visual Studio.

  3. Na Configuração da Solução , selecione Depurar.

  4. Na Plataforma de Soluções, selecione x86, Máquina Remota. Ser-lhe-á pedido que insira o endereço IP de um dispositivo remoto (o HoloLens, neste caso, que anotou).

    Definir endereço IP

  5. Aceda ao menu Compilar e clique em Implementar Solução para colocar a aplicação sideload no HoloLens.

  6. A sua aplicação deverá agora aparecer na lista de aplicações instaladas no HoloLens, prontas para serem iniciadas!

Nota

Para implementar em headsets envolventes, defina a Plataforma de Soluções como Máquina Local e defina a Configuração como Depuração, com x86 como Plataforma. Em seguida, implemente no computador local, com o item de menu Compilar , selecionando Implementar Solução.

Para utilizar a aplicação:

Para alternar a funcionalidade da aplicação entre o Modo de preparação e o modo de Predição , tem de atualizar a variável AppMode , localizada no método Awake() localizado na 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;

No modo de preparação :

  • Observe Rato ou Teclado e utilize o gesto Tocar.

  • Em seguida, será apresentado texto a pedir-lhe para fornecer uma etiqueta.

  • Diga Rato ou Teclado.

No modo de Predição :

  • Observe um objeto e utilize o gesto Tocar.

  • O texto será apresentado fornecendo o objeto detetado, com a maior probabilidade (isto é normalizado).

Capítulo 14 – Avaliar e melhorar o modelo de Visão Personalizada

Para tornar o seu Serviço mais preciso, terá de continuar a preparar o modelo utilizado para predição. Isto é feito através da utilização da sua nova aplicação, com os modos de preparação e predição , sendo que este último exige que visite o portal, que é o que está abrangido neste Capítulo. Esteja preparado para revisitar o portal muitas vezes, para melhorar continuamente o seu modelo.

  1. Aceda novamente ao portal Visão Personalizada do Azure e, quando estiver no seu projeto, selecione o separador Predições (no centro superior da página):

    Separador Selecionar predições

  2. Verá todas as imagens que foram enviadas para o seu Serviço enquanto a sua aplicação estava em execução. Se pairar o cursor sobre as imagens, estas irão fornecer-lhe as predições que foram feitas para essa imagem:

    Lista de imagens de predição

  3. Selecione uma das suas imagens para abri-la. Assim que abrir, verá as predições feitas para essa imagem à direita. Se as predições estiverem corretas e quiser adicionar esta imagem ao modelo de preparação do seu Serviço, clique na caixa de entrada As Minhas Etiquetas e selecione a etiqueta que pretende associar. Quando terminar, clique no botão Guardar e fechar na parte inferior direita e continue para a imagem seguinte.

    Selecionar imagem a abrir

  4. Assim que voltar à grelha de imagens, irá reparar que as imagens às quais adicionou etiquetas (e guardou) serão removidas. Se encontrar imagens que considere que não têm o item etiquetado, pode eliminá-las ao clicar no tiquetaque nessa imagem (pode fazê-lo para várias imagens) e, em seguida, clicar em Eliminar no canto superior direito da página da grelha. No pop-up que se segue, pode clicar em Sim, eliminar ou Não, para confirmar a eliminação ou cancelá-la, respetivamente.

    Eliminar imagens

  5. Quando estiver pronto para continuar, clique no botão verde Preparar no canto superior direito. O modelo de Serviço será preparado com todas as imagens que já forneceu (o que o tornará mais preciso). Assim que a preparação estiver concluída, certifique-se de que clica no botão Tornar predefinido mais uma vez, para que o URL de Predição continue a utilizar a iteração mais atualizada do seu Serviço.

    Iniciar modelo de serviço de preparaçãoSelecione fazer a opção predefinida

A sua aplicação de API Visão Personalizada concluída

Parabéns, criou uma aplicação de realidade mista que tira partido da API do Azure Visão Personalizada para reconhecer objetos do mundo real, preparar o modelo de Serviço e mostrar confiança no que foi visto.

Exemplo de projeto concluído

Exercícios de bónus

Exercício 1

Prepare o serviço Visão Personalizada para reconhecer mais objetos.

Exercício 2

Como forma de expandir o que aprendeu, conclua os seguintes exercícios:

Reproduza um som quando um objeto é reconhecido.

Exercício 3

Utilize a API para voltar a preparar o Seu Serviço com as mesmas imagens que a sua aplicação está a analisar, para tornar o Serviço mais preciso (faça a predição e a preparação em simultâneo).