2. 数据集构建与标注:从零搭好你的训练地基

做目标检测,尤其是 YOLO 这种监督学习模型,数据就是你的命根子。模型再牛,喂进去的是垃圾,吐出来的还是垃圾。这一章,我带你走一遍从采集到标注再到划分的完整流程。说白了,就是教会你怎么给模型准备一份「标准答案」。

2.1 数据采集策略:别让模型「偏科」

很多新手上来就找一堆网图,标注完直接开训。结果模型在测试集上表现不错,一到真实场景就翻车。为什么?因为数据分布太单一了。

我个人习惯,在采集阶段会先问自己三个问题:

  • 场景够不够全? 白天、夜晚、逆光、阴天、室内、室外……你的模型最终要在什么环境下跑,数据里就得有什么。
  • 目标够不够「歪」? 正面的、侧面的、被遮挡的、模糊的、只有一半的。YOLO 要学的是目标的通用特征,不是「证件照」。
  • 背景够不够杂? 纯色背景和复杂街景,模型学到的特征完全不同。我建议至少 30% 的数据要包含复杂背景。
我的小技巧: 如果条件允许,用手机或摄像头自己拍一段视频,然后按帧抽图。这样能保证场景的连续性和真实性,比网上东拼西凑的图靠谱得多。

另外,数据量不是越多越好。对于 YOLOv8 这种模型,每个类别有个 500-1000 张高质量标注图,基本就能训出一个可用的模型。再多,边际效益就递减了。

2.2 LabelImg 标注工具:手把手画出你的第一个框

标注工具很多,但 LabelImg 是我用得最顺手的。轻量、免费、支持 YOLO 和 VOC 格式。嗯,这里要注意,它虽然界面简陋,但功能一点不差。

安装方式(我推荐用 pip):

pip install labelImg
labelImg

启动后,界面很简单。我带你走一遍核心操作:

  1. 打开图片目录:点击「Open Dir」,选择你存放图片的文件夹。
  2. 选择保存格式:点击「Change Save Dir」设置标注文件保存路径。然后在菜单栏 View 里勾选「Auto Save mode」和「Single Class Mode」(如果你一次只标一个类别)。
  3. 创建矩形框:按键盘 W 键,鼠标变成十字。从目标左上角拖到右下角,松开。
  4. 输入标签:弹窗里输入类别名称,比如 person、car。注意大小写,后面训练时类别名要一致。
  5. 快捷键加速:
    • A / D:上一张 / 下一张图片
    • Ctrl + S:保存
    • Ctrl + 鼠标滚轮:缩放图片
    • Delete:删除当前选中的框
我曾经踩过的坑: 标注框一定要紧贴目标边缘,不要留太多空白,也不要切掉目标的关键部位。YOLO 的损失函数对框的精度很敏感,框偏了 5 个像素,模型学出来的位置就偏 5 个像素。别偷懒,每个框都仔细拉。

标注完成后,LabelImg 会为每张图片生成一个同名的 .txt 文件(YOLO 格式)。里面每一行代表一个目标:

<class_id> <x_center> <y_center> <width> <height>

注意,这里的坐标都是归一化到 0-1 之间的。比如一张 1920x1080 的图,目标中心在 (960, 540),宽 200,高 300,那对应的值就是:

0 0.5 0.5 0.104 0.278

你想想看,如果直接存像素值,模型对不同分辨率的图片就没法泛化了。归一化就是为了解决这个问题。

2.3 COCO 数据集格式详解:不只是「标准答案」

COCO 格式是目前目标检测领域最通用的数据格式之一。很多预训练模型都是用 COCO 数据集训出来的。理解它的结构,对你后续做模型微调、数据转换都很有帮助。

COCO 标注文件是一个 JSON 文件,核心包含三个字段:

字段 类型 说明
images 数组 每张图片的信息:id、file_name、width、height
annotations 数组 每个标注框的信息:id、image_id、category_id、bbox、area、iscrowd
categories 数组 类别列表:id、name、supercategory

这里有个关键点:COCO 的 bbox 格式是 [x, y, width, height],其中 x, y 是框左上角的像素坐标。而 YOLO 用的是中心点坐标。所以做格式转换时,一定要算清楚。

我贴一段我自己常用的转换代码,把 COCO 转成 YOLO 格式:

import json
import os

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

    # 建立 category_id 到 class_id 的映射
    cat_id_map = {cat['id']: idx for idx, cat in enumerate(coco_data['categories'])}

    # 建立 image_id 到图片信息的映射
    img_info_map = {img['id']: img for img in coco_data['images']}

    for ann in coco_data['annotations']:
        img_id = ann['image_id']
        img_info = img_info_map[img_id]
        img_w = img_info['width']
        img_h = img_info['height']

        # COCO bbox: [x, y, w, h] 左上角坐标
        x, y, w, h = ann['bbox']
        # 转成 YOLO 格式:中心点 + 宽高,归一化
        x_center = (x + w / 2) / img_w
        y_center = (y + h / 2) / img_h
        w_norm = w / img_w
        h_norm = h / img_h

        class_id = cat_id_map[ann['category_id']]
        line = f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n"

        # 写入对应图片的 txt 文件
        txt_name = img_info['file_name'].replace('.jpg', '.txt')
        txt_path = os.path.join(output_dir, txt_name)
        with open(txt_path, 'a') as out_f:
            out_f.write(line)
核心要点: COCO 的 bbox 是 [x, y, w, h](左上角),YOLO 是 [x_center, y_center, w, h](中心点)。转换时别忘了除以图片宽高做归一化。

2.4 数据集划分:train / val / test 的黄金比例

数据准备好了,接下来就是划分。很多同学直接把所有数据扔进去训,结果模型过拟合了都不知道。划分数据集的目的,就是让你能客观评估模型的泛化能力。

我一般遵循这个比例:

  • 训练集(train):70% - 80%。模型学习的主力数据。
  • 验证集(val):10% - 15%。用于调参、选模型、早停。每次 epoch 结束后,模型都会在 val 上跑一次,看 loss 有没有下降。
  • 测试集(test):10% - 15%。最终评估用。训完模型后,只在 test 上跑一次,得到最终指标。不能拿 test 来调参!

划分时要注意:保证每个类别在各个集合中的比例大致相同。如果某个类别只有 50 张图,你随机划分时可能全分到训练集,验证集里一个都没有,那验证指标就失真了。

我习惯用 sklearn 的 train_test_split 做分层采样:

from sklearn.model_selection import train_test_split
import os
import shutil

# 假设 images 是所有图片路径列表,labels 是对应的类别标签(每个图片的主类别)
images = [...]  # 你的图片路径列表
labels = [...]  # 每个图片对应的主类别(比如取该图中出现最多的类别)

# 先分出 test(15%),剩下的再按比例分 train 和 val
train_val_images, test_images, train_val_labels, test_labels = train_test_split(
    images, labels, test_size=0.15, stratify=labels, random_state=42
)

# 再从 train_val 中分出 val(占原始数据的 15%)
train_images, val_images, train_labels, val_labels = train_test_split(
    train_val_images, train_val_labels, test_size=0.176, stratify=train_val_labels, random_state=42
)

# 然后复制文件到对应文件夹
# ... (复制逻辑略)

最后,你的数据集目录结构应该是这样的:

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

每一张图片在 images/train/ 里,对应的标注文件就在 labels/train/ 里,文件名相同,只是后缀不同。YOLO 训练时就是按这个目录结构来找数据的。

注意: 划分前一定要检查数据有没有「泄露」。比如同一场景的连续帧,不能一部分在训练集,一部分在验证集。否则模型相当于「见过」验证集的内容,指标会虚高。我一般按视频片段或场景来划分,而不是按单张图随机分。

本章知识体系总览

下面这张图,帮你把本章的核心流程串起来:

数据集构建与标注全流程 数据采集 场景/角度/背景 LabelImg 标注 YOLO / VOC 格式 格式统一 COCO / YOLO 互转 数据集划分 train / val / test 训练集 (70-80%) 验证集 (10-15%) 测试集 (10-15%) 关键:分层采样保证类别分布一致,按场景划分避免数据泄露 最终目录结构:images/{train,val,test} + labels/{train,val,test}

数据集的构建,说白了就是给模型准备一份高质量的「教材」。采集要全、标注要准、格式要对、划分要科学。这一步做得越扎实,后面训练就越省心。我见过太多项目因为数据问题反复返工,浪费大量时间。嗯,希望你能一次搞定。