Не так давно я был свидетелем запуска Apple Vision Pro. Презентация оказалась очень интересной, но больше всего моё внимание зацепила одна деталь — дистанционное управление вводом с помощью пальцев. Выглядит очень интуитивно — использовать перемещение и сведение пальцев для управления курсором на экране. Меня этот механизм заинтриговал, и я решил воссоздать его сам.
▍ План
Основная цель — реализовать механизм, позволяющий использовать в качестве устройства ввода для компьютера кисть руки. Соответствующая программа должна обрабатывать перемещение курсора и клики с его помощью. Очевидно, что для этого нам потребуется камера, которую я направлю вниз, поскольку именно в этой области будут находиться руки в процессе использования компьютера.
Далее нам нужен способ обнаружения положения кисти и пальцев для управления курсором. Это я реализую с помощью инструмента MediaPipe от Google, представляющего набор готовых решений для машинного обучения. Среди этих решений есть функция обнаружения ключевых точек кисти, которая нам и нужна. Ну и последним делом мне каким-то образом потребуется симулировать ввод мыши.
Обнаружение ключевых точек руки с помощью MediaPipe
Общая схема механизма
▍ Первые шаги
Опираясь на эту общую схему, можно воспользоваться версией MediaPipe для Python. Мы запрограммируем OpenCV на считывание видеопотока с камеры и его передачу в MediaPipe, после чего полученные контрольные точки используем для симуляции работы мыши. Вроде несложно. За исключением того, что работает такая схема не очень. Стоило мне только наладить подключение между OpenCV и MediaPipe, как я столкнулся с сильными тормозами.
Версия для Python сильно тормозит из-за проблем с OpenCV
Покопавшись в этом вопросе, я выяснил, что причина кроется в OpenCV, а именно в работе функции waitKey
. Я до сих пор не нашёл, как это исправить, поэтому не стану тратить время и просто откажусь от Python.
▍ Глупая идея
Изучая возможность использования MediaPipe, в одном из демо я увидел, что есть веб-версия этого пакета инструментов, которая, как выяснилось, работает очень плавно. В итоге я решил использовать её. Но оставалась одна проблема — как управлять мышью через браузер? Тогда мне в голову пришла безумная идея: раз я могу запустить MediaPipe локально, почему бы мне не использовать эту её версию в качестве бэкенда для симуляции мыши? Нужно лишь понять, как наладить взаимодействие фронтенд-версии MediaPipe с её бэкенд-вариантом.
Веб-версия работает плавно
▍ Симуляция мыши
Теперь нужно было найти способ наладить взаимодействие фронтенда с бэкендом. Я рассматривал три варианта: простой HTTP-запрос, WebSocket и gRPC с потоковой передачей. Учитывая, что мне нужна минимальная задержка, HTTP-запросы отпали сразу. Осталось два варианта, оба из которых подразумевают потоковую передачу данных. В итоге я остановился на WebSocket, так как он позволяет наладить между клиентом и сервером диалог в реальном времени, который необходим для нашего случая. Да и механизм gRPC мне не особо знаком.
Я настроил с помощью Python простой WebSocket-сервер, который будет получать JSON-строку, содержащую координаты x
и y
для перемещения курсора. Теперь мне нужно просто объединить координаты одного пальца для отправки бэкенду — я использую в качестве ориентира кончик большого.
WebSocket-сервер, получающий информацию для управления движением курсора
Управление курсором через браузер:
Сработало! Причём на удивление хорошо. Однако управление курсором через браузер кажется совсем неудачным вариантом. И хотя задержка не особо заметна, я думаю, она может сказываться на общей эффективности. Но пока этот нюанс оставим.
Перейдём к логике обработки кликов. Чтобы фиксировать события клика, необходимо обнаруживать «щипательное» движение между большим и указательным пальцами. Для этого нужно просто измерять промежуток между их кончиками, используя евклидово расстояние. Если оно будет оказываться меньше установленного порога, будет вызываться событие движения курсора вниз, а если больше — событие движения вверх. Обратите внимание, что мы вместо клика используем движение курсора вниз/вверх — это нужно для поддержки перетаскивания.
Такое решение прекрасно работает, но при приближении руки к камере возникает проблема. Поскольку мы перемещаем 3D-объект в 2D-пространстве, приближение кисти к камере также ведёт к увеличению расстояния между кончиками пальцев.
Расстояние между кончиками пальцев: удалённо и вблизи
Решить эту проблему можно обходным путём, используя относительное расстояние. Мы будем вычислять промежуток между кончиками пальцев и соответствующими костяшками их суставов, который при приближении кисти к камере будет увеличиваться, а затем сравнивать этот промежуток с расстоянием между кончиками. Таким образом, мы будем получать правильное расстояние, независимо от удалённости кисти от камеры.
Вычисление относительного расстояния между кончиками пальцев
▍ Дрожание
Следующей проблемой стало дрожание. Заметьте, что даже когда моя кисть просто неподвижно лежит на столе, курсор дрожит. Это характерный недочёт самой модели обнаружения ключевых точек, и единственный способ исправить его — использовать более качественную модель. Однако не всё потеряно. Пока что мы можем реализовать в качестве позиции курсора простое скользящее среднее. Тогда дрожание уменьшится, и движение станет плавным.
Единственный нюанс в том, что с увеличением буфера для скользящего среднего увеличивается и задержка. Кроме того, я также решил добавить буфер для состояния сведения пальцев, поскольку при их одновременном сведении и движении часто происходит сбой считывания.
Кисть не движется, но курсор всё равно дрожит
Использование скользящего среднего для сглаживания движения
Скользящее среднее позволяет значительно сгладить движение:
В качестве ещё одного улучшения я решил добавить по краям экрана безопасные зоны. В настоящее время позиция курсора основана на координатах большого пальца. Из-за этого для перемещения в край экрана необходимо соответственным образом сместить кисть, в результате чего она частично покидает зону видимости камеры. Решить эту проблему можно путём реализации простой линейной трансформации координат. Для этого я просто добавлю с каждой стороны экрана отступы.
Реализация линейной трансформации
До и после добавления отступов
▍ Идея получше
Вернёмся к потере эффективности из-за задержки, о которой я говорил выше. И здесь я сталкиваюсь не только с этой проблемой, но и с тем, что для работы курсора вкладка постоянно должна быть открыта. Нам нужно найти способ использовать веб-версию MediaPipe так, чтобы она продолжала работать, даже когда её вкладка неактивна. Покопавшись в сети, я нашёл одно подходящее решение — Tauri, фреймворк для создания десктопных приложений с помощью веб-технологий и Rust.
Смысл в том, что он может выполнять фронтенд в качестве отдельной программы, а бэкенд, написанный на Rust, повысит эффективность взаимодействия с этим фронтендом. Такой механизм позволит нам симулировать ввод мышью, который у нас был в Python. Мне потребуется лишь немного скорректировать свой код. Ну а поскольку Rust мне незнаком, эту программу я реализовал с помощью ChatGPT.
Страница Tauri на GitHhub
Бэкенд на Rust для управления курсором мыши
JS-код, вызывающий бэкенд
И вот итоговый результат:
▍ Разработка ещё одного режима
На этой стадии проект должен был завершиться, но я увлёкся просмотром на YouTube разных роликов, посвящённых реализации ввода с помощью отслеживания движений кисти. Я узнал, что подобный ввод жестами тоже весьма неплохо реализовали в проекте Meta Quest. Если в механизме Apple Vision Pro для определения положения курсора вам нужен датчик отслеживания движения глаз, то в технологии Meta Quest для этого достаточно направить кисть или палец на «экран». И я подумал, почему бы не добавить этот режим, чтобы камера в итоге смотрела прямо, а пользователю нужно было просто направлять пальцы в сторону экрана.
Вот ролик с YouTube-канала «Tricks Tips Fix», демонстрирующий функцию отслеживания движений кисти в Meta Quest:
В этом случае, чтобы определить, куда указывает палец, нам уже недостаточно его координат x
и y
, как было ранее, когда камера смотрела вниз. Теперь нам нужно узнать, под каким углом палец направлен, а также расстояние z
между камерой и этим пальцем. Эти значения мы используем для вычисления точки проецирования курсора на экране.
Как определяется положение курсора на экране
Для определения угла можно просто взять 2 из ключевых точек кисти и произвести необходимые тригонометрические расчёты. Мы проделаем это для углов YZ
и XZ
, чтобы получить значения по горизонтали и вертикали. Вычислить же расстояние между пальцем и камерой будет сложнее, так как ось Z
в схеме ключевых точек означает не его удалённость от экрана, а лишь расстояние между точкой пальца и точкой запястья. Поэтому, чтобы определить нужное расстояние, потребуется поэкспериментировать с масштабом. Помните, я говорил о том, что расстояние между точками пальцев при приближении к экрану увеличивается и наоборот. Эту информацию я и использую для вычисления расстояния от камеры до кисти.
Пример для оси Y
: используя угол наклона пальца и его расстояние до экрана, можно найти координату Y
курсора.
Формула
Тестируя этот режим с камерой, направленной прямо, я наблюдаю больше дрожания курсора, чем в первом варианте, когда она смотрела вниз. Даже при использовании скользящего среднего, дрожание просто невыносимо. В итоге я занялся поиском более удачной альтернативы этому методу и нашёл One Euro Filter. Это разновидность низкочастотного фильтра, который, по сути, сглаживает шумный ввод, как у нашего курсора. Чтобы сделать его пригодным под наши задачи, пришлось подкорректировать некоторые параметры, так как нас интересует не только уменьшение дрожания, но и адекватная задержка.
Плюсом ко всему, для ещё большего сглаживания дрожания я добавил пороговую обработку углов. Вот теперь использовать такой ввод стало более-менее удобно. Вдобавок ко всему, я также поменял в One Euro Filter скользящее среднее для режима «камера смотрит вниз», чтобы снизить задержку.
Курсор сильно дёргается даже при использовании скользящего среднего
После добавления One Euro Filter и пороговой обработки курсор стал более послушен, но дрожать не перестал
И всё же, сколько я ни старался, в режиме «камера смотрит вперёд» проблемы по-прежнему остались. Самая серьёзная — это дрожание курсора. При определённом положении и наклоне пальца показания MediaPipe оказываются очень нестабильными, сводя на нет всю фильтрацию и сглаживание. Вторая проблема возникает при сведении пальцев вместе — курсор слегка съезжает, в результате чего становится сложно кликнуть по объекту. Эти проблемы являются характерными для данной модели, и я понял, что никакое сглаживание и фильтрация их не исправят — только доработка самой модели.
Определённое положение кисти ведёт к нестабильности определения её ключевых точек моделью
Когда пальцы сведены вместе, курсор мыши съезжает
▍ Заключение
В этом проекте мне удалось реализовать свой замысел, который заключался в создании механизма ввода, аналогичного Apple Vision Pro или гарнитуре Meta Quest. Было очень интересно, так как мне довелось опробовать некоторые крутые технологии вроде MediaPipe, Tauri и Rust, а также немного размяться в геометрии. Найти этот проект вы можете в моём репозитории. Я его протестировал только для Windows, так что в Linux или MacOS он может не работать.
Ниже показана пара роликов с демонстрацией конечного результата.
Работа в режиме «камера смотрит вниз»
Работа в режиме «камера смотрит вперёд»
В целом получается, что режим «камера смотрит вниз» работает довольно уверенно, а вот при её направлении вперёд из-за обозначенных мной проблем наблюдается нестабильность.
Примечания:
- Минимизировать смещение при сведении пальцев можно, положив большой палец сбоку на кончик среднего.
- Для вычисления угла в режиме «камера смотрит вперёд», особенно вертикального, я добавил смещение. Поскольку камера расположена в верхней части экрана, оно позволяет скорректировать проецирование, когда курсор оказывается ниже точки, в которую указывает палец.
- Мне ещё нужно найти способ обеспечить фоновую работу приложения Tauri — пока что сворачивание его окна ведёт к остановке курсора. Поэтому для переключения окон просто кликайте по нужному, не сворачивая Tauri.
Автор: Bright_Translate