"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

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

f:id:arakan_no_boku:20210215234003p:plain

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

続きものの3回目です。

目次

pico.jsを使う準備をする

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

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

github.com 

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

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

f:id:arakan_no_boku:20210220203252p:plain

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

モザイク風・・です。

JavaScript側の概要

 pico_webcam_sample.jsの内容です。

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

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

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

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

  1. パラメータ設定
  2. 顔検出の実行
  3. 検出した顔の範囲にマスク画像を表示する
流れ1:顔検出カスケードダウンロード

顔検出カスケードダウンロードとは、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');
	})
})

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

流れ2: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のサイズにあわせます。

流れ3:コールバック関数の定義

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つのステップに対応する部分は以下の通りです。 

コールバック関数1:パラメータ設定
    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 
    }

 

コールバック関数2:顔検出の実行 
    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2);

 

コールバック関数3:検出した顔にマスク画像する
    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