Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки. liveness detection.. liveness detection. mediapipe face mesh.. liveness detection. mediapipe face mesh. компьютерное зрение.. liveness detection. mediapipe face mesh. компьютерное зрение. разминка.. liveness detection. mediapipe face mesh. компьютерное зрение. разминка. трекинг лица.

Меня зовут Степанян Альберт (albert.stepanyan@beorg.ru), и я всё ещё бегун по граблям и разработчик отдела R&D в компании «Биорг».

Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки - 1

В Древнем Риме говорили: «Mens sana in corpore sano» — в здоровом теле здоровый дух. Два тысячелетия спустя мы, айтишники, превратили эту мудрость в её полную противоположность: здоровый дух, сидящий в нездоровом теле перед монитором. Наш позвоночник принимает форму вопросительного знака, а шея начинает напоминать ржавый механизм — повернуть голову можно только всем корпусом, издавая при этом характерный хруст.

Мы в «Биорг» знаем об этой проблеме не понаслышке. Но, в отличие от большинства офисных работников, у нас под рукой оказалась система видеоидентификации, способная видеть каждое микродвижение человеческого лица. И как-то раз, тестируя систему видеоидентификации, мы поймали себя на мысли:

— То, что мы создавали для идентификации личности, по сути готовый трекер движений. Дай ему только интерфейс и он превратится в игру. Осталось только придумать, во что играть…

Попробовать разминку с Джонни и Джулией

Бесплатно, без смс, без регистрации и без подвоха. Под музыку или без! Нам не жалко. Мы вообще-то хотим просто шею размять, а не бизнес на этом строить.

Обычный антифрод и его необычная судьба

Одна из основных наших задач в R&D — борьба с фродом. Мы разработали систему видеоидентификации, которая определяет живость человека по движениям головы, согласно командам. Корректность их выполнения подтверждает, что перед нами живой пользователь.

О том, как мы пришли к этому решению и почему оно эффективно, уже рассказано в статье Развенчан маскарад подделок: прорыв в проверке документов

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

Тогда и родилась идея: а что, если мы сделаем не просто идентификацию, а полезную разминку для шеи, встроенную прямо в браузер?

Рождение Джонни и Джулии

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

Их история похожа на красивый роман. За плечами дипломы одного из лучших университетов мира и блестящие перспективы за рубежом. Они приехали в Россию как гости, желая увидеть страну. Россия встретила их искренним гостеприимством, новой культурой, непередаваемой атмосферой. Наша страна покорила их сердца навсегда. Очарованные увиденным, они приняли главное решение в своей жизни — остаться здесь, с нами.

Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки - 2

Почему двое? Мы добавили в виджет определение пола пользователя на старте — система определяет, кто перед камерой, и предлагает тренера того же пола. Это мелочь, но она делает взаимодействие чуть более персонализированным. Для определения пола используется открытая библиотека face-api.js с лицензией MIT. По желанию, пол инструктора можно сменить. Для этого есть специальная кнопка на экране – справа вверху.

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

Есть один нехитрый приём, который делает картинку цельной. Мы убираем фон у окна с пользователем и заменяем его на белый. То же самое делаем с фоном инструктора. В результате их фоны сливаются с общим белым фоном страницы, и визуально окна перестают быть «окнами» — они превращаются в единую композицию. Пользователь видит себя и тренера в одном пространстве, а не в двух отдельных квадратах.

Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки - 3

Как мы считаем синхронизацию

Разминка состоит из 30 движений, и они каждый раз генерируются случайным образом. Никаких заучиваний — каждый новый подход будет отличаться от предыдущего. Это не даёт скучать и не позволяет мозгу «засыпать» на автоматических движениях.

Разминка состоит из нескольких упражнений. У каждого есть ожидаемая длительность — t.time. Пока пользователь выполняет движение, мы замеряем его фактическую скорость и фиксируем задержку (тот самый delay) — насколько пользователь отстаёт от тренера.

Когда все упражнения завершены, мы суммируем две величины:

  • totalExp — общее ожидаемое время всех движений;

  • totalDelay — суммарное отставание пользователя.

Дальше простая формула:

raw = 100 - (totalDelay / totalExp * 100);
score = Math.max(0, Math.min(100, raw));

То есть, если вы выполнили разминку идеально синхронно — твой скор стремится к 100. Но мы оставили люфт, потому что в реальном мире есть погрешности: камера может чуть дрожать, браузер — подвисать, а твоя шея — иметь свою точку зрения на то, как быстро надо поворачиваться.

И ещё один важный нюанс по геймплею. Мы заметили в тестах, что некоторые пользователи не могли с первого раза повторить то или иное движение. Может шея затекла, может не поняли траекторию. Зависать на одном движении бесконечно — плохая идея. Это раздражает и убивает смысл разминки.

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

Итоговый результат мы очеловечиваем:

if (score >= 98) text = "Синхронизация 100% — Вы копия тренера!";
else if (score >= 90) text = "90% синхронизации — Осталось совсем чуть‑чуть!";
else if (score >= 70) text = "70% синхронизации — Подтягиваемся к идеалу!";
else if (score >= 50) text = "50% синхронизации — Половина пути пройдена!";
else text = "30% синхронизации — Ловим темп тренера!";

98%, а не 100% специально обозначено как идеал. Потому что перфекционизм — это хорошо в коде, но вредно для здоровья. Если ты получил 98% — считай, что ты чемпион. А вот 30% — это повод не расстраиваться, а попробовать ещё раз, просто поймав ритм.

Как мы подружили разминку с телефонами

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

— А на телефоне это работать будет?

Первая мысль была — определять устройство по размеру экрана. Но это оказалось ловушкой. Современные телефоны имеют диагональ почти как у небольших ноутбуков, а планшеты и вовсе размывают границы.

Я лично столкнулся с этой проблемой на своём Redmi Note 9. Экран у него, конечно, не как у ноутбука, но по ширине в пикселях он вполне сопоставим с некоторыми дешёвыми ноутбуками. Система считала, что раз экран достаточно широкий — значит, это компьютер, и отказывалась переключаться в мобильный режим. Кнопки оставались мелкими, интерфейс не адаптировался, а инструктор с пользователем пытались уместиться в той же раскладке, что и на большом экране. Работать было невозможно.

Стало понятно, размер экрана  — это ненадёжный признак, и нужно искать что-то другое.

Тогда я пошёл другим путём: проверять наличие датчика ориентации, но не одним, а двумя признаками:

function isMobileDevice() {
    return 'DeviceOrientationEvent' in window && 'ontouchstart' in window;
}

Первое условие — поддержка события ориентации (есть у телефонов и планшетов). Второе — поддержка касаний (отсекает компьютеры, даже если у них случайно затесался акселерометр). Вместе 2 условия дают почти 100% точность.

Если проверка проходит — добавляем на страницу класс mobile:

if (isMobileDevice()) {
    document.body.classList.add('mobile');
}

И дальше уже через CSS управляем всем, что должно отличаться. Но самое главное — это размер и положение окна с пользователем и инструктором.

На компьютере, как и на телефоне, в горизонтальном режиме, мы используем классическую раскладку: пользователь слева, инструктор справа. Это естественно для широкого экрана — взгляд скользит слева направо, и ты видишь одновременно и себя, и тренера.

На телефоне в вертикальном режиме всё иначе. Экран узкий, и раскладка «слева-направо» не работает. Поэтому пользователь занимает верхнюю половину экрана, а окно с инструктором — нижнюю. Так пользователь видит и тренера, и себя одновременно.

И это не просто визуальная прихоть. Если окно с пользователем слишком маленькое — он не видит, правильно ли выполняет движения. Если слишком большое — перекрывает инструктора: теряется смысл «повторяй за мной». Поэтому мы потратили несколько итераций, чтобы найти идеальные пропорции для каждого типа устройства и каждой ориентации.

Сам полноэкранный режим мы, кстати, реализовали и для компа, и для телефона:

const hint = document.querySelector('.exit-fullscreen-hint');
if (hint) {
    hint.style.display = 'none';
}

Справедливости ради: подсказка про выход из полноэкранного режима появляется и на компьютере, и на телефоне. Просто она разная. На компьютере — «Нажмите Esc для выхода из полноэкранного режима». На телефоне — «Проведите снизу вверх для выхода из полноэкранного режима». В обоих случаях она исчезает через 3 секунды, чтобы не мешать пользователю разминаться.

Трекинг лица: локальная сборка без сюрпризов

Для трекинга лица в разминке мы используем MediaPipe Face Mesh. Это открытая библиотека от Google, распространяемая под лицензией Apache 2.0. Мы взяли её в локальную сборку, поэтому всё работает стабильно и без внешних запросов к CDN. Никаких сюрпризов с недоступными серверами или обновлёнными API. Только предсказуемая логика в твоём браузере. 

Инициализируем трекер так:

this.faceLandmarker = new FaceMesh({
    locateFile: (file) => /static/mediapipe-face-mesh/${file}
});

this.faceLandmarker.setOptions({
    maxNumFaces: 1,
    refineLandmarks: false,
    minDetectionConfidence: 0.5,
    minTrackingConfidence: 0.5
});

Обратите внимание на refineLandmarks: false — мы выключили его специально. Нам не нужна микро-детализация губ и глаз, только общий контур головы. Экономия ресурсов браузера — наше всё. И, кстати, именно благодаря локальной сборке мы можем настраивать параметры под свои задачи, не оглядываясь на внешние зависимости.

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

И главное, что MediaPipe работает независимо от того, как ты держишь телефон: вертикально или горизонтально. Главное, чтобы лицо было в кадре!

Какой режим выбрать?

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

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

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

Музыка — двигатель прогресса (и шеи)

Когда прототип с Джонни и Джулией заработал, мы дали его потестить коллегам. Офис разделился на два лагеря.

Первые, хмуро глядя в монитор, синхронно поворачивали головы и напоминали роботов на конвейере. Вторые незаметно покачивались в такт и слегка пританцовывали, даже когда никто не смотрел. Разница была в одном: у вторых на фоне тихо играла музыка.

Мы решили, что пусть пользователь сам решает, хочет ли он разминаться в тишине или под ритмичный трек. Так появились две кнопки: «Под музыку» и «Без музыки».

И если с «тихим» режимом всё просто, то с музыкой пришлось повозиться. Мы перебрали десяток треков разного темпа, пока не нашли тот самый, который совпадает с комфортной скоростью поворота головы при разминке. Слишком быстро — пользователь не успевает, слишком медленно — скучно. Идеальный темп нашёлся где-то около 140 BPM, как у хорошего транс-хауса.

И вот теперь представь: ты сидишь после пяти часов дебаггинга, запускаешь разминку, выбираешь «Под музыку», и на экране появляется симпатичная Джулия, которая под ритмичный бит начинает поворачивать голову. Ты повторяешь и незаметно для себя перестаёшь думать о багах и просто двигаешься. Это работает как медитация.

Тайна, которую мы не раскроем (но вы всё равно узнаете)

Когда разминка заканчивается и на экране появляется твой результат что-то происходит.

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

Так что когда дойдёте до конца, поделитесь в комментариях, какой бонус увидели и понравилось ли вам.

Заключение

История этой разминки — отличное напоминание о том, что лучшие идеи рождаются не из строгих ТЗ. Мы не планировали делать разминку. Мы планировали делать антифрод. А заодно получили продукт, который одинаково полезен и для безопасности системы, и для шеи разработчика.

А вообще, Джонни и Джулии у нас нравится. Они уже члены нашей команды — ходят на парады и поздравляют нас с праздниками.

Как мы детектили фрод, а получили Джонни и Джулию: история одной браузерной разминки - 4

Автор: Albertino_S

Источник