第三章:数据集准备——从标注到转换的完整流程

说实话,做目标检测最磨人的不是调模型,而是准备数据。我见过太多同学花两周调参,结果发现是标注格式不对,白忙活一场。这一章,咱们就把数据准备这件事彻底讲透。

3.1 LabelImg 标注工具使用

LabelImg 是我个人最常用的标注工具。它轻量、开源,而且支持 YOLO 和 Pascal VOC 两种格式。说白了,它就是给图片画框、打标签的。

安装与启动

安装其实很简单。我建议直接用 pip 装:

pip install labelImg
labelImg

如果你在 macOS 上遇到权限问题,加个 sudo 就行。Windows 用户记得用管理员模式打开命令行。嗯,这里要注意:第一次启动可能会有点慢,别急。

标注流程

打开软件后,你会看到一个简洁的界面。我个人习惯按以下步骤操作:

  1. 打开图片目录:点击 "Open Dir" 选择你的图片文件夹
  2. 选择保存格式:点击 "Change Save Dir" 设置标注文件保存位置
  3. 开始标注:按 W 键进入画框模式,拖拽鼠标框选目标
  4. 输入标签:弹窗里输入类别名称,比如 "person"、"car"
  5. 保存:按 Ctrl+S 保存当前标注
我的小技巧:标注时按 D 键切换到下一张图,按 A 键回到上一张。这样能大幅提升效率。我曾经一天标了 500 张图,全靠快捷键。

标注注意事项

我在项目中遇到过不少标注翻车的情况。这里列几个避坑点:

  • 框要贴边:标注框必须紧贴目标边缘,留白太多会影响模型学习
  • 遮挡处理:目标被遮挡时,只标注可见部分,不要脑补完整轮廓
  • 小目标:小于 10x10 像素的目标,我建议直接放弃标注,模型基本学不到
  • 类别统一:同一个类别在不同图片里必须用完全相同的标签名,大小写都不能错
我曾经踩过的坑:有一次我把 "car" 和 "Car" 混着用,结果模型训练出来,红色车能检测到,白色车就漏检。后来排查了两天才发现是标签大小写不一致。从那以后,我所有标签都用小写字母。

3.2 COCO 数据集格式详解

COCO 格式是目前目标检测领域最通用的数据格式。YOLOv10 原生支持 COCO 格式,所以理解它很重要。

COCO 的 JSON 结构

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

{
  "images": [
    {
      "id": 1,
      "file_name": "000001.jpg",
      "width": 640,
      "height": 480
    }
  ],
  "annotations": [
    {
      "id": 1,
      "image_id": 1,
      "category_id": 1,
      "bbox": [100, 150, 200, 300],
      "area": 60000,
      "iscrowd": 0
    }
  ],
  "categories": [
    {
      "id": 1,
      "name": "person"
    }
  ]
}

你想想看,这个结构其实很清晰:images 记录图片信息,annotations 记录标注框,categories 记录类别映射。说白了就是三张表。

bbox 格式说明

COCO 的 bbox 是 [x, y, width, height] 格式,注意不是 YOLO 的 [x_center, y_center, width, height]。这里容易搞混,我刚开始也犯过这个错。

格式 表示方式 示例
COCO [左上角x, 左上角y, 宽, 高] [100, 150, 200, 300]
YOLO [中心x, 中心y, 宽, 高] [0.3, 0.4, 0.2, 0.3]
关键点:YOLO 格式的坐标是归一化的(0~1),而 COCO 是像素坐标。转换时一定要除以图片宽高。

3.3 自定义数据集转换脚本

实际项目中,我们很少直接用 COCO 格式的数据。更多时候是从 LabelImg 标注的 XML 或 TXT 文件转成 COCO JSON。我写了一个通用的转换脚本,分享给你。

从 LabelImg XML 转 COCO JSON

LabelImg 默认保存为 Pascal VOC 格式的 XML 文件。下面这个脚本可以把 XML 批量转成 COCO 格式:

import xml.etree.ElementTree as ET
import json
import os
from glob import glob

def xml_to_coco(xml_dir, output_json):
    images = []
    annotations = []
    categories = []
    cat_id_map = {}
    ann_id = 1
    
    # 先收集所有类别
    for xml_file in glob(os.path.join(xml_dir, "*.xml")):
        tree = ET.parse(xml_file)
        root = tree.getroot()
        for obj in root.findall("object"):
            name = obj.find("name").text
            if name not in cat_id_map:
                cat_id = len(categories) + 1
                cat_id_map[name] = cat_id
                categories.append({"id": cat_id, "name": name})
    
    # 处理每张图片
    for img_id, xml_file in enumerate(glob(os.path.join(xml_dir, "*.xml")), 1):
        tree = ET.parse(xml_file)
        root = tree.getroot()
        
        # 图片信息
        filename = root.find("filename").text
        size = root.find("size")
        width = int(size.find("width").text)
        height = int(size.find("height").text)
        images.append({
            "id": img_id,
            "file_name": filename,
            "width": width,
            "height": height
        })
        
        # 标注信息
        for obj in root.findall("object"):
            name = obj.find("name").text
            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)
            
            # 转成 COCO 格式
            w = xmax - xmin
            h = ymax - ymin
            annotations.append({
                "id": ann_id,
                "image_id": img_id,
                "category_id": cat_id_map[name],
                "bbox": [xmin, ymin, w, h],
                "area": w * h,
                "iscrowd": 0
            })
            ann_id += 1
    
    # 保存
    coco_data = {
        "images": images,
        "annotations": annotations,
        "categories": categories
    }
    with open(output_json, "w") as f:
        json.dump(coco_data, f, indent=2)
    print(f"转换完成,共 {len(images)} 张图片,{len(annotations)} 个标注")

# 使用示例
xml_to_coco("path/to/xml_folder", "annotations.json")
使用建议:运行前先检查 XML 文件里有没有损坏的。我曾经遇到过某个 XML 缺少 size 字段,脚本直接报错。建议加个 try-except 做容错处理。

验证转换结果

转换完成后,我习惯用下面这段代码快速验证:

import json

with open("annotations.json") as f:
    data = json.load(f)

print(f"图片数量: {len(data['images'])}")
print(f"标注数量: {len(data['annotations'])}")
print(f"类别数量: {len(data['categories'])}")
print("类别列表:", [c["name"] for c in data["categories"]])

如果数据量对不上,多半是 XML 解析出了问题。我建议先拿一两张图手动验证,确认没问题再批量处理。

3.4 数据划分与目录结构

数据准备好后,还需要按训练集、验证集、测试集划分。YOLOv10 要求的目录结构是这样的:

dataset/
├── train/
│   ├── images/
│   │   ├── 000001.jpg
│   │   └── ...
│   └── labels/
│       ├── 000001.txt
│       └── ...
├── val/
│   ├── images/
│   └── labels/
└── test/
    ├── images/
    └── labels/

我个人习惯按 8:1:1 的比例划分。注意:划分时要保证每个类别在三个集合中都有分布,不然验证集里缺了某个类别,评估指标会出问题。

重要提醒:YOLOv10 训练时,图片和标注文件必须同名,只是后缀不同。比如 000001.jpg 对应 000001.txt。文件名里不要有空格或中文,否则训练时会报错。

好了,数据准备这块就讲到这里。下一章咱们开始搭建 YOLOv10 的训练环境,到时候你就能用今天准备的数据跑起来了。