0. 概要
- スマートフォンでリアルタイムに物体検出を行います。ROG Phone 3 (ゲーミングスマートフォン ASUS社 2020年)で約6fpsで動作します。
- 物体検出にはyoloxという機械学習モデルを使用します。yoloxのpytorchのモデルファイルをonnx形式に変換してjavascript上で動作させることによりスマートフォンで実行可能とします。
- Google Colaboratoryに開発・実行環境を構築するので、パソコン、スマートフォンに何もインストールする必要がありません。
構成は ↑ のようになっています。Google Colaboratoryを使うわけですが、基本はパソコンで作業して、実行確認をスマートフォンで行います。
1. 手順
- Google ColaboratoryからGoogle Driveを接続します。
- Colaboratoryの操作によってDrive上にyoloxをインストールします。そして公式の学習済み重みファイルを用いて、Colaboratory上でyoloxの動作確認をします。
- yoloxのモデルをjavascriptで動作させるためにonnx形式に変換します。
- onnx形式のモデルファイルを読み込むjavascriptが入ったhtmlファイルを用意します(以下からサンプルをダウンロードできます)。
- 4のhtmlファイルをスマートフォンで読み込むために、Colaboratory上にWebサーバを立ち上げてスマートフォンからアクセスして動作を確認します。
2. ダウンロード
すべてのファイルは以下からダウンロードできます。とりあえず実行してみる場合、Google Driveの同一フォルダに以下の3つのipynbファイルとindex.htmlをアップロードして、①install.ipynb, ②translate.ipynbをパソコンから実行し、③publish.ipynbをスマートフォンから実行すれば試すことができます。
ここから先は、個々のipynbとindex.htmlで何をしているのかを説明していきます。
3. yoloxのGoogle Driveへのインストール (install.ipynb)
基本的には ↓ の公式ページに従って作業を行います。
ここで説明するプログラムは、本記事のダウンロードの箇所からダウンロードできるinstall.ipynbにすべて書かれています。
# google drive のマウント
current_folder = "/content/drive/MyDrive/Colab Notebooks/yolox"
from google.colab import drive
drive.mount('/content/drive')
%cd $current_folder
Colaboratoryではランタイムと呼ばれる実行環境が毎回リセットされます。よってランタイムのディスク上にyoloxをインストールすると次に使用するときに消えてしまうため、永続的に利用できるgoogle drive上にyoloxをインストールします。install.ipynbにあるプログラムを実行していくと、Google Driveへのアクセス許可が表示されるので許可してください。また関連するGoogleアカウントに許可した動作を確認するメールが送られてくるので、メールの「心当たりがある」を選択しておいてください。
上記のプログラムは、最後にカレントフォルダがgoogle driveのフォルダに代わるようになっています。以降はそのフォルダでコマンドが実行されます。
import numpy as np
import torch
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image
# 各種のバージョンを確認する。
!python --version
print(f"numpy {np.__version__}")
print(f"pytorch {torch.__version__}")
print(f"matplotlib {matplotlib.__version__}")
Python 3.10.12
numpy 1.22.4
pytorch 2.0.1+cu118
matplotlib 3.7.1
インストールの前に、本記事を作成した2023年7月19日時点の各モジュールのバージョンを表示しておきます。
# yolox install
!git clone https://github.com/Megvii-BaseDetection/YOLOX.git
%cd YOLOX
!pip install -v -e .
...
Successfully installed loguru-0.7.0 ninja-1.11.1 onnx-1.14.0 onnx-simplifier-0.4.10 thop-0.1.1.post2209072238 yolox-0.3.0
上記のコマンドを実行してyoloxをインストールします。インストールに成功すると、上記のように"Successfully ... "と出力されます。おそらく、いろいろとwarningが表示されると思いますが、たぶん最後のSuccessfullyが表示されていれば大丈夫だと思います。
3.1. Colaboratory上でyoloxの実行確認
yoloxを動作させるために、公式ホームページで公開されている学習済みモデルの重みファイルをダウンロードします。
モデルはいくつかありますが、今回はスマートフォンでリアルタイムに動作させることを考えて、最も小さなモデルであるyolox-nanoを選択します。手動でダウンロードする場合には、以下のgithubという個所をクリックすると重みファイルをダウンロードできます。ただここでは、wgetコマンドによってダウンロードするので、実際に公式ホームページを開く必要はありません。
モデルファイルを手動でダウンロードする場合、↑ を選択してください。手動でダウンロードしたファイルは、install.ipynbをアップしたフォルダにcheckpointsという名前のフォルダを作成し、そこに保存してください。また以下のプログラムを実行すると、そのモデルファイルをGoogle Drive上のcheckpointsフォルダにダウンロードします。
# 学習済み重みファイルのダウンロード
!mkdir ../checkpoints
%cd ../checkpoints
!wget https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_nano.pth
%cd ../YOLOX
...
2023-xx-xx 00:00:00 (47.2 MB/s) - ‘yolox_nano.pth’ saved [7694953/7694953]
...
このセルの実行時のカレントフォルダは /content/drive/MyDrive/Colab Notebooks/yolox/YOLOX になっています。この1つ上に checkpoints というフォルダを作成してそこにモデルの重みファイルをダウンロードします。成功すると上記のような出力が表示されます。
# 動作確認
!python tools/demo.py image -n yolox-nano -c ../checkpoints/yolox_nano.pth --path assets/dog.jpg --conf 0.25 --nms 0.45 --tsize 640 --save_result --device cpu
Saving detection result in ./YOLOX_outputs/yolox_nano/vis_res/2023_07_14_03_42_47/dog.jpg
ダウンロードしたモデルの重みファイルを用いて、yoloxの中にあるサンプル画像から物体検出を実行してみます。demo.pyを実行し、モデル名を yolox-nano、重みファイルを先ほどダウンロードした ../checkpoints/yolox_nano.pth、画像は assets/dog.jpg、実行時のパラメータはそれぞれ公式ページに載っている値を使用しています。実行に成功すると、上記のように ./YOLOX_outputs/yolox_nano/vis_res/2023_07_14_03_42_47/dog.jpg ファイルが作られたことが出力されます。
# 画像の表示
fig,axs = plt.subplots(nrows=1,ncols=2)
axs[0].imshow(np.asarray(Image.open("./assets/dog.jpg")))
axs[0].set_title("original image")
axs[1].imshow(np.asarray(Image.open("./YOLOX_outputs/yolox_nano/vis_res/2023_07_14_03_42_47/dog.jpg")))
axs[1].set_title("found objects")
fig.show()
物体検出結果のファイルをColaboratory上で表示します。左の画像が元画像で、右が検出結果です。検出結果には3つの枠が表示されそれぞれのラベルが表示されています。これでyoloxが正常に動作していることが確認できます。
4. yoloxの学習済みモデルファイルをonnx形式に変換 (translate.ipynb)
yoloxはpytorchによって作成されていてモデルの重みファイルはそのままではjavascriptで動作させることができません(2023.07.21時点)。そこでonnx(open neural network exchange)の形式に変換します。ここで記載しているプログラムはすべて本記事のダウンロードの箇所からダウンロードできる translate.ipynb に記載されています。
モデルの変換は、以下のmicrosoftのサイトを参考にさせて頂きました。
# google drive のマウント
current_folder = "/content/drive/MyDrive/Colab Notebooks/yolox/YOLOX"
from google.colab import drive
drive.mount('/content/drive')
%cd $current_folder
yoloxのインストールの時と同様に、Google Driveに接続します。
# yolox.expで使用する loguru のインストール
!pip install loguru
# pytorch onnx moduleのインストール
!pip install onnx-pytorch
...
Successfully installed loguru-0.7.0
...
Successfully installed coloredlogs-15.0.1 humanfriendly-10.0 onnx-1.14.0 onnx-pytorch-0.1.5 onnxruntime-1.15.1
モデル変換で使用するモジュールを追加でインストールします。出力に上記のようなものが表示されていれば、インストールは成功しています。
import numpy as np
import torch
import torch.onnx
from yolox.exp import get_exp
# 各種のバージョンを確認する。
!python --version
print(f"numpy {np.__version__}")
print(f"pytorch {torch.__version__}")
Python 3.10.6
numpy 1.22.4
pytorch 2.0.1+cu118
各モジュールのバージョンを確認します。ただ先ほどと違い、python の importに yolox.exp.get_exp が追加されていますので、ご注意ください。
# translate
# 参考url: https://learn.microsoft.com/ja-jp/windows/ai/windows-ml/tutorials/pytorch-convert-model
input_size = [416,416]
model_name = "yolox-nano"
exp_filename = None
checkpoint_file = "../checkpoints/yolox_nano.pth"
output_filename = f"../yolox_nano_{'_'.join(map(str,input_size))}.onnx"
model = get_exp(exp_filename,model_name).get_model()
ckpt = torch.load(checkpoint_file,map_location="cpu")
model.load_state_dict(ckpt["model"])
model.eval()
dummy_input = torch.randn(1, 3,*input_size, requires_grad=True)
torch.onnx.export(model, # model being run
dummy_input, # model input (or a tuple for multiple inputs)
output_filename, # where to save the model
export_params=True, # store the trained parameter weights inside the model file
opset_version=10, # the ONNX version to export the model to
do_constant_folding=True, # whether to execute constant folding for optimization
input_names = ['modelInput'], # the model's input names
output_names = ['modelOutput'], # the model's output names
dynamic_axes={'modelInput' : {0 : 'batch_size'}, # variable length axes
'modelOutput' : {0 : 'batch_size'}})
...
======================= 0 NONE 0 NOTE 0 WARNING 0 ERROR ========================
実行して変換されると、出力の最後に上記のような表示がされると思います。
変換後のファイルは、上記のようにGoogle Driveから確認することができます。yoloxのフォルダにあるyolox_nano_416_416.onnxファイルが変換後のonnx形式のモデルファイルです。
5. onnx形式のモデルファイルを読み込んで実行するJavascript (index.html)
ここで記述するすべてのプログラムは、本記事のダウンロードの箇所からダウンロードできる index.html ファイルに記載されています。とりあえず実行する場合は、ダウンロードしたファイルをGoogle Drive上にアップロードすれば良いです。
// スクリプトの動的読み込み
const load_script = fname => {
return new Promise((resolve, reject) => {
const sc = document.createElement("script");
sc.type = "text/javascript";
sc.src = fname;
sc.onload = () => resolve();
sc.onerror = (e) => reject(e);
const s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(sc, s);
});
};
// モデルの読み込み: fname 読み込み機械学習ファイル名
const load_model = async fname => {
console.log("loading ort.min.js");
await load_script("https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js");
console.log(`loading ${fname}`);
return await ort.InferenceSession.create(fname);
};
// 非同期でライブラリ、モデルファイルの読み込みを実施する。
let model = undefined;
load_model("yolox_nano_416_416.onnx").then(e => model = e);
関連するスクリプトのファイルを動的に読み込むようにしています。scriptタグを使わずに動的にする理由は、拡張性です。このプロジェクト1つだけではあまり関係がないのですが、例えばtensorflow.jsを用いる場合との比較を容易に同じindex.htmlファイルで行うために、動的に切り替えられるようにしています。
onnxのjavascriptランタイムは、https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js
です。htmlのscriptタグを使う場合、このファイルをscriptタグで読み込んで、モデルのファイルであるyolox_nano_416_416.onnxをort.InferenceSession.createの引数として与えて読み込めばOKです。
index.htmlファイルでは、デバイスに装着されているカメラデバイスの一覧を取得し、ユーザが選択したカメラ映像をHTMLのcanvasエレメントにコピーします。この際に、yoloxモデルの入力サイズである416x416の部分だけをカメラ映像の中心部分から抜き出してコピーしています。ここら辺のソースコードは、qiita記事の上では省略いたします。詳しくは、ソースコード自体をご覧いただければと思います。
// canvasのサイズは、yoloxの入力サイズと同じに設定している。
const float32array = new Float32Array(canvas.width * canvas.height * 3);
for (let y = 0; y < canvas.height; y += 1) {
for (let x = 0; x < canvas.width; x += 1) {
const pi = y * canvas.width + x;
for (let i = 0; i < 3; i += 1) {
float32array[pi + i * canvas.width * canvas.height] = imageData.data[pi * 4 + i];
}
}
}
const tensor = new ort.Tensor("float32", float32array, [1, 3, canvas.height, canvas.width]);
const results = await model.run({ [model.inputNames?.[0]]: tensor });
yoloxモデルの推論実行のためにモデルへの入力データをjavascriptの型であるFloat32Arrayを用いて作成します。画像のピクセルごとのコピーは、配列データの構造が違うので、1ピクセルごとにコピーしています。構造とは、canvasから取得したimageDataではrgbaというチャネルが4つあり、[縦,横,チャネル]の順にデータが格納されています。一方yoloxの入力データは、[チャネル,縦,横]です。この違いを考慮して1ピクセルずつコピーしています。
Float32Arrayにコピーされたデータは、ort.Tensorメソッドによってonnxのデータ形式に変換され、model.runで推論が実行されます。
// 信頼度の高い bbox を抽出して、枠を描画する。
yolox_postprocess(results.modelOutput).forEach(obj => {
ctx.strokeStyle = pallet.color(obj.class);
ctx.strokeRect(...obj.bbox);
ctx.fillStyle = pallet.color(obj.class);
ctx.fillText(obj.class, obj.bbox[0], obj.bbox[1]);
});
yolox_postprocessは、独自に作成したyoloxの後処理です。yoloxは、CNNが作成する大きさの異なる3つの特徴マップから物体を検出します。特徴マップ上の1つの要素から1つのbboxと呼ばれる枠を計算します。この特徴マップの要素数は416x416画像の場合は合計で3,549個となります。yoloxの出力は、この3,549個のbboxの位置と信頼度です。ここから後処理として信頼度の高いbboxのみを抽出します。その関数をjavascript上にyolox_postprocessという名前で作成しています。yolox_postprocessの詳細についてはqiita記事上では省略いたします。気になる方はソースコードをご参照ください。
また、上記のプログラムにあるpalletは、検出した物体の名称であるクラス名とそれを表示する色を管理するものです。クラス名を指定して色を取得すると、すでに色が割り当てられたクラス名ならばその色を、まだ割り当てられていないならば新しい色を割り当ててからその色を返します。これも詳しくはソースコードをご参照ください。
以下にindex.htmlの全文を載せておきます。
index.htmlファイルの全文
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=yes">
<style>
body {
margin: 0;
}
video {
display: none;
}
</style>
</head>
<body>
<video id="video" playsinline="true" muted="true" autoplay="true"></video>
<canvas id="canvas"></canvas>
<div id="console"><span id="fps"></span></div>
<script type="module">
// スクリプトの動的読み込み
const load_script = fname => {
return new Promise((resolve, reject) => {
const sc = document.createElement("script");
sc.type = "text/javascript";
sc.src = fname;
sc.onload = () => resolve();
sc.onerror = (e) => reject(e);
const s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(sc, s);
});
};
// モデルの読み込み: fname 読み込み機械学習ファイル名
const load_model = async fname => {
console.log("loading ort.min.js");
await load_script("https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js");
console.log(`loading ${fname}`);
return await ort.InferenceSession.create(fname);
};
// userAgentの表示(デバッグ用)
// const el = document.createElement("div");
// el.innerText = navigator.userAgent;
// document.getElementsByTagName("body")[0].appendChild(el);
// 非同期でライブラリ、モデルファイルの読み込みを実施する。
let model = undefined;
load_model("yolox_nano_416_416.onnx").then(e => model = e);
// enumerateDevicesを実行する前にカメラ使用の権限を得るために一度 getUserMedia を実行する。
const temp_mediastream = await navigator.mediaDevices.getUserMedia({ video: true });
// getUserMediaで得られたmediaStreamは明示的にstopしないとandroid chromeの場合には2つ目以降のカメラデバイスをオープンできない。
temp_mediastream?.getTracks().forEach(e => e.stop());
// デバイスに装着されているカメラデバイス一覧を html のラジオボタンとして作成し、
// 選択されたカメラのデバイス情報を返す関数。処理は非同期で実施される。
const video_selection = async (parent) => {
const div = document.createElement("div");
const cls = "video_selection";
parent.appendChild(div)
const devices = await navigator.mediaDevices.enumerateDevices();
console.log("devices", devices);
devices.filter((d) => d.kind == "videoinput").forEach((e, i) => {
const rd = document.createElement("input");
rd.id = e.deviceId;
rd.setAttribute("type", "radio");
rd.setAttribute("name", "videoinput");
rd.setAttribute("value", e.deviceId)
rd.classList.add(cls);
div.appendChild(rd);
const lb = document.createElement("label");
lb.setAttribute("for", rd.id);
lb.innerText = e.label.length === 0 ? `video ${i}` : e.label;
div.appendChild(lb);
div.appendChild(document.createElement("br"));
});
return new Promise((resolve, reject) => {
document.querySelectorAll(`.${cls}`).forEach(e => {
e.addEventListener("change", (e) => {
parent.removeChild(div);
const device = devices.filter((d) => d.deviceId == e.target.value)[0];
resolve(device)
})
});
});
};
// カメラデバイスの取得、表示の開始
const device = await video_selection(document.getElementsByTagName("body")[0]);
const video = document.getElementById("video");
video.srcObject?.getTracks().forEach(e => e.stop());
video.srcObject = await navigator.mediaDevices.getUserMedia({ audio: false, video: { deviceId: device.deviceId }, zoom: true, });
await video.play();
const settings = video.srcObject?.getTracks().filter(e => e.kind === "video")[0].getSettings() ??
{ width: video.videoWidth || video.width, height: video.videoHeight || video.height };
console.log(settings);
// canvasの幅高さは、モデルの入力に合わせておく。
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
canvas.width = 416;
canvas.height = 416;
// videoからcanvasに画像をコピーするときに、送り先のcanvasの方が幅高さが小さい場合はvideoの中心の一部を、
// 逆にcanvasの方が大きい場合はvideoをcanvasの中心に位置するようにコピーする。これはそのための、
// sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight
// (それぞれの意味は https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/drawImage 参照)
const drect = [
Math.max(0, Math.trunc((settings.width - canvas.width) / 2)),
Math.max(0, Math.trunc((settings.height - canvas.height) / 2)),
Math.min(canvas.width, settings.width),
Math.min(canvas.height, settings.height),
Math.max(0, Math.trunc((canvas.width - settings.width) / 2)),
Math.max(0, Math.trunc((canvas.height - settings.height) / 2)),
Math.min(canvas.width, settings.width),
Math.min(canvas.height, settings.height),
];
// 色を自動選択するためのパレット: クラスラベルを指定してすでにその色が割り当てられている場合はその色を出力し、
// まだ割り当てられていない場合は新しい色を割り当てる。
const pallet = {
_brewer_aired12: ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c',
'#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928'],
_classLabel_palletIdx: {},
color: function (class_label) {
const pallet_index = this._classLabel_palletIdx[class_label] ??
Object.keys(this._classLabel_palletIdx).length % this._brewer_aired12.length;
this._classLabel_palletIdx[class_label] = pallet_index;
return this._brewer_aired12[pallet_index];
},
};
// fpsの描画関数
const draw_fps = (function (elem, interval_sec) {
let frame_count = 0;
let updated_time = -1;
return function () {
frame_count += 1;
const t = Math.trunc(Date.now() / 1000);
if (t >= updated_time + interval_sec) {
elem.innerText = `${(frame_count / (t - updated_time)).toFixed(1)} fps`;
frame_count = 0;
updated_time = t;
}
};
})(document.getElementById("fps"), 2);
// htmlの描画関数
const render = async () => {
ctx.drawImage(video, ...drect);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
if (model === undefined) {
// ctx.fillStyle = "white";
// ctx.fillRect(0, 0, canvas.width, canvas.height);
// モデルファイルの読み込み中の場合
if (Math.trunc(Date.now() / 1000) % 2 === 0) {
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.font = "20px sans-serif";
ctx.fillText("loading yolox model", canvas.width / 2, canvas.height / 2);
}
requestAnimationFrame(render);
return;
}
// imageDataからtensorの元になるFloat32Arrayへのコピー
// チャネル数: imageData rgba4チャネル, float32 rgb3チャネル
// 要素の並び順: imageData rgba...のチャネルラストの[h,w,c], float32 rr..gg..bb のチャネルファースト[c,h,w]
const float32array = new Float32Array(canvas.width * canvas.height * 3);
for (let y = 0; y < canvas.height; y += 1) {
for (let x = 0; x < canvas.width; x += 1) {
const pi = y * canvas.width + x;
for (let i = 0; i < 3; i += 1) {
float32array[pi + i * canvas.width * canvas.height] = imageData.data[pi * 4 + i];
}
}
}
const tensor = new ort.Tensor("float32", float32array, [1, 3, canvas.height, canvas.width]);
const results = await model.run({ [model.inputNames?.[0]]: tensor });
ctx.font = "10px sans-serif";
ctx.textAlign = "left";
// 信頼度の高い bbox を抽出して、枠を描画する。
yolox_postprocess(results.modelOutput).forEach(obj => {
ctx.strokeStyle = pallet.color(obj.class);
ctx.strokeRect(...obj.bbox);
ctx.fillStyle = pallet.color(obj.class);
ctx.fillText(obj.class, obj.bbox[0], obj.bbox[1]);
});
// fps(frame per second)の描画
draw_fps();
requestAnimationFrame(render);
}
// yoloxの後処理: yoloxの出力はすべてのアンカーに紐づくbboxである。そこから信頼度の高いものだけをこの関数で抽出する。
// 引数のconf_threはオブジェクト検出の信頼度*クラス識別信頼度のしきい値で、これ以上の値を持つものだけが残される。
// nms_threは、隣り合うbboxが別のbboxとして扱われる最大のIoU値である。別ではないと扱われる場合、信頼度の低い方は削除される。
// IoUはbboxの重なり具合であり1に近いほど類似したbboxとなる。
// each_classは、trueにすると隣り合うbboxでもクラス値が違うと抽出対象になる。falseの場合はクラス値が違っていても信頼度の小さい隣り合うbboxは抽出されない。
const yolox_postprocess = (modelOutput, conf_thre = 0.7, nms_thre = 0.45, each_class = false) => {
const coco_class_labels = [
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "trafficlight",
"firehydrant", "stopsign", "parkingmeter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sportsball", "kite", "baseballbat", "baseballglove", "skateboard", "surfboard", "tennisracket", "bottle",
"wineglass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
"broccoli", "carrot", "hotdog", "pizza", "donut", "cake", "chair", "couch", "pottedplant", "bed",
"diningtable", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cellphone", "microwave", "oven",
"toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddybear", "hairdrier", "toothbrush",
];
// 出力候補数
const num_cands = modelOutput.dims[1];
const indices = [...Array(num_cands).keys()];
// 1つの出力候補の要素数
const num_vals = modelOutput.dims[2];
// max class score and index [[class pred,class label index],....]
const class_preds = indices.map(
i => modelOutput.data.slice(i * num_vals + 5, (i + 1) * num_vals).reduce((a, e, i) => a[0] >= e ? a : [e, i], [0, -1])
);
const class_obj_preds = class_preds.map(
(e, i) => ({
class_pred: e[0],
obj_pred: modelOutput.data[i * num_vals + 4],
class_index: e[1], class_obj_pred: modelOutput.data[i * num_vals + 4] * e[0]
})
);
const detected_indices = indices.filter(i => class_obj_preds[i].class_obj_pred >= conf_thre)
.sort((i0, i1) => class_obj_preds[i1].class_obj_pred - class_obj_preds[i0].class_obj_pred)
const _iou = (b0, b1) => {
const [c0, c1] = [b0, b1].map(b => [b[0] - b[2] / 2, b[1] - b[3] / 2, b[0] + b[2] / 2, b[1] + b[3] / 2]);
const intersect = Math.max(0, Math.min(c0[2], c1[2]) - Math.max(c0[0], c1[0])) *
Math.max(0, Math.min(c0[3], c1[3]) - Math.max(c0[1], c1[1]));
const union = [b0, b1].map(e => e[2] * e[3]).reduce((a, e) => a + e) - intersect;
return intersect / union;
}
const nms_out_indices = [];
let open_indices = [...detected_indices];
while (open_indices.length > 0) {
nms_out_indices.push(open_indices.shift());
const i0 = nms_out_indices[nms_out_indices.length - 1];
const cls_index0 = class_obj_preds[i0].class_index;
const bbox0 = modelOutput.data.slice(i0 * num_vals, i0 * num_vals + 4);
open_indices = open_indices.filter(i1 =>
(each_class && cls_index0 !== class_obj_preds[i1].class_index) ||
_iou(bbox0, modelOutput.data.slice(i1 * num_vals, i1 * num_vals + 4)) < nms_thre);
}
return nms_out_indices.map(i => ({
bbox: [
modelOutput.data[i * num_vals + 0] - modelOutput.data[i * num_vals + 2] / 2,
modelOutput.data[i * num_vals + 1] - modelOutput.data[i * num_vals + 3] / 2,
modelOutput.data[i * num_vals + 2],
modelOutput.data[i * num_vals + 3],
],
class: coco_class_labels?.[class_obj_preds[i].class_index],
}));
};
requestAnimationFrame(render);
</script>
</body>
</html>
6. Colaboratory上にWebサーバを立ち上げてスマートフォンでアクセスして動作確認する (publish.ipynb)
スマートフォンで先に示した index.html を読み込んで実行するために、Colaboratory上にWebサーバを立ち上げます。すべてのプログラムは、publish.ipynbファイルに記述されています。
作業手順としては次のようになります。
- PCでColaboratoryを使用して publish.ipynbファイルを作成、または本記事のダウンロードからダウンロードしたpublish.ipynbファイルをGoogle Driveのindex.html, yolox_nano_416_416.onnxファイルのあるフォルダにアップロードする。
- スマートフォンでpublish.ipynbを表示する。publish.ipynbのURLは、QRコードを作成してスマートフォンから読み込ませると簡単かと思います。
- スマートフォンでpublish.ipynbのすべてのセルを実行する。
- 最後のセルの出力をクリックして、index.htmlを開く。
publish.ipynbをダウンロードではなく作成する場合は、PCでの作業の方がやりやすいと思います。ただ実行は、スマートフォンで行ってください。以下に、これまで通りプログラムの簡単な説明をしていきます。
# google drive のマウント
current_folder = "/content/drive/MyDrive/Colab Notebooks/yolox"
from google.colab import drive
drive.mount('/content/drive')
%cd $current_folder
まず、これまでと同様にGoogle Driveに接続します。スマートフォンで実行しても、PCのときと操作方法は変わりません。
PORT = 8000
!nohup python3 -m http.server $PORT >/dev/null 2>&1 &
from google.colab import output
output.serve_kernel_port_as_window(PORT)
上記のプログラムは、まずpython3のhttp.serverモジュールをバックグラウンドで実行します。次にgoogle.colab.outputにあるserve_kernel_port_as_windowというコマンドによって指定されたポート番号を開放します。ポート番号は、webサーバを起動したポート番号です。
実行すると、https://localhost:8000/
というリンクが出力されます。実際にはlocalhostではないのですけど、このリンクによってGoogle Driveのindex.htmlファイルのあるフォルダにブラウザで接続することができます。
リンクをクリックするとindex.htmlがスマートフォンで読み込まれて、カメラの許可を求められます。許可し、表示するカメラを選択してください。上記のような画面が表示されます。初めて起動するときは、onnxのjavascriptランタイムやモデルファイルの読み込みに低回線の場合は4,5分の時間がかかります。
6.1. 実行速度
ROG Phone (ゲーミングスマホ ASUS社 2020年) の場合、約6fpsで物体検知を行うことができます。
iPhone 7 (2016年)の場合、ColaboratoryからWebサーバにアクセスしようとすると403のエラーとなります。原因は不明です。そこでindex.htmlとyolox_nano_416_416.onnxファイルを適当なWebサーバにアップして実行してみたところ、約1fpsぐらいでした。