4. 数据标注:LabelImg安装与使用、标注规范与技巧、标注格式转换(VOC转YOLO)、数据集划分

数据标注这件事,说实话,是YOLO实战里最枯燥但也是最关键的一环。模型能不能收敛、检测精度高不高,很大程度上取决于你标注的质量。我见过太多同学花大把时间调参,结果模型效果差,最后发现是标注数据出了问题——框歪了、类别标错了、漏标了目标。嗯,咱们这节课就把这些坑都填上。

4.1 LabelImg 安装与使用

LabelImg 是目前最主流的图像标注工具之一,支持 VOC 和 YOLO 两种格式。我个人习惯用 Python 版,跨平台兼容性好,更新也及时。

4.1.1 安装方式

我推荐直接用 pip 安装,干净利落:

pip install labelImg
labelImg

如果遇到 PyQt5 依赖问题,可以试试:

pip install labelImg pyqt5
labelImg
小提示: 在 Windows 上如果命令行启动报错,右键以管理员身份运行命令提示符再试。我曾经在 Win10 上折腾了半小时,最后发现是权限问题。

4.1.2 界面与基本操作

启动后界面很简洁,左边是工具栏,中间是图像显示区,右边是标签列表。核心操作就三步:

  1. 打开图像目录:点击 "Open Dir" 选择存放图片的文件夹
  2. 创建矩形框:按 w 键进入标注模式,按住鼠标左键拖拽
  3. 输入标签名:松开鼠标后弹出对话框,输入类别名称(如 car、person)

常用快捷键我列在下面,记熟了效率翻倍:

快捷键 功能
w 创建矩形框
a / d 上一张 / 下一张图片
Ctrl + s 保存当前标注
Ctrl + u 加载下一张未标注图片
Delete 删除选中的标注框
Ctrl + z 撤销上一步操作
核心建议: 标注前先在 "Change Save Dir" 里设置好保存路径,默认保存为 VOC 格式的 XML 文件。我习惯把图片和标注文件放在同一个目录下,方便后续处理。

4.2 标注规范与技巧

标注不是随便画个框就完事了。这里面的门道,我踩过不少坑,总结了几条硬性规范。

4.2.1 标注基本原则

  • 紧贴目标边缘:矩形框要刚好包住目标,不要留太多背景,也不要切掉目标的关键部位
  • 遮挡处理:目标被遮挡超过 50% 时,建议不标注;轻微遮挡可以标注,但框要尽量贴合可见部分
  • 边界目标:目标在图像边缘被切掉超过 30%,不标注;只切掉一点点,正常标注
  • 类别一致性:同一个类别在不同图片中的标注标准要统一,比如 "car" 不能有的标 "vehicle"
避坑指南: 我曾经在一个项目中,标注员把远处的小车标成了 "car",近处的大车也标成了 "car",结果模型对远处小目标的召回率极低。后来发现是因为小目标的框太小,模型学不到有效特征。建议对小于 32x32 像素的目标单独评估是否标注。

4.2.2 实战技巧

标注效率和质量可以兼得,关键是用对方法:

  1. 先标难样本:把光照差、遮挡多、角度刁钻的图片先标出来,让模型优先学习困难场景
  2. 使用 "Verify" 功能:LabelImg 支持标注验证,标完一轮后可以快速复查
  3. 多人标注统一规范:团队协作时,一定要写一份标注规范文档,贴在项目目录里
  4. 定期抽检:每标注 500 张图片,随机抽 50 张检查标注质量

4.3 标注格式转换:VOC 转 YOLO

LabelImg 默认保存为 VOC 格式(XML 文件),但 YOLO 训练需要的是 TXT 格式。这两种格式的核心区别在于坐标表示方式:

对比项 VOC 格式 YOLO 格式
坐标类型 左上角 (xmin, ymin) + 右下角 (xmax, ymax) 中心点 (x_center, y_center) + 宽高 (width, height)
坐标值 像素值 归一化到 [0, 1]
文件格式 XML TXT(每行一个目标)
文件命名 与图片同名,扩展名为 .xml 与图片同名,扩展名为 .txt

转换公式其实很简单,我手写了一个脚本,你们可以直接用:

import xml.etree.ElementTree as ET
import os

def voc_to_yolo(xml_file, classes, output_dir):
    tree = ET.parse(xml_file)
    root = tree.getroot()
    
    # 获取图片尺寸
    img_width = int(root.find('size/width').text)
    img_height = int(root.find('size/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.findall('object'):
            class_name = obj.find('name').text
            if class_name not in classes:
                continue
            class_id = classes.index(class_name)
            
            # 提取 VOC 坐标
            xmin = int(obj.find('bndbox/xmin').text)
            ymin = int(obj.find('bndbox/ymin').text)
            xmax = int(obj.find('bndbox/xmax').text)
            ymax = int(obj.find('bndbox/ymax').text)
            
            # 转换为 YOLO 格式
            x_center = (xmin + xmax) / 2.0 / img_width
            y_center = (ymin + ymax) / 2.0 / img_height
            width = (xmax - xmin) / img_width
            height = (ymax - ymin) / img_height
            
            f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")

# 使用示例
classes = ['car', 'person', 'bicycle']  # 你的类别列表
xml_dir = 'Annotations/'
output_dir = 'labels/'

for xml_file in os.listdir(xml_dir):
    if xml_file.endswith('.xml'):
        voc_to_yolo(os.path.join(xml_dir, xml_file), classes, output_dir)
注意: 转换后的坐标值必须在 [0, 1] 范围内。如果出现负数或大于 1 的值,说明原始标注框超出了图像边界,需要检查标注数据。

4.4 数据集划分

数据集划分看似简单,但划分方式直接影响模型评估的可靠性。我见过有人随机划分后,训练集和验证集里出现了同一张图片的不同版本,导致验证指标虚高。

4.4.1 划分比例与策略

对于车辆行人检测这类任务,我推荐以下划分方案:

数据集 比例 用途
训练集 70% 模型学习参数
验证集 15% 调参和模型选择
测试集 15% 最终性能评估

划分时要特别注意:

  • 按场景划分:同一段视频的连续帧不要同时出现在训练集和验证集中
  • 保持类别分布:划分后检查每个类别在三个集合中的比例是否接近原始分布
  • 固定随机种子:用 random.seed(42) 保证每次划分结果一致

4.4.2 自动化划分脚本

我写了一个划分脚本,支持按文件列表或按目录划分:

import os
import random
import shutil

def split_dataset(image_dir, label_dir, output_dir, 
                  train_ratio=0.7, val_ratio=0.15, seed=42):
    random.seed(seed)
    
    # 获取所有图片文件
    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 = train_end + int(total * val_ratio)
    
    # 划分
    splits = {
        'train': images[:train_end],
        'val': images[train_end:val_end],
        'test': images[val_end:]
    }
    
    # 创建目录并复制文件
    for split_name, file_list in splits.items():
        img_out = os.path.join(output_dir, split_name, 'images')
        lbl_out = os.path.join(output_dir, split_name, 'labels')
        os.makedirs(img_out, exist_ok=True)
        os.makedirs(lbl_out, exist_ok=True)
        
        for img_file in file_list:
            # 复制图片
            shutil.copy(os.path.join(image_dir, img_file), 
                       os.path.join(img_out, img_file))
            # 复制对应的标签文件
            label_file = os.path.splitext(img_file)[0] + '.txt'
            if os.path.exists(os.path.join(label_dir, label_file)):
                shutil.copy(os.path.join(label_dir, label_file),
                           os.path.join(lbl_out, label_file))
    
    print(f"划分完成:训练集 {len(splits['train'])} 张,"
          f"验证集 {len(splits['val'])} 张,"
          f"测试集 {len(splits['test'])} 张")

# 使用示例
split_dataset('images/', 'labels/', 'dataset/')
重要提醒: 划分完成后,一定要检查验证集和测试集中是否有训练集里出现过的图片。我习惯用 MD5 校验来去重,确保三个集合完全独立。

4.5 本章知识体系

下面这张图把数据标注的完整流程串起来了,从原始图片到最终可训练的数据集:

数据标注完整流程 原始图片 LabelImg 标注 VOC 格式 (XML) 格式转换 YOLO 格式 (TXT) 数据集划分 训练集 (70%) 验证集 (15%) 测试集 (15%) 可训练的数据集 关键检查点 • 坐标归一化范围 [0,1] • 类别名称与 ID 对应 • 三个子集无重复图片

数据标注是整个检测项目的基石。你想想看,模型再强大,喂进去的数据是错的,结果能对吗?我建议你在正式标注前,先拿 50 张图片练手,熟悉流程后再批量操作。标注完成后,用转换脚本统一处理,再用划分脚本生成训练集、验证集和测试集。每一步都检查一遍,后面训练的时候就能省很多心。