YOLO11で物体検出

# 環境
* Windows11
* CPU

# 1. uvで環境構築
powershellでuvをインストール
~~~powershell
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
~~~

powershell再起動後，インストールできているか確認
~~~powershell
uv --version
~~~

新規フォルダを作成（my-projectは任意の名前）
~~~powershell
mkdir my-project
~~~

my-projectに移動
~~~powershell
cd my-project
~~~

仮想環境作成（my-project-venvは任意の名前）
~~~powershell
uv venv my-project-venv -p 3.12
~~~

仮想環境のアクティベート
~~~powershell
my-project-venv/Scripts/activate
~~~

アクティベートできていれば，以下のようになっている
~~~terminal
(my-project-venv) C:\Users\[USER]\my-project>
~~~

[公式サイト](https://pytorch.org/)の通りPyTorch, Torchvision, Torchaudioをインストール（先頭にuvをつける）
cpuを使うなら，以下をそのまま実行
~~~powershell
uv pip install torch torchvision torchaudio
~~~

ultralyticsをインストール
~~~powershell
uv pip install ultralytics
~~~

labelImgをインストール
~~~powershell
uv pip install labelimg
~~~

# 2. labelImgでアノテーション

## エラー原因の修正

このままlabelImgを実行するとエラーが発生するので，事前に修正する

`C:\Users\[USER]\my-project\my-project-env\Lib\site-packages\labelImg\labelImg.py`
のプログラムファイルをテキストエディタで開く

965行目を以下のように修正
~~~diff_python
- bar.setValue(bar.value() + bar.singleStep() * units)
+ bar.setValue(int(bar.value() + bar.singleStep() * units))
~~~

971行目を以下のように修正
~~~diff_python
- self.zoom_widget.setValue(value) 
+ self.zoom_widget.setValue(int(value)) 
~~~

1025, 1026行目を以下のように修正
~~~diff_python
- h_bar.setValue(new_h_bar_value) 
- v_bar.setValue(new_v_bar_value) 
+ h_bar.setValue(int(new_h_bar_value)) 
+ v_bar.setValue(int(new_v_bar_value)) 
~~~

`C:\Users\[USER]\my-project\my-project-env\Lib\site-packages\libs\canvas.py`
のプログラムファイルをテキストエディタで開く

526行目を以下のように修正
~~~diff_python
- p.drawRect(left_top.x(), left_top.y(), rect_width, rect_height)
+ p.drawRect(int(left_top.x()), int(left_top.y()), int(rect_width), int(rect_height))
~~~

530行目, 531行目を以下のように修正
~~~diff_python
- p.drawLine(self.prev_point.x(), 0, self.prev_point.x(), self.pixmap.height())
- p.drawLine(0, self.prev_point.y(), self.pixmap.width(), self.prev_point.y())
+ p.drawLine(int(self.prev_point.x()), 0, int(self.prev_point.x()), self.pixmap.height())
+ p.drawLine(0, int(self.prev_point.y()), self.pixmap.width(), int(self.prev_point.y()))
~~~

参考元：

https://note.com/nagisa_hoshimori/n/n4bb4a4a5019d

## アノテーション準備

`detection`フォルダを作成（省略可）
~~~powershell
mkdir detection
~~~

`all_images`フォルダと`all_labels`フォルダを作成
~~~powershell
mkdir detection/all_images
mkdir detection/all_labels
~~~

`all_images`フォルダにアノテーションしたい画像を入れてください

## アノテーション

labelImgを起動
~~~powershell
labelimg
~~~

1. 画面左上から2番目の`Open Dir`を押し，`all_images`フォルダを選択。画像が表示される
2. 画面左上から3番目の`Change Save Dir`を押し，`all_labels`フォルダを選択
3. 画面左上から8番目の表示が`YOLO`になるまでクリック
4. 画面左上にある`View`を押し，`Auto Save mode`をクリック
5. 表示された画像を右クリックし，`Create RectBox`を選択
6. クリック＆ホールドで，検出対象を囲むように長方形を描く
7. ホールド解除すると，ラベル名を入力するポップアップが出るので，入力する
8. 画面左上から4番目の`Next Image`を押す
9. `6.`から`8.`を繰り返してアノテーションを行う
10. `Next Image`を押しても次の画像が表示されなくなったら終了，画面右上の`×`で閉じる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950815/3ea39e21-7e36-146d-1b3b-15e356a77037.png)

# 3. YOLO11で物体検出

## データセットの作成

`detection`フォルダに移動して，`CreateDataset.py`を作成する
~~~powershell
cd detection
ni CreateDataset.py
~~~

`CreateDataset.py`をテキストエディタで開き，以下のコードを貼り付けて保存する

<details><summary>１画像につき１ラベルの場合</summary>

`image_format`と`test_ratio`は自由に調整してください
~~~CreateDataset.py
import os
import shutil
from sklearn.model_selection import train_test_split
from glob import glob

DATASET_PATH = 'datasets'
IMAGE_PATH = os.path.join(DATASET_PATH, 'images')
LABEL_PATH = os.path.join(DATASET_PATH, 'labels')
IMAGE_TRAIN_PATH = os.path.join(IMAGE_PATH, 'train')
LABEL_TRAIN_PATH = os.path.join(LABEL_PATH, 'train')
IMAGE_VAL_PATH = os.path.join(IMAGE_PATH, 'val')
LABEL_VAL_PATH = os.path.join(LABEL_PATH, 'val')

ALL_IMAGES_PATH = 'all_images'
ALL_LABELS_PATH = 'all_labels'

# データセットフォルダの作成
os.makedirs(IMAGE_TRAIN_PATH, exist_ok=True)
os.makedirs(LABEL_TRAIN_PATH, exist_ok=True)
os.makedirs(IMAGE_VAL_PATH, exist_ok=True)
os.makedirs(LABEL_VAL_PATH, exist_ok=True)

# ラベルのクラス名を取得
with open(os.path.join(ALL_LABELS_PATH, 'classes.txt'), 'r') as f:
    classes = f.read().splitlines()

# 画像とラベルを訓練データと検証データに分割
import os
import shutil
from sklearn.model_selection import train_test_split
from glob import glob

DATASET_PATH = 'datasets'
IMAGE_PATH = os.path.join(DATASET_PATH, 'images')
LABEL_PATH = os.path.join(DATASET_PATH, 'labels')
IMAGE_TRAIN_PATH = os.path.join(IMAGE_PATH, 'train')
LABEL_TRAIN_PATH = os.path.join(LABEL_PATH, 'train')
IMAGE_VAL_PATH = os.path.join(IMAGE_PATH, 'val')
LABEL_VAL_PATH = os.path.join(LABEL_PATH, 'val')

ALL_IMAGES_PATH = 'all_images'
ALL_LABELS_PATH = 'all_labels'

# データセットフォルダの作成
os.makedirs(IMAGE_TRAIN_PATH, exist_ok=True)
os.makedirs(LABEL_TRAIN_PATH, exist_ok=True)
os.makedirs(IMAGE_VAL_PATH, exist_ok=True)
os.makedirs(LABEL_VAL_PATH, exist_ok=True)

# ラベルのクラス名を取得
with open(os.path.join(ALL_LABELS_PATH, 'classes.txt'), 'r') as f:
    classes = f.read().splitlines()

# 画像とラベルを訓練データと検証データに分割
def split_dataset(image_format='.jpg', test_ratio=0.2):
    images = []
    labels = []
    label_indices = []

    for image in os.listdir(ALL_IMAGES_PATH):
        if not image.endswith(image_format):
            continue         
        image_name = image.replace(image_format, '')
        label = f'{image_name}.txt'
        with open(os.path.join(ALL_LABELS_PATH, label), 'r') as f:
            content = f.read()
            label_index = content[0]
        
        images.append(image)
        labels.append(label)
        label_indices.append(label_index)
    
    print(f'images:\n{images}')
    print(f'labels:\n{labels}')
    print(f'label_indices:\n{label_indices}')

    train_images, val_images, train_labels, val_labels = train_test_split(images, labels, test_size=test_ratio, stratify=label_indices)

    for image, label in zip(train_images, train_labels):
        image_path = os.path.join(ALL_IMAGES_PATH, image)
        label_path = os.path.join(ALL_LABELS_PATH, label)

        shutil.copy2(image_path, os.path.join(IMAGE_TRAIN_PATH, image))
        shutil.copy2(label_path, os.path.join(LABEL_TRAIN_PATH, label))
    
    for image, label in zip(val_images, val_labels):
        image_path = os.path.join(ALL_IMAGES_PATH, image)
        label_path = os.path.join(ALL_LABELS_PATH, label)

        shutil.copy2(image_path, os.path.join(IMAGE_VAL_PATH, image))
        shutil.copy2(label_path, os.path.join(LABEL_VAL_PATH, label))
    
    print(f'Train images: {len(train_images)}, Val images: {len(val_images)}')

def create_yaml(classes):
    path = DATASET_PATH
    train = IMAGE_TRAIN_PATH.replace(DATASET_PATH + '\\', '').replace('\\', '/')
    val = IMAGE_VAL_PATH.replace(DATASET_PATH + '\\', '').replace('\\', '/')

    with open('dataset.yaml', 'w') as f:
        f.write('# Path\n')
        f.write(f'path: ./{path}\n')
        f.write(f'train: {train}\n')
        f.write(f'val: {val}\n')
        f.write('\n')
        f.write('# Classes\n')
        f.write(f'nc: {len(classes)}\n')
        f.write(f'names: {classes}')

if __name__ == '__main__':
    split_dataset(image_format='.jpg', test_ratio=0.2)
    create_yaml(classes)

~~~

</details>

<details><summary>１画像につき複数ラベルの場合</summary>

工事中

</details>

実行前に，必要なパッケージをインストールする
~~~powershell
uv pip install scikit-learn
~~~

以下を実行し，データセットを作成する
~~~powershell
python CreateDataset.py
~~~

<details><summary>エラーが発生した場合</summary>

:::note warn
画像の数が少ない場合，以下のエラーが発生します。画像を増やしてください
~~~powershell
ValueError: The least populated class in y has only 1 member, which is too few. The minimum number of groups for any class cannot be less than 2.
~~~
:::

</details>

## モデルの学習

`TrainModel.py`を作成する
~~~powershell
ni TrainModel.py
~~~

`TrainModel.py`をテキストエディタで開き，以下のコードを貼り付けて保存する
~~~TrainModel.py
from ultralytics import YOLO

if __name__ == "__main__":
    model = YOLO('yolo11n.pt')
    model.train(data="dataset.yaml", epochs=100, batch=8)
~~~

`yolo11n.pt`の`n`はモデルサイズを表します。他のサイズは[公式サイト](https://docs.ultralytics.com/tasks/detect/)に載っています

以下を実行し，モデルを学習させる
~~~powershell
python TrainModel.py
~~~

学習完了後，`runs/detect/train`を確認する。`weights/best.pt`が学習済みのモデルです

<details><summary>エラーが発生した場合</summary>

:::note warn
以前にUltralyticsをインストールしていた場合，以下のエラーが出るかもしれません
~~~powershell
raise RuntimeError(emojis(f"Dataset '{clean_url(self.args.data)}' error ❌ {e}")) from e
RuntimeError: Dataset 'dataset.yaml' error
Dataset 'dataset.yaml' images not found , missing path 'C:\images\val'
Note dataset download directory is '何か'. You can update this in 'C:\Users\[USER]\AppData\Roaming\Ultralytics\settings.json'
~~~

以下を実行すれば解決します
~~~powershell
yolo settings datasets_dir='.'
~~~
:::

</details>

## モデルの推論

学習させたモデルを使って推論を行う

`Predict.py`を作成する
~~~powershell
ni Predict.py
~~~

`Predict.py`をテキストエディタで開き，以下のコードを貼り付ける
~~~Predict.py
from ultralytics import YOLO

if __name__ == "__main__":
    image_path = 'test.jpg'
    save_path = 'result.jpg'

    model = YOLO('./runs/detect/train/weights/best.pt')
    results = model(image_path, save=False)
    results[0].save(save_path)
~~~
`image_path`や`save_path`はすきなように設定してください

以下を実行し，推論を行う
~~~powershell
python Predict.py
~~~

# 以上

未記入部分あり