В кадре весь класс, а слушаться нужно учителя: как M2 отличает свои руки от чужих

Привет, Хабр! Это первая статья уже в блоге M2 Holo Lab. До этого я писал со своего аккаунта: сначала как крутить 3D-модели голыми руками через обычную вебку, потом про мозг, мышь, дом и ДНК.

Если вкратце: M2 это 3D-лаборатория в браузере. Камера следит за руками, а ты жестами крутишь, разбираешь и собираешь модели. Руки распознаёт MediaPipe, рисует Three.js, видео с камеры никуда не уходит.

После прошлых статей мне стали писать учителя, и сейчас у меня планируется пилот со школой. И вот тут вылезла проблема, о которой я дома даже не думал. Учитель стоит перед камерой, а за спиной у него сидит класс. У каждого тоже есть руки, и камера видит их все.

Ниже расскажу, как я научил M2 слушаться только того, кто ведёт урок. Без датчиков глубины, без второй камеры и без новой нейронки. Только те же 21 точка на руку, что и раньше.

Как было раньше: «берём любые две руки»

Раньше M2 просил у нейронки две руки и считал их руками пользователя. В MediaPipe HandLandmarker это вообще один параметр:

const HAND_O = { runningMode: 'VIDEO', numHands: 2, /* ... */ };

Дома за ноутом этого хватает: в кадре один человек, значит и руки его. А в классе нейронка понятия не имеет, кто тут главный. Она отдаёт первые две руки, которые нашла. Иногда это руки учителя, а иногда рука пацана с третьей парты, который просто почесал голову.

Отсюда две проблемы:

  • Перехват. Учитель держит деталь двигателя, на секунду опускает руку, и её место тут же занимает чья-то рука сзади. Модель улетает в другой конец экрана.

  • Вытеснение. Даже когда обе руки учителя в кадре, нейронка может отдать одну его и одну чужую. Слотов всего два, и чужая рука просто занимает один.

Первый шаг вышел смешной: чтобы игнорить чужие руки, нейронке надо показывать их больше. Теперь M2 просит до четырёх рук. Так руки учителя попадают в выдачу, даже когда сзади кто-то машет. А какие из них свои, дальше решает уже сам M2.

Визуализация на синтетических руках: так работал старый принцип «любые две руки»

Идея: как автопилот держит свою полосу

Первое, что приходит в голову: просто брать самые крупные руки. Но это работает, только пока все стоят на месте. Учитель отшёл на шаг, и его руки стали мельче. Ученик с первой парты потянулся вперёд, и его рука стала крупнее. По одному кадру это не различить.

В итоге я подсмотрел решение у автопилота в машине. Он не ищет «лучшую полосу» в каждом кадре заново. Он один раз цепляется за свою и дальше следит, куда она идёт. Разметка на секунду пропала? Он не прыгает на соседнюю, а ждёт её там, где она была.

С руками ровно то же. M2 больше не спрашивает «какие руки тут главные?». Он спрашивает «куда делись руки, которые я уже принял за свои?». Из этого выросло три правила:

  1. Новую руку берём, только если она достаточно крупная.

  2. Рука, которая продолжает движение своей, остаётся своей.

  3. Своя рука пропала, и её место какое-то время никому не отдаём.

Всё это сидит в одной функции-фильтре между нейронкой и всем остальным. На вход до четырёх рук, на выход не больше двух. Жесты, щипки и физика моделей про чужие руки даже не узнают.

Правило 1: свои руки крупнее

Учитель стоит к камере ближе всех, значит и ладони у него в кадре самые крупные. Осталось нормально этот размер мерить.

Размер всей руки не подходит. Кулак и раскрытая ладонь одного человека отличаются в разы, а жесты в M2 как раз из них и состоят. Поэтому считаю только ладонь, без пальцев. Беру два расстояния между точками MediaPipe: длину от запястья до основания среднего пальца и ширину от указательного до мизинца. Из двух беру большее. Если рука повёрнута ребром к камере, ширина схлопывается, а длина остаётся.

// точки MediaPipe: 0 — запястье, 9 — основание среднего, 5 и 17 — основания указательного и мизинцаconst palmSize = (p) => Math.max( dist(p[0], p[9]), 1.15 * dist(p[5], p[17]));

Дальше нужен эталон, с чем сравнивать. Калибровка в M2 и так была: перед работой человек показывает ладонь, щипок и кулак, и жесты подстраиваются под его руки. Теперь на шаге с ладонью она ещё запоминает медианный размер ладони в кадре. Пропустил калибровку? Тогда эталон набирается сам из рук, которые уже приняты за свои. Двигается он медленно, с весом 4% за кадр. Так он идёт за учителем, если тот отошёл, но не скачет от одного случайного кадра.

Новую руку M2 считает своей, только если:

  • она не мельче 62% эталона;

  • она не мельче 55% самой крупной руки в кадре. Это страховка на случай, когда эталона ещё нет.

Человек за партой в паре метров позади учителя в кадре заметно мельче, так что до 62% его руки не дотягивают. А если включено 3D по положению головы, есть ещё одна проверка. В этом режиме M2 и так знает, где лицо главного человека, и новая рука должна быть не дальше шести ширин этого лица от него. Рука в другом конце кадра точно чужая.

Правило 2: своя рука та, что продолжает движение

Одного размера мало. Учитель откинулся назад, чтобы показать классу разобранный двигатель, и его ладони стали мельче порога. Если смотреть только на размер, M2 его тут же бросит.

Поэтому руку, которую уже приняли за свою, проверяю по другому, более мягкому правилу. В каждом кадре M2 ищет руку, которая находится рядом с прошлым местом своей и похожа по размеру:

  • «Рядом» значит в пределах 2,2 размера ладони, но не меньше 18% ширины кадра. Нижняя граница нужна для резких жестов: за один кадр рука может улететь далеко.

  • «Похожа по размеру» значит от 0,45 до 2,2 от размера в прошлом кадре.

Вся фишка в разнице порогов. Чтобы стать своей, рука должна быть не мельче 62% эталона. Чтобы остаться своей, ей достаточно не сжаться вдвое между соседними кадрами. Учитель может спокойно отойти: руки мельчают кадр за кадром, а управление остаётся у него. А чужая рука такого же размера не пройдёт, потому что у неё нет истории.

В одном из тестов руки за 40 кадров уменьшались до 60% от исходного размера, то есть ниже порога для новых рук. Обе остались своими.

Правило 3: рука пропала, но место занято

Руки из кадра пропадают постоянно. Опустил руку, повернул ребром, провёл перед лицом, или нейронка просто моргнула на пару кадров. Старая логика в такой момент хватала первую попавшуюся руку.

Теперь своя рука после пропажи держит место 1,5 секунды. M2 помнит, где она была и какого размера. Чужую руку на это место не берём, даже если в кадре больше никого нет. А свою подхватываем по правилу 2, как только она появится рядом с тем местом, где пропала.

Слотов по-прежнему два, так что правило укладывается в одну строку: новую руку берём, только если свои руки плюс недавно пропавшие ещё не занимают оба.

Почему именно полторы секунды. Если меньше, чужие руки успевают проскочить, пока учитель перехватывает деталь. Если больше, учитель убрал руку, поднял её в другом месте и слишком долго ждёт, пока M2 её увидит.

Визуализация на синтетических руках, отбор по тем же правилам и порогам, что в M2

Два подвоха, которые вылезли по ходу

Холодный старт. Учитель ещё не подошёл к камере, а в кадре уже машет кто-то с задней парты. Эталона нет, сравнивать не с чем, и чужая рука честно становится «своей». Дальше правило 2 играет против нас: оно держит эту руку, и когда подходит учитель, один слот уже занят.

Лечится простым наблюдением: своя ладонь всегда среди самых крупных в кадре. Если принятая рука вдруг мельче 55% самой крупной, значит это чужая, которая проскочила в пустой кадр. M2 её забывает и освобождает место для настоящих рук.

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

Ну и на всякий случай есть выключатель. index.html?allhands возвращает старое поведение. А отброшенные руки пишутся в диагностическую запись вместе с размерами всех рук в кадре, так что пороги можно крутить по цифрам, а не на глаз.

Та же сцена с отбором своих рук. Визуализация на синтетических руках, правила и пороги как в M2

Как проверять, когда под рукой нет класса

Собирать класс ради каждой правки порога слегка нереально, поэтому фильтр я гонял на синтетических руках. Тестовый скрипт на Node вырезает функцию-фильтр прямо из рабочего index.html. Так проверяется ровно тот код, который работает, а не копия. Рука в тесте это 21 точка с заданным центром и размером ладони, а сценарий это последовательность кадров с шагом 33 мс, как при 30 fps.

Сценарий

Что должно получиться

Учитель с двумя руками, за спиной два человека

взяты только руки учителя

Правая рука учителя ушла, чужая в кадре

чужая не взята

Правая вернулась туда же

подхвачена

Крупная третья рука соседа

игнорируется

Учитель откинулся, руки сжались до 60%

обе остались своими

Холодный старт: в кадре только чужая рука

взята, других вариантов нет

Потом подошёл учитель

чужая отпала, взяты руки учителя

Учитель с двумя руками, без помех

обе взяты

Обе руки пропали на полсекунды, в кадре чужая

ничего не взято

Обе вернулись

снова свои

После калибровки в кадре одна чужая рука в 55% от ладони учителя

не взята

Все 11 сценариев проходят. Потом я ещё загрузил всю страницу в headless Chrome с фейковой камерой и прогнал фильтр уже внутри приложения. Результат тот же, ошибок на странице нет.

Шестой сценарий я специально оставил в таблице. Пока в кадре никого, кроме ученика с задней парты, M2 слушается его, и это намеренно. Иначе пришлось бы запретить мелкие руки вообще, и тот, кто сидит далеко от ноута, не смог бы работать совсем.

Что пока не решено

У подхода есть свои границы, и лучше сказать о них сразу:

  • Человек рядом с учителем. Если ученик стоит на том же расстоянии от камеры, по размеру его не отличишь. Пока у учителя обе руки в кадре, спасает лимит слотов. Но если учитель убрал руку дольше чем на полторы секунды, его место может занять сосед. И ещё одно, это вылезло, когда я делал гифки для статьи. Если рука соседа оказалась в паре ладоней от места, где пропала рука учителя, M2 решает, что это та же рука, и отдаёт ей управление сразу, без полутора секунд ожидания. Правило 2 тут работает против нас.

  • Передача управления. Иногда учитель хочет, чтобы ученик сам собрал двигатель. Сейчас для этого ученик просто встаёт на место учителя, отдельного жеста «передать» нет.

  • Настоящий класс. Вживую режим я пока не проверял. Синтетика показывает, что логика верная, но свет, угол камеры и десять машущих рук сразу она не заменит. Пороги 62% и 1,5 секунды почти наверняка придётся подкрутить по записям с настоящего урока. Для этого и нужен пилот со школой.

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

M2 сейчас в закрытом тестировании. Если вы учитель и хотите попробовать его на уроке, на m2holo.com есть форма заявки на пилот. А если вы решали похожую задачу по-другому, например через трекинг всего тела или оценку глубины по одной камере, пишите в комментариях, сравним.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев