SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

JavaScriptでリアルタイムに顔認識して顔にモザイク風マスクをかぶせる/pico.js(3)

f:id:arakan_no_boku:20210215234003p:plain

目次

pico.jsでリアルタイム顔認識

高速に顔認識ができるJavaScriptライブラリ「pico.js」を使って、WEBカメラに映った自分の顔にリアルタイムでモザイク風のマスクをかぶせてみます。

続きものの3回目です。

JavaScriptのみでブラウザ上で顔認識ができる「pico.js」を使います。

ダウンロードはこちらからできます。

github.com 

以後の説明は、HTMLを置く作業フォルダがあって、そこに「js」というサブフォルダがある前提で行います。

GitHubから上記をダウンロード後解凍し、「js」フォルダに「pico.js」と「examples」フォルダにある「camvas.js」をコピーします。

f:id:arakan_no_boku:20210220203252p:plain

pico.js:HTMLファイル側のポイント 

HTML側の記述です。

まず、<head></head>の間にスクリプトタグを置きます。

<script src="js/camvas.js"></script>
<script src="js/pico.js"></script>

変換すみのカメラ映像を表示するCanvasタグと、マスクする画像を読み込むimgタグを置きます。

<div>
   <canvas id="canvas" width="640" height="480" ></canvas>
</div>

<div style="display:none;">
   <img id="mask" src="images/mask.jpg">
</div>

処理はjs/pico_webcam_sample.jsファイルに記述します。

bodyの最後の方で以下のように読み込みます。

<script src="js/pico_webcam_sample.js"></script>

HTML側で、WEBカメラを使うときは、ふつう<video>タグを使います。

でも、上記は<canvas>タグになってます。

WEBカメラの映像は、camvas.jsの中で動的に定義され<video>タグに流すので、HTML側に書いておく必要がないからです。

JavaScript内でVideo0タグの映像を加工して、canvasに描きだしています。 

マスクで使う画像(mask.jpg)の画像はこのようなものを準備しました。

f:id:arakan_no_boku:20210220221950p:plain

モザイク風・・です。

pico.js:JavaScript側の概要

 pico_webcam_sample.jsの内容です。

おおまかな流れは、前々回(1)と同じです。

事前準備として以下をやります。

  1. 顔認識カスケードダウンロード
  2. 認識対象の画像の準備

そして、以下の処理をリアルタイムで繰り返します。

  1. パラメータ設定
  2. 顔認識の実行
  3. 認識した顔の範囲にマスク画像を表示する
pico.js:事前準備_顔認識カスケードダウンロード

顔認識カスケードダウンロードとは、pico.jsの顔認識に必要な作成済決定木をダウンロードする工程です。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
var update_memory = pico.instantiate_detection_memory(5); 
var cascadeurl = 'https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder';
fetch(cascadeurl).then(function(response) {
	response.arrayBuffer().then(function(buffer) {
	var bytes = new Int8Array(buffer);
		facefinder_classify_region = pico.unpack_cascade(bytes);
		console.log('* cascade loaded');
	})
})

ここは、あれこれ考えず、この通りにする以外の手はありません。

pico.js:事前準備_Canvasとマスク画像のロード 

次に、認識対象の画像の準備として、Canvasとマスク画像をロードします。

var ctx = document.getElementById('canvas').getContext('2d');
//var img = document.getElementById('image');
var msk_img = document.getElementById('mask');
//img.onload = () => ctx.drawImage(img, 0, 0);
var nrow = 480
var ncol = 640

幅と高さは、HTMLで定義するcanvasのWidthとHeightのサイズにあわせます。

pico.js:認識処理_コールバック関数の定義

WEBカメラの画像にあわせてリアルタイムで繰り返すコールバック関数内で以下の3つの処理を定義します。

  • パラメータ設定
  • 顔認識の実行
  • 認識した顔の範囲にマスク画像を表示する

リアルタイムで繰り返すコールバック関数は、「processfnc」としました。

このコールバック関数名は「camvas.js」を使うときに引数として渡します。

var mycamvas = new camvas(ctx, processfunc);

これだけで、WEBカメラから映像を読み込んで、processfncに定義した顔認識処理を行い、結果をcanvasに 出力する処理を「camvas」がやってくれます。

コールバック関数 processfncのソース全体

さてprocessfncの内容です。 

最初に「processfnc」全体のソースを示します。

var processfunc = function(video, dt) {
    ctx.drawImage(video, 0, 0);
    var rgba = ctx.getImageData(0, 0, ncol, nrow).data;
    image = {
        "pixels": rgba_to_grayscale(rgba, nrow, ncol),
        "nrows": nrow,
        "ncols": ncol,
        "ldim": ncol
    }
    params = {
        "shiftfactor": 0.1, 
        "minsize": 100,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2);
    qthresh = 50.0
  
    for(i=0; i<dets.length; ++i){
        if(dets[i][3]>qthresh)
        {
            ctx.beginPath();
            ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2])
        }
    }
}

ここで、3つのステップに対応する部分は以下の通りです。 

pico.js:認識処理_コールバック関数:パラメータ設定
    ctx.drawImage(video, 0, 0);
    var rgba = ctx.getImageData(0, 0, ncol, nrow).data;
    image = {
        "pixels": rgba_to_grayscale(rgba, nrow, ncol),
        "nrows": nrow,
        "ncols": ncol,
        "ldim": ncol
    }
    params = {
        "shiftfactor": 0.1, 
        "minsize": 100,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

 

pico.js:認識処理_コールバック関数:顔認識の実行 
    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2);

 

pico.js:認識処理_コールバック関数:認識した顔にマスクする
    qthresh = 50.0
    for(i=0; i<dets.length; ++i){
        if(dets[i][3]>qthresh)
        {
            ctx.beginPath();
            ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2])
        }
    }

静止画の処理との違いは、入力が「video」になっていることと、パラメータ「qthresh = 50.0」の値が、静止画の約10倍くらいになっていることです。

この値が小さいと感度があがりすぎて、チラチラした見づらい画面になるためです。

50.0くらいがちょうどいい感じみたいです。

静止画の処理のポイントは、前々回(1)に書いています。

実行したイメージ 

動かしてみました。

f:id:arakan_no_boku:20210221203716p:plain

おーー。顔がモザイクされたっぽくなってる。

顔を横にずらしてみます。

f:id:arakan_no_boku:20210221203834p:plain

ちゃんと追随してきます。

スペックの高くないノートPCでも、動きが非常にスムースです。

いいですね。

確かに顔を横にむけたり、カメラの端で顔が一部隠れたりすると、顔が認識されずにマスクがはずれたりしますけど、この実行速度を考えたら、認識精度もたいしたもんだと思いますけどね。

JavaScriptソース全体 

今回のJavaScriptソースだけ全体を掲載しておきます。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
var update_memory = pico.instantiate_detection_memory(5); 
var cascadeurl = 'https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder';
fetch(cascadeurl).then(function(response) {
	response.arrayBuffer().then(function(buffer) {
	var bytes = new Int8Array(buffer);
		facefinder_classify_region = pico.unpack_cascade(bytes);
		console.log('* cascade loaded');
	})
})


var ctx = document.getElementById('canvas').getContext('2d');
//var img = document.getElementById('image');
var msk_img = document.getElementById('mask');
//img.onload = () => ctx.drawImage(img, 0, 0);
var nrow = 480
var ncol = 640

function rgba_to_grayscale(rgba, nrows, ncols) {
    var gray = new Uint8Array(nrows*ncols);
    for(var r=0; r<nrows; ++r)
        for(var c=0; c<ncols; ++c)
            gray[r*ncols + c] = (2*rgba[r*4*ncols+4*c+0]+7*rgba[r*4*ncols+4*c+1]+1*rgba[r*4*ncols+4*c+2])/10;
    return gray;
}

var processfunc = function(video, dt) {
    ctx.drawImage(video, 0, 0);
    var rgba = ctx.getImageData(0, 0, ncol, nrow).data;
    image = {
        "pixels": rgba_to_grayscale(rgba, nrow, ncol),
        "nrows": nrow,
        "ncols": ncol,
        "ldim": ncol
    }
    params = {
        "shiftfactor": 0.1, 
        "minsize": 100,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2);
    qthresh = 50.0
  
    for(i=0; i<dets.length; ++i){
        if(dets[i][3]>qthresh)
        {
            ctx.beginPath();
            ctx.drawImage(msk_img, dets[i][1]-(dets[i][2]/2), dets[i][0]-(dets[i][2]/2), dets[i][2], dets[i][2])
        }
    }
}

var mycamvas = new camvas(ctx, processfunc);

ではでは。

関連:第一回・第二回のリンク

前回(2)と前々回(1)と重複する部分は、今回説明していませんので、以下のリンクから記事を参照してください。。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com