从VOC到YOLO:用LabelImg标注后,你的数据集还需要这几步处理才能喂给模型
·
从VOC到YOLO:用LabelImg标注后,你的数据集还需要这几步处理才能喂给模型
刚用LabelImg标注完几百张猫狗图片,看着满屏的XML文件,是不是觉得离训练模型还差一口气?别急,这只是数据准备的开始。VOC格式的标注文件就像原材料,而YOLO需要的是精加工后的数据套餐——包括格式转换、数据集划分和质检三道工序。下面我们一步步拆解这个"数据预处理流水线"。
1. 解剖VOC XML文件:理解标注数据的DNA
打开任意一个XML文件,你会看到类似这样的结构:
<annotation>
<filename>cat_001.jpg</filename>
<size>
<width>640</width>
<height>480</height>
<depth>3</depth>
</size>
<object>
<name>cat</name>
<bndbox>
<xmin>120</xmin>
<ymin>80</ymin>
<xmax>350</xmax>
<ymax>400</ymax>
</bndbox>
</object>
</annotation>
关键字段解析表:
| 字段 | 含义 | YOLO转换时需要关注的点 |
|---|---|---|
| width/height | 图像原始尺寸 | 用于坐标归一化计算 |
| xmin/ymin | 边界框左上角坐标(绝对像素值) | 需转换为相对坐标 |
| xmax/ymax | 边界框右下角坐标(绝对像素值) | 需转换为边界框宽高 |
| name | 物体类别名称 | 需要映射为类别ID |
提示:VOC格式使用绝对坐标,而YOLO需要的是归一化后的相对坐标(0-1之间),这是转换的核心难点。
2. VOC转YOLO格式:手写Python转换脚本实战
新建一个convert_voc_to_yolo.py文件,以下是完整转换代码:
import os
import xml.etree.ElementTree as ET
# 配置区
VOC_ANNOTATIONS_DIR = "VOC2007/Annotations"
YOLO_LABELS_DIR = "yolo_labels"
CLASSES = ["cat", "dog"] # 必须与标注时的类别顺序一致
def convert(size, box):
"""将VOC坐标转换为YOLO坐标"""
dw = 1. / size[0]
dh = 1. / size[1]
x = (box[0] + box[1]) / 2.0
y = (box[2] + box[3]) / 2.0
w = box[1] - box[0]
h = box[3] - box[2]
x = x * dw
w = w * dw
y = y * dh
h = h * dh
return (x, y, w, h)
if not os.path.exists(YOLO_LABELS_DIR):
os.makedirs(YOLO_LABELS_DIR)
for xml_file in os.listdir(VOC_ANNOTATIONS_DIR):
if not xml_file.endswith('.xml'):
continue
tree = ET.parse(os.path.join(VOC_ANNOTATIONS_DIR, xml_file))
root = tree.getroot()
txt_file = open(os.path.join(YOLO_LABELS_DIR, xml_file.replace('.xml', '.txt')), 'w')
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
for obj in root.iter('object'):
cls = obj.find('name').text
if cls not in CLASSES:
continue
cls_id = CLASSES.index(cls)
xmlbox = obj.find('bndbox')
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text),
float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))
bb = convert((w, h), b)
txt_file.write(f"{cls_id} {' '.join([str(a) for a in bb])}\n")
txt_file.close()
关键操作说明:
- 先创建
yolo_labels文件夹存放转换结果 CLASSES列表必须与标注时的类别顺序完全一致- 转换后的txt文件每行格式:
类别ID x_center y_center width height
常见踩坑点:
- 忘记创建输出目录导致程序报错
- 类别顺序与训练时不一致导致预测混乱
- 未处理中文路径导致的编码错误
3. 数据集科学划分:防止模型"偏科"
随机划分数据集是初学者常犯的错误。正确的做法是:
import os
import random
from sklearn.model_selection import train_test_split
# 配置区
IMAGE_DIR = "VOC2007/JPEGImages"
LABEL_DIR = "yolo_labels"
TEST_RATIO = 0.2
VAL_RATIO = 0.1 # 占训练集的比例
# 获取所有图片文件名(不带后缀)
all_images = [f.split('.')[0] for f in os.listdir(IMAGE_DIR) if f.endswith('.jpg')]
# 分层划分:保证各类别比例一致
train_val, test = train_test_split(all_images, test_size=TEST_RATIO, random_state=42)
train, val = train_test_split(train_val, test_size=VAL_RATIO, random_state=42)
def write_file_list(file_list, output_file):
with open(output_file, 'w') as f:
for name in file_list:
f.write(f"{IMAGE_DIR}/{name}.jpg\n")
write_file_list(train, "train.txt")
write_file_list(val, "val.txt")
write_file_list(test, "test.txt")
划分策略对比表:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 完全随机 | 实现简单 | 可能破坏类别分布 | 数据分布均匀时 |
| 分层抽样 | 保持类别比例 | 需要预先统计类别 | 类别不平衡时 |
| 按时间划分 | 符合真实场景时序 | 可能引入时间偏差 | 时序相关数据 |
| 按目录划分 | 管理方便 | 需提前规划目录结构 | 已有明确分类的数据 |
注意:划分完成后建议检查每个集合的类别分布,可使用
matplotlib绘制分布直方图验证。
4. 标签质量检查:揪出标注中的"害群之马"
即使是最仔细的标注也难免出错,常见问题包括:
- 坐标越界:边界框超出图像范围
- 尺寸异常:标注框过大或过小
- 类别错误:猫标成了狗
- 漏标多标:该标的没标或重复标注
用这个脚本快速检测坐标问题:
import cv2
import os
def check_labels(image_dir, label_dir):
for label_file in os.listdir(label_dir):
if not label_file.endswith('.txt'):
continue
image_path = os.path.join(image_dir, label_file.replace('.txt', '.jpg'))
img = cv2.imread(image_path)
if img is None:
print(f"警告:找不到对应图片 {image_path}")
continue
h, w = img.shape[:2]
with open(os.path.join(label_dir, label_file)) as f:
for line in f.readlines():
parts = line.strip().split()
if len(parts) != 5:
continue
_, x, y, bw, bh = map(float, parts)
# 转换回像素坐标检查
x_pixel = int(x * w)
y_pixel = int(y * h)
bw_pixel = int(bw * w)
bh_pixel = int(bh * h)
if (x_pixel < 0 or y_pixel < 0 or
x_pixel + bw_pixel > w or
y_pixel + bh_pixel > h):
print(f"异常文件:{label_file} 坐标超出图像范围")
check_labels("VOC2007/JPEGImages", "yolo_labels")
可视化检查更直观(需要安装opencv):
def visualize_random_samples(image_dir, label_dir, num_samples=5):
import random
samples = random.sample(os.listdir(image_dir), num_samples)
for img_file in samples:
if not img_file.endswith('.jpg'):
continue
img_path = os.path.join(image_dir, img_file)
label_path = os.path.join(label_dir, img_file.replace('.jpg', '.txt'))
img = cv2.imread(img_path)
h, w = img.shape[:2]
with open(label_path) as f:
for line in f.readlines():
cls_id, x, y, bw, bh = map(float, line.strip().split())
# 转换为像素坐标
x1 = int((x - bw/2) * w)
y1 = int((y - bh/2) * h)
x2 = int((x + bw/2) * w)
y2 = int((y + bh/2) * h)
# 绘制边界框
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(img, CLASSES[int(cls_id)], (x1, y1-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36,255,12), 2)
cv2.imshow('Preview', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
5. 高级技巧:数据增强与自动修正
发现标注问题后,除了手动修正,还可以用这些自动化工具:
- CVAT:开源标注工具,支持智能修正
- LabelMe:具有多边形标注和自动边界功能
- Albumentations:直接在YOLO格式上进行数据增强
示例增强代码:
import albumentations as A
transform = A.Compose([
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.2),
A.RandomSnow(p=0.1),
], bbox_params=A.BboxParams(format='yolo'))
def augment_image(image, bboxes):
transformed = transform(image=image, bboxes=bboxes)
return transformed['image'], transformed['bboxes']
处理完所有步骤后,你的数据集目录结构应该是这样的:
dataset_yolo/
├── images/
│ ├── train/
│ ├── val/
│ └── test/
├── labels/
│ ├── train/
│ ├── val/
│ └── test/
├── train.txt
├── val.txt
└── test.txt
最后提醒:永远保留原始VOC格式的标注文件,这是你的"数据底片",所有转换都应通过脚本可重现。在实际项目中,我习惯把整个预处理流程封装成Makefile,这样只需一个make all命令就能从原始标注生成训练就绪的数据集。
更多推荐


所有评论(0)