開発備忘録:npm+TypeScript入門 Part 2

当サイトではアフィリエイト広告を利用しております。

開発

はじめに

前回、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はかなり大きくなるので、ファイル.gitignorepublic/を追加します。

また、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 まで。

タイトルとURLをコピーしました
inserted by FC2 system