3. 数据集准备与标注:LabelImg安装与使用、COCO数据集格式详解、自定义数据集制作、数据集划分

做目标检测,数据就是燃料。模型再牛,没有高质量的数据集,也是白搭。这一章,我带你亲手搞定数据准备的全流程——从标注工具到格式转换,再到数据集划分。嗯,每一步都有坑,我会把踩过的坑都指出来。

本章核心目标: 掌握从零构建一个可用目标检测数据集的完整流程,包括工具使用、格式理解和工程化划分。

3.1 标注工具:LabelImg 安装与使用

LabelImg 是我用得最顺手的标注工具。它轻量、免费,而且支持 YOLO 和 Pascal VOC 两种主流格式。说白了,它就是给图片画框、打标签的利器。

3.1.1 安装

安装方式很简单,我推荐用 pip 直接装,省心。

# 安装 LabelImg
pip install labelImg

# 启动
labelImg

如果你在 macOS 上遇到权限问题,加个 --user 参数就行。Windows 用户记得用管理员身份打开命令行。

我的习惯: 我会把 LabelImg 的快捷键表打印出来贴在显示器边上。标注是个重复劳动,快捷键能帮你省下 30% 的时间。

3.1.2 核心操作流程

  1. 打开图片目录:点击 "Open Dir",选择你存放图片的文件夹。
  2. 设定保存目录:点击 "Change Save Dir",标注文件会存到这里。
  3. 选择输出格式:点击 "PascalVOC" 按钮,切换到 "YOLO" 格式。YOLO 格式是 .txt 文件,每行一个目标。
  4. 开始标注:按 w 键进入画框模式,拖拽鼠标框住目标,弹出对话框输入类别名称。
  5. 保存下一张:按 Ctrl+S 保存,按 d 键切换到下一张图片。
我曾经踩过的坑: 标注时类别名称一定要统一。比如 "car" 和 "Car" 会被模型当成两个不同的类。我建议提前写好一个 classes.txt 文件,标注时直接复制粘贴,避免手误。

3.2 COCO 数据集格式详解

COCO 格式是目前目标检测领域的事实标准。YOLOv5、YOLOv8 等主流框架都原生支持它。你想想看,搞懂 COCO 格式,就等于拿到了通行证。

3.2.1 核心结构

COCO 数据集的核心是一个 JSON 文件。它包含五个主要字段:

字段 说明 是否必填
info 数据集描述信息(版本、日期等) 推荐
licenses 许可证信息 可选
images 所有图片的元数据(id、宽、高、文件名) 必填
annotations 所有标注信息(目标框、类别id、分割掩码等) 必填
categories 类别列表(id 到名称的映射) 必填

3.2.2 标注格式细节

每个 annotation 对象长这样:

{
  "id": 1,
  "image_id": 1,
  "category_id": 3,
  "bbox": [100, 150, 200, 300],
  "area": 60000,
  "iscrowd": 0,
  "segmentation": []
}

注意 bbox 的格式是 [x, y, width, height],其中 (x, y) 是框的左上角坐标。这和 YOLO 格式不一样——YOLO 用的是 [x_center, y_center, width, height] 且归一化到 0-1。

关键区别: COCO 的 bbox 是坐标,YOLO 的 bbox 是归一化相对坐标。转换时一定要除以图片的宽和高。

3.3 自定义数据集制作

实际项目中,你很少会用现成的数据集。我做过一个交通监控项目,需要识别特定型号的车辆,只能自己标。下面是我的标准流程。

3.3.1 数据采集

  • 视频抽帧:用 OpenCV 从视频中每隔 N 帧抽取一张。我一般设 N=30,保证帧间差异足够大。
  • 网络爬取:注意版权问题,只爬取开源图片。
  • 实地拍摄:最可靠,但成本最高。记得覆盖不同光照、角度和背景。

3.3.2 标注与格式转换

标注完成后,你需要把 LabelImg 生成的格式转成 COCO JSON。我写了一个转换脚本,核心逻辑如下:

import json
import os
from PIL import Image

def yolo_to_coco(yolo_dir, img_dir, output_json, classes):
    """
    将 YOLO 格式标注转换为 COCO JSON 格式
    """
    coco = {
        "images": [],
        "annotations": [],
        "categories": [{"id": i+1, "name": cls} for i, cls in enumerate(classes)]
    }
    
    ann_id = 1
    for img_name in os.listdir(img_dir):
        if not img_name.endswith(('.jpg', '.png')):
            continue
        
        img_path = os.path.join(img_dir, img_name)
        img = Image.open(img_path)
        w, h = img.size
        
        img_id = len(coco["images"]) + 1
        coco["images"].append({
            "id": img_id,
            "file_name": img_name,
            "width": w,
            "height": h
        })
        
        # 读取对应的 YOLO 标注文件
        txt_name = os.path.splitext(img_name)[0] + ".txt"
        txt_path = os.path.join(yolo_dir, txt_name)
        if not os.path.exists(txt_path):
            continue
        
        with open(txt_path, 'r') as f:
            for line in f.readlines():
                parts = line.strip().split()
                cls_id = int(parts[0]) + 1  # YOLO 类别从0开始,COCO从1开始
                x_center, y_center, bw, bh = map(float, parts[1:])
                
                # 转成 COCO 格式 [x, y, width, height]
                x = (x_center - bw / 2) * w
                y = (y_center - bh / 2) * h
                bw = bw * w
                bh = bh * h
                
                coco["annotations"].append({
                    "id": ann_id,
                    "image_id": img_id,
                    "category_id": cls_id,
                    "bbox": [x, y, bw, bh],
                    "area": bw * bh,
                    "iscrowd": 0
                })
                ann_id += 1
    
    with open(output_json, 'w') as f:
        json.dump(coco, f, indent=2)
    
    print(f"转换完成,共 {len(coco['images'])} 张图片,{len(coco['annotations'])} 个标注。")

# 使用示例
classes = ["person", "car", "bicycle"]
yolo_to_coco("labels/", "images/", "annotations.json", classes)
避坑指南: 转换时最容易出错的是坐标计算。我建议转换后随机抽几张图,用 OpenCV 把框画出来看看,确认位置正确再往下走。

3.4 数据集划分:train/val/test

数据集划分看似简单,但比例和策略直接影响模型评估的可信度。我个人习惯用 70% 训练、15% 验证、15% 测试。

3.4.1 划分策略

  • 随机划分:适用于数据分布均匀的场景。直接用 sklearn.model_selection.train_test_split。
  • 分层划分:保证每个类别在三个集合中的比例一致。我遇到类别不平衡时必用这个。
  • 按视频序列划分:如果数据来自视频,同一视频的帧必须分到同一个集合,否则会造成数据泄露。

3.4.2 代码实现

import json
import random
from sklearn.model_selection import train_test_split

def split_coco_dataset(json_path, train_ratio=0.7, val_ratio=0.15, test_ratio=0.15):
    """
    划分 COCO 数据集为 train/val/test
    """
    with open(json_path, 'r') as f:
        coco = json.load(f)
    
    images = coco["images"]
    annotations = coco["annotations"]
    
    # 获取所有图片ID
    img_ids = [img["id"] for img in images]
    
    # 先分出 train 和 temp(val+test)
    train_ids, temp_ids = train_test_split(
        img_ids, test_size=(1 - train_ratio), random_state=42
    )
    
    # 再从 temp 中分出 val 和 test
    val_ratio_adjusted = val_ratio / (val_ratio + test_ratio)
    val_ids, test_ids = train_test_split(
        temp_ids, test_size=(1 - val_ratio_adjusted), random_state=42
    )
    
    def filter_by_ids(ids):
        imgs = [img for img in images if img["id"] in ids]
        anns = [ann for ann in annotations if ann["image_id"] in ids]
        return {"images": imgs, "annotations": anns}
    
    splits = {
        "train": filter_by_ids(train_ids),
        "val": filter_by_ids(val_ids),
        "test": filter_by_ids(test_ids)
    }
    
    # 保存
    for split_name, data in splits.items():
        data["categories"] = coco["categories"]
        output_path = json_path.replace(".json", f"_{split_name}.json")
        with open(output_path, 'w') as f:
            json.dump(data, f, indent=2)
        print(f"{split_name}: {len(data['images'])} 张图片, {len(data['annotations'])} 个标注")
    
    return splits

# 使用
split_coco_dataset("annotations.json")
我曾经犯过的错: 有一次我忘了设置 random_state,每次运行划分结果都不一样,导致实验结果无法复现。记住,随机种子一定要固定。

3.5 知识体系总览

下面这张图把本章的核心流程串起来了。你可以把它当作操作手册的索引。

数据集准备与标注流程 数据采集 LabelImg 标注 格式转换 COCO JSON 数据集划分 train.json val.json test.json 💡 关键点 • 类别名称统一 • 坐标转换验证