TF.js 実践開発レシピ

TensorFlow.jsでWebカメラ画像を使ったリアルタイムオブジェクト検出を実装する

Tags: TensorFlow.js, 画像認識, オブジェクト検出, Webカメラ, JavaScript

はじめに

本記事では、Webブラウザ上でユーザーのWebカメラ映像からリアルタイムに物体(オブジェクト)を検出する方法について解説します。Pythonによる機械学習開発に慣れている方にとって、Webブラウザ環境での画像認識処理は、非同期処理やWeb APIの利用など、異なる考慮事項が必要となる場合があります。TensorFlow.jsを用いることで、使い慣れた機械学習の概念をJavaScriptの世界に持ち込み、ブラウザの強力な描画能力(Canvas APIなど)やデバイス連携(Webカメラ)を活用したアプリケーション開発が可能になります。

PythonでOpenCVやその他のライブラリを使って画像や映像から物体を検出した経験をお持ちの方も多いかと思います。ブラウザ上でのリアルタイム処理では、映像フレームを連続的に取得し、各フレームに対して検出処理を実行し、結果を描画するという一連の流れを効率的に行う必要があります。

ここでは、TensorFlow.jsと、オブジェクト検出タスクのために事前に訓練されたモデルであるCOCO-SSDモデルを利用して、Webカメラ映像に対するリアルタイムオブジェクト検出を実装する具体的なコード例とその解説を行います。

使用する技術要素

環境構築の準備

簡単なHTMLファイルとJavaScriptファイルを作成し、TensorFlow.jsおよびCOCO-SSDモデルのライブラリをロードします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TF.js Realtime Object Detection</title>
    <style>
        #container {
            position: relative;
            width: 640px; /* 適切なサイズに調整 */
            height: 480px; /* 適切なサイズに調整 */
            margin: auto;
        }
        video, canvas {
            position: absolute;
            top: 0;
            left: 0;
        }
        video {
             /* 映像を非表示にし、Canvasに描画 */
            display: none;
        }
    </style>
</head>
<body>
    <h1>Webカメラを使ったリアルタイムオブジェクト検出</h1>

    <div id="container">
        <video id="webcam" autoplay playsinline muted></video>
        <canvas id="output"></canvas>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>
    <script src="script.js"></script>
</body>
</html>

このHTMLでは、映像ストリームを受け取るための非表示の <video> 要素と、映像および検出結果を描画するための <canvas> 要素を配置しています。これらを重ねて表示するために、親要素 (#container) を基準に絶対配置しています。TensorFlow.jsライブラリ本体とCOCO-SSDモデルライブラリはCDN経由でロードしています。実際の処理は script.js に記述します。

JavaScriptによる実装

script.js に、Webカメラの起動、モデルのロード、リアルタイム検出処理、結果描画の実装を行います。

const video = document.getElementById('webcam');
const canvas = document.getElementById('output');
const ctx = canvas.getContext('2d');

let model = undefined;

// モデルをロードし、Webカメラを起動する関数
async function setupWebcamAndModel() {
    // COCO-SSDモデルをロード
    // 'lite_mobilenet_v2'は軽量なモデルオプション
    model = await cocoSsd.load();
    console.log('Model loaded.');

    // Webカメラのストリームを取得
    try {
        const stream = await navigator.mediaDevices.getUserMedia({ 'video': true });
        video.srcObject = stream;
        return new Promise((resolve) => {
            video.onloadedmetadata = () => {
                resolve();
            };
        });
    } catch (err) {
        console.error('Failed to get webcam access:', err);
        alert('Webカメラへのアクセスが拒否されました。許可してください。');
    }
}

// 映像フレームごとに検出処理を行う関数
async function detectFrame() {
    if (!model) {
        console.error('Model not loaded.');
        return;
    }

    // Canvasのサイズを映像に合わせる
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;

    // Canvasに映像を描画
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    // オブジェクト検出を実行
    // video要素を直接推論関数に渡すことができます
    const predictions = await model.detect(video);

    // 検出されたオブジェクトごとに結果を描画
    predictions.forEach(prediction => {
        // バウンディングボックスの描画
        ctx.beginPath();
        ctx.rect(...prediction.bbox); // [x, y, width, height] の配列
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#00FFFF'; // シアン色
        ctx.fillStyle = '#00FFFF';
        ctx.stroke();

        // クラス名と信頼度スコアの描画
        const text = `${prediction.class} (${Math.round(prediction.score * 100)}%)`;
        ctx.font = '18px Arial';
        // テキストの背景
        const textWidth = ctx.measureText(text).width;
        const textHeight = parseInt(ctx.font, 10); // フォントサイズから高さを推定
        ctx.fillRect(prediction.bbox[0], prediction.bbox[1] > textHeight ? prediction.bbox[1] - textHeight : prediction.bbox[1], textWidth + 4, textHeight);
        // テキスト本体
        ctx.fillStyle = '#000000'; // 黒色
        ctx.fillText(text, prediction.bbox[0] + 2, prediction.bbox[1] > textHeight ? prediction.bbox[1] - 2 : prediction.bbox[1] + textHeight - 2);
    });

    // 次のフレームで再度検出処理を要求
    // requestAnimationFrameを使うことでブラウザのリフレッシュレートに合わせた描画が可能
    requestAnimationFrame(detectFrame);
}

// アプリケーションの起動
async function run() {
    await setupWebcamAndModel();
    // 映像のメタデータがロードされたら検出ループを開始
    video.addEventListener('loadedmetadata', detectFrame);
}

run();

コード解説

モデルのロードとWebカメラ起動

setupWebcamAndModel 関数内で、非同期処理としてモデルのロードとWebカメラへのアクセスを行っています。

model = await cocoSsd.load();

cocoSsd.load() はCOCO-SSDモデルを非同期でロードする関数です。await キーワードでモデルのロード完了を待ってから、model 変数に格納します。モデルのロードにはネットワークの状態によって時間がかかる場合があります。

const stream = await navigator.mediaDevices.getUserMedia({ 'video': true });
video.srcObject = stream;

navigator.mediaDevices.getUserMedia({ 'video': true }) は、ユーザーにWebカメラの使用許可を求め、許可されればカメラ映像の MediaStream オブジェクトを返します。これも非同期処理です。取得したストリームは <video> 要素の srcObject プロパティに設定することで、映像が再生されるようになります。音声が必要ない場合は 'audio': false も指定できます。権限エラーが発生した場合は catch ブロックでハンドリングします。

リアルタイム検出ループ

detectFrame 関数が検出処理の本体であり、これを requestAnimationFrame でループさせることでリアルタイム処理を実現します。

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

検出対象となる映像フレームを取得するために、まず <canvas> のサイズを <video> の現在の映像サイズに合わせます。そして、ctx.drawImage() を使用して <video> 要素の現在のフレームを <canvas> に描画します。これにより、Canvasのコンテキスト (ctx) を通じてピクセルデータにアクセス可能になります。

const predictions = await model.detect(video);

COCO-SSDモデルの detect() メソッドを呼び出し、オブジェクト検出を実行します。非常に便利な点として、detect() メソッドは <video> 要素や <canvas> 要素、<img> 要素、またはImageDataオブジェクトなどを直接入力として受け取ることができます。モデルは入力された画像のピクセルデータに対して推論を行い、検出されたオブジェクトのリストを非同期で返します。

Pythonで画像処理ライブラリ(例: OpenCV)を使用する場合、通常は画像データをNumPy配列として扱い、モデルの入力テンソル形式に合わせて前処理(リサイズ、正規化など)を行う必要があります。TensorFlow.jsの多くのモデル(特に @tensorflow-models 以下で提供される高レベルAPIを持つモデル)では、こうした一般的な画像入力要素を直接扱えるため、ブラウザでの画像処理が容易になっています。

predictions はオブジェクトの配列です。各オブジェクトは検出された1つのオブジェクトを表し、以下のプロパティを持ちます。 * bbox: [x, y, width, height] 形式のバウンディングボックスの配列。座標は画像(Canvas)の左上を原点とします。 * class: 検出されたオブジェクトのクラス名(例: 'person', 'car', 'cat')。 * score: 検出の信頼度スコア(0から1の間の数値)。

predictions.forEach(prediction => {
    // ... 描画コード ...
});

検出されたオブジェクトのリスト predictions をループ処理し、各オブジェクトに対してバウンディングボックスとクラス名、信頼度スコアをCanvas上に描画します。Canvas APIの rect, stroke, fillText, fillRect などのメソッドを使用しています。描画座標は prediction.bbox を利用します。

requestAnimationFrame(detectFrame);

すべての描画が完了した後、requestAnimationFrame を呼び出します。これはブラウザが次の描画更新を行う直前に指定したコールバック関数を実行するAPIです。これにより、ブラウザの描画サイクルに合わせた効率的なループ処理が実現できます。単純な setIntervalsetTimeout よりも滑らかなアニメーションやリアルタイム処理に適しています。detectFrame 関数が非同期 (async) であるため、await model.detect(video) の処理中に次のフレームの描画要求がなされることになりますが、requestAnimationFrame の性質上、前のフレームの処理が終わっていなくても次のフレームの処理がキューイングされ、描画更新のタイミングで実行されることになります。これにより、モデルの推論に時間がかかってもアプリケーション全体が固まることなく、可能な限り高いフレームレートで処理を継続しようとします。

アプリケーションの起動

run();

最後に run() 関数を呼び出すことで、アプリケーション全体が起動します。モデルとWebカメラのセットアップが完了した後、loadedmetadata イベントをリッスンし、映像の準備ができ次第 detectFrame ループを開始します。

実践的な考慮事項

パフォーマンス

リアルタイム処理においてパフォーマンスは非常に重要です。 * モデルの選択: COCO-SSDモデルにはいくつかバリアントがあり、cocoSsd.load({ modelUrl: ..., base: 'mobilenet_v1' | 'mobilenet_v2' | 'lite_mobilenet_v2' }) のように base オプションで異なるベースモデルを選択できます。lite_mobilenet_v2 は最も軽量で高速ですが、精度は他のモデルに劣る場合があります。使用するデバイスや要求される精度に応じて適切なモデルを選択してください。 * 入力解像度: モデルへの入力サイズはパフォーマンスに大きく影響します。Webカメラの解像度を下げることで、処理するピクセル数を減らし、推論速度を向上させることができます。getUserMedia のオプションで { video: { width: 320, height: 240 } } のように解像度を指定可能です。 * TensorFlow.js バックエンド: TensorFlow.jsはWebGL, WebAssembly (WASM), CPUなどのバックエンドをサポートしています。通常、GPUを利用するWebGLが最も高速ですが、デバイスによってはWASMやCPUの方が安定している場合もあります。デフォルトで最適なバックエンドが選択されますが、tf.setBackend('wasm') のように手動で設定することも可能です。最新のブラウザではWebGPUバックエンドも利用可能になりつつあり、さらなるパフォーマンス向上が期待されます。 * モデルの量子化: モデルを量子化(多くの場合、浮動小数点数から整数に変換)することで、モデルサイズを削減し、推論速度を向上させることができます。COCO-SSDモデルの軽量版は既に量子化されていますが、カスタムモデルを利用する際は、モデル変換時に量子化を検討してください。PythonのTensorFlow Model Optimization Toolkitなどを使用できます。

Python環境ではGPUの選択肢が多く、高性能なハードウェアを利用しやすいですが、ブラウザ環境ではユーザーのデバイス性能に依存します。そのため、軽量なモデルを選択したり、入力サイズを調整したりといった工夫がより重要になります。

エラーハンドリングとユーザー体験

モデルの入力サイズとアスペクト比

COCO-SSDモデルは通常、特定の入力サイズ(例: 300x300ピクセル)で訓練されています。model.detect()<video> 要素を渡した場合、内部で適切な前処理(リサイズやパディング)が行われます。しかし、入力される映像のアスペクト比とモデルの訓練時アスペクト比が大きく異なる場合、検出精度に影響が出る可能性があります。必要に応じて、Canvas上で映像をクロップしたり、パディングを追加したりといった前処理をカスタマイズすることも検討できます。

まとめ

本記事では、TensorFlow.jsとCOCO-SSDモデル、そしてWeb標準APIを活用して、ブラウザ上でWebカメラ映像に対するリアルタイムオブジェクト検出機能を実装する方法を解説しました。Pythonによる機械学習開発の経験を持つ方にとって、Webブラウザ環境でのリアルタイム処理におけるデータの扱い方(映像フレームの取得とCanvasへの描画)や非同期処理の記述方法について理解を深める一助となれば幸いです。

ご紹介したコードは基本的な実装例です。これを基に、検出対象のカスタマイズ(特定のオブジェクトのみを検出結果として表示するなど)、検出閾値(スコア)の調整、検出結果に基づいたインタラクションの実装など、様々な応用が可能となります。

ブラウザという実行環境の特性を理解し、適切なモデル選択やパフォーマンス最適化を行うことで、ユーザーのデバイス上で直接動作する強力な画像認識アプリケーションを構築できることが、TensorFlow.jsの大きな魅力の一つです。