Пространственная осведомленность: что могут очки Hololens?


    Сегодня мы совершим прыжок в область Spatial Awareness с помощью очков Hololens от Microsoft и поговорим о развитии Windows Mixed Reality (WMR), а также о том, чего стоит ожидать от второго поколения.


    Статья рассчитана на тех, кто занимается разработкой под Hololens с использованием среды Unity и Mixed Reality Toolkit (MRTK).


    На всякий случай оставим здесь руководство по UWP от Microsoft.


    Что нужно знать о Hololens?


    Одна их отличительных черт Hololens – это потенциал в области смешанной реальности (Mixed Reality), то есть возможность воспринимать пространство и взаимодействовать с ним (Spatial Awareness). Не только Microsoft стремится занять данную нишу. Недавно вышел EasyAR SDK 4.0 с поддержкой Sparse & Dense SpatialMap, которые решают аналогичные задачи.


    Для Microsoft важно представить «сильное» решение (25 февраля вышел очередной анонс): под второе поколение очков разработана отдельная Scene Understanding SDK.


    Вероятно, основой этой идеи является Spatial Understanding, который можно встретить в Holotoolkit 2017.4.1.0 (сейчас – MRTK) под Unity версии 2017.4. На тот момент был актуален отличный материал – «How-To: Use Spatial Understanding to Query your Room with HoloLens».


    Для того чтобы лучше разобраться в теме, рассмотрим очки Hololens подробнее и приведем описание их работы.


    Насколько хорошо «видят» Hololens?


    (Спойлер: не очень хорошо)


    Для восприятия пространства в Hololens используется ToF-камера глубины.
    Камера глубины принимает отраженный от объектов свет и может за счет этого определить объем объекта и расстояние до него. На основании данных, полученных от камеры, Hololens выстраивают меш (как на изображении ниже):



    Стандартный меш в Hololens Spatial Awareness


    Меш – объект в Unity, описывающий 3D-модель. Статью с описанием терминов можно почитать здесь.


    Полученные «сырые» данные поступают в виде набора поверхностей (entry) и образуют итоговый меш пространства, который также привязан к якорной системе. В Hololens данная технология используется для взаимодействия с пространством: объекты, имеющие коллайдеры, могут быть размещены на поверхности. При включении Hololens автоматически подгружают сохраненное пространство в зависимости от его якоря.


    Получаемое пространство не претендует на чрезмерную детальность (погрешность составляет 5–10 см минимум, но для расположения предметов на полу / не за стеной этого достаточно). При этом угловатость получаемой в итоге модели в любом случае будет высокой. Шум, который появляется при сканировании, может мешать как при взаимодействии с внешней средой, так и при построении модели объекта.


    Входные данные


    Для работы со Spatial Awareness мы будем использовать MRTK в среде Unity.
    MRTK также является проектом Microsoft. Если есть необходимость посмотреть, как проходит установка в Unity и что там можно делать, то добро пожаловать на github.


    В экспериментальных целях возьмем следующие параметры для сканирования:


    • Observation Extents = 1.0f – на каком расстоянии от нас происходит отрисовка пространства;
    • TPСM = 600 – количество треугольников на квадратный метр (scanning density). Чем плотнее, тем точнее (и тем выше нагрузка);
    • Surface Update Time = 0.3f (секунды) – интервал между сканированием.

    За две минуты сканирования с такими параметрами мы получили следующее:



    Выравнивание сетки


    Основные параметры, задающие объемную фигуру в Unity:


    • mesh.vertices типа Vector3 []– массив координат вершин для меша;
    • mesh.triangles типа int []– массив индексов, где каждая тройка массива соответствует грани меша;
    • mesh.uv типа Vector2[]– массив координат для UV-маппинга.
      Чтобы лучше понять vertices и triangles, обратимся к рисунку ниже:


    Сначала применим к полученной поверхности алгоритм Лапласа, который сводится к довольно простому принципу: каждая вершина центруется относительно смежных ей по всем координатам:


    • для каждой вершины $i$ меша находим смежные ей вершины;
    • позиция вершины $p$$i$ изначально характеризуется набором координат $(x,y,z)$ в трехмерном пространстве;
    • при сглаживании позиция вершины $p$$i$ заменяется на позицию, координаты которой являются средним взвешенным смежных $i$ вершин:

    $p_{i} =\frac{1}{|Adj(i)|} \sum_{j \in Adj(i)}^{} q_{j}$


    где:


    • $Adj(i)$ – множество вершин, смежных $p$$i$;
    • $q$$j$ – позиции вершин, смежных $p$$i$.

    На Wiki Unity имеется набор скриптов, которые отвечают за сглаживание меша по Лапласу. Исходники были написаны MarkGX еще в 2011 году (тем не менее они до сих пор остаются актуальными). Вышеописанный алгоритм реализован в виде функции laplacianFilter, которой на вход подаются:


    • набор вершин меша Vector3[] sv;
    • набор индексов вершин меша int[] t – массив индексов вершин, составляющих грань. Разбит по тройкам (тройка индексов для одной грани).

    Для каждой вершины мы сначала находим смежные (соседние) вершины. Делается это следующим образом:


    Для вершины с индексом $i$ меша в массиве $t$ проверяем каждый индекс $t[k], t[k+1], t[k+2]$ в тройке на условие: если хотя бы один индекс совпадет с индексом $i$, то вершины, соответствующие этой тройке индексов, сохраняются как смежные.

    Саму функцию поиска смежных вершин можете посмотреть здесь.


    Далее, если смежные вершины найдены, для каждой из координат вычисляется среднее значение по x, y, z, которое присваивается данной вершине.


    Код основной функции сглаживания по Лапласу:


        public static Vector3[] laplacianFilter(Vector3[] sv, int[] t)
        {
            Vector3[] wv = new Vector3[sv.Length];
            List<Vector3> adjacentVertices = new List<Vector3>();
    
            float dx = 0.0f;
            float dy = 0.0f;
            float dz = 0.0f;
    
            for (int vi=0; vi< sv.Length; vi++)
            {
                adjacentVertices = MeshUtils.findAdjacentNeighbors (sv, t, sv[vi]);
    
                if (adjacentVertices.Count != 0)
                {
                    dx = 0.0f;
                    dy = 0.0f;
                    dz = 0.0f;
    
                    for (int j=0; j<adjacentVertices.Count; j++)
                    {
                        dx += adjacentVertices[j].x;
                        dy += adjacentVertices[j].y;
                        dz += adjacentVertices[j].z;
                    }
    
                    wv[vi].x = dx / adjacentVertices.Count;
                    wv[vi].y = dy / adjacentVertices.Count;
                    wv[vi].z = dz / adjacentVertices.Count;
                }
            }
            return wv;
        }

    Результат подобного преобразования выглядит следующим образом:



    С перегородкой ситуация стала лучше, но на более «тонких» объектах меш стал значительно разорван. Дело в том, что сглаживание по Лапласу приводит к сильному сжатию объекта. Чтобы этого не происходило, после сглаживания скорректируем положение вершин на среднее значение от разницы.


    Для коррекции сглаживания по Лапласу может быть применен алгоритм Классов Хамфри (HC-algorithm).
    С целью предотвращения сжатия фигуры в точку будем сдвигать полученные после сглаживания по Лапласу вершины $p$$i$ назад к их предыдущим позициям $q$$i$ и (или) оригинальным позициям $o$$i$ на среднее значение разниц $d$$i$ между ними.


    Полученное положение $b$$i$ будет вычисляться следующим образом:


    $b_{i} := p_{i} - (\alpha o_{i} + (1-\alpha)q_{i})$


    где $α$ – то есть корректировка происходит на $d$$i$. Получается, что разница $d$$i$, которая вычисляется как среднее взвешенное значение разниц, равна:


    $d_{i} := -\frac{1}{|Adj(i)|} \sum_{j \in Adj(i)}^{} b_{j}$


    Для того чтобы учитывать и сдвиг для нужной нам вершины $i$, включим в расчет $b$$i$ с весом $\beta \in[0;1]$:


    $d_{i} := -(\beta b_{i} + \frac{1-\beta}{|Adj(i)|} \sum_{j \in Adj(i)}^{} b_{j})$


    Скаляр $\beta$ отвечает за степень жесткости привязки к изначальным позициям.


    Математические выкладки можно посмотреть здесь. Более подробно ознакомиться со сглаживанием по Лапласу и его практическим применением можно здесь.

    Поскольку сглаживание достаточно трудоемкая операция, мы будем использовать одну итерацию $(\alpha = 0)$. А для баланса между более гладкой стенкой и не сильно «рваной» лампой установим $\beta=0,5$.


    Получилась следующая картинка:



    В итоге


    Даже сокращение числа итераций сглаживания до одного не отменит того, что во время обработки не самого большого меша (сканировали не всю комнату) происходит «заморозка» приложения.


    Базовая точность сканирования и ограниченные ресурсы самих Hololens представляют собой довольно серьезное ограничение для работы с пространством, поэтому работа с представленными подходами пока не выйдет за рамки эксперимента. Самым полезным для нашей работы оказалось написание шейдера материала, о котором, возможно, мы расскажем в следующей статье.


    Если же брать небольшие значения Extents и обрабатывать только часть объектов, то описанным выше подходом можно решать интересные задачи, например реконструкцию частей тела.


    Мы очень надеемся, что если когда второе поколение все же выйдет на российский рынок, точность и возможность обработки существенно возрастут (привет, Azure Kinect и Snapdragon). А пока лучше обращать внимание на отдельные камеры глубины или же работать в рамках существенных ограничений.

    НТЦ Вулкан
    Исследуем, разрабатываем, защищаем

    Комментарии 2

      0
      Метод Лапласа создаст сетку с наименьшей длиной ребра элемента. Это также самый быстрый метод. Есть ещё несколько разных видов, например центроидное сглаживание, которое создает сетку с более однородными размерами элементов.
      Центроидное тянет узел к центроиду, взвешенному по площадям граней, уравновешивая площади. Пример: --A image
      и результат: image
        0

        Совершенно верно. Существует несколько способов фильтрации сетки (Mesh). Мы выбирали наиболее быстрый, исходя из ограничений, накладываемых "железом"

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое