简介:面向yolo系列算法目标检测任务,这套车牌检测数据集包含1019张已标注图像,配套yolo格式(txt)与VOC格式(xml)两种标签文件,并已按训练和验证需求划分好数据集,内置data.yaml配置文件,解压后即可直接用于yolov5、yolov8等主流版本的模型训练与验证测试,免去自行采集图像、整理标注的繁琐流程。资源包共2000个文件,其中1019个xml对应VOC格式标注框信息,980个txt为yolo格式的类别与归一化坐标,1个yaml用于定义数据集路径和类别名称,整体压缩包约47.28MB,目录结构清晰,方便按需取用。目前已有153人学习下载,内容适合刚入门目标检测的开发者练习完整训练流程,也适合需要在车牌识别场景中快速迭代算法的工程师直接作为基础数据使用。借助该数据集,读者可以专注在模型调参、精度提升和部署验证上,从而更快构建自己的车牌检测应用。
1. 一个 1019 张的 YOLO 车牌检测数据集,问题从来不在数量
做停车场上位机、高速卡口测试或者安防网关的工程师,基本都被同一件事卡过:模型公式烂熟于心,一到真实数据上就废。标题里这个压缩包只有 1019 张图像,放在 COCO、KITTI 面前确实不算多,但车牌检测是单类、场景固定的任务,这个规模足够跑通一版像样的检测器。真正的分水岭在“带标签”三个字上:标签是不是 YOLO 统一的class x_center y_center width height格式,坐标有没有归一化,有没有空标签、重复标签、越界框,这些不先理干净,后面调任何参数都是白费。这篇按解压、验签、训练、验证的顺序把整条链路走通,新手能照抄,做过多年的也能在数据对账和增强策略上再抠出几个细节。
2. 解压、目录结构与 YOLO 标签格式解析
2.1 先摸清目录结构,再决定要不要写转换脚本
拿到yolo算法-车牌检测数据集数据集-1019张图像带标签-.zip,第一件事不是找训练脚本,而是解压看目录。第三方数据集作者习惯五花八门:图片放images/、标签放labels/是标准做法;也有作者把图片和张标签平铺在同一个文件夹;更常见的是图片是.jpg,标签却是.xml或.json,那这个数据集的标注本质是 VOC 或 COCO,标题里写 YOLO 只代表“可用于 YOLO 训练”。
mkdir plate && unzip -q "/data/yolo算法-车牌检测数据集数据集-1019张图像带标签-.zip" -d plate find plate -maxdepth 3 -type f | head -30-q是 quiet 模式,不输出逐个解压文件名,避免刷屏;-d指定解压目录,不用先cd进目标文件夹。压缩包名称带中文,Linux 下一般没问题,Windows 下如果用工具解压出现乱码,是文件名编码 GBK 和 UTF-8 不一致,不代表数据损坏。find加head -30只预览前 30 个文件,重点确认图片和标签的命名规则、目录层级。接下来统计图片和标签数量:
find plate -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.jpeg" \) | wc -l find plate -type f -name "*.txt" | wc -l两个数都应该接近 1019。如果图片数对得上而 txt 数少很多,说明部分标注文件没进 zip,或者有的标注被导出成了其他扩展名。此时直接拿 YOLO 训练会静默忽略缺少标签的图片,造成训练集实际样本数缩水。
2.2 YOLO 标签文件:一行五个数字,多了少了都会出事
YOLO 系列标签和 VOC、COCO 完全不同。每张图对应一个同名.txt,比如img001.jpg对应img001.txt。文件里每一行描述一个目标:
0 0.5234 0.3860 0.1120 0.0851从左到右分别是:类别编号、归一化后的目标中心点 x、中心点 y、目标框宽度 w、目标框高度 h。归一化是指除以图片自身宽高,所以五个值都在 [0, 1] 区间内;坐标系原点在图片左上角,x 向右,y 向下。
| 字段 | 含义 | 车牌检测常见取值 |
|---|---|---|
| class | 类别编号,从 0 开始 | 0 或 1,取决于数据集规定 |
| x_center | 框中心 x 坐标 / 图片宽度 | 0.4~0.6 |
| y_center | 框中心 y 坐标 / 图片高度 | 0.3~0.5 |
| width | 框宽度 / 图片宽度 | 0.05~0.2 |
| height | 框高度 / 图片高度 | 0.03~0.15 |
车牌这类目标占比通常不大,如果标签里出现width=0.8这种值,基本可以判断坐标归一化用错了基准。用一段小脚本把前几个标签文件内容打出来:
import glob, os for f in glob.glob("plate/**/labels/*.txt", recursive=True)[:3]: print(os.path.basename(f)) with open(f) as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: print(" bad line:", line.strip()) print(" ", parts)脚本检查两个点:每行是否正好五个数字,以及类别编号是否越界。如果某行有四个或六个字段,YOLO dataloader 会尝试按五个变量解包,常见报错是not enough values to unpack;如果类别编号是1而你的 YAML 只定义了一个license_plate类,训练会去找不存在的类别 1,报错信息往往指向标签目录而不是类名。
注意:YOLO 的类别编号是从 0 开始,不是从 1 开始。这是新手最容易踩的坑。
2.3 从 VOC、KITTI 或 CVAT 标注转换到 YOLO 格式
如果解压后是 XML,说明原始标注是 VOC 格式。转换逻辑很简单:把bndbox里的绝对像素坐标除以图片宽高,得到归一化值。车牌只有一类,转换脚本可以写得很短:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): if obj.find("name").text != "license_plate": continue box = obj.find("bndbox") x1 = int(box.find("xmin").text) y1 = int(box.find("ymin").text) x2 = int(box.find("xmax").text) y2 = int(box.find("ymax").text) cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w") as out: out.write("\n".join(lines))注意size节点里的宽高必须和图片真实尺寸一致,如果图片被 resize 过而 XML 没改,转换出来的框会整体偏移。有些人会问 VOC 的x2 - x1要不要加 1,因为这个坐标系有的是 0-based 有的是 1-based,实际操作中 1 个像素的差别对 YOLO 这种 anchor-based 的回归影响很小,不用过度纠结。KITTI 标注转换思路相同,只是 KITTI 的坐标字段本身就是浮点数,解析时注意第三行不是类别名而是 truncated、occluded 这类属性。如果你习惯用 CVAT 打标,导出时选择 YOLO 格式,得到的就是这套 txt,直接放进训练目录就行。
3. 数据校验:1019 张图里有多少标签是废的
3.1 先对账:图片和标注文件必须严格同名
训练前先写对账脚本,确认三件事:图片数是否等于 1019,标签数是否等于 1019,有多少图片找不到同名标签。
cd plate/ for img in images/*.jpg; do name=$(basename "$img" .jpg) if [ ! -f labels/$name.txt ]; then echo "missing label: $name" fi done这段bash脚本遍历images目录下所有.jpg,取出主文件名,再检查labels目录下是否存在同名.txt。缺失量在个位数的话,直接删掉对应图片,不要保留无标签样本。原因在于 YOLO 会把没有标签文件的图片当作“无目标正样本”送进 dataloader,这类图片占比一旦超过 5%,模型会整体偏向保守,输出框的数量会变少,P 和 R 同时下降。实际操作中,更完整的检查是用 Python 做集合差,顺带检查是否有图片后缀不是.jpg而是.jpeg或.bmp。
import glob, os images = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob("images/*.*")} labels = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob("labels/*.txt")} print("only image:", sorted(images - labels)[:10]) print("only label:", sorted(labels - images)[:10]) print("total images:", len(images), "total labels:", len(labels))这个脚本直接输出两边文件名的差集,同时以集合长度确认总数。如果labels里出现大量没有对应图片的 txt,往往是压缩包里混进了旧版本标注;训练时这些文件不会报错,但会污染目录统计。
3.2 把标签画回原图,这一步不能省
数字读起来正常不代表框是准的。画框检查是最直观的手段,OpenCV 就能做,不需要引入额外工具。
import cv2 img_path = "images/img001.jpg" label_path = "labels/img001.txt" img = cv2.imread(img_path) h, w = img.shape[:2] for line in open(label_path): cls, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f"plate:{cls:.0f}", (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) cv2.imwrite("check.jpg", img)脚本原理是先把归一化坐标还原成像素坐标,再画矩形和类别文本。max(y1 - 5, 0)这一段很重要,避免文字画到图片上边界之外,OpenCV 遇到负坐标会直接抛异常。抽查 30 到 50 张,重点看三类场景:逆光、夜晚、车牌被前车遮挡。逆光图片中车牌边缘不清晰,标注框经常把车灯或进气格栅圈进去;夜晚图片则容易出现标注框偏移半个车牌的现象。这些图片在训练中是模型最难学的样本,标签质量直接影响模型收敛上限。
3.3 常见标签问题的自查清单
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| loss 降得很低但 mAP 不动 | 标签坐标越界或宽高为 0 | 写脚本扫描每行,找出不在 [0,1] 区间或宽高为 0 的框 |
| 验证集一张都检测不到 | 标签文件和图片完全对不上 | 用 3.1 的集合差脚本输出缺失列表 |
| 预测框整体偏左上或右下 | 归一化时用了错误的图片尺寸 | 检查原图宽高和 XML 的 size 是否一致 |
| 类别编号报错 | txt 写的是 1,YOLO 从 0 数起 | 用 `awk '{print $1}' labels/*.txt |
| 同一张图跑出重复框 | 标签文件里有重复行 | `sort |
另外,Windows 下用标注工具导出的文件有时会残留.txt.bak,这类文件后缀不是.txt,但会被部分本地脚本误读。建议训练目录里只保留严格.txt后缀的标签。
提示:不要因为标题写着“带标签”就跳过校验。1019 张图的 zip 包只要混入 100 张旧标注,训练结果就会莫名其妙变差,而你排查半天都不会怀疑数据本身。
4. 划分数据集并跑通 YOLOv8 训练
4.1 组织目录结构与 plate.yaml 配置
校验完成后,按 YOLO 官方推荐的目录组织:
plates/ ├── train/ │ ├── images/ │ └── labels/ └── val/ ├── images/ └── labels/1019 张图建议按 9:1 划分,验证集控制在 100 到 150 张,不要一刀切成 5:5。划分时用固定随机种子,保证结果可复现。
import os, shutil, random random.seed(42) images = sorted(os.listdir("images")) random.shuffle(images) n_val = int(len(images) * 0.1) os.makedirs("plates/val/images", exist_ok=True) os.makedirs("plates/val/labels", exist_ok=True) for img in images[:n_val]: name = os.path.splitext(img)[0] shutil.copy(f"images/{img}", f"plates/val/images/{img}") shutil.copy(f"labels/{name}.txt", f"plates/val/labels/{name}.txt") for img in images[n_val:]: name = os.path.splitext(img)[0] shutil.copy(f"images/{img}", f"plates/train/images/{img}") shutil.copy(f"labels/{name}.txt", f"plates/train/labels/{name}.txt")代码里的exist_ok=True允许目录已存在,脚本重复执行不会报错。划分完成后数一下 train 和 val 各有多少 txt,确认总和是 1019。然后写 YAML 描述数据位置:
path: /data/plates train: train/images val: val/images nc: 1 names: - license_platepath用绝对路径,避免换目录或换机器后相对路径失效;train和val是相对于path的图片目录,YOLO 会自动推导同级的 labels 目录;nc是类别数,这里只有一个车牌类,所以是 1。如果不写names,训练也能跑,但结果里不会出现可读类别名。
4.2 训练命令与核心参数说明
确认数据没问题后,最省事的训练方式是用 ultralytics 提供的命令行:
yolo detect train \ data=plate.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=100 \ batch=16 \ lr0=0.01 \ patience=20 \ device=0参数含义拆开看:model=yolov8s.pt表示加载 COCO 预训练权重,从这个基础开始微调,比从随机权重训练收敛快得多;imgsz=640会把输入图统一缩放填充到 640x640,占用显存较小,但车牌是小目标,imgsz=960往往效果更好,代价是显存和训练时间增加;batch=16在 8G 显存下刚好,显存吃紧就降到 8;lr0=0.01是初始学习率,数据集小、lr0 太大会在前面几个 epoch 直接发散;patience=20是早停参数,验证集指标连续 20 轮不更新就停掉。
训练结束后,模型权重在runs/detect/train/weights/best.pt。验证命令:
yolo detect val \ data=plate.yaml \ model=runs/detect/train/weights/best.pt \ batch=16输出结果里四个指标最值得看:mAP50一般要大于 0.95,mAP50-95低于 0.7 时优先检查imgsz;Precision高但Recall低,说明图片里实际存在的车牌被漏掉不少,可以调低置信度阈值或者增加训练数据增强强度。训练日志里的box_loss、cls_loss如果出现某个异常跳点,往往是该 batch 包含了一张标签越界的坏数据,回到第 3 章继续排查。
4.3 显存不够或老机器上的几个实用参数
rect=True让 dataloader 按照图片宽高比分组,减少 padding 带来的空白区域,但 batch 内图片尺寸不一致,BatchNorm 统计会有轻微波动。workers=4控制数据加载线程数,Windows 下建议设成 2,否则 DataLoader 在多线程下容易崩。amp=False关闭混合精度,老显卡上训练更稳定,显存会多占约三分之一。cache=ram把 1019 张图一次性读进内存,能显著缩短每个 epoch 的数据加载时间,内存 16G 以下慎开。
这部分如果用的还是 YOLOv5,没有yolo这个命令行入口,改调:
python train.py --data plate.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100YOLOv5 会自动针对数据集重新计算 anchor,不需要手动处理。YOLOv8 是 anchor-free 设计,也就没有自动 anchor 的环节。
5. 验证集可视化与失败样本的循环再利用
5.1 批量导出预测结果到图片和 txt
yolo detect val只能给指标,看不到具体框在哪。实际项目里我会把预测结果批量导出,然后人工翻图:
yolo predict \ model=runs/detect/train/weights/best.pt \ source=plates/val/images \ save_txt=True \ save_conf=True \ conf=0.25 \ project=runs/detect/val_outsave_txt=True会为每张验证图输出同名 txt,格式和训练标签一致;save_conf=True让 txt 里多一个置信度字段;conf=0.25把阈值放低,目的是暴露出更多漏检框。导出后用第 3 章的画框脚本把预测 txt 画回图片,和原标签做对比,重点关注两类失败:原标签里有、预测结果里没有的漏检;预测框和原标签完全不对齐的错检。
5.2 车牌增强的两个反直觉点
车牌检测数据增强和通用目标检测不一样。第一,fliplr左右翻转对检测任务可以用,但如果你后面还要接车牌识别 OCR,翻转后文字方向反了,识别模型需要单独处理;第二,Mosaic拼接增强是很有效的手段,官方训练默认mosaic=1.0,但车牌这种宽高比固定的对象在拼接时往往被裁掉一半,需要把mosaic降到 0.8 左右。
在训练命令里直接覆盖这几个增强参数:
yolo detect train \ data=plate.yaml \ model=runs/detect/train/weights/best.pt \ imgsz=960 \ epochs=50 \ fliplr=0.5 \ flipud=0.0 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ mosaic=0.8flipud=0.0是因为上下翻转车牌后几乎没有现实意义,把它关掉能让模型把能力集中在水平方向上;hsv_h、hsv_s、hsv_v控制 HSV 颜色空间的扰动幅度,车牌颜色本身是训练特征之一,色相偏移过大会让蓝色车牌看起来像绿色,这部分要保守。
5.3 把失败样本回灌到下一轮训练
验证集里连续几轮都检测错的图,是我做车牌项目最看重的数据资产。做法是单独建一个hard_set/目录,把这些图连同原始标签从训练集里复制一份,下一轮训练时用cache=ram加载,同时把mosaic=0.5降低,避免难例在拼接时进一步被扭曲。如果 hard_set 里超过 50 张,基本可以判断原训练集在某个光线条件下覆盖不足,这时候再考虑针对性补数据,而不是盲目增加 epoch。回灌后重新跑yolo detect val,对照 hard_set 的命中率变化,比盯全局 mAP 更能说明模型是否真正变强。
本文还有配套的精品资源,点击获取