YOLO学習データを自動生成する|射影変換・ラベル生成・データ拡張をまとめて自動化

YOLO

前回は、牌画像を貼り付けるベース画像に対して、牌を配置したい場所を4点でアノテーションするツールを作りました。

ここまでで、

撮影画像
↓
矩形アノテーション
↓
牌画像を切り出す
↓
ベース画像の牌配置領域を4点アノテーション

まで準備できました。

今回は、いよいよこれらを組み合わせて、YOLOの学習に使う画像とラベルを自動生成します。

今回作成したdataset_create.pyでは、

切り出した牌画像
+
4点アノテーションしたベース画像
↓
牌画像を射影変換
↓
ベース画像へ貼り付け
↓
YOLOラベルを自動生成
↓
明るさ・色・ノイズなどをランダムに変更
↓
画像を移動・回転
↓
train / val / test に分けて保存

までをまとめて自動化しています。

通常のYOLO学習では、撮影した画像を1枚ずつアノテーションしてデータを増やしていきます。

今回は、これまでの記事で準備してきた牌素材とベース画像を組み合わせて、新しい学習画像を大量に生成するという独自の方法を使います。

この記事の後半には、今回使用しているdataset_create.pyのコード全文も掲載しています。


今回のプログラムでやること

今回のプログラムは、これまで作ってきたツールをつなぐ役割を持っています。

大きく分けると、次の処理を行っています。

ベース画像と4点座標を読み込む
↓
切り出した牌画像をランダムに選ぶ
↓
4点の形へ射影変換して貼り付ける
↓
貼り付け位置からYOLOラベルを作る
↓
画像全体へデータ拡張をかける
↓
ラベルも画像の変形に合わせて更新する
↓
train / val / testへ保存

これによって、同じベース画像でも使用する牌や画像処理が毎回変わり、異なる学習画像を作れるようにしています。


4点座標に合わせて牌画像を射影変換する

前回の4点アノテーションで保存したTXTファイルを読み込みます。

TXTには、

x1,y1,x2,y2,x3,y3,x4,y4

という形で、牌を配置したい領域の4点が保存されています。

プログラムでは、この座標をload_quads()で読み込んでいます。

def load_quads(txt_path):

    quads = []

    if not os.path.exists(txt_path):
        return quads

    with open(txt_path, "r") as f:

        for line in f:

            line = line.strip()

            if not line:
                continue

            vals = list(map(float, line.split(",")))

            if len(vals) != 8:
                continue

            quad = np.float32([
                [vals[0], vals[1]],
                [vals[2], vals[3]],
                [vals[4], vals[5]],
                [vals[6], vals[7]],
            ])

            quads.append(quad)

    return quads

そして、切り出した牌画像の四隅と、ベース画像側の4点から射影変換行列を作ります。

H = cv2.getPerspectiveTransform(
    src_pts,
    dst_pts
)

その変換行列を使って、

warped_img = cv2.warpPerspective(
    crop_pad,
    H,
    (base_w, base_h),
    flags=cv2.INTER_LINEAR
)

とすることで、切り出した牌画像をベース画像上の形に合わせて変形します。

これにより、単純に画像を長方形のまま貼り付けるのではなく、ベース画像の角度や遠近感に合わせた形で牌を配置できます。


貼り付け部分が不自然にならないようにする

単純に牌画像を射影変換して貼り付けるだけでは、画像の境界が目立つことがあります。

そこで今回のプログラムでは、貼り付け先周辺の色を取得しています。

mean_color = get_surrounding_color(
    base_img,
    dst_pts
)

その色を使って、牌画像の周囲へパディングを追加します。

crop_pad = cv2.copyMakeBorder(
    crop_img,
    pad,
    pad,
    pad,
    pad,
    cv2.BORDER_CONSTANT,
    value=mean_color
)

さらに、貼り付け後の境界部分だけを抽出して、

blurred = cv2.GaussianBlur(
    result,
    (
        EDGE_BLUR_SIZE,
        EDGE_BLUR_SIZE
    ),
    0
)

少しぼかしています。

これによって、牌画像とベース画像の境界が目立ちにくくなるようにしています。


貼り付けた位置からYOLOラベルも自動生成する

画像だけを自動生成しても、YOLOで学習するにはラベルが必要です。

そこで、前回指定した4点座標から、自動的にYOLO形式の矩形ラベルも作成します。

def quad_to_yolo(quad, img_w, img_h):

    x_min = np.min(quad[:, 0]) - 2
    x_max = np.max(quad[:, 0]) + 2

    y_min = np.min(quad[:, 1]) - 10
    y_max = np.max(quad[:, 1]) + 10

    x_center = (x_min + x_max) / 2.0
    y_center = (y_min + y_max) / 2.0

    width = x_max - x_min
    height = y_max - y_min

    x_center /= img_w
    y_center /= img_h

    width /= img_w
    height /= img_h

    return (
        x_center,
        y_center,
        width,
        height
    )

4点を囲む外接矩形を作り、

class_id x_center y_center width height

というYOLO形式へ変換します。

今回のモデルでは牌の種類をYOLOで分類するのではなく、牌そのものを検出するため、クラスIDは0で保存しています。

label_lines.append(
    f"0 "
    f"{x_center:.6f} "
    f"{y_center:.6f} "
    f"{width:.6f} "
    f"{height:.6f}"
)

これによって、画像を作るたびに人が矩形アノテーションをする必要がありません。

画像生成とラベル生成を同時に自動化できることが、この方法の大きなメリットです。


同じ牌ばかりにならないようランダムに選ぶ

各配置場所には、croppedフォルダにある牌画像からランダムに1枚を選んで貼り付けます。

crop_path = random.choice(
    available_crops
)

ただし、完全にランダムにすると同じ牌ばかり選ばれる可能性があります。

そこで、1枚の生成画像内で同じ牌素材を使える回数を、

MAX_DUPLICATE = 4

に制限しています。(実際同じ柄の牌は4枚までなので)

available_crops = [
    path
    for path in crop_files
    if crop_use_count[path] < MAX_DUPLICATE
]

また、50%の確率で牌画像を180度回転させています。

if random.random() < 0.5:

    crop_img = cv2.rotate(
        crop_img,
        cv2.ROTATE_180
    )

これによって、同じ牌素材でも少し違った状態の画像を作れるようにしています。


画像全体にもデータ拡張をかける

牌を貼り付けたあと、画像全体にもランダムなデータ拡張をかけています。

今回設定している項目は次のとおりです。

AUGMENT_CONFIG = {
    "brightness": (0.7, 1.3),
    "contrast": (0.7, 1.3),
    "color": (0.7, 1.3),
    "sharpness": (0.5, 2.0),
    "gamma": (0.8, 1.2),
    "noise_sigma": (0, 10),
    "jpeg_quality": (50, 100),
    "rotate_deg": (-5, 5),
    "shift_x": (-0.05, 0.05),
    "shift_y": (-0.05, 0.05),
}

具体的には、

  • 明るさ
  • コントラスト
  • 彩度
  • シャープネス
  • ガンマ
  • ノイズ
  • JPEG圧縮
  • 回転
  • 上下左右への移動

をランダムに変更しています。

同じベース画像から作った画像でも、毎回少しずつ見え方が変わるため、学習データのバリエーションを増やせます。


回転や移動に合わせてラベルも更新する

画像を移動したり回転したりすると、当然YOLOの矩形位置も変わります。

そのため今回のプログラムでは、画像処理だけでなくラベルも同時に変換しています。

例えば画像を移動するときは、

img, new_labels = shift_image_and_boxes(
    img,
    label_lines
)

画像の移動量に合わせて矩形座標も移動します。

さらに回転するときは、

img, new_labels_ = rotate_image_and_boxes(
    img,
    new_labels
)

矩形の4頂点を画像中心のまわりに回転させ、その後の外接矩形を新しいYOLOラベルとして保存しています。

これによって、

画像だけ回転
↓
ラベル位置がずれる

という問題を防いでいます。


train・val・testも自動で振り分ける

生成した画像は、YOLOでそのまま使いやすいように、

train
val
test

へ自動で振り分けています。

比率は次のように設定しています。

TRAIN_RATIO = 0.8
VAL_RATIO = 0.1
TEST_RATIO = 0.1

生成されたフォルダは、

sample/
└── out_dir/
    ├── images/
    │   ├── train/
    │   ├── val/
    │   └── test/
    │
    └── labels/
        ├── train/
        ├── val/
        └── test/

という構成になります。

画像とラベルが最初からYOLO向けの構成で保存されるため、この後の学習へそのまま進められます。


1枚のベース画像から複数枚生成する

生成枚数は、

NUM = 10

で指定しています。

つまり現在の設定では、1枚のベース画像から10枚の学習画像を生成します。

例えばベース画像が100枚あれば、単純計算で1000枚の画像を作れます。

さらに、それぞれで、

  • 使用する牌
  • 180度回転
  • 明るさ
  • コントラスト
  • ノイズ
  • 位置
  • 回転角度

などがランダムに変化します。

これが、今回この自動生成方式を作った一番大きな目的です。


実際に自動生成した学習画像

ここまで説明してきた処理を使って、実際に生成した学習画像がこちらです。

同じベース画像を使っていても、

  • 貼り付けられる麻雀牌
  • 牌の向き
  • 明るさ
  • コントラスト
  • 色味
  • ノイズ
  • JPEG圧縮
  • 画像全体の回転
  • 上下左右への移動

がランダムに変わるため、それぞれ少しずつ異なる画像になります。

例えば、元になるベース画像が同じでも、

ベース画像
↓
生成①:明るめ・少し回転
生成②:暗め・ノイズあり
生成③:色味を変更
生成④:位置を少し移動
...

というように、複数のパターンを自動で作ることができます。

そして重要なのは、画像だけでなく、その画像に対応するYOLOラベルも同時に生成・変換されていることです。

そのため、生成後に改めて1枚ずつ矩形アノテーションする必要はありません。

現在は、

NUM = 10

としているため、1枚のベース画像から10パターン生成しています。

この値を増やせば、同じ素材からさらに多くの学習画像を作ることもできます。

今回この仕組みを作ったことで、

「素材を準備するところまでは人が行い、その後の大量生成とアノテーションはプログラムに任せる」

という形にすることができました。


完成したコード

今回使用したdataset_create.pyの全文です。

コードが長いため、デフォルトでは折りたたんでいます。必要に応じて「dataset_create.py の全文を見る」をクリックしてください。

dataset_create.py の全文を見る
import io
import os
import cv2
import random
import numpy as np
from PIL import Image, ImageEnhance

# 設定
BASE_DIR = "./sample/base_dir"
CROP_DIR = "./sample/crop_dir"
OUT_ROOT = "./sample/out_dir"

PAD = 5
EDGE_KERNEL_SIZE = 5
EDGE_BLUR_SIZE = 5
NUM = 10
MAX_DUPLICATE = 4
TRAIN_RATIO = 0.8
VAL_RATIO = 0.1
TEST_RATIO = 0.1


for split in ["train", "val", "test"]:
    os.makedirs(os.path.join(OUT_ROOT, "images", split), exist_ok=True)
    os.makedirs(os.path.join(OUT_ROOT, "labels", split), exist_ok=True)


# augmentation設定
AUGMENT_CONFIG = {
    "brightness": (0.7, 1.3),
    "contrast": (0.7, 1.3),
    "color": (0.7, 1.3),
    "sharpness": (0.5, 2.0),
    "gamma": (0.8, 1.2),
    "noise_sigma": (0, 10),
    "jpeg_quality": (50, 100),
    "rotate_deg": (-5, 5),
    "shift_x": (-0.05, 0.05),
    "shift_y": (-0.05, 0.05),
}

def clip_box(x1, y1, x2, y2, w, h):
    x1 = max(0, min(x1, w - 1))
    y1 = max(0, min(y1, h - 1))
    x2 = max(0, min(x2, w - 1))
    y2 = max(0, min(y2, h - 1))
    return x1, y1, x2, y2

def xyxy_to_yolo(x1, y1, x2, y2, w, h):
    xc = ((x1 + x2) / 2) / w
    yc = ((y1 + y2) / 2) / h
    bw = abs(x2 - x1) / w
    bh = abs(y2 - y1) / h
    return xc, yc, bw, bh

def apply_gamma(img, gamma):
    arr = np.array(img).astype(np.float32) / 255.0
    arr = np.power(arr, gamma)
    arr = np.clip(arr * 255, 0, 255).astype(np.uint8)
    return Image.fromarray(arr)

def apply_noise(img, sigma):
    arr = np.array(img).astype(np.float32)
    noise = np.random.normal(0, sigma, arr.shape)
    arr += noise
    arr = np.clip(arr, 0, 255).astype(np.uint8)
    return Image.fromarray(arr)

def apply_jpeg(img, quality):
    buf = io.BytesIO()
    img.save(buf, format="JPEG", quality=int(quality))
    buf.seek(0)
    return Image.open(buf).convert("RGB")

def augment_image(img):
    img = ImageEnhance.Brightness(img).enhance(
        random.uniform(*AUGMENT_CONFIG["brightness"])
    )
    img = ImageEnhance.Contrast(img).enhance(
        random.uniform(*AUGMENT_CONFIG["contrast"])
    )
    img = ImageEnhance.Color(img).enhance(
        random.uniform(*AUGMENT_CONFIG["color"])
    )
    img = ImageEnhance.Sharpness(img).enhance(
        random.uniform(*AUGMENT_CONFIG["sharpness"])
    )

    img = apply_gamma(
        img,
        random.uniform(*AUGMENT_CONFIG["gamma"])
    )

    img = apply_noise(
        img,
        random.uniform(*AUGMENT_CONFIG["noise_sigma"])
    )

    img = apply_jpeg(
        img,
        random.randint(*AUGMENT_CONFIG["jpeg_quality"])
    )

    return img

def shift_image_and_boxes(img, labels):
    w, h = img.size
    dx = int(random.uniform(*AUGMENT_CONFIG["shift_x"]) * w)
    dy = int(random.uniform(*AUGMENT_CONFIG["shift_y"]) * h)

    shifted = Image.new("RGB", (w, h), (0, 0, 0))
    shifted.paste(img, (dx, dy))

    out = []

    for line in labels:
        cid, xc, yc, bw, bh = map(float, line.split())
        x1 = (xc - bw/2) * w + dx
        y1 = (yc - bh/2) * h + dy
        x2 = (xc + bw/2) * w + dx
        y2 = (yc + bh/2) * h + dy
        x1,y1,x2,y2 = clip_box(x1,y1,x2,y2,w,h)
        if x2 <= x1 or y2 <= y1:
            continue
        xc2,yc2,bw2,bh2 = xyxy_to_yolo(x1,y1,x2,y2,w,h)
        out.append(f"{int(cid)} {xc2:.6f} {yc2:.6f} {bw2:.6f} {bh2:.6f}")

    return shifted, out


def rotate_image_and_boxes(img, labels):
    w, h = img.size
    angle = random.uniform(*AUGMENT_CONFIG["rotate_deg"])
    rotated = img.rotate(
        angle,
        expand=False,
        resample=Image.BICUBIC
    )
    rad = np.deg2rad(-angle)
    cx = w / 2.0
    cy = h / 2.0
    cos_r = np.cos(rad)
    sin_r = np.sin(rad)
    out = []

    for line in labels:
        cid, xc, yc, bw, bh = map(float, line.split())
        # YOLO -> xyxy
        box_w = bw * w
        box_h = bh * h

        x_center = xc * w
        y_center = yc * h

        x1 = x_center - box_w / 2
        y1 = y_center - box_h / 2
        x2 = x_center + box_w / 2
        y2 = y_center + box_h / 2

        # 4頂点
        corners = np.array([
            [x1, y1],
            [x2, y1],
            [x2, y2],
            [x1, y2]
        ])

        # 中心基準へ移動
        corners[:, 0] -= cx
        corners[:, 1] -= cy

        # 回転
        rotated_corners = np.zeros_like(corners)

        rotated_corners[:, 0] = (
            corners[:, 0] * cos_r -
            corners[:, 1] * sin_r
        )

        rotated_corners[:, 1] = (
            corners[:, 0] * sin_r +
            corners[:, 1] * cos_r
        )

        # 元の座標系へ戻す
        rotated_corners[:, 0] += cx
        rotated_corners[:, 1] += cy

        # 回転後の外接矩形
        new_x1 = rotated_corners[:, 0].min()
        new_y1 = rotated_corners[:, 1].min()
        new_x2 = rotated_corners[:, 0].max()
        new_y2 = rotated_corners[:, 1].max()

        # 画像外クリップ
        new_x1, new_y1, new_x2, new_y2 = clip_box(
            new_x1,
            new_y1,
            new_x2,
            new_y2,
            w,
            h
        )

        if new_x2 <= new_x1 or new_y2 <= new_y1:
            continue

        xc2, yc2, bw2, bh2 = xyxy_to_yolo(
            new_x1,
            new_y1,
            new_x2,
            new_y2,
            w,
            h
        )

        out.append(f"{int(cid)} {xc2:.6f} {yc2:.6f} {bw2:.6f} {bh2:.6f}")

    return rotated, out



def load_quads(txt_path):
    quads = []

    if not os.path.exists(txt_path):
        return quads

    with open(txt_path, "r") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue

            vals = list(map(float, line.split(",")))

            if len(vals) != 8:
                continue

            quad = np.float32([
                [vals[0], vals[1]],
                [vals[2], vals[3]],
                [vals[4], vals[5]],
                [vals[6], vals[7]],
            ])

            quads.append(quad)
    return quads


def get_surrounding_color(base_img, dst_pts):
    h, w = base_img.shape[:2]

    x_min = int(np.min(dst_pts[:, 0]))
    x_max = int(np.max(dst_pts[:, 0]))

    y_min = int(np.min(dst_pts[:, 1]))
    y_max = int(np.max(dst_pts[:, 1]))

    margin = 20

    x1 = max(0, x_min - margin)
    x2 = min(w, x_max + margin)

    y1 = max(0, y_min - margin)
    y2 = min(h, y_max + margin)

    region = base_img[y1:y2, x1:x2]

    if region.size == 0:
        return (127, 127, 127)

    mean_color = tuple(
        int(v)
        for v in region.mean(axis=(0, 1))
    )

    return mean_color


def paste_perspective(base_img, crop_img, dst_pts, pad=100,):
    base_h, base_w = base_img.shape[:2]
    crop_h, crop_w = crop_img.shape[:2]

    mean_color = get_surrounding_color(
        base_img,
        dst_pts
    )

    crop_pad = cv2.copyMakeBorder(
        crop_img,
        pad,
        pad,
        pad,
        pad,
        cv2.BORDER_CONSTANT,
        value=mean_color
    )

    pad_h, pad_w = crop_pad.shape[:2]

    mask = np.zeros((pad_h, pad_w), dtype=np.uint8)

    mask[pad:pad + crop_h, pad:pad + crop_w] = 255


    src_pts = np.float32([
        [pad, pad],
        [pad + crop_w - 1, pad],
        [pad + crop_w - 1, pad + crop_h - 1],
        [pad, pad + crop_h - 1]
    ])

    H = cv2.getPerspectiveTransform(
        src_pts,
        dst_pts
    )

    warped_img = cv2.warpPerspective(
        crop_pad,
        H,
        (base_w, base_h),
        flags=cv2.INTER_LINEAR
    )

    warped_mask = cv2.warpPerspective(
        mask,
        H,
        (base_w, base_h),
        flags=cv2.INTER_NEAREST
    )

    result = base_img.copy()

    result[warped_mask > 0] = warped_img[warped_mask > 0]

    kernel = np.ones(
        (
            EDGE_KERNEL_SIZE,
            EDGE_KERNEL_SIZE
        ),
        np.uint8
    )

    edge_mask = cv2.morphologyEx(
        warped_mask,
        cv2.MORPH_GRADIENT,
        kernel
    )

    blurred = cv2.GaussianBlur(result, (EDGE_BLUR_SIZE,EDGE_BLUR_SIZE), 0)

    result[edge_mask > 0] = blurred[
        edge_mask > 0
    ]

    return result


def quad_to_yolo(quad, img_w, img_h):
    x_min = np.min(quad[:, 0]) - 2
    x_max = np.max(quad[:, 0]) + 2

    y_min = np.min(quad[:, 1]) - 10
    y_max = np.max(quad[:, 1]) + 10

    x_center = (x_min + x_max) / 2.0
    y_center = (y_min + y_max) / 2.0

    width = x_max - x_min
    height = y_max - y_min

    x_center /= img_w
    y_center /= img_h

    width /= img_w
    height /= img_h

    return (
        x_center,
        y_center,
        width,
        height
    )


def get_random_split():
    r = random.random()
    if r < TRAIN_RATIO:
        return "train"
    elif r < TRAIN_RATIO + VAL_RATIO:
        return "val"
    else:
        return "test"



crop_files = sorted([
    os.path.join(CROP_DIR, f)
    for f in os.listdir(CROP_DIR)
    if f.lower().endswith(".jpg")
])

if len(crop_files) == 0:
    raise RuntimeError(
        "crop jpg が見つかりません"
    )


base_files = sorted([
    f
    for f in os.listdir(BASE_DIR)
    if f.lower().endswith(".jpg")
])

for idx, file_name in enumerate(base_files):
    print(f"{idx} / {len(base_files)}")

    base_path = os.path.join(BASE_DIR,file_name)
    txt_path = os.path.splitext(base_path)[0] + ".txt"

    if not os.path.exists(txt_path):
        print("skip (txtなし):", file_name)
        continue

    print("processing:", file_name)

    base_img = cv2.imread(base_path)

    if base_img is None:
        print("skip (読込失敗):", file_name)
        continue

    quads = load_quads(txt_path)
    stem = os.path.splitext(file_name)[0]

    for aug_idx in range(NUM):
        result_img = base_img.copy()
        # crop使用回数管理
        crop_use_count = {
            path: 0
            for path in crop_files
        }

        label_lines = []
        for quad in quads:
            available_crops = [
                path
                for path in crop_files
                if crop_use_count[path] < MAX_DUPLICATE
            ]

            if len(available_crops) == 0:
                break

            crop_path = random.choice(available_crops)

            crop_use_count[crop_path] += 1

            crop_img = cv2.imread(crop_path)

            if crop_img is None:
                continue


            if random.random() < 0.5:

                crop_img = cv2.rotate(
                    crop_img,
                    cv2.ROTATE_180
                )

            result_img = paste_perspective(
                result_img,
                crop_img,
                quad,
                pad=PAD
            )
            
            img_h, img_w = result_img.shape[:2]

            x_center, y_center, width, height = quad_to_yolo(
                quad,
                img_w,
                img_h
            )

            label_lines.append(
                f"0 "
                f"{x_center:.6f} "
                f"{y_center:.6f} "
                f"{width:.6f} "
                f"{height:.6f}"
            )
        
        
        # 合成後に画像処理で変換
        result_img
        img = Image.fromarray(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB))
        img = augment_image(img)
        img, new_labels = shift_image_and_boxes(img, label_lines)
        img, new_labels_ = rotate_image_and_boxes(img, new_labels)
         
        split = get_random_split() 

        img.save(os.path.join(OUT_ROOT, "images", split, f"{stem}_{aug_idx:03d}.jpg"))

        with open(os.path.join(OUT_ROOT, "labels", split, f"{stem}_{aug_idx:03d}.txt"), "w") as f:
            f.write("\n".join(new_labels_))

print("finished")

実行方法

最後に、今回の学習データ自動生成プログラムの実行方法を紹介します。

必要なライブラリ

今回使用している主なライブラリは、

  • OpenCV
  • NumPy
  • Pillow

です。

入っていない場合はインストールします。

pip install opencv-python numpy pillow

素材を用意する

まず、これまでの記事で作成した2種類のデータを用意します。

① 切り出した牌画像
② 4点アノテーション済みのベース画像

コードでは、次のフォルダを使用しています。

BASE_DIR = "./sample/base_dir"
CROP_DIR = "./sample/crop_dir"

ベース画像側は、画像と同名のTXTファイルを配置します。

base_001.jpg
base_001.txt

base_002.jpg
base_002.txt

TXTには、前回作成した4点座標が入っています。

出力先を設定する

生成したデータの保存先は、

OUT_ROOT = "./sample/out_dir"

で指定しています。

必要に応じて好きなフォルダへ変更します。

生成枚数を設定する

1枚のベース画像から生成する枚数は、

NUM = 10

で変更できます。

例えば、

NUM = 100

にすれば、1枚のベース画像から100枚生成します。

プログラムを実行する

設定が終わったら、

python dataset_create.py

を実行します。

環境によっては、

python3 dataset_create.py

で実行します。

処理が完了すると、

out_dir/
├── images/
│   ├── train/
│   ├── val/
│   └── test/
│
└── labels/
    ├── train/
    ├── val/
    └── test/

へ画像とYOLOラベルが保存されます。

これで、YOLOを学習するためのデータセットが完成です。


まとめ

今回は、これまで準備してきた牌画像とベース画像を使って、YOLO学習データを自動生成するプログラムを作りました。

今回の処理では、

切り出した牌画像
↓
ベース画像の4点へ射影変換
↓
貼り付け
↓
YOLOラベルを自動生成
↓
画像をデータ拡張
↓
ラベルも同時に変換
↓
train / val / testへ保存

までを自動化しています。

この仕組みを作ったことで、1枚ずつ撮影してアノテーションするだけではなく、一度用意した素材から大量の学習データを作れるようになりました。

今回のシリーズで作ってきた、

撮影
↓
矩形アノテーション
↓
牌画像を切り出す
↓
ベース画像を4点アノテーション
↓
データセットを自動生成

という独自の学習データ作成の流れが、ここでつながります。

次回はいよいよ、今回作成したデータセットを使ってUltralytics YOLOを実際に学習します。

YAMLファイルの設定から学習、学習結果の確認、推論までをまとめて紹介します。


関連記事

コメント

タイトルとURLをコピーしました