"BOKU"のITな日常

テクノロジー以外にも、日常には、面白そうな”イット”がつまってるんだな

JavaScriptだけで人物の写真から顔を検出して、顔部分だけを丸で囲む/pico.js(1)

f:id:arakan_no_boku:20210215234003p:plain 

ブラウザ上で高速に顔検出ができるJavaScriptライブラリ「pico.js」です。 

github.com

使うためには、上記のリンク(GitHub)から「ZIP」でダウンロードして、適当なところに解凍します。

f:id:arakan_no_boku:20210215234542p:plain

中に。

  • pico.js
  • lploc.js

という2つのスクリプトファイルが含まれています。

今回は「pico.js」のみ使います。

lploc.jsは「瞳孔検出」をするライブラリです。

目の動きでお絵描きするとか面白いそうなんですけど、今回は使いません。

HTMLファイルを置くフォルダに「js」というサブフォルダを作って、その下に「pico.js」をコピーします。

 

今回こんな感じに画面上に表示した画像に対して、ボタンを押したら顔検出して、結果を表示する感じにしてみました。

f:id:arakan_no_boku:20210216001127p:plain

 

pico.jsを使うHTML側のポイントです。 

まず。

<head></head>部で、上記でコピーした「pico.js」を読み込みます。

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

顔検出の対象とする画像をimgタグで読み込みます。 

<img id="image" width="512" height="341" src="images/people-512x341.jpg" />

顔検出結果画像の表示エリアは「canvas」タグで指定します。

<canvas id="canvas" width="512" height="341" ></canvas>

画像検出をスタートさせるボタンを配置します。

<input type="button" value="検出開始" onclick="fnc_callback(341, 512)">

今回は顔検出処理の本体は「s/pico_sample.js」に書くので、それをロードします。、

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

HTML側のポイントはこれだけです。

 

今度は、JavaScript側です。

pico.jsで顔検出を行う手順は。

事前準備として以下をやっておき

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

検出開始ボタンが押されたとき

  1. パラメータ設定
  2. 顔検出の実行
  3. 検出した顔の範囲を囲む(今回は円<arc>)

を実行する・・という感じです。

 

顔検出カスケードダウンロードで作成済の顔検出用の決定木をダウンロードします。 

pico.jsの顔検出は、450もの決定木を25段階に連結しているので「カスケード」(同じものがいくつも数珠つなぎに連結された構造や、連鎖的あるいは段階的に物事が生じる様子)と呼びます。

それを行う部分が以下です。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
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');
	})
})

ここは、サンプルのまま使うしかないです。

 

次に、検出対象の画像の準備で、Canvasに変換対象の画像をロードします。

var ctx = document.getElementById('canvas').getContext('2d');
var img = document.getElementById('image');
img.onload = () => ctx.drawImage(img, 0, 0);

事前準備は以上です。 

 

ここからは「検出開始」ボタンを押された後(つまり、fnc_callback関数の中身)の処理になります。

pico.jsで顔検出するメソッドに渡すパラメータを準備します。

  • image
  • params

の2つです。

imageパラメータの「pixels」には、顔検出対象画像データの生のピクセル値(赤、緑、青+アルファ形式を取得して、グレースケール変換したものをセットします。

あとのパラメータは、とりあえずサンプル(デフォルト)のままでやるのが無難です。

ctx.drawImage(img, 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": 20,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

グレースケール変換しているのは「rgba_to_grayscale」メソッドです。

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;
}

画像データのWidthとHeightの指定が必要です。

 

顔検出の実行は以下のようにします。

2行でワンセットと考えたほうがいいです。

dets = pico.run_cascade(image, facefinder_classify_region, params);
dets = pico.cluster_detections(dets, 0.2);

上の1行だけだと、同じ顔を少しずつ違った座標で何回も検出してしまいます。 

それを整理して、一番有効なひとつに絞り込むのが2行目・・って感じです。

 

検出した顔の範囲は上記の「dets」にはいってます。

複数検出しても、全部はいってます。

なので、ループでまわして、検出した顔のまわりを線で囲います。

qthresh = 5.0
for(i=0; i<dets.length; ++i){
    if(dets[i][3]>qthresh)
    {
        ctx.beginPath();
        ctx.arc(dets[i][1], dets[i][0], dets[i][2]/2, 0, 2*Math.PI, false);
        ctx.lineWidth = 3;
        ctx.strokeStyle = 'red';
        ctx.stroke();
    }
}

beginPathから始まる一連の処理で「赤い円」を描いてます。 

このへんは、html5canvasの処理そのものなので、ここでは解説しません。

興味があれば、こちらをどうぞ。

developer.mozilla.org

  

実行するにあたっては、ちょっと注意点があります。

ちゃんと、WEBサーバーたちあげて「http://localhost」で実行しないで、HTMLをダブルクリックで動かそうとしてもエラーになる場合があります。

Chromeだとこんな感じのメッセージ。

Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data

これは「 Chrome では情報漏洩対策ですべてのローカルファイルが別オリジンだとみなされる」という仕様によるものなので、回避不可能です。

横着しないで、ローカルでもWEBサーバーをちゃんと立てましょうということですね。

 

こういう写真に対して「検出開始」ボタンを押すと。

f:id:arakan_no_boku:20210217225001p:plain

結果はこんな感じ。

f:id:arakan_no_boku:20210217225114p:plain

よしよし。

とりあえず、いけてます。

 

最後にJaScriptのソース全体です。

HTMLは上記を参考にして、適当にレイアウトして作ってください。

顔写真は適当にネットからひろってきて、リサイズしてください。

横を向いた顔だと検出されないときがあったり、あまり、サイズの小さい顔はスルーされたりもしますので、何枚かためして、感触をつかむ必要はあるかもです。

var facefinder_classify_region = function(r, c, s, pixels, ldim) {return -1.0;};
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 nrow = 341;
//var ncol = 512;
var ctx = document.getElementById('canvas').getContext('2d');
var img = document.getElementById('image');
img.onload = () => ctx.drawImage(img, 0, 0);

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;
}

function fnc_callback(nrow, ncol) {
    ctx.drawImage(img, 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": 20,       
        "maxsize": 1000,     
        "scalefactor": 1.1 
    }

    dets = pico.run_cascade(image, facefinder_classify_region, params);
    dets = pico.cluster_detections(dets, 0.2);
    qthresh = 5.0
    for(i=0; i<dets.length; ++i){
        if(dets[i][3]>qthresh)
        {
            ctx.beginPath();
            ctx.arc(dets[i][1], dets[i][0], dets[i][2]/2, 0, 2*Math.PI, false);
            ctx.lineWidth = 3;
            ctx.strokeStyle = 'red';
            ctx.stroke();
        }
    }
}

ではでは。