HoloLens (1.ª geração) e Azure 310: deteção de objetos
Nota
Os tutoriais da Academia de Realidade Mista foram projetados com HoloLens (1ª geração) e Headsets Imersivos de Realidade Mista em mente. Como tal, sentimos que é importante deixar estes tutoriais no lugar para desenvolvedores que ainda estão procurando orientação no desenvolvimento para esses dispositivos. Esses tutoriais não serão atualizados com os conjuntos de ferramentas ou interações mais recentes que estão sendo usados para o HoloLens 2. Eles serão mantidos para continuar trabalhando nos dispositivos suportados. Haverá uma nova série de tutoriais que serão publicados no futuro que demonstrarão como desenvolver para o HoloLens 2. Este aviso será atualizado com um link para esses tutoriais quando eles forem publicados.
Neste curso, você aprenderá a reconhecer conteúdo visual personalizado e sua posição espacial em uma imagem fornecida, usando os recursos de "Deteção de Objetos" da Visão Personalizada do Azure em um aplicativo de realidade mista.
Este 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 PC para fones de ouvido imersivos (VR).
Azure Custom Vision, Object Detection é um serviço da Microsoft que permite aos desenvolvedores criar classificadores de imagem personalizados. Esses classificadores podem ser usados com novas imagens para detetar objetos dentro dessa nova imagem, fornecendo limites de caixa dentro da própria imagem. O Serviço disponibiliza um portal online simples e fácil de utilizar para agilizar este 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 será capaz de fazer o seguinte:
- O usuário poderá olhar para um objeto, que ele treinou usando o Serviço de Visão Personalizada do Azure, Deteçã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 através da utilização do Spatial Tracking do Microsoft HoloLens, como uma forma de entender a posição do mundo do objeto reconhecido e, em seguida, usando a Tag associada ao que é detetado 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 enviada.
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 e 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 retreinado, o que aumentará a probabilidade de ele reconhecer objetos do mundo real.
Este curso ensinará como obter os resultados do Serviço de Visão Personalizada do Azure, Deteção de Objetos, em um aplicativo de exemplo baseado em Unity. Caberá a você aplicar esses conceitos a um aplicativo personalizado que você pode estar criando.
Suporte de dispositivos
Curso | HoloLens | Auriculares imersivos |
---|---|---|
MR e Azure 310: Deteção de objetos | ✔️ |
Pré-requisitos
Nota
Este tutorial foi projetado para desenvolvedores que têm experiência básica com Unity e C#. Tenha também em atenção que os pré-requisitos e as instruções escritas contidas 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 neste curso corresponderão perfeitamente ao que você encontrará em software mais recente do que o listado 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 ativado
- O SDK mais recente do Windows 10
- Unidade 2017.4 LTS
- Visual Studio 2017
- Um Microsoft HoloLens com o modo de desenvolvedor ativado
- Acesso à Internet para configuração do Azure e recuperação do Serviço de Visão Personalizada
- É necessária uma série de pelo menos quinze (15) imagens) para cada objeto que você gostaria que a Visão Personalizada reconhecesse. Se desejar, pode usar as imagens já disponibilizadas com este curso, uma série de copos).
Antes de começar
- Para evitar encontrar 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 em tempo de compilação).
- Configure e teste o seu HoloLens. Se você precisar de suporte para isso, visite o artigo de configuraçã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 calibração, siga este link para o artigo Calibração HoloLens.
Para obter ajuda sobre o ajuste do sensor, siga este link para o artigo HoloLens Sensor Tuning.
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.
Inicie sessão no Portal de Visão Personalizada.
Se ainda não tiver uma conta do Azure, terá de criar uma. Se você estiver seguindo este tutorial em uma situação de sala de aula ou laboratório, peça ajuda ao seu instrutor ou a um dos proctors para configurar sua nova conta.
Depois de iniciar sessão pela primeira vez, ser-lhe-á apresentado 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, que solicitará 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).
Defina os tipos de projeto como deteção de objeto (visualização).
Quando terminar, clique em Criar projeto e 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 você gostaria que seu aplicativo reconhecesse. Você pode usar as imagens fornecidas com este curso (uma série de copos).
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 copos para reconhecimento, então nomeamos a tag para isso, Copa. Clique em Salvar depois de concluído.
Você notará que sua tag foi adicionada (talvez seja 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 você gostaria de carregar para um objeto, com o mínimo sendo quinze (15).
Gorjeta
Você pode selecionar várias imagens ao mesmo tempo, para carregar.
Pressione Carregar arquivos depois de selecionar todas as imagens com as quais gostaria de treinar o projeto. Os ficheiros começarão a ser carregados. Assim que tiver a confirmação do carregamento, 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 a imagem, um realce de seleção irá ajudá-lo 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 englobar 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 lâmina ou fechar a folha da marca (clicando no X no canto superior direito da lâmina) e, em seguida, clicar na próxima imagem. Assim que tiver a próxima imagem pronta, repita o mesmo procedimento. Faça isso para todas as imagens que você carregou, até que todas estejam marcadas.
Nota
Você pode selecionar vários objetos na mesma imagem, como a imagem abaixo:
Depois de marcar todos, 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á.
Uma vez que é construído, você será capaz de ver dois botões chamados Make default e Prediction URL. Clique em Tornar padrão primeiro e, em seguida, clique em URL de previsão.
Nota
O ponto de extremidade que é fornecido a partir disso, é definido como qualquer iteração que tenha sido marcada como padrão. Como tal, se mais tarde fizer uma nova iteração e atualizá-la como padrão, não precisará alterar seu código.
Depois de clicar no URL de previsão, abra o bloco de notas e copie e cole o URL (também chamado de ponto de extremidade de previsão) e a chave de previsão do serviço, para que você possa recuperá-lo quando precisar dele mais tarde no código.
Capítulo 3 - Configurar o projeto Unity
O seguinte é uma configuração típica para desenvolver com realidade mista e, como tal, é um bom modelo para outros projetos.
Abra o Unity e clique em Novo.
Agora você precisará fornecer um nome de projeto Unity. Insira CustomVisionObjDetection. Certifique-se de que o tipo de projeto está definido como 3D e defina o Local para algum lugar apropriado para você (lembre-se, mais perto de 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 Scripts Externo para Visual Studio. Feche a janela Preferências .
Em seguida, vá para Configurações de compilação de arquivo > e mude a plataforma para a plataforma universal do Windows e, em seguida, clique no botão Alternar plataforma.
Na mesma janela Configurações de compilação, verifique se o seguinte está definido:
O dispositivo alvo está definido como HoloLens
O tipo de compilação está definido como D3D
O SDK está definido como Instalado mais recente
Versão do Visual Studio está definida como A versão mais recente instalada
Build and Run está definido como Máquina Local
As configurações restantes, em Configurações de compilação, devem ser deixadas como padrão por enquanto.
Na mesma janela Configurações de Compilaçã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 Scripting Runtime deve ser experimental (equivalente ao .NET 4.6), o que acionará a necessidade de reiniciar o Editor.
O back-end de scripts deve ser .NET.
O nível de compatibilidade da API deve ser o .NET 4.6.
Na guia Configurações de publicação , em Recursos, verifique:
InternetClient
Webcam
Perceção Espacial
Mais abaixo no painel, em Configurações XR (encontradas abaixo de Configurações de publicação), marque Realidade Virtual suportada e, em seguida, verifique se o SDK de realidade mista do Windows foi adicionado.
De volta às Configurações de compilação, o Unity C# Projects não está mais acinzentado: marque a caixa de seleção ao lado disso.
Feche a janela Configurações de compilação.
No Editor, clique em Editar>gráficos de configurações>do projeto.
No painel Inspetor, as Configurações gráficas estarão abertas. Role para baixo até ver uma matriz chamada Sempre incluir sombreadores. Adicione um slot aumentando a variável Tamanho em um (neste exemplo, era 8, então fizemos 9). Um novo slot aparecerá, na última posição da matriz, como mostrado abaixo:
No slot, clique no pequeno círculo de destino ao lado do slot para abrir uma lista de sombreadores. Procure o sombreador Legacy Shaders/Transparent/Diffuse e clique duas vezes nele.
Capítulo 4 - Importando o pacote CustomVisionObjDetection Unity
Para este curso, você recebe um pacote de ativos Unity chamado Azure-MR-310.unitypackage.
[DICA] Todos os objetos suportados por 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 Unity.
Você pode encontrar o pacote Azure-MR-310 que precisa baixar aqui.
Com o painel 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 concluir a importação, você notará que as pastas do pacote foram adicionadas à sua pasta Ativos . Esse tipo de estrutura de pastas é típico de um projeto Unity.
A pasta Materiais contém o material utilizado 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 construída pelo Editor Unity e pela compilação UWP.
A pasta Prefabs contém os pré-fabricados contidos na cena. São eles:
- O GazeCursor, o cursor usado no aplicativo. Trabalhará em conjunto com o pré-fabricado SpatialMapping para poder ser colocado em cena em cima de objetos físicos.
- O Label, que é o objeto da 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 criar 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 Projeto, e clique duas vezes em ObjDetectionScene, para carregar a cena que você usará para este curso.
Nota
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 .
Nota
As chamadas para o Serviço de Visão Personalizada, feitas no código mostrado abaixo, são feitas usando a API REST de 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). Esteja ciente 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 Custom Vision SDK.
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 Azure Custom Vision Service para análise.
Recebendo a resposta como uma cadeia de caracteres JSON.
Desserializar a resposta e passar a previsão resultante para a classe SceneOrganiser, que cuidará de como a resposta deve ser exibida.
Para criar esta classe:
Clique com o botão direito do mouse na Pasta de ativos, localizada no Painel do projeto, e clique em Criar>pasta. Chame a pasta Scripts.
Clique duas vezes na pasta recém-criada para abri-la.
Clique com o botão direito do rato dentro da pasta e, em seguida, 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;
Nota
Certifique-se de inserir sua Service Prediction-Key na variável predictionKey e seu Prediction-Endpoint 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 corotina (com o método estático GetImageAsByteArray() abaixo dele), que obterá os resultados da análise da imagem, capturados pela classe ImageCapture .
Nota
Na co-rotina 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, o que corrigirá isso.
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 esta classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Chame o script 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 siga cuidadosamente as instruções seguintes. 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 Collider de Mapeamento Espacial na cena para poder detetar colisões entre objetos virtuais e objetos reais.
Para criar esta 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 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 esta classe:
Clique com o botão direito do mouse dentro da pasta Scripts e clique em Criar>Script C#. Chame o script de 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; } }
Nota
Não se preocupe com o erro para a classe SceneOrganiser não ser encontrado, você irá criá-lo 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 é detetado, ele será responsável por calcular sua posição no mundo real e colocar uma etiqueta de tag perto dele com o nome de tag apropriado.
Para criar esta 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 irá instanciar o rótulo na cena (que neste ponto é invisível para o usuário). Também coloca o quad (também invisível) onde a imagem é colocada, e sobrepõe-se ao mundo real. Isso é importante porque as coordenadas da caixa recuperadas do Serviço após a análise são rastreadas até este 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(). Compete-lhe:
- Definir o texto do Rótulo com a Tag da Previsão com a maior confiança.
- Chamando o cálculo da Caixa Delimitadora no objeto quádruplo, posicionado anteriormente, e colocando o rótulo na cena.
- Ajustar a profundidade da etiqueta usando um Raycast em direção à Caixa Delimitadora, que deve colidir com o objeto no mundo real.
- Redefinir 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 traduzir 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, dentro do 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);
Nota
Não se preocupe com a mensagem 'não foi encontrado' da classe ImageCapture , você a criará no próximo capítulo.
Capítulo 10 - Criar a classe ImageCapture
A próxima classe que você vai criar é a classe ImageCapture .
Esta classe é responsável por:
- Capturar uma imagem usando a câmera HoloLens e armazená-la na pasta Aplicativo .
- Manuseamento Toque em gestos do utilizador.
Para criar esta classe:
Vá para a pasta Scripts que você criou anteriormente.
Clique com o botão direito do rato dentro da pasta e, em seguida, 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âmara está disponível para tirar a imagem. Quando o cursor está vermelho, significa que a câmara está ocupada.
Adicione o método que o aplicativo usa para iniciar o processo de captura de imagem e armazenar 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 tiver sido capturada e para 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 Unity, no Painel Hierarquia, selecione a Câmara Principal.
No Painel do 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 Pré-fabricados, arraste o pré-fabricado Rótulo para a área de entrada de destino de referência vazia do rótulo, no script SceneOrganiser que você acabou de adicionar à câmera principal, conforme mostrado na imagem abaixo:
No Painel Hierarquia, selecione o filho GazeCursor da Câmara Principal.
No Painel do 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âmara Principal.
No Painel Inspetor, com o SpatialMapping selecionado, clique em Add Component, 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 tempo de execução.
Capítulo 12 - Antes de construir
Para executar um teste completo do seu aplicativo, você precisará fazer o sideload dele no Microsoft HoloLens.
Antes de o fazer, 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, Passo 6:
- Certifique-se de inserir sua chave de previsão de serviço na variável predictionKey .
- Você inseriu seu Prediction Endpoint na classe predictionEndpoint .
Capítulo 13 - Criar a solução UWP e fazer sideload do seu 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 compilação de arquivo>.
Marque Unity C# Projects.
Clique em Adicionar cenas abertas. Isso adicionará a cena atualmente aberta à compilação.
Clique em Compilar. Unity irá iniciar uma janela do Explorador de Arquivos, onde você precisa criar e, em seguida, selecionar uma pasta para construir o aplicativo. Crie essa pasta agora e nomeie-a como App. Em seguida, com a pasta App selecionada, clique em Selecionar pasta.
Unity começará a construir seu projeto para a pasta App .
Assim que o Unity terminar de construir (pode levar algum tempo), ele abrirá uma janela do Explorador de Arquivos no local da sua compilação (verifique sua barra de tarefas, pois nem sempre ela aparecerá acima de suas 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 tal:
Enquanto estiver a usar o HoloLens, abra as Definições.
Ir para Rede & Opções Avançadas de Wi-Fi>da Internet>
Observe o endereço IPv4 .
Em seguida, navegue de volta para Configurações e, em seguida, para Atualizar & Segurança>para desenvolvedores
Defina o modo de desenvolvedor ativado.
Navegue até sua nova compilação 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ção, selecione x86, Máquina Remota. Você será solicitado a inserir o endereço IP de um dispositivo remoto (o Microsoft HoloLens, neste caso, que você observou).
Vá para o menu Build e clique em Deploy Solution para fazer sideload do aplicativo para o seu HoloLens.
Seu aplicativo agora deve aparecer na lista de aplicativos instalados no seu Microsoft HoloLens, pronto para ser iniciado!
Para usar o aplicativo:
- Observe um objeto que você treinou com seu Serviço de Visão Personalizada do Azure, Deteção de Objetos e use o gesto Toque.
- Se o objeto for detetado com êxito, um texto de rótulo de espaço mundial aparecerá com o nome da marca.
Importante
Toda vez que você capturar uma foto e enviá-la para o Serviço, poderá voltar para a página 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 ser mais preciso e treinar novamente o Serviço.
Nota
O Texto do rótulo colocado pode não aparecer perto do objeto quando os sensores 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 esse for o caso.
A sua visão personalizada, aplicação de deteção de objetos
Parabéns, você criou um aplicativo de realidade mista que aproveita a Visão Personalizada do Azure, API de Deteçã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
Reproduzir um som quando um objeto é 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 previsão e treinamento simultaneamente).