🔰

FaceAPIであっさり顔認識

2021/08/30に公開

face-api.jsとは!?

face-api.jsは、JavaScriptで気軽に顔検出が出来るライブラリです。

face-api.js

ライブラリのダウンロード

face-api.jsのソースコードは、GitHubに公開されております。
次のサイトにアクセスし、1, 2の順番にクリックしてライブラリ1式をダウンロードしましょう。

face-api.js

必要なファイル

圧縮ファイルをダウンロードしたら、解凍して必要なファイルを取り出しましょう。

1, face-api.jsライブラリ本体

2, モデルデータ1式(学習済みデータです)

プロジェクトを作る

プロジェクトを作り、これらのファイルを配置します。

MyProject01/
 ├ assets/ (顔認識にかける画像データを格納します)
 ├ weights/ (モデルデータ1式です)
 ├ face-api.js (face-api.jsライブラリ本体です)
 ├ index.html (プログラムを起動するファイルです)
 ├ main.js (メインのプログラムを記述するファイルです)

HTMLファイルを用意する

では、作っていきましょう。
HTMLファイルを用意して、下記コードを記述します。

index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<style type="text/css">body{margin:0; padding:0;}</style>
	<script src="./face-api.js"></script>
	<script src="./main.js"></script>
</head>
<body>
	<canvas id="myCanvas" />
</body>
</html>

"canvas"には、顔認識に使った画像と、その結果を描画していきます。

JavaScriptファイルを用意する

次はメインのプログラムです。

face-api.jsライブラリを読み込む事で、"faceapi"を扱える様になります。

最初に、Promiseを利用して下記の3つの関数を実行しましょう。
引数には、それぞれ"モデルデータへのパス"を指定します。
"loadSsdMobilenetv1Model関数"で、オブジェクト検出モデルのロード、
"loadFaceLandmarkModel関数"で、ランドマーク検出モデルのロード、
"loadFaceRecognitionModel関数"で、顔認識モデルのロードを実行します。

main.js
Promise.all([
	faceapi.loadSsdMobilenetv1Model(モデルデータへのパス),
	faceapi.loadFaceLandmarkModel(モデルデータへのパス),
	faceapi.loadFaceRecognitionModel(モデルデータへのパス)
]).then(ライブラリの準備が出来た時に実行される関数);

実装例は、次の様になります。

main.js
const FILE_URL  = "./assets/sample_01.png";
const MODEL_URL = "./weights";

let img, canvas, context;

window.onload = (event)=>{
	console.log("onload!");
	loadModels();
}

async function loadModels(){
	console.log("loadModels");
	Promise.all([
		faceapi.loadSsdMobilenetv1Model(MODEL_URL),
		faceapi.loadFaceLandmarkModel(MODEL_URL),
		faceapi.loadFaceRecognitionModel(MODEL_URL)
	]).then(detectAllFaces);
}

async function detectAllFaces(){
	console.log("detectAllFaces");
	// ここで画像解析を行います。
}

画像を解析する

次はいよいよ画像解析です。
今回は、フリー素材を利用して顔認識をしてみようと思います。
次の画像を保存して、"assets"フォルダに格納しましょう。

Luff&Peace <- とても素敵なサイトです!!

次の順番で処理を記述していきます。

1, 画像の読み込み

"faceapi.fetchImage関数"に、解析対象である画像へのパスを指定します。

main.js
img = await faceapi.fetchImage(画像へのパス);

2, HTMLからキャンバスを取得し画像を描画

そして、HTMLからキャンバス要素を取り出し、先ほどの画像を描画します。

main.js
context.drawImage(img, 0, 0);// 画像の描画

3, 顔認識の実行と認識結果の取得

次は、顔認識の実行です。
"faceapi.detectAllFaces関数"に、画像データを渡します。
"withFaceLandmarks関数"は、ランドマークを取得する命令です。
返り値として、認識データを取得する事ができます。

main.js
const fData = await faceapi.detectAllFaces(画像データ).withFaceLandmarks();

4, 認識結果のリサイズ

最後に、"faceapi.resizeResults関数"で認識結果をリサイズします。
ここで、認識データ全体を目的の画角へリサイズする事が可能です。
リサイズ後のデータを"forEach"を使って取り出します。

main.js
const rData = await faceapi.resizeResults(fData, iSize);
rData.forEach(data=>{drawResult(data);});

認識結果データの"data.detection.box"には、顔の位置データが格納されています。
このデータを取り出して、そのままキャンバスに描画します。

キャンバスの描画については、CanvasAPIをご参照ください。

main.js
context.strokeRect(box.x, box.y, box.width, box.height);

ここまでのコードをまとめると、次の様になります。

main.js
// 省略

async function detectAllFaces(){
	console.log("detectAllFaces");
	
	// 1, 画像の読み込み
	img = await faceapi.fetchImage(FILE_URL);

	// 2, HTMLからキャンバスを取得し画像を描画
	canvas = document.getElementById("myCanvas");
	canvas.width = img.width;
	canvas.height = img.height;
	context = canvas.getContext("2d");
	context.fillRect(0, 0, canvas.width, canvas.height);
	context.drawImage(img, 0, 0);// 画像の描画

	// 3, 顔認識の実行と認識結果の取得
	const iSize = {width: img.width, height: img.height};
	const fData = await faceapi.detectAllFaces(img).withFaceLandmarks();
	
	// 4, 認識結果のリサイズ
	const rData = await faceapi.resizeResults(fData, iSize);
	rData.forEach(data=>{drawResult(data);});
}

function drawResult(data){
	console.log("drawResult!!");
	//console.log(data);

	const box = data.detection.box;// 長方形のデータ
	const mrks = data.landmarks.positions;

	context.fillStyle = "red";
	context.strokeStyle = "red";
	context.lineWidth = 4;
	context.strokeRect(box.x, box.y, box.width, box.height);// 長方形の描画
}

顔の上に長方形が描かれています。(皆さんとても爽やかですね!!)

ランドマークについて

次は、顔の輪郭や目や鼻、口の位置を取得してみます。
これらは、"data.landmarks.positions"で取得する事が出来ます。
このデータには、合計68箇所の座標データが含まれています。

これらのデータを使って、鼻メガネを描いてみましょう。(思いつきです!!)

キャンバスの描画については、CanvasAPIをご参照ください。

main.js
// 省略

function drawResult(data){
	console.log("drawResult!!");

	const det = data.detection;
	const mrks = data.landmarks.positions;
	const box = det.box;

	context.fillStyle = "red";
	context.strokeStyle = "red";
	context.lineWidth = 4;
	context.strokeRect(box.x, box.y, box.width, box.height);

	drawNose(mrks);// 鼻を描きます
	drawGrasses(mrks);// メガネを描きます
}

function drawNose(mrks){
	context.strokeStyle = "black";
	context.lineWidth = 2;
	context.beginPath();
	for(let i=27; i<35; i++){
		let fX = mrks[i].x;
		let fY = mrks[i].y;
		let tX = mrks[i+1].x;
		let tY = mrks[i+1].y;
		context.moveTo(fX, fY);
		context.lineTo(tX, tY);
	}
	context.stroke();
}

function drawGrasses(mrks){
	context.strokeStyle = "black";
	context.lineWidth = 2;
	context.beginPath();
	context.moveTo(mrks[39].x, mrks[39].y);
	context.lineTo(mrks[42].x, mrks[42].y);
	context.stroke();
	// 左
	const lX = (mrks[39].x + mrks[36].x)/2;
	const lY = (mrks[41].y + mrks[38].y)/2;
	const lR = (mrks[39].x - mrks[36].x);
	context.beginPath();
	context.arc(lX, lY, lR, 0, Math.PI*2);
	context.stroke();
	// 右
	const rX = (mrks[45].x + mrks[42].x)/2;
	const rY = (mrks[46].y + mrks[43].y)/2;
	const rR = (mrks[45].x - mrks[42].x);
	context.beginPath();
	context.arc(rX, rY, rR, 0, Math.PI*2);
	context.stroke();
}

素敵な鼻メガネを描く事が出来ました。(いろいろ御免なさい!!)

最後に、全体のコードを載せておきますね。

main.js
const FILE_URL  = "./assets/sample_01.png";
const MODEL_URL = "./weights";

let img, canvas, context;

window.onload = (event)=>{
	console.log("onload!");
	loadModels();
}

async function loadModels(){
	console.log("loadModels");
	Promise.all([
		faceapi.loadSsdMobilenetv1Model(MODEL_URL),
		faceapi.loadFaceLandmarkModel(MODEL_URL),
		faceapi.loadFaceRecognitionModel(MODEL_URL)
	]).then(detectAllFaces);
}

async function detectAllFaces(){
	console.log("detectAllFaces");

	// Image
	img = await faceapi.fetchImage(FILE_URL);

	// Canvas, Context
	canvas = document.getElementById("myCanvas");
	canvas.width = img.width;
	canvas.height = img.height;
	context = canvas.getContext("2d");
	context.fillRect(0, 0, canvas.width, canvas.height);
	context.drawImage(img, 0, 0);

	const iSize = {width: img.width, height: img.height};
	const fData = await faceapi.detectAllFaces(img).withFaceLandmarks();
	const rData = await faceapi.resizeResults(fData, iSize);
	rData.forEach(data=>{drawResult(data);});
}

function drawResult(data){
	console.log("drawResult!!");

	const det = data.detection;
	const mrks = data.landmarks.positions;
	const box = det.box;

	context.fillStyle = "red";
	context.strokeStyle = "red";
	context.lineWidth = 4;
	context.strokeRect(box.x, box.y, box.width, box.height);

	drawLandmarks(mrks);
	drawNose(mrks);
	drawGrasses(mrks);
}

function drawLandmarks(mrks){
	for(let i=0; i<mrks.length; i++){
		let x = mrks[i].x;
		let y = mrks[i].y;
		context.fillRect(x, y, 2, 2);
		context.fillText(i, x, y, 18);
	}
}

function drawNose(mrks){
	context.strokeStyle = "black";
	context.lineWidth = 2;
	context.beginPath();
	for(let i=27; i<35; i++){
		let fX = mrks[i].x;
		let fY = mrks[i].y;
		let tX = mrks[i+1].x;
		let tY = mrks[i+1].y;
		context.moveTo(fX, fY);
		context.lineTo(tX, tY);
	}
	context.stroke();
}

function drawGrasses(mrks){
	context.strokeStyle = "black";
	context.lineWidth = 2;
	context.beginPath();
	context.moveTo(mrks[39].x, mrks[39].y);
	context.lineTo(mrks[42].x, mrks[42].y);
	context.stroke();
	// 左
	const lX = (mrks[39].x + mrks[36].x)/2;
	const lY = (mrks[41].y + mrks[38].y)/2;
	const lR = (mrks[39].x - mrks[36].x);
	context.beginPath();
	context.arc(lX, lY, lR, 0, Math.PI*2);
	context.stroke();
	// 右
	const rX = (mrks[45].x + mrks[42].x)/2;
	const rY = (mrks[46].y + mrks[43].y)/2;
	const rR = (mrks[45].x - mrks[42].x);
	context.beginPath();
	context.arc(rX, rY, rR, 0, Math.PI*2);
	context.stroke();
}

最後に

face-api.jsを駆け足で説明させていただきましたが、いかがでしたでしょうか?
クライアントサイドだけでここまで出来てしまうなんて!!
驚いてしまいますよね。
他にも、検出できるデータとして"感情"等も取得する事ができます。(これも面白そうです!!)
ここまで読んでいただき有難うございました。

Discussion