Статьи

Обнаружение близости лица с помощью JavaScript

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

Моей команде разработчиков в Mozilla требовалась функция «видеостенд», где люди могли записывать свои разговоры в веб-камеру и загружать эти видео и делиться ими в веб-приложении. В подобных случаях проблемы с качеством обычно связаны с аудио. В идеале люди должны использовать микрофон приличного качества и сидеть в комнате с минимальным эхом, но часто это невозможно. Лучший способ решить эту проблему — убедиться, что пользователь сидит рядом со встроенным микрофоном своего ноутбука. Чтобы сделать это, мы реализовали распознавание лица в видоискателе, чтобы сказать им, достаточно ли они сидят рядом или нет.

В этой статье я покажу вам, как создать эту функциональность, и узнайте кое-что о задействованных компонентах.

Давайте заставим камеру работать

Потоковый API getUserMedia активно использовался во всех современных браузерах. API navigator.getUserMedia действительно работал только с хаки префиксами различных поставщиков. Новый потоковый API getUserMedia

Минимальный код, который нужно запустить, показан ниже:

 navigator.mediaDevices
   .getUserMedia({audio: false, video: true})
   .then(function(stream) {
       // OK
   })
   .catch(function(error) {
       // Error
   });

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

 navigator.mediaDevices = navigator.mediaDevices || ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia) ? {
   getUserMedia: function(c) {
     return new Promise(function(y, n) {
       (navigator.mozGetUserMedia ||
        navigator.webkitGetUserMedia).call(navigator, c, y, n);
     });
   }
} : null);

// this is not part of the polyfill
if (!navigator.mediaDevices) {
  throw new Error('getUserMedia() not supported.');
}

navigator.mediaDevices
   .getUserMedia({audio: false, video: true})
   .then(function(stream) {
       // OK
   })
   .catch(function(error) {
       // Error
   });

Теперь, когда у вас есть объект MediaStreamstream Вы проецируете его на элемент videoURL.createObjectURL(blob) Мы добавим один из них и продолжим пример кода, где доступен объект stream

 function startCamera() {}
  return navigator.mediaDevices
    .getUserMedia({audio: false, video: true})
    .then(function(stream) {
      // assume you have a `` tag somewhere in the DOM
      var video = document.querySelector('video');
      video.src = URL.createObjectURL(stream);
      video.play();

      // return the stream so that chained promises can use it
      return stream;
    })
}  `тег где-то в DOM
       var video = document.querySelector ('video');
       video.src = URL.createObjectURL (stream);
       video.play ();

       // возвращаем поток, чтобы цепочечные обещания могли его использовать
       обратный поток;
     })
 } 

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

 document.querySelector('button').onclick = function() {
  var button = this;
  button.textContent = 'Starting camera';

  // the function that wraps the getUserMedia call
  startCamera()
  .then(function() {
    button.textContent = 'Stop camera';
  });
}

Вы можете найти чуть более полный и функциональный пример здесь .

Один решающий трюк для видоискателя

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

Решение этой проблемы на удивление легко. Мы можем использовать CSS, чтобы отразить изображение видоискателя. На самом деле мы поворачиваем его на 180 градусов вокруг своей оси Y. Волшебство сделано с простым правилом transform

 video {
  transform: rotateY(180deg);
}

На CodePen вы можете найти первое улучшенное демо, которое также доступно ниже:

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

Проецирование рамок на холст

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

По сути, вся хитрость здесь сводится к следующему: вы берете поток (из запуска экземпляра navigator.mediaDevices.getUserMedia Один на <video><canvas> После этого мы отправляем каждый кадр <canvas>

В качестве первого шага, давайте добавим элемент canvas

 // assuming you have a  tag somewhere in the DOM
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');

function startCamera() {}
  return navigator.mediaDevices.getUserMedia({audio: false, video: true})
  .then(function(stream) {
    // same as before
    var video = document.querySelector('video');
    video.src = URL.createObjectURL(stream);
    video.play();

    // The critical point where we transfer some frames to
    // a canvas element.
    setInterval(function() {
      context.drawImage(video, 0, 0, canvas.width, canvas.height);
    }, 100);
  })
}   тег где-то в DOM
 var canvas = document.querySelector ('canvas');
 var context = canvas.getContext ('2d');

 function startCamera () {}
   возвращать navigator.mediaDevices.getUserMedia ({audio: false, video: true})
   .then (функция (поток) {
     // так же, как и раньше
     var video = document.querySelector ('video');
     video.src = URL.createObjectURL (stream);
     video.play ();

     // Критическая точка, в которую мы передаем некоторые кадры
     // элемент canvas.
     setInterval (function () {
       context.drawImage (video, 0, 0, canvas.width, canvas.height);
     }, 100);
   })
 } 

На самом деле вам не нужно, чтобы <canvas>

 canvas {
  display: none;
}

Вот демонстрация, где элемент canvas .

API context.drawImage Мы можем просто передать ему элемент video

Вот так! У нас есть изображение нашей веб-камеры, полностью спроецированное на <canvas> Ухоженная!

Внедрение фактического распознавания лиц

Файлы, которые мы хотим использовать, доступны из проекта GitHub ccv разработанного Лю Лю, который работает в Snapchat . CCV — это в основном проект, написанный на C, с некоторыми нетривиальными алгоритмами обучения, которые знают, какие шаблоны ожидать, например, при поиске человеческого лица.

Тем не менее мы не собираемся использовать код на C, так как мы будем делать все в браузере без использования сервера. Прелесть проекта CCV в том, что у его создателя есть модель, сохраненная в виде файла JavaScript размером 236 КБ, и небольшой скрипт, который использует эту модель с <canvas>

API работает очень просто. Вы передаете ему <canvas> После загрузки face.jsccv.js

 // using global `ccv` from ccv.js and `cascade` from face.js
var faces = ccv.detect_objects({
  canvas: ccv.pre(canvas),
  cascade: cascade,
  interval: 2,
  min_neighbors: 1
});

Эти объекты являются лицами, и каждый из них выглядит так:

 {
  confidence: 0.33769726999999994,
  height: 60.97621039495391,
  width: 60.97621039495391,
  neighbors: 1,
  x: 131.5317891890668,
  y: 66.0158945945334
}

Как мы на самом деле используем это? Давайте расширим нашу существующую демонстрацию и позволим ей выплевывать любые найденные лица на консоли.

Здесь вы можете найти демонстрацию, где каждое обнаруженное лицо зарегистрировано в консоли .

Чтобы понять, что у нас есть, достаточно традиционно сделать квадрат над изображением (в <canvas>widthheightxy Давайте сделаем это, не скрывая canvas

На CodePen вы можете увидеть код для рисования типичного квадрата вокруг лица, а в демонстрации ниже вы можете поиграть с ним:

На данный момент, есть несколько вещей, на которые стоит обратить внимание перед просмотром демо:

  1. Прямоугольник лица иногда исчезает. Особенно, если вы «уходите из поля зрения» или поднимаете руку за половину лица. Это потому, что модель не идеальна. Это приблизительная модель, которая пытается работать наилучшим образом.
  2. По мере того, как вы приближаетесь все дальше и дальше от камеры (т.е. экрана вашего ноутбука), прямоугольник сжимается и увеличивается. Это ключ к нашему следующему шагу!

Выяснить, как далеко вы находитесь от экрана

Расстояние от вашего носа до экрана (там, где предположительно находится камера) определяется благодаря значению height Если вы знаете высоту лица и высоту canvas Но учтите, что вы решаете этот процент! Лучший способ понять это — проверить это на себе и посмотреть, какой процент у прямоугольника.

Эта демонстрация поможет вам отладить процент / отношение вашего лица по сравнению с холстом .

Одна вещь, которую вы могли заметить, это то, что число сильно «прыгает». Он мерцает вверх и вниз, и почти невозможно сказать, что это такое. Простым решением этого является вычисление «скользящего среднего» (что означает скользящее среднее). Мы не делаем причудливую статистику, а просто усредняем последние пять процентов. Функция для этого проста:

 var percentages = [];
function rollingAverage(size) {
  percentages.splice(0, percentages.length - size);
  var sum = percentages.reduce(function(total, num) {
    return total + num
  }, 0);
  return sum / percentages.length;
}

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

А как насчет фактического расстояния от экрана?

Если вы хотите, чтобы фактическое расстояние от вашего лица до экрана в дюймах или сантиметрах, опять же, используйте процент от того, сколько ваше лицо занимает canvas Однако, чтобы сделать преобразование, вы должны сначала выяснить, что проценты означают для вашего лица. В идеале на этом этапе вы должны получить линейку и попросить партнера измерить вас, пока вы сидите на месте, наблюдая за процентным числом. Вот один из таких выводов из измерения пары точек данных и использования Google Docs для извлечения экспоненциальной линии тренда:

Дюймы против процента

Как видите, это дает нам формулу. Давайте использовать это, чтобы сказать расстояние в дюймах вместо.

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

Как вы, вероятно, видите, это не очень точно, поскольку модель программного обеспечения не идеальна, и есть много расчетов, которые нужно повторить. Вы не можете зависеть от точного числа. Приложение, которое мы создали на работе, не показывало никаких цифр, как в этой последней демонстрации. Он просто показывал значок, который сказал бы: «Немного ближе, пожалуйста», «Немного назад, пожалуйста» и «Хорошее расстояние лица», когда вы двигаетесь назад и вперед. Никто не ожидает, что оно будет особенно точным, но каждый раз, когда мы видим людей, использующих приложение в первый раз, на их лице появляется улыбка, которую мы, по крайней мере, пытаемся.

Выводы

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

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