HoloLens (1.ª generación) y Azure 310: Detección de objetos
Nota:
Los tutoriales de Mixed Reality Academy se han diseñado teniendo en cuenta HoloLens (1.ª generación) y los cascos envolventes de realidad mixta. Por lo tanto, creemos que es importante conservar estos tutoriales para los desarrolladores que sigan buscando instrucciones sobre el desarrollo para esos dispositivos. Estos tutoriales no se actualizarán con los conjuntos de herramientas o las interacciones más recientes que se usan para HoloLens 2. Se mantendrán para que sigan funcionando en los dispositivos compatibles. Habrá una nueva serie de tutoriales que se publicarán en el futuro que demostrarán cómo desarrollar para HoloLens 2. Este aviso se actualizará con un vínculo a esos tutoriales cuando se publiquen.
En este curso, aprenderá a reconocer el contenido visual personalizado y su posición espacial dentro de una imagen proporcionada mediante las funcionalidades de "Detección de objetos" de Azure Custom Vision en una aplicación de realidad mixta.
Este servicio le permitirá entrenar un modelo de Machine Learning mediante imágenes de objetos. Después, usará el modelo entrenado para reconocer objetos similares y aproximarse a su ubicación en el mundo real, tal como lo proporciona la captura de cámara de Microsoft HoloLens o una cámara que se conecta a un EQUIPO para cascos envolventes (VR).
Azure Custom Vision, Detección de objetos es un servicio de Microsoft que permite a los desarrolladores crear clasificadores de imágenes personalizados. A continuación, estos clasificadores se pueden usar con nuevas imágenes para detectar objetos dentro de esa nueva imagen, proporcionando límites de cuadro dentro de la propia imagen. El servicio proporciona un portal en línea sencillo y fácil de usar para simplificar este proceso. Para más información, visite los vínculos siguientes:
Tras completar este curso, tendrá una aplicación de realidad mixta que podrá hacer lo siguiente:
- El usuario podrá mirar un objeto, que ha entrenado mediante Azure Custom Vision Service, Detección de objetos.
- El usuario usará el gesto De pulsar para capturar una imagen de lo que está mirando.
- La aplicación enviará la imagen a Azure Custom Vision Service.
- Habrá una respuesta del Servicio que mostrará el resultado del reconocimiento como texto de espacio mundial. Esto se logrará mediante el uso del seguimiento espacial de Microsoft HoloLens, como una manera de comprender la posición mundial del objeto reconocido y, a continuación, usar la etiqueta asociada a lo que se detecta en la imagen, para proporcionar el texto de la etiqueta.
El curso también tratará la carga manual de imágenes, la creación de etiquetas y el entrenamiento del servicio para reconocer objetos diferentes (en el ejemplo proporcionado, una taza) estableciendo el cuadro límite dentro de la imagen que envíe.
Importante
Después de la creación y el uso de la aplicación, el desarrollador debe volver a Azure Custom Vision Service e identificar las predicciones realizadas por el servicio y determinar si eran correctas o no (mediante el etiquetado de todo lo que el servicio ha perdido y ajustar los cuadros de límite). A continuación, se puede volver a entrenar el servicio, lo que aumentará la probabilidad de que reconozca objetos del mundo real.
Este curso le enseñará a obtener los resultados de Azure Custom Vision Service, Detección de objetos, en una aplicación de ejemplo basada en Unity. Dependerá de usted aplicar estos conceptos a una aplicación personalizada que pueda compilar.
Compatibilidad con dispositivos
Curso | HoloLens | Cascos envolventes |
---|---|---|
MR y Azure 310: detección de objetos | ✔️ |
Requisitos previos
Nota:
Este tutorial está diseñado para desarrolladores que tienen experiencia básica con Unity y C#. Tenga en cuenta también que los requisitos previos y las instrucciones escritas de este documento representan lo que se ha probado y comprobado en el momento de redactarlo (julio de 2018). Puede usar el software más reciente, como se muestra en el artículo de instalación de las herramientas , aunque no debe asumirse que la información de este curso coincidirá perfectamente con lo que encontrará en el software más reciente que lo que se muestra a continuación.
Se recomienda el siguiente hardware y software para este curso:
- Un equipo de desarrollo
- Windows 10 Fall Creators Update (o posterior) con el modo desarrollador habilitado
- El SDK de Windows 10 más reciente
- Unity 2017.4 LTS
- Visual Studio 2017
- Microsoft HoloLens con el modo de desarrollador habilitado
- Acceso a Internet para la configuración de Azure y la recuperación de Custom Vision Service
- Se requiere una serie de al menos quince (15) imágenes) para cada objeto que desee que custom Vision reconozca. Si lo desea, puede usar las imágenes ya proporcionadas con este curso, una serie de tazas).
Antes de comenzar
- Para evitar encontrar problemas al compilar este proyecto, se recomienda encarecidamente crear el proyecto mencionado en este tutorial en una carpeta raíz o casi raíz (las rutas de acceso de carpeta largas pueden causar problemas en tiempo de compilación).
- Configure y pruebe holoLens. Si necesita soporte técnico para esto, visite el artículo configuración de HoloLens.
- Es una buena idea realizar calibración y ajuste del sensor al empezar a desarrollar una nueva aplicación de HoloLens (a veces puede ayudar a realizar esas tareas para cada usuario).
Para obtener ayuda sobre calibración, siga este vínculo al artículo Calibración de HoloLens.
Para obtener ayuda sobre la optimización del sensor, siga este vínculo al artículo Optimización de sensores de HoloLens.
Capítulo 1: El portal de Custom Vision
Para usar Azure Custom Vision Service, deberá configurar una instancia de ella para que esté disponible para la aplicación.
Vaya a la página principal de Custom Vision Service.
Haga clic en Introducción.
Inicie sesión en Custom Vision Portal.
Si aún no tiene una cuenta de Azure, deberá crear una. Si sigue este tutorial en una situación de clase o laboratorio, pida a su instructor o a uno de los proctores que le ayuden a configurar la nueva cuenta.
Una vez que haya iniciado sesión por primera vez, se le pedirá el panel Términos de servicio . Haga clic en la casilla para aceptar los términos. A continuación, haga clic en Acepto.
Después de haber aceptado los términos, ahora está en la sección Mis proyectos . Haz clic en Nuevo proyecto.
Aparecerá una pestaña en el lado derecho, que le pedirá que especifique algunos campos para el proyecto.
Insertar un nombre para el proyecto
Insertar una descripción para el proyecto (opcional)
Elija un grupo de recursos o cree uno nuevo. Un grupo de recursos proporciona una manera de supervisar, controlar el acceso, aprovisionar y administrar la facturación de una colección de recursos de Azure. Se recomienda mantener todos los servicios de Azure asociados a un único proyecto (por ejemplo, estos cursos) en un grupo de recursos común).
Establezca los tipos de proyecto como detección de objetos (versión preliminar).
Una vez que haya terminado, haga clic en Crear proyecto y se le redirigirá a la página del proyecto de Custom Vision Service.
Capítulo 2: Entrenamiento del proyecto de Custom Vision
Una vez en Custom Vision Portal, el objetivo principal es entrenar el proyecto para reconocer objetos específicos en imágenes.
Necesita al menos quince (15) imágenes para cada objeto que quiera que la aplicación reconozca. Puede usar las imágenes proporcionadas con este curso (una serie de tazas).
Para entrenar el proyecto de Custom Vision:
Haga clic en el + botón situado junto a Etiquetas.
Agregue un nombre para la etiqueta a la que se usará para asociar las imágenes. En este ejemplo se usan imágenes de tazas para el reconocimiento, por lo que se ha llamado la etiqueta para este, Cup. Haga clic en Guardar una vez finalizada.
Observará que se ha agregado la etiqueta (es posible que tenga que volver a cargar la página para que aparezca).
Haga clic en Agregar imágenes en el centro de la página.
Haga clic en Examinar archivos locales y vaya a las imágenes que desea cargar para un objeto, con el mínimo de quince (15).
Sugerencia
Puede seleccionar varias imágenes a la vez para cargarlas.
Presione Cargar archivos una vez que haya seleccionado todas las imágenes con las que desea entrenar el proyecto. Los archivos comenzarán a cargarse. Una vez que haya confirmado la carga, haga clic en Listo.
En este momento, las imágenes se cargan, pero no se etiquetan.
Para etiquetar las imágenes, use el mouse. Al mantener el puntero sobre la imagen, un resaltado de selección le ayudará a dibujar automáticamente una selección alrededor del objeto. Si no es preciso, puede dibujar su propio. Esto se logra manteniendo presionado el clic izquierdo en el mouse y arrastrando la región de selección para abarcar el objeto.
Después de la selección del objeto dentro de la imagen, un pequeño aviso le pedirá que agregue la etiqueta de región. Seleccione la etiqueta creada anteriormente ("Cup", en el ejemplo anterior), o si va a agregar más etiquetas, escriba esa etiqueta y haga clic en el botón + (más).
Para etiquetar la siguiente imagen, puede hacer clic en la flecha situada a la derecha de la hoja o cerrar la hoja de etiqueta (haciendo clic en la X en la esquina superior derecha de la hoja) y, a continuación, hacer clic en la siguiente imagen. Una vez que tenga lista la siguiente imagen, repita el mismo procedimiento. Haz esto para todas las imágenes que hayas cargado, hasta que estén etiquetadas.
Nota:
Puede seleccionar varios objetos en la misma imagen, como la imagen siguiente:
Una vez que los haya etiquetado todo, haga clic en el botón etiquetado , a la izquierda de la pantalla, para mostrar las imágenes etiquetadas.
Ya está listo para entrenar su servicio. Haga clic en el botón Entrenar y comenzará la primera iteración de entrenamiento.
Una vez compilado, podrá ver dos botones denominados Make default (Establecer dirección URL predeterminada ) y Prediction URL (Dirección URL de predicción). Haga clic en Convertir el valor predeterminado en primer lugar y, a continuación, haga clic en Url de predicción.
Nota:
El punto de conexión que se proporciona a partir de este, se establece en cualquier iteración que se haya marcado como predeterminada. Por lo tanto, si más adelante realiza una nueva iteración y la actualiza como predeterminada, no tendrá que cambiar el código.
Una vez que haya hecho clic en Url de predicción, abra el Bloc de notas y copie y pegue la dirección URL (también denominada Prediction-Endpoint) y la clave de predicción del servicio, para que pueda recuperarla cuando la necesite más adelante en el código.
Capítulo 3: Configuración del proyecto de Unity
A continuación se muestra una configuración típica para desarrollar con realidad mixta y, como tal, es una buena plantilla para otros proyectos.
Abra Unity y haga clic en Nuevo.
Ahora deberá proporcionar un nombre de proyecto de Unity. Inserte CustomVisionObjDetection. Asegúrese de que el tipo de proyecto esté establecido en 3D y establezca la ubicación en algún lugar adecuado para usted (recuerde que más cerca de los directorios raíz es mejor). A continuación, haga clic en Crear proyecto.
Con Unity abierto, vale la pena comprobar que el Editor de scripts predeterminado está establecido en Visual Studio. Vaya a Editar>preferencias y, a continuación, en la nueva ventana, vaya a Herramientas externas. Cambie el Editor de scripts externos a Visual Studio. Cierre la ventana Preferencias.
A continuación, vaya a Configuración de compilación de archivos > y cambie la plataforma a Plataforma universal de Windows y, a continuación, haga clic en el botón Cambiar plataforma.
En la misma ventana Configuración de compilación, asegúrese de que se establecen las siguientes opciones:
El dispositivo de destino está establecido en HoloLens
Tipo de compilación se establece en D3D
El SDK se establece en Latest installed (Versión más reciente instalada)
La versión de Visual Studio se establece en Latest installed (Versión más reciente instalada)
Build and Run (Compilar y ejecutar ) está establecido en Equipo local
La configuración restante, en Configuración de compilación, debe dejarse como predeterminada por ahora.
En la misma ventana Configuración de compilación, haga clic en el botón Configuración del reproductor; se abrirá el panel relacionado en el espacio donde se encuentra el Inspector.
En este panel, es necesario comprobar algunos valores:
En la pestaña Otros valores :
La versión del entorno de ejecución de scripting debe ser experimental (equivalente a.NET 4.6), lo que desencadenará la necesidad de reiniciar el editor.
El back-end de scripting debe ser .NET.
El nivel de compatibilidad de API debe ser .NET 4.6.
En la pestaña Configuración de publicación, en Funcionalidades, active:
InternetClient
Cámara web
SpatialPerception
Más abajo en el panel, en Configuración de XR (que se encuentra a continuación de Configuración de publicación), marque Virtual Reality Supported (Compatible con la realidad virtual) y asegúrese de que se agrega el SDK de Windows Mixed Reality.
De nuevo en Configuración de compilación, los proyectos de C# de Unity ya no están atenuados: marque la casilla situada junto a esto.
Cierre la ventana Build Settings (Configuración de compilación).
En el Editor, haga clic en Editar>gráficos de configuración>del proyecto.
En el Panel inspector , se abrirá la configuración de gráficos. Desplácese hacia abajo hasta que vea una matriz denominada Sombreadores de inclusión always. Agregue una ranura aumentando la variable Size por una (en este ejemplo, era 8, por lo que lo hicimos 9). Aparecerá una nueva ranura, en la última posición de la matriz, como se muestra a continuación:
En la ranura, haga clic en el círculo de destino pequeño junto a la ranura para abrir una lista de sombreadores. Busque sombreadores heredados/ Sombreador transparente/difuso y haga doble clic en él.
Capítulo 4: Importación del paquete customVisionObjDetection de Unity
Para este curso, se le proporciona un paquete de recursos de Unity denominado Azure-MR-310.unitypackage.
[SUGERENCIA] Los objetos admitidos por Unity, incluidas las escenas completas, se pueden empaquetar en un archivo .unitypackage y exportarse o importarse en otros proyectos. Es la manera más segura y eficaz de mover recursos entre diferentes proyectos de Unity.
Puede encontrar el paquete Azure-MR-310 que debe descargar aquí.
Con el panel de Unity delante de usted, haga clic en Activos en el menú de la parte superior de la pantalla y, a continuación, haga clic en Importar paquete >personalizado.
Use el selector de archivos para seleccionar el paquete Azure-MR-310.unitypackage y haga clic en Abrir. Se mostrará una lista de componentes para este recurso. Para confirmar la importación, haga clic en el botón Importar .
Una vez que haya terminado de importarse, observará que las carpetas del paquete se han agregado ahora a la carpeta Assets . Este tipo de estructura de carpetas es típico de un proyecto de Unity.
La carpeta Materiales contiene el material utilizado por el Cursor de mirada.
La carpeta Plugins contiene el archivo DLL newtonsoft usado por el código para deserializar la respuesta web del servicio. Las dos (2) versiones diferentes contenidas en la carpeta y la subcarpeta son necesarias para permitir que la biblioteca la usen y compilen tanto el Editor de Unity como la compilación para UWP.
La carpeta Prefabs contiene los objetos prefabricados contenidos en la escena. Estos son:
- GazeCursor, el cursor usado en la aplicación. Funcionará junto con el objeto prefabricado SpatialMapping para poder colocarse en la escena sobre objetos físicos.
- Etiqueta, que es el objeto de interfaz de usuario que se usa para mostrar la etiqueta de objeto en la escena cuando sea necesario.
- SpatialMapping, que es el objeto que permite a la aplicación usar la creación de un mapa virtual, mediante el seguimiento espacial de Microsoft HoloLens.
La carpeta Escenas que contiene actualmente la escena pregenerada para este curso.
Abra la carpeta Escenas, en el Panel del proyecto y haga doble clic en ObjDetectionScene para cargar la escena que usará para este curso.
Nota:
No se incluye ningún código, escribirá el código siguiendo este curso.
Capítulo 5: Creación de la clase CustomVisionAnalyser.
En este momento está listo para escribir código. Comenzará con la clase CustomVisionAnalyser .
Nota:
Las llamadas a Custom Vision Service, realizadas en el código que se muestra a continuación, se realizan mediante la API rest de Custom Vision. Mediante este uso, verá cómo implementar y usar esta API (útil para comprender cómo implementar algo similar por su cuenta). Tenga en cuenta que Microsoft ofrece un SDK de Custom Vision que también se puede usar para realizar llamadas al servicio. Para más información, visite el artículo sdk de Custom Vision.
Esta clase es responsable de:
Cargando la imagen más reciente capturada como una matriz de bytes.
Envío de la matriz de bytes a la instancia de Azure Custom Vision Service para su análisis.
Recepción de la respuesta como una cadena JSON.
Deserializar la respuesta y pasar la predicción resultante a la clase SceneOrganiser, que se encargará de cómo se debe mostrar la respuesta.
Para crear esta clase:
Haga clic con el botón derecho en la carpeta de recursos, que se encuentra en el Panel del proyecto y, a continuación, haga clic en Crear>carpeta. Llame a la carpeta Scripts.
Haga doble clic en la carpeta recién creada para abrirla.
Haga clic con el botón derecho en la carpeta y, a continuación, haga clic en Crear>script de C#. Asigne al script el nombre CustomVisionAnalyser.
Haga doble clic en el nuevo script CustomVisionAnalyser para abrirlo con Visual Studio.
Asegúrese de que tiene los siguientes espacios de nombres a los que se hace referencia en la parte superior del archivo:
using Newtonsoft.Json; using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking;
En la clase CustomVisionAnalyser , agregue las siguientes variables:
/// <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:
Asegúrese de insertar la clave de predicción del servicio en la variable predictionKey y prediction-Endpoint en la variable predictionEndpoint . Los copió en el Bloc de notas anteriormente, en el capítulo 2, paso 14.
Ahora es necesario agregar código para Awake() para inicializar la variable Instance:
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; }
Agregue la corrutina (con el método estático GetImageAsByteArray() debajo de él), que obtendrá los resultados del análisis de la imagen, capturados por la clase ImageCapture .
Nota:
En la corrutina AnalyseImageCapture , hay una llamada a la clase SceneOrganiser que todavía tiene que crear. Por lo tanto, deje esas líneas comentadas por ahora.
/// <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); }
Elimine los métodos Start() y Update(), ya que no se usarán.
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Importante
Como se mencionó anteriormente, no se preocupe por el código que podría parecer tener un error, ya que proporcionará más clases pronto, lo que corregirá estos.
Capítulo 6: Creación de la clase CustomVisionObjects
La clase que va a crear ahora es la clase CustomVisionObjects .
Este script contiene una serie de objetos usados por otras clases para serializar y deserializar las llamadas realizadas a Custom Vision Service.
Para crear esta clase:
Haga clic con el botón derecho en la carpeta Scripts y, a continuación, haga clic en Crear>script de C#. Llame al script CustomVisionObjects.
Haga doble clic en el nuevo script CustomVisionObjects para abrirlo con Visual Studio.
Asegúrese de que tiene los siguientes espacios de nombres a los que se hace referencia en la parte superior del archivo:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking;
Elimine los métodos Start() y Update() dentro de la clase CustomVisionObjects ; esta clase debería estar vacía.
Advertencia
Es importante que siga cuidadosamente la siguiente instrucción. Si coloca las nuevas declaraciones de clase dentro de la clase CustomVisionObjects , obtendrá errores de compilación en el capítulo 10, indicando que no se encuentran AnalysisRootObject y BoundingBox .
Agregue las siguientes clases fuera de la clase CustomVisionObjects. La biblioteca Newtonsoft usa estos objetos para serializar y deserializar los datos de respuesta:
// 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; } }
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Capítulo 7: Creación de la clase SpatialMapping
Esta clase establecerá el colisionador de asignación espacial en la escena para poder detectar colisiones entre objetos virtuales y objetos reales.
Para crear esta clase:
Haga clic con el botón derecho en la carpeta Scripts y, a continuación, haga clic en Crear>script de C#. Llame al script SpatialMapping.
Haga doble clic en el nuevo script SpatialMapping para abrirlo con Visual Studio.
Asegúrese de que tiene los siguientes espacios de nombres a los que se hace referencia encima de la clase SpatialMapping :
using UnityEngine; using UnityEngine.XR.WSA;
A continuación, agregue las siguientes variables dentro de la clase SpatialMapping, encima del 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;
Agregue El valor de Awake() y 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); }
Elimine el método Update().
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Capítulo 8: Creación de la clase GazeCursor
Esta clase es responsable de configurar el cursor en la ubicación correcta en el espacio real, haciendo uso de SpatialMappingCollider, creado en el capítulo anterior.
Para crear esta clase:
Haga clic con el botón derecho en la carpeta Scripts y, a continuación, haga clic en Crear>script de C#. Llamada al script GazeCursor
Haga doble clic en el nuevo script GazeCursor para abrirlo con Visual Studio.
Asegúrese de que tiene el siguiente espacio de nombres al que se hace referencia encima de la clase GazeCursor :
using UnityEngine;
A continuación, agregue la siguiente variable dentro de la clase GazeCursor, encima del método Start().
/// <summary> /// The cursor (this object) mesh renderer /// </summary> private MeshRenderer meshRenderer;
Actualice el método Start() con el código siguiente:
/// <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); }
Actualice el método Update() con el código siguiente:
/// <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:
No se preocupe por el error de la clase SceneOrganiser que no se encuentra, lo creará en el capítulo siguiente.
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Capítulo 9: Creación de la clase SceneOrganiser
Esta clase:
Configure la cámara principal mediante la conexión de los componentes adecuados.
Cuando se detecta un objeto, será responsable de calcular su posición en el mundo real y colocar una etiqueta etiqueta cerca de él con el nombre de etiqueta adecuado.
Para crear esta clase:
Haga clic con el botón derecho en la carpeta Scripts y, a continuación, haga clic en Crear>script de C#. Asigne al script el nombre SceneOrganiser.
Haga doble clic en el nuevo script SceneOrganiser para abrirlo con Visual Studio.
Asegúrese de que tiene los siguientes espacios de nombres a los que se hace referencia encima de la clase SceneOrganiser :
using System.Collections.Generic; using System.Linq; using UnityEngine;
A continuación, agregue las siguientes variables dentro de la clase SceneOrganiser, encima del 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;
Elimine los métodos Start() y Update().
Debajo de las variables, agregue el método Awake(), que inicializará la clase y configurará la escena.
/// <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>(); }
Agregue el método PlaceAnalysisLabel(), que creará una instancia de la etiqueta en la escena (que en este momento es invisible para el usuario). También coloca el quad (también invisible) donde se coloca la imagen y se superpone con el mundo real. Esto es importante porque las coordenadas de cuadro recuperadas del servicio después de realizar el análisis se rastrean de nuevo en este quad para determinar la ubicación aproximada del objeto en el 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; }
Agregue el método FinaliseLabel(). Es el responsable de:
- Establecer el texto Etiqueta con la etiqueta de la predicción con la mayor confianza.
- Llamar al cálculo del rectángulo delimitador en el objeto quad, colocado anteriormente y colocando la etiqueta en la escena.
- Ajuste de la profundidad de la etiqueta mediante un Raycast hacia el rectángulo delimitador, que debe colisionar contra el objeto en el mundo real.
- Restablecer el proceso de captura para permitir que el usuario capture otra imagen.
/// <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(); }
Agregue el método CalculateBoundingBoxPosition(), que hospeda una serie de cálculos necesarios para traducir las coordenadas de Bounding Box recuperadas del servicio y volver a crearlas proporcionalmente en el 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); }
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Importante
Antes de continuar, abra la clase CustomVisionAnalyser y, en el método AnalysisLastImageCaptured(), quite la marca de comentario de las líneas siguientes:
// 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:
No se preocupe por el mensaje "No se encontró" de la clase ImageCapture , lo creará en el capítulo siguiente.
Capítulo 10: Creación de la clase ImageCapture
La siguiente clase que va a crear es la clase ImageCapture .
Esta clase es responsable de:
- Capturar una imagen con la cámara HoloLens y almacenarla en la carpeta Aplicación .
- Control de gestos de pulsación del usuario.
Para crear esta clase:
Vaya a la carpeta Scripts que creó anteriormente.
Haga clic con el botón derecho en la carpeta y, a continuación, haga clic en Crear>script de C#. Asigne al script el nombre ImageCapture.
Haga doble clic en el nuevo script ImageCapture para abrirlo con Visual Studio.
Reemplace los espacios de nombres en la parte superior del archivo por lo siguiente:
using System; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.XR.WSA.Input; using UnityEngine.XR.WSA.WebCam;
A continuación, agregue las siguientes variables dentro de la clase ImageCapture, encima del 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;
Ahora es necesario agregar código para los métodos Awake() e Start():
/// <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 un controlador al que se llamará cuando se produzca un gesto de pulsar:
/// <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
Cuando el cursor es verde, significa que la cámara está disponible para tomar la imagen. Cuando el cursor es rojo, significa que la cámara está ocupada.
Agregue el método que usa la aplicación para iniciar el proceso de captura de imágenes y almacenar la imagen:
/// <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); }); }); }
Agregue los controladores a los que se llamará cuando se haya capturado la foto y para cuando esté listo para analizarse. El resultado se pasa a CustomVisionAnalyser para su análisis.
/// <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(); }
Asegúrese de guardar los cambios en Visual Studio antes de volver a Unity.
Capítulo 11: Configuración de los scripts en la escena
Ahora que ha escrito todo el código necesario para este proyecto, es el momento de configurar los scripts en la escena y, en los objetos prefabricados, para que se comporten correctamente.
En el Editor de Unity, en el Panel jerarquía, seleccione la cámara principal.
En el Panel inspector, con la cámara principal seleccionada, haga clic en Agregar componente y busque Script SceneOrganiser y haga doble clic para agregarlo.
En el Panel de proyectos, abra la carpeta Prefabs, arrastre el objeto prefabricado Label al área De entrada de destino de referencia vacía Label, en el script SceneOrganiser que acaba de agregar a la cámara principal, como se muestra en la imagen siguiente:
En el panel jerarquía, seleccione el elemento secundario GazeCursor de la cámara principal.
En el Panel inspector, con la opción GazeCursor seleccionada, haga clic en Agregar componente y busque script GazeCursor y haga doble clic para agregarlo.
De nuevo, en el Panel de jerarquía, seleccione el elemento secundario SpatialMapping de la cámara principal.
En el Panel inspector, con spatialMapping seleccionado, haga clic en Agregar componente y busque script SpatialMapping y haga doble clic para agregarlo.
El código del script SceneOrganiser agregará los scripts restantes que no haya establecido durante el tiempo de ejecución.
Capítulo 12: Antes de la construcción
Para realizar una prueba exhaustiva de la aplicación, deberá transferirla localmente a Microsoft HoloLens.
Antes de hacerlo, asegúrese de que:
Todas las configuraciones mencionadas en el capítulo 3 se establecen correctamente.
El script SceneOrganiser está asociado al objeto Main Camera .
El script GazeCursor está asociado al objeto GazeCursor .
El script SpatialMapping está asociado al objeto SpatialMapping.
En el capítulo 5, paso 6:
- Asegúrese de insertar la clave de predicción del servicio en la variable predictionKey .
- Ha insertado el punto de conexión de predicción en la clase predictionEndpoint .
Capítulo 13: Compilar la solución para UWP y transferir localmente la aplicación
Ya estás listo para compilar la aplicación como una solución para UWP que podrás implementar en Microsoft HoloLens. Para comenzar el proceso de compilación:
Vaya a Configuración de compilación de archivos>.
Marque Proyectos de C# de Unity.
Haga clic en Agregar escenas abiertas. Esto agregará la escena abierta actualmente a la compilación.
Haga clic en Generar. Unity iniciará una ventana de Explorador de archivos, donde debe crear y, a continuación, seleccionará una carpeta para compilar la aplicación. Cree esa carpeta ahora y asígnela el nombre App. A continuación, con la carpeta Aplicación seleccionada, haga clic en Seleccionar carpeta.
Unity comenzará a compilar el proyecto en la carpeta Aplicación .
Una vez que Unity haya terminado de compilar (puede tardar algún tiempo), se abrirá una ventana de Explorador de archivos en la ubicación de la compilación (compruebe la barra de tareas, ya que puede que no siempre aparezca encima de las ventanas, pero le notificará la adición de una nueva ventana).
Para realizar la implementación en Microsoft HoloLens, necesitará la dirección IP de ese dispositivo (para la implementación remota) y para asegurarse de que también tiene establecido el modo de desarrollador. Para ello, siga estos pasos:
Mientras llevas tu HoloLens, abre la configuración.
Vaya a Opciones avanzadas de Red e Internet>Wi-Fi>
Anote la dirección IPv4 .
A continuación, vuelva a Configuración y, a continuación, a Actualizar y seguridad>para desarrolladores.
Establezca el modo de desarrollador activado.
Vaya a la nueva compilación de Unity (la carpeta Aplicación ) y abra el archivo de solución con Visual Studio.
En Configuración de la solución, seleccione Depurar.
En la Plataforma de soluciones, seleccione x86, Máquina remota. Se le pedirá que inserte la dirección IP de un dispositivo remoto (Microsoft HoloLens, en este caso, que anotó).
Vaya al menú Compilar y haga clic en Implementar solución para transferir localmente la aplicación a HoloLens.
La aplicación debería aparecer ahora en la lista de aplicaciones instaladas en Microsoft HoloLens, lista para iniciarse.
Para usar la aplicación:
- Examine un objeto, que ha entrenado con Azure Custom Vision Service, Detección de objetos y use el gesto De pulsar.
- Si el objeto se detecta correctamente, aparecerá un texto de etiqueta de espacio mundial con el nombre de etiqueta.
Importante
Cada vez que capture una foto y la envíe al servicio, puede volver a la página Servicio y volver a entrenar el servicio con las imágenes recién capturadas. Al principio, probablemente también tendrá que corregir los cuadros de límite para ser más precisos y volver a entrenar el servicio.
Nota:
Es posible que el texto de etiqueta colocado no aparezca cerca del objeto cuando los sensores de Microsoft HoloLens o SpatialTrackingComponent en Unity no pueden colocar los colisionadores adecuados, en relación con los objetos del mundo real. Intente usar la aplicación en una superficie diferente si es así.
La aplicación Custom Vision, detección de objetos
Enhorabuena, ha creado una aplicación de realidad mixta que aprovecha Azure Custom Vision, la API de detección de objetos, que puede reconocer un objeto de una imagen y, a continuación, proporcionar una posición aproximada para ese objeto en espacio 3D.
Ejercicios extra
Ejercicio 1
Agregar a la etiqueta de texto, use un cubo semitransparente para encapsular el objeto real en un cuadro de límite 3D.
Ejercicio 2
Entrene a Custom Vision Service para reconocer más objetos.
Ejercicio 3
Reproducir un sonido cuando se reconoce un objeto.
Ejercicio 4:
Use la API para volver a entrenar el servicio con las mismas imágenes que está analizando la aplicación, por lo que para que el servicio sea más preciso (realice la predicción y el entrenamiento simultáneamente).