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 核心操作流程
- 打开图片目录:点击 "Open Dir",选择你存放图片的文件夹。
- 设定保存目录:点击 "Change Save Dir",标注文件会存到这里。
- 选择输出格式:点击 "PascalVOC" 按钮,切换到 "YOLO" 格式。YOLO 格式是
.txt文件,每行一个目标。 - 开始标注:按
w键进入画框模式,拖拽鼠标框住目标,弹出对话框输入类别名称。 - 保存下一张:按
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 知识体系总览
下面这张图把本章的核心流程串起来了。你可以把它当作操作手册的索引。