JavaScriptでWebカメラの顔を追う方法は、2026年11月時点で2系統あります。TensorFlow.jsのモデルパッケージ @tensorflow-models/face-landmarks-detection を使う方法と、Googleの @mediapipe/tasks-vision の Face Landmarker を使う方法です。どちらもブラウザ内で推論が終わり、映像をサーバーへ送りません。この記事では前者の実装手順を、Node.js上で実際にモデルを動かして測った値を添えて説明し、後者へ移るべき条件も示します。TensorFlow.js本体の導入やテンソルの基礎はTensorFlow.jsとは?導入手順と動くコード、Node.js 24以降で落ちる原因と対処【4.22.0】で扱っています。
まとめ:TensorFlow.jsフェイストラッキングの要点
顔の位置だけなら face-detection(6点)、表情や視線まで扱うなら face-landmarks-detection を使います。後者は既定のままでは468点で、refineLandmarks: true を付けたときだけ虹彩の10点が加わって478点になります。maxFaces の既定は1です。今回のCPU測定では、入力解像度よりも顔検出を毎回実行するかどうかの影響が大きく、追跡モード(staticImageMode: false)は毎フレーム検出に比べて1回当たりの処理時間が約36%短くなりました。ランドマークモデルの入力は192×192に固定されており、画像を縦横半分にしても処理時間はほぼ変わりませんでした。カメラ映像の前処理やGPU転送を含む時間は、対象端末で別途測定します。
ただし tfjs系の顔モデルは2024年10月を最後に更新が止まり、runtime: 'mediapipe' が読み込む @mediapipe/face_mesh は2023年3月1日にサポートが終わった旧ソリューションです。既存のTensorFlow.jsアプリに顔追跡を足すならこの記事の手順で十分です。新規に作るなら、2026年10月27日に1.2.0が出た MediaPipe Tasks の Face Landmarker を先に検討します。
face-detectionとface-landmarks-detectionの選び方
TensorFlow.jsで使える顔モデルは、公式のtfjs-modelsリポジトリに2パッケージあります。版と公開日はnpmレジストリの値です。
| パッケージ | 最新版(公開日) | keypoints | 向く用途 |
|---|---|---|---|
| @tensorflow-models/face-detection | 1.0.3(2024-10-10) | 6 | 顔の枠・人数・モザイク |
| @tensorflow-models/face-landmarks-detection | 1.0.6(2024-10-10) | 468/478 | 表情・顔の向き・視線・ARフィルター |
| @tensorflow-models/facemesh | 0.0.5(2020-10-12) | 468 | 非推奨(アーカイブ済み) |
face-detectionの6点は、実測で rightEye・leftEye・noseTip・mouthCenter・rightEarTragion・leftEarTragion という名前で返りました。顔がどこにあるかは分かっても、まばたきや口の開き具合は取れません。表情に反応するエフェクトや視線の推定には face-landmarks-detection が要ります。
点数については資料によって468点と478点が混在しています。公式READMEは「MediaPipeFaceMesh returns 478 keypoints」と書いていますが、これは虹彩まで出したときの数です。tfjsランタイムで同じ顔写真を推論したところ、refineLandmarks が既定の false のときは468点、true のときは478点でした。tfjs-modelsのtfjsランタイムのREADMEも refineLandmarks を「Defaults to false」とし、true にすると目と唇の周りの座標を補正したうえで虹彩の点を追加すると説明しています。視線を扱うなら true が前提です。
tfjs 4.22.0系のパッケージ導入とランタイム選択
npmとCDNでの読み込み
アプリに組み込むならnpmです。face-landmarks-detection 1.0.6 は tfjs-core・tfjs-converter・tfjs-backend-webgl の 4.13.0 以上と、face-detection 1.0.3、@mediapipe/face_mesh 0.4系をpeerDependenciesに持ちます。まとめて入れるなら @tensorflow/tfjs 4.22.0 が手軽です。
npm install @tensorflow/[email protected] \
@tensorflow-models/[email protected] \
@tensorflow-models/[email protected] \
@mediapipe/[email protected]
jsDelivrから <script> で読む場合も版は固定します。tfjs本体(4.22.0)とモデルパッケージ(1.0.x)は版の系列が別です。
runtime ‘mediapipe’ と ‘tfjs’ の違い
createDetector() の runtime には2つの値があります。'mediapipe' は solutionPath に置いた @mediapipe/face_mesh のWebAssemblyとモデルを呼ぶ経路、'tfjs' はTensorFlow.jsのバックエンド(WebGLなど)でモデルを動かす経路です。
| 観点 | runtime: ‘mediapipe’ | runtime: ‘tfjs’ |
|---|---|---|
| 追加で読む資産 | @mediapipe/face_mesh のwasmとモデル | なし(モデルはURLから取得) |
| 依存の保守状況 | 0.4.1633559619(2021-10-06)で停止 | tfjs 4.22.0(2024-10-21)で停止 |
| モデルの取得先 | solutionPath(CDNか自ホスト) | 既定はtfhub.dev(Kaggleへ転送) |
| Node.jsでの実行 | 不可 | 可(この記事の実測はこちら) |
判断を分けるのは保守状況です。@mediapipe/face_mesh はGoogleが「Legacy Solutions」と呼ぶ旧世代の配布物で、MediaPipeの公式ガイドは「We have ended support for the MediaPipe Legacy Solutions listed here as of March 1, 2023.」と明記し、旧ソリューション一覧でFace Meshの移行先に Face landmark detection(Face Landmarker)を挙げています。公式サポートが終了した資産に新しく依存するより、tfjsで統一するか、次章以降で扱うMediaPipe Tasksへ移る方が筋が通ります。
tfjsランタイムの既定のモデルURLは https://tfhub.dev/mediapipe/tfjs-model/... です。2026年11月1日に確認したところ、GETはKaggleのストレージへ2回転送されて取得でき、HEADは404を返しました。社内ネットワークで外部への転送が通らない場合や、配信元を自社に寄せたい場合は、モデルを自ホストして detectorModelUrl と landmarkModelUrl で指定します。
import '@tensorflow/tfjs-backend-webgl';
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection';
const detector = await faceLandmarksDetection.createDetector(
faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh,
{
runtime: 'tfjs',
refineLandmarks: true, // 虹彩10点を加えて478点にする
maxFaces: 1, // 既定も1。複数人を追うときだけ増やす
}
);
Webカメラ映像からの478点の取得と描画
getUserMediaの利用条件と安全なコンテキスト
navigator.mediaDevices.getUserMedia() はMDNの解説のとおり、HTTPSか localhost などの安全なコンテキストでしか使えません。開発機のIPアドレス(http://192.168.x.x)を別の端末で開くと navigator.mediaDevices が undefined になり、カメラの許可ダイアログも出ません。スマートフォン実機で確かめる段階で詰まりやすいので、検証用の証明書かポートフォワードを先に用意します。カメラやマイクの取得そのものはWebRTCとは?P2Pの仕組み・ICE/STUN/TURNからSFU選定・実装・運用判断まで実装者向けに解説でも扱っています。
const video = document.querySelector('video') ?? document.createElement('video');
video.muted = true;
video.playsInline = true;
if (!video.isConnected) document.body.appendChild(video);
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480, facingMode: 'user' },
});
video.srcObject = stream;
await video.play();
estimateFacesの戻り値とkeypointsの読み方
estimateFaces() にはvideo要素をそのまま渡せます。戻り値は顔ごとの配列で、各要素が box(xMin・yMin・xMax・yMax・width・height)と keypoints を持ちます。keypointsの x と y は入力画像のピクセル座標です。z は頭部中心を原点とする相対的な深度で、小さいほどカメラに近く、おおむねxと同じ尺度です。メートル単位の実距離ではありません。
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D context unavailable');
async function renderLoop() {
const faces = await detector.estimateFaces(video, { flipHorizontal: false });
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const face of faces) {
for (const kp of face.keypoints) {
ctx.fillRect(kp.x, kp.y, 2, 2);
}
}
requestAnimationFrame(renderLoop);
}
renderLoop();
name は全点には付きません。実測では468点のうち125点、478点のうち133点にだけ付き、値は lips・leftEye・rightEye・leftEyebrow・rightEyebrow・faceOval、虹彩を出したときは leftIris・rightIris の8種類でした。点ごとの固有名ではなく部位の名前です。「上唇の内側」のような特定の点を追うなら、name ではなくFaceMeshのインデックス番号で参照します。
ループは setInterval ではなく requestAnimationFrame で回し、推論が終わってから次のフレームを要求します。推論より短い間隔で呼ぶと処理待ちが積み上がり、遅延が伸び続けます。前面カメラの映像を鏡像で表示するなら、描画側で反転するか flipHorizontal: true を渡して座標を合わせます。
追跡(トラッキング)の仕組みと速度の実測
tfjsランタイムのstaticImageModeと顔検出の省略条件
face-landmarks-detectionは、最初のフレームで顔検出モデルを走らせて顔の位置を決め、以降は前フレームのランドマークから次の位置を予測して追跡します。追跡中の顔数が maxFaces に達している間は顔検出を省略しますが、人数が不足すると検出を再実行します。この切り替えが estimateFaces() の第2引数の staticImageMode で、既定は false(追跡する)です。true にすると毎回検出から始めます。この切り替えは runtime: 'tfjs' の仕様で、runtime: 'mediapipe' の実装では staticImageMode は使われません。
| 条件(cpuバックエンド・820×1024) | 1フレームの平均 |
|---|---|
| 468点・追跡(staticImageMode: false) | 931〜948ms |
| 478点・追跡(refineLandmarks: true) | 1,237ms |
| 468点・毎フレーム検出(staticImageMode: true) | 1,476ms |
| 468点・毎フレーム検出・入力を410×512に縮小 | 1,471ms |
| face-detection(6点・short) | 552ms |
測定はIntel Core i9-9880H、Node.js 26.5.0、純JavaScriptのcpuバックエンドで、820×1024の同じ顔写真をテンソル化し、各条件でウォームアップを1回行った後、10回反復した平均を取りました。カメラの取得時間と画像のデコード時間は含みません。絶対値はWebGLより大幅に遅く、条件どうしの比率を見るための値です。動画で使うなら staticImageMode は既定の false のままにします。静止画を1枚ずつ別人として処理するときだけ true にします。
入力解像度を下げても速くならない理由
表の毎フレーム検出の2条件を比べると、入力を縦横半分にしても推論時間は1,476msから1,471msでほぼ変わりませんでした。モデルの入力サイズが固定されているためです。tfhub.devから取得したmodel.jsonの入力は、ランドマークモデルが192×192、顔検出モデル(short)が128×128で、どの解像度の映像も切り出してこの大きさに縮めてから推論します。解像度を下げて効くのは縮小処理とGPUへの転送の分だけです。重いときに先に見るのは次の2点です。
maxFacesが必要数より大きくないか。READMEは、期待する最大人数に合わせないと足りない顔を探し続けて遅くなると書いています。- バックエンドがWebGLに乗っているか(次項)。
バックエンドの確認(WebGL・WebGPU・CPUへの落ち込み)
runtime: 'tfjs' では、TensorFlow.jsが使えるバックエンドを優先度順に選びます。WebGLが使えず、cpuバックエンドが登録されている構成ではcpuに切り替わることがあり、上の表のとおり1フレーム1秒前後かかって実用になりません。起動時に一度確かめます。
import * as tf from '@tensorflow/tfjs-core';
await tf.ready();
console.log(tf.getBackend()); // 'webgl' なら正常。'cpu' ならGPUが使えていない
WebGPUを試すなら @tensorflow/tfjs-backend-webgpu 4.22.0 を読み込み、await tf.setBackend('webgpu') を明示します。WebGLが有効かどうかの確かめ方はWebGLとは?仕組み・対応ブラウザの確認・動くコードとUnity WebGLビルドの手順【2026年】にまとめています。
旧facemeshのload()から現行APIへの移行
2020〜2021年の記事やサンプルは @tensorflow-models/facemesh か、face-landmarks-detection 0.x の load() を前提にしています。facemeshはnpm上で「archived in favor of @tensorflow-models/face-landmarks-detection」と非推奨になっています。書き換えるのは次の4箇所です。
| 旧API | 現行API(1.0.6) |
|---|---|
| facemesh.load() / faceLandmarksDetection.load() | createDetector(SupportedModels.MediaPipeFaceMesh, config) |
| model.estimateFaces({ input: video }) | detector.estimateFaces(video, estimationConfig) |
| prediction.scaledMesh([x, y, z] の配列) | face.keypoints({x, y, z, name} の配列) |
| prediction.boundingBox.topLeft | face.box.xMin / face.box.yMin |
3行目では座標の参照方法が変わります。旧 scaledMesh は数値の配列だったので point[0]、point[1] で読むコードが残っています。現行の keypoints はオブジェクトなので同じ添字で読むと undefined になり、描画が無反応になるだけでエラーは出ません。kp.x、kp.y に直します。インデックス番号そのものは同じFaceMeshのトポロジに従うので、部位を指す番号の定数はそのまま使えます。
MediaPipe Tasks(Face Landmarker)との違いと移行の判断
Face Mesh の後継として Google が案内しているのが、@mediapipe/tasks-vision の Face Landmarker です。npmの最新は1.2.0(2026-10-27公開)で、tfjs系と違って更新が続いています。同じ478点を返しますが、APIと出力の形が違います。以下の例は npm install @mediapipe/[email protected] で版を固定し、前章で初期化した video・canvas・ctx をそのまま使います。TensorFlow.js側の描画ループは止めてから動かしてください。
| 観点 | face-landmarks-detection 1.0.6 | tasks-vision 1.2.0 Face Landmarker |
|---|---|---|
| 最終更新 | 2024-10-10 | 2026-10-27 |
| 点数 | 468(既定)/478 | 478 |
| 座標 | ピクセル座標 | 0〜1の正規化座標 |
| 表情の係数 | なし | blendshapes 52種 |
| 変換行列 | なし | facialTransformationMatrixes |
| 推論の呼び方 | await estimateFaces() | detectForVideo()(同期) |
| モデル | tfhub.dev(Kaggle) | face_landmarker.task(3,758,596バイト) |
| 利用データの送信 | 記載なし | 性能と利用状況のメトリクスをGoogleへ送信 |
表情に反応するアバターやARエフェクトを作るなら、blendshapesがあるだけでFace Landmarkerを選ぶ理由になります。Face LandmarkerのWeb向けガイドによると、outputFaceBlendshapes: true で顔ごとに52個の係数が返ります。名前に Left/Right が付く係数は被写体から見た左右なので、鏡像の映像を渡すと左右が入れ替わります。numFaces の既定は1で、平滑化は1のときだけ効きます。
import { FilesetResolver, FaceLandmarker } from '@mediapipe/tasks-vision';
const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm'
);
const landmarker = await FaceLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath:
'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
delegate: 'GPU',
},
runningMode: 'VIDEO',
numFaces: 1,
outputFaceBlendshapes: true,
});
let lastVideoTime = -1;
function loop() {
if (video.currentTime !== lastVideoTime) {
lastVideoTime = video.currentTime;
const result = landmarker.detectForVideo(video, performance.now());
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of result.faceLandmarks[0] ?? []) {
// 正規化座標なのでキャンバスの幅と高さを掛ける
ctx.fillRect(p.x * canvas.width, p.y * canvas.height, 2, 2);
}
const scores = result.faceBlendshapes[0]?.categories ?? [];
// scores[i].categoryName と scores[i].score で表情の強さを読む
}
requestAnimationFrame(loop);
}
loop();
移行で手が止まりやすいのは3点です。座標が正規化されているので、tfjs時代の描画コードはキャンバスの幅と高さを掛ける形に直します。runningMode は型定義上 'IMAGE' と 'VIDEO' の2値で、動画では detectForVideo() に増え続けるタイムスタンプを渡します。detect() と detectForVideo() は同期でUIスレッドを止めるため、ガイドはカメラ入力ではWeb Workerで動かすことを勧めています。
プライバシーの説明も書き換えが要ります。tasks-visionのREADMEのPrivacy Noticeは、画像や映像は端末内で処理しGoogleへ送らないとする一方で、「MediaPipe Tasks APIs send metrics about the performance and utilization of the APIs in your app to Google.」と書き、このメトリクスについて利用者から同意を得る責任は導入側にあるとしています。「映像は外部に出ません」とだけ書いた利用規約のまま移行すると、説明が足りなくなります。MediaPipe全体の位置づけと商用利用の条件はMediaPipe(メディアパイプ)とは?Google製AIライブラリの機能・読み方・商用利用【1.1.0対応】、Python版での顔検出はMediaPipe(メディアパイプ)の使い方|Python版1.1.0で手・姿勢・顔を検出する手順とmp.solutions移行で扱っています。
TensorFlow.jsの顔追跡を採用すべきでない場面
次のどれかに当てはまるなら、face-landmarks-detectionを選びません。
- 新規開発で、TensorFlow.jsを他の用途に使っていない:顔追跡のためだけに更新の止まったtfjs 4.22.0とレガシーの
@mediapipe/face_meshを抱える理由がありません。Face Landmarkerにします。 - 表情の強さを数値で使う:face-landmarks-detectionにはblendshapesが無く、口の開きや眉の上がり具合は点の距離から自分で計算することになります。
- 本人確認・なりすまし防止に使う:FaceMeshが返すのは顔の形の点群です。印刷した顔写真やディスプレイに映した顔からも478点は取れ、実物の人物かどうかは区別できません。生体検知が要件なら専用のサービスを使います。偽の顔や声を使う攻撃の手口はディープフェイク詐欺とは?BEC・音声クローンの手口と実装者向けの検知・防御を解説にまとめています。
逆に、すでにTensorFlow.jsで画像分類や姿勢推定を動かしているアプリに顔の位置や向きを足すだけなら、バックエンドとメモリ管理を共有できるface-landmarks-detectionの方が導入は軽く済みます。映像をサーバーへ送らない構成の考え方はエッジAIとは?クラウドAIとの違い・仕組み・実装の判断基準を解説と同じです。OpenCV.jsで前処理をしてから顔を検出したい場合はOpenCV.jsの使い方|JavaScriptで画像処理する導入(CDN・npm)とWebカメラ実装【5.0/4.14対応】が参考になります。自社の現場写真や検査画像に合わせたモデルが必要な案件は、画像認識AIモデル構築としてご相談いただけます。
よくある質問(FAQ)
TensorFlow.jsのフェイストラッキングは無料で商用利用できますか?
できます。@tensorflow/tfjs、face-detection、face-landmarks-detectionはいずれもApache License 2.0で、利用料はかかりません。端末内推論のため推論用サーバーは不要ですが、アプリやモデルの配信費用は構成によって発生します。再配布時はライセンス全文の同梱、該当する著作権等の表示の保持、変更ファイルへの変更表示、NOTICEがある場合の必要な帰属表示など、Apache License 2.0の条件を満たしてください。
FaceMeshの特徴点は468点と478点のどちらですか?
設定によります。face-landmarks-detection 1.0.6は既定(refineLandmarks: false)で468点、true で虹彩の10点を加えた478点を返します。MediaPipe TasksのFace Landmarkerは常に478点です。
スマートフォンのブラウザでも動きますか?
iOSのSafariとAndroidのChromeはどちらもgetUserMediaとWebGLに対応しているので、動かせます。条件はHTTPSで配信することです。getUserMedia は安全なコンテキストでしか使えないため、開発機のIPアドレスを直接開くとカメラの許可ダイアログが出ません。
旧facemeshのサンプルコードが動かないのはなぜですか?
facemesh.load() と prediction.scaledMesh が現行の1.0.6に無いためです。createDetector() と face.keypoints に置き換えます。keypointsはオブジェクトの配列なので、point[0] のような添字は kp.x に直します。
TensorFlow.jsで顔の動きを取ると重いのはなぜですか?
原因の一つはcpuバックエンドでの実行なので、まず tf.getBackend() で確かめます。WebGLでも遅いなら maxFaces を必要な人数にし、staticImageMode を false のままにします。今回の静止画・CPU測定では、解像度を下げても処理時間はほぼ変わりませんでした。ブラウザでは前処理とGPU転送を含む時間を測って判断します。