実装はLearning TensorFlow.jsのChapter 6 Advanced Models and UIで紹介されていますが、実際に試してみるとモデルが無かったり、キャンバスへのバウンディング・ボックスの描画がビデオに重ねて表示されなかったりと、うまくいかない点がありました。結果として、かなり違う実装になっています。
作成したAPEXアプリケーションは以下のように動作します。ボタンDETECTをクリックすると物体認識を開始し、ボタンStopをクリックすると停止します。
作成した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;
}
物体検知に関するほとんどの設定は、ページ・プロパティの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を選択しています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* ページ・ロード時に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は、ファンクションおよびグローバル変数の宣言に記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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のアプリケーション作成の参考になれば幸いです。
完