Современная фронтенд-разработка — это не только пользовательские интерфейсы. В нашем арсенале есть мощное Web API, WebGL, WebAssembly и множество библиотек и фреймворков для решения нетривиальных задач. Эти инструменты позволяют использовать в браузерах 2D- и 3D-графику, VR/AR, а также заниматься машинным обучением, компьютерным зрением и не ограничиваться тем, что мы уже умеем и знаем.

Меня зовут Ярослав Французяк, и я фронтенд-разработчик в GARPIX. В этой статье расскажу о таком инструменте, как фреймворк MediaPipe от Google. На основе готовых моделей он позволяет разработчикам внедрять сложные функции компьютерного зрения и обработки мультимедиа в веб-приложениях — распознавать лица, анализировать изображения, отслеживать движение, обрабатывать видео в реальном времени и многое другое. Мы разберём работу фреймворка на примере, погрузимся в векторную математику в трёхмерном пространстве и сложности распознавания ключевых точек лица.

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

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

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

Оставалось найти инструмент, который позволит создать такое решение в моей задаче.

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

Инструменты: фреймворк MediaPipe

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

Для работы с изображениями доступны модели для распознавания образов, сегментации объектов, определения жестов рук, положения тела в пространстве и другие. Среди них нас интересует модель распознавания ключевых точек. С её помощью можно воссоздать трёхмерную модель лица человека.

Давайте представим задачу определения взгляда пользователя на экране: есть экран, веб-камера и наблюдатель.

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

Представим все объекты этой сцены в трёхмерном пространстве и измерим их.

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

Инструменты: движок Three.js

Помимо MediaPipe, нам пригодится Three.js — 3D движок на WebGL. Строгой необходимости использовать его нет, он громоздкий и большей частью его возможностей мы пользоваться не будем. В нём реализована нужная математика, и с его помощью удобно демонстрировать логику работы системы.

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

В Three.js реализованы декларативные методы для сложения, вычитания, умножения и деления векторов, а также нахождения расстояния между ними.

Проектирование системы отслеживания взгляда

Итак, система будет состоять из трёх основных модулей.

  • FaceCamera — модуль работы с камерой. В нём инициализируется видеопоток и определяются некоторые характеристики внутреннего пространства камеры.

  • FaceTracker — модуль трекинга лица. Здесь обнаруживаются ключевые точки, определяется направление головы и вычисляются точки пересечения луча направления с плоскостью.

  • FaceControls — результирующий модуль, который вычисляет финальные координаты точки на экране.

Разберём эти модули по порядку. Начнём с камеры.

Модуль FaceCamera

Здесь мы инициализируем видеопоток через параметры.

class FaceCamera {

  ...

  async init() {

    const stream = await navigator.mediaDevices.getUserMedia({

      video: {

        facingMode: { ideal: 'user' },

        width: { ideal: 4096 },

        height: { ideal: 2160 },

      },

    })

    this.video.srcObject = stream

    await this.video.play()

  }

}

Сообщаем, что хотим получать видео с фронтальной камеры, и устанавливаем идеальное разрешение 4k. Если устройство не поддерживает такое разрешение, то будет установлено максимально приближенное к нему.

Видеопоток передаём в видео-элемент, чтобы он получал изображение с камеры, и запускаем трансляцию.

class FaceCamera {
  ...

  async init() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
          facingMode: { ideal: 'user' },
          width: { ideal: 4096 },
          height: { ideal: 2160 },
      },
    })

    this.video.srcObject = stream

    await this.video.play()
  }
}

Характеристики камеры

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

Световые лучи проходят сквозь линзу и проецируются на матрице. Расстояние, пройденное лучом от линзы до матрицы, называется «фокусным». Оно зависит от диагонали матрицы и диагонального угла обзора линзы. Зная фокусное расстояние и размеры лица, мы можем рассчитать реальное расстояние между лицом и камерой, а это необходимо для вычисления точки пересечения луча с экраном.

Забегая вперёд, вместо того, чтобы определять положение объектов в реальном пространстве, я предлагаю перенести всё во внутреннее пространство камеры. Тогда расстояние между лицом и камерой всегда будет соответствовать фокусному, а вот размеры экрана будут пропорционально меняться, в зависимости от приближения или удаления головы от камеры. Дальше покажу, как это работает.

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

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

class FaceCamera { ...
  get width() {
    return this.video.videoWidth
  }

  get height() {
    return this.video.videoHeight
  }

  get aspectRatio() {
    return this.width / this.height
  }

  get diagonal() {
    return Math.hypot(this.width, this.height)
  }

  get focalLength() {
    return (this.diagonal / 2) * (1 / Math.tan(this.diagonalFov / 2))
  }
}

На данном этапе 3D-сцена выглядит пустовато. Будем постепенно её наполнять.

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

class FaceCamera { ...
  plane!: THREE.Plane

  async init() {
    ...

    this.plane = new THREE.Plane(
      new THREE.Vector3(0, 0, -1), this.focalLength
    )
  }
}

Теперь поработаем с модулем трекинга лица.

Модуль FaceTracking

В модуле трекинга объявляем набор файлов, содержащих скомпилированный в WebAssembly код для работы с моделью.

import { FilesetResolver, FaceLandmarker } from '@mediapipe/tasks-vision'

class FaceTracker {

  landmarker!: FaceLandmarker

  async init() {

    const wasmFileset = await FilesetResolver.forVisionTasks('./wasm')

    this.landmarker = await FaceLandmarker.createFromOptions(wasmFileset, {

      baseOptions: {

        modelAssetPath: './face_landmarker.task',

        delegate: 'GPU',

      },

      runningMode: 'VIDEO',

      numFaces: 1,

      outputFacialTransformationMatrixes: true,

      outputFaceBlendshapes: false,

    })

  }

}

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

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

  • runningMode определяет, с чем работает модель — со статичной картинкой или с видео. В нашем случае, это видео.

  • numFaces говорит, сколько лиц можно определить на изображении. То есть потенциально систему могут использовать сразу несколько людей одновременно, и из этого можно придумать что-то интересное. Только предупреждаю, чем больше лиц, тем больше ресурсов потребуется модели, что, очевидно, скажется на производительности.

  • outputFacialTransformationMatrixes — флаг, который во включённом состоянии, помимо контрольных точек, даёт матрицу трансформации. Эта матрица содержит в себе информацию о том, как смещено, масштабировано и повёрнуто лицо относительно исходного положения. В исходном положении лицо просто смотрит вперёд.

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

  • outputFaceBlendshapes — интересный флаг, но в нашем примере не понадобится. Если он активен, модель будет выводить множество параметров, характеризующих мимику лица: открыт или закрыт рот, глаза, подняты ли брови. С помощью этих характеристик можно оживить специальные 3D-аватары, а также распознавать эмоции человека.

Метод update класса FaceTracker принимает экземпляр камеры, чтобы передать видеокадр в синхронный метод обнаружения ключевых точек. В случае успеха, метод вернёт множество обнаруженных точек, каждая из которых имеет x-, y- и z-координаты, но с нормализоваными значениями. Это значит, они имеют величину от нуля до единицы, и чтобы получить привычную размерность в пикселях, нужно умножить x- и z- координаты на ширину изображения, а y-координату — на высоту.

Положение лица на 3D-сцене

И вот на 3D сцене можно увидеть лицо: оно находится в углу, потому что положение центра изображения не совпадает с положением центра сцены.

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: [landmarks] } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points[i].set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

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

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: [landmarks] } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points[i].set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

Стало лучше, но лицо перевёрнуто и направлено в сторону от камеры. Чтобы это исправить, нужно инвертировать оси Y и Z.

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: [landmarks] } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points[i].set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

Теперь лицо отображается корректно.

Кстати поле points класса FaceTracker — это множество трёхмерных векторов, их количество всегда постоянно и равно сумме: 468 точек на лице и 10 точек, которые описывают форму радужек глаз.

Направление головы

Чтобы определить направление головы, добавим в FaceTracker два поля:

  • transform — буферная матрица 4х4, в которую мы вкладываем значения матрицы трансформации.

  • direction — вектор направления головы. Его значение равно третьему столбцу матрицы.

class FaceTracker { ...

  transform = new THREE.Matrix4()

  direction = new THREE.Vector3()

  update(camera: FaceCamera) {

    const {

      faceLandmarks: [landmarks],

      facialTransformationMatrixes: [transformationMatrix],

    } = this.landmarker.detectForVideo(...)

    ...

    this.transform.fromArray(transformationMatrix.data)

    this.direction.setFromMatrixColumn(this.transform, 2)

  }

}

Каждый столбец матрицы трансформации, кроме последнего, показывает направление головы по трём осям. Третья ось Z направлена на нас и соответствует направлению, куда указывает голова.

Когда определились с направлением лица, нужно представить его физическое воплощение.

Модель лица

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

class FaceTracker {

  ...

  ray = new THREE.Ray()

  update(camera: FaceCamera) {

    ...

    this.ray.set(

      this.points[168], this.direction

    )

  }

}

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

Теперь вычислим точку пересечения луча с абстрактной плоскостью, относительно которой закреплена камера.

class FaceTracker {

  ...

  ray = new THREE.Ray()

  intersection = new THREE.Vector3()

  update(camera: FaceCamera) {

    ...

    this.ray.intersectPlane(

      camera.plane,

      this.intersection

    )

  }

}

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

Перенос экрана в систему координат камеры

Просим пользователя сообщить нам диагональ экрана и получаем результат в дюймах. Чтобы перевести дюймы в пиксели, используем такой параметр как PPI — плотность пикселей. Эта величина показывает, сколько пикселей приходится на один дюйм.

С дюймами работать неудобно, переведём их в миллиметры. Но возникает вопрос — как перевести миллиметры в пиксели пространства камеры? Для этого нужно знать размеры какого-то реального объекта в милиметрах и пикселях. И на человеческом лице такой объект есть, это радужка глаза. Её диаметр примерно одинаков у всех взрослых людей и в среднем равен двенадцати миллиметрам. Так, имея реальный размер радужки в миллиметрах и пикселях, мы можем найти их отношение. Далее будем называть его «коэффициент радужки», при умножении на который миллиметры будут переводиться в пиксели.

У нас есть 10 контрольных точек, которые отвечают за форму радужки. Поэтому в классе FaceTracker добавим геттер, который возвращает её среднюю ширину.

class FaceTracker {

  ...

  get irisWidthInPx() {

    const rightIrisWidth =

      this.points[469].distanceTo(this.points[471])

    

    const leftIrisWidth =

      this.points[474].distanceTo(this.points[476])

    return (rightIrisWidth + leftIrisWidth) / 2

  }

}

Модуль FaceControls

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

type FaceControlsConfig = {

  ... irisWidth?: number; screenDiagonal: number }

class FaceControls { ...

  irisWidth: number

  screenHalfScaleReal: THREE.Vector3

  constructor({ ..., irisWidth = 12, screenDiagonal }) { ...

    this.irisWidth = irisWidth

    const ppMm =

      Math.hypot(window.screen.width, window.screen.height) /

      (screenDiagonal * 2.54 * 10)

    this.screenHalfScaleReal = new THREE.Vector3(

      window.screen.width  / 2 / ppMm,

      window.screen.height / 2 / ppMm

    )

  }

}

Коэффициент радужки и положение центра экрана

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

class FaceControls { ...
  screen = {
    halfScale: new THREE.Vector3(),
    center: new THREE.Vector3(),
  }

  get irisRatio() {
    return this.tracker.irisWidthInPx / this.irisWidth
  }

  loop() { ...
    this.screen.halfScale
      .copy(this.screenHalfScaleReal)
      .multiplyScalar(this.irisRatio)

    this.screen.center.setY(-this.screen.halfScale.y)
  }
}

Нарисуем экран на 3D-сцене.

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

Теперь мы можем переводить точку пересечения в пространство самого экрана.

class FaceControls { ...
  screen = {
    halfScale: new THREE.Vector3(),
    center: new THREE.Vector3(),
  }

  get irisRatio() {
    return this.tracker.irisWidthInPx / this.irisWidth
  }

  loop() { ...
    this.screen.halfScale
      .copy(this.screenHalfScaleReal)
      .multiplyScalar(this.irisRatio)

    this.screen.center.setY(-this.screen.halfScale.y)
  }
}

Для этого вычислим разницу между точкой пересечения и центром экрана. О чём говорит эта величина? Если точка находится справа сверху от центра, то она имеет отрицательное значение по оси X и положительное по Y. Слева снизу, наоборот, положительное по X и отрицательное по Y. Если точка находится в центре, у неё будут нулевые координаты. Эти значения актуальны для системы координат камеры, но не имеют ничего общего с системой координат внутри самого экрана.

Разделим полученный результат на размеры экрана в пространстве камеры и тем самым нормализуем координаты. Теперь, если точка находится на правой границе экрана, она имеет координату X, равную минус единице. На левой границе — координату Y, равную плюс единице.

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

Сглаживание точки пересечения

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

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

Инициализируем фильтр внутри класса FaceControls с такой конфигурацией, в ней мы указываем параметры:

  • sensorDimension — размерность пространства, в нашем случае оно двумерное.

  • covariance — скорость изменения состояния системы. Чем быстрее меняется система, тем она менее стабильна, а значит точка будет дёргаться. И, наоборот, если система меняется медленно, точка не будет успевать за движением головы.

import { KalmanFilter } from 'kalman-filter'

class FaceControls { ...
  kalman = new KalmanFilter({
    observation: { name: 'sensor', sensorDimension: 2 },
    dynamic: { name: 'constant-position', covariance: [0.005, 0.005] },
  })
  kalmanState: any // простите

  loop() { ...
    this.kalmanState = this.kalman.filter({
      previousCorrected: this.kalmanState,
      observation: [this.target.x, this.target.y],
    })
    const { mean } = this.kalmanState
    this.target.setX(mean[0][0])
    this.target.setY(mean[1][0])
    ...
  }
}

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

import { KalmanFilter } from 'kalman-filter'

class FaceControls { ...
  kalman = new KalmanFilter({
    observation: { name: 'sensor', sensorDimension: 2 },
    dynamic: { name: 'constant-position', covariance: [0.005, 0.005] },
  })
  kalmanState: any // простите

  loop() { ...
    this.kalmanState = this.kalman.filter({
      previousCorrected: this.kalmanState,
      observation: [this.target.x, this.target.y],
    })
    const { mean } = this.kalmanState
    this.target.setX(mean[0][0])
    this.target.setY(mean[1][0])
    ...
  }
}

Теперь задачу можно считать решенной. Мы получили базу для разработки полноценной системы отслеживания взгляда.

Возможности использования модели

Где ещё можно использовать эту же модель распознавания ключевых точек? Как примеры можно привести разработку игр, а также социальную и развлекательную сферы.

Разработка игр

Систему можно применять и для разработки интерактивных игр. Покажу, как работает модель, на примере игры Fruit Ninja.

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

Нам понадобится библиотека Matter.js. Это двухмерный физический движок.

В начале объявим основные сущности, укажем контейнер для рендера, гравитацию и размеры мира.

const canvas = document.createElement('canvas')

canvas.id = 'fruit'

document.body.appendChild(canvas)

const engine = Matter.Engine.create({ gravity: { y: 0.5 } })
const render = Matter.Render.create({ canvas, engine, options: {
  width: WIDTH,
  height: HEIGHT,
  wireframes: false,
}})

Matter.Render.run(render)

Клинок, разрезающий фрукты, выглядит как кривая линия, которая сужается от начала к концу. Положение острия постоянно обновляется на основе точки, куда направлена голова.

class Blade {
  trail: Point[] = [ { x: 0, y: 0 }, { x: 0, y: 0 } ]

  get angle() {
    return getAngleBetweenPoints(this.trail[0], this.trail[1])
  }
  get velocity() {
    return getDistanceBetweenPoints(this.trail[0], this.trail[1])
  }

  update({ x, y }: Point) {
    this.trail = [{ x, y }, ...this.trail.slice(0, BLADE.TRAIL_LENGTH - 1)]
  }

  render(ctx: CanvasRenderingContext2D) { }
}

blade.update(faceControls.target)

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

delayCounter++

if (delayCounter > delay) {
  const fruit = createCircle(SLING.X, SLING.Y)
  fruits.push(fruit)

  Matter.Composite.add(engine.world, fruit)
  Matter.Body.applyForce(fruit, fruit.position,
    Matter.Vector.create(
      Matter.Common.random(-0.1, 0.1),
      Matter.Common.random(-0.3, -0.5)
    )
  )

  delayCounter = 0
}

Если фрукт улетает за пределы экрана, мы удаляем его из памяти, уменьшаем очки и интервал выстрелов.

fruits = fruits.filter(fruit => {
  if (fruit.position.y > SLING.Y) {
    Matter.World.remove(engine.world, fruit)
    score--
    delay += DELAY_STEP
    return false
  }

  ...
})

Разрезанный фрукт удаляется, а вместо него создаются две половинки, которые летят в стороны, в зависимости от направления и скорости перемещения клинка.

fruits = fruits.filter(fruit => { ...
  if (!areCirclesColliding(bladeCircle, fruitCircle)) return true
  
  const velocity = blade.velocity * 0.002
  const pieces = [blade.angle, blade.angle + Math.PI].map((angle, i) => {
    ...
  })

  fruitPieces.push(...pieces)
  Matter.Composite.add(engine.world, pieces)
  Matter.World.remove(engine.world, fruit)
  score++
  delay -= DELAY_STEP
  return false
})

Со временем кусочки фруктов пропадают и удаляются из мира.

fruitPieces = fruitPieces.filter(piece => {
  const opacity = (piece.render.opacity || 1) - 0.02

  if (opacity <= 0) {
    Matter.World.remove(engine.world, piece)
    return false
  }

  piece.render.opacity = opacity

  return true
})

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

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

Так как модель способна распознавать сразу несколько лиц, можно играть с друзьями. Или добавить какие-нибудь эффекты на основе мимики: поднял брови — фрукты летят выше, опустил — ниже, раскрыл рот — вылетела бомба. Демку можно доработать.

Сфера развлечений

Также MediaPipe предлагает использовать модель для накладывания фильтров и эффектов на человеческое лицо.

Это действительно может быть востребовано среди контент-мейкеров, но я приведу более серьёзные и социально-значимые примеры.

Социальная сфера

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

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

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

Конфиденциальность и безопасность

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

Если пользователь предоставил доступ к камере, то требования предъявляются не только к браузеру, но и к разработчикам. MediaPipe заявляет, что вся обработка происходит исключительно на клиенте. В принципе это open source фреймворк, его исходный код можно внимательно изучить на предмет нарушения конфиденциальности, но неизвестно, что там происходит в момент сборки и публикации пакета в NPM.

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

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

Выводы

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

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

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

И хотя модель распознавания точек даёт нам некоторое представление о геометрии человеческого лица, результат, который мы получили, является идеализированным, ведь он основывается на канонической модели и не учитывает индивидуальные особенности пользователя. Если нужна более точная модель, придётся использовать систему в связке с сенсором глубины LIDAR, а это накладывает серьёзные ограничения на доступность и кросс-платформенность.

Комментарии (0)