HoloLens (1ª geração) e Azure 310: detecção de objetos
Observação
Os tutoriais do Mixed Reality Academy foram projetados com o HoloLens (1ª geração) e os headsets imersivos de realidade misturada em mente. Dessa forma, achamos que é importante continuar disponibilizando esses tutoriais para os desenvolvedores que ainda buscam obter diretrizes para o desenvolvimento visando esses dispositivos. Esses tutoriais não serão atualizados com os conjuntos de ferramentas mais recentes nem com as interações usadas para o HoloLens 2. Eles serão mantidos para continuar funcionando nos dispositivos compatíveis. Haverá uma nova série de tutoriais que serão postados no futuro que demonstrarão como desenvolver para o HoloLens 2. Este aviso será atualizado com um link para esses tutoriais quando eles forem postados.
Neste curso, você aprenderá a reconhecer o conteúdo visual personalizado e sua posição espacial em uma imagem fornecida, usando os recursos de "Detecção de Objetos" da Visão Personalizada do Azure em um aplicativo de realidade misturada.
Esse serviço permitirá que você treine um modelo de aprendizado de máquina usando imagens de objeto. Em seguida, você usará o modelo treinado para reconhecer objetos semelhantes e aproximar sua localização no mundo real, conforme fornecido pela captura de câmera do Microsoft HoloLens ou uma câmera conectada a um computador para headsets imersivos (VR).
A Visão Personalizada do Azure, Detecção de Objetos é um serviço da Microsoft que permite que os desenvolvedores criem classificadores de imagem personalizados. Esses classificadores podem ser usados com novas imagens para detectar objetos dentro dessa nova imagem, fornecendo limites de caixa dentro da própria imagem. O Serviço fornece um portal online simples e fácil de usar para agilizar esse processo. Para obter mais informações, visite os seguintes links:
Após a conclusão deste curso, você terá um aplicativo de realidade mista que poderá fazer o seguinte:
- O usuário poderá olhar para um objeto, que ele treinou usando o Serviço de Visão Personalizada do Azure, Detecção de Objetos.
- O usuário usará o gesto Tocar para capturar uma imagem do que está vendo.
- O aplicativo enviará a imagem para o Serviço de Visão Personalizada do Azure.
- Haverá uma resposta do Serviço que exibirá o resultado do reconhecimento como texto do espaço mundial. Isso será feito por meio da utilização do Rastreamento Espacial do Microsoft HoloLens, como uma forma de entender a posição mundial do objeto reconhecido e, em seguida, usando a Tag associada ao que é detectado na imagem, para fornecer o texto do rótulo.
O curso também abordará o upload manual de imagens, a criação de tags e o treinamento do Serviço para reconhecer diferentes objetos (no exemplo fornecido, um copo) definindo a Caixa de Limite dentro da imagem que você enviar.
Importante
Após a criação e o uso do aplicativo, o desenvolvedor deve navegar de volta para o Serviço de Visão Personalizada do Azure, identificar as previsões feitas pelo Serviço e determinar se elas estavam corretas ou não (marcando qualquer coisa que o Serviço perdeu e ajustando as Caixas Delimitadoras). O Serviço pode então ser treinado novamente, o que aumentará a probabilidade de reconhecer objetos do mundo real.
Este curso ensinará como obter os resultados do Serviço de Visão Personalizada do Azure, Detecção de Objetos, em um aplicativo de exemplo baseado em Unity. Caberá a você aplicar esses conceitos a um aplicativo personalizado que você possa estar criando.
Suporte a dispositivos
Curso | HoloLens | Headsets imersivos |
---|---|---|
MR e Azure 310: detecção de objetos | ✔️ |
Pré-requisitos
Observação
Este tutorial foi desenvolvido para desenvolvedores que têm experiência básica com Unity e C#. Esteja ciente também de que os pré-requisitos e as instruções escritas neste documento representam o que foi testado e verificado no momento da redação (julho de 2018). Você é livre para usar o software mais recente, conforme listado no artigo instalar as ferramentas , embora não se deva presumir que as informações deste curso corresponderão perfeitamente ao que você encontrará em softwares mais recentes do que os listados abaixo.
Recomendamos o seguinte hardware e software para este curso:
- Um PC de desenvolvimento
- Windows 10 Fall Creators Update (ou posterior) com o modo de desenvolvedor habilitado
- O SDK mais recente do Windows 10
- Unidade 2017.4 LTS
- Visual Studio 2017
- Um Microsoft HoloLens com o modo de desenvolvedor habilitado
- Acesso à Internet para instalação do Azure e recuperação do Serviço de Visão Personalizada
- Uma série de pelo menos quinze (15) imagens é necessária) para cada objeto que você gostaria que a Visão Personalizada reconhecesse. Se desejar, você pode usar as imagens já fornecidas com este curso, uma série de xícaras).
Antes de começar
- Para evitar problemas ao criar este projeto, é altamente recomendável que você crie o projeto mencionado neste tutorial em uma pasta raiz ou quase raiz (caminhos de pasta longos podem causar problemas no momento da compilação).
- Configure e teste seu HoloLens. Se você precisar de suporte para isso, visite o artigo de instalação do HoloLens.
- É uma boa ideia executar a Calibração e o Ajuste do Sensor ao começar a desenvolver um novo aplicativo HoloLens (às vezes, pode ajudar a executar essas tarefas para cada usuário).
Para obter ajuda sobre a calibração, siga este link para o artigo Calibração do HoloLens.
Para obter ajuda sobre o Ajuste do Sensor, siga este link para o artigo Ajuste do Sensor do HoloLens.
Capítulo 1 – O Portal de Visão Personalizada
Para usar o Serviço de Visão Personalizada do Azure, você precisará configurar uma instância dele para ser disponibilizada para seu aplicativo.
Navegue até a página principal do Serviço de Visão Personalizada.
Clique em Introdução.
Entre no Portal de Visão Personalizada.
Se você ainda não tiver uma conta do Azure, precisará criar uma. Se você estiver seguindo este tutorial em uma sala de aula ou laboratório, peça ajuda ao seu instrutor ou a um dos supervisores para configurar sua nova conta.
Depois de fazer login pela primeira vez, você será solicitado com o painel Termos de Serviço . Clique na caixa de seleção para concordar com os termos. Em seguida, clique em Concordo.
Tendo concordado com os termos, você está agora na seção Meus projetos . Clique em Novo Projeto.
Uma guia aparecerá no lado direito, solicitando que você especifique alguns campos para o projeto.
Insira um nome para o seu projeto
Insira uma descrição para o seu projeto (opcional)
Escolha um grupo de recursos ou crie um novo. Um grupo de recursos fornece uma maneira de monitorar, controlar o acesso, provisionar e gerenciar a cobrança de uma coleção de ativos do Azure. É recomendável manter todos os serviços do Azure associados a um único projeto (por exemplo, como esses cursos) em um grupo de recursos comum).
Observação
Se você quiser ler mais sobre os Grupos de Recursos do Azure, navegue até os Documentos associados
Defina os Tipos de Projeto como Detecção de Objeto (versão prévia).
Quando terminar, clique em Criar projeto e você será redirecionado para a página do projeto do Serviço de Visão Personalizada.
Capítulo 2 – Treinando seu projeto de Visão Personalizada
Uma vez no Portal de Visão Personalizada, seu objetivo principal é treinar seu projeto para reconhecer objetos específicos em imagens.
Você precisa de pelo menos quinze (15) imagens para cada objeto que deseja que seu aplicativo reconheça. Você pode usar as imagens fornecidas com este curso (uma série de xícaras).
Para treinar seu projeto de Visão Personalizada:
Clique no + botão ao lado de Tags.
Adicione um nome para a tag que será usada para associar suas imagens. Neste exemplo, estamos usando imagens de xícaras para reconhecimento, então nomeamos a tag para isso, Copa. Clique em Salvar quando terminar.
Você notará que sua tag foi adicionada (pode ser necessário recarregar sua página para que ela apareça).
Clique em Adicionar imagens no centro da página.
Clique em Procurar arquivos locais e navegue até as imagens que deseja enviar para um objeto, com o mínimo de quinze (15).
Dica
Você pode selecionar várias imagens ao mesmo tempo para fazer upload.
Pressione Carregar arquivos depois de selecionar todas as imagens com as quais deseja treinar o projeto. Os arquivos começarão a ser carregados. Depois de confirmar o upload, clique em Concluído.
Neste ponto, suas imagens são carregadas, mas não marcadas.
Para marcar suas imagens, use o mouse. À medida que você passa o mouse sobre sua imagem, um destaque de seleção o ajudará desenhando automaticamente uma seleção ao redor do objeto. Se não for preciso, você pode desenhar o seu próprio. Isso é feito segurando o botão esquerdo do mouse e arrastando a região de seleção para abranger seu objeto.
Após a seleção do seu objeto na imagem, um pequeno prompt solicitará que você adicione a tag de região. Selecione sua tag criada anteriormente ('Cup', no exemplo acima) ou, se estiver adicionando mais tags, digite-a e clique no botão + (mais).
Para marcar a próxima imagem, você pode clicar na seta à direita da folha ou fechar a folha de marca (clicando no X no canto superior direito da folha) e clicar na próxima imagem. Depois de ter a próxima imagem pronta, repita o mesmo procedimento. Faça isso para todas as imagens que você carregou, até que todas sejam marcadas.
Observação
Você pode selecionar vários objetos na mesma imagem, como na imagem abaixo:
Depois de marcar todos eles, clique no botão marcado , à esquerda da tela, para revelar as imagens marcadas.
Agora você está pronto para treinar seu serviço. Clique no botão Treinar e a primeira iteração de treinamento começará.
Depois de criado, você poderá ver dois botões chamados Tornar padrão e URL de previsão. Clique em Tornar padrão primeiro e, em seguida, clique em URL de previsão.
Observação
O ponto de extremidade fornecido a partir disso é definido como qualquer iteração marcada como padrão. Dessa forma, se você fizer uma nova iteração posteriormente e atualizá-la como padrão, não precisará alterar seu código.
Depois de clicar em URL de Previsão, abra o Bloco de Notas e copie e cole a URL (também chamada de Ponto de Extremidade de Previsão) e a Chave de Previsão de Serviço, para que você possa recuperá-la quando precisar dela posteriormente no código.
Capítulo 3 – Configurar o projeto do Unity
Veja a seguir uma configuração típica para desenvolvimento com realidade misturada e, como tal, é um bom modelo para outros projetos.
Abra o Unity e clique em Novo.
Agora você precisará fornecer um nome de projeto do Unity. Insira CustomVisionObjDetection. Certifique-se de que o tipo de projeto esteja definido como 3D e defina o Local para um lugar apropriado para você (lembre-se, mais próximo dos diretórios raiz é melhor). Em seguida, clique em Criar projeto.
Com o Unity aberto, vale a pena verificar se o Editor de Scripts padrão está definido como Visual Studio. Vá para Editar>preferências e, na nova janela, navegue até Ferramentas externas. Altere o Editor de Script Externo para Visual Studio. Feche a janela Preferências.
Em seguida, vá para Configurações de Build de Arquivo > e alterne a Plataforma para Plataforma Universal do Windows e clique no botão Alternar Plataforma.
Na mesma janela Configurações de Compilação, verifique se o seguinte está definido:
O dispositivo de destino está definido como HoloLens
O Tipo de Construção está definido como D3D
O SDK está definido como Instalado mais recente
A versão do Visual Studio está definida como Mais recente instalado
Build and Run está definido como Computador Local
As configurações restantes, em Configurações de Build, devem ser deixadas como padrão por enquanto.
Na mesma janela Configurações de construção, clique no botão Configurações do player, isso abrirá o painel relacionado no espaço onde o Inspetor está localizado.
Neste painel, algumas configurações precisam ser verificadas:
Na guia Outras configurações:
A versão do tempo de execução de script deve ser experimental (equivalente ao .NET 4.6), o que disparará a necessidade de reiniciar o editor.
O back-end de script deve ser .NET.
O nível de compatibilidade da API deve ser .NET 4.6.
Na guia Configurações de Publicação, em Recursos, marque:
InternetClient
Webcam
SpatialPerception
Mais abaixo no painel, em Configurações XR (encontradas abaixo de Configurações de Publicação), marque Realidade Virtual com Suporte e verifique se o SDK do Windows Mixed Reality foi adicionado.
De volta às Configurações de Build, os Projetos C# do Unity não estão mais esmaecidos: marque a caixa de seleção ao lado disso.
Feche a janela Configurações de Build.
No Editor, clique em Editar>gráficos de configurações> do projeto.
No Painel Inspetor, as Configurações Gráficas serão abertas. Role para baixo até ver uma matriz chamada Sempre Incluir Sombreadores. Adicione um slot aumentando a variável Size em um (neste exemplo, era 8, então tornamos 9). Um novo slot aparecerá, na última posição do array, conforme mostrado abaixo:
No slot, clique no pequeno círculo de destino ao lado do slot para abrir uma lista de shaders. Procure o sombreador Legacy Shaders/Transparent/Diffuse e clique duas vezes nele.
Capítulo 4 – Importando o pacote CustomVisionObjDetection do Unity
Para este curso, você recebe um Pacote de Ativos do Unity chamado Azure-MR-310.unitypackage.
[DICA] Todos os objetos suportados pelo Unity, incluindo cenas inteiras, podem ser empacotados em um arquivo .unitypackage e exportados/importados em outros projetos. É a maneira mais segura e eficiente de mover ativos entre diferentes projetos do Unity.
Você pode encontrar o pacote Azure-MR-310 que precisa baixar aqui.
Com o painel do Unity à sua frente, clique em Ativos no menu na parte superior da tela e, em seguida, clique em Importar pacote > personalizado.
Use o seletor de arquivos para selecionar o pacote Azure-MR-310.unitypackage e clique em Abrir. Uma lista de componentes para este ativo será exibida para você. Confirme a importação clicando no botão Importar .
Depois de terminar a importação, você notará que as pastas do pacote foram adicionadas à pasta Ativos . Esse tipo de estrutura de pastas é típico para um projeto do Unity.
A pasta Materiais contém o material usado pelo Cursor do Olhar.
A pasta Plugins contém a DLL Newtonsoft usada pelo código para desserializar a resposta da Web do serviço. As duas (2) versões diferentes contidas na pasta e na subpasta são necessárias para permitir que a biblioteca seja usada e criada pelo Editor do Unity e pela compilação UWP.
A pasta Prefabs contém os prefabs contidos na cena. Eles são:
- O GazeCursor, o cursor usado no aplicativo. Funcionará em conjunto com o pré-fabricado SpatialMapping para poder ser colocado na cena em cima de objetos físicos.
- O Rótulo, que é o objeto de interface do usuário usado para exibir a marca de objeto na cena quando necessário.
- O SpatialMapping, que é o objeto que permite que o aplicativo use a criação de um mapa virtual, usando o rastreamento espacial do Microsoft HoloLens.
A pasta Cenas que atualmente contém a cena pré-criada para este curso.
Abra a pasta Cenas , no Painel do Projeto, e clique duas vezes em ObjDetectionScene para carregar a cena que você usará para este curso.
Observação
Nenhum código está incluído, você escreverá o código seguindo este curso.
Capítulo 5 – Crie a classe CustomVisionAnalyser.
Neste ponto, você está pronto para escrever algum código. Você começará com a classe CustomVisionAnalyser .
Observação
As chamadas para o Serviço de Visão Personalizada, feitas no código mostrado abaixo, são feitas usando a API REST da Visão Personalizada. Ao usar isso, você verá como implementar e fazer uso dessa API (útil para entender como implementar algo semelhante por conta própria). Lembre-se de que a Microsoft oferece um SDK de Visão Personalizada que também pode ser usado para fazer chamadas para o Serviço. Para obter mais informações, visite o artigo SDK da Visão Personalizada.
Esta classe é responsável por:
Carregando a imagem mais recente capturada como uma matriz de bytes.
Enviar a matriz de bytes para sua instância do Serviço de Visão Personalizada do Azure para análise.
Recebendo a resposta como uma cadeia de caracteres JSON.
Desserializando a resposta e passando a Prediction resultante para a classe SceneOrganiser, que cuidará de como a resposta deve ser exibida.
Para criar essa classe:
Clique com o botão direito do mouse na pasta Ativo, localizada no painel Projeto, e clique em Criar>pasta. Chame a pasta de Scripts.
Clique duas vezes na pasta recém-criada para abri-la.
Clique com o botão direito do mouse dentro da pasta e clique em Criar>Script C#. Nomeie o script CustomVisionAnalyser.
Clique duas vezes no novo script CustomVisionAnalyser para abri-lo com o Visual Studio.
Verifique se você tem os seguintes namespaces referenciados na parte superior do arquivo:
using Newtonsoft.Json; using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking;
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> /// Bite array of the image to submit for analysis /// </summary> [HideInInspector] public byte[] imageBytes;
Observação
Certifique-se de inserir sua Chave de Previsão de Serviço na variável predictionKey e seu Ponto de Extremidade de Previsão na variável predictionEndpoint . Você os copiou para o Bloco de Notas anteriormente, no Capítulo 2, Etapa 14.
O código para Awake() agora precisa ser adicionado para inicializar a variável Instance:
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; }
Adicione a corrotina (com o método estático GetImageAsByteArray() abaixo dela), que obterá os resultados da análise da imagem, capturados pela classe ImageCapture .
Observação
Na corrotina AnalyseImageCapture, há uma chamada para a classe SceneOrganiser que você ainda não criou. Portanto, deixe essas linhas comentadas por enquanto.
/// <summary> /// Call the Computer Vision Service to submit the image. /// </summary> public IEnumerator AnalyseLastImageCaptured(string imagePath) { Debug.Log("Analyzing..."); 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; Debug.Log("response: " + jsonResponse); // Create a texture. Texture size does not matter, since // LoadImage will replace with the incoming image size. //Texture2D tex = new Texture2D(1, 1); //tex.LoadImage(imageBytes); //SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex); // The response will be in JSON format, therefore it needs to be deserialized //AnalysisRootObject analysisRootObject = new AnalysisRootObject(); //analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse); //SceneOrganiser.Instance.FinaliseLabel(analysisRootObject); } } /// <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); }
Exclua os métodos Start() e Update(), pois eles não serão usados.
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Importante
Como mencionado anteriormente, não se preocupe com o código que pode parecer ter um erro, pois você fornecerá mais classes em breve, que as corrigirão.
Capítulo 6 – Criar a classe CustomVisionObjects
A classe que você criará agora é a classe CustomVisionObjects .
Esse script contém vários objetos usados por outras classes para serializar e desserializar as chamadas feitas para o Serviço de Visão Personalizada.
Para criar essa classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Chame o script de CustomVisionObjects.
Clique duas vezes no novo script CustomVisionObjects para abri-lo com o Visual Studio.
Verifique se você tem os seguintes namespaces referenciados na parte superior do arquivo:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking;
Exclua os métodos Start() e Update() dentro da classe CustomVisionObjects , essa classe agora deve estar vazia.
Aviso
É importante que você siga as próximas instruções cuidadosamente. Se você colocar as novas declarações de classe dentro da classe CustomVisionObjects , obterá erros de compilação no capítulo 10, informando que AnalysisRootObject e BoundingBox não foram encontrados.
Adicione as seguintes classes fora da classe CustomVisionObjects . Esses objetos são usados pela biblioteca Newtonsoft para serializar e desserializar os 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 /// Includes Bounding Box /// </summary> public class AnalysisRootObject { public string id { get; set; } public string project { get; set; } public string iteration { get; set; } public DateTime created { get; set; } public List<Prediction> predictions { get; set; } } public class BoundingBox { public double left { get; set; } public double top { get; set; } public double width { get; set; } public double height { get; set; } } public class Prediction { public double probability { get; set; } public string tagId { get; set; } public string tagName { get; set; } public BoundingBox boundingBox { get; set; } }
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Capítulo 7 – Criar a classe SpatialMapping
Esta classe definirá o Spatial Mapping Collider na cena para poder detectar colisões entre objetos virtuais e objetos reais.
Para criar essa classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Chame o script SpatialMapping.
Clique duas vezes no novo script SpatialMapping para abri-lo com o Visual Studio.
Verifique se você tem os seguintes namespaces referenciados acima da classe SpatialMapping :
using UnityEngine; using UnityEngine.XR.WSA;
Em seguida, adicione as seguintes variáveis dentro da classe SpatialMapping, acima do método Start():
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static SpatialMapping Instance; /// <summary> /// Used by the GazeCursor as a property with the Raycast call /// </summary> internal static int PhysicsRaycastMask; /// <summary> /// The layer to use for spatial mapping collisions /// </summary> internal int physicsLayer = 31; /// <summary> /// Creates environment colliders to work with physics /// </summary> private SpatialMappingCollider spatialMappingCollider;
Adicione o awake() e o start():
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; } /// <summary> /// Runs at initialization right after Awake method /// </summary> void Start() { // Initialize and configure the collider spatialMappingCollider = gameObject.GetComponent<SpatialMappingCollider>(); spatialMappingCollider.surfaceParent = this.gameObject; spatialMappingCollider.freezeUpdates = false; spatialMappingCollider.layer = physicsLayer; // define the mask PhysicsRaycastMask = 1 << physicsLayer; // set the object as active one gameObject.SetActive(true); }
Exclua o método Update().
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Capítulo 8 – Criar a classe GazeCursor
Esta classe é responsável por configurar o cursor no local correto no espaço real, fazendo uso do SpatialMappingCollider, criado no capítulo anterior.
Para criar essa classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Chamar o script GazeCursor
Clique duas vezes no novo script GazeCursor para abri-lo com o Visual Studio.
Verifique se você tem o seguinte namespace referenciado acima da classe GazeCursor :
using UnityEngine;
Em seguida, adicione a seguinte variável dentro da classe GazeCursor, acima do método Start().
/// <summary> /// The cursor (this object) mesh renderer /// </summary> private MeshRenderer meshRenderer;
Atualize o método Start() com o seguinte código:
/// <summary> /// Runs at initialization right after the Awake method /// </summary> void Start() { // Grab the mesh renderer that is on the same object as this script. meshRenderer = gameObject.GetComponent<MeshRenderer>(); // Set the cursor reference SceneOrganiser.Instance.cursor = gameObject; gameObject.GetComponent<Renderer>().material.color = Color.green; // If you wish to change the size of the cursor you can do so here gameObject.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); }
Atualize o método Update() com o seguinte código:
/// <summary> /// Update is called once per frame /// </summary> void Update() { // Do a raycast into the world based on the user's head position and orientation. Vector3 headPosition = Camera.main.transform.position; Vector3 gazeDirection = Camera.main.transform.forward; RaycastHit gazeHitInfo; if (Physics.Raycast(headPosition, gazeDirection, out gazeHitInfo, 30.0f, SpatialMapping.PhysicsRaycastMask)) { // If the raycast hit a hologram, display the cursor mesh. meshRenderer.enabled = true; // Move the cursor to the point where the raycast hit. transform.position = gazeHitInfo.point; // Rotate the cursor to hug the surface of the hologram. transform.rotation = Quaternion.FromToRotation(Vector3.up, gazeHitInfo.normal); } else { // If the raycast did not hit a hologram, hide the cursor mesh. meshRenderer.enabled = false; } }
Observação
Não se preocupe com o erro da classe SceneOrganiser não ser encontrada, você a criará no próximo capítulo.
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Capítulo 9 – Criar a classe SceneOrganiser
Esta classe irá:
Configure a câmera principal conectando os componentes apropriados a ela.
Quando um objeto é detectado, ele será responsável por calcular sua posição no mundo real e colocar um Tag Label próximo a ele com o Tag Name apropriado.
Para criar essa classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Nomeie o script como SceneOrganiser.
Clique duas vezes no novo script SceneOrganiser para abri-lo com o Visual Studio.
Verifique se você tem os seguintes namespaces referenciados acima da classe SceneOrganiser :
using System.Collections.Generic; using System.Linq; using UnityEngine;
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 Main Camera /// </summary> internal GameObject cursor; /// <summary> /// The label used to display the analysis on the objects in the real world /// </summary> public GameObject label; /// <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.8f; /// <summary> /// The quad object hosting the imposed image captured /// </summary> private GameObject quad; /// <summary> /// Renderer of the quad object /// </summary> internal Renderer quadRenderer;
Exclua os métodos Start() e Update().
Abaixo 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 CustomVisionObjects class to this Gameobject gameObject.AddComponent<CustomVisionObjects>(); }
Adicione o método PlaceAnalysisLabel(), que instanciará o rótulo na cena (que neste momento é invisível para o usuário). Ele também coloca o quad (também invisível) onde a imagem é colocada e se sobrepõe ao mundo real. Isso é importante porque as coordenadas da caixa recuperadas do Serviço após a análise são rastreadas de volta para esse quad para determinar a localização aproximada do objeto no mundo real.
/// <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>(); lastLabelPlacedText.text = ""; lastLabelPlaced.transform.localScale = new Vector3(0.005f,0.005f,0.005f); // Create a GameObject to which the texture can be applied quad = GameObject.CreatePrimitive(PrimitiveType.Quad); quadRenderer = quad.GetComponent<Renderer>() as Renderer; Material m = new Material(Shader.Find("Legacy Shaders/Transparent/Diffuse")); quadRenderer.material = m; // Here you can set the transparency of the quad. Useful for debugging float transparency = 0f; quadRenderer.material.color = new Color(1, 1, 1, transparency); // Set the position and scale of the quad depending on user position quad.transform.parent = transform; quad.transform.rotation = transform.rotation; // The quad is positioned slightly forward in font of the user quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f); // The quad scale as been set with the following value following experimentation, // to allow the image on the quad to be as precisely imposed to the real world as possible quad.transform.localScale = new Vector3(3f, 1.65f, 1f); quad.transform.parent = null; }
Adicione o método FinaliseLabel(). Ele é responsável por:
- Definir o texto do rótulo com a marca da previsão com a maior confiança.
- Chamar o cálculo da Caixa Delimitadora no objeto quad, posicionado anteriormente, e colocar o rótulo na cena.
- Ajustando a profundidade do rótulo usando um Raycast em direção à caixa delimitadora, que deve colidir com o objeto no mundo real.
- Redefinindo o processo de captura para permitir que o usuário capture outra imagem.
/// <summary> /// Set the Tags as Text of the last label created. /// </summary> public void FinaliseLabel(AnalysisRootObject analysisObject) { if (analysisObject.predictions != null) { lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>(); // Sort the predictions to locate the highest one List<Prediction> sortedPredictions = new List<Prediction>(); sortedPredictions = analysisObject.predictions.OrderBy(p => p.probability).ToList(); Prediction bestPrediction = new Prediction(); bestPrediction = sortedPredictions[sortedPredictions.Count - 1]; if (bestPrediction.probability > probabilityThreshold) { quadRenderer = quad.GetComponent<Renderer>() as Renderer; Bounds quadBounds = quadRenderer.bounds; // Position the label as close as possible to the Bounding Box of the prediction // At this point it will not consider depth lastLabelPlaced.transform.parent = quad.transform; lastLabelPlaced.transform.localPosition = CalculateBoundingBoxPosition(quadBounds, bestPrediction.boundingBox); // Set the tag text lastLabelPlacedText.text = bestPrediction.tagName; // Cast a ray from the user's head to the currently placed label, it should hit the object detected by the Service. // At that point it will reposition the label where the ray HL sensor collides with the object, // (using the HL spatial tracking) Debug.Log("Repositioning Label"); Vector3 headPosition = Camera.main.transform.position; RaycastHit objHitInfo; Vector3 objDirection = lastLabelPlaced.position; if (Physics.Raycast(headPosition, objDirection, out objHitInfo, 30.0f, SpatialMapping.PhysicsRaycastMask)) { lastLabelPlaced.position = objHitInfo.point; } } } // Reset the color of the cursor cursor.GetComponent<Renderer>().material.color = Color.green; // Stop the analysis process ImageCapture.Instance.ResetImageCapture(); }
Adicione o método CalculateBoundingBoxPosition(), que hospeda vários cálculos necessários para converter as coordenadas da caixa delimitadora recuperadas do serviço e recriá-las proporcionalmente no quad.
/// <summary> /// This method hosts a series of calculations to determine the position /// of the Bounding Box on the quad created in the real world /// by using the Bounding Box received back alongside the Best Prediction /// </summary> public Vector3 CalculateBoundingBoxPosition(Bounds b, BoundingBox boundingBox) { Debug.Log($"BB: left {boundingBox.left}, top {boundingBox.top}, width {boundingBox.width}, height {boundingBox.height}"); double centerFromLeft = boundingBox.left + (boundingBox.width / 2); double centerFromTop = boundingBox.top + (boundingBox.height / 2); Debug.Log($"BB CenterFromLeft {centerFromLeft}, CenterFromTop {centerFromTop}"); double quadWidth = b.size.normalized.x; double quadHeight = b.size.normalized.y; Debug.Log($"Quad Width {b.size.normalized.x}, Quad Height {b.size.normalized.y}"); double normalisedPos_X = (quadWidth * centerFromLeft) - (quadWidth/2); double normalisedPos_Y = (quadHeight * centerFromTop) - (quadHeight/2); return new Vector3((float)normalisedPos_X, (float)normalisedPos_Y, 0); }
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Importante
Antes de continuar, abra a classe CustomVisionAnalyser e, no método AnalyseLastImageCaptured(), remova o comentário das seguintes linhas:
// Create a texture. Texture size does not matter, since // LoadImage will replace with the incoming image size. Texture2D tex = new Texture2D(1, 1); tex.LoadImage(imageBytes); SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex); // The response will be in JSON format, therefore it needs to be deserialized AnalysisRootObject analysisRootObject = new AnalysisRootObject(); analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse); SceneOrganiser.Instance.FinaliseLabel(analysisRootObject);
Observação
Não se preocupe com a mensagem 'não foi possível encontrar' da classe ImageCapture , você a criará no próximo capítulo.
Capítulo 10 – Criar a classe ImageCapture
A próxima classe que você criará é a classe ImageCapture .
Esta classe é responsável por:
- Capturar uma imagem usando a câmera do HoloLens e armazená-la na pasta App .
- Manipulando gestos de toque do usuário.
Para criar essa classe:
Vá para a pasta Scripts que você criou anteriormente.
Clique com o botão direito do mouse dentro da pasta e clique em Criar>Script C#. Nomeie o script ImageCapture.
Clique duas vezes no novo script ImageCapture para abri-lo com o Visual Studio.
Substitua os namespaces na parte superior do arquivo pelo seguinte:
using System; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.XR.WSA.Input; using UnityEngine.XR.WSA.WebCam;
Em seguida, adicione as seguintes variáveis dentro da classe ImageCapture, acima do método 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> /// Flagging if the capture loop is running /// </summary> internal bool captureIsActive; /// <summary> /// File path of current analysed photo /// </summary> internal string filePath = string.Empty;
O código para os métodos Awake() e Start() agora precisa ser adicionado:
/// <summary> /// Called on initialization /// </summary> private void Awake() { Instance = this; } /// <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 Microsoft HoloLens API gesture recognizer to track user gestures recognizer = new GestureRecognizer(); recognizer.SetRecognizableGestures(GestureSettings.Tap); recognizer.Tapped += TapHandler; recognizer.StartCapturingGestures(); }
Implemente um manipulador que será chamado quando ocorrer um gesto de toque:
/// <summary> /// Respond to Tap Input. /// </summary> private void TapHandler(TappedEventArgs obj) { if (!captureIsActive) { captureIsActive = true; // Set the cursor color to red SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red; // Begin the capture loop Invoke("ExecuteImageCaptureAndAnalysis", 0); } }
Importante
Quando o cursor está verde, significa que a câmera está disponível para tirar a imagem. Quando o cursor está vermelho, significa que a câmera está ocupada.
Adicione o método que o aplicativo usa para iniciar o processo de captura de imagem e armazene a imagem:
/// <summary> /// Begin process of image capturing and send to Azure Custom Vision Service. /// </summary> private void ExecuteImageCaptureAndAnalysis() { // Create a label in world space using the ResultsLabel 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(true, delegate (PhotoCapture captureObject) { photoCaptureObject = captureObject; CameraParameters camParameters = new CameraParameters { hologramOpacity = 1.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); }); }); }
Adicione os manipuladores que serão chamados quando a foto for capturada e quando estiver pronta para ser analisada. O resultado é então passado para o CustomVisionAnalyser para análise.
/// <summary> /// Register the full execution of the Photo Capture. /// </summary> void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result) { try { // Call StopPhotoMode once the image has successfully captured photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } catch (Exception e) { Debug.LogFormat("Exception capturing photo to disk: {0}", e.Message); } } /// <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; // Call the image analysis StartCoroutine(CustomVisionAnalyser.Instance.AnalyseLastImageCaptured(filePath)); } /// <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; // Stop the capture loop if active CancelInvoke(); }
Certifique-se de salvar suas alterações no Visual Studio antes de retornar ao Unity.
Capítulo 11 - Configurando os scripts na cena
Agora que você escreveu todo o código necessário para este projeto, é hora de configurar os scripts na cena e nos pré-fabricados, para que eles se comportem corretamente.
No Editor do Unity, no Painel de Hierarquia, selecione a Câmera Principal.
No Painel Inspetor, com a Câmera Principal selecionada, clique em Adicionar Componente, procure o script SceneOrganiser e clique duas vezes para adicioná-lo.
No Painel do Projeto, abra a pasta Prefabs, arraste o prefab Label para a área de entrada de destino de referência vazia do Label , no script SceneOrganiser que você acabou de adicionar à Câmera Principal, conforme mostrado na imagem abaixo:
No Painel de Hierarquia, selecione o filho GazeCursor da Câmera Principal.
No Painel Inspetor, com o GazeCursor selecionado, clique em Adicionar componente, procure o script GazeCursor e clique duas vezes para adicioná-lo.
Novamente, no Painel de Hierarquia, selecione o filho SpatialMapping da Câmera Principal.
No Painel Inspetor, com o SpatialMapping selecionado, clique em Adicionar componente, procure o script SpatialMapping e clique duas vezes para adicioná-lo.
Os scripts restantes que você não definiu serão adicionados pelo código no script SceneOrganiser durante o runtime.
Capítulo 12 - Antes de construir
Para executar um teste completo do seu aplicativo, você precisará carregá-lo no Microsoft HoloLens.
Antes de fazer isso, certifique-se de que:
Todas as configurações mencionadas no Capítulo 3 estão definidas corretamente.
O script SceneOrganiser é anexado ao objeto Main Camera .
O script GazeCursor é anexado ao objeto GazeCursor .
O script SpatialMapping é anexado ao objeto SpatialMapping .
No Capítulo 5, Etapa 6:
- Certifique-se de inserir sua Chave de Previsão de Serviço na variável predictionKey .
- Você inseriu seu Ponto de Extremidade de Previsão na classe predictionEndpoint .
Capítulo 13 – Criar a solução UWP e fazer sideload do aplicativo
Agora você está pronto para criar seu aplicativo como uma solução UWP que poderá implantar no Microsoft HoloLens. Para iniciar o processo de compilação:
Vá para Configurações de Build de Arquivo>.
Marque Projetos C# do Unity.
Clique em Adicionar cenas abertas. Isso adicionará a cena aberta no momento à compilação.
Clique em Compilar. O Unity abrirá uma janela do Explorador de Arquivos, onde você precisa criar e selecionar uma pasta para criar o aplicativo. Crie essa pasta agora e nomeie-a como App. Em seguida, com a pasta App selecionada, clique em Select Folder.
O Unity começará a criar seu projeto na pasta App .
Depois que o Unity terminar de compilar (pode levar algum tempo), ele abrirá uma janela do Explorador de Arquivos no local da compilação (verifique a barra de tarefas, pois nem sempre ela aparece acima das janelas, mas notificará você sobre a adição de uma nova janela).
Para implantar no Microsoft HoloLens, você precisará do endereço IP desse dispositivo (para Implantação Remota) e garantir que ele também tenha o Modo de Desenvolvedor definido. Para fazer isso:
Enquanto estiver usando seu HoloLens, abra as Configurações.
Ir para Rede e Internet>Wi-Fi>Opções avançadas
Observe o endereço IPv4 .
Em seguida, navegue de volta para Configurações e, em seguida, para Atualização e segurança>para desenvolvedores
Ative o modo de desenvolvedor.
Navegue até o novo build do Unity (a pasta App ) e abra o arquivo de solução com o Visual Studio.
Na Configuração da Solução, selecione Depurar.
Na Plataforma de Soluções, selecione x86, Computador Remoto. Você será solicitado a inserir o endereço IP de um dispositivo remoto (o Microsoft HoloLens, neste caso, que você anotou).
Vá para o menu Compilar e clique em Implantar Solução para fazer o sideload do aplicativo no HoloLens.
Seu aplicativo agora deve aparecer na lista de aplicativos instalados em seu Microsoft HoloLens, pronto para ser iniciado!
Para usar o aplicativo:
- Examine um objeto que você treinou com o Serviço de Visão Personalizada do Azure, a Detecção de Objetos, e use o gesto Tocar.
- Se o objeto for detectado com sucesso, um texto de rótulo de espaço mundial aparecerá com o nome da tag.
Importante
Toda vez que você captura uma foto e a envia para o Serviço, você pode voltar para a página do Serviço e treinar novamente o Serviço com as imagens recém-capturadas. No início, você provavelmente também terá que corrigir as caixas delimitadoras para serem mais precisas e treinar novamente o serviço.
Observação
O Texto do Rótulo colocado pode não aparecer próximo ao objeto quando os sensores do Microsoft HoloLens e/ou o SpatialTrackingComponent no Unity não conseguem colocar os colisores apropriados, em relação aos objetos do mundo real. Tente usar o aplicativo em uma superfície diferente, se for o caso.
Seu aplicativo de Visão Personalizada, Detecção de Objetos
Parabéns, você criou um aplicativo de realidade misturada que aproveita a Visão Personalizada do Azure, API de Detecção de Objetos, que pode reconhecer um objeto de uma imagem e, em seguida, fornecer uma posição aproximada para esse objeto no espaço 3D.
Exercícios de bônus
Exercício 1
Adicionando ao rótulo de texto, use um cubo semitransparente para envolver o objeto real em uma caixa delimitadora 3D.
Exercício 2
Treine seu Serviço de Visão Personalizada para reconhecer mais objetos.
Exercício 3
Reproduza um som quando um objeto for reconhecido.
Exercício 4
Use a API para treinar novamente seu serviço com as mesmas imagens que seu aplicativo está analisando, para tornar o serviço mais preciso (faça a previsão e o treinamento simultaneamente).