ことのはじまり
フィールドにある銀色・黒色のボールを認識したいと思った時に、「どうにかして楽にできないか」と考えたことが、Label StudioとYOLOへの出会いの始まりでした。
最初はCV2などの輪郭線抽出などを駆使して検出することも考えましたが、「物体検知と呼ばれるものをすれば楽では?」と思って、使ってみることにしました。
今回使うもの
YOLO(You Only Look Once)
ワシントン大学のJoseph RedmonとAli Farhadiによって開発された物体検知のライブラリです。
Pythonライブラリとして提供されており、比較的少ない画像枚数であっても精度が高い物体検知をすることができるのに特化しています。
Label Studio
Label Studioは、画像のアノテーションをするためのWebアプリケーションで、無料で使えるようになっています。
様々なライブラリ向けにアノテーションのデータをエクスポートできるようになっており、今回YOLOで学習させるためのデータのアノテーションもします。
やり方
とりあえず画像のラベリングをするためのプロジェクトを作成しましょう。
mkdir yolo-demo
cd $_
まず、Label Studioを起動する場所を作ります。
mkdir label-studio
cd $_
Label Studioは様々な形態で提供されていますが、今回はDockerで動かすことにします。
次のようなdocker-compose.ymlを作成します。
version: "3.9"
services:
label_studio:
container_name: label_studio
image: heartexlabs/label-studio:latest
volumes:
- label-studio-data:/label-studio/data
ports:
- 8080:8080
environment:
- LOCAL_FILES_SERVING_ENABLED=true
volumes:
label-studio-data:
書き込んだら、次のコマンドで起動します。
docker compose up --build -d
なお、落とすときは
docker compose down
で終了できます。
また、二回目以降の起動は
docker compose up -d
で十分です。
さて、しばらく待つと、Label Studioがlocalhost:8080に立ち上がります。
ログイン
Label Studioにアクセスすると、ログイン画面が出てきます。
ローカルですが、アカウントが必要です。
さて、アカウントが作成されると、以下のような画面が出ます。
プロジェクトの作成
Create projectを選択して、新しいプロジェクトを開始します。
プロジェクトやDescriptionを設定したのち、Labeling Setupを選択します。
今回は、YOLOで物体検知をするため、Object Detection with Bounding Boxesを選択します。
デフォルトでAirplaneとCarのラベルがあるので、削除しましょう。
そのあと、自分が使用したいラベルを追加します。
今回は、faceの一種類だけ登録します(複数のラベルを作成することも可能です)。
そして右上にあるSaveを押してプロジェクト構成を保存します。
プロジェクトの学習に使用する画像をインポートする
現在プロジェクトを開いているので、そこから画像をインポートします。
実は画像をアップロードするという手法もありますが、今回はローカルのファイルを参照するという方法で画像をアップロードしたいと思います。
まずは、今立ち上げているLabel Studioを終了します。
docker compose down
さて、一つ上のディレクトリに戻って、画像を入れるフォルダを作成します。
cd .. # cwd: yolo-demo
mkdir images
そして、ラベリングをしたい画像をそのフォルダに入れます。
さて、Label StudioのDockerコンテナからもアクセスできるように設定を書き足します。
version: "3.9"
services:
label_studio:
container_name: label_studio
image: heartexlabs/label-studio:latest
volumes:
- label-studio-data:/label-studio/data
+ - ../images/:/tmp/test_images
ports:
- 8080:8080
environment:
- LOCAL_FILES_SERVING_ENABLED=true
volumes:
label-studio-data:
networks:
default:
name: label_link
もう一度立ち上げてみて、フォルダに入れた画像をインポートします。
Connect cloud storageを選択し、ProviderはLocal filesにします。
ストレージに好きな名前を設定し、ローカルへのパスは/tmp/test_imagesを選択します。
すると、次のような画面に遷移するため、Import methodがFiles - Automatically creates a task for each storage object (e.g. JPG, MP3, TXT)と選択されていることを確認し、Nextを押し、Saveを押します。
その後、Sync storageを押して同期します。
今後も画像を追加すると、ここからSyncボタンを押すことによって同期されます。
さて、画像が追加されたので、今度はトップに戻ってラベリングを開始します。
Label All Tasksを選択し、そこからラベリングを開始します。
画面下部にある自分で設定したラベルをクリックし、該当している範囲を箱で囲みます。
その後、Submitを選択し、終わるまで続けます。
さて、一旦ラベリングが終了したところで、エクスポートをします。
プロジェクトのトップ画面に戻って、エクスポートボタンを押すと、エクスポート方式を選択できるので、YOLO with Imagesを選択してエクスポートします。

これでLabel Studio上での作業は終わりです。次に、画像を学習させていくための準備をしましょう。
まず、画像のデータを格納するディレクトリを作成します。
#cwd: yolo-demo
mkdir data
cd data
ここで、先程エクスポートしたときにダウンロードしたZIPファイルを展開して、data/exported_data/の中にコピーします。
そして、ここに画像ラベリングをした画像をコピーします。
#cwd: yolo-demo/data/exported_data/
cp ../../images/* images/
現在のディレクトリ構成は以下のようになっているはずです(画像の名前は異なっていると思います)。
tree
.
├── data
│ └── exported_data
│ ├── classes.txt
│ ├── images
│ │ ├──1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ ├── labels
│ │ ├── 1.txt
│ │ ├── 2.txt
│ │ ├── 3.txt
│ └── notes.json
├── images
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
└── label-studio
└── docker-compose.yml
さて、ここまでできたということで、学習させる環境を作成していきます。
まず、ディレクトリのトップに戻ってPythonの仮想環境を作成します。
#cwd: yolo-demo
python3 -m venv .venv
source .venv/bin/activate
さて、必要なパッケージをリストアップします。
ultralytics
そしてインストールします。
pip3 install -r requirements.txt
さて、インストール途中に初期化用のスクリプトも作成しておきます。
import os
from ultralytics import settings
dirname = os.path.dirname(__file__)
runs_dir = os.path.join(dirname, "runs")
print(f'set up runs_dir to {runs_dir}')
settings.update({'runs_dir': runs_dir})
このスクリプトは従来のUltralyticsはデフォルトのパスを使用して学習時のデータを保存するところをyolo-demo/runs/を指定します。
これは一度だけ実行すれば設定は保存される(はず)なのでプロジェクトを始めるときに一度実行をします。
python3 setup-dirs.py
次に、学習させるときの学習用データと評価用のデータの分類をします。
#cwd: yolo-demo
mkdir -p data/{train,valid}/{images,labels}
その後、以下のファイルをyolo-demo/data/の下に配置します。
#!/usr/bin/env python
import os
import random
import re
data_dir = 'exported_data/images'
labels_dir = 'exported_data/labels'
train_data_dir = 'train'
test_data_dir = 'valid'
# Get all image files
image_files = [
f for f in os.listdir(data_dir) if os.path.isfile(os.path.join(data_dir, f))
]
# Get all label files
label_files = [
f for f in os.listdir(labels_dir) if os.path.isfile(os.path.join(labels_dir, f))
]
# Extract base names (without extension) for matching
image_names = {}
for img_file in image_files:
match = re.match(r"(.*).(jpg|png)", img_file)
if match:
base_name = match.group(1)
image_names[base_name] = img_file
label_names = set()
for label_file in label_files:
match = re.match(r"(.*).txt", label_file)
if match:
base_name = match.group(1)
label_names.add(base_name)
# Find files that exist in both directories
matched_files = []
for base_name in image_names:
if base_name in label_names:
# Include all files that have both image and label (even if label is empty)
matched_files.append((base_name, image_names[base_name]))
print(f"Found {len(matched_files)} files with both images and labels (including empty labels)")
print(f"Total images: {len(image_files)}")
print(f"Total labels: {len(label_files)}")
# Shuffle for random split
random.shuffle(matched_files)
# Create directories if they don't exist
os.makedirs(f"{train_data_dir}/images", exist_ok=True)
os.makedirs(f"{train_data_dir}/labels", exist_ok=True)
os.makedirs(f"{test_data_dir}/images", exist_ok=True)
os.makedirs(f"{test_data_dir}/labels", exist_ok=True)
# Calculate split point (80% train, 20% test)
split_point = len(matched_files) * 4 // 5
# Copy training files
for i in range(0, split_point):
base_name, image_file = matched_files[i]
os.system(f"cp {data_dir}/{image_file} {train_data_dir}/images/")
os.system(f"cp {labels_dir}/{base_name}.txt {train_data_dir}/labels/")
print(f"Copied {split_point} files to training set")
# Copy test files
for i in range(split_point, len(matched_files)):
base_name, image_file = matched_files[i]
os.system(f"cp {data_dir}/{image_file} {test_data_dir}/images/")
os.system(f"cp {labels_dir}/{base_name}.txt {test_data_dir}/labels/")
print(f"Copied {len(matched_files) - split_point} files to test set")
# Copy classes.txt
if os.path.exists(f"{labels_dir}/../classes.txt"):
os.system(f"cp {labels_dir}/../classes.txt {train_data_dir}/classes.txt")
os.system(f"cp {labels_dir}/../classes.txt {test_data_dir}/classes.txt")
print("Copied classes.txt to both directories")
そして、実行します。
#cwd: yolo-demo/data
python3 random-copy.py
簡単に何をしているのか説明すると、これは学習用の画像・ラベルデータと、評価用の学習・ラベルデータを4:1の割合でランダムにコピーします。
次に、Datasetの構造を示すファイルを作成します。
yolo-demo/data/の下に作成してください。
# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
train: train/
val: valid/
# number of classes
nc: 1
# class names
names: ["face"]
クラスの数、クラスの名前は適宜変更してください。
ただし、クラスの順番は、yolo-demo/data/exported_data/classes.txtの順番で書きましょう。
最後に、YOLOからベースになるモデルを取得して終わりです。
ここのページから好きなモデルを選択して、yolo-demo/base_model/xxx.ptとして保存します。
さて、いよいよ学習です。
#cwd yolo-demo/data
yolo detect train \
data=${PWD}/dataset.yml \
model=../base_model/xxx.pt \
epochs=30 imgsz=128
epochs、imgszは適宜変更してください。
さて、学習が着実に進んでいくのを眺めます。
最後の方に、
Results saved to /home/rotarymars/projects/personal/yolo-demo/runs/detect/train3
と出ると思います。
そこのパスに移動すると、以下のような感じになっていると思います。
#cwd: yolo-demo/runs/detect/train3
ls
args.yaml
BoxF1_curve.png
BoxP_curve.png
BoxPR_curve.png
BoxR_curve.png
confusion_matrix_normalized.png
confusion_matrix.png
labels.jpg
results.csv
results.png
train_batch0.jpg
train_batch1.jpg
train_batch20.jpg
train_batch21.jpg
train_batch22.jpg
train_batch2.jpg
val_batch0_labels.jpg
val_batch0_pred.jpg
weights
ここで、weights/best.ptというのが学習の過程で最も精度が良かったモデルです。
これをプロジェクトのルートにコピーします。
今度は、この画像を使って判定をかけます。
import os
import sys
from ultralytics import YOLO
import re
# Load a model
model = YOLO("best.pt") # load a custom model
dirname = os.path.dirname(__file__)
runs_dir = os.path.join(dirname, "runs", "predict")
for i in os.listdir("data/exported_data/images/"):
if re.match(r"^.*\.(jpg|png)$",i):
image_path = os.path.join("data/exported_data/images/", i)
results = model(source=image_path,
project=runs_dir,
name="predict",
save=True)
実際にはあまり意味はありませんが、今回は簡潔にするため、学習に使用した画像をそのまま判定にかけます。
これをyolo-demoで実行すると、
image 1/1 /home/rotarymars/projects/personal/yolo-demo/data/exported_data/images/syokuji_sennin_kasumi.png: 128x128 11 faces, 10.8ms
Speed: 0.3ms preprocess, 10.8ms inference, 2.3ms postprocess per image at shape (1, 3, 128, 128)
Results saved to /home/rotarymars/projects/personal/yolo-demo/runs/predict/predict
image 1/1 /home/rotarymars/projects/personal/yolo-demo/data/exported_data/images/monogatari_inaba_shirousagi_ookuninushi.png: 128x128 6 faces, 10.8ms
Speed: 0.3ms preprocess, 10.8ms inference, 1.6ms postprocess per image at shape (1, 3, 128, 128)
Results saved to /home/rotarymars/projects/personal/yolo-demo/runs/predict/predict2
image 1/1 /home/rotarymars/projects/personal/yolo-demo/data/exported_data/images/mukashibanashi_ojiisan_okina.png: 128x128 2 faces, 10.8ms
Speed: 0.3ms preprocess, 10.8ms inference, 1.0ms postprocess per image at shape (1, 3, 128, 128)
Results saved to /home/rotarymars/projects/personal/yolo-demo/runs/predict/predict3
image 1/1 /home/rotarymars/projects/personal/yolo-demo/data/exported_data/images/mukashibanashi_obaasan_ouna.png: 128x128 1 face, 10.7ms
Speed: 0.2ms preprocess, 10.7ms inference, 0.8ms postprocess per image at shape (1, 3, 128, 128)
Results saved to /home/rotarymars/projects/personal/yolo-demo/runs/predict/predict4
のような出力が続くので、yolo-demo/runs/predict/*/*.(png|jpg)の画像を見れば、検知された状態の画像を見ることができます。
(例)
終わりに
アドベントカレンダーを始めようという思いで記事を書きましたが、諦めそうな気持ちになりながら書き終えました。誰かのためになれば幸いです。








