YOLOv5を利用した学習と物体検出

# 目的

- `YOLOv5`について、入門までとはいかないが、門の前に立てる程度の知識を身につける。
- `YOLOv5`を利用して学習から物体検出（今回はサングラス）を行い、最低限の使い方を身につける。

# 背景

チュートリアルでもよくある、自前でモデルの作成、学習、分類を行ったりしたことはあったが、`YOLO`等の物体検出アルゴリズムを利用したことがなかったため、試しに利用し、知識を得たかった。

# YOLOv5とは

`YOLOv5`とは物体検出アルゴリズムで、`You Only Look Once`の頭文字をとっています。
処理速度が非常に速いのが特徴で、リアルタイムに物体検出を行うことも可能です。
`YOLOv5`は`Pytorch`をベースに作られています。

自身でpythonコードを書かずに（もちろん書くこともできます）、`YOLOv5`が用意している処理を実行するだけで、学習や物体検出を簡単に行うことができます。

- YOLOv5のGitHub

https://github.com/ultralytics/yolov5

- YOLOv5の公式ドキュメント

https://docs.ultralytics.com/

# 実行環境

Google Colab

# 内容

ここからは`YOLOv5`を使った学習と物体検出の流れを説明します。
ソースコードについてはGitHubにアップしてあります。GitHubへのリンクは本投稿後半にある`ソースコード`の章を参照願います。

## 1.事前準備

学習、物体検出を行うための事前準備を行います。

- YOLOv5のインストール

```
!git clone https://github.com/ultralytics/yolov5

%cd yolov5
!pip install -r requirements.txt
```

- データセットの準備

`YOLOv5`で読み込むことが可能なデータセットを準備願います。
準備できた方は任意のディレクトリ配置してください。

2024/1/1 追記-----
下記にて、YOLOにて読み込み可能なデータセットを作成するGoogleColabを用意しました。
GoogleOpenImageDatasetにあるデータ限定となりますが、お試しする分には十分と思います。

https://qiita.com/shinya_sun_sun/items/d04bdd33dabc841b319f

出来上がったtrain_data.zipをyolov5フォルダ直下に解凍し利用してください。

## 2.学習

学習を行います。
今回は手持ちのデータ数が少なかったため、`転移学習`を行います。

:::note
転移学習とは

1から学習する通常学習と異なり、学習済みのモデルを元に、少ないデータでモデルを素早く再学習する方法です。
学習済みモデルのパラメータを再利用するため、いくつかの層のパラメータを固定する場合がほとんどです。

人に例えると、経験を活かすと同じ感じです。野球のバットを振るのがゴルフのスイングに活かされるように。
:::

```
!python train.py --batch 16 --epochs 50 --data train_data/data.yaml --weights yolov5s.pt --optimizer AdamW --cache --freeze 10
```

学習が完了すると、`yolov5/runs/train/exp`の下に学習結果が格納されます。
学習時に`--name`を指定しない場合、`exp`,`exp2`・・・と自動的にフォルダ名が割り振られます。複数回学習を行った場合、間違えないよう注意願います。

学習済みモデルは`weight`フォルダに格納され、`best.pt`が一番よかったモデルとなります。
他、精度や損失についての結果等が格納されていますが、今回はそこには触れず、別の投稿で解説したいと思います。（私もまだちゃんと理解できていないため）

今回指定したオプションについての説明です。
~~あまり細かなことまで調べきれていないため、概要程度の説明となります。
別な投稿にて詳しい解説を予定しています。~~
こちらにて解説を投稿しましたので参照願います。（2022/8/2　追記）

https://qiita.com/shinya_sun_sun/items/8c368f3024bf5b0d14aa

| オプション | 指定値 | 説明 |
|:-|:-|:-|
| --batch  |  16 | バッチ数です。お使いの環境に合わせ調整してください。  |
| --epochs  |  50 | 学習回数です。気の済むまで学ばせてやってください。  |
| --data  |  train_data/data.yaml | 読み込みたいデータセットの`data.yaml`へのパスです。train.pyを実行する際のディレクトリからの相対パスとなります。データセットの場所に合わせ変えてください。  |
| --weights  | yolov5s.pt  | 転移学習する元の学習済みモデルです。モデルによって、学習性能や精度が異なるため、ベストなモデルを選択してください。  |
| --optimizer  | AdamW  | オプティマイザの指定です。この辺はあまりわかってないので、名前だけでカッコ良さそうなものをチョイスしました。ちゃんと理解した上でベストなオプティマイザを選択してください。  |
| --cache  |   | 訓練、検証用の画像をキャッシュするどうかの指定です。指定なしの場合、キャッシュされず、学習時間が長くかかる可能性があります。指定ありの場合、画像がメモリ上にキャッシュされ、学習時間が短縮されます。お使いの環境に合わせ選択してください。  |
| --freeze  | 10  | モデルのパラメータを固定する層の指定です。固定したい層分指定してください。今回は`backbone`を固定するため`10`を指定しました。  |

`YOLOv5`にて転移学習をする場合、学習時に下記オプションの指定が必要となります。
`--weights`
`--freeze`

https://github.com/ultralytics/yolov5/issues/1314

公式GitHubのIssuesに転移学習の解説があります。

解説によると、出力層を除く、モデル全体のパラメータを固定することもできるようですが、精度が下がる結果が出ているようです。`backbone`のみの固定も多少精度が下がるようです。
全てが良い方向に向くわけではありませんが、手持ちのデータが少ない場合、1から始めるよりは効果があると思われます。

詳しく知りたい方は、ぜひ公式GitHubのIssuesを覗いてみてください。

## 物体検出

物体検出を行います。
学習同様、`YOLOv5`が用意した処理を呼び出すだけでも可能ですが、システムに組み込んだりする場合も想定し、コマンドから物体検出処理を呼び出す方法と、ソースコードから呼び出す方法の２種類やってみました。

- コマンドから

コマンドから物体検出処理を呼び出します。

```
!python detect.py --source input.jpg --conf 0.5 --weights runs/train/exp/weights/best.pt
```

今回指定したオプションについての説明です。
あまり細かなことまで調べきれていないため、概要程度の説明となります。
学習同様、別な投稿にて詳しい解説を予定しています。
| オプション | 指定値 | 説明 |
|:-|:-|:-|
| --source  |  input.jpg |  物体検出に入力する画像、または、画像が格納されているフォルダの指定です。 |
| --conf  | 0.5  | 物体を検出した際のクラス分類（今回はサングラスのみ）の判定値に対する閾値です。指定した値を下回る判定をされた物体については検出されません。  |
| --weights  |  runs/train/exp/weights/best.pt | 物体検出を行う際のモデルです。  |

物体検出が完了すると、yolov5/runs/detect/expの下に検出結果画像が格納されます。
画像を開くとこんな感じです。
![sunglasses (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/a0dcd79c-4b31-97ee-336f-84ba419cd214.png)
検出できていないところ、バウンディングボックスがイマイチなところありますが、比較的ちゃんとサングラスを検出できてますね！！

- ソースコードから

今度は物体検出処理をpythonのソースコード上から行います。
検出した物体のバウンディングボックスを取得し画像を切り抜き保存してみます。

```python
import math

import torch
import cv2

# モデルの読み込み
model = torch.hub.load(".", "custom", path="runs/train/exp/weights/best.pt", source="local")

# 入力画像の読み込み
img = cv2.imread("input.jpg")

# 検出の閾値設定
model.conf = 0.5

# 物体検出
result = model(img)

# バウンディングボックスを取得し画像をクリップ
for idx, row in enumerate(result.pandas().xyxyn[0].itertuples()):
    height, width = img.shape[:2]

    xmin = math.floor(width * row.xmin)
    xmax = math.floor(width * row.xmax)
    ymin = math.floor(height * row.ymin)
    ymax = math.floor(height * row.ymax)

    cripped_img = img[ymin:ymax, xmin:xmax]

    cv2.imwrite(f"crip_{idx}.jpg", cripped_img)
```

切り抜いた画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38724/3fd33da1-a445-07c7-e231-4ca7a189f634.png)
物体検出後、結果オブジェクトからバウンディングボックスの座標（`xyxyn`）を取得し、画像を切り抜いてますが、結果オブジェクトが持っているバウンディングボックス座標はいくつか種類があります。

| 変数名 | 説明 |
|:-|:-|
| xyxy  | 左上xy座標、右下xy座標がピクセルで格納されている。  |
| xywh  | 中心xy座標、幅、高さがピクセルで格納されている。  |
| xyxyn  | 画像の幅、高さを元に標準化（x座標を幅で割った値　yの場合は高さ）された左上xy座標、右下xy座標が格納されている。<br>例えば、幅1,000pxの画像で、左上x座標が400pxの場合、標準化された値は0.4となる。  |
| xywhn  | 画像の幅、高さを元に標準化された中心xy座標、幅、高さが格納されている。  |

変数の命名パターンは、`xyxy`は左上右下xy座標、`xywh`は中心xy座標幅高さ、後ろに`n`がついてる場合、値が標準化されています。

尚、結果オブジェクトは`models.common.Detections`です。

```python
print(type(result))

# 出力結果
# <class 'models.common.Detections'>
```

参考としてソースコードを掲載しておきます。
今後処理が変わると厄介なので、現時点の断面を指定しておきます。

https://github.com/ultralytics/yolov5/blob/e309a855860bc3f618c3541909c515a65ffc35b0/models/common.py#L640

以上で学習〜物体検出までの操作が完了です。

# ソースコード

今回のソースコードは下記に入ってます。

https://github.com/MamedenQ/Yolov5Example

ノートブックしかないので、下記リンクを叩くでも良いです。
<a href="https://colab.research.google.com/github/MamedenQ/Yolov5Example/blob/main/YOLOv5Example.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>

# 最後に

`YOLOv5`を利用して、簡単に学習や物体検出を行うことができました。
まだまだ知らないことが多いため、本文中で積み残した不明点を今後調べ、もっと深めていきたいと思います。