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を選択しています。



物体検出を行なうファンクション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のアプリケーション作成の参考になれば幸いです。

2024年11月15日金曜日

Transformers.jsを使ってブラウザでWhisperによる文字起こしを実行する

Hugging FaceのTransformers.jsのページに掲載されているExamplesのひとつに、Whisperがありました。デモのサイトおよびコードもGitHubから公開されているのですが、Reactベースです。この例をそのままOracle APEXのアプリーションに組み込むというのは難しいのですが、Transformers.jsを使えばブラウザでWhisperを実行できる、というのは間違いありません。

とういうことで、Transformers.jsを使ってブラウザでWhisperを動かすOracle APEXアプリケーションを作ってみました。可能であればWebGPUも使用します。

作成したアプリケーションは以下のように動作します。長さが23秒の日本語の音声ファイルの処理に、WebGPUを使って3.6秒かかっています。


WebGPUを使わない場合は10.4秒なので、WebGPUが使えると1/3程度の処理時間になっています。WebGPUの有無や処理の短縮度合いなどは、ブラウザが動作しているPCに依存するかと思います。


作成したAPEXアプリケーションについて紹介します。

空のアプリケーションを作成し、ホーム・ページにすべての機能を実装しています。

ページ・プロパティJavaScriptファンクションおよびグローバル変数の宣言に、以下を記述しています。Whisperによる文字起こしを行なうオブジェクトtranscriberを、ページのどこからでも呼び出せるようにしています。

var transcriber;


WhisperをロードするJavaScriptのコードは、ページに静的コンテンツのリージョンを作成し、そのリージョンのソースHTMLコードに記述します。

最初にP1_USE_GPUのスイッチを確認します。スイッチがオン(つまりP1_USE_GPUの値がY)で、かつ、ブラウザでWebGPUが有効であればWebGPUを使用します。それ以外の場合は、WebGPUは使用しません。

続いてWhisperのモデルを読み込みます。ここではonnx-community/whisper-smallを決め打ちでロードしています。

画面には表示しないため、外観テンプレートなしにしています。


ページ・プロパティP1_GPUにはGPUの使用の有無P1_STATUSには、Whisperの状態を表示します。その他、P1_ELAPSED_TIMEには、文字起こしに要した時間をミリ秒単位で表示します。

ページ・アイテムP1_AUDIOで文字起こしをする音声ファイルを選択します。P1_OUTOUTに音声ファイルから起こした文字列を出力します。

ボタンTRANSCRIBEをクリックしたときに、ページ・アイテムP1_AUDIOで選択済みの音声ファイルの文字起こしを実行します。これは、以下のJavaScriptを実行する動的アクションで実装しています。



以上で実装は完了です。

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

Hugging Faceのモデルの説明によると(Long-Form Transcriptionの章)、処理できる音声の長さは30秒が上限のようです。そのため、長い音声の場合は30秒以下のチャンクに分割して、それぞれのチャンクごとに文字起こしをする必要がありそうです。

今回の記事は以上になります。

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

2024年11月14日木曜日

HTMLの画像に重ねて矩形を描画する手順のまとめ

HTMLのページに表示した画像に重ねて矩形を描画する、いくつかの異なった方法がありました。それぞれの実装方法を後で参照できるように、ひとつのAPEXアプリケーションにまとめました。

以下の3つの方法を実装しています。

  • 画像をキャンバスに描画し、そのキャンバスに矩形を描画する。
  • 画像にキャンバスを重ねて、そのキャンバスに矩形を描画する。
  • 画像に矩形を表示するDIV要素を重ねる。
作成したアプリケーションは以下のように動作します。それぞれの異なる処理を実装していますが、見た目はほぼ同じです。


作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/draw-rectangle-on-image.zip

アプリケーションはすべてホーム・ページに実装しています。

DIV要素でバウンディング・ボックスとなる矩形を表示するため、ページ・プロパティCSSインラインに以下を記述します。


画像の選択にはページ・アイテムP1_IMAGEを使います。タイプイメージ・アップロードです。これは、実質的にはtype="file"のINPUT要素になります。表示形式としてネイティブ・ファイル参照が選択されている場合はINPUT要素がそのまま生成されますが、その他の表示形式ではカスタム要素が生成されます。しかし表示形式に関わらず、ページ・アイテムの要素からfiles(の配列)属性を参照することができます。


ページ・アイテムP1_X0P1_X1P1_Y0P1_Y1で描画する矩形の座標を設定します。

それぞれ0から1の間の数値を指定します。



画像をキャンバスに描画し、そのキャンバスに矩形を描画する。



描画の対象は、静的コンテンツのリージョンCONTAINER_CANVAS_ONLYに以下のように設定します。

<canvas id="canvas-only" class="w100p"><canvas/>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。



画像にキャンバスを重ねて、そのキャンバスに矩形を描画する。



描画の対象は、静的コンテンツのリージョンCONTAINER_CANVAS_ON_IMGに以下のように設定します。

<div style="position: relative;" class="w100p">
    <img id="canvas-on-img-image" src="" width="100%" />
    <canvas id="canvas-on-img-canvas" style="position: absolute; left: 0; "></canvas>
</div>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。




画像に矩形を表示するDIV要素を重ねる。



描画の対象は、静的コンテンツのリージョンCONTAINER_DIV_ON_IMGに以下のように設定します。

<div id="image-container" class="w100p">
<img id="image" src="" width="100%"></img>
</div>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。



注意点


IMG要素にDIV要素を重ねて矩形を表示する際に、親となるリージョンのテンプレート・オプションマージンが設定されていると、矩形を描画する位置にズレが生じました。

本来、矩形の描画を指定した位置は以下です。


画像を含むリージョン(今回の実装ではCONTAINER_DIV_ON_IMG)のテンプレート・オプションにて、Top MarginLeft MarginLargeを設定します。


画像および矩形の表示は以下のように変わり、意図した位置からズレて矩形が描画されます。


比較対象がないと見つかりにくい現象ではないかと思います。

今回の記事は以上になります。

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

2024年11月13日水曜日

Learning TensorFlow.jsのChapter 5で紹介されているペットの顔を検出するアプリを作成する

O'Reillyから出版されている亀が表紙の本「Learning TensorFlow.js」のChapter 5、Introducing Modelsで紹介されている、ペットの顔を検出するアプリをOracle APEXで作成してみました。

元のアプリケーションではIMG要素にCANVAS要素を重ねて表示させ、CANVAS要素に矩形を表示させています。同じ仕組みで矩形を表示させるとコードがほとんど同じになります。今回は少しコードを変更し、選択した画像をCANVAS要素に描画し、そのCANVAS要素を解析した上で、矩形の描画もCANVAS要素に直接行なうようしています。

今回作成したAPEXアプリケーションは以下のように動作します。


ホーム・ページにすべての機能を実装しています。

ページ・プロパティJavaScriptファイルURLに以下を記述しています。

https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js

ファンクションおよびグローバル変数の宣言に以下を記述しています。

var model;

ページ・ロード時に実行に以下を記述し、Learning TensorFlow.jsのChapter 5で使用しているモデルをロードしています。
tf.ready().then(() => {
    /*
     * Learning TensorFlow.js, Grant Laborde, O'Reilly Media, Inc.
     * Chapter 5: Introducing Model
     * ISBN-13: 978-1492090793
     */
    const modelPath = "https://raw.githubusercontent.com/GantMan/learn-tfjs/refs/heads/master/chapter5/simple/simple-object-localization/model/tfjs_quant_uint8/model.json";
    
    apex.items.P1_STATUS.setValue("TensorFlow.js is Ready, Loading Model...");
    tf.loadLayersModel(modelPath, { fromTFHub: true }).then((m) => {
        model = m;
        apex.items.P1_STATUS.setValue("Model is Loaded");
    });
});

画像を表示するCANVAS要素は、静的コンテンツのリージョンのソースHTMLコードに記述します。

<canvas id="detection" class="w50p"><canvas/>


TensorFlow.jsのステータスは、タイプ表示のみのページ・アイテムP1_STATUSに表示します。


解析する画像はページ・アイテムP1_IMAGEで選択します。タイプイメージ・アップロードですが、このアプリケーションはページを送信しないため、選択した画像がサーバーに送信されることはありません。


解析する画像を選択したときに実行される動的アクションにより、選択した画像をCANVAS要素へ書き出します。

以下のJavaScriptコードを実行します。
// get image file and its URL for img src.
const imageFile = this.triggeringElement.files[0];
const imageUrl = URL.createObjectURL(imageFile);
// load image into virtual img element.
const img = new Image();
img.src = imageUrl;
// draw image to canvas.
img.onload = function() {
    const detection = document.getElementById("detection");
    const ctx = detection.getContext("2d");
    // Clear Canvas
    ctx.clearRect(0, 0, detection.width, detection.height);
    // set new height then draw
    detection.height = Math.ceil(img.height * ( detection.width / img.width ));
    ctx.drawImage(this, 0, 0, detection.width, detection.height);
    // Reset Status
    apex.items.P1_STATUS.setValue("Ready for Detection");
}

顔の検出はボタンDETECTを押した時に実行される動的アクションで処理します。

ボタンDETECTを押した時に、以下のJavaScriptコードを実行します。
tf.tidy(() => {
    const detection = document.getElementById("detection");
    const myTensor = tf.browser.fromPixels(detection);
    // Model expects 256x256 0-1 value 3D tensor
    const readyfied = tf.image
        .resizeNearestNeighbor(myTensor, [256,256], true)
        .div(255)
        .reshape([1,256,256,3]);
    apex.items.P1_STATUS.setValue("Start prediction !");
    // predict.
    const result = model.predict(readyfied);
    result.print();
    apex.items.P1_STATUS.setValue("Prediction Done !");
    // Draw box on canvas
    const box = result.dataSync();
    const startX = box[0] * detection.width;
    const startY = box[1] * detection.height;
    const width = (box[2] - box[0]) * detection.width;
    const height = (box[3] - box[1]) * detection.height;
    const ctx = detection.getContext("2d");
    ctx.strokeStyle = "#0F0";
    ctx.lineWidth = 4;
    ctx.strokeRect(startX, startY, width, height);
});

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

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

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

Hugging Face Transformers.jsのObject detectionをOracle APEXで実装してみる

Hugging FaceのTransformers.jsのExamplesに含まれているIn-browser object detectionを、Oracle APEXのアプリケーションとして実装してみます。この例の名前がVanilla JavaScriptであることから分かるように、APEXに組み込みやすいサンプルになっています。GitHubのリポジトリへのリンクはこちらです。

作成したアプリケーションは以下のように動作します。

タヌキはdog、シマウマはzebraとして認識されました。


空のAPEXアプリケーションを作成し、ホーム・ページに機能を実装しています。

ホーム・ページのページ・プロパティCSSインラインに、以下を記述します。サンプルに含まれているstyle.cssの定義を、ほぼそのまま貼り付けています。



解析する画像を選択するページ・アイテムとしてP1_IMAGEを作成しています。タイプイメージ・アップロードです。


ページ・アイテムP1_STATUSに設定した文字列をステータスとして表示します。タイプ表示のみです。


画像を表示する要素およびオブジェクトを検出するJavaScriptコードは、すべて静的コンテンツのリージョンに記述します。サンプルに含まれているindex.jsのコードを、ほぼそのまま貼り付けています。今回はimportmapの定義を省略しています。


以上でObject detectorのアプリケーションは完成です。

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

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

ONNX Runtime WebのQuick StartをOracle APEXに組み込んでみる

ONNX Runtime WebQuick StartをOracle APEXのアプリケーションに組み込んでみます。ONNX Runtime WebはONNX Runtimeによる機械学習モデルをブラウザのJavaScriptで実行するという、Microsoftさんによるプロジェクトです。

ONNX Runtime WebのQuick Startは、画面に一行表示するだけの簡単なスクリプトです。


本記事は、ONNX Runtime WebをOracle APEXに組み込むための作法の紹介になります。

GitHubの以下のページより、Quick Startのコードやモデルを取得します。


参照するコードが記述されたファイルは、ESモジュールを使ったサンプルであるindex_esm.htmlです。model.onnxは、Oracle APEXの静的アプリケーション・ファイルとして保存するため、あらかじめGitHubよりダウンロードしておきます。


空のAPEXアプリケーションを作成し、ホーム・ページにQuick Startを組み込みます。

ONNX Runtime Webを組み込むページのページ・プロパティHTMLヘッダーに、以下のimportmapを定義します。主にWasmを使うことを想定しており、WebGLやWebGPUの設定は含めていません。それらは必要に応じて追記するとよいでしょう。
<script type="importmap">
    {
        "imports": {
            "onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/esm/ort.min.js",
            "onnxruntime-web/wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/esm/ort.wasm.min.js"
        }
    }
</script>

共有コンポーネント静的アプリケーション・ファイルとして、先ほどGitHubのリポジトリよりダウンロードしたmodel.onnxをアップロードします。参照#APP_FILES#model.onnxになります。


静的コンテンツのリージョンを作成し、index_esm.htmlの内容をほぼそのまま転記します。index_esm.html内では、document.bodyに結果を出力していますが、APEXのアプリケーションでは静的コンテンツのリージョンに含まれるDIV要素に出力するように変更しています。



以上でONNX Runtime WebのQuick Startが動作するようになりました。

簡単なアプリケーションですが、アプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/onnx-runtime-web-quick-start.zip

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

2024年11月12日火曜日

Oracle APEXのページにTensorFlow.jsを組み込みInception_v3による分類を実行する

O'Reillyから出版されている亀が表紙の本「Learning TensorFlow.js」のChapter 5、Introducing Modelsで紹介されている、Inception_v3による分類をOracle APEXのアプリケーションに組み込んでみました。Chapter 5は、Inception_v3を使った分類とペットの顔を検出する2つのサンプルの実装を含んでいますが、分類の方を実装しています。

TensorFlow.jsをOracle APEXに組み込む、といっても特別な作業ではなく、HTMLのページに実装できるものであれば、大抵はAPEXのアプリケーションに組み込めます。TensorFlow.jsも同様です。

今回作成したAPEXアプリケーションは以下のように動作します。

リスはsquirrel、レッサーパンダはlesser panda、シマウマはzebraと分類されました。


ホーム・ページにすべての機能を実装しています。


TensorFlow.jsの状態を表示するために、ページ・アイテムP1_STATUSを作成しています。タイプ表示のみで、JavaScriptのコード中よりページ・アイテムの値を設定します。


分類の対象となる画像は、静的コンテンツのリージョンに表示します。ソースHTMLコードに以下を記述します。

<img id="mystery" src="" class="w80p" />

属性srcの値は、ページ・アイテムP1_IMAGEの動的アクション中で設定します。


分類の結果はページ・アイテムP1_OBJECTSに出力します。タイプテキスト領域とし、複数行を表示できるようにしています。


分類する画像を選択するページ・アイテムとしてP1_IMAGEを作成します。タイプイメージ・アップロードです。このアプリケーションはサーバーにページを送信することはありません。画像ファイルをブラウザ上で選択するだけで、イメージのアップロードは行いません。


ページ・アイテムP1_IMAGEで画像を選択すると、変更イベントが発生します。変更イベントにより呼び出される動的アクション(のTRUEアクション)として、JavaScriptコードの実行を選び、設定コードに以下を記述します。
// get image file and its URL for img src.
const imageFile = this.triggeringElement.files[0];
const imageUrl = URL.createObjectURL(imageFile);
document.getElementById("mystery").src = imageUrl;
// Reset Status
apex.items.P1_OBJECTS.setValue("");
apex.items.P1_STATUS.setValue("Ready for Detection");
ページ・アイテムに選択された画像を、静的コンテンツに含まれるIMG要素の属性SRCに設定しています。そうすることにより、選択した画像がIMG要素として画面に表示されます。


ページ・ロード時にTensorFlow.jsを初期化します。

ページ・プロパティJavaScriptファイルURLとして、以下を記述します。

https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js

ファンクションおよびグローバル変数の宣言に以下を記述します。
var model;
var INCEPTION_CLASSES = [];

ページ・ロード時に実行に、以下を記述します。TensorFlow.jsの初期化が完了したのち、Inception_v3のモデルを読み込んでいます。また、分類のラベルをテキスト・ファイルからJSON配列INCEPTION_CLASSESに読み込んでいます。
/*
 * ページ・ロード時にTensorFlow.jsが読み込まれる。TensorFlow.jsがReadyになったら、続けてモデルを読み込む。
 * ページを離れたらロードしたモデルは破棄されると思うので、モデルにたいしてはdispose()は呼び出していない。
 */
tf.ready().then(() => {
    // const modelPath = "https://tfhub.dev/google/tfjs-model/imagenet/inception_v3/classification/3/default/1";
    const modelPath = "https://www.kaggle.com/models/google/inception-v3/TfJs/classification/2";
    apex.items.P1_STATUS.setValue("TensorFlow.js is Ready, Loading Model...");
    tf.loadGraphModel(modelPath, { fromTFHub: true }).then((m) => {
        model = m; // modelはページ・グローバルな変数
        apex.items.P1_STATUS.setValue("Model is Loaded");
    });
});

/*
 * ImageNetのラベルを配列INCEPTION_CLASSESに読み込む。
 */
fetch('https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
    .then(response => {
        if (!response.ok) {
            throw new Error('Failed to get ImageNetLabels.txt');
        }
        return response.text();
    })
    .then( (text) => {
        // テキストを行ごとに分割し、配列に変換
        INCEPTION_CLASSES = text.split('\n').filter(label => label.trim() !== '');
        console.log(INCEPTION_CLASSES);
    })
    .catch(error => {
        console.error('Failed to read ImageNetLabels.txt:', error);
    });
モデルのロードが完了してから(P1_STATUSModel is Loadedと表示されたのち)、画像の分類を実行できるようになりますが、それまでの間、ボタンを無効化するなどの処理は行っていないため、注意が必要です。


kaggle.comでホストされているモデルのブラウザへの読み込みを許可するため、アプリケーション定義セキュリティブラウザ・セキュリティのセクションにあるHTTPレスポンス・ヘッダーに以下を記述します。

Access-Control-Allow-Origin: https://www.kaggle.com


最後に分類を実行するボタンDETECTを作成し、ボタンをクリックしたときに呼び出される動的アクションを定義します。

実行するJavaScriptのコードは以下になります。
tf.tidy(() => {
    const mysteryImage = document.getElementById("mystery");
    const myTensor = tf.browser.fromPixels(mysteryImage);
    // Inception v3 expects an image resized to 299x299
    const readyfied = tf.image
        .resizeBilinear(myTensor, [299,299], true)
        .div(255)
        .reshape([1,299,299,3]);
    apex.items.P1_STATUS.setValue("Start prediction !");
    // predict.
    const result = model.predict(readyfied);
    result.print();
    apex.items.P1_STATUS.setValue("Prediction Done !");

    const { values, indices } = tf.topk(result, 3);
    indices.print();

    // Let's hear those winners
    const winners = indices.dataSync();
    
    apex.items.P1_OBJECTS.setValue(
`First place ${INCEPTION_CLASSES[winners[0]]},
Second place ${INCEPTION_CLASSES[winners[1]]},
Third place ${INCEPTION_CLASSES[winners[2]]}`);

});

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

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

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