NanoDetをWebGLで動かす

この記事は[UMITRON Advent Calendar 2022](https://adventar.org/calendars/7398) 8日目の記事です

[English page](https://qiita.com/hosonot/items/d1bd4f260ce59e1b9163)

# まえがき
ウミトロンでは物体検出を複数のプロダクトで活用しています。
アプリケーションの要件によっては、WebGLを使いブラウザで動作させることがあります。
今回は、比較的最近の物体検出アルゴリズムであるNanoDetをTensorFlow.jsを使いWebGLで動作させたので、その際に得た知見をまとめます。

# NanoDetについて
[NanoDet](https://github.com/RangiLyu/nanodet)は高速な物体検出アルゴリズムであり、いくつかのモデルが提供されています。
CPUで推論する場合、ShuffleNetV2をバックボーンとするNanoDet-Plusが精度及び速度の観点から良さそうです。
今回はGPUを使用したいので、Custom CSP NetをバックボーンとするNanoDet-gを使います。

実際に、NVIDIA Quadro P2000を使い処理時間を比較してみました。
40フレームの映像を使い、1フレームの処理にかかった平均時間を計測しました。
ネットワーク部分の推論時間だけでなく前処理や後処理も含めて時間を計測しています。
また、参考までに高速な物体検出手法の一つであるMobileNet SSDとも比較しています。
結果は以下の通りです。


|手法|処理時間[msec.]|
|---|---|
|MobileNet SSD | 106 |
|NanoDet-Puls| 203 |
|NanoDet-g |79|

結果から、GPUで推論する場合、NanoDet-PulsよりもNanoDet-gの方が倍以上高速なことがわかります。
また、MobileNet SSDと比べても1.3倍程度高速なことがわかります。

# モデルの学習
READMEの[How to Train](https://github.com/RangiLyu/nanodet#how-to-train)に従って学習します。
学習データのアノテーションはVOC XMLフォーマットとMS COCOフォーマットに対応しています。
NanoDet-gのデフォルトの設定ファイルは[こちら](https://github.com/RangiLyu/nanodet/blob/main/config/legacy_v0.x_configs/nanodet-g.yml)です。
設定ファイルを編集した後、以下のようにtrain.pyを実行することで学習します。
~~~
python tools/train.py [config file path]
~~~

# モデルの変換
CNNをWebGLで動作させる方法はいくつかありますが、NanoDetを組み込みたかったアプリケーションとの親和性の観点から、今回はTensorFlow.jsを採用しました。
NanoDetはPyTrochで実装されているため、学習済みモデルをTensorFlow.jsに変換する必要があります。
手順は以下のとおりです。

## 1. PyTrochからONNXに変換
[README](https://github.com/RangiLyu/nanodet#export-model-to-onnx)に記載されている通り、export_onnx.pyを使って変換できるのですが、ONNXのoperator setのバージョンを10に下げないと後のTensorFlowへの変換時にエラーがでるので、[export_onnx.pyのopset_version](https://github.com/RangiLyu/nanodet/blob/0f4d8f114431d80729c900c158708b7d70ee2384/tools/export_onnx.py#L56)を11から10に変更した後、以下のように変換します。

~~~
python tools/export_onnx.py --cfg_path config/legacy_v0.x_configs/nanodet-g.yml --model_path [network ckpt path]
~~~

## 2. ONNXからTensorFlowに変換
[onnx-tf](https://pypi.org/project/onnx-tf/)を使って変換します。

~~~
onnx-tf convert -i [onnx file path] -o [output TensorFlow model directory]
~~~


## 3. TensorFlowからTensorFlow.jsに変換
[tensorflowjs](https://pypi.org/project/tensorflowjs/)に含まれるtensorflowjs_converterを使って変換します。
このとき、TensorFlow.jsがint64に対応していないことによるオーバーフローを防ぐために、int32にキャストする必要があります（[参考](https://github.com/vladmandic/nanodet)）。
~~~
tensorflowjs_converter --input_format tf_saved_model --output_format tfjs_graph_model --strip_debug_ops=* --weight_shard_size_bytes 18388608 [input TensorFlow model directory] [output TensorFlow.js model directory]
~~~

# TensofFlow.jsでの推論
上記手順で変換されたモデルは、リサイズ及び正規化された画像を入力とし、クラス尤度と矩形位置が混ざったテンソルを出力します。
そのため、前処理と後処理は別途実装する必要があります。
参考までに、著者らによる前処理と後処理の実装箇所を記載しておきます。

## 前処理: 画像の正規化
BGRの画像を平均と分散で正規化しています（[該当箇所](https://github.com/RangiLyu/nanodet/blob/480ee5ae83341e09f3e3e7939dfd13d8e301a2cc/nanodet/data/transform/color.py#L38)）。
画像サイズはNanoDet-gであれば416x416です。

## 後処理: 矩形の位置とクラス尤度を計算
NanoDetは[YOLO](https://arxiv.org/abs/1506.02640)の様に画像をグリッドセルに分割し推定を行います。
そのため、出力は各グリッドセルにおけるクラス尤度と矩形位置になり、テンソルの形状は
[グリッドセル数, クラス数 + 出力矩形数 * 4]
となります。

[こちらの関数](https://github.com/RangiLyu/nanodet/blob/dcc7e798adbb9e64c406173a0792b934dbd76497/nanodet/model/head/nanodet_plus_head.py#L364)でこのテンソルを各クラスの矩形に変換しているのですが、テンソルの中身がもう少し分かっていた方が実装を追いやすいと思うので、以下で簡単に説明します。

NanoDetはマルチスケールでグリッドセルに分割しており、グリッドセル数は学習時に指定された入力画像の解像度と各スケールのグリッドセルの分割数に依存します。
NanoDet-gの場合、入力画像の解像度は[cofigのinput_size](https://github.com/RangiLyu/nanodet/blob/main/config/legacy_v0.x_configs/nanodet-g.yml#L58)、グリッドセルの分割数は[configのstrides](https://github.com/RangiLyu/nanodet/blob/main/config/legacy_v0.x_configs/nanodet-g.yml#L37)で指定できます。
デフォルトの設定では、各スケールのグリッドセル数は(416 / 8)^2 = 2704、(416 / 16)^2 = 676、(416 / 32)^2 = 169、総数は3549となります。
また、クラス数は[configのnum_classes](https://github.com/RangiLyu/nanodet/blob/main/config/legacy_v0.x_configs/nanodet-g.yml#L28)、矩形出力数は[configのreg_max](https://github.com/RangiLyu/nanodet/blob/main/config/legacy_v0.x_configs/nanodet-g.yml#L38)で指定できます。reg_max + 1個の矩形が出力されるので注意してください。
矩形の位置は、各グリッドセルの中心から矩形の左端、上端、右端、下端までの距離が尤度としてそれぞれ格納されています。
NanoDet-Plusの場合、中心位置ではなくグリッドセルの左上からの距離になっているので注意してください。
そのため、後処理で使われているget_single_level_center_point()は[NanoDet-Plusのもの](https://github.com/RangiLyu/nanodet/blob/dcc7e798adbb9e64c406173a0792b934dbd76497/nanodet/model/head/nanodet_plus_head.py#L478)ではなく[NanoDetのもの](https://github.com/RangiLyu/nanodet/blob/dcc7e798adbb9e64c406173a0792b934dbd76497/nanodet/model/head/gfl_head.py#L637)を参考にする必要があります。

# 余談
NanoDetと同様に高速な物体検出アルゴリズムである[PicoDet](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.5/configs/picodet)も候補に挙がりました。
ONNXにエクスポートする際、後処理もモデルに含めてくれるオプションがあるので実装コストは下がるのですが、ONNX Runtime Webで動作させる際に一部のオペレーターがサポートされておらず断念しました(ONNXからTensoFlowに変換する場合は後処理を含めることはできませんでした)。
ONNX Runtime Webでのオペレーターのサポート状況は[こちら](https://github.com/microsoft/onnxruntime/blob/main/js/web/docs/webgl-operators.md)で確認できます。
現在は少なくともHardSigmoidが未対応ですが、将来的に利用できるようになるかもしれません。

また、PicoDetのバックボーンはEnhance ShuffleNetというShuffleNteV2を改良したものなので、GPUで推論する場合、CPUでの推論時間とあまり差が無いかもしれません。

---
ウミトロンでは一緒に働く仲間を募集しております。持続可能な水産養殖を地球に実装するというミッションの元で、私たちと一緒に水産養殖xテクノロジーに取り組みませんか？

https://umitron.com/ja/career.html