コンテンツにスキップ

ONNX RuntimeとNext.jsで画像を分類する

ONNX Runtime WebでWebアプリケーションの画像を分類する

Section titled “ONNX Runtime WebでWebアプリケーションの画像を分類する”

このチュートリアルでは、GitHubリポジトリテンプレートを使用して、ONNX Runtime webを使った画像分類Webアプリを構築します。コンピュータビジョンモデルのブラウザでのJavaScript推論を行います。

データサイエンスで一般的に使用されていない言語でデプロイと推論を行う際の最も困難な部分の1つは、データ処理と推論の方法を理解することです。このテンプレートで、すべての困難な作業を完了しました!

以下は、テンプレートからのサイトの外観です。サンプル画像のリストをループし、SqueezeNetモデルで推論セッションを呼び出し、推論からスコアとラベルを返しています。

テンプレート出力例:

サンプル画像でブラウザ推論を行っている画像。

このアプリケーションは、onnxruntime-web JavaScriptライブラリを使用して、ブラウザ内のデバイス上で推論を実行します。

ONNX Model ZooSqueezeNetを使用します。SqueezeNetモデルは画像分類を実行します - 画像を入力として受け取り、画像内の主要なオブジェクトを事前定義されたクラスのセットに分類します。これらは1000の異なるクラスを含むImageNetデータセットでトレーニングされています。SqueezeNetモデルは、優れた精度を提供しながら、サイズと速度の点で非常に効率的です。これにより、クライアント側推論のようなサイズに厳しい制約があるプラットフォームに理想的です。

さらにモデルのメモリとディスク効率が必要な場合、ONNXモデルをORT形式に変換し、アプリケーションでONNXモデルの代わりにORTモデルを使用できます。また、アプリケーション内の特定のモデルのサポートのみを含むようにONNX Runtimeのサイズを削減することもできます。

NextJS(ReactJSフレームワーク)で静的サイトを作成してブラウザでモデルをデプロイする

Section titled “NextJS(ReactJSフレームワーク)で静的サイトを作成してブラウザでモデルをデプロイする”

このテンプレートの目標は、加速されたML Webアプリケーションの出発点を提供することです。テンプレートはNextJSフレームワークを使用してコンピュータビジョンアプリケーションを生成し、TypeScriptで作成され、webpackでビルドされます。テンプレートに飛び込んで、コードを分解しましょう。

UtilsフォルダにはimageHelper.tsmodelHelper.tspredict.tsの3つのファイルがあります。Predictは、Webコンポーネントから推論を開始するエントリポイントです。ここでヘルパーをインポートし、デフォルト関数を呼び出して画像テンソルを取得し、モデル推論を実行します。

react-next\utils\predict.ts
// Language: typescript
import { getImageTensorFromPath } from './imageHelper';
import { runSqueezenetModel } from './modelHelper';
export async function inferenceSqueezenet(path: string): Promise<[any,number]> {
// 1. 画像をテンソルに変換
const imageTensor = await getImageTensorFromPath(path);
// 2. モデルを実行
const [predictions, inferenceTime] = await runSqueezenetModel(imageTensor);
// 3. 予測と推論にかかった時間を返す
return [predictions, inferenceTime];
}

まず、ローカルファイルまたはURLから画像を取得し、テンソルに変換する必要があります。imageHelper.tsgetImageTensorFromPath関数はJIMPを使用してファイルを読み取り、リサイズしてimageDataを返します。JIMPはJavaScript画像操作ライブラリです。リサイズ、グレースケール、書き込みなど、画像データを操作するための多くの組み込み関数があります。この例ではリサイズのみが必要ですが、コードでは追加の画像データ処理が必要な場合があります。

import * as Jimp from 'jimp';
import { Tensor } from 'onnxruntime-web';
export async function getImageTensorFromPath(path: string, dims: number[] = [1, 3, 224, 224]): Promise<Tensor> {
// 1. 画像を読み込む
var image = await loadImagefromPath(path, dims[2], dims[3]);
// 2. テンソルに変換
var imageTensor = imageDataToTensor(image, dims);
// 3. テンソルを返す
return imageTensor;
}
async function loadImagefromPath(path: string, width: number = 224, height: number= 224): Promise<Jimp> {
// Jimpを使用して画像を読み込み、リサイズする
var imageData = await Jimp.default.read(path).then((imageBuffer: Jimp) => {
return imageBuffer.resize(width, height);
});
return imageData;
}

imageDataを取得したら、それをimageDataToTensor関数に送信して、推論用のORTテンソルに変換します。JavaScriptで画像をテンソルに変換するには、RGB(赤、緑、青)値を配列に取得する必要があります。これを行うために、各ピクセルのRGBAの4チャンネルごとにimageBufferDataをループします。画像のRGBピクセルチャンネルを取得したら、transposedDataからFloat32Arrayを作成し、255で割って値を正規化します。なぜ255がピクセル値を正規化するのでしょうか?正規化は、違いを歪めることなく値を共通のスケールに変更するために使用される技術です。255はRGB値の最大数であるため、255で割ることで統計的な違いを失うことなく値を0と1の間に正規化します。画像のFloat32Array表現を取得したので、型、データ、次元を送信してORTテンソルを作成できます。次に、推論用のinputTensorを返します。

function imageDataToTensor(image: Jimp, dims: number[]): Tensor {
// 1. 画像からバッファデータを取得し、R、G、B配列を作成
var imageBufferData = image.bitmap.data;
const [redArray, greenArray, blueArray] = new Array(new Array<number>(), new Array<number>(), new Array<number>());
// 2. 画像バッファをループし、R、G、Bチャンネルを抽出
for (let i = 0; i < imageBufferData.length; i += 4) {
redArray.push(imageBufferData[i]);
greenArray.push(imageBufferData[i + 1]);
blueArray.push(imageBufferData[i + 2]);
// アルファチャンネルをフィルタリングするためにdata[i + 3]をスキップ
}
// 3. RGBを連結して[224, 224, 3] -> [3, 224, 224]に転置し、数値配列にする
const transposedData = redArray.concat(greenArray).concat(blueArray);
// 4. float32に変換
let i, l = transposedData.length; // 長さ、ループに必要
// これらの次元出力用にサイズ3 * 224 * 224のFloat32Arrayを作成
const float32Data = new Float32Array(dims[1] * dims[2] * dims[3]);
for (i = 0; i < l; i++) {
float32Data[i] = transposedData[i] / 255.0; // floatに変換
}
// 5. onnxruntime-webからテンソルオブジェクトを作成
const inputTensor = new Tensor("float32", float32Data, dims);
return inputTensor;
}

inputTensorは推論の準備ができています。デフォルトのmodelHelper.ts関数を呼び出して、ロジックを見てみましょう。まず、モデルへのパスとSessionOptionsを送信してort.InferenceSessionを作成します。executionProvidersでは、GPUを使用する場合はwebgl、CPUを使用する場合はwasmを使用できます。推論構成で利用可能なSessionOptionsについて詳しく学ぶには、こちらのドキュメントを参照してください。

import * as ort from 'onnxruntime-web';
import _ from 'lodash';
import { imagenetClasses } from '../data/imagenet';
export async function runSqueezenetModel(preprocessedData: any): Promise<[any, number]> {
// セッションを作成してオプションを設定。詳細なオプションについてはこちらのドキュメントを参照:
//https://onnxruntime.ai/docs/api/js/interfaces/InferenceSession.SessionOptions.html#graphOptimizationLevel
const session = await ort.InferenceSession
.create('./_next/static/chunks/pages/squeezenet1_1.onnx',
{ executionProviders: ['webgl'], graphOptimizationLevel: 'all' });
console.log('Inference session created');
// 推論を実行して結果を取得
var [results, inferenceTime] = await runInference(session, preprocessedData);
return [results, inferenceTime];
}

次に、sessionと入力テンソルpreprocessedDataを送信してrunInference関数を呼び出しましょう。

async function runInference(session: ort.InferenceSession, preprocessedData: any): Promise<[any, number]> {
// 推論時間を計算するために開始時間を取得
const start = new Date();
// モデルエクスポートからの入力名と前処理されたデータでフィードを作成
const feeds: Record<string, ort.Tensor> = {};
feeds[session.inputNames[0]] = preprocessedData;
// セッション推論を実行
const outputData = await session.run(feeds);
// 推論時間を計算するために終了時間を取得
const end = new Date();
// 秒に変換
const inferenceTime = (end.getTime() - start.getTime())/1000;
// モデルエクスポートからの出力名で出力結果を取得
const output = outputData[session.outputNames[0]];
// 出力データのソフトマックスを取得。ソフトマックスは値を0と1の間に変換
var outputSoftmax = softmax(Array.prototype.slice.call(output.data));
// トップ5の結果を取得
var results = imagenetClassesTopK(outputSoftmax, 5);
console.log('results: ', results);
return [results, inferenceTime];
}

推論が完了すると、トップ5の結果と推論の実行にかかった時間を返します。これはImageCanvas Webコンポーネントに表示されます。

このテンプレートのdataフォルダには、推論結果インデックスに基づいてラベルを割り当てるために使用されるimagenetClassesがあります。さらに、アプリケーションをテストするためのsample-image-urls.tsが提供されています。

ImageCanvas FSX要素Webコンポーネント

Section titled “ImageCanvas FSX要素Webコンポーネント”

ImageCanvas.tsx Webコンポーネントには、ボタンと表示要素があります。以下はWebコンポーネントのロジックです:

import { useRef, useState } from 'react';
import { IMAGE_URLS } from '../data/sample-image-urls';
import { inferenceSqueezenet } from '../utils/predict';
import styles from '../styles/Home.module.css';
interface Props {
height: number;
width: number;
}
const ImageCanvas = (props: Props) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
var image: HTMLImageElement;
const [topResultLabel, setLabel] = useState("");
const [topResultConfidence, setConfidence] = useState("");
const [inferenceTime, setInferenceTime] = useState("");
// IMAGE_URLS配列から画像を読み込む
const getImage = () => {
var sampleImageUrls: Array<{ text: string; value: string }> = IMAGE_URLS;
var random = Math.floor(Math.random() * (9 - 0 + 1) + 0);
return sampleImageUrls[random];
}
// 画像とその他のUI要素を描画してから推論を実行
const displayImageAndRunInference = () => {
// 画像を取得
image = new Image();
var sampleImage = getImage();
image.src = sampleImage.value;
// 以前の値をクリア
setLabel(`推論中...`);
setConfidence("");
setInferenceTime("");
// キャンバスに画像を描画
const canvas = canvasRef.current;
const ctx = canvas!.getContext('2d');
image.onload = () => {
ctx!.drawImage(image, 0, 0, props.width, props.height);
}
// 推論を実行
submitInference();
};
const submitInference = async () => {
// キャンバスから画像データを取得して推論を送信
var [inferenceResult,inferenceTime] = await inferenceSqueezenet(image.src);
// 最高の信頼度を取得
var topResult = inferenceResult[0];
// ラベルと信頼度を更新
setLabel(topResult.name.toUpperCase());
setConfidence(topResult.probability);
setInferenceTime(`推論速度: ${inferenceTime}`);
};
return (
<>
<button
className={styles.grid}
onClick={displayImageAndRunInference} >
Squeezenet推論を実行
</button>
<br/>
<canvas ref={canvasRef} width={props.width} height={props.height} />
<span>{topResultLabel} {topResultConfidence}</span>
<span>{inferenceTime}</span>
</>
)
};
export default ImageCanvas;

このWebコンポーネント要素はindex.tsxにインポートされます。

<ImageCanvas width={240} height={240}/>

next.config.jsにいくつかのプラグインを追加する必要があります。これはNextJSフレームワークで実装されたwebpack構成です。CopyPluginは、wasmファイルとモデルフォルダファイルをデプロイ用のoutフォルダにコピーするために使用されます。

/** @type {import('next').NextConfig} */
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
reactStrictMode: true,
//distDir: 'build',
webpack: (config, { }) => {
config.resolve.extensions.push(".ts", ".tsx");
config.resolve.fallback = { fs: false };
config.plugins.push(
new NodePolyfillPlugin(),
new CopyPlugin({
patterns: [
{
from: './node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.wasm',
to: 'static/chunks/pages',
}, {
from: './node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.mjs',
to: 'static/chunks/pages',
},
{
from: './model',
to: 'static/chunks/pages',
},
],
}),
);
return config;
}
}

これを静的サイトとしてデプロイしたいので、package.jsonのビルドコマンドをnext build && next exportに更新して、静的サイト出力を生成する必要があります。これにより、静的サイトのデプロイに必要なすべてのアセットが生成され、outフォルダに配置されます。

{
"name": "ort-web-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"fs": "^0.0.1-security",
"jimp": "^0.16.1",
"lodash": "^4.17.21",
"ndarray": "^1.0.19",
"ndarray-ops": "^1.2.2",
"next": "^11.1.2",
"onnxruntime-web": "^1.9.0",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"node-polyfill-webpack-plugin": "^1.1.4",
"copy-webpack-plugin": "^9.0.1",
"@types/lodash": "^4.14.176",
"@types/react": "17.0.19",
"eslint": "7.32.0",
"eslint-config-next": "11.1.0",
"typescript": "4.4.2"
}
}

プロジェクトをローカルで実行する

Section titled “プロジェクトをローカルで実行する”

プロジェクトを実行する準備ができました。デバッグで開始するか、outフォルダをビルドするか、デバッグなしで開始するかに基づいてコマンドを実行します。

// デバッグで実行
npm run dev
// プロジェクトをビルド
npm run build
// デバッグなしで実行
npm run start

サイトを構築したので、Azure Static Web Appsにデプロイする準備ができました。Azureを使用してデプロイする方法については、こちらのドキュメントを確認してください。

このテンプレートの使用方法について説明しましたが、ここにはボーナスがあります!テンプレートのnotebookフォルダには、必要な変更を実験して試すためのこのコードを含むノートブックがあります。これにより、異なるモデルや試したい画像がある場合、非常に簡単に行うことができます。TypeScript Jupyterノートブックを使用するには、VS Code Jupyterノートブック拡張機能をダウンロードしてください。

  • GitHub NextJS ORT-Web Templateリポジトリに移動して、今すぐテンプレートの使用を開始してください。

  • こちらのリリースブログを確認してください

  • テンプレートは、ReactJSでアプリケーションを構築するためのフレームワークであるNextJSを使用しています。

  • より多くのモデルについてはONNX Runtime Web Demoを確認してください。ONNX Runtime Web demoは、VueJSでONNX Runtime Webを実行する実際のユースケースを示すインタラクティブなデモポータルです。現在、ONNX Runtime Webの力を素早く体験するための4つの例をサポートしています。

  • このブログでは、事前トレーニング済みのAlexNetモデルをブラウザにデプロイするためにORT WebをPythonと組み合わせて使用する方法を示しています。

  • より多くのONNX Runtime JS examplesを確認してください