LoginSignup
3
7

More than 1 year has passed since last update.

Tensorflow.jsを使ってブラウザで任意の物体検出がしたい2022【Tensorflow.jsを使ってブラウザで物体検出編】

Posted at

はじめに

Tensorflow.jsを使って、ブラウザで任意の物体検出がしたい!
見事大ハマリしたので、同じエラーで苦しんでいる人を解決方向に導けたら。
後の自分への備忘録も兼ねて。

概要

Tensorflow.jsを使って最終的に有名巨大像(牛久大仏、鎌倉大仏、奈良大仏、高崎観音、自由の女神)をブラウザでリアルタイム検出するモデルを作成した。
その過程を2回に分けて記事にする。
大きくTensorflowのObject Detection APIを使ってモデルを学習する部分と、そのモデルをTensorflow.jsで使えるように変換、ブラウザで読み取れるようにする2ステップだ。
今回は後編の 【Tensorflow.jsを使ってブラウザで物体検出編】 である。
スクリーンショット 2022-07-19 19.38.01.png

準備

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()

スクリーンショット 2022-07-15 15.06.31.png

そこでこれらをアップデートする。
ここでは、バージョンを管理している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を参照する。
像の検出では以下のようになった。各々自分の検出したいクラス名で書き換えてほしい。

index.js
/*
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行目のコメントアウトを外す。

index.js
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から予測データを受け取っている場所を見てほしい。

index.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自体をコンソール出力すると長さは確認できる。)

index.js
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')

エラーが起きている場所を見ると

index.js
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だと情報を詰めていることが分かる。
どうやら検出したクラスのラベル(物体の名前)を配列に書き込むところでエラーが起きているようだ。
エラー内容は「未定義の属性の読み取り」。
しかし、classesDirnameは存在するので、どうやら怪しいのはインデックスのclasses[i]ではないかと絞り込む。
classesは最初に見たとおり、predictionsの中身を受け取っている。

index.js
const boxes = predictions[4].arraySync();
const scores = predictions[5].arraySync();
const classes = predictions[6].dataSync(); // これ

ここで、先程埋め込んだログを読んでいくのでブラウザの検証画面に移動。
classespredictions[6]なので、7番目のログを見てみる。すると、
スクリーンショット 2022-07-15 21.21.57.png
なんだか物凄い数の2次元配列が入っている…。
classesが2次元配列ではclassesDir[classes[i]].nameが未定義になってしまうのも当然だ。

何故このようなことになっているのか。
実は、後に解説するがこれを実行する人全てのclassesが上の画像のように2次元配列を示していることはない。
ここで、predictionsのログを全部見ていく。画像は全部載せると大きくなりすぎるので適宜カットしてある。

predictions[0]
長さ100の配列。1以下の正の少数が入っている模様。
prediction0.png

predictions[1]
51150行4列の2次元配列。-1~1間の少数が入っている模様。
prediction1.png

predictions[2]
100行4列の2次元配列。0~1間の数が入っている模様。
prediction2.png

predictions[3]
長さ1の配列。中には100が入っている。
prediction3.png

predictions[4]
長さ100の配列。4~5桁の比較的大きい整数が入っている模様。
prediction4.png

predictions[5]
長さ100の配列。1~5の整数が入っている模様。
prediction5.png

predictions[6]
51150行6列の2次元配列。0.01以下の比較的小さい少数が入っている模様。
prediction5.png

predictions[7]
100行6列の2次元配列。0.1以下の少数が入っている模様。
prediction7.png

ここで先程のバウンディングボックスを描画するために必要な3つの値と照らし合わせてみる。
もともと以下のような対応だったが、中の値を見るとclasses同様他の2つも入っているデータが違うように思える。

index.js
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]だが、classesscoresの長さが100であることを考えるとpredictions[2]に絞られる。
よって、今回は以下のようになると思われる。

変数名 予測値の内容 数値 どのpredictionか
boxes バウンディングボックスの座標 4つの値 predictions[2]
scores 物体検出スコア 0.0~1.0の少数値 predictions[0]
classes 物体識別ID(タグの数)       (記事のモデルの場合)1~5の整数値 predictions[5]

この結果を元にコードを書き換える。

index.js
// 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
スクリーンショット 2022-07-19 17.49.16.png

これにて終了!!!

まとめ(という名の大ハマり解説)

とんでもない罠が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」エラーが出る原因と対処法を現役エンジニアが解説【初心者向け】

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7