- PVSM.RU - https://www.pvsm.ru -
Не так давно я был свидетелем запуска Apple Vision Pro. Презентация оказалась очень интересной, но больше всего моё внимание зацепила одна деталь — дистанционное управление вводом с помощью пальцев. Выглядит очень интуитивно — использовать перемещение и сведение пальцев для управления курсором на экране. Меня этот механизм заинтриговал, и я решил воссоздать его сам.
Основная цель — реализовать механизм, позволяющий использовать в качестве устройства ввода для компьютера кисть руки. Соответствующая программа должна обрабатывать перемещение курсора и клики с его помощью. Очевидно, что для этого нам потребуется камера, которую я направлю вниз, поскольку именно в этой области будут находиться руки в процессе использования компьютера.
Далее нам нужен способ обнаружения положения кисти и пальцев для управления курсором. Это я реализую с помощью инструмента MediaPipe [1] от Google, представляющего набор готовых решений для машинного обучения. Среди этих решений есть функция обнаружения ключевых точек кисти, которая нам и нужна. Ну и последним делом мне каким-то образом потребуется симулировать ввод мыши.

Обнаружение ключевых точек руки с помощью MediaPipe

Общая схема механизма
Опираясь на эту общую схему, можно воспользоваться версией MediaPipe для Python. Мы запрограммируем OpenCV на считывание видеопотока с камеры и его передачу в MediaPipe, после чего полученные контрольные точки используем для симуляции работы мыши. Вроде несложно. За исключением того, что работает такая схема не очень. Стоило мне только наладить подключение между OpenCV и MediaPipe, как я столкнулся с сильными тормозами.
Версия для Python сильно тормозит из-за проблем с OpenCV
Покопавшись в этом вопросе, я выяснил, что причина кроется в OpenCV, а именно в работе функции waitKey. Я до сих пор не нашёл, как это исправить, поэтому не стану тратить время и просто откажусь от Python.
Изучая возможность использования MediaPipe, в одном из демо я увидел, что есть веб-версия этого пакета инструментов, которая, как выяснилось, работает очень плавно. В итоге я решил использовать её. Но оставалась одна проблема — как управлять мышью через браузер? Тогда мне в голову пришла безумная идея: раз я могу запустить MediaPipe локально, почему бы мне не использовать эту её версию в качестве бэкенда для симуляции мыши? Нужно лишь понять, как наладить взаимодействие фронтенд-версии MediaPipe с её бэкенд-вариантом.
Веб-версия работает плавно
Теперь нужно было найти способ наладить взаимодействие фронтенда с бэкендом. Я рассматривал три варианта: простой HTTP-запрос, WebSocket [2] и gRPC [3] с потоковой передачей. Учитывая, что мне нужна минимальная задержка, HTTP-запросы отпали сразу. Осталось два варианта, оба из которых подразумевают потоковую передачу данных. В итоге я остановился на WebSocket, так как он позволяет наладить между клиентом и сервером диалог в реальном времени, который необходим для нашего случая. Да и механизм gRPC мне не особо знаком.
Я настроил с помощью Python простой WebSocket-сервер, который будет получать JSON-строку, содержащую координаты x и y для перемещения курсора. Теперь мне нужно просто объединить координаты одного пальца для отправки бэкенду — я использую в качестве ориентира кончик большого.

WebSocket-сервер, получающий информацию для управления движением курсора
Управление курсором через браузер:
Сработало! Причём на удивление хорошо. Однако управление курсором через браузер кажется совсем неудачным вариантом. И хотя задержка не особо заметна, я думаю, она может сказываться на общей эффективности. Но пока этот нюанс оставим.
Перейдём к логике обработки кликов. Чтобы фиксировать события клика, необходимо обнаруживать «щипательное» движение между большим и указательным пальцами. Для этого нужно просто измерять промежуток между их кончиками, используя евклидово расстояние [4]. Если оно будет оказываться меньше установленного порога, будет вызываться событие движения курсора вниз, а если больше — событие движения вверх. Обратите внимание, что мы вместо клика используем движение курсора вниз/вверх — это нужно для поддержки перетаскивания.
Такое решение прекрасно работает, но при приближении руки к камере возникает проблема. Поскольку мы перемещаем 3D-объект в 2D-пространстве, приближение кисти к камере также ведёт к увеличению расстояния между кончиками пальцев.

Расстояние между кончиками пальцев: удалённо и вблизи
Решить эту проблему можно обходным путём, используя относительное расстояние. Мы будем вычислять промежуток между кончиками пальцев и соответствующими костяшками их суставов, который при приближении кисти к камере будет увеличиваться, а затем сравнивать этот промежуток с расстоянием между кончиками. Таким образом, мы будем получать правильное расстояние, независимо от удалённости кисти от камеры.

Вычисление относительного расстояния между кончиками пальцев
Следующей проблемой стало дрожание. Заметьте, что даже когда моя кисть просто неподвижно лежит на столе, курсор дрожит. Это характерный недочёт самой модели обнаружения ключевых точек, и единственный способ исправить его — использовать более качественную модель. Однако не всё потеряно. Пока что мы можем реализовать в качестве позиции курсора простое скользящее среднее. Тогда дрожание уменьшится, и движение станет плавным.
Единственный нюанс в том, что с увеличением буфера для скользящего среднего увеличивается и задержка. Кроме того, я также решил добавить буфер для состояния сведения пальцев, поскольку при их одновременном сведении и движении часто происходит сбой считывания.
Кисть не движется, но курсор всё равно дрожит

Использование скользящего среднего для сглаживания движения
Скользящее среднее позволяет значительно сгладить движение:
В качестве ещё одного улучшения я решил добавить по краям экрана безопасные зоны. В настоящее время позиция курсора основана на координатах большого пальца. Из-за этого для перемещения в край экрана необходимо соответственным образом сместить кисть, в результате чего она частично покидает зону видимости камеры. Решить эту проблему можно путём реализации простой линейной трансформации координат. Для этого я просто добавлю с каждой стороны экрана отступы.

Реализация линейной трансформации

До и после добавления отступов
Вернёмся к потере эффективности из-за задержки, о которой я говорил выше. И здесь я сталкиваюсь не только с этой проблемой, но и с тем, что для работы курсора вкладка постоянно должна быть открыта. Нам нужно найти способ использовать веб-версию MediaPipe так, чтобы она продолжала работать, даже когда её вкладка неактивна. Покопавшись в сети, я нашёл одно подходящее решение — Tauri [5], фреймворк для создания десктопных приложений с помощью веб-технологий и Rust.
Смысл в том, что он может выполнять фронтенд в качестве отдельной программы, а бэкенд, написанный на Rust, повысит эффективность взаимодействия с этим фронтендом. Такой механизм позволит нам симулировать ввод мышью, который у нас был в Python. Мне потребуется лишь немного скорректировать свой код. Ну а поскольку Rust мне незнаком, эту программу я реализовал с помощью ChatGPT.

Страница Tauri на GitHhub

Бэкенд на Rust для управления курсором мыши

JS-код, вызывающий бэкенд
И вот итоговый результат:
На этой стадии проект должен был завершиться, но я увлёкся просмотром на YouTube разных роликов, посвящённых реализации ввода с помощью отслеживания движений кисти. Я узнал, что подобный ввод жестами тоже весьма неплохо реализовали в проекте Meta Quest. Если в механизме Apple Vision Pro для определения положения курсора вам нужен датчик отслеживания движения глаз, то в технологии Meta Quest для этого достаточно направить кисть или палец на «экран». И я подумал, почему бы не добавить этот режим, чтобы камера в итоге смотрела прямо, а пользователю нужно было просто направлять пальцы в сторону экрана.
Вот ролик с YouTube-канала «Tricks Tips Fix» [6], демонстрирующий функцию отслеживания движений кисти в Meta Quest:
В этом случае, чтобы определить, куда указывает палец, нам уже недостаточно его координат x и y, как было ранее, когда камера смотрела вниз. Теперь нам нужно узнать, под каким углом палец направлен, а также расстояние z между камерой и этим пальцем. Эти значения мы используем для вычисления точки проецирования курсора на экране.

Как определяется положение курсора на экране
Для определения угла можно просто взять 2 из ключевых точек кисти и произвести необходимые тригонометрические расчёты. Мы проделаем это для углов YZ и XZ, чтобы получить значения по горизонтали и вертикали. Вычислить же расстояние между пальцем и камерой будет сложнее, так как ось Z в схеме ключевых точек означает не его удалённость от экрана, а лишь расстояние между точкой пальца и точкой запястья. Поэтому, чтобы определить нужное расстояние, потребуется поэкспериментировать с масштабом. Помните, я говорил о том, что расстояние между точками пальцев при приближении к экрану увеличивается и наоборот. Эту информацию я и использую для вычисления расстояния от камеры до кисти.

Пример для оси Y: используя угол наклона пальца и его расстояние до экрана, можно найти координату Y курсора.

Формула
Тестируя этот режим с камерой, направленной прямо, я наблюдаю больше дрожания курсора, чем в первом варианте, когда она смотрела вниз. Даже при использовании скользящего среднего, дрожание просто невыносимо. В итоге я занялся поиском более удачной альтернативы этому методу и нашёл One Euro Filter [7]. Это разновидность низкочастотного фильтра, который, по сути, сглаживает шумный ввод, как у нашего курсора. Чтобы сделать его пригодным под наши задачи, пришлось подкорректировать некоторые параметры, так как нас интересует не только уменьшение дрожания, но и адекватная задержка.
Плюсом ко всему, для ещё большего сглаживания дрожания я добавил пороговую обработку углов. Вот теперь использовать такой ввод стало более-менее удобно. Вдобавок ко всему, я также поменял в One Euro Filter скользящее среднее для режима «камера смотрит вниз», чтобы снизить задержку.
Курсор сильно дёргается даже при использовании скользящего среднего
После добавления One Euro Filter и пороговой обработки курсор стал более послушен, но дрожать не перестал
И всё же, сколько я ни старался, в режиме «камера смотрит вперёд» проблемы по-прежнему остались. Самая серьёзная — это дрожание курсора. При определённом положении и наклоне пальца показания MediaPipe оказываются очень нестабильными, сводя на нет всю фильтрацию и сглаживание. Вторая проблема возникает при сведении пальцев вместе — курсор слегка съезжает, в результате чего становится сложно кликнуть по объекту. Эти проблемы являются характерными для данной модели, и я понял, что никакое сглаживание и фильтрация их не исправят — только доработка самой модели.
Определённое положение кисти ведёт к нестабильности определения её ключевых точек моделью
Когда пальцы сведены вместе, курсор мыши съезжает
В этом проекте мне удалось реализовать свой замысел, который заключался в создании механизма ввода, аналогичного Apple Vision Pro или гарнитуре Meta Quest. Было очень интересно, так как мне довелось опробовать некоторые крутые технологии вроде MediaPipe, Tauri и Rust, а также немного размяться в геометрии. Найти этот проект вы можете в моём репозитории [8]. Я его протестировал только для Windows, так что в Linux или MacOS он может не работать.
Ниже показана пара роликов с демонстрацией конечного результата.
Работа в режиме «камера смотрит вниз»
Работа в режиме «камера смотрит вперёд»
В целом получается, что режим «камера смотрит вниз» работает довольно уверенно, а вот при её направлении вперёд из-за обозначенных мной проблем наблюдается нестабильность.
Примечания:
Автор: Bright_Translate
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/403089
Ссылки в тексте:
[1] MediaPipe: https://developers.google.com/mediapipe
[2] WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
[3] gRPC: https://grpc.io/
[4] евклидово расстояние: https://en.wikipedia.org/wiki/Euclidean_distance
[5] Tauri: https://tauri.app/
[6] YouTube-канала «Tricks Tips Fix»: https://www.youtube.com/c/TricksTipsFix
[7] One Euro Filter: https://gery.casiez.net/1euro/
[8] моём репозитории: https://github.com/reynaldichernando/pinch
[9] Источник: https://habr.com/ru/companies/ruvds/articles/860374/?utm_campaign=860374&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.