HoloLens (1-го поколения) и Azure 310: обнаружение объектов
Примечание.
Руководства Mixed Reality Academy были разработаны для иммерсивных гарнитур HoloLens (1-го поколения) и иммерсивных гарнитур Mixed Reality. Поэтому мы считаем, что важно оставить эти руководства для разработчиков, которые ищут рекомендации по разработке для этих устройств. Данные руководства не будут обновляться с учетом последних наборов инструментов или возможностей взаимодействия для HoloLens 2. Они будут сохранены для работы на поддерживаемых устройствах. В будущем будет появиться новая серия учебников, которые будут размещены в будущем, которые продемонстрировали, как разрабатывать для HoloLens 2. Это уведомление будет обновлено со ссылкой на эти учебники при публикации.
В этом курсе вы узнаете, как распознать пользовательское визуальное содержимое и его пространственное положение в предоставленном изображении с помощью возможностей Azure Пользовательское визуальное распознавание "Обнаружение объектов" в приложении смешанной реальности.
Эта служба позволяет обучить модель машинного обучения с помощью изображений объектов. Затем вы будете использовать обученную модель для распознавания аналогичных объектов и приближения их расположения в реальном мире, как обеспечивается камерой захвата Microsoft HoloLens или камеры подключения к компьютеру для иммерсивных гарнитур (VR).
Пользовательское визуальное распознавание Azure обнаружение объектов — это служба Майкрософт, которая позволяет разработчикам создавать пользовательские классификаторы изображений. Затем эти классификаторы можно использовать с новыми изображениями для обнаружения объектов в новом изображении, предоставляя границы поля в самом изображении. Служба предоставляет простой, простой и простой портал для упрощения этого процесса. Дополнительные сведения см. по следующим ссылкам:
После завершения этого курса у вас будет приложение смешанной реальности, которое сможет выполнить следующие действия:
- Пользователь сможет посмотреть на объект, который они обучены с помощью службы Azure Пользовательское визуальное распознавание, обнаружения объектов.
- Пользователь будет использовать жест Tap для записи изображения того, что они смотрят.
- Приложение отправит изображение в службу Пользовательское визуальное распознавание Azure.
- В службе будет ответ, который будет отображать результат распознавания в виде текста мирового пространства. Это будет сделано путем использования пространственного отслеживания Microsoft HoloLens в качестве способа понимания положения распознанного объекта, а затем использования тега , связанного с обнаруженным в изображении, для предоставления текста метки.
Курс также будет охватывать вручную отправку изображений, создание тегов и обучение службы для распознавания различных объектов (в приведенном примере чашки), задав поле границы в отправленном изображении.
Внимание
После создания и использования приложения разработчик должен вернуться в службу Azure Пользовательское визуальное распознавание и определить прогнозы, сделанные службой, и определить, правильно ли они были (путем добавления тегов, пропущенных служб и настройки ограничивающих прямоугольниц). Затем служба может быть переобучена, что повысит вероятность его распознавания реальных объектов.
В этом курсе вы узнаете, как получить результаты из службы Пользовательское визуальное распознавание Azure, обнаружения объектов в примере приложения на основе Unity. Это будет до вас, чтобы применить эти понятия к пользовательскому приложению, которое вы можете создать.
Поддержка устройств
Курс | HoloLens | Иммерсивные гарнитуры |
---|---|---|
MR и Azure 310: обнаружение объектов | ✔️ |
Предварительные требования
Примечание.
Это руководство предназначено для разработчиков, имеющих базовый опыт работы с Unity и C#. Также помните, что предварительные требования и письменные инструкции в этом документе представляют тестируемые и проверенные на момент написания статьи (июль 2018 г.). Вы можете использовать последнее программное обеспечение, как указано в статье об установке инструментов , хотя не следует предполагать, что информация в этом курсе будет идеально соответствовать тому, что вы найдете в новом программном обеспечении, чем указано ниже.
Для этого курса рекомендуется использовать следующее оборудование и программное обеспечение:
- Компьютер разработки
- Windows 10 Fall Creators Update (или более поздней версии) с включенным режимом разработчика
- Последний пакет SDK для Windows 10
- Unity 2017.4 LTS
- Visual Studio 2017
- Microsoft HoloLens с включенным режимом разработчика
- Доступ к Интернету для установки Azure и получения Пользовательское визуальное распознавание службы
- Для каждого объекта требуется по крайней мере пятнадцать изображений (по крайней мере 15) для каждого объекта, который требуется распознать Пользовательское визуальное распознавание. Если вы хотите, вы можете использовать изображения, уже предоставленные этим курсом, ряд кубков).
Перед началом работы
- Чтобы избежать проблем с сборкой этого проекта, настоятельно рекомендуется создать проект, упомянутый в этом руководстве, в корневой или почти корневой папке (длинные пути к папкам могут вызвать проблемы во время сборки).
- Настройте и проверьте HoloLens. Если вам нужна поддержка этого, посетите статью о настройке HoloLens.
- Рекомендуется выполнять калибровку и настройку датчика при разработке нового приложения HoloLens (иногда это может помочь выполнить эти задачи для каждого пользователя).
Дополнительные сведения о калибровке см. по этой ссылке в статье о калибровке HoloLens.
Дополнительные сведения о настройке датчика см. по этой ссылке в статье по настройке датчика HoloLens.
Глава 1. Портал Пользовательское визуальное распознавание
Чтобы использовать службу Azure Пользовательское визуальное распознавание, необходимо настроить экземпляр приложения.
Перейдите на главную страницу службы Пользовательское визуальное распознавание.
Нажмите кнопку "Начало работы".
Войдите на портал Пользовательское визуальное распознавание.
Если у вас еще нет учетной записи Azure, необходимо создать ее. Если вы используете это руководство в классе или лаборатории, попросите преподавателя или одного из прокторов, чтобы помочь настроить новую учетную запись.
После первого входа в систему появится запрос на панели условий обслуживания . Установите флажок, чтобы согласиться с условиями. Затем нажмите кнопку "Я согласен".
Согласившись с условиями, вы находитесь в разделе "Мои проекты ". Щелкните новый проект.
На правой стороне появится вкладка, которая предложит указать некоторые поля для проекта.
Вставка имени проекта
Вставка описания проекта (необязательно)
Выберите группу ресурсов или создайте новую. Группа ресурсов предоставляет способ мониторинга, контроля доступа, подготовки и управления выставлением счетов для коллекции ресурсов Azure. Рекомендуется сохранить все службы Azure, связанные с одним проектом (например, такими, как эти курсы) в рамках общей группы ресурсов.
Примечание.
Если вы хотите узнать больше о группах ресурсов Azure, перейдите к связанным документам
Задайте типы проектов как обнаружение объектов (предварительная версия).
После завершения нажмите кнопку "Создать проект", и вы будете перенаправлены на страницу проекта службы Пользовательское визуальное распознавание.
Глава 2. Обучение проекта Пользовательское визуальное распознавание
Когда на портале Пользовательское визуальное распознавание основной задачей является обучение проекта для распознавания определенных объектов на изображениях.
Для каждого объекта, распознаваемого приложением, требуется по крайней мере пятнадцать (15). Вы можете использовать изображения, предоставляемые этим курсом (серия кубков).
Чтобы обучить проект Пользовательское визуальное распознавание:
Нажмите кнопку рядом + с тегами.
Добавьте имя тега, который будет использоваться для связывания изображений с. В этом примере мы используем изображения кубков для распознавания, поэтому назвали тег для этого кубка. После завершения нажмите кнопку " Сохранить ".
Вы заметите , что тег добавлен (может потребоваться перезагрузить страницу, чтобы она появилась).
Щелкните "Добавить изображения" в центре страницы.
Щелкните "Обзор локальных файлов" и перейдите к изображениям, которые вы хотите отправить для одного объекта, с минимальным числом пятнадцать (15).
Совет
Для отправки можно выбрать несколько изображений.
Нажмите кнопку "Отправить файлы ", выбрав все изображения, с которыми вы хотите обучить проект. Файлы начнут отправляться. После подтверждения отправки нажмите кнопку "Готово".
На этом этапе ваши изображения отправляются, но не помечены.
Чтобы пометить изображения, используйте мышь. При наведении указателя мыши на изображение выделение поможет вам автоматически нарисовать выделение вокруг объекта. Если это не точно, вы можете нарисовать свой собственный. Это достигается, удерживая левую кнопку мыши и перетаскивая область выбора, чтобы охватывать объект.
После выбора объекта в изображении небольшой запрос предложит добавить тег региона. Выберите созданный ранее тег ("Кубок", в приведенном выше примере) или если вы добавляете дополнительные теги, введите его и нажмите кнопку +(плюс).
Чтобы пометить следующее изображение, можно щелкнуть стрелку справа от колонки или закрыть колонку тега (щелкнув X в правом верхнем углу колонки), а затем щелкните следующее изображение. После подготовки следующего образа повторите ту же процедуру. Сделайте это для всех отправленных изображений, пока они не будут помечены.
Примечание.
Вы можете выбрать несколько объектов в одном и том же изображении, как показано ниже.
После того как вы помечаете их все, нажмите кнопку с тегами слева от экрана, чтобы отобразить помеченные изображения.
Теперь вы готовы обучить службу. Нажмите кнопку "Обучение ", и начнется первая итерация обучения.
После создания вы увидите две кнопки с именем Make default и Prediction URL. Сначала щелкните "Сделать по умолчанию", а затем щелкните URL-адрес прогнозирования.
Примечание.
Конечная точка, предоставляемая из этой точки, имеет значение, для которого задано значение по умолчанию. Таким образом, если вы позже внесите новую итерацию и обновите ее как по умолчанию, вам не потребуется изменить код.
Щелкнув URL-адрес прогнозирования, откройте блокнот и скопируйте и вставьте URL-адрес (также называемый прогноз-конечная точка) и ключ прогнозирования службы, чтобы получить его при необходимости позже в коде.
Глава 3. Настройка проекта Unity
Ниже приведена типичная настройка для разработки с смешанной реальностью, и таким образом является хорошим шаблоном для других проектов.
Откройте Unity и нажмите кнопку "Создать".
Теперь необходимо указать имя проекта Unity. Вставьте CustomVisionObjDetection. Убедитесь, что для типа проекта задано значение 3D и укажите расположение в нужном месте (помните, что ближе к корневым каталогам лучше). Затем нажмите кнопку "Создать проект".
При открытии Unity стоит проверить, установлен ли редактор скриптов по умолчанию в Visual Studio. Перейдите к разделу "Изменить>параметры", а затем в новом окне перейдите к внешним средствам. Измените внешний редактор скриптов на Visual Studio. Закройте окно параметров.
Затем перейдите к параметрам сборки файлов > и переключите платформу на универсальная платформа Windows, а затем нажмите кнопку "Переключить платформу".
В том же окне параметров сборки убедитесь, что заданы следующие параметры:
Целевое устройство имеет значение HoloLens
Тип сборки имеет значение D3D
Для пакета SDK установлено значение "Последняя версия"
Версия Visual Studio установлена в качестве последней версии
Для сборки и запуска задано значение Local Machine
Остальные параметры в параметрах сборки должны оставаться по умолчанию.
В том же окне "Параметры сборки " нажмите кнопку "Параметры проигрывателя", откроется связанная панель в пространстве, где находится инспектор .
На этой панели необходимо проверить несколько параметров:
На вкладке "Другие параметры" :
Версия среды выполнения сценариев должна быть экспериментальной (.NET 4.6 эквивалентной), что приведет к перезапуску редактора.
Серверная часть скрипта должна быть .NET.
Уровень совместимости API должен быть .NET 4.6.
На вкладке "Параметры публикации" в разделе "Возможности" проверьте:
InternetClient;
Веб-камера
SpatialPerception;
Далее вниз по панели в параметрах XR (найденных ниже параметров публикации), установите флажок "Поддержка виртуальной реальности", а затем убедитесь, что пакет SDK для Windows Смешанная реальность добавлен.
Вернувшись в параметры сборки, проекты C# Unity больше не серые: установите флажок рядом с этим.
Закройте окно Build Settings (Параметры сборки).
В редакторе щелкните "Изменить>параметры проекта" графики.>
На панели инспектора откроется окно параметров графики. Прокрутите вниз, пока не увидите массив с именем Always Include Шейдеры. Добавьте слот, увеличив переменную size на один (в этом примере было 8, поэтому мы сделали ее 9). Появится новый слот в последней позиции массива, как показано ниже:
В слоте щелкните небольшой целевой круг рядом с слотом, чтобы открыть список шейдеров. Найдите устаревшие шейдеры/ прозрачные или диффузные шейдеры и дважды щелкните его.
Глава 4. Импорт пакета Unity CustomVisionObjDetection
Для этого курса вы предоставляете пакет активов Unity с именем Azure-MR-310.unitypackage.
[СОВЕТ] Все объекты, поддерживаемые Unity, включая все сцены, можно упаковывать в файл unitypackage и экспортировать или импортировать в другие проекты. Это самый безопасный и наиболее эффективный способ перемещения ресурсов между различными проектами Unity.
Вы можете найти пакет Azure-MR-310, который необходимо скачать здесь.
На панели мониторинга Unity перед вами щелкните "Ресурсы" в меню в верхней части экрана, а затем выберите "Импорт пользовательского пакета>".
Выберите пакет Azure-MR-310.unitypackage и нажмите кнопку "Открыть". Список компонентов для этого ресурса будет отображаться. Подтвердите импорт, нажав кнопку "Импорт ".
После завершения импорта вы заметите, что папки из пакета теперь добавлены в папку "Активы ". Такая структура папок является типичной для проекта Unity.
Папка "Материалы" содержит материал, используемый курсором взгляда.
Папка подключаемых модулей содержит библиотеку DLL Newtonsoft, используемую кодом для десериализации веб-ответа службы. Две (2) разные версии, содержащиеся в папке и вложенной папке, необходимы, чтобы библиотека использовалась и создавалась как редактором Unity, так и сборкой UWP.
Папка prefabs содержит префабы, содержащиеся в сцене. Это следующие.
- GazeCursor, курсор, используемый в приложении. Будет работать вместе с префабом SpatialMapping, чтобы иметь возможность размещаться в сцене на вершине физических объектов.
- Метка, которая является объектом пользовательского интерфейса, используемым для отображения тега объекта в сцене при необходимости.
- Объект SpatialMapping, который позволяет приложению использовать виртуальную карту с помощью пространственного отслеживания Microsoft HoloLens.
Папка "Сцены ", которая в настоящее время содержит предварительно созданную сцену для этого курса.
Откройте папку "Сцены" на панели проекта и дважды щелкните objDetectionScene, чтобы загрузить сцену, которую вы будете использовать для этого курса.
Примечание.
Код не включен, вы напишете код, следуя этому курсу.
Глава 5. Создание класса CustomVisionAnalyser.
На этом этапе вы готовы написать код. Вы начнете с класса CustomVisionAnalyser .
Примечание.
Вызовы службы Пользовательское визуальное распознавание, сделанные в приведенном ниже коде, выполняются с помощью REST API Пользовательское визуальное распознавание. С помощью этого вы узнаете, как реализовать и использовать этот API (полезно для понимания того, как реализовать что-то подобное самостоятельно). Помните, что корпорация Майкрософт предлагает пакет SDK Пользовательское визуальное распознавание, который также можно использовать для вызова службы. Дополнительные сведения см. в статье о пакете SDK Пользовательское визуальное распознавание.
Этот класс отвечает за:
Загрузка последнего изображения, записанного в виде массива байтов.
Отправка массива байтов в экземпляр службы Пользовательское визуальное распознавание Azure для анализа.
Получение ответа в виде строки JSON.
Десериализация ответа и передача полученного прогноза в класс SceneOrganiser , который будет заботиться о том, как должен отображаться ответ.
Чтобы создать этот класс, выполните указанные ниже действия.
Щелкните правой кнопкой мыши папку ресурса, расположенную на панели проекта, а затем щелкните "Создать>папку". Вызовите скрипты папок.
Дважды щелкните только что созданную папку, чтобы открыть ее.
Щелкните правой кнопкой мыши в папке и нажмите кнопку "Создать>скрипт C#". Назовите скрипт CustomVisionAnalyser.
Дважды щелкните новый скрипт CustomVisionAnalyser , чтобы открыть его с помощью Visual Studio.
Убедитесь, что в верхней части файла указана ссылка на следующие пространства имен:
using Newtonsoft.Json; using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking;
В классе CustomVisionAnalyser добавьте следующие переменные:
/// <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;
Примечание.
Убедитесь, что вы вставляете ключ прогнозирования службы в переменную predictionKey и свою конечную точку прогнозирования в переменную прогнозаEndpoint. Вы скопировали их в Блокнот ранее, в главе 2, шаг 14.
Теперь необходимо добавить код для пробуждения(), чтобы инициализировать переменную экземпляра:
/// <summary> /// Initializes this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; }
Добавьте под ним метод coroutine (со статическим методом GetImageAsByteArray(), который получит результаты анализа изображения, захваченного классом ImageCapture .
Примечание.
В корутине АнализаImageCapture есть вызов класса SceneOrganiser , который вы еще не создали. Поэтому оставьте эти строки закомментированы на данный момент.
/// <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); }
Удалите методы Start() и Update(), так как они не будут использоваться.
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Внимание
Как упоминалось ранее, не беспокойтесь о коде, который, как представляется, может возникнуть ошибка, так как вы предоставите дополнительные классы в ближайшее время, что исправит эти ошибки.
Глава 6. Создание класса CustomVisionObjects
Создаваемый класс — это класс CustomVisionObjects .
Этот скрипт содержит ряд объектов, используемых другими классами для сериализации и десериализации вызовов, выполненных в службе Пользовательское визуальное распознавание.
Чтобы создать этот класс, выполните указанные ниже действия.
Щелкните правой кнопкой мыши в папке "Скрипты", а затем щелкните "Создать>скрипт C#". Вызовите скрипт CustomVisionObjects.
Дважды щелкните новый скрипт CustomVisionObjects , чтобы открыть его с помощью Visual Studio.
Убедитесь, что в верхней части файла указана ссылка на следующие пространства имен:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking;
Удалите методы Start() и Update() в классе CustomVisionObjects, теперь этот класс должен быть пустым.
Предупреждение
Важно внимательно следовать следующей инструкции. Если вы помещаете объявления нового класса в класс CustomVisionObjects , вы получите ошибки компиляции в главе 10, заявив, что AnalysisRootObject и BoundingBox не найдены.
Добавьте следующие классы за пределами класса CustomVisionObjects . Эти объекты используются библиотекой Newtonsoft для сериализации и десериализации данных ответа:
// 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; } }
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Глава 7. Создание класса SpatialMapping
Этот класс установит в сцене коллайдер пространственного сопоставления, чтобы обнаруживать столкновения между виртуальными объектами и реальными объектами.
Чтобы создать этот класс, выполните указанные ниже действия.
Щелкните правой кнопкой мыши в папке "Скрипты", а затем щелкните "Создать>скрипт C#". Вызовите скрипт SpatialMapping.
Дважды щелкните новый скрипт SpatialMapping, чтобы открыть его с помощью Visual Studio.
Убедитесь, что у вас есть следующие пространства имен, на которые ссылается класс SpatialMapping :
using UnityEngine; using UnityEngine.XR.WSA;
Затем добавьте следующие переменные в класс SpatialMapping над методом 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;
Добавьте Awake() и 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); }
Удалите метод Update().
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Глава 8. Создание класса GazeCursor
Этот класс отвечает за настройку курсора в правильном расположении в реальном пространстве путем использования SpatialMappingCollider, созданного в предыдущей главе.
Чтобы создать этот класс, выполните указанные ниже действия.
Щелкните правой кнопкой мыши в папке "Скрипты", а затем щелкните "Создать>скрипт C#". Вызов скрипта GazeCursor
Дважды щелкните новый скрипт GazeCursor , чтобы открыть его с помощью Visual Studio.
Убедитесь, что вы ссылаетесь на следующее пространство имен над классом GazeCursor :
using UnityEngine;
Затем добавьте следующую переменную в класс GazeCursor над методом Start().
/// <summary> /// The cursor (this object) mesh renderer /// </summary> private MeshRenderer meshRenderer;
Обновите метод Start() следующим кодом:
/// <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); }
Обновите метод Update() следующим кодом:
/// <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; } }
Примечание.
Не беспокойтесь об ошибке для класса SceneOrganiser , который не найден, вы создадите его в следующей главе.
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Глава 9. Создание класса SceneOrganiser
Этот класс:
Настройте основную камеру, подключив к ней соответствующие компоненты.
При обнаружении объекта он будет отвечать за вычисление его положения в реальном мире и поместить метку тега рядом с ним с соответствующим именем тега.
Чтобы создать этот класс, выполните указанные ниже действия.
Щелкните правой кнопкой мыши в папке "Скрипты", а затем щелкните "Создать>скрипт C#". Назовите скрипт SceneOrganiser.
Дважды щелкните новый скрипт SceneOrganiser , чтобы открыть его с помощью Visual Studio.
Убедитесь, что у вас есть следующие пространства имен, на которые ссылается класс SceneOrganiser :
using System.Collections.Generic; using System.Linq; using UnityEngine;
Затем добавьте следующие переменные в класс SceneOrganiser над методом 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;
Удалите методы Start() и Update().
Под переменными добавьте метод Awake( ), который инициализирует класс и настроит сцену.
/// <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>(); }
Добавьте метод PlaceAnalysisLabel(), который создаст экземпляр метки в сцене (которая на этом этапе невидима для пользователя). Он также помещает квадрат (также невидимый), где размещается изображение, и перекрывается с реальным миром. Это важно, так как координаты поля, полученные из службы после анализа, трассируются обратно в этот квадрат, чтобы определить приблизительное расположение объекта в реальном мире.
/// <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; }
Добавьте метод FinaliseLabel(). Он отвечает за следующее:
- Задание текста метки с тегом прогноза с наивысшей уверенностью.
- Вызов вычисления ограничивающего прямоугольника на квадратном объекте, расположенном ранее, и размещение метки в сцене.
- Настройка глубины метки с помощью Raycast к ограничивающей коробке, которая должна столкнуться с объектом в реальном мире.
- Сброс процесса записи, чтобы разрешить пользователю записывать другое изображение.
/// <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(); }
Добавьте метод CalculateBoundingBoxPosition(), в котором размещается ряд вычислений, необходимых для перевода координат Ограничивающего прямоугольника, полученных из службы, и повторно создайте их пропорционально на квадрате.
/// <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); }
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Внимание
Прежде чем продолжить, откройте класс CustomVisionAnalyser и в методе AnalyseLastImageCaptured() раскомментируйте следующие строки:
// 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);
Примечание.
Не беспокойтесь о сообщении класса ImageCapture "не удалось найти", вы создадите его в следующей главе.
Глава 10. Создание класса ImageCapture
Следующий класс, который вы собираетесь создать, — это класс ImageCapture .
Этот класс отвечает за:
- Захват изображения с помощью камеры HoloLens и его хранения в папке приложения .
- Обработка жестов касания от пользователя.
Чтобы создать этот класс, выполните указанные ниже действия.
Перейдите в папку "Скрипты", созданную ранее.
Щелкните правой кнопкой мыши в папке и нажмите кнопку "Создать>скрипт C#". Назовите скрипт ImageCapture.
Дважды щелкните новый скрипт ImageCapture , чтобы открыть его с помощью Visual Studio.
Замените пространства имен в верхней части файла следующим образом:
using System; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.XR.WSA.Input; using UnityEngine.XR.WSA.WebCam;
Затем добавьте следующие переменные в класс ImageCapture над методом 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;
Теперь необходимо добавить код для методов Awake() и 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(); }
Реализуйте обработчик, который будет вызываться при возникновении жеста касания:
/// <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); } }
Внимание
Когда курсор зеленый, это означает, что камера доступна для получения изображения. Когда курсор красный, это означает, что камера занята.
Добавьте метод, который приложение использует для запуска процесса захвата изображений и хранения образа:
/// <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); }); }); }
Добавьте обработчики, которые будут вызываться, когда фотография была записана и когда она будет готова к анализу. Затем результат передается в CustomVisionAnalyser для анализа.
/// <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(); }
Не забудьте сохранить изменения в Visual Studio, прежде чем вернуться в Unity.
Глава 11. Настройка скриптов в сцене
Теперь, когда вы написали весь код, необходимый для этого проекта, пришло время настроить скрипты в сцене и на префабах, чтобы они правильно ведут себя.
В редакторе Unity на панели иерархии выберите основную камеру.
На панели инспектора выберите выбранную главную камеру, нажмите кнопку "Добавить компонент", а затем найдите скрипт SceneOrganiser и дважды щелкните его, чтобы добавить его.
На панели проекта откройте папку prefabs, перетащите префаб метки в пустую целевую область ввода метки в скрипте SceneOrganiser, который вы только что добавили в главную камеру, как показано на рисунке ниже:
На панели иерархии выберите дочерний элемент GazeCursor основной камеры.
На панели инспектора с выбранным параметром GazeCursor нажмите кнопку "Добавить компонент", а затем найдите скрипт GazeCursor и дважды щелкните его, чтобы добавить его.
Снова на панели иерархии выберите дочерний элемент SpatialMapping основной камеры.
На панели инспектора с выбранным параметром SpatialMapping нажмите кнопку "Добавить компонент", а затем найдите скрипт SpatialMapping и дважды щелкните его, чтобы добавить его.
Остальные скрипты, которые не заданы, будут добавлены кодом в скрипте SceneOrganiser во время выполнения.
Глава 12. Перед созданием
Чтобы выполнить тщательный тест приложения, вам потребуется загрузить его на microsoft HoloLens.
Перед выполнением убедитесь, что:
Все параметры, упомянутые в главе 3 , задаются правильно.
Скрипт SceneOrganiser присоединен к объекту Main Camera .
Скрипт GazeCursor присоединен к объекту GazeCursor.
Скрипт SpatialMapping присоединен к объекту SpatialMapping .
В главе 5 шаг 6.
- Убедитесь, что ключ прогнозирования службы вставляется в переменную predictionKey.
- Вы вставляете конечную точку прогнозирования в класс predictionEndpoint .
Глава 13. Создание решения UWP и загрузка неопубликованных приложений
Теперь вы готовы создать приложение в качестве решения UWP, которое вы сможете развернуть в Microsoft HoloLens. Чтобы начать процесс сборки, выполните следующие действия.
Перейдите к параметрам сборки файлов>.
Тик Unity C# Projects.
Нажмите кнопку "Добавить открытые сцены". При этом в сборку будет добавлена открытая сцена.
Нажмите кнопку " Создать". Unity запустит окно проводник, в котором необходимо создать, а затем выбрать папку для сборки приложения. Создайте папку и присвойте ей имя приложения. Затем с выбранной папкой приложения нажмите кнопку " Выбрать папку".
Unity начнет создание проекта в папку приложения .
После завершения сборки Unity (может потребоваться некоторое время), откроется окно проводник в расположении сборки (проверьте панель задач, так как она может не всегда отображаться над окнами, но уведомит вас о добавлении нового окна).
Чтобы развернуть в Microsoft HoloLens, вам потребуется IP-адрес этого устройства (для удаленного развертывания) и убедиться, что он также имеет режим разработчика. Для этого:
При ношении HoloLens откройте параметры.
Переход к сети и расширенным параметрам Wi-Fi>в Интернете>
Обратите внимание на IPv4-адрес .
Затем вернитесь к параметрам, а затем в разделе "Обновление и безопасность>для разработчиков"
Установите режим разработчика.
Перейдите к новой сборке Unity (папке приложения) и откройте файл решения с помощью Visual Studio.
В разделе "Конфигурация решения" выберите "Отладка".
На платформе решения выберите x86, удаленный компьютер. Вам будет предложено вставить IP-адрес удаленного устройства (Microsoft HoloLens, в этом случае вы указали).
Перейдите в меню "Сборка" и щелкните "Развернуть решение ", чтобы загрузить неопубликованное приложение в HoloLens.
Теперь ваше приложение должно появиться в списке установленных приложений в Microsoft HoloLens, готовых к запуску!
Чтобы использовать приложение, выполните следующие действия.
- Просмотрите объект, обученный службой Azure Пользовательское визуальное распознавание, обнаружением объектов и жестом касания.
- Если объект успешно обнаружен, текст метки мирового пространства появится с именем тега.
Внимание
Каждый раз, когда вы записываете фотографию и отправляете ее в службу, вы можете вернуться на страницу службы и повторно обучить службу только что захваченными изображениями. В начале вы, вероятно, также придется исправить ограничивающие коробки , чтобы быть более точным и переобучение службы.
Примечание.
Текст метки может не отображаться рядом с объектом, если датчики Microsoft HoloLens и /или SpatialTrackingComponent в Unity не могут размещать соответствующие коллидеры относительно реальных объектов. Попробуйте использовать приложение на другой поверхности, если это так.
Ваше Пользовательское визуальное распознавание, приложение обнаружения объектов
Поздравляем, вы создали приложение смешанной реальности, которое использует Azure Пользовательское визуальное распознавание, API обнаружения объектов, который может распознать объект из изображения, а затем предоставить приблизительную позицию для этого объекта в трехмерном пространстве.
Дополнительные упражнения
Упражнение 1
Добавление к текстовой метке используйте полупрозрачный куб для упаковки реального объекта в трехмерном ограничивающем поле.
Упражнение 2
Обучите службу Пользовательское визуальное распознавание, чтобы распознать больше объектов.
Упражнение 3
Воспроизведение звука при распознавании объекта.
Упражнение 4
Используйте API для повторного обучения службы с теми же изображениями, что и приложение анализирует, чтобы сделать службу более точной (одновременно делать прогнозирование и обучение).