高位合成によるFPGA回路設計を読んで、YOLOv3を試してみた3

## 前回までの経緯
YOLOv3をDPUで実行した場合と、CPUで実行した場合とで物体検出の結果を比較したところ、DPUの場合は一部のバウンディングボックスが検出できていないなど、若干の推論精度の低下が見られた。定量的に評価したら、どの程度の差が見られるんだろうか？と気になり、COCO datasetでmAPを評価してみた。

前回のリンク
[高位合成によるFPGA回路設計を読んで、YOLOv3を試してみた1](https://qiita.com/noyary155/items/3519460c1bed5863920b)
[高位合成によるFPGA回路設計を読んで、YOLOv3を試してみた2](https://qiita.com/noyary155/items/ba1b7746fa845fc4323f)

## COCO dataset 2017 Val images
高位合成によるFPGA回路設計のサンプルプログラムで使用されていた画像が、COCO datasetから一部抜粋されたものだったので、同じCOCO datasetから、ちょうどそれなりの画像枚数がある、2017 validation datasetを使うことにしました。

https://cocodataset.org/#download
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413488/f458203e-b8cb-a030-7293-0cbda866de0b.png)

## mAP (mean Average Precision)の評価方法
まさにやりたいことそのものを実現してもらっているプログラムがあったので、こちらを使わせてもらうことにしました。

[https://github.com/Cartucho/mAP](https://github.com/Cartucho/mAP)

## Data formatの変換
上記のプログラムでは、正解データと予測データのフォーマットはそれぞれ次のようになっているらしい。
COCO datasetと、DPUでの予測データをこの専用フォーマットに変換する必要がある。

**ground-truth files (正解データ)**
```text
tvmonitor 2 10 173 238
book 439 157 556 241
book 437 246 518 351 difficult
pottedplant 272 190 316 259
```

**detection-result files (予測データ)**
```text
tvmonitor 0.471781 0 13 174 244
cup 0.414941 274 226 301 265
book 0.460851 429 219 528 247
chair 0.292345 0 199 88 436
book 0.269833 433 260 506 336
```

### ground-truthデータの準備
COCOのAnotation dataはJson形式になっており、だいぶフォーマットが異なる。どうやって変換したものかと、調べてみたところ、mAP評価プログラムのリポジトリで、xml/YOLO/keras-yolo3のいずれかから、専用フォーマットに変換するスクリプトが準備されていた。ということは、COCOからYOLO/kera-yolo3に変換できれば、なんとかなりそうだ。

こちらで、まさに求めていたものが紹介されていたので、ありがたく使わせていただくことにしました。
[COCO FormatをYolo v3 Formatへ変換する](https://qiita.com/harmegiddo/items/d4387ae5aba8f9bcd354)
[https://github.com/ssaru/convert2Yolo](https://github.com/ssaru/convert2Yolo)

変換方法は、基本的にはリポジトリのREADMEに従って実施していくだけなんですが、READMEで指定されているdarknetのclassファイルを使用すると、coco datasetのclass名と一部齟齬があるようで、変換エラーに、、、

```shell
python3 example.py --datasets COCO --img_path ./COCO/val2017/ --label ./COCO/annotations/instances_val2017.json --convert_output_path ~/convert2Yolo/YOLO/ --img_type ".jpg" --manifest_path ./ --cls_list_file ./COCO/coco.names
```

変換エラーを回避するため、coco datasetに合わせて一部変更。tvをtvmonitorにしたり、など。
```text
person
bicycle
car
motorbike
aeroplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
sofa
pottedplant
bed
diningtable
toilet
tvmonitor
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush
```

YOLOフォーマットに変換できた。こんな感じ。
```
58 0.39 0.416 0.039 0.163
62 0.128 0.505 0.233 0.223
62 0.934 0.583 0.127 0.185
(snip)
```

ここから、さらにconvert YOLO to our formatが必要。これも、基本的にREADMEに従って変換するだけではあるが、class_list.txtは別途準備が必要でした。上で使ったclass_listをそのまま使用したところ、mAP評価プログラムが正常に実行できない。以下のようにスペースを含むclass名だと、正常に読み込めないようで、、、class_listを若干修正。

```text
trafficlight
firehydrant
stopsign
sportsball
baseballbat
baseballglove
tennisracket
wineglass
hotdog
cellphone
teddybear
hairdrier
```

以下のように無事変換完了。
```
pottedplant 238 143 263 212
tvmonitor 8 168 157 263
tvmonitor 558 209 639 288
(snip)
```

### detection-resultsデータの準備
元々の書籍のサンプルプログラムは予測結果をテキストに出力するようにはなっていないので、評価プログラム用のフォーマットで予測結果を出力するように修正。また、元のcoco_classes.txtのclass名も、ground-truthのclassと微妙に名称が異なっている部分があり、その部分だけすべて予測NGになってしまうので、併せて微修正。

https://www.morikita.co.jp/books/mid/078741

```diff
@@ -3,6 +3,7 @@
 #include <chrono>
 #include <assert.h>
 #include <iostream>
+#include <regex>
 
 inline float sigmoid(float x) {
   return 1.0 / (1 + exp(-x));
@@ -232,7 +233,10 @@ cv::Scalar hsv_to_bgr(int h, int s, int v) {
   return cv::Scalar(bgr_mat.data[0], bgr_mat.data[1], bgr_mat.data[2]);
 } 
 
-void draw_box(cv::Mat& image, std::vector<std::vector<float>>& boxes, std::vector<std::string>& classes, int sWidth, int sHeight) {
+void draw_box(cv::Mat& image, std::vector<std::vector<float>>& boxes, std::vector<std::string>& classes, int sWidth, int sHeight, std::string textfile) {
+  std::ofstream write_file;
+  write_file.open(textfile, std::ios::out);
+
   for (std::vector<float>& box : boxes) {
     int xmin = box[0] - box[2] / 2.0;
     int ymin = box[1] - box[3] / 2.0;
@@ -242,19 +246,35 @@ void draw_box(cv::Mat& image, std::vector<std::vector<float>>& boxes, std::vecto
     float* box_ptr = box.data();
 
     int class_index = box[5];
+    std::ostringstream oss;
+    oss << std::fixed << std::setprecision(6) << box[4];
+    std::string obj_score = " " + oss.str();
+
+    string class_text = classes[class_index].c_str() + obj_score;
+    // std::cout << "class_text " << class_text << std::endl;
+    std::string text_concat = class_text + " " + std::to_string(xmin) + " " + std::to_string(ymin) + " " + std::to_string(xmax) + " " + std::to_string(ymax);
+    write_file << text_concat << std::endl;
 
-    cv::rectangle(image, cv::Point(xmin, ymin), cv::Point(xmax, ymax), hsv_to_bgr((180 * class_index) / 80, 255, 255), 2, 1, 0);
+    cv::rectangle(image, cv::Point(xmin, ymin), cv::Point(xmax, ymax), hsv_to_bgr((180 * class_index) / 80, 255, 255), 1, 1, 0);
     cv::rectangle(image, cv::Point(xmin, ymin - 15), cv::Point(xmin + 100, ymin), hsv_to_bgr((180 * class_index) / 80, 255, 255), CV_FILLED, 1, 0);
-    cv::putText(image, classes[class_index].c_str(), cv::Point(xmin, ymin), 1, 1, cv::Scalar(0, 0, 0), 1);
+//    cv::putText(image, classes[class_index].c_str(), cv::Point(xmin, ymin), 1, 1, cv::Scalar(0, 0, 0), 1);
+    cv::putText(image, class_text, cv::Point(xmin, ymin-5), cv::FONT_HERSHEY_PLAIN , 0.75, cv::Scalar(0, 0, 0), 1);
   }
+
+  write_file.close();
 }
 
-void analyzeOutput(vart::Runner* runner, cv::Mat& image, std::vector<float*> tensors, int sWidth, int sHeight, std::vector<float> anchors, std::vector<std::string>& classes) {
+void analyzeOutput(vart::Runner* runner, cv::Mat& image, std::vector<float*> tensors, int sWidth, int sHeight, std::vector<float> anchors,\
+ std::vector<std::string>& classes, std::string outfilename) {
   std::vector<std::vector<float>> boxes;
+
+
   extract_boxes(runner, tensors, boxes, anchors);
   transform_boxes(boxes, sWidth, sHeight);
-  std::vector<std::vector<float>> selected_boxes = applyNMS(boxes, 80, 0.3);
-  draw_box(image, selected_boxes, classes, sWidth, sHeight);
+  std::vector<std::vector<float>> selected_boxes = applyNMS(boxes, 80, 0.1);
+  draw_box(image, selected_boxes, classes, sWidth, sHeight, outfilename);
+
+
 }
 
 void runYOLO(vart::Runner* runner) {
@@ -327,11 +347,12 @@ void runYOLO(vart::Runner* runner) {
     result.push_back(result0);
     result.push_back(result1);
     result.push_back(result2);
-
-    analyzeOutput(runner, raw_image, result, processed_image.w, processed_image.h, anchors, classes);
-
-    cv::imwrite(out_path + image_name, raw_image);
-
+    std::string text_name = std::regex_replace(image_name, regex("jpg"), "txt");
+    text_name = "./text/" + text_name;
+    analyzeOutput(runner, raw_image, result, processed_image.w, processed_image.h, anchors, classes, text_name);
+    std::cout << text_name << std::endl;
+    cv::imwrite("./images/outputs/" + image_name, raw_image);
+//    cv::imwrite(out_path + image_name, raw_image);
   }
   std::cout << "Elapsed Time per frame: " << elapsed / count << "[us]" << std::endl;
 
```


上記修正を実施した後、Ultra96上で実行。COCO dataset 約5000枚分だが、20~30分で予測完了。こんな感じで、5000枚分の予測結果が出力できていることを確認できました。

```text
person 0.880797 434 180 470 283
chair 0.995930 295 241 355 303
chair 0.939913 362 238 409 300
chair 0.622459 404 235 440 297
(snip)
```

## mAPの評価結果
よし！これで評価プログラム実行できるぞ！と実行してみたら、ground-truthとdetection_resultsのファイル数が不一致というエラーが発生。ground-truthが4950個、detection_resultsが5000個と、何故か50個ほど少ない。どうもよくわからないが、COCO datasetのannotationデータを、YOLOフォーマットに変換する際に、50個ほど取りこぼしている模様。
原因は定かではないが、50枚ほどであれば、とりあえず結果に大きく影響はないだろうと思い、下記スクリプトで評価対象から除外。

```shell
python3 scripts/extra/intersect-gt-and-dr.py 
```

気を取り直して、実行。DPUでの予測結果のmAPは40.49%となりました。
CPUで実行したら、1枚あたり140秒ほど処理時間がかかり、5000枚もやっていたら日が暮れてしまうということもあり、CPUでの予測結果は比較対象としては準備できてません。しかし、YOLOv3公式のYOLOv3-416のmAPが55.3%となっているので、これをリファレンスとすると、約15%近く低下しているので、精度は結構低下しているなあ、という印象です。
ただ、元のネットワーク構成で、かつUltra96のようなエッジデバイスで処理速度を出すのは非常に厳しいと思うので、ある程度の精度劣化を留意した上で、処理速度を優先するということであれば、許容範囲内だったりするんですかね？よく知らんけど。

**mAP評価結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413488/6e383858-dfe5-f4fe-b2c6-ca85c252e045.png)

**YOLOv3公式リファレンス**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413488/f7363f02-5f18-31f0-8801-d3c2ddcbb7f9.png)
https://pjreddie.com/darknet/yolo/