プログラミング

Javascriptでまばたき検知 face-api.js landmarks

投稿日:2022年2月3日 更新日:

顔認識の技術の発展により、今日ではまばたきの検知まで容易に行えるようです。
例えばPython、OpenCV、dlibを使用したものでは

https://www.pyimagesearch.com/2017/04/24/eye-blink-detection-opencv-python-dlib/

があります。
日本語でないと読む気がしない方はQiitaにも複数の記事があるのでそちらをご参照ください。

さて、本記事では同様の事をJavascriptでやってみようと思います。Webサイトで手軽に作成できたら様々な用途に使用できそうです。

face-api.jsを使用する

JavascriptではPythonのようにdlibを簡単には使用できないようです。かわりに顔認識のライブラリ「face-api.js」を使用します。導入手順はWeb情報にお任せで省略します。

“eye aspect ratio”が得られない

しかし、face-api.jsではdlibのように目を閉じた状態の目の形状を得る事ができないようです。Webカメラで顔を映し、face landmarkの68点を表示させた状態で目を閉じても目を開けている状態と同じ形状が表示されます。どうやら目を閉じた状態の学習データまでは組み込まれていないようです。なので上記ページ(Python,dlib)で用いられているEAR(eye aspectr ratio)でまばたきを検知する事ができません。少々、こまった事になりましたが、Webを検索すると黒目の部分(Iris)の状態変化を追うことでも検知できるようです。サンプルコードまでは見つけられませんでしたがEAR方式と同様に単純な計算で実装が可能そうなのでやってみました。

Irisの色の変化で目の開閉を判別

face landmarkにより目の位置は分かるので、黒目の位置も特定する事ができます。黒目の中心付近のRGB値を毎フレームごとに取ってくれば、目が開いている状態では黒目、閉じている状態ではまぶたの色を得る事ができます。これにより目の開閉を判別できそうです。

実用化には様々な工夫が必要

簡単なサンプルは上記手順で作成できるでしょうが、映像側に色々と制限が必要です。

・顔の正面からの映像が得られている事
・目を正面から動かさない

など。

何かの用途に使用するなら色々と工夫が必要となりそうです。
(黒目の位置の追従など)

コード

ご参考にしてください。

faceApiSample.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script defer src="face-api.min.js"></script>
  <script defer src="script.js"></script>
  <style>
    body {
      margin: 0;
      padding: 0;
      width: 100vw;
      height: 100vh;
      display: flex; 
      justify-content: center; 
      align-items: center;
    }

    canvas {
      position: absolute;
    }
  </style>
</head>

<body>
  <video id="video" width="720" height="540" autoplay muted playsinline="true"></video>
 
</body>
</html>

script.js

const video = document.getElementById('video')
var mBlinkSound = new Audio("/sound/shotgun-firing1.mp3");

Promise.all([
  faceapi.nets.tinyFaceDetector.loadFromUri('/facemodels'),
  faceapi.nets.faceLandmark68Net.loadFromUri('/facemodels'),
  faceapi.nets.faceRecognitionNet.loadFromUri('/facemodels'),
  faceapi.nets.faceExpressionNet.loadFromUri('/facemodels')
]).then(startVideo)

function startVideo() {

  if (navigator.userAgent.match(/iPhone|iPad|Android/)) { ///iPhone|Android.+Mobile/
    console.log("Mobile");
     video.width = 400; //1080;

    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
    .then(localMediaStream => {
      if ('srcObject' in video) {
        video.srcObject = localMediaStream;
      } else {
        video.src = window.URL.createObjectURL(localMediaStream);
      }
      video.play();
    })
    .catch(err => {
      console.error(`Not available!!!!`, err);
    });

  } 
  else {
    console.log("PC");
    navigator.getUserMedia(
        { video: {} },
        stream => video.srcObject = stream,
        err => console.error(err)
      )    
  }
  console.log("video:"+[video.width, video.height]);

  // let div = document.createElement('div')
  // div.innerText = 'video size:'+video.width+', '+video.height
  // console.log(div.innerText);
  // document.body.appendChild(div)
}

video.addEventListener('play', () => {

  var canvas_bg = document.createElement("canvas");
  canvas_bg.width = video.width;
  canvas_bg.height = video.height;
  document.body.append(canvas_bg)
  var ctx_bg = canvas_bg.getContext('2d');
  // ctx_bg.fillStyle = "rgb(0,0,0)";
  // ctx_bg.fillRect(0, 0, video.width, video.height/2);

  var canvas_face = document.createElement("canvas");
  canvas_face.width = video.width;
  canvas_face.height = video.height;
  var ctx_face = canvas_face.getContext('2d');

  const canvas = faceapi.createCanvasFromMedia(video)
  document.body.append(canvas)
  const displaySize = { width: video.width, height: video.height }
  faceapi.matchDimensions(canvas, displaySize)
  
  var t1 = performance.now();
  var irisC = [];
  let nowBlinking = false;
  let blinkCount = 0;

  setInterval(async () => {
    //const detections = await faceapi.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
    const detections = await faceapi.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks()
    const resizedDetections = faceapi.resizeResults(detections, displaySize)
    canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
    //faceapi.draw.drawDetections(canvas, resizedDetections)
    faceapi.draw.drawFaceLandmarks(canvas, resizedDetections)
    //faceapi.draw.drawFaceExpressions(canvas, resizedDetections)

    //console.log(resizedDetections);
    const landmarks = resizedDetections[0].landmarks;
    //console.log(landmarks);
    const landmarkPositions = landmarks.positions;

    //--- Iric mark ---//
    ctx_bg.clearRect(0, 0, canvas_bg.width, canvas_bg.height)
    var x_ = landmarkPositions[38-1].x
    var y_ = landmarkPositions[38-1].y
    var w_ = landmarkPositions[39-1].x - landmarkPositions[38-1].x
    var h_ = landmarkPositions[42-1].y - landmarkPositions[38-1].y
    ctx_bg.fillStyle = "rgb(255,0,0)";
    ctx_bg.fillRect(x_, y_, w_, h_)

    x_ = landmarkPositions[44-1].x
    y_ = landmarkPositions[44-1].y
    w_ = landmarkPositions[45-1].x - landmarkPositions[44-1].x
    h_ = landmarkPositions[48-1].y - landmarkPositions[44-1].y
    ctx_bg.fillRect(x_, y_, w_, h_)

    //--- Face mask ---//
    ctx_bg.fillStyle = 'rgb(0,200,0)';
    ctx_bg.beginPath();
    ctx_bg.moveTo(landmarkPositions[0].x, landmarkPositions[0].y);
    for(var i=1;i<17;i++){
      ctx_bg.lineTo(landmarkPositions[i].x, landmarkPositions[i].y);
    }
    ctx_bg.fill();

    ctx_bg.moveTo(landmarkPositions[0].x, landmarkPositions[0].y);
    ctx_bg.lineTo(landmarkPositions[17].x, landmarkPositions[17].y);
    ctx_bg.lineTo(landmarkPositions[27].x, landmarkPositions[17].y);
    ctx_bg.lineTo(landmarkPositions[27].x, landmarkPositions[0].y);
    //ctx_bg.lineTo(landmarkPositions[26].x, landmarkPositions[26].y);
    ctx_bg.lineTo(landmarkPositions[16].x, landmarkPositions[16].y);
    ctx_bg.lineTo(landmarkPositions[16].x, landmarkPositions[16].y-200);
    ctx_bg.lineTo(landmarkPositions[0].x, landmarkPositions[0].y-200);
    ctx_bg.lineTo(landmarkPositions[0].x, landmarkPositions[0].y);
    ctx_bg.fill();

    //--- Iris value ---//
    ctx_face.clearRect(0, 0, canvas_face.width, canvas_face.height)
    ctx_face.drawImage(video, 0, 0, video.width, video.height);
    var frame = ctx_face.getImageData(0, 0, video.width, video.height);
    var p_ = Math.floor(x_+w_/2) + Math.floor(y_+h_/2) * video.width
    //console.log("eye_RGB:"+[frame.data[p_*4+0], frame.data[p_*4+1], frame.data[p_*4+2]]);
    var v_ = Math.floor( (frame.data[p_*4+0] + frame.data[p_*4+1] + frame.data[p_*4+2])/3 );
    console.log("irisC:"+v_);

    irisC.push(v_);
    if(irisC.length>100){
        irisC.shift();
    }//

    let meanIrisC = irisC.reduce(function(sum, element){
      return sum + element;
    }, 0);
    meanIrisC = meanIrisC / irisC.length;
    let vThreshold = 1.5;

    let currentIrisC = irisC[irisC.length-1];
    if(irisC.length==100){
       if(nowBlinking==false){
          if(currentIrisC>=meanIrisC*vThreshold){
              nowBlinking = true;
          }//
       }//
       else{
          if(currentIrisC<meanIrisC*vThreshold){
              nowBlinking = false;
              blinkCount += 1;
              mBlinkSound.pause();
              mBlinkSound.currentTime = 0;
              mBlinkSound.play();
          }//
       }//

    }//

    //--- Graph ---//
    ctx_bg.strokeStyle = 'red';
    ctx_bg.lineWidth = 5;
    var Ox = 0;
    var Oy = canvas_bg.height/2;
    var Lx = canvas_bg.width;
    var Ly = canvas_bg.height/2;
    var vx = 0/irisC.length * Lx;
    var vy = irisC[0]/255 * Ly;
    ctx_bg.beginPath();
    ctx_bg.moveTo(Ox+vx, Oy-vy);
    for(var i=1;i<irisC.length;i++){
      vx = i/irisC.length * Lx;
      vy = irisC[i]/255 * Ly;
      ctx_bg.lineTo(Ox+vx, Oy-vy);
    }
    ctx_bg.stroke();

    //--- mean value x threshold(1.X)
    ctx_bg.strokeStyle = 'rgb(0,255,0)';
    ctx_bg.lineWidth = 2;
    ctx_bg.beginPath();
    vx = 0 * Lx;
    vy = meanIrisC*vThreshold/255 * Ly;
    ctx_bg.moveTo(Ox+vx, Oy-vy);
    vx = 1 * Lx;
    ctx_bg.lineTo(Ox+vx, Oy-vy);
    ctx_bg.stroke();

    var ctx = canvas.getContext('2d');

    var t2 = performance.now();//ms
    ctx.font = "48px serif";
    ctx.fillText("FPS:"+ Math.floor(1000.0/(t2-t1)), 10, 50);
    ctx.fillText("Count:"+blinkCount, 10, 100);
    if(nowBlinking){
      ctx.fillText("Blinking", 10, 150);
    }
    //ctx.fillText("FPS:"+ (t2-t1), 10, 50);
    t1 = t2;

  }, 33)

})

動作確認

こちらから動作確認できます。
Webカメラが必要です。スマホは現在無理そうです。

https://g-llc.co.jp/faceApiSample


動画です。

-プログラミング

執筆者:

関連記事

100万DLアプリを生み出せ

アプリ公開で収益を得るようになってから数年、いまだ大当たりはありません。1本でも大当たりアプリを生み出した=アプリで成功と言って良いでしょう。では、そもそも「大当たり」アプリとはどんなものでしょうか? …

How to use FFmpeg.wasm. What’s the Cross Origin Isoration?

Nowadays, I got the information that FFmpeg can be used with Javascript and I tried it immediately. …

Found an unexpected Mach-O header code: 0x72613c21 への対処 Xcode, Admob, xcframework

日々変わっていくAdmobの仕様への対応にとても苦労しています。そんな中、アプリをApple Store Connectへ提出するためXcodeにてArchiveを作成してValidateしようとした …

Javascript eye blink detection using face-api.js landmarks

With the development of face recognition technology, today it seems that eye blinking can be easily …

サイトの仕様: ナビゲーションを解決する方法。Admobポリシーセンター広告制限

一年ほど前、突如としてAndroidアプリのAdmob広告に配信制限がかかりました。理由は「サイトの仕様: ナビゲーション」。これが厄介で、一体何を修正すべきなのかが全く分かりません。Web検索をかけ …

スポンサーリンク