3、数据集准备:LabelImg标注工具使用、数据集格式转换(VOC转YOLO)、数据集划分与配置文件编写

做目标检测,数据是地基。模型再强,数据不行也白搭。这一章咱们就聊聊怎么把原始图片变成YOLOv11能吃的数据。说白了,就是三件事:标注、格式转换、划分。

3.1 标注工具:LabelImg

LabelImg这个工具,我用了好多年了。它轻量、免费,而且支持VOC和YOLO两种格式输出。我个人习惯先用VOC格式标,后面再转,因为VOC的XML文件可读性更好,方便检查。

3.1.1 安装与启动

安装很简单,直接pip搞定:

pip install labelImg
labelImg

启动后界面很清爽。左边是图片列表,中间是画布,右边是标注框。嗯,这里要注意:默认打开的是图片文件夹,你需要先设置好标注文件的保存路径。

我的习惯:在项目根目录下建一个 datasets/raw 文件夹,里面放原始图片。标注文件默认存到 datasets/Annotations。这样结构清晰,不容易乱。

3.1.2 标注流程

标注其实就几个步骤,我列一下:

  1. 打开图片目录 —— 选择存放图片的文件夹。
  2. 选择输出目录 —— 标注文件存哪。
  3. 创建矩形框 —— 按住鼠标左键,从目标左上角拖到右下角。
  4. 输入标签名 —— 比如 person、car。
  5. 保存 —— 快捷键 Ctrl+S,生成XML文件。
  6. 下一张 —— 快捷键 D。

快捷键能省不少时间。我刚开始用的时候,一张张点保存,效率极低。后来记住了快捷键,速度翻倍。

避坑指南:我曾经因为标签名大小写不一致,导致训练时类别数对不上。比如有的标了"Person",有的标了"person"。YOLO是区分大小写的,所以务必统一。建议全部用小写。

3.2 数据集格式转换:VOC转YOLO

VOC格式用XML存标注信息,YOLO格式用TXT存。YOLO的TXT文件每行代表一个目标,格式是:

class_id x_center y_center width height

注意,这里的坐标都是归一化后的,范围0~1。x_center和y_center是目标框中心点相对于图片宽高的比例,width和height是框的宽高相对于图片宽高的比例。

为什么会用归一化坐标?说白了,就是让模型不依赖图片的原始尺寸。你想想看,一张1920x1080的图和一张640x480的图,如果都用像素,模型就懵了。归一化之后,不管图片多大,坐标都在0~1之间,模型学起来更稳定。

3.2.1 转换脚本

我写了个转换脚本,你们可以直接用:

import xml.etree.ElementTree as ET
import os

def convert_voc_to_yolo(xml_file, classes, output_dir):
    tree = ET.parse(xml_file)
    root = tree.getroot()
    
    # 获取图片尺寸
    size = root.find('size')
    width = int(size.find('width').text)
    height = int(size.find('height').text)
    
    # 生成对应的txt文件名
    txt_file = os.path.join(output_dir, 
                os.path.splitext(os.path.basename(xml_file))[0] + '.txt')
    
    with open(txt_file, 'w') as f:
        for obj in root.iter('object'):
            class_name = obj.find('name').text
            if class_name not in classes:
                continue
            class_id = classes.index(class_name)
            
            # 解析VOC的bbox坐标
            bbox = obj.find('bndbox')
            xmin = int(bbox.find('xmin').text)
            ymin = int(bbox.find('ymin').text)
            xmax = int(bbox.find('xmax').text)
            ymax = int(bbox.find('ymax').text)
            
            # 转换为YOLO格式
            x_center = (xmin + xmax) / 2.0 / width
            y_center = (ymin + ymax) / 2.0 / height
            w = (xmax - xmin) / width
            h = (ymax - ymin) / height
            
            f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n")

# 使用示例
classes = ['person', 'car', 'bicycle']  # 你的类别列表
xml_dir = 'datasets/Annotations'
output_dir = 'datasets/labels'
os.makedirs(output_dir, exist_ok=True)

for xml_file in os.listdir(xml_dir):
    if xml_file.endswith('.xml'):
        convert_voc_to_yolo(os.path.join(xml_dir, xml_file), classes, output_dir)
关键点:转换时一定要确保 classes 列表的顺序和YOLO配置文件中的一致。否则类别就全乱了。我见过有人把'person'放在索引0,配置文件里却写成了索引1,结果训练出来的模型把人都识别成了车。

3.3 数据集划分

数据标好了,格式也转了,接下来要分成训练集、验证集和测试集。比例一般是7:2:1或者8:1:1。我个人习惯用8:1:1,数据多的话可以适当调高验证集比例。

3.3.1 划分脚本

import os
import random
from shutil import copyfile

def split_dataset(image_dir, label_dir, output_dir, 
                  train_ratio=0.8, val_ratio=0.1):
    images = [f for f in os.listdir(image_dir) 
              if f.endswith(('.jpg', '.png', '.jpeg'))]
    random.shuffle(images)
    
    total = len(images)
    train_end = int(total * train_ratio)
    val_end = int(total * (train_ratio + val_ratio))
    
    splits = {
        'train': images[:train_end],
        'val': images[train_end:val_end],
        'test': images[val_end:]
    }
    
    for split_name, split_images in splits.items():
        img_out = os.path.join(output_dir, 'images', split_name)
        lbl_out = os.path.join(output_dir, 'labels', split_name)
        os.makedirs(img_out, exist_ok=True)
        os.makedirs(lbl_out, exist_ok=True)
        
        for img in split_images:
            copyfile(os.path.join(image_dir, img), 
                     os.path.join(img_out, img))
            
            label_file = os.path.splitext(img)[0] + '.txt'
            label_path = os.path.join(label_dir, label_file)
            if os.path.exists(label_path):
                copyfile(label_path, os.path.join(lbl_out, label_file))

# 使用
split_dataset('datasets/images', 'datasets/labels', 'datasets/split')
我的经验:划分前一定要检查有没有空标签文件。有些图片可能没有目标,但YOLO训练时要求每张图片都有对应的标签文件。对于空图片,可以生成一个空的TXT文件,或者干脆删掉。我一般选择删掉,因为空图片对训练没帮助。

3.4 配置文件编写

YOLOv11用YAML文件来配置数据集路径和类别信息。这个文件是训练时的入口,写错了模型就找不到数据。

3.4.1 标准YAML结构

# dataset.yaml
path: ./datasets/split  # 数据集根目录
train: images/train      # 训练集图片路径(相对于path)
val: images/val          # 验证集图片路径
test: images/test        # 测试集图片路径(可选)

# 类别信息
nc: 3                    # 类别数量
names: ['person', 'car', 'bicycle']  # 类别名称列表

这里有个容易踩的坑:path 路径建议用路径或者相对于项目根目录的路径。我之前用相对路径,结果换了个目录运行训练脚本,路径就找不到了。后来统一用路径,再也没出过问题。

3.4.2 路径配置对比

配置方式 示例 优点 缺点
路径 path: /home/user/project/datasets/split 稳定,不依赖运行目录 换机器要改路径
相对路径 path: ./datasets/split 方便迁移 依赖运行目录,容易出错
软链接 创建软链接到固定位置 兼顾稳定和迁移 多一步操作

我个人推荐用路径,然后在项目里写个脚本自动生成YAML文件,这样换机器时只需要改一个变量。

3.5 本章知识体系

下面这张图把整个数据准备流程串起来了,你可以对照着看:

数据集准备流程 数据集准备流程 原始图片 LabelImg 标注 VOC格式 (XML) YOLO格式 (TXT) 格式转换 数据集划分 训练集 (80%) 验证集 (10%) 测试集 (10%) dataset.yaml 配置文件 ⚠️ 类别顺序必须一致 ⚠️ 路径建议用路径

整个流程走下来,数据就准备好了。嗯,这里要提醒一句:标注质量直接决定模型上限。你花一天时间标数据,比花一周调参效果要好得多。我见过太多人急着训练,结果数据标得乱七八糟,最后模型根本没法用。

总结一下:数据准备是YOLO实战的第一步,也是最耗时的一步。LabelImg标注、VOC转YOLO格式、数据集划分、YAML配置,这四个环节环环相扣。任何一个环节出错,训练都会出问题。所以,每一步做完都检查一下,别嫌麻烦。