はじめに
前回、npmとTypeScriptの初歩について学びました。
今回は MediaPipe で顔認識をやってみましょう。npmの話も登場します。
必要なもの
- npm
- HTML/CSS/JavaScriptの知識
- Webカメラの接続
顔認識の基本の基本
顔認識の基本の基本からやってみましょう。
次のようなファイル face-mesh.html を作成してください:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>MediaPipe FaceMesh Demo</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
}
video, canvas {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
object-fit: cover;
}
</style>
<!-- MediaPipeライブラリをCDN経由で読み込み -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
</head>
<body>
<video id="input_video" playsinline style="display:none;"></video>
<canvas id="output_canvas"></canvas>
<script>
const videoElement = document.getElementById("input_video");
const canvasElement = document.getElementById("output_canvas");
const canvasCtx = canvasElement.getContext("2d");
// FaceMesh初期化
const faceMesh = new FaceMesh({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`,
});
faceMesh.setOptions({
maxNumFaces: 1,
refineLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
faceMesh.onResults((results) => {
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
if (results.multiFaceLandmarks) {
for (const landmarks of results.multiFaceLandmarks) {
drawConnectors(canvasCtx, landmarks, FACEMESH_TESSELATION, {color: "lightgreen", lineWidth: 1});
drawLandmarks(canvasCtx, landmarks, {fillColor: "green", radius: 0.5});
}
}
canvasCtx.restore();
});
// カメラを起動
const camera = new Camera(videoElement, {
onFrame: async () => {
await faceMesh.send({image: videoElement});
},
width: 640,
height: 480
});
camera.start();
</script>
</body>
</html>
face-mesh.htmlをブラウザで開くと、カメラの使用許可を求められ、許可すると、カメラの映像が画面いっぱいに表示されます。顔をカメラに向けると、顔が認識されていることがわかります。
顔認識で使うMediaPipeライブラリをCDN経由で読み込んでいますので、必要なライブラリも自動で読み込まれます。でも、本番環境ではCDNを使うべきではないかもしれません。そこで、前回学んだnpmを使って開発環境と本番環境を構築しましょう。
開発環境の構築
今回は、Viteというフロンドエンドツールを使います。Viteはあれこれ自動化してくれるので便利です。
Viteのレポジトリを作成します。
mkdir mediapipe-face
cd mediapipe-face
npm create vite@latest . # "." はカレントフォルダに作る
Vite側から質問(英語)がある場合は、次のように矢印キーとEnterキーなどで答えてください。
- Framework → Vanilla
- Variant → JavaScript
今回は、DOM操作を伴うので、TypeScriptではなくJavaScriptを選びます。
次に依存関係をインストールします。
npm install
必要な MediaPipe パッケージを追加します。
npm install @mediapipe/face_mesh @mediapipe/camera_utils @mediapipe/drawing_utils
MediaPipe の各ソリューション(Face Mesh 等)は NPM パッケージとして公開されています。開発用ユーティリティ(camera_utils / drawing_utils)も用意されています。
それから、HTMLを構築します。Viteならindex.htmlを自動的に作成してくれるので、
次のように <body> 内部を編集するだけでOKです。
...
<body>
<video id="input_video" class="input_video" autoplay playsinline style="display:none"></video>
<canvas id="output_canvas"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
...
ファイル src/main.jsを次の内容で置き換えます:
// src/main.js
import { FaceMesh, FACEMESH_TESSELATION, FACEMESH_RIGHT_EYE, FACEMESH_LEFT_EYE, FACEMESH_LIPS } from "@mediapipe/face_mesh";
import { Camera } from "@mediapipe/camera_utils";
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils";
const videoElement = document.getElementById("input_video");
const canvasElement = document.getElementById("output_canvas");
const canvasCtx = canvasElement.getContext("2d");
// FaceMesh を初期化。locateFile でモデル等を CDN から取得する例。
const faceMesh = new FaceMesh({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
});
// 設定(パラメータ調整で速度/精度をトレードオフできます)
faceMesh.setOptions({
maxNumFaces: 1, // 検出する顔の最大数
refineLandmarks: true, // 目の虹彩など精密化(重くなる)
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
// 結果コールバック
faceMesh.onResults(onResults);
function onResults(results) {
// キャンバスサイズを動画に合わせる
if (!results.image) return;
canvasElement.width = results.image.width || videoElement.videoWidth;
canvasElement.height = results.image.height || videoElement.videoHeight;
// クリアして動画フレームを描画
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
// ランドマークがある場合、接続線や点を描画
if (results.multiFaceLandmarks) {
for (const landmarks of results.multiFaceLandmarks) {
// 全体のメッシュ
drawConnectors(canvasCtx, landmarks, FACEMESH_TESSELATION, { lineWidth: 0.5, color: "lightgreen" });
// 目や唇を強調
drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYE);
drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYE);
drawConnectors(canvasCtx, landmarks, FACEMESH_LIPS);
// 各ランドマーク点(小さな円など)
drawLandmarks(canvasCtx, landmarks, {radius: 0.5});
}
if (results.multiFaceLandmarks[0]) {
// 例: 鼻先 (index 1) の正規化座標(0..1)
const nose = results.multiFaceLandmarks[0][1];
const noseX = nose.x * canvasElement.width;
const noseY = nose.y * canvasElement.height;
// console.log("鼻のピクセル座標:", noseX.toFixed(1), noseY.toFixed(1));
}
}
canvasCtx.restore();
}
// カメラを起動して毎フレーム faceMesh に送る
const camera = new Camera(videoElement, {
onFrame: async () => {
await faceMesh.send({image: videoElement});
},
width: 640,
height: 480,
facingMode: "user"
});
camera.start();
そしてサーバを起動します。
npm run dev
ブラウザで http://localhost:5173(Vite の表示に従う)を開き、カメラ許可を与えて、顔を向ければキャンバスに顔メッシュが描画されるはずです。
Viteのおかげで、src/main.jsを変更すると、すぐさま自動的にサーバが更新され、ブラウザで再読み込みされます。
Windowsの場合、サーバはCtrl+Cで停止します。
本番環境の構築
npm run build
これで dist/ に index.html, assets などが生成されるはずです。中を見て確認してください。JavaScriptがコンパイルされて難読化されていることがわかります。
この dist/の中身をWebサーバーに置けば、顔認識が動作するようになります。
http-serverでテスト
http-serverがない場合はあらかじめ npm install -g http-server を実行します。
npm install -g http-server
次のコマンドを実行すれば、ブラウザとサーバが起動し、顔認識が動作します。
http-server ./dist -o
CDNのURLが残っている!?
難読化されたJavaScriptを確認すると、
...
const faceMesh = new FaceMesh({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
});
...
まだ、CDNのURLが残っています。こりゃまずい。ということで要対処。
本番環境にライブラリがないということなので、今回はpublic/3rdpartyフォルダにサードパーティーのライブラリを置こうと思います。次のようなprebuild.shというBashスクリプトをルートに置きます。
#!/usr/bin/bash
mkdir -p public/3rdparty
rm -fr public/3rdparty/*
cp -r node_modules/@mediapipe/* public/3rdparty
面倒臭いのでまとめてコピーです。バンドルサイズを削減したい場合はrmコマンドで各自削ってください。ビルド時にpublicの中身はdistの中に自動的にコピーされます。
public/3rdpartyはかなり大きくなるので、ファイル.gitignoreにpublic/を追加します。
また、package.jsonの"prebuild"に"bash prebuild.sh"を設定します。
...
"scripts": {
...
"prebuild": "bash prebuild.sh",
...
},
...
これでビルド前に prebuild.shが走ります。
そして、src/main.jsの該当箇所を次のように変更します。
...
// FaceMesh を初期化。
const faceMesh = new FaceMesh({
//locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
locateFile: (file) => `/3rdparty/face_mesh/${file}`
});
...
古いURLも後々のため、残しておきます。そしてビルド&テストです。
npm run build
http-server ./dist -o
ブラウザの開発者ツールの「ネットワーク」タブで外部と通信をしていないことを確認します。これでOKです。
本番環境はルートとは限らない
次の行を見てください:
locateFile: (file) => `/3rdparty/face_mesh/${file}`
ルートの3rdpartyを見ているでしょう? 本番環境をルートに置くとは限らないので、これはちょっとまずいです。
ドット(.)を付けて、こうします:
locateFile: (file) => `./3rdparty/face_mesh/${file}`
さらに、パッケージフォルダに次のような内容のファイル vite.config.tsを作成します:
import { defineConfig } from 'vite';
export default defineConfig({
base: "./"
});
これで本番環境がルートじゃなくても動きます。
未保護の変数は丸見えで変更可能
ブラウザのメニューの「開発者ツール」から「コンソール」タブを開いてください。JavaScriptのコンソールでエラーや情報があれば、ここに表示されます。cameraなどの定義済みの変数名を入力すると、変数の値が表示されます。また、保護されていない変数は自由に変更可能です。任意の利用可能なJavaScript関数を実行できます。おまけに難読化されていなければ、子どもでも「ハッキング」できます。ですから難読化やアクセス制限は重要なのです。
サンプルコードであれば、変数にアクセスできることは便利ですが、本番環境でフルアクセスは問題があります。損害賠償を想定してください。簡単な対策としては、使用する変数とメイン処理を無名関数で包むこと((function(){ ... })();)やクラスを使うことですが、対策が難しいものにはオートクリッカーがあります。「ミサイルを発射する」ボタンを勝手に押されるとまずいのはわかりますが、対策は困難です。重大な処理には確認が必要です。
終わりに
顔認識の実装を通じてnpmとVite.jsの使い方について学びました。連絡は、katayama.hirofumi.mz@gmail.com まで。
