Darknet YOLOv4  を Google Open Images Datasetのデータを使って学習させる with Elixir

# はじめに
この記事はcvlibで使うように独自データセットでyolov4を学習した手順をまとめたものです
ファイル周りの処理やファイルリストの作成にElixirを使用しています

# 環境
OS ubuntu 20.04.2 LTS
CPU intel core i7-9700k
GPU Geforce RTX 3060
メモリ 32GB
CUDA 11030
cuDNN 8.2.1
OpenCV 4.2.0

# datasetの準備
データは[Google Open Images Dataset](https://storage.googleapis.com/openimages/web/download_v5.html)から
pythonの[openimages](https://pypi.org/project/openimages/)を使用してダウンロードします
darknet形式のannotationファイルを出力してくれるので[OIDv4_Toolkit](https://github.com/EscVM/OIDv4_ToolKit)より楽です

インストールはpipで行いダウンロード先を作っておきます

```
pip install openimages
mkdir ~/openimages
```
ダウンロードはコマンドかスクリプトで行います
コマンドはdirを指定してlabelsに取得したいクラス
クラス名は[サークル](https://storage.googleapis.com/openimages/2018_04/bbox_labels_600_hierarchy_visualizer/circle.html)か[csv](https://storage.googleapis.com/openimages/v5/class-descriptions-boxable.csv)に一覧があるのでそこから選んでください
クラス名の注意点としてv4とv5で以下のように微妙に違ってダウンロード時にエラーになるので気をつけましょう

```
Bat -> Bat (Animal)
Lady bug -> Ladybug
Jugger -> Jaguar (Animal)
```

```
oi_download_dataset --csv_dir ~/openimages --base_dir ~/openimages --labels Dog Cat --format darknet
```

```python
from openimages.download import download_dataset
labels = ["Woodpecker","Blue jay","Ostrich","Penguin","Raven","Chicken","Eagle","Owl","Duck","Canary","Goose","Swan","Falcon","Parrot","Sparrow","Turkey","Tick","Centipede","Starfish","Isopod","Squid","Jellyfish","Shrimp","Bee","Beetle","Ladybug","Ant","Caterpillar","Butterfly","Dragonfly","Scorpion","Worm","Spider","Snail","Bat (Animal)","Brown bear","Panda","Polar bear","Teddy bear","Cat","Fox","Jaguar (Animal)","Lynx","Red panda","Tiger","Lion","Dog","Leopard","Cheetah","Otter","Raccoon","Camel","Cattle","Giraffe","Rhinoceros","Goat","Horse","Hamster","Kangaroo","Koala","Mouse","Pig","Rabbit","Squirrel","Sheep","Zebra","Monkey","Hippopotamus","Deer","Elephant","Porcupine","Hedgehog","Bull","Antelope","Mule","Dolphin","Whale","Sea lion","Harbor seal","Skunk","Alpaca","Armadillo","Dinosaur","Lizard","Snake","Turtle","Tortoise","Sea turtle","Crocodile","Frog","Goldfish","Shark","Rays and skates","Seahorse","Shellfish","Oyster","Lobster","Shrimp","Crab","Person"]
labels.sort()
download_dataset("openimages",labels, annotation_format="darknet")
```

# darknet学習用に準備
darknetの学習には以下が必要になりますのでそちらも準備していきましょう

- クラス名一覧
- 画像とアノテーションデータ
- train,testの画像リスト

# クラス名一覧
openimagesでダウンロードしたときに darknet_obj_names.txtというファイルが作成されていますのでそちらをそのまま使用します

# 画像とアノテーションデータ
darknetの学習用データセットは１つのフォルダに画像ファイルと同名アノテーションデータのファイルを以下のようにいれます

```
    └── datasets
       ├── image1.jpg
       ├── image1.txt
       ...
       ├── imagex.jpg
       └── imagex.txt
```
画像ファイルとアノテーションデータはopenimagesでダウンロードされていますがクラスごとに別れていますのでそれをまとめていきましょう

アノテーションデータの中身はこんな感じで先頭はIDでそれ以降が対象が描画されている位置になります
IDはopenimageのIDではなく、darknet_obj_names.txtの行番号になります
これは犬だけをダウンロードしたときなので dog => 0を示しています

```
0 0.77625 0.597092 0.21999999999999997 0.20356400000000005
```
ダウンロード先のopenimagesに移動して iexを起動させて以下を実行します
mkdirでまとめる用のdatasetフォルダを作成しておきましょう

```elixir
File.ls! 
|> Enum.each(fn d -> 
  System.cmd("/bin/sh",["-c","mv #{d}/images/* ./dataset/"]) 
  System.cmd("/bin/sh",["-c","mv #{d}/darknet/* ./dataset/"]) 
end)
```

File.ls!でクラスのフォルダ一覧を取得し
Enum.eachでフォルダ以下の画像とアノテーションデータを移動させるコマンドをbashで移動させます
cpでもいいですが容量がでかいのでmvにしています、ストレージ容量と相談でお好きな方を
System.cmd("mv",opt)だと*(wildcard)が使えないのでsh -cで実行しています

# Train, Test 画像のリスト
こちらも, train-annotations-bbox.csvとtest-annotations-bbox.csvがありますが
全open imagesのファイルのリストなので不要なものを除いていきます

中身はこんな感じ

```
ImageID,Source,LabelName,Confidence,XMin,XMax,YMin,YMax,IsOccluded,IsTruncated,IsGroupOf,IsDepiction,IsInside
000002b66c9c498e,xclick,/m/01g317,1,0.012500,0.195312,0.148438,0.587500,0,1,0,0,0
000002b66c9c498e,xclick,/m/01g317,1,0.025000,0.276563,0.714063,0.948438,0,1,0,0,0
000002b66c9c498e,xclick,/m/01g317,1,0.151562,0.310937,0.198437,0.590625,1,0,0,0,0
```

```elixir
class = File.read!("path/to/train-annotations-bbox.csv")
  |> String.split("\n") # 一塊の文字列で来るので改行コードで分割
  |> Enum.map(fn s -> String.split(s, ",") end) # ListになったのでEnum.mapでコンマで分割
  |> Enum.drop(1) # header部分が邪魔なので先頭をdrop
  |> Enum.filter(fn s -> File.exists?("dataset/#{Enum.at(s, 0)}.jpg") end)
  |> Enum.map(fn s -> Enum.at(s, 0) end)

File.open("train.txt",[:write], fn file ->  
  Enum.each(class, fn str -> IO.write(file, "/home/user_name/openimages/dataset/#{str}.jpg\n") end)                
end)
```

前半はFile.read!だと結果が文字列で返ってくるのでsplitでcsvの形にして
imageIDと同じ画像が存在するか？を条件にフィルタリングして、imageIDだけのListを作成しています
後半はimageIDのListの内容を絶対パスで書き込んでいます

darknetの仕様なので相対パスではなく絶対パスにしてください

# darknetの準備
データセット準備が整ったのでdarknetの準備をしていきましょう
[こちら](https://qiita.com/Ihmon/items/a331d63a7a5d3fa51918)を参考にインストール

```
git clone https://github.com/AlexeyAB/darknet.git
cd darknet
vim Makefile
```
変更箇所
GPUがリストになかったけどアーキテクチャ的に同じだろうから ARCHは86
nvccのパスがエラーになったのでフルパスでセット

```Makefile.
GPU=1
CUDNN=1
CUDNN_HALF=0
OPENCV=1

ARCH= -gencode arch=compute_86,code=[sm_86,compute_86]

NVCC=/usr/local/cuda/bin/nvcc
```
変更したらmakeでコンパイル

# .dataファイルの準備
train,testファイルの一覧や学習途中のweigthファイルの保存先を設定します
ファイル名はお好みで今回はdog.dataとしています
エラーになるのでそれぞれ絶対パスで設定してください

```
class= 1 # 今回はdogだけなので１
train = /home/user_name/openimages/train.txt
valid = /home/user_name/openimages/test.txt
names = /home/user_name/openimages/darknet_obj_names.txt
backup = /home/user_name/darknet/backup/
```

# configの編集
custom-objectのときはこれ使ってと[ここ](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)にあるのでyolov4-custom.cfgを使用します
git cloneしたdarknetのcfg以下あるのでコピーしておく

```
cp darknet/cfg/yolov4-custom.cfg ~/openimages/
```
[こちら](https://qiita.com/kitata9/items/936c97f0ac33dae13ab5#config%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)を参考に編集

```
subdivisions=64
max_batches = 46000
4000以上で、クラス数×2000
steps=36800,41400
max_batchesの80％,90%
classes=1
クラス数を指定します
下の方のyoloが３つあるのでそこを編集
filters=18
(クラス数+5)×3
yoloの１つ前のconv層にあるので編集
```

# 学習
以下のコマンドで学習を開始します
重みファイルのyolov4.conv.137は[こちら](https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.conv.137)からダウンロード

```
darknet/darknet detector train ~/openimages/dog.data ~/openimages/yolov4-custom.cfg ~/openimages/yolov4.conv.137
```

あとは気長にお待ち下さい

# cvlibで使う
学習中でも学習済みパラメーターがbackupに保存されてると思うのでそちらを使用して
wights, .cfg, darknet_obj_names.txtを渡せば物体検知を行って検知した箇所を囲ってくれます
https://github.com/arunponnusamy/cvlib/blob/master/examples/yolo_custom_weights_inference.py

```yolo_custom_weights_inference.py
import cvlib as cv
from cvlib.object_detection import YOLO
import sys
import cv2

weights = sys.argv[1]
config = sys.argv[2]
labels = sys.argv[3]

# read input image
image = cv2.imread(sys.argv[4])

yolo = YOLO(weights, config, labels)

# apply object detection
bbox, label, conf = yolo.detect_objects(image)

print(bbox, label, conf)

# draw bounding box over detected objects
yolo.draw_bbox(image, bbox, label, conf, write_conf=True)

# display output
# press any key to close window           
cv2.imshow("object_detection", image)
cv2.waitKey()

# save output
cv2.imwrite("yolo_object_detection.jpg", image)

# release resources
cv2.destroyAllWindows()
```

```
python yolo_custom_weights_inference.py yolov4-custom_2000.weights yolov4-custom.cfg darknet_obj_names.txt dog.jpeg
```

![スクリーンショット 2021-07-11 1.35.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1215f11c-ad5a-8af7-fec4-34b42ba1aab7.png)

無事犬だけを検知するyolov4が完成しました！
本記事は以上になりますありがとうございました


参考ページ
https://github.com/arunponnusamy/cvlib
https://qiita.com/Ihmon/items/a331d63a7a5d3fa51918
https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects
https://pjreddie.com/darknet/yolo/
https://qiita.com/kitata9/items/936c97f0ac33dae13ab5
https://pypi.org/project/openimages/
https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.conv.137
https://storage.googleapis.com/openimages/2018_04/bbox_labels_600_hierarchy_visualizer/circle.html
https://github.com/EscVM/OIDv4_ToolKit
https://stackoverflow.com/questions/49952153/elixir-shell-command-with-wildcard-failing-when-using-system-cmd