Введение:

В этой статье основное внимание уделяется шагам, необходимым для реализации облака точек сетки лица в реальном времени с помощью Three.js и Tensorflow.js. Он предполагает предварительное знание асинхронного javascript и основ Three.js, он не будет охватывать основы.

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

В этой статье также будет реализован проект объектно-ориентированным способом с большим количеством абстракций, поэтому базовое понимание классов в Typescript является преимуществом.

Этапы реализации:

  1. Получите настройку Three Js
  2. Создание видеоданных с веб-камеры
  3. Создайте детектор сетки лица
  4. Создать пустое облако точек
  5. Подача информации об отслеживании в облако точек
  6. Настройте Three.js:

Поскольку наша цель в этом уроке — визуализировать облако точек лица, нам нужно начать с настройки нашей сцены Three js.

Данные и методы, необходимые для настройки сцены, инкапсулированы в фабричном классе под названием ThreeSetUp в файле sceneSetUp.ts. Этот класс отвечает за создание всех необходимых объектов сцены, таких как рендерер, камера и сама сцена. Он также инициирует обработчик изменения размера элемента холста. Этот класс имеет следующие общедоступные методы:

а. getSetUp: эта функция возвращает объект, содержащий камеру, сцену, средство визуализации и информацию о размерах холста.

getSetUp(){
    return {
      camera: this.camera,
      scene: this.scene,
      renderer : this.renderer,
      sizes: this.sizes,
    }
  }

б. applyOrbitControls: этот метод позаботится о добавлении элементов управления орбитой в нашу настройку и вернет функцию, которую нам нужно вызвать для обновления элементов управления орбитой.

applyOrbitControls(){
    const controls = new OrbitControls(
      this.camera, this.renderer.domElement!
    )
    controls.enableDamping = true
    return ()=> controls.update();
  }

Наш основной класс реализации FacePointCloud инициирует класс ThreeSetUP и вызывает эти два метода для получения элементов настройки и применения элементов управления орбитой.

2. Создание видеоданных с веб-камеры:

Для того, чтобы мы могли получить информацию об отслеживании сетки лица, нам нужен входной сигнал пикселей для подачи на трекер сетки лица. В этом случае мы будем использовать веб-камеру устройства для создания такого ввода. Мы также будем использовать элемент HTML-видео (не добавляя его в Dom), чтобы считывать медиапоток с веб-камеры и загружать его таким образом, чтобы наш код мог с ним взаимодействовать. После этого шага мы настроим элемент холста HTML (также без добавления в Dom) и отобразим на нем наш видеовыход. Это позволяет нам также иметь возможность генерировать текстуру Three Js из холста и использовать ее в качестве материала (мы не будем реализовывать это в этом уроке). Элемент canvas — это то, что мы будем использовать в качестве входных данных для FaceMeshTracker.

Чтобы обрабатывать считывание медиапотока с веб-камеры и загрузку его в HTML-элемент видео, мы создадим класс с именем WebcamVideo. Этот класс будет обрабатывать создание видеоэлемента HTML и вызов API-интерфейса навигатора для load получить разрешение пользователя и загрузить информацию с веб-камеры устройства.

При запуске этого класса будет вызван частный метод инициализации со следующим кодом:

private init(){
  navigator.mediaDevices.getUserMedia(this.videoConstraints)
  .then((mediaStream)=>{
    this.videoTarget.srcObject = mediaStream
    this.videoTarget.onloadedmetadata = () => this.onLoadMetadata()
    }
  ).catch(function (err) {
    alert(err.name + ': ' + err.message)
    }
  )
}

Этот метод вызывает метод getUserMedia для свойства mediaDevices объекта navigator. Этот метод принимает ограничения видео (также известные как настройки видео) в качестве параметра и возвращает обещание. Это обещание разрешается в объект mediaStream, который содержит видеоданные с веб-камеры. В обратном вызове разрешения промиса мы устанавливаем источник нашего видеоэлемента как возвращенный mediaStream.

В обратном вызове разрешения обещания мы также добавляем прослушиватель loadedmetadata event для элемента видео. Обратный вызов этого слушателя запускает метод onLoadMetaData объекта и устанавливает следующие побочные эффекты:

а. Автовоспроизведение видео

б. Обеспечение встроенного воспроизведения видео

в. Вызывает необязательный обратный вызов, который мы передаем объекту для вызова при возникновении события.

private onLoadMetadata(){
  this.videoTarget.setAttribute('autoplay', 'true')
  this.videoTarget.setAttribute('playsinline', 'true')
  this.videoTarget.play()
  this.onReceivingData()
}

На данный момент у нас есть объект WebcamVideo, который обрабатывает создание видеоэлемента, содержащего данные нашей веб-камеры в реальном времени. Следующим шагом будет нанесение видео на объект холста.

Для этого мы создадим специальный класс WebcamCanvas, который использует класс WebcamVideo. Этот класс создаст экземпляр класса WebcamVideo и будет использовать его для рисования вывода видео на холст с помощью метода контекста холста drawImage(). Это будет реализовано в методе updateFromWebcam.

updateFromWebCam(){
  this.canvasCtx.drawImage(
    this.webcamVideo.videoTarget,
    0,
    0,
    this.canvas.width,
    this.canvas.height
  )
}

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

На данный момент у нас есть готовый пиксельный ввод в виде элемента холста, который отображает нашу веб-камеру.

3. Создайте детектор Face Mesh с помощью Tensorflow.js:

Создание детектора сетки лица и генерация данных обнаружения является основной частью этого руководства. Это реализует модель обнаружения ориентиров лица Tensorflow.js.

npm add @tensorflow/tfjs-core, @tensorflow/tfjs-converter
npm add @tensorflow/tfjs-backend-webgl
npm add @tensorflow-models/face-detection
npm add @tensorflow-models/face-landmarks-detection

После установки всех соответствующих пакетов мы создадим класс, который обрабатывает следующее:

а. Загрузка модели,

б. Получение объекта детектора,

в. Добавление детектора в класс,

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

Мы создали файл с именем faceLandmark.ts, который реализует класс. Импорт в верхней части файла:

import '@mediapipe/face_mesh'
import '@tensorflow/tfjs-core'
import '@tensorflow/tfjs-backend-webgl'
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection'

эти модули понадобятся для запуска и создания объекта детектора.

Мы создаем FaceMeshDetectorClass, который выглядит следующим образом:

основным методом в этом классе является getDetector, который вызывает метод createDetector для faceLandMarksDetection, который мы импортировали из Tensorflow.js. Затем createDetector берет модель, которую мы привели в конструкторе:

this.model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;

и объект конфигурации обнаружения, который указывает параметры для детектора:

this.detectorConfig = {
  runtime: 'mediapipe',
  refineLandmarks: true,
  solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh',
}

функция обнаружения вернет обещание, обещание будет разрешено объекту детектора. Затем приватная функция getDetector используется в общедоступном асинхронном методе loadDetector. Этот метод задает для детектора свойство this.detector класса.

Класс FaceMeshDetector также реализует общедоступный метод detectFace:

async detectFace(source){
  const data = await this.detector!.estimateFaces(source)
  const keypoints = (data as FaceLandmark[])[0]?.keypoints
  if(keypoints) return keypoints;
  return [];
}

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

faceMeshDetector.detectFace(this.webcamCanvas.canvas)

этот метод вызывает метод estimateFaces детектора. Если этот метод обнаруживает лица на выходе веб-камеры, он возвращает массив с объектом, содержащим данные обнаружения. Этот объект имеет свойство, называемое ключевыми точками, оно включает в себя массив объектов для каждой из 478 точек, обнаруженных моделью на лице. Каждый объект имеет свойства x, y и z, которые включают координаты точки на холсте. Пример:

[
  {
    box: {
      xMin: 304.6476503248806,
      xMax: 502.5079975897382,
      yMin: 102.16298762367356,
      yMax: 349.035215984403,
      width: 197.86034726485758,
      height: 246.87222836072945
    },
    keypoints: [
      {x: 406.53152857172876, y: 256.8054528661723, z: 10.2, name:
      "lips"},
      {x: 406.544237446397, y: 230.06933367750395, z: 8},
      ...
    ],
  }
]

Важно отметить, что эти точки возвращаются как координаты в пространстве холста, это означает, что контрольная точка, точки x: 0 и y: 0 находятся в верхнем левом углу холста. Это будет актуально позже, когда нам нужно будет преобразовать координаты в пространство сцены Three.js, у которого точка отсчета находится в центре сцены.

На данный момент у нас есть источник ввода пикселей, а также детектор сетки лица, который даст нам обнаруженные точки. Теперь мы можем перейти к части Three.js!

4. Создайте пустое облако точек:

Чтобы сгенерировать сетку лица в Three.js, нам нужно будет загрузить точки сетки лица из детектора, а затем использовать их в качестве атрибутов положения для объекта Three js Points. Чтобы заставить сетку лица Three js отражать движения в видео (реагировать в реальном времени), нам придется обновлять этот атрибут положения всякий раз, когда происходит изменение обнаружения лица от созданного нами детектора.

Чтобы реализовать это, мы создадим еще один фабричный класс под названием PointCloud, который создаст пустой объект Points и общедоступный метод, который мы можем использовать для обновления атрибутов этого объекта Points, таких как атрибут position. Этот класс будет выглядеть так:

export default class PointCloud {
  bufferGeometry: THREE.BufferGeometry;
  material: THREE.PointsMaterial;
  cloud: THREE.Points<THREE.BufferGeometry, THREE.PointsMaterial>;
  
  constructor() {
    this.bufferGeometry = new THREE.BufferGeometry();
    this.material = new THREE.PointsMaterial({
      color: 0x888888,
      size: 0.0151,
      sizeAttenuation: true,
    });
    this.cloud = new THREE.Points(this.bufferGeometry,     this.material);
  }
updateProperty(attribute: THREE.BufferAttribute, name: string){
  this.bufferGeometry.setAttribute(
    name,
    attribute
  );
  this.bufferGeometry.attributes[name].needsUpdate = true;
  }
}

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

Класс PointCloud также предоставляет метод updateProperty, который принимает атрибут буфера и имя свойства. Затем он вызовет метод setAttribute bufferGeometry и установит для свойства needUpdate значение true. Это позволит Three.js отражать изменения атрибута bufferAttribute на следующей итерации requestAnimationFrame.

Этот метод updateProperty — это метод, который мы будем использовать для изменения формы облака точек на основе точек, полученных от детектора Tensorflow.js.

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

5. Передайте информацию об отслеживании в PointCloud:

Чтобы связать все вместе, мы создадим класс реализации для вызова классов, методов и шагов, необходимых для того, чтобы все заработало. Этот класс называется FacePointCloud. В конструкторе он создаст экземпляры следующих классов:

а. Класс ThreeSetUp для получения объектов настройки сцены

б. CanvasWebcam для получения объекта холста, отображающего содержимое веб-камеры.

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

д. Класс PointCloud для настройки пустого облака точек и последующего обновления его данными обнаружения.

constructor() {
  this.threeSetUp = new ThreeSetUp()
  this.setUpElements = this.threeSetUp.getSetUp()
  this.webcamCanvas = new WebcamCanvas();
  this.faceMeshDetector = new faceLandMark()
  this.pointCloud = new PointCloud()
}

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

async bindFaceDataToPointCloud(){
  const keypoints = await
  this.faceMeshDetector.detectFace(this.webcamCanvas.canvas)
  const flatData = flattenFacialLandMarkArray(keypoints)
  const facePositions = createBufferAttribute(flatData)
  this.pointCloud.updateProperty(facePositions, 'position')
}

Таким образом, мы передаем наш источник пикселя холста методу detectFace, а затем выполняем манипуляции с возвращенными данными в служебной функции flattenFacialLandMarkArray. Это очень важно по двум причинам:

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

keypoints: [
  {x: 0.542, y: 0.967, z: 0.037},
  ...
]

в то время как атрибут буфера ожидает данные/числа в следующей форме:

number[] or [0.542, 0.967, 0.037, .....]

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

и система координат сцены Three.js, которая выглядит так:

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

function flattenFacialLandMarkArray(data: vector[]){
  let array: number[] = [];
  data.forEach((el)=>{
    el.x = mapRangetoRange(500 / videoAspectRatio, el.x,
      screenRange.height) - 1
    
    el.y = mapRangetoRange(500 / videoAspectRatio, el.y,
      screenRange.height, true)+1
    el.z = (el.z / 100 * -1) + 0.5;
    
    array = [
      ...array,
      ...Object.values(el),
    ]
  })
  return array.filter((el)=> typeof el === 'number');
}

функция flattenFacialLandMarkArray принимает ввод ключевых точек, которые мы получаем от детектора лиц, и распределяет их в массив, чтобы они были в форме числа [] вместо объектов []. Перед передачей чисел в новый выходной массив он сопоставляет их из системы координат холста с системой координат three.js с помощью функции mapRangetoRange. Эта функция выглядит следующим образом:

function mapRangetoRange(from: number, point: number, range: range, invert: boolean = false): number{
  let pointMagnitude: number = point/from;
  if(invert) pointMagnitude = 1-pointMagnitude;
  const targetMagnitude = range.to - range.from;
  const pointInRange = targetMagnitude * pointMagnitude +
    range.from;
  
  return pointInRange
}

теперь мы можем создать нашу функцию инициализации и наш цикл анимации. Это было реализовано в методе initWork класса FacePointCloud следующим образом:

async initWork() {
  const { camera, scene, renderer } = this.setUpElements
  camera.position.z = 3
  camera.position.y = 1
  camera.lookAt(0,0,0)
  const orbitControlsUpdate = this.threeSetUp.applyOrbitControls()
  const gridHelper = new THREE.GridHelper(10, 10)
  scene.add(gridHelper)
  scene.add(this.pointCloud.cloud)
  
  await this.faceMeshDetector.loadDetector()
  
  const animate = () => {
    requestAnimationFrame(animate)
    if (this.webcamCanvas.receivingStreem){
      this.bindFaceDataToPointCloud()
    }
    this.webcamCanvas.updateFromWebCam()
    orbitControlsUpdate()
    renderer.render(scene, camera)
  }
  
  animate()
}

Мы можем видеть, как эта функция инициализации связывает все вместе, она получает элементы настройки Three.js и настраивает камеру, добавляет gridHelper к сцене и нашему pointCloud.

Затем он загружает детектор в класс faceLandMark и переходит к настройке нашей функции animate. Внутри этой функции анимации мы сначала проверяем, получает ли наш элемент WebcamCanvas поток с веб-камеры, а затем вызываем метод bindFaceDataToPointCloud, который внутренне вызывает функцию обнаружения лица и преобразует данные в атрибут bufferAttribute и обновляет их. атрибут положения облака точек.

Теперь, если вы запустите код, вы должны получить следующий результат в браузере!

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