Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 5

от автора

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

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


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

Создание волшебной шляпы

Помните, как мы ранее в этой серии статей создавали функцию обнаружения эмоций на лице в реальном времени? Теперь давайте добавим немного графики в этот проект – придадим ему, так сказать, «лицо».

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

<img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" /> <img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" /> <img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" /> <img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" /> <img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" /> <img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" /> <img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />

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

let currentEmotion = "neutral"; let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };

Рисовать шляпу этого размера и в этом положении мы будем с помощью 2D-преобразования полотна в каждом кадре.

async function trackFace() {     ...      output.drawImage(         video,         0, 0, video.width, video.height,         0, 0, video.width, video.height     );     let hatImage = document.getElementById( `hat-${currentEmotion}` );     output.save();     output.translate( -hatImage.width / 2, -hatImage.height / 2 );     output.translate( hat.position.x, hat.position.y );     output.drawImage(         hatImage,         0, 0, hatImage.width, hatImage.height,         0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale     );     output.restore();      ... }

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

Размер головы можно оценить по расстоянию между глазами. Вектор «вверх» аппроксимируем по точке midwayBetweenEyes и точке noseBottom, которые можно использовать для перемещения шляпы вверх ближе к верхней части лица (в отличие от виртуальных очков из предыдущей статьи).

const eyeDist = Math.sqrt(     ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +     ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +     ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2 );  const faceScale = eyeDist / 80; let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ]; let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ]; const length = Math.sqrt( upX ** 2 + upY ** 2 ); upX /= length; upY /= length;  hat = {     scale: faceScale,     position: {         x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,         y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,     } };

После сохранения названия спрогнозированной эмоции в currentEmotion отображается соответствующее изображение шляпы, и мы готовы её примерить!

if( points ) {     let emotion = await predictEmotion( points );     setText( `Detected: ${emotion}` );     currentEmotion = emotion; } else {     setText( "No Face" ); }
Вот полный код этого проекта
<html>     <head>         <title>Building a Magical Emotion Detection Hat</title>         <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>         <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>     </head>     <body>         <canvas id="output"></canvas>         <video id="webcam" playsinline style="             visibility: hidden;             width: auto;             height: auto;             ">         </video>         <h1 id="status">Loading...</h1>         <img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" />         <img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" />         <img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" />         <img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" />         <img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" />         <img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" />         <img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />         <script>         function setText( text ) {             document.getElementById( "status" ).innerText = text;         }          function drawLine( ctx, x1, y1, x2, y2 ) {             ctx.beginPath();             ctx.moveTo( x1, y1 );             ctx.lineTo( x2, y2 );             ctx.stroke();         }          async function setupWebcam() {             return new Promise( ( resolve, reject ) => {                 const webcamElement = document.getElementById( "webcam" );                 const navigatorAny = navigator;                 navigator.getUserMedia = navigator.getUserMedia ||                 navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                 navigatorAny.msGetUserMedia;                 if( navigator.getUserMedia ) {                     navigator.getUserMedia( { video: true },                         stream => {                             webcamElement.srcObject = stream;                             webcamElement.addEventListener( "loadeddata", resolve, false );                         },                     error => reject());                 }                 else {                     reject();                 }             });         }          const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];         let emotionModel = null;          let output = null;         let model = null;          let currentEmotion = "neutral";         let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };          async function predictEmotion( points ) {             let result = tf.tidy( () => {                 const xs = tf.stack( [ tf.tensor1d( points ) ] );                 return emotionModel.predict( xs );             });             let prediction = await result.data();             result.dispose();             // Get the index of the maximum value             let id = prediction.indexOf( Math.max( ...prediction ) );             return emotions[ id ];         }          async function trackFace() {             const video = document.querySelector( "video" );             const faces = await model.estimateFaces( {                 input: video,                 returnTensors: false,                 flipHorizontal: false,             });             output.drawImage(                 video,                 0, 0, video.width, video.height,                 0, 0, video.width, video.height             );             let hatImage = document.getElementById( `hat-${currentEmotion}` );             output.save();             output.translate( -hatImage.width / 2, -hatImage.height / 2 );             output.translate( hat.position.x, hat.position.y );             output.drawImage(                 hatImage,                 0, 0, hatImage.width, hatImage.height,                 0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale             );             output.restore();              let points = null;             faces.forEach( face => {                 const x1 = face.boundingBox.topLeft[ 0 ];                 const y1 = face.boundingBox.topLeft[ 1 ];                 const x2 = face.boundingBox.bottomRight[ 0 ];                 const y2 = face.boundingBox.bottomRight[ 1 ];                 const bWidth = x2 - x1;                 const bHeight = y2 - y1;                  // Add just the nose, cheeks, eyes, eyebrows & mouth                 const features = [                     "noseTip",                     "leftCheek",                     "rightCheek",                     "leftEyeLower1", "leftEyeUpper1",                     "rightEyeLower1", "rightEyeUpper1",                     "leftEyebrowLower", //"leftEyebrowUpper",                     "rightEyebrowLower", //"rightEyebrowUpper",                     "lipsLowerInner", //"lipsLowerOuter",                     "lipsUpperInner", //"lipsUpperOuter",                 ];                 points = [];                 features.forEach( feature => {                     face.annotations[ feature ].forEach( x => {                         points.push( ( x[ 0 ] - x1 ) / bWidth );                         points.push( ( x[ 1 ] - y1 ) / bHeight );                     });                 });                  const eyeDist = Math.sqrt(                     ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                     ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                     ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                 );                 const faceScale = eyeDist / 80;                 let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];                 let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ];                 const length = Math.sqrt( upX ** 2 + upY ** 2 );                 upX /= length;                 upY /= length;                  hat = {                     scale: faceScale,                     position: {                         x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,                         y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,                     }                 };             });              if( points ) {                 let emotion = await predictEmotion( points );                 setText( `Detected: ${emotion}` );                 currentEmotion = emotion;             }             else {                 setText( "No Face" );             }                          requestAnimationFrame( trackFace );         }          (async () => {             await setupWebcam();             const video = document.getElementById( "webcam" );             video.play();             let videoWidth = video.videoWidth;             let videoHeight = video.videoHeight;             video.width = videoWidth;             video.height = videoHeight;              let canvas = document.getElementById( "output" );             canvas.width = video.width;             canvas.height = video.height;              output = canvas.getContext( "2d" );             output.translate( canvas.width, 0 );             output.scale( -1, 1 ); // Mirror cam             output.fillStyle = "#fdffb6";             output.strokeStyle = "#fdffb6";             output.lineWidth = 2;              // Load Face Landmarks Detection             model = await faceLandmarksDetection.load(                 faceLandmarksDetection.SupportedPackages.mediapipeFacemesh             );             // Load Emotion Detection             emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );              setText( "Loaded!" );              trackFace();         })();         </script>     </body> </html>

Что дальше? Возможен ли контроль по состоянию глаз и рта?

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

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

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы

ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/545336/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *