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

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

# 概要
Tensorflow.jsを使って最終的に有名巨大像(牛久大仏、鎌倉大仏、奈良大仏、高崎観音、自由の女神)をブラウザでリアルタイム検出するモデルを作成した。
その過程を2回に分けて記事にする。
大きくTensorflowのObject Detection APIを使ってモデルを学習する部分と、そのモデルをTensorflow.jsで使えるように変換、ブラウザで読み取れるようにする2ステップだ。
今回は後編の **【Tensorflow.jsを使ってブラウザで物体検出編】** である。
<img width="600" alt="スクリーンショット 2022-07-19 19.38.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/d7730007-8fbc-2fcd-d852-786cdd275b80.png">

# 準備
#### SavedModel形式のモデルデータ
前編で作った自作のモデルデータ。無い人は前編へ[GO！](https://qiita.com/manae/items/7579de933b7fc054b067)

# 手順
## 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のクローン
ここからはこちらの記事をもとに解説していく。

https://blog.tensorflow.org/2021/01/custom-object-detection-in-browser.html

まずは配布されている[TFJS-object-detection](https://github.com/hugozanini/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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/0ede8a39-99bf-61d5-18f7-d05906a86cb2.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`を参照する。
像の検出では以下のようになった。各々自分の検出したいクラス名で書き換えてほしい。
```javascript: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行目のコメントアウトを外す。
```javascript: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から予測データを受け取っている場所を見てほしい。

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

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

ここで、先程埋め込んだログを読んでいくのでブラウザの検証画面に移動。
`classes`は`predictions[6]`なので、7番目のログを見てみる。すると、
<img width="700" alt="スクリーンショット 2022-07-15 21.21.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/9e9c293f-6a41-ab93-3362-31b922e3b770.png">
なんだか物凄い数の2次元配列が入っている…。
`classes`が2次元配列では`classesDir[classes[i]].name`が未定義になってしまうのも当然だ。

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

**predictions[0]**
長さ100の配列。1以下の正の少数が入っている模様。
<img width="700" alt="prediction0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/90c34938-2aa8-133d-d5b5-52b8ded8537b.png">

**predictions[1]**
51150行4列の2次元配列。-1~1間の少数が入っている模様。
<img width="700" alt="prediction1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/98e6dabd-e0f7-94dd-ce7e-40cbbac9ca14.png">

**predictions[2]**
100行4列の2次元配列。0~1間の数が入っている模様。
<img width="700" alt="prediction2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/65d36b31-f552-dc7c-c96f-4ee963162fde.png">

**predictions[3]**
長さ1の配列。中には100が入っている。
<img width="700" alt="prediction3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/c7f57f28-0226-c128-300a-59a64bee41fb.png">

**predictions[4]**
長さ100の配列。4~5桁の比較的大きい整数が入っている模様。
<img width="700" alt="prediction4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/85b37e0c-d76f-a225-884e-d675e4c1d49c.png">

**predictions[5]**
長さ100の配列。1~5の整数が入っている模様。
<img width="700" alt="prediction5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/2d3d493a-3032-a577-9db1-21f247d81330.png">

**predictions[6]**
51150行6列の2次元配列。0.01以下の比較的小さい少数が入っている模様。
<img width="700" alt="prediction5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/3989f6b7-8a74-df24-09eb-359f7e89f671.png">

**predictions[7]**
100行6列の2次元配列。0.1以下の少数が入っている模様。
<img width="700" alt="prediction7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/59432796-1121-6ec2-52d9-c35ac8b08543.png">


ここで先程のバウンディングボックスを描画するために必要な3つの値と照らし合わせてみる。
もともと以下のような対応だったが、中の値を見ると`classes`同様他の2つも入っているデータが違うように思える。
```javascript: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]`だが、`classes`と`scores`の長さが100であることを考えると`predictions[2]`に絞られる。
よって、<font color=red>**今回は**</font>以下のようになると思われる。

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

この結果を元にコードを書き換える。
```javascript: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
```
<img width="700" alt="スクリーンショット 2022-07-19 17.49.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616335/f7e5d8f6-54b0-a959-60dc-5869d499f66f.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](https://blog.tensorflow.org/2021/01/custom-object-detection-in-browser.html)
・ [hugozanini / TFJS-object-detection](https://github.com/hugozanini/TFJS-object-detection)

### tfjs-converter周り
・ [【tensorflowjs_converter】TensorflowのモデルをTensorflow.jsの形式へ変換する方法](https://qiita.com/kwashi/items/12d2dfe653699ccfe3f4)

### npm周り
・ [【初心者向け】NPMとpackage.jsonを概念的に理解する](https://qiita.com/righteous/items/e5448cb2e7e11ab7d477)
・ [package.json に記載されているパッケージのバージョンアップ方法 【 npm-check-updates, outdated 】](https://qiita.com/sugurutakahashi12345/items/df736ddaf65c244e1b4f)
・ [npmのpackage.jsonを最新のバージョンに更新する](https://omachizura.com/2016/02/npm-package-new.html)

### Tensorflow.js仕様周り
・ [テンソルと演算](https://www.tensorflow.org/js/guide/tensors_operations?hl=ja)
・ [Tensors](https://js.tensorflow.org/api/latest/)
・ [ディープラーニングのお勉強～その13。TensorFlow.jsでMNISTリアルタイム推論してみる](https://www.mgo-tec.com/blog-entry-tensorflow-js-m5cam01.html)

### Tensorflow.jsエラー周り
・ [Uncaught (in promise) Error: TensorList shape mismatch: Shapes -1 and must match](https://stackoverflow.com/questions/71822165/uncaught-in-promise-error-tensorlist-shape-mismatch-shapes-1-and-must-match)
・ [Unhandled Rejection (TypeError): Unknown op 'TensorListFromTensor'](https://github.com/tensorflow/tfjs/issues/4638)

### JavaScriptエラー周り
・ [JavaScriptで「Cannot read property ‘プロパティ名’ of undefined」エラーが出る原因と対処法を現役エンジニアが解説【初心者向け】](https://techacademy.jp/magazine/26836)