【YOLO 物件偵測教學】#03 資料集準備:標註、格式轉換與資料增強

YOLO 資料集準備 – 基礎測驗

1. YOLO 標註檔的格式中,座標值的範圍是多少?

2. YOLO 標註檔每一行的格式順序是什麼?

3. 在 YOLOv8 的資料集目錄結構中,圖片和標註檔應該放在哪裡?

4. Mosaic 資料增強技術的特點是什麼?

5. data.yaml 設定檔的主要作用是什麼?

測驗完成!

0/5

前言

訓練 YOLO 模型就像教小孩認識物品,你需要準備大量的「教材」——也就是已經標記好答案的圖片。這篇文章會帶你走過資料集準備的完整流程:從標註工具的選擇、YOLO 格式的理解,到資料增強技術的應用。

這是系列第 3 篇,假設你已經讀過前兩篇,了解 YOLO 的基本概念和環境設定。


一、標註工具介紹

標註工具的作用是讓你在圖片上畫框框,告訴模型「這裡有一隻貓」、「那裡有一輛車」。以下是三款常見的標註工具:

1. LabelImg(本地端、免費)

LabelImg 是經典的桌面標註工具,直接支援 YOLO 格式輸出。

安裝方式:

pip install labelImg

啟動方式:

labelImg

操作流程:

  1. 開啟圖片資料夾(Open Dir)
  2. 設定輸出格式為 YOLO(左側選單)
  3. W 鍵開始畫框
  4. 選擇類別名稱
  5. Ctrl+S 儲存標註

小提醒:LabelImg 會為每張圖片產生一個同名的 .txt 檔案。

2. Roboflow(雲端、有免費方案)

Roboflow 是一站式的資料集管理平台,特別適合團隊協作。

優點:

  • 網頁介面,不需安裝
  • 自動資料增強功能
  • 支援多種匯出格式
  • 提供預訓練資料集

使用流程:

  1. 建立帳號並新增專案
  2. 上傳圖片
  3. 在網頁上進行標註
  4. 匯出為 YOLOv8 格式

3. CVAT(自建或雲端、開源免費)

CVAT(Computer Vision Annotation Tool)是 Intel 開發的開源標註工具,功能最完整。

適合場景:

  • 大型專案、需要多人協作
  • 需要影片標註功能
  • 企業內部自建標註系統

本地部署(使用 Docker):

git clone https://github.com/opencv/cvat.git
cd cvat
docker compose up -d
Code language: PHP (php)

二、YOLO 標註格式解析

YOLO 的標註格式是一種簡潔的純文字格式,每張圖片對應一個 .txt 檔案。

格式結構

每一行代表一個物件,格式為:

<class_id> <x_center> <y_center> <width> <height>
Code language: HTML, XML (xml)

各欄位說明:

欄位 說明 範圍
class_id 類別編號(從 0 開始) 整數
x_center 框框中心點的 X 座標(正規化) 0.0 ~ 1.0
y_center 框框中心點的 Y 座標(正規化) 0.0 ~ 1.0
width 框框寬度(正規化) 0.0 ~ 1.0
height 框框高度(正規化) 0.0 ~ 1.0

正規化座標的意義

「正規化」是指將實際像素座標除以圖片的寬度或高度。這樣做的好處是:

  • 不同尺寸的圖片可以使用相同的座標系統
  • 訓練時圖片縮放不會影響標註

計算公式:

x_center = (x_min + x_max) / 2 / image_width
y_center = (y_min + y_max) / 2 / image_height
width = (x_max - x_min) / image_width
height = (y_max - y_min) / image_height

實際範例

假設有一張 640×480 的圖片,上面有一隻貓(類別 0),框框位置為左上角 (100, 150),右下角 (300, 350):

# 原始像素座標
x_min, y_min = 100, 150
x_max, y_max = 300, 350
image_width, image_height = 640, 480

# 計算正規化座標
x_center = (100 + 300) / 2 / 640  # = 0.3125
y_center = (150 + 350) / 2 / 480  # = 0.5208
width = (300 - 100) / 640          # = 0.3125
height = (350 - 150) / 480         # = 0.4167

# 標註檔內容
# 0 0.3125 0.5208 0.3125 0.4167
Code language: PHP (php)

對應的 image001.txt 內容:

0 0.3125 0.5208 0.3125 0.4167
Code language: CSS (css)

如果同一張圖片有多個物件,每個物件一行:

0 0.3125 0.5208 0.3125 0.4167
1 0.7500 0.6000 0.2000 0.3000
Code language: CSS (css)

三、資料集目錄結構

YOLOv8 預期的資料集結構非常明確:

dataset/
├── data.yaml           # 資料集設定檔
├── train/
│   ├── images/         # 訓練圖片
│   │   ├── img001.jpg
│   │   ├── img002.jpg
│   │   └── ...
│   └── labels/         # 訓練標註
│       ├── img001.txt
│       ├── img002.txt
│       └── ...
├── valid/
│   ├── images/         # 驗證圖片
│   └── labels/         # 驗證標註
└── test/               # (選用)測試集
    ├── images/
    └── labels/
Code language: PHP (php)

關鍵規則:

  • 圖片和標註檔必須同名(只有副檔名不同)
  • images/labels/ 資料夾必須在同一層目錄下
  • 沒有物件的圖片,對應的 .txt 檔可以是空的(或不建立)

四、data.yaml 設定檔

data.yaml 是告訴 YOLO 資料集在哪裡、有哪些類別的設定檔。

基本結構

# 資料集路徑(可用絕對或相對路徑)
path: /path/to/dataset    # 資料集根目錄
train: train/images       # 訓練集相對於 path 的路徑
val: valid/images         # 驗證集相對於 path 的路徑
test: test/images         # (選用)測試集

# 類別定義
names:
  0: cat
  1: dog
  2: bird
Code language: PHP (php)

實際範例:寵物偵測資料集

path: ./pet_dataset
train: train/images
val: valid/images

names:
  0: cat
  1: dog
  2: rabbit
  3: hamster
Code language: HTTP (http)

使用這個設定檔訓練

from ultralytics import YOLO

model = YOLO('yolov8n.pt')
results = model.train(data='pet_dataset/data.yaml', epochs=100)
Code language: JavaScript (javascript)

五、格式轉換

如果你的標註檔是其他格式(例如 COCO、Pascal VOC),需要轉換為 YOLO 格式。

COCO 格式轉 YOLO 格式

COCO 格式使用 JSON 檔案,標註是絕對像素座標:

{
  "images": [...],
  "annotations": [
    {
      "image_id": 1,
      "category_id": 1,
      "bbox": [100, 150, 200, 200]  // [x, y, width, height]
    }
  ],
  "categories": [...]
}
Code language: JSON / JSON with Comments (json)

轉換腳本:

import json
import os

def coco_to_yolo(coco_json_path, output_dir, image_dir):
    """將 COCO 格式轉換為 YOLO 格式"""

    with open(coco_json_path, 'r') as f:
        coco_data = json.load(f)

    # 建立圖片 ID 到檔名和尺寸的映射
    images = {img['id']: img for img in coco_data['images']}

    # 建立類別 ID 映射(COCO 類別 ID 可能不連續)
    category_map = {cat['id']: idx for idx, cat in enumerate(coco_data['categories'])}

    # 依照圖片分組標註
    annotations_by_image = {}
    for ann in coco_data['annotations']:
        img_id = ann['image_id']
        if img_id not in annotations_by_image:
            annotations_by_image[img_id] = []
        annotations_by_image[img_id].append(ann)

    os.makedirs(output_dir, exist_ok=True)

    # 轉換每張圖片的標註
    for img_id, img_info in images.items():
        img_w = img_info['width']
        img_h = img_info['height']
        file_name = os.path.splitext(img_info['file_name'])[0]

        yolo_lines = []
        for ann in annotations_by_image.get(img_id, []):
            # COCO bbox: [x, y, width, height](左上角座標)
            x, y, w, h = ann['bbox']

            # 轉換為 YOLO 格式(中心點、正規化)
            x_center = (x + w / 2) / img_w
            y_center = (y + h / 2) / img_h
            width = w / img_w
            height = h / img_h

            class_id = category_map[ann['category_id']]
            yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")

        # 寫入 YOLO 標註檔
        output_path = os.path.join(output_dir, f"{file_name}.txt")
        with open(output_path, 'w') as f:
            f.write('\n'.join(yolo_lines))

# 使用方式
coco_to_yolo(
    'annotations.json',
    'labels/',
    'images/'
)
Code language: PHP (php)

Pascal VOC 格式轉 YOLO 格式

Pascal VOC 使用 XML 檔案,每張圖片一個:

<annotation>
  <size>
    <width>640</width>
    <height>480</height>
  </size>
  <object>
    <name>cat</name>
    <bndbox>
      <xmin>100</xmin>
      <ymin>150</ymin>
      <xmax>300</xmax>
      <ymax>350</ymax>
    </bndbox>
  </object>
</annotation>
Code language: HTML, XML (xml)

轉換腳本:

import xml.etree.ElementTree as ET
import os

def voc_to_yolo(xml_path, class_names):
    """將單個 VOC XML 轉換為 YOLO 格式字串"""

    tree = ET.parse(xml_path)
    root = tree.getroot()

    # 取得圖片尺寸
    size = root.find('size')
    img_w = int(size.find('width').text)
    img_h = int(size.find('height').text)

    yolo_lines = []
    for obj in root.findall('object'):
        class_name = obj.find('name').text
        if class_name not in class_names:
            continue

        class_id = class_names.index(class_name)

        bbox = obj.find('bndbox')
        xmin = float(bbox.find('xmin').text)
        ymin = float(bbox.find('ymin').text)
        xmax = float(bbox.find('xmax').text)
        ymax = float(bbox.find('ymax').text)

        # 轉換為 YOLO 格式
        x_center = (xmin + xmax) / 2 / img_w
        y_center = (ymin + ymax) / 2 / img_h
        width = (xmax - xmin) / img_w
        height = (ymax - ymin) / img_h

        yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")

    return '\n'.join(yolo_lines)

# 批次轉換
def batch_convert_voc_to_yolo(voc_dir, output_dir, class_names):
    os.makedirs(output_dir, exist_ok=True)

    for xml_file in os.listdir(voc_dir):
        if not xml_file.endswith('.xml'):
            continue

        xml_path = os.path.join(voc_dir, xml_file)
        yolo_content = voc_to_yolo(xml_path, class_names)

        output_path = os.path.join(output_dir, xml_file.replace('.xml', '.txt'))
        with open(output_path, 'w') as f:
            f.write(yolo_content)

# 使用方式
class_names = ['cat', 'dog', 'bird']
batch_convert_voc_to_yolo('voc_annotations/', 'yolo_labels/', class_names)
Code language: PHP (php)

六、資料增強策略

資料增強是用程式自動產生更多變化版本的訓練資料,讓模型學到更多樣的特徵。

YOLOv8 內建的資料增強

YOLOv8 訓練時會自動套用這些增強:

from ultralytics import YOLO

model = YOLO('yolov8n.pt')

# 這些是預設值,可以自行調整
results = model.train(
    data='data.yaml',
    epochs=100,

    # 幾何變換
    flipud=0.0,        # 上下翻轉機率
    fliplr=0.5,        # 左右翻轉機率
    degrees=0.0,       # 旋轉角度範圍(-degrees ~ +degrees)
    translate=0.1,     # 平移範圍(圖片尺寸的比例)
    scale=0.5,         # 縮放範圍
    shear=0.0,         # 剪切角度
    perspective=0.0,   # 透視變換

    # 色彩變換
    hsv_h=0.015,       # 色相變化範圍
    hsv_s=0.7,         # 飽和度變化範圍
    hsv_v=0.4,         # 明度變化範圍

    # 進階增強
    mosaic=1.0,        # Mosaic 增強機率
    mixup=0.0,         # MixUp 增強機率
    copy_paste=0.0,    # Copy-Paste 增強機率
)
Code language: PHP (php)

Mosaic 增強

Mosaic 是 YOLO 的招牌資料增強技術,將 4 張圖片拼成一張:

+--------+--------+
| 圖片 1  | 圖片 2  |
+--------+--------+
| 圖片 3  | 圖片 4  |
+--------+--------+

好處:

  • 一次訓練看到更多物件
  • 學習不同背景下的物件
  • 提升小物件的偵測能力

MixUp 增強

MixUp 將兩張圖片「混合」(透明度疊加),標註也按比例混合:

混合圖片 = 0.5 * 圖片A + 0.5 * 圖片B

七、使用 Albumentations 進行進階資料增強

Albumentations 是專業的影像增強函式庫,可以和 YOLO 搭配使用。

安裝

pip install albumentations

基本範例:離線增強資料集

import albumentations as A
import cv2
import os

def augment_dataset(image_dir, label_dir, output_image_dir, output_label_dir, num_augmented=3):
    """對資料集進行離線增強"""

    # 定義增強流程(標註會自動轉換)
    transform = A.Compose([
        A.HorizontalFlip(p=0.5),
        A.RandomBrightnessContrast(p=0.5),
        A.GaussNoise(var_limit=(10, 50), p=0.3),
        A.Blur(blur_limit=3, p=0.2),
        A.Rotate(limit=15, p=0.3),
    ], bbox_params=A.BboxParams(
        format='yolo',           # 使用 YOLO 格式
        label_fields=['class_labels']
    ))

    os.makedirs(output_image_dir, exist_ok=True)
    os.makedirs(output_label_dir, exist_ok=True)

    for image_file in os.listdir(image_dir):
        if not image_file.lower().endswith(('.jpg', '.png', '.jpeg')):
            continue

        # 讀取圖片
        image_path = os.path.join(image_dir, image_file)
        image = cv2.imread(image_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        # 讀取標註
        label_file = os.path.splitext(image_file)[0] + '.txt'
        label_path = os.path.join(label_dir, label_file)

        bboxes = []
        class_labels = []
        if os.path.exists(label_path):
            with open(label_path, 'r') as f:
                for line in f:
                    parts = line.strip().split()
                    if len(parts) == 5:
                        class_id = int(parts[0])
                        bbox = [float(x) for x in parts[1:5]]
                        bboxes.append(bbox)
                        class_labels.append(class_id)

        # 儲存原始圖片
        base_name = os.path.splitext(image_file)[0]
        cv2.imwrite(
            os.path.join(output_image_dir, image_file),
            cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        )
        if bboxes:
            with open(os.path.join(output_label_dir, label_file), 'w') as f:
                for cls, bbox in zip(class_labels, bboxes):
                    f.write(f"{cls} {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}\n")

        # 產生增強版本
        for i in range(num_augmented):
            try:
                transformed = transform(
                    image=image,
                    bboxes=bboxes,
                    class_labels=class_labels
                )

                aug_image = transformed['image']
                aug_bboxes = transformed['bboxes']
                aug_labels = transformed['class_labels']

                # 儲存增強後的圖片
                aug_image_file = f"{base_name}_aug{i}.jpg"
                cv2.imwrite(
                    os.path.join(output_image_dir, aug_image_file),
                    cv2.cvtColor(aug_image, cv2.COLOR_RGB2BGR)
                )

                # 儲存增強後的標註
                aug_label_file = f"{base_name}_aug{i}.txt"
                with open(os.path.join(output_label_dir, aug_label_file), 'w') as f:
                    for cls, bbox in zip(aug_labels, aug_bboxes):
                        f.write(f"{cls} {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}\n")

            except Exception as e:
                print(f"增強失敗 {image_file}: {e}")
                continue

# 使用方式
augment_dataset(
    'dataset/train/images',
    'dataset/train/labels',
    'dataset_augmented/train/images',
    'dataset_augmented/train/labels',
    num_augmented=3  # 每張圖產生 3 個增強版本
)
Code language: PHP (php)

常用的增強組合

# 輕度增強(適合資料量充足時)
light_transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.5),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))

# 中度增強(一般情況)
medium_transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),
    A.RandomBrightnessContrast(p=0.5),
    A.GaussNoise(p=0.2),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))

# 重度增強(資料量不足時)
heavy_transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.2),
    A.ShiftScaleRotate(shift_limit=0.2, scale_limit=0.3, rotate_limit=30, p=0.7),
    A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5),
    A.GaussNoise(var_limit=(10, 80), p=0.3),
    A.Blur(blur_limit=5, p=0.3),
    A.CLAHE(p=0.2),
    A.HueSaturationValue(p=0.3),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
Code language: PHP (php)

八、實作練習:準備一個完整資料集

以下是準備資料集的完整流程:

步驟 1:收集圖片

# 假設你已經有一批圖片在 raw_images/ 資料夾
import os
import shutil

def organize_images(source_dir, dest_dir, train_ratio=0.8):
    """將圖片分成訓練集和驗證集"""

    images = [f for f in os.listdir(source_dir)
              if f.lower().endswith(('.jpg', '.png', '.jpeg'))]

    # 隨機打亂
    import random
    random.shuffle(images)

    # 計算分割點
    split_idx = int(len(images) * train_ratio)
    train_images = images[:split_idx]
    valid_images = images[split_idx:]

    # 建立目錄結構
    for split, image_list in [('train', train_images), ('valid', valid_images)]:
        img_dir = os.path.join(dest_dir, split, 'images')
        lbl_dir = os.path.join(dest_dir, split, 'labels')
        os.makedirs(img_dir, exist_ok=True)
        os.makedirs(lbl_dir, exist_ok=True)

        for img in image_list:
            shutil.copy(
                os.path.join(source_dir, img),
                os.path.join(img_dir, img)
            )

    print(f"訓練集:{len(train_images)} 張")
    print(f"驗證集:{len(valid_images)} 張")

organize_images('raw_images/', 'my_dataset/')
Code language: PHP (php)

步驟 2:使用 LabelImg 標註

# 啟動 LabelImg
labelImg my_dataset/train/images my_dataset/classes.txt my_dataset/train/labels
Code language: PHP (php)

classes.txt 內容(每行一個類別):

cat
dog
bird

步驟 3:建立 data.yaml

# 自動產生 data.yaml
def create_data_yaml(dataset_dir, class_names, output_path):
    import yaml

    data = {
        'path': os.path.abspath(dataset_dir),
        'train': 'train/images',
        'val': 'valid/images',
        'names': {i: name for i, name in enumerate(class_names)}
    }

    with open(output_path, 'w') as f:
        yaml.dump(data, f, default_flow_style=False, allow_unicode=True)

    print(f"data.yaml 已建立:{output_path}")

create_data_yaml(
    'my_dataset/',
    ['cat', 'dog', 'bird'],
    'my_dataset/data.yaml'
)
Code language: PHP (php)

步驟 4:驗證資料集

def validate_dataset(data_yaml_path):
    """檢查資料集的完整性"""
    import yaml

    with open(data_yaml_path, 'r') as f:
        data = yaml.safe_load(f)

    base_path = data['path']

    for split in ['train', 'val']:
        img_dir = os.path.join(base_path, data[split])
        lbl_dir = img_dir.replace('images', 'labels')

        images = set(os.path.splitext(f)[0] for f in os.listdir(img_dir)
                    if f.lower().endswith(('.jpg', '.png', '.jpeg')))
        labels = set(os.path.splitext(f)[0] for f in os.listdir(lbl_dir)
                    if f.endswith('.txt'))

        missing_labels = images - labels
        orphan_labels = labels - images

        print(f"\n{split} 資料集:")
        print(f"  圖片數量:{len(images)}")
        print(f"  標註數量:{len(labels)}")

        if missing_labels:
            print(f"  缺少標註:{len(missing_labels)} 個")
        if orphan_labels:
            print(f"  孤立標註:{len(orphan_labels)} 個")

validate_dataset('my_dataset/data.yaml')
Code language: PHP (php)

總結

這篇文章涵蓋了資料集準備的完整流程:

主題 重點
標註工具 LabelImg(本地)、Roboflow(雲端)、CVAT(企業級)
YOLO 格式 class x_center y_center width height,座標正規化到 0~1
目錄結構 images/ 和 labels/ 對應,檔名相同
data.yaml 定義路徑和類別名稱
資料增強 YOLOv8 內建 Mosaic、可搭配 Albumentations

下一篇我們會進入模型訓練,學習如何調整超參數、監控訓練過程,以及處理常見的訓練問題。


延伸資源

YOLO 資料集準備 – 進階測驗

測試你對格式轉換、座標計算和資料增強的深入理解

1. 座標計算題 計算題

假設有一張 800×600 的圖片,物件框框的左上角座標是 (200, 150),右下角是 (400, 450)。請計算 YOLO 格式的 x_center 值(取到小數點後兩位)。

2. 格式差異題 比較分析

COCO 格式的 bbox 是 [x, y, width, height],其中 (x, y) 代表什麼?這和 YOLO 格式有什麼不同?

3. 資料增強配置題 實作應用

在 YOLOv8 訓練時,以下哪個增強配置最適合用於「訓練後期減少增強干擾」的情境?

# 配置 A
mosaic=1.0, mixup=0.5

# 配置 B
mosaic=0.0, hsv_h=0.015

# 配置 C
fliplr=0.5, degrees=45

# 配置 D
mosaic=0.0, mixup=0.0, fliplr=0.5

4. 程式碼分析題 除錯

以下的 VOC 轉 YOLO 程式碼有一個潛在問題,是什麼?

def voc_to_yolo(xml_path, class_names):
    tree = ET.parse(xml_path)
    root = tree.getroot()
    size = root.find('size')
    img_w = int(size.find('width').text)
    img_h = int(size.find('height').text)

    for obj in root.findall('object'):
        class_name = obj.find('name').text
        class_id = class_names.index(class_name)  # <-- 這行
        # ... 後續處理

5. Albumentations 應用題 進階應用

使用 Albumentations 進行物件偵測的資料增強時,為什麼需要設定 bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])

進階測驗完成!

0/5

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *