День смешных шляп! 👒🎩 Как распознать лицо с помощью веб-камеры и JavaScript 📸🧠

(Изображение на обложке создано с помощью Dall-E mini и надписью «ИИ в смешной шляпе» — знаете, потому что сегодня мы занимаемся машинным обучением).

Прошло много времени с моего последнего сообщения. Я работаю над чем-то довольно крупным; ждите новостей в ближайшее время!

Но сегодня мы посмотрим на вас. Да, на вас. Точнее, на ваши прекрасные лица. Мы заставим вас носить шляпы. Для этого мы будем использовать face-api.js и Media Stream API.

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

Давайте приступим!

Шаблон

Во-первых, нам понадобится HTML: Элемент <video>, шляпа, две кнопки для запуска и остановки видео, и два <select> для выбора шляпы и устройства. У вас может быть две веб-камеры.

<div class="container">
  <div id="hat">
    🎩
  </div>
  <!-- autoplay is important here, otherwise it doesn't immediately show the camera input. -->
  <video id="video" width="1280" height="720" autoplay></video>
</div>

<div>
  <label for="deviceSelector">
    Select device
  </label>
  <select id="deviceSelector"></select>
</div>

<div>
  <label for="hatSelector">
    Select hat
  </label>
  <select id="hatSelector"></select>
</div>

<button id="start">
  Start video
</button>

<button id="stop">
  Stop video
</button>
Вход в полноэкранный режим Выход из полноэкранного режима

Далее, немного CSS для позиционирования шапки:

#hat {
  position: absolute;
  display: none;
  text-align: center;
}
#hat.visible {
  display: block;
}
.container {
  position: relative;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Потрясающе. Далее мы установим face-api.js с помощью npm и создадим файл index.js для работы в нем:

npm i face-api.js && touch index.js
Вход в полноэкранный режим Выход из полноэкранного режима

И, наконец, для шаблона мы выбираем все нужные нам элементы из HTML:

/**
 * All of the necessary HTML elements
 */
const videoEl = document.querySelector('#video')
const startButtonEl = document.querySelector('#start')
const stopButtonEl = document.querySelector('#stop')
const deviceDropdownEl = document.querySelector('#deviceSelector')
const hatSelectorEl = document.querySelector('#hatSelector')
const hatEl = document.querySelector('#hat')
Войти в полноэкранный режим Выход из полноэкранного режима

Потрясающе. Переходим к самому интересному.

Доступ к веб-камере

Для доступа к веб-камере мы будем использовать API Media Stream. Этот API позволяет нам получить доступ к видео- и аудиоустройствам, но нас интересуют только видеоустройства. Кроме того, мы будем кэшировать эти устройства в глобальной переменной, чтобы не искать их снова. Итак, давайте посмотрим:

const listDevices = async () => {
  if (devices.length > 0) {
    return
  }

  devices = await navigator.mediaDevices.enumerateDevices()
  // ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Объект mediaDevices позволяет нам получить доступ ко всем устройствам, как видео, так и аудио. Каждое устройство представляет собой объект класса InputDeviceInfo или MediaDeviceInfo. Оба эти объекта выглядят примерно так:

{
  deviceId: "someHash",
  groupId: "someOtherHash"
  kind: "videoinput", // or "audioinput"
  label: "Some human readable name (some identifier)"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Для нас интересен kind. Мы можем использовать его для фильтрации всех устройств videoinput, что даст нам список доступных веб-камер. Мы также добавим эти устройства в <select>, который мы добавили в шаблон, и пометим первое попавшееся устройство как выбранное:

/**
 * List all available camera devices in the select
 */
let selectedDevice = null

let devices = []

const listDevices = async () => {
  if (devices.length > 0) {
    return
  }

  devices = (await navigator.mediaDevices.enumerateDevices())
    .filter(d => d.kind === 'videoinput')

  if (devices.length > 0) {
    deviceDropdownEl.innerHTML = devices.map(d => `
      <option value="${d.deviceId}">${d.label}</option>
    `).join('')

    // Select first device
    selectedDevice = devices[0].deviceId
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы действительно покажем пользователю вход веб-камеры. Для этого API Media Stream предлагает метод getUserMedia. Он получает в качестве аргумента объект config, который определяет, к чему именно мы хотим получить доступ. Нам не нужен звук, но нам нужен видеопоток с selectedDevice. Мы также можем сообщить API предпочтительный размер видео. Наконец, мы присваиваем вывод этого метода <video>, а именно его srcObject:

const startVideo = async () => {
  // Some more face detection stuff later

  videoEl.srcObject = await navigator.mediaDevices.getUserMedia({
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      deviceId: selectedDevice,
    },
    audio: false,
  })

  // More face detection stuff later
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это должно помочь. Поскольку <video> имеет атрибут autoplay, он должен немедленно показать то, что видит камера. Конечно, если мы не разрешили браузеру доступ к камере. Но почему бы и нет, верно? В конце концов, мы хотим носить шляпы.

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

const stopVideo = () => {
  // Some face detection stuff later on

  if (videoEl.srcObject) {
    videoEl.srcObject.getTracks().forEach(t => {
      t.stop()
    })
    videoEl.srcObject = null
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем запускать и останавливать видео. Далее:

Распознавание лиц

Давайте запустим машинное обучение. Во время установки котла мы установили face-api.js, который является довольно фантастическим либом для выполнения всех видов задач ML, связанных с распознаванием, определением и интерпретацией лиц. Она также может определять настроение, говорить нам, где находятся различные части лица, такие как линия челюсти или глаза, и способна использовать различные веса модели. И самое приятное: Она не нуждается в удаленном обслуживании; нам нужно только предоставить правильные веса модели! Учитывая, что они могут быть довольно большими, но нам нужно загрузить их только один раз, и мы можем заниматься распознаванием лиц до конца сеанса.

Сначала нам нужны модели. В репозитории face-api.js есть все предварительно обученные модели, которые нам нужны:

  • face_landmark_68_model-shard1
  • face_landmark_68_model-weights_manifest.json
  • ssd_mobilenetv1_model-shard1
  • ssd_mobilenetv1_model-shard2
  • ssd_mobilenetv1_model-weights_manifest.json
  • tiny_face_detector_model-shard1
  • tiny_face_detector_model-weights_manifest.json

Мы поместим их в папку model и заставим face-api загрузить их:

let faceApiInitialized = false

const initFaceApi = async () => {
  if (!faceApiInitialized) {
    await faceapi.loadFaceLandmarkModel('/models')
    await faceapi.nets.tinyFaceDetector.loadFromUri('/models')

    faceApiInitialized = true
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

С помощью face-api.js мы можем создать асинхронную функцию для обнаружения лица в потоке элемента видео. face-api.js делает всю магию за нас, и нам нужно только указать ему, в каком элементе мы хотим искать лица и какую модель использовать. Однако сначала нам нужно инициализировать API.

const detectFace = async () => {
  await initFaceApi()

  return await faceapi.detectSingleFace(videoEl, new faceapi.TinyFaceDetectorOptions())
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Это вернет нам объект dd с атрибутом _box. Эта коробка содержит всевозможную информацию, а именно: наборы координат для каждого угла, координаты x и y левого верхнего угла, ширину и высоту. Для позиционирования коробки, содержащей шляпу, нам нужны атрибуты top, left, width и height. Поскольку все шляпы эмодзи немного отличаются друг от друга, мы не можем просто наложить их прямо на лицо — они не подойдут.

Поэтому давайте добавим шляпы и способ настройки их расположения:

/**
 * All of the available hats
 */
const hats = {
  tophat: {
    hat: '🎩',
    positioning: box => ({
      top: box.top - (box.height * 1.1),
      left: box.left,
      fontSize: box.height,
    }),
  },
  bowhat: {
    hat: '👒',
    positioning: box => ({
      top: box.top - box.height,
      left: box.left + box.width * 0.1,
      width: box.width,
      fontSize: box.height,
    }),
  },
  cap: {
    hat: '🧢',
    positioning: box => ({
      top: box.top - box.height * 0.8,
      left: box.left - box.width * 0.10,
      fontSize: box.height * 0.9,
    }),
  },
  graduationcap: {
    hat: '🎓',
    positioning: box => ({
      top: box.top - box.height,
      left: box.left,
      fontSize: box.height,
    }),
  },
  rescuehelmet: {
    hat: '⛑️',
    positioning: box => ({
      top: box.top - box.height * 0.75,
      left: box.left,
      fontSize: box.height * 0.9,
    }),
  },
}
Войти в полноэкранный режим Выход из полноэкранного режима

Основная причина

Поскольку мы еще не использовали <select> для шляп, давайте добавим его следующим:

let selectedHat = 'tophat'

const listHats = () => {
  hatSelectorEl.innerHTML = Object.keys(hats).map(hatKey => {
    const hat = hats[hatKey]

    return `<option value="${hatKey}">${hat.hat}</option>`
  }).join('')
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как носить шляпы

Теперь мы можем начать склеивать все вместе. С помощью переменной selectedHat и поля мы можем расположить выбранную шляпу на обнаруженном лице:

/**
 * Positions the hat by a given box
 */
const positionHat = (box) => {
  const hatConfig = hats[selectedHat]
  const positioning = hatConfig.positioning(box)

  hatEl.classList.add('visible')
  hatEl.innerHTML = hatConfig.hat
  hatEl.setAttribute('style', `
    top: ${positioning.top}px; 
    left: ${positioning.left}px; 
    width: ${box.width}px; 
    height: ${box.height}px; 
    font-size: ${positioning.fontSize}px;
  `)
}
Войти в полноэкранный режим Выход из полноэкранного режима

Как видите, для этого мы используем CSS. Конечно, мы могли бы нарисовать это с помощью canvas и т.д., но CSS делает все более простым и менее лагающим.

Теперь нам нужно интегрировать определение лица в функции startVideo и stopVideo. Для полноты картины я покажу здесь весь код этих функций.

/**
 * Start and stop the video
 */
let faceDetectionInterval = null

const startVideo = async () => {
  listHats()
  await listDevices()

  stopVideo()

  try {
    videoEl.srcObject = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        deviceId: selectedDevice,
      },
      audio: false
    })

    faceDetectionInterval = setInterval(async () => {
      const positioning = await detectFace()

      if (positioning) {
        positionHat(positioning._box)
      }
    }, 60)
  } catch(e) {
    console.error(e)
  }
}

const stopVideo = () => {
  clearInterval(faceDetectionInterval)
  hatEl.classList.remove('visible')

  if (videoEl.srcObject) {
    videoEl.srcObject.getTracks().forEach(t => {
      t.stop()
    })
    videoEl.srcObject = null
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Наконец, мы добавляем несколько слушателей событий, и все готово:

/**
 * Event listeners
 */
startButtonEl.addEventListener('click', startVideo)

stopButtonEl.addEventListener('click', stopVideo)

deviceDropdownEl.addEventListener('change', e => {
  selectedDevice = e.target.value
  startVideo()
})

hatSelectorEl.addEventListener('change', e => {
  selectedHat = e.target.value
})
Вход в полноэкранный режим Выход из полноэкранного режима

Результат

И вот результат:

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

Если хотите, поделитесь в комментариях скриншотом, на котором вы изображены в своей любимой шляпе emoji?


Надеюсь, вам понравилось читать эту статью так же, как и мне! Если да, оставьте ❤️ или 🦄! Я пишу технические статьи в свободное время и люблю время от времени выпить кофе.

Если вы хотите поддержать мои усилия, вы можете предложить мне кофе ☕ или следовать за мной в Twitter 🐦! Вы также можете поддержать меня напрямую через Paypal!

Оцените статью
devanswers.ru
Добавить комментарий