Хочу поделиться интересной задачей и рассказать, как я её решил. К сожалению, я не могу опубликовать готовую реализацию. Но сама задача оказалась довольно простой. И при желании любой может ее повторить.
Чем я занимался до этого
Моя компания занималась играми смешанной реальности (MR). Мы использовали Kinect для захвата движений игрока и китайское оборудование с ПО для создания настенного тира с игрушечными лазерными пистолетами. Так же использовали OpenCV для сканирования детских рисунков и ряд других технологий.
У нас оставалось еще три проекта, которые откладывались из-за низкого приоритета и отсутствия готового решения.
Интерактивный стол, где дети могут тапать милых зверушек или играть в мини-игры на поверхности стола.
Интерактивная горка, оставляющая след или создающая эффект при скатывании.
Песочница, где дети формируют руками горы из песка. Из вершин гор вытекает лава, а по впадинам течет вода.
Да, такие игры уже существовали, нужно было повторить их. Однако Kinect из коробки не имел готового решения. Китайского оборудования, подходящего для этих задач, тоже не нашлось. Оставались два варианта: купить готовое решение у компаний, которые уже занимались этим, или сделать всё самостоятельно.
Мы начали переговоры о покупке технологии, но только для стола. Сначала договорились о цене в 40 тысяч рублей, но потом она выросла до 80, что всё равно очень дёшево. Перед тем как отказаться от сделки, они, если я правильно помню, повысили стоимость до 220 тысяч и сказали, что не могут продать технологию, потому что на ней зарабатывают.
Мы откладывали это, потому что были заняты другими делами. Но я не переставал думать, что всё проще, чем кажется. Мне нужно было всего лишь время — хотя бы неделя, чтобы попробовать. И такой момент мне представился, и не неделя, а целых две.
Появилось окно, в котором можно было поэкспериментировать. Я взял Kinect, прилепил его скотчем к потолку, проектор развернул так, чтобы он светил на стол. И начал пробовать.
Немного об оборудовании
Почему мы выбрали Kinect? Существуют другие камеры, которые продолжают выпускаться и имеют более высокую скорость и глубину сканирования. Однако дело в Kinect SDK. Нам было необходимо отслеживать движения игрока, а камера сама по себе не может распознавать людей на изображении. Есть несколько вариантов решить задачу: OpenCV, Nuitrack, Kinect SDK.
-
OpenCV использует нейронную сеть для сегментации объектов. Она хорошо справляется с задачей, сегментируя части тела, такие как голова или рука. Однако у нее есть два недостатка:
Определение расстояния от камеры до объекта затруднительно, так как OpenCV работает только с RGB-изображением, которое не содержит данных о глубине. Я думал, что решил эту проблему, но стоило игроку прыгнуть или присесть, как все шло наперекосяк.
Нейросеть сильно нагружает ПК, что требует установки мощных ПК на каждом аттракционе, что увеличивает цену конечного продукта.
Nuitrack значительно лучше. У них есть SDK для Unity, но решение не дешевое. Реализация у них нативное, и поэтому быстрое. Но вот точность отслеживания не очень. Руки и ноги постоянно скручивались и пропадали. Связавшись с ними, они сказали, что есть функция, которая подключает нейронку и улучшает отслеживание. Так оно и было, но при включении этой функции нагрузка на ПК была едва ли больше, чем с OpenCV.
-
Kinect SDK, в свою очередь, был бесплатным, отслеживание было великолепное и очень производительное за счет того, что оно было создано до того, как о нейронках все заговорили. Оно было полностью нативным. Когда игрок поворачивался боком и одна из его ног выходила за пределы обзора камеры, она не исчезала и не деформировалась. Система автоматически определяла её положение, хотя и не всегда точно, но всё же значительно лучше, чем остальные решения. Было большое количество готовых решений, таких как, например, события выполнения движений игроком, когда игрок свайпал рукой вправо или влево, то я менял ему скин и т.д.
Я освоил работу с Kinect и не торопился переходить на RealSense или китайские аналоги.
Реализация
От камеры мне приходил массив 16-битных чисел. Каждое число значило расстояние в миллиметрах от камеры до точки. Это не матрица, а массив, поэтому, зная ширину Depth-изображения, мы можем превратить это в матрицу.
Но спойлер.
Не делайте так. Работать с массивом гораздо быстрее. Уверен, вы и так это знали, я сразу не сориентировался. Достаточно было заглянуть в расчет сложности алгоритмов, чтобы это понять.
Позже выяснилось, что даже при маленьком разрешении Depth-изображения (512 × 424) для обработки в реальном времени это слишком много данных. А ведь нужно еще рендерить игру и снизить стоимость оборудования. Размер руки или тела значительно больше одного пикселя. Мне хватило бы шести крупных пикселей, чтобы понять, что это рука.
Существует множество способов уменьшить изображение, например, брать по четыре пикселя, вычислять среднее значение и оставлять его. Однако для ускорения процесса я решил просто брать каждый пиксель через определенный промежуток (N). Это значительно ускорило обработку.
Чтобы понять, как работает эта камера, нужно разобраться в её принципах. Разные камеры функционируют по-разному, и даже технологии Kinect v1 и Kinect v2 сильно отличаются. Моя камера использовала луч невидимого спектра для измерения расстояний. Специальная камера фиксировала время, за которое луч отражался от объекта и возвращался обратно. Зная скорость луча и время его возвращения, камера могла вычислить расстояние. Однако луч мог поглощаться матовыми поверхностями, полностью отражаться от зеркала или глянцевых материалов. Кроме того, чистота луча влияла на его яркость: от одних объектов возвращались более яркие лучи, а от других — более тусклые. Как видите, нюансов много. Это все выражается в большого количества шумах на итоговом изображении.
Большое количество шумов — это проблема. Поэтому я использовал несколько способов уменьшить шумы. Например, не учитывал взаимодействие, если значение расстояния было 0 это означало, что луч не вернулся и отразился или поглотился с концами. Больше рекомендаций я дать не могу, так как идеального баланса между производительностью и эффективностью я не добился.
Когда я справился с шумами, возник другой вопрос. Как же отслеживать только нужные взаимодействия в непонятной мешанине пикселей? Тут тоже не сложно. Я просто сохранил один результат и все сравниваю с ним. Теперь камера считала не расстояние от камеры до объекта, а только изменение расстояния с момента так называемой калибровки.

Следующая проблема в избытке данных. Человек просто стоит рядом со столом или держит руку на весу и не касается его. Нужно найти комплексное решение:
Создать маску для исключения ненужных данных.
Учитывать изменения только в определенном диапазоне, например, до 2 сантиметров остальные игнорировать.
Эти данные мы превращаем в картинку и выводим на проектор. Ставим на стол, например, подушку и подгоняем с помощью множителей и отступов картинку на проекторе и подушку.
Когда я увидел, как это работает, мне пришла мысль: а что, если создать горку? Я взял коробку и лист ватмана, поставил коробку на стол и приклеил один конец ватмана к коробке, а другой — к столу, образовав наклонную поверхность. При калибровке сенсору было неважно, ровная ли поверхность. Он использовал горку как основу. Когда я скатил с нее шар, сенсор заметил изменения.

Это был успех. Это означало, что он может считывать касания с любой поверхности, неважно, ровная она или кривая.
Но и на этом я не остановился. Я нашел камеру RealSense, которая валялась в моей коморке, и поменял местами эти две камеры. Всё, что нужно было поменять- несколько значений, и всё заработало аналогично. Это значит, что можно использовать абсолютно любую камеру глубины, не переписывая при этом код. Просто сохранить готовые пресеты для каждой используемой камеры.
Это был второй успех. Получается, что можно использовать любую камеру. Даже китайскую, что сильно удешевляло итоговый продукт.
Когда я посветил камерой и проектором на стену, все заработало так же хорошо. Несмотря на то, что камера висела под углом двадцать градусов от потолка. Конечно, на стене можно было перекрыть руку головой, и тогда касание не считывалось. Но китайское решение, которое мы использовали, имело такую же проблему. А технологию они использовали совсем другую.

Это был третий успех. Данный подход будет работать под любым наклоном.
Оставалась песочница. Тут я просто генерировал меш с количеством точек, равным итоговому массиву. Дальше массив нужно было сгладить. Нашел в интернете формулу интерполяции. И просто двигал вершины на расстояние величины значения матрицы, предварительно умножив ее на фактор высоты. После, так как не ас в написании шейдеров, я обратился к чату гпт, чтобы он мне помог написать шейдер, где от высоты точки будет изменяться ее цвет и текстура. И песочница была готова.
Это был четвертый успех.
Конечно на этом все не закончилось. Это был лишь прототип и впереди было еще очень много работы и проблем. Например с тем, чтобы правильно понимать касания, работа с шумами, создание инструментов для создания маски и калибровки, проработка функционала под разные типы игры. Но всё это уже было потом.
Мораль такова. Даже если кажется, что задача очень сложная, попробовать ее сделать стоит.
Если вы здесь, значит, дочитали до конца. Надеюсь, вам было интересно. Я не считаю себя гением или супер мастером своего дела. Думаю, при желании каждый может повторить мой опыт. Так что ай-да покупать Kinect и превращайте кухонный стол в планшет.
Quiensabe
К слову, на базовом уровне такую штуку очень легко сделать в TouchDesigner. Вообще без программирования :)