はじめに
Tensorflow.jsを使って、ブラウザで任意の物体検出がしたい!
見事大ハマリしたので、同じエラーで苦しんでいる人を解決方向に導けたら。
後の自分への備忘録も兼ねて。
概要
Tensorflow.jsを使って最終的に有名巨大像(牛久大仏、鎌倉大仏、奈良大仏、高崎観音、自由の女神)をブラウザでリアルタイム検出するモデルを作成した。
その過程を2回に分けて記事にする。
大きくTensorflowのObject Detection APIを使ってモデルを学習する部分と、そのモデルをTensorflow.jsで使えるように変換、ブラウザで読み取れるようにする2ステップだ。
今回は後編の 【Tensorflow.jsを使ってブラウザで物体検出編】 である。
準備
SavedModel形式のモデルデータ
前編で作った自作のモデルデータ。無い人は前編へGO!
手順
1. Tensorflow.jsのインストール
ブラウザで物体検出をするには、JavaScriptで動かせるTensorflowであるTensorflow.jsが必要。
バージョンについては紆余曲折したが、モデルをTensorflow2.9.1で作ったので最新バージョン(2022年7月現在:3.18.0)で構わない。
以下をターミナルでコマンド実行する。
pip install tensorflowjs
2. SavedModelをTensorflow.jsで使える形式に変換する
SavedModelはそのままではTensorflow.jsで使えない。そのため、上記でインストールしたtensorflowjsに付随してくるtensorflowjs_converterでSavedModelを変換する必要がある。
変換は以下のコマンドを実行。以下はデスクトップで展開しているがパスは任意。
tensorflowjs_converter --input_format=tf_saved_model \
/Users/username/Desktop/saved_model \
/Users/username/Desktop/tfjs_model
オプションの --input_format=tf_saved_model
はSavedModel形式のモデルを変換するという意味。
他にもKeras modelやTensorFlow Hub モジュール形式の変換が可能。
あとはsaved_model.pb
が入っているフォルダのパスと書き出し先のフォルダのパスを指定すればよい。
(上記ではtfjs_model
フォルダが作成されてその中に変換されたモデルが書き出される。)
3. TFJS-object-detectionのクローン
ここからはこちらの記事をもとに解説していく。
まずは配布されているTFJS-object-detectionをクローンするか、ダウンロードする。
この記事ではTensorflow.jsで、「ウェブカメラを使ったブラウザ上でのリアルタイムカンガルー検出器」を作っている。
フォルダには既にカンガルーのモデルがセットされているのでREADMEと記事を参考に一通りカンガルー検出器を試してみるとこれからの流れが分かりやすくなると思う。
記事はデータセットの作り方から解説しているので、飛ばして Configuring the application から参照するとよい。
4. 自作モデルを使ったTFJS-object-detectionの改造
クローンできたら今度は、これをカンガルーではなく自作のモデルで物体検出するよう改造していく。
自作モデルフォルダをセット
まずはクローンしてきたフォルダに自作モデルをセットする。
クローンしてきたフォルダの構造は以下のようになっている。
TFJS-object-detection
├── models
│ └── kangaroo-detector
│ ├── group1-shard1of5.bin
│ ├── group1-shard2of5.bin
│ ├── group1-shard3of5.bin
│ ├── group1-shard4of5.bin
│ ├── group1-shard5of5.bin
│ └── model.json
├── package.json
├── package-lock.json
├── public
│ └── index.html
├── README.MD
└── src
├── index.js
└── styles.css
この中のmodels
フォルダの中に手順2.で変換したモデルフォルダ(tfjs_model
フォルダ)を入れる。
パッケージのインストール・アップデート
TFJS-object-detection
フォルダに必要なパッケージはnpmで管理されている。
そのため、まずは必要なパッケージをnpmでインストールする必要があるので以下のコマンドを TFJS-object-detection
フォルダ内 で実行する。
npm install
次に今インストールされたパッケージのバージョンを以下のコマンドで確認すると
npm outdated
Package Current Wanted Latest Location Depended by
@tensorflow/tfjs 1.7.4 1.7.4 3.18.0 node_modules/@tensorflow/tfjs TFJS-object-detection
@tensorflow/tfjs-converter 1.7.4 1.7.4 3.18.0 node_modules/@tensorflow/tfjs-converter TFJS-object-detection
@tensorflow/tfjs-core 1.7.4 1.7.4 3.18.0 node_modules/@tensorflow/tfjs-core TFJS-object-detection
@tensorflow/tfjs-node 1.7.4 1.7.4 3.18.0 node_modules/@tensorflow/tfjs-node TFJS-object-detection
react 16.5.2 16.5.2 18.2.0 node_modules/react TFJS-object-detection
react-dom 16.5.2 16.5.2 18.2.0 node_modules/react-dom TFJS-object-detection
react-scripts 2.0.3 2.0.3 5.0.1 node_modules/react-scripts TFJS-object-detection
現在インストールされているtfjs(Tensorflow.js)系のパッケージがかなり古いことが分かる。
このまま下記の工程を進めブラウザで物体検出をしようとすると、自作モデルを作るのに使った新しいTensorflow / Tensorflow.jsのバージョンと合わず以下のようなエラーに苦しめられることになる。
Unhandled Rejection (TypeError): Unknown op 'TensorListReserve'. File an issue at https://github.com/tensorflow/tfjs/issues so we can add it, or register a custom execution with tf.registerOp()
そこでこれらをアップデートする。
ここでは、バージョンを管理しているpackage.json
も一緒に更新してしまいたいのでnpm-check-updates
を使うことにした。
以下の一連のコマンドを実行する。
まずはnpm-check-updates
をインストール。
npm install -g npm-check-updates
次にpackage.json
をアップデート。
npm-check-updates -u
最後にパッケージをアップデート。
npm update
これで自作モデルがこのコードで使えるようになる。
index.jsの書き換え
次にモデルを読み込んだり、Tensorflow.jsから結果を受け取って結果を描画したりするindex.js
を書き換えていく。
ここが今回1番肝になる部分。注意点がいくつかあるので順に解説していく。
- classesDirの書き換え
まずはカンガルー検出用になっているクラス名を書き換える。
クラス名はtf_label_map.pbtxt
を参照する。
像の検出では以下のようになった。各々自分の検出したいクラス名で書き換えてほしい。
/*
let classesDir = {
1: {
name: 'Kangaroo',
id: 1,
},
2: {
name: 'Other',
id: 2,
}
}
*/
let classesDir = {
1: {
name: 'kamakura',
id: 1,
},
2: {
name: 'liberty',
id: 2,
},
3: {
name: 'nara',
id: 3,
},
4: {
name: 'takasaki',
id: 4,
},
5: {
name: 'ushiku',
id: 5,
}
}
- モデルの読み込み場所の切り替え
デフォルトではindex.js
はカンガルーのモデルをGithubのURLで読み込みに行っている。
親切なことにローカルサーバーを立ててそこにモデルを置く方法もコードにコメントアウトされて残されているので、今回はこちらの方法で自作モデルを読み込ませる。
14行目をコメントアウトし、13行目のコメントアウトを外す。
async function load_model() {
// It's possible to load the model locally or from a repo
// You can choose whatever IP and PORT you want in the "http://127.0.0.1:8080/model.json" just set it before in your https server
const model = await loadGraphModel("http://127.0.0.1:8080/model.json");
//const model = await loadGraphModel("https://raw.githubusercontent.com/hugozanini/TFJS-object-detection/master/models/kangaroo-detector/model.json");
return model;
}
- Tensorflow.jsからの予測データの観察
ここが1番大切かつ、私が大・大・大ハマりしたところである。
まずは以下のTensorflow.jsから予測データを受け取っている場所を見てほしい。
const boxes = predictions[4].arraySync();
const scores = predictions[5].arraySync();
const classes = predictions[6].dataSync();
predictions
とはTensorflow.jsが推論してできたテンソルオブジェクトであり、様々な予測値が配列のようにまとめられている。
テンソルオブジェクトは特殊で、dataSync()
やarraySync()
などを使うことによってその中身が取り出せる。
このコードではboxes
は物体の位置を示すバウンディングボックスの座標、scores
は検出された物体がどのくらい正しくその物体と認識されているかのスコア、classes
はそれがどの物体なのか識別するID値をそれぞれテンソルオブジェクトから受け取っている。このコードではこのデータを使って検出した物体の周りにバウンディングボックスと呼ばれる枠を描いている。
変数名 | 予測値の内容 | 数値 |
---|---|---|
boxes |
バウンディングボックスの座標 | 4つの値 |
scores |
物体検出スコア | 0.0~1.0の少数値(0~100%) |
classes |
物体識別ID(タグの数) | (記事のモデルの場合)1~5の整数値 |
predictions
の中身を見たいので、上記の3つの変数の記述の下にconsole.logを並べる。
predictions
の長さは8なので8つ並べた。(predictions自体をコンソール出力すると長さは確認できる。)
const boxes = predictions[4].arraySync();
const scores = predictions[5].arraySync();
const classes = predictions[6].dataSync();
// 以下追記
const tensor_0 = predictions[0].arraySync();
const tensor_1 = predictions[1].arraySync();
const tensor_2 = predictions[2].arraySync();
const tensor_3 = predictions[3].arraySync();
const tensor_4 = predictions[4].arraySync();
const tensor_5 = predictions[5].arraySync();
const tensor_6 = predictions[6].arraySync();
const tensor_7 = predictions[7].arraySync();
console.log(tensor_0);
console.log(tensor_1);
console.log(tensor_2);
console.log(tensor_3);
console.log(tensor_4);
console.log(tensor_5);
console.log(tensor_6);
console.log(tensor_7);
ログを作ったらindex.js
を保存し、そのまま次は物体検出器の実行に移る。
GithubのREADMEの通りにまずはhttp-server
を用意する。
TFJS-object-detection
フォルダ内から以下のコマンドを実行する。
npm install http-server -g
次にターミナルをもう1つ立ち上げてtfjs_model
フォルダ内に移動、以下のコマンドでローカルサーバーを立ち上げる。
これで自作モデルにアクセスできるようになる。
http-server -c1 --cors .
元のターミナルに戻り、以下のコマンドを実行するとブラウザで物体検出器が立ち上がる。
npm start
ここで物体検出器がうまく動けば良いのだが…そうは問屋がおろさない。
おそらくカメラ映像などは映るが、物体検出は全くできないはずだ。
運良く動いている場合もあるかもしれないが、それでもこの先は読んでほしい。
動いていない場合、ブラウザのコンソールを覗くと以下のようなエラーが出ているはずだ。
(実は運が悪いとエラーも出ないので原因究明が出来なくなる。)
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'name')
エラーが起きている場所を見ると
buildDetectedObjects(scores, threshold, boxes, classes, classesDir) {
const detectionObjects = []
var video_frame = document.getElementById('frame');
scores[0].forEach((score, i) => {
if (score > threshold) {
const bbox = [];
const minY = boxes[0][i][0] * video_frame.offsetHeight;
const minX = boxes[0][i][1] * video_frame.offsetWidth;
const maxY = boxes[0][i][2] * video_frame.offsetHeight;
const maxX = boxes[0][i][3] * video_frame.offsetWidth;
bbox[0] = minX;
bbox[1] = minY;
bbox[2] = maxX - minX;
bbox[3] = maxY - minY;
detectionObjects.push({
class: classes[i],
label: classesDir[classes[i]].name, // この行でエラーが起きている!!!
score: score.toFixed(4),
bbox: bbox
})
}
})
return detectionObjects
}
この関数ではdetectionObjects
という検出された物体の情報を入れるための配列を作り、そこに一定以上のscore
だと情報を詰めていることが分かる。
どうやら検出したクラスのラベル(物体の名前)を配列に書き込むところでエラーが起きているようだ。
エラー内容は「未定義の属性の読み取り」。
しかし、classesDir
のname
は存在するので、どうやら怪しいのはインデックスのclasses[i]
ではないかと絞り込む。
classes
は最初に見たとおり、predictions
の中身を受け取っている。
const boxes = predictions[4].arraySync();
const scores = predictions[5].arraySync();
const classes = predictions[6].dataSync(); // これ
ここで、先程埋め込んだログを読んでいくのでブラウザの検証画面に移動。
classes
はpredictions[6]
なので、7番目のログを見てみる。すると、
なんだか物凄い数の2次元配列が入っている…。
classes
が2次元配列ではclassesDir[classes[i]].name
が未定義になってしまうのも当然だ。
何故このようなことになっているのか。
実は、後に解説するがこれを実行する人全てのclasses
が上の画像のように2次元配列を示していることはない。
ここで、predictions
のログを全部見ていく。画像は全部載せると大きくなりすぎるので適宜カットしてある。
predictions[0]
長さ100の配列。1以下の正の少数が入っている模様。
predictions[1]
51150行4列の2次元配列。-1~1間の少数が入っている模様。
predictions[2]
100行4列の2次元配列。0~1間の数が入っている模様。
predictions[3]
長さ1の配列。中には100が入っている。
predictions[4]
長さ100の配列。4~5桁の比較的大きい整数が入っている模様。
predictions[5]
長さ100の配列。1~5の整数が入っている模様。
predictions[6]
51150行6列の2次元配列。0.01以下の比較的小さい少数が入っている模様。
predictions[7]
100行6列の2次元配列。0.1以下の少数が入っている模様。
ここで先程のバウンディングボックスを描画するために必要な3つの値と照らし合わせてみる。
もともと以下のような対応だったが、中の値を見るとclasses
同様他の2つも入っているデータが違うように思える。
const boxes = predictions[4].arraySync();
const scores = predictions[5].arraySync();
const classes = predictions[6].dataSync();
そこで、predictions
のインデックスとそれぞれの変数の対応がおかしいと考える。
まず考えやすいのはclasses
。おそらく1~5の整数値が入っているpredictions[5]
と思われる。
次にscores
。0%~100%なので1以下の正の少数が入っているpredictions[0]
だろう。
最後にboxes
だが、x, y, w, hの4つの値が入っていると考えられる。それに合致しそうなのは4列のpredictions[1]
かpredictions[2]
だが、classes
とscores
の長さが100であることを考えるとpredictions[2]
に絞られる。
よって、今回は以下のようになると思われる。
変数名 | 予測値の内容 | 数値 | どのpredictionか |
---|---|---|---|
boxes |
バウンディングボックスの座標 | 4つの値 | predictions[2] |
scores |
物体検出スコア | 0.0~1.0の少数値 | predictions[0] |
classes |
物体識別ID(タグの数) | (記事のモデルの場合)1~5の整数値 | predictions[5] |
この結果を元にコードを書き換える。
// const boxes = predictions[4].arraySync();
// const scores = predictions[5].arraySync();
// const classes = predictions[6].dataSync();
const boxes = predictions[2].arraySync();
const scores = predictions[0].arraySync();
const classes = predictions[5].dataSync();
今回は、と強調したのには理由がある。
実はこのpredictions
、作ったモデルによって中の順番が全く違うのである。
上でclasses
が必ず2次元配列になるわけではない、と述べたのもこれが理由だ。
自分自身この記事を書くまでに何個かモデルを作り表示してきたのだが、モデルを変える度に受け取る順番が違っていてとんでもなく驚いた。
この記事を見ながら自作モデルで作った人は当然順番が異なると思われるので、ログを見ながら当てはめてほしい。
実行
手順4.の途中で実行した手順と一緒。
ターミナルを閉じてしまった人向けにさらっとおさらい。
2つターミナルを起動し、1つは自作モデルフォルダ内(tfjs_model
)に移動して以下のコマンドを実行。
http-server -c1 --cors .
もう1つはTFJS-object-detection
フォルダ内に移動して以下のコマンドを実行。
npm start
これにて終了!!!
まとめ(という名の大ハマり解説)
とんでもない罠が2個も仕掛けられていた…
まず、バージョンの齟齬だが、ありがちなバグであるもののエラーの内容が意味不明すぎて「Opってなんじゃ…」と数日間迷子。もうこのTFJS-object-detectionではできないのではと様々なチュートリアルや記事を片っ端から試したものの上手くいかず、結局ここに戻る羽目に。0→1→0→1→0の繰り返しはだいぶ辛いものがあった。
しかし魔王はその先に待っていた。
テンソルオブジェクトの中身はモデルごとに順番が違う…?!
最初、運悪くエラーも吐かず検出もしてくれず途方にくれたが、モデルを取り替えてみたところ怪しいundefinedエラーが出現。(さらっと1行で書いているがここに辿り着くにもかなりの時間が…)
そのエラーの意味にも翻弄されたが、ようやく順番が違うことに気づけたときは思わず社内でガッツポーズしてしまった。
結局順番の違いについて法則であったり、何かしら値を引っ張るためのタグのようなものがあるのではと思ったが見当たらないので、暫定的にログを観察するという手法に落ち着いた。画期的な解決方法求む。
この記事でどこかの誰かが少しでも救われれば何よりだ。
参考記事・サイト
この他にも数多くの記事やサイトを参考にしましたが、主に参考にした代表的なものを以下に紹介します。
過去の知恵には頭が上がりません。
TFJS-object-detection
このカンガルー検出器が無ければ1から作ることになり、ちんぷんかんぷんだったと思います。
きっとこの記事も存在しません。
とても分かりやすい解説記事とコード、感謝してもしきれません。ありがとうございました。
・ Custom object detection in the browser using TensorFlow.js
・ hugozanini / TFJS-object-detection
tfjs-converter周り
・ 【tensorflowjs_converter】TensorflowのモデルをTensorflow.jsの形式へ変換する方法
npm周り
・ 【初心者向け】NPMとpackage.jsonを概念的に理解する
・ package.json に記載されているパッケージのバージョンアップ方法 【 npm-check-updates, outdated 】
・ npmのpackage.jsonを最新のバージョンに更新する
Tensorflow.js仕様周り
・ テンソルと演算
・ Tensors
・ ディープラーニングのお勉強~その13。TensorFlow.jsでMNISTリアルタイム推論してみる
Tensorflow.jsエラー周り
・ Uncaught (in promise) Error: TensorList shape mismatch: Shapes -1 and must match
・ Unhandled Rejection (TypeError): Unknown op 'TensorListFromTensor'
JavaScriptエラー周り
・ JavaScriptで「Cannot read property ‘プロパティ名’ of undefined」エラーが出る原因と対処法を現役エンジニアが解説【初心者向け】