シンプルな顔認識システムを作った midori221
顔を検出して、目・鼻・口・耳の位置を表示する。それだけ。シンプルだから分かりやすく、シンプルだから速い。midori221は、MediaPipe Face Detectionを使った、基本に忠実な顔認識システムです。カメラ切り替え機能も付いて、複数のカメラデバイスに対応しています。
ℹ️
INFO
この記事について: この記事は技術的な詳細を丁寧に説明しているため、長い内容になっています(読了時間: 約32分)。12個のインタラクティブデモも含まれています。時間に余裕のあるときに、じっくり読んでいただけると幸いです。
ちょっと技術的に基本的な内容で、眠くなる内容ですが、途中のインタラクティブコンテンツで遊んでこういうかんじなんだなぁ~~とつかんでいただけたら幸いです。
⚠️
WARNING
カメラ使用について: この記事内のデモでは、お使いのデバイスのWEBカメラを使用します。デモを試す際は、ブラウザからカメラへのアクセス許可が必要です。カメラの映像はすべてブラウザ内で処理され、外部のサーバーには一切送信されません。すべての処理はお使いのデバイス上で完結するため、プライバシーは完全に保護されています。
対象読者
- ブラウザ上で顔認識を実装したいフロントエンドエンジニア
- MediaPipe Face Detectionの基本的な使い方を学びたい開発者
- TensorFlow.js(機械学習をブラウザで実行するライブラリ)を初めて触る技術者
- シンプルな顔認識システムを短時間で構築したい人
記事に書いてあること
- MediaPipe Face Detection(Googleが開発した顔検出モデル)の基本的な実装方法
- 顔の特徴点(目・鼻・口・耳)を検出して描画する処理
- 複数カメラデバイスの検出と切り替え機能
- TensorFlow.jsプラットフォームの初期化とモデル読み込み
- リアルタイム検出ループの実装とパフォーマンス最適化
- カメラストリームの適切な管理とリソース解放
ℹ️
INFO
注意デモのプログラム実行には、信頼度の表示%機能を削除してあります。
作ったもの
midori221は、Webカメラの映像から顔を検出し、顔の周りに枠を描き、目・鼻・口・耳の位置に点を表示するブラウザアプリです。MediaPipe Face Detectionのshortモデルを使用し、2メートル以内の距離に最適化された高速処理を実現しています。
検出された顔には緑色の枠が表示され、信頼度スコアも一緒に表示されます。検出人数も左上にリアルタイムで表示され、顔が検出されるとカウンターが緑色に光ります。
カメラは複数台に対応し、カメラ選択パネルから切り替え可能。接続状態は緑・オレンジ・赤のインジケーターで一目で分かります。
なぜ作ったか
映像を利用したAI処理というのは、ちょっとかっこいい、、、、AIっぽい。。。という印象で、機械学習の初期の頃から世界で様々なAIモデルが研究されてきた模様。
それらを試して使うシステムを作りたくて。写真や映像などを見た方々の反応を判断するための基礎的な技術です。まず第一歩は顔認識で読み取りたい。でも、最初から複雑な機能は必要ない。まずは顔を検出する基本を押さえたい。
年齢や感情の分析は後回し。まずは、どこに顔があるか、目・鼻・口・耳がどこにあるかを正確に検出できるシステムを作りたかった。
MediaPipe Face Detectionは、Googleが開発した高性能な顔検出モデル。ブラウザだけで動き、サーバー不要。無料で使える範囲で、実用的なシステムを作りたい。貧乏でくたびれたおっさんで、かつ、お金がない身としては、これ以上の選択肢はない。ひたすらあるもの生かしてやるしかない!!!という理由。
シンプルに、分かりやすく、速く。そしてお金をかけないで、最高性能を、それがmidori221の目標でした。
MediaPipe Face Detectionの基本実装
MediaPipe Face Detectionは、TensorFlow.jsランタイムで動作する軽量な顔検出モデルです。modelType: 'short'を指定すると、2メートル以内の距離に最適化された高速モデルが読み込まれます。
// プラットフォームの初期化(1回だけ実行)
await tf.ready();
platformInitialized = true;
// 検出器の初期化(初回のみ)
const model = faceDetection.SupportedModels.MediaPipeFaceDetector;
const detectorConfig = {
runtime: 'tfjs', // TensorFlow.jsランタイムを使用
modelType: 'short' // 短距離モデル(高速・軽量)
};
detector = await faceDetection.createDetector(model, detectorConfig);
最初は、毎回検出器を初期化していました。でも、これは無駄。検出器は一度初期化すれば、使い回せる。platformInitializedフラグで、プラットフォームの初期化を1回だけ実行するようにしました。
検出処理はestimateFacesメソッドで実行します。
// 顔検出の実行
const faces = await detector.estimateFaces(video);
faces.forEach(face => {
// 顔の位置情報(box)
const { xMin, yMin, width, height } = face.box;
// 信頼度スコア(0.0〜1.0)
const score = face.score;
// 特徴点(目・鼻・口・耳など)
face.keypoints.forEach(keypoint => {
const { x, y } = keypoint;
// 描画処理...
});
});
検出結果には、顔の位置(box)、信頼度スコア(score)、特徴点(keypoints)が含まれます。特徴点には、目・鼻・口・耳の位置が含まれ、全部で6個の特徴点が検出されます。
最初は、TensorFlow.jsプラットフォームの初期化を毎回実行していました。でも、これは無駄。tf.ready()は非同期で実行され、初期化には時間がかかります。毎回実行すると、起動時間が長くなってしまいます。
初期化時間を測定すると、tf.ready()の呼び出しには約300ミリ秒から500ミリ秒かかります。カメラを切り替えるたびに、この時間だけ待つ必要があり、ユーザー体験が悪くなります。
platformInitializedフラグを追加して、プラットフォームの初期化を1回だけ実行するようにしました。これにより、カメラを切り替えても、追加の初期化時間がかからなくなります。
検出器の初期化も、同様に1回だけ実行します。detector変数に保持し、再利用することで、パフォーマンスを最適化しました。最初は、カメラを切り替えるたびに検出器を再初期化していましたが、これも無駄でした。
検出器の初期化時間は、約500ミリ秒から1秒程度かかります。モデルファイルの読み込みと、TensorFlow.jsでのモデルの準備に時間がかかります。毎回初期化すると、起動時間が大幅に増加します。
検出器を1回だけ初期化し、再利用することで、カメラの切り替えがスムーズになりました。検出器は、カメラが変わっても、同じモデルを使い続けることができるため、再初期化の必要はありません。
ℹ️
INFO
検出精度: 照明が明るく、正面を向いている場合に最も高い精度で検出できます。カメラから1〜2メートルの距離が最適です。modelType: 'short'は、2メートル以内の距離に最適化されているため、この範囲内で使用することを推奨します。
顔枠と特徴点の描画
検出された顔の周りに緑色の枠を描き、特徴点を赤い点で表示します。Canvas 2D APIを使用して、リアルタイムで描画しています。
// キャンバスのコンテキストを取得
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
// キャンバスをクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 顔の周りに枠を描画(グラデーション効果)
ctx.strokeStyle = '#00ff00'; // 緑色
ctx.lineWidth = 3;
ctx.shadowColor = '#00ff00';
ctx.shadowBlur = 10;
ctx.strokeRect(
face.box.xMin,
face.box.yMin,
face.box.width,
face.box.height
);
ctx.shadowBlur = 0;
// 特徴点(目・鼻・口・耳)を描画
ctx.fillStyle = '#ff0000'; // 赤色
ctx.shadowColor = '#ff0000';
ctx.shadowBlur = 5;
face.keypoints.forEach(keypoint => {
ctx.beginPath();
ctx.arc(keypoint.x, keypoint.y, 4, 0, 2 * Math.PI);
ctx.fill();
});
ctx.shadowBlur = 0;
// 信頼度スコアを表示(背景付き)
const scoreText = `信頼度: ${Math.round(face.score * 100)}%`;
ctx.font = 'bold 16px Arial';
const textMetrics = ctx.measureText(scoreText);
const textWidth = textMetrics.width;
// 背景の矩形
ctx.fillStyle = 'rgba(0, 255, 0, 0.8)';
ctx.fillRect(
face.box.xMin - 2,
face.box.yMin - 25,
textWidth + 10,
22
);
// テキスト
ctx.fillStyle = '#000';
ctx.fillText(
scoreText,
face.box.xMin + 3,
face.box.yMin - 8
);
最初は、シンプルな線だけで描画していました。でも、見づらい。背景が暗い場合や、顔の輪郭が複雑な場合、線が見えにくくなってしまいます。
グロー効果(発光効果)を追加して、視認性を向上させました。shadowBlur: 10で、緑色の枠が光るように見えます。背景の明るさに関係なく、顔枠がはっきりと見えるようになりました。
試行錯誤の過程では、shadowBlurの値を5、10、15、20と変化させて試しました。5では効果が薄く、20ではぼやけすぎます。10が、視認性と見た目のバランスが最も良い値でした。
特徴点の描画も、同様にグロー効果を追加しました。shadowBlur: 5で、目・鼻・口・耳の位置がはっきりと見えます。半径は4ピクセル。3ピクセルでは小さすぎて見えにくく、6ピクセルでは大きすぎて邪魔になります。4ピクセルが最適なサイズでした。
信頼度スコアの背景は、半透明の緑色(rgba(0, 255, 0, 0.8))。透明度を0.5から0.9まで試しましたが、0.8が最も読みやすい値でした。テキストは黒で、読みやすさを重視しました。
テキストの位置は、顔枠の上に配置します。face.box.yMin - 25で、顔枠の上25ピクセルの位置に表示します。25ピクセルは、テキストの高さ(22ピクセル)と余白(3ピクセル)を考慮した値です。
カメラ切り替え機能の実装
複数のカメラデバイスに対応し、カメラ選択パネルから切り替えられる機能を実装しました。navigator.mediaDevices.enumerateDevices()で、利用可能なカメラデバイスを取得します。
// 利用可能なカメラデバイスを取得
async function getCameras() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
availableCameras = devices.filter(device => device.kind === 'videoinput');
return availableCameras;
} catch (error) {
console.error('カメラデバイスの取得に失敗:', error);
return [];
}
}
カメラを切り替える際は、既存のストリームを完全に停止してから、新しいストリームを開始する必要があります。
// カメラストリームを停止
async function stopCurrentStream() {
if (currentStream) {
// すべてのトラックを停止
currentStream.getTracks().forEach(track => {
track.stop();
});
// ビデオ要素のsrcObjectをクリア
const video = document.getElementById('video');
if (video && video.srcObject) {
video.srcObject = null;
}
currentStream = null;
// ストリームが完全に解放されるまで待機
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// 指定したカメラIDでストリームを開始(リトライ機能付き)
async function startCamera(deviceId, retryCount = 0) {
const maxRetries = 3;
const retryDelay = 500; // ミリ秒
try {
// 既存のストリームを完全に停止
await stopCurrentStream();
// ストリーム解放のための追加待機時間
await new Promise(resolve => setTimeout(resolve, 200));
const constraints = {
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user' // デフォルトでフロントカメラを優先
}
};
// デバイスIDが指定されている場合は追加
if (deviceId) {
constraints.video.deviceId = { exact: deviceId };
delete constraints.video.facingMode; // exact指定の場合は競合を避ける
}
const stream = await navigator.mediaDevices.getUserMedia(constraints);
currentStream = stream;
const video = document.getElementById('video');
video.srcObject = null; // 既存のsrcObjectをクリア
await new Promise(resolve => setTimeout(resolve, 50));
video.srcObject = stream;
await video.play();
return true;
} catch (error) {
// エラーハンドリングとリトライ処理...
}
}
最初は、ストリームの停止と開始が適切に行われていませんでした。カメラを切り替えると、前のストリームが残ってしまい、リソースが無駄になる。
ストリームを完全に停止し、srcObjectをnullにクリアしてから、新しいストリームを設定するようにしました。待機時間も追加して、ストリームが完全に解放されるまで待つようにしています。
リトライ機能も追加しました。カメラが使用中の場合は、最大3回までリトライします。NotReadableError(デバイスが使用中)やNotFoundError(カメラが見つからない)など、エラータイプに応じた適切なメッセージを表示します。
リトライの待機時間は、段階的に増やしていきます。1回目のリトライは500ミリ秒、2回目は1000ミリ秒、3回目は1500ミリ秒待機します。これにより、カメラが解放されるまでの時間を与えます。
最初は、待機時間を固定で500ミリ秒にしていました。でも、すぐには解放されない場合があります。段階的に待機時間を増やすことで、成功率が向上しました。
エラーメッセージも、ユーザーにとって分かりやすくしました。「カメラが使用中です。他のアプリを閉じてから再試行してください」のように、具体的な対処方法を提示します。開発者向けのエラーコードだけでは、ユーザーは何をすれば良いか分かりません。
重要な発見
カメラの切り替えは、ストリームの完全な解放が重要です。stop()を呼ぶだけでは不十分で、srcObjectをnullにクリアし、さらに待機時間を設けることで、確実に切り替えができるようになりました。
リアルタイム検出ループの実装
requestAnimationFrameを使用して、ブラウザのリフレッシュレートに同期した検出処理を実装しています。検出ループは、非同期関数として実装し、エラーハンドリングも追加しています。
// 検出ループの開始
async function detectFaces() {
try {
const faces = await detector.estimateFaces(video);
// キャンバスをクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 検出人数を更新
updateFaceCount(faces.length);
if (faces.length > 0) {
// 顔の描画処理...
faces.forEach(face => {
// 顔枠と特徴点の描画
});
}
// 次のフレームをスケジュール
detectionLoopId = requestAnimationFrame(detectFaces);
} catch (error) {
console.error('顔検出中にエラーが発生:', error);
}
}
// 検出ループを開始
detectFaces();
最初は、検出ループの管理が適切ではありませんでした。カメラを切り替えると、古い検出ループが残ってしまい、複数のループが同時に実行されてしまう。
問題が発生したのは、カメラを切り替える際に、既存の検出ループをキャンセルしていなかったからです。新しいループが開始されても、古いループは実行され続けます。結果として、2つのループが同時に実行され、検出処理が2倍の負荷になってしまいます。
detectionLoopIdで検出ループのIDを保持し、カメラ切り替え時にcancelAnimationFrameで既存のループをキャンセルするようにしました。これで、常に1つの検出ループだけが実行されます。
カメラを切り替えるたびに、以下の処理を実行します:
1. 既存の検出ループをキャンセル(cancelAnimationFrame(detectionLoopId))
2. detectionLoopIdをnullにリセット
3. 新しいカメラで新しい検出ループを開始
これにより、メモリリークを防ぎ、パフォーマンスを維持できます。
エラーハンドリングも追加しました。検出中にエラーが発生しても、ループが止まらないようにしています。エラーはコンソールに出力され、処理は継続されます。try-catchでエラーを捕捉し、ループが停止しないようにしています。
最初は、エラーが発生すると検出ループが停止してしまいました。これでは、一時的なエラーでも、検出が再開できません。エラーハンドリングを追加することで、安定性が向上しました。
⚠️
WARNING
検出ループの停止: カメラを切り替える際は、必ず既存の検出ループをキャンセルしてください。複数のループが同時に実行されると、パフォーマンスが大幅に低下します。
カメラ選択UIの実装
カメラ選択パネルは、グラデーション背景とカード形式のUIで実装しています。各カメラはカードとして表示され、クリックで選択できます。
// カメラ選択UIを更新(カード形式)
async function updateCameraSelector() {
const cameras = await getCameras();
const cameraList = document.getElementById('cameraList');
cameraList.innerHTML = '';
cameras.forEach((camera, index) => {
const cameraCard = document.createElement('div');
cameraCard.className = 'camera-card';
cameraCard.dataset.deviceId = camera.deviceId;
cameraCard.innerHTML = `
<div class="camera-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
<circle cx="12" cy="13" r="4"></circle>
</svg>
</div>
<div class="camera-card-info">
<div class="camera-card-name">${camera.label || `カメラ ${index + 1}`}</div>
<div class="camera-card-id">ID: ${camera.deviceId.substring(0, 8)}...</div>
</div>
<div class="camera-card-check">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
`;
cameraCard.addEventListener('click', async () => {
// カメラ切り替え処理...
});
cameraList.appendChild(cameraCard);
});
}
カメラカードは、グラデーション背景のパネル内に表示されます。選択中のカメラは、activeクラスが追加され、緑色のチェックマークが表示されます。カードには、カメラの名前とデバイスIDの一部が表示され、視認性を向上させています。
最初は、シンプルなドロップダウンメニューで実装していました。でも、カメラの数が多い場合、分かりづらい。カード形式に変更して、視認性を向上させました。
カード形式のUIでは、以下の情報を表示します:
- カメラアイコン: SVGで描画されたカメラのアイコン
- カメラ名:
camera.labelから取得したカメラの名前(例: "Integrated Camera")
- デバイスID: デバイスIDの最初の8文字を表示(例: "ID: 12345678...")
カメラが選択されていない場合は、デフォルトのカメラ名(カメラ 1、カメラ 2など)を表示します。camera.labelが空の場合に備えて、フォールバック処理を追加しました。
カメラの切り替え処理では、エラー時には選択状態を元に戻すようにしています。切り替えに失敗した場合、前のカメラの選択状態を復元し、エラーメッセージを表示します。
具体的には、以下の処理を実行します:
1. クリックされたカメラを一時的にactiveクラスでマーク
2. カメラの切り替え処理を実行
3. 成功時:activeクラスを維持
4. エラー時:activeクラスを削除し、前のカメラの選択状態を復元
これにより、ユーザーが混乱しないよう、適切なフィードバックを提供します。
ホバーエフェクトも追加しました。マウスオーバーで、カードが少し浮き上がるアニメーションを表示します。transform: translateY(-2px)で、2ピクセル上に移動させ、視覚的なフィードバックを提供します。
パフォーマンスと最適化
MediaPipe Face Detectionのshortモデルは、軽量で高速な処理を実現します。デスクトップPCでは、約60 FPS(1秒間に60フレーム)で動作します。
処理速度は、以下の要因に依存します:
- カメラ解像度: 640×480が最適(処理速度と精度のバランス)
- 検出対象の数: 顔の数が増えると処理時間が増加
- デバイスの性能: GPU(グラフィック処理装置)があると高速化
最初は、1920×1080の高解像度で処理していました。でも、処理が重く、リアルタイム処理が難しい。640×480に下げると、処理速度が大幅に向上しました。
解像度の選択は、処理速度と精度のバランスが重要です。高解像度では、検出精度は向上しますが、処理時間が増加します。低解像度では、処理速度は向上しますが、検出精度が低下する可能性があります。
試行錯誤の過程では、以下の解像度を試しました:
- 1920×1080(Full HD): 処理時間約50ミリ秒、約20 FPS。精度は高いが、リアルタイム処理には重い。
- 1280×720(HD): 処理時間約25ミリ秒、約40 FPS。バランスが良い。
- 640×480(VGA): 処理時間約10ミリ秒、約60 FPS。処理速度が最速。
640×480が、処理速度と精度のバランスが最も良い解像度でした。写真展示での使用には、この解像度で十分です。
検出器の初期化は1回だけ実行し、再利用しています。毎回初期化すると、起動に時間がかかります。初期化済みの検出器を使い回すことで、パフォーマンスを最適化しました。
検出器の初期化時間は、約500ミリ秒から1秒程度かかります。毎回初期化すると、カメラを切り替えるたびに、この時間だけ待つ必要があります。1回だけ初期化することで、カメラの切り替えがスムーズになりました。
メモリ管理にも注意しています。カメラを切り替える際は、既存のストリームを完全に解放し、メモリリークを防いでいます。検出ループも、適切にキャンセルすることで、不要な処理を避けています。
メモリリークを防ぐため、以下の処理を実行します:
1. ストリームのすべてのトラックを停止(track.stop())
2. ビデオ要素のsrcObjectをnullにクリア
3. currentStream変数をnullにリセット
4. 検出ループをキャンセル(cancelAnimationFrame)
これにより、長期間の使用でも、メモリ使用量が増加しません。
✅
SUCCESS
パフォーマンスの目安: デスクトップPC(GPU搭載)で約60 FPS、モバイルデバイス(中性能)で約30 FPS程度の性能を実現できます。
実装のハマりポイント
実装中に遭遇した問題と、その解決方法をまとめます。
問題1: 検出ループの二重起動
カメラを切り替えると、古い検出ループが残ってしまい、複数のループが同時に実行される問題が発生しました。
最初は、検出ループの管理を適切に行っていませんでした。カメラを切り替える際に、既存の検出ループを停止せずに、新しいループを開始していました。結果として、2つの検出ループが同時に実行され、検出処理が2倍の負荷になってしまいます。
パフォーマンスを測定すると、検出時間が約20ミリ秒から約40ミリ秒に増加しました。CPU使用率も、約30%から約60%に増加。明らかに、2つのループが同時に実行されていることが分かりました。
解決方法: 検出ループのIDを保持し、切り替え時にcancelAnimationFrameで既存のループをキャンセルするようにしました。
// 既存の検出ループを停止
if (detectionLoopId) {
cancelAnimationFrame(detectionLoopId);
detectionLoopId = null;
}
// 新しい検出ループを開始
detectFaces();
問題2: カメラストリームの解放が不完全
カメラを切り替えると、前のストリームが残ってしまい、リソースが無駄になる問題が発生しました。
最初は、track.stop()を呼ぶだけで、ストリームが解放されると考えていました。でも、実際には不十分でした。ビデオ要素のsrcObjectにストリームが残っており、新しいストリームを設定しようとすると、エラーが発生することがありました。
エラーメッセージを見ると、NotReadableError(デバイスが使用中)が頻繁に発生していました。これは、前のストリームが完全に解放されていないためです。
解決方法: stop()を呼ぶだけでなく、srcObjectをnullにクリアし、さらに待機時間を設けることで、確実にストリームを解放するようにしました。
待機時間は、50ミリ秒、100ミリ秒、200ミリ秒と試しました。50ミリ秒では不十分で、エラーが発生することがありました。200ミリ秒では、待機時間が長すぎて、切り替えが遅く感じられます。100ミリ秒が、確実性と速度のバランスが最も良い値でした。
// すべてのトラックを停止
currentStream.getTracks().forEach(track => {
track.stop();
});
// ビデオ要素のsrcObjectをクリア
video.srcObject = null;
// ストリームが完全に解放されるまで待機
await new Promise(resolve => setTimeout(resolve, 100));
問題3: TensorFlow.jsプラットフォームの重複初期化
検出器を初期化するたびに、TensorFlow.jsプラットフォームも初期化され、無駄な処理が発生していました。
最初は、検出器を初期化するたびに、tf.ready()を呼び出していました。これにより、TensorFlow.jsプラットフォームが毎回初期化され、起動時間が長くなってしまいます。
起動時間を測定すると、毎回初期化する場合、約1.5秒かかります。1回だけ初期化する場合、初回のみ約1秒、2回目以降は約0.1秒で完了します。大幅な時間短縮です。
解決方法: platformInitializedフラグで、プラットフォームの初期化を1回だけ実行するようにしました。
// プラットフォームの初期化を1回だけ行う
if (!platformInitialized) {
await tf.ready();
platformInitialized = true;
}
実際に試した例
例1: デスクトップPCでの動作確認
デスクトップPC(GPU搭載)で動作確認しました。640×480の解像度で、約60 FPSの処理速度を実現。顔が1人の場合、検出時間は約10ミリ秒(0.01秒)程度。リアルタイム処理に十分な性能です。
例2: モバイルデバイスでの動作確認
モバイルデバイス(中性能)でも動作確認しました。処理速度は約30 FPS。デスクトップPCより遅いですが、実用範囲内です。照明条件が良い場合、検出精度も高い。
例3: 複数カメラでの切り替えテスト
ノートPCの内蔵カメラと、USB接続の外付けカメラで切り替えテストを実施。両方のカメラで問題なく動作し、切り替えもスムーズ。ストリームの解放処理が適切に動作していることを確認しました。
切り替え時間を測定すると、約150ミリ秒で完了しました。ストリームの停止(100ミリ秒)と新しいストリームの取得(約50ミリ秒)を合わせた時間です。ユーザーにとっては、ほぼ即座に切り替わる感覚です。
例4: 長時間動作テスト
1時間連続で動作させ、メモリ使用量を監視しました。メモリリークがなく、メモリ使用量は約120MBで安定していました。検出ループの適切な管理と、ストリームの完全な解放が、メモリリークを防いでいることが確認できました。
使ってみて
midori221は、シンプルな顔認識システムとして、実用的な機能を詰め込んでいます。MediaPipe Face Detectionの基本を押さえ、カメラ切り替えやリアルタイム検出まで、必要な機能が揃っています。
写真展示で使うなら、来場者の顔を検出して、展示の雰囲気を変えたり、統計を取ったりできます。年齢や感情の分析が必要になったら、midori222の記事を参考にしてください。
midori221を試す
実際のコードを見たい場合は、GitHubのリポジトリを参照してください。顔認識の基本実装から、カメラ切り替え、エラーハンドリングまで、すべてのコードが公開されています。
まとめ
midori221は、シンプルな顔認識システムとして、以下の機能を実現しました:
- MediaPipe Face Detectionの基本実装: TensorFlow.jsランタイムで動作する軽量な顔検出モデルを使用
- 顔の特徴点の描画: 目・鼻・口・耳の位置を検出して、視覚的に表示
- カメラ切り替え機能: 複数のカメラデバイスに対応し、スムーズに切り替え可能
- リアルタイム検出ループ:
requestAnimationFrameを使用した、滑らかな検出処理
- パフォーマンス最適化: 検出器の再利用、メモリ管理、ストリームの適切な解放
シンプルだから分かりやすく、シンプルだから速い。midori221は、顔認識の基本を押さえた、実用的なシステムです。
さらに深く学ぶには
最後まで読んでいただき、ありがとうございました。midori221が、あなたの顔認識プロジェクトの出発点になれば幸いです。