2024年11月20日水曜日

TensorFlow.jsによるビデオをソースとした物体検出を実装する

TensorFlow.jsを使ってビデオをソースとした物体検出を実装します。

実装はLearning TensorFlow.jsのChapter 6 Advanced Models and UIで紹介されていますが、実際に試してみるとモデルが無かったり、キャンバスへのバウンディング・ボックスの描画がビデオに重ねて表示されなかったりと、うまくいかない点がありました。結果として、かなり違う実装になっています。

作成したAPEXアプリケーションは以下のように動作します。ボタンDETECTをクリックすると物体認識を開始し、ボタンStopをクリックすると停止します。


APEXアプリケーションにはホーム・ページのみがあり、機能はすべてホーム・ページに実装しています。


静的コンテンツのリージョンを作成し、スマートフォンではリア・カメラ、PCではメイン・カメラをソースとした動画を表示するVIDEO要素をソースHTMLコードに記述します。
<div id="video-container">
<video id="mystery" playsinline autoplay muted>
    <source src="" type="video/mp4">
       Your browser does not support the video tag.
    </source>
</video>
</div>
VIDEO要素の親のDIV要素(ID: video-container)の子ノードとして、検出した物体を囲むバウンディング・ボックスとなるDIV要素を作成します。


VIDEO要素やバウンディング・ボックスの表示は、ページ・プロパティCSSインラインに定義します。
#mystery {
    position: absolute;
    top: 0;
    left: 10%;
    width: 80%;
}

.bounding-box {
    border-color: #0F0;
    position: absolute;
    box-sizing: border-box;
    border-width: 2px;
    border-style: solid;
}

.bounding-box-label {
    color: black;
    background-color: #0F0;
    position: absolute;
    top: 0;
    left: 0;
    font-size: 12px;
    padding: 1px;
}

TensorFlow.js自体やロードしたモデル、Webカメラの状態などを表示するページ・アイテムとしてP1_STATUSを作成します。タイプ表示のみです。


物体検知に関するほとんどの設定は、ページ・プロパティJavaScriptに行います。

ファイルURLに以下を記述します。TensorFlow.jsのバックエンドとしてWebGLとWebGPUをロードしています。iPhone(Safari)ではWebGL、Mac(Chrome)ではWebGPUのバックエンドを使用できました。この他にWasm、CPUがありますが、それらはロードしていません。
https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js
https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl/dist/tf-backend-webgl.min.js
https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgpu/dist/tf-backend-webgpu.min.js
https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd/dist/coco-ssd.min.js
物体検知のモデルとしてcoco-ssd.min.jsを読み込んでいます。jsDelivrの以下のページに説明されているように、ポスト処理としてNonMaxSuppressionの処理を含んでいるため、Learning TensorFlow.js(Chapter 6のIoUs and NMSのセクション)で説明されているコーディングは不要になっています。

TensorFlow.jsやモデルを準備するコードは、ページ・ロード時に実行に記述します。

ブラウザでWebGPUが利用可能であれば、WebGPUを使用します。それ以外はWebGLを使用します。WebGLが使えない場合はエラーになります。Coco-SSDのベースとして、軽量なlite_mobilenet_v2を選択しています。

/*
* ページ・ロード時にTensorFlow.jsが読み込まれる。
* バックエンドにはWebGPUかWebGLを使う。この他にwasm、cpuを使うこともできる。
*/
option = 'webgl';
if ('gpu' in navigator) {
option = 'webgpu';
};
/*
* TensorFlow.jsがReadyになったら、モデルを読み込む。
*/
tf.ready()
.then(() => {
tf.setBackend(option).then(() => {
apex.items.P1_STATUS.setValue("TensorFlow.js is Ready, Loading Model...");
cocoSsd.load( { base: 'lite_mobilenet_v2' } ).then( (m) => {
model = m; // modelはページ・グローバルな変数
const selectedBackend = tf.getBackend();
apex.items.P1_STATUS.setValue(`Model is Loaded - ${selectedBackend}`);
/*
* モデルが読み込めたら、Webcamのキャプチャを開始する。
*/
navigator.mediaDevices.getUserMedia({
audio: false,
video: {
facingMode: false
}
})
.then((stream) => {
const currentStatus = apex.items.P1_STATUS.getValue();
apex.items.P1_STATUS.setValue(`${currentStatus}, Webcam is ready`);
const videoElement = document.getElementById("mystery");
videoElement.srcObject = stream;
})
.catch((error) => {
apex.items.P1_STATUS.setValue('Error accessing webcam');
console.error('Error accessing webcam:', error);
});
});
});
})
.catch((error) => {
apex.items.P1_STATUS.setValue('Error loading model');
console.error('Error loading model:', error);
});


物体検出を行なうファンクションdetectAnimateは、ファンクションおよびグローバル変数の宣言に記述します。

var model;
var animationId;
function detectAnimate() {
/*
* video-containerのDIV要素はmysteryのvideo要素を含む。
* 検出したオブジェクトのバウンディング・ボックスは、video-containerの
* 子ノードになるDIV要素(classにbounding-boxを設定)として作成する。
*/
const videoContainer = document.getElementById("video-container");
const video = document.getElementById("mystery");
const vStyle = getComputedStyle(video);
// width, height, left は px で返されることを期待。
const wRatio = parseInt(vStyle.width,10) / video.videoWidth;
const hRatio = parseInt(vStyle.height,10) / video.videoHeight;
const mLeft = parseInt(vStyle.left, 10);
/*
* 物体検出の実行。
*/
model.detect(video).then( (predictions) => {
// すでに作成済みのバウンディング・ボックスのDIV要素を削除する。
const allBboxes = videoContainer.querySelectorAll('.bounding-box');
allBboxes.forEach( (child) => videoContainer.removeChild(child) );
// 検出した物体の数だけバウンディング・ボックスを作成。
predictions.forEach( (p) => {
// バウンディング・ボックスを描画する。
const boxElement = document.createElement("div");
boxElement.className = "bounding-box";
const nLeft = Math.ceil(p.bbox[0] * wRatio + mLeft);
const nTop = Math.ceil(p.bbox[1] * hRatio);
const nWidth = Math.ceil(p.bbox[2] * wRatio);
const nHeight = Math.ceil(p.bbox[3] * hRatio);
Object.assign(boxElement.style, {
left: `${nLeft}px`,
top: `${nTop}px`,
width: `${nWidth}px`,
height: `${nHeight}px`,
});
// 描画する矩形の左上にラベルをつける。
const labelElement = document.createElement('span');
const percentScore = Math.round(p.score * 100);
labelElement.textContent = `${p.class} ${percentScore}%`;
labelElement.className = 'bounding-box-label';
boxElement.appendChild(labelElement);
// バウンディング・ボックスを子ノードとして追加する。
videoContainer.appendChild(boxElement);
});
});
// アニメーションの開始。
animationId = requestAnimationFrame(detectAnimate);
};


ボタンDETECTをクリックした時に物体検出を開始します。動的アクションで以下のJavaScriptを実行します。

requestAnimationFrame(detectAnimate);


ボタンSTOPを押した時に、物体検出を停止します。以下のJavaScriptを実行します。

cancelAnimationFrame(animationId);


以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/tensorflow-js-video-object-detection.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。