简介:本资源为面向YOLO系列目标检测算法的车牌检测数据集,适合正在学习或落地车牌识别项目的开发者、学生及算法工程师使用,可直接用于模型训练与验证测试。数据集已按训练需求划分完毕,并附带data.yaml配置文件,兼容YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本。压缩包共约2000个文件,包含1019个xml标注、980个txt标注和1个yaml配置,整体约47.28MB,其中txt为YOLO格式标注,xml为VOC格式标注,分别存放于两个文件夹,便于按需选用。YOLO格式采用类别索引与归一化中心点、宽高比例值,符合标准训练输入规范。目前已有154人学习下载,读者可借此快速搭建车牌检测训练流程,省去数据采集与标注成本,并灵活切换两种标签格式完成模型验证与调优。
1. 1019 张车牌图带标签,够不够训一个能上路的 YOLO
手上拿到一个yolo算法-车牌检测数据集数据集-1019张图像带标签-.zip,第一反应通常不是兴奋,而是犯嘀咕:一千张出头的图,放到今天动辄几万张的检测任务里,是不是太寒酸了。我一开始也这么想,直到把它真正跑起来才发现,车牌检测这个场景有个特殊性——目标类别单一、纹理高度规整、成像几何相对固定,1019 张标注得当的图,完全能训出一个在固定卡口、停车场出入口、园区闸机这类受控场景下可用的检测器。它解决的不是「通用世界里的车牌」,而是「我的摄像头视野里的车牌」。适合谁?做智慧停车、门禁道闸、车辆出入管理的嵌入式或后端工程师,手里有明确机位、能补拍数据、想快速验证 YOLO 车牌检测链路的人。不适合指望一个 zip 直接商用落地、机位千变万化的团队。这篇就按「拿到 zip 之后怎么走完全程」来讲,从数据体检、格式转换、训练参数到避坑,把每一步能抄的作业都摆出来。
2. 先给数据集做体检:1019 张图到底藏着什么
拿到压缩包别急着解压进训练脚本,先做一次数据体检。车牌检测的坑,八成在训练之前就埋好了——标注框贴边、图像尺寸极端、正负样本失衡、重复图混入,这些问题不提前暴露,训练时 loss 曲线会给你演一出玄学大戏,你还以为是学习率没调好。
2.1 解压后的目录结构与标签格式判断
先看结构。常见的车牌数据集有两种组织方式:一种是 VOC 风格,每张图配一个同名.xml;一种是 YOLO 风格,每张图配一个同名.txt,每行class x_center y_center width height,坐标是归一化到 0~1 的。标题里写「带标签」但没说格式,所以第一步是确认。
# 解压并看目录层级,先别急着改结构 unzip "yolo算法-车牌检测数据集数据集-1019张图像带标签-.zip" -d plate_dataset cd plate_dataset find . -maxdepth 2 -type d # 统计图片和标签文件数量,两者应该基本相等 find . -iname "*.jpg" -o -iname "*.png" | wc -l find . -iname "*.txt" -o -iname "*.xml" | wc -l逻辑说明:find的-maxdepth 2是为了先看清顶层组织,避免一上来就递归到几千个文件刷屏。图片数和标签数对不上,说明有图没标或有标没图,这两种都要单独拎出来处理,不能直接丢进训练。参数上,如果你的图是.jpeg或.bmp,把-iname的模式补全,别漏统计。
如果标签是.xml,说明是 VOC 格式,后面要转;如果是.txt,大概率已经是 YOLO 格式,但要抽查坐标是否归一化。抽查方法很简单,随便打开一个 txt,看数值是不是都在 0~1 之间。如果出现几百上千的数字,那是像素坐标没归一化,得先除宽高。
2.2 用脚本统计框的分布,找出「脏数据」
光看数量不够,得看框长什么样。写个小脚本统计每张图的框数、框的宽高比、框是否贴边。车牌有个强先验:宽高比大致在 2:1 到 5:1 之间(中国蓝牌约 440:140,接近 3.14:1)。偏离这个范围太多的框,要么是标注错误,要么是特殊车牌,得人工看一眼。
import os, glob from PIL import Image img_dir = "plate_dataset/images" lbl_dir = "plate_dataset/labels" ratios, box_counts, tiny = [], [], [] for lbl in glob.glob(os.path.join(lbl_dir, "*.txt")): with open(lbl) as f: lines = [l.strip() for l in f if l.strip()] box_counts.append(len(lines)) # 找到对应图片拿宽高,用于还原像素尺寸 stem = os.path.splitext(os.path.basename(lbl))[0] img_path = None for ext in (".jpg", ".png", ".jpeg"): p = os.path.join(img_dir, stem + ext) if os.path.exists(p): img_path = p break if not img_path: continue w, h = Image.open(img_path).size for line in lines: cls, xc, yc, bw, bh = map(float, line.split()) pw, ph = bw * w, bh * h # 还原成像素宽高 if ph > 0: ratios.append(pw / ph) if pw < 20 or ph < 8: # 小于这个尺寸基本没法训 tiny.append((stem, pw, ph)) print("图片数:", len(box_counts)) print("平均每图框数:", sum(box_counts) / len(box_counts)) print("宽高比 min/median/max:", min(ratios), sorted(ratios)[len(ratios)//2], max(ratios)) print("过小框数量:", len(tiny))逻辑说明:这段脚本干三件事——统计每图框数看密度、还原像素宽高算真实宽高比、标记过小框。参数上,pw < 20 or ph < 8这个阈值不是死的,取决于你的输入分辨率。如果你打算用 640 训练,一个原始像素宽 20 的框缩到 640 后可能只剩几个像素,YOLO 的下采样会让它彻底消失,这种框留着只会污染正样本。宽高比中位数如果落在 2.5~3.5,说明数据整体健康;如果中位数跑到 1 附近,八成是把整个车头框进去了,得回去核对标注规范。
体检完你会对这批数据有个底:能用的有多少、要修的有多少、要不要补拍。这一步花半小时,能省后面几小时的无效训练。
3. 把标签喂给 YOLO:格式转换与数据划分的实操
体检过关,接下来是让数据能被 YOLO 吃进去。这一步的核心是两件事:标签格式对齐、训练/验证集划分。别小看划分,车牌数据如果按随机划分,同一辆车、同一机位连拍的图可能同时进训练和验证,验证指标虚高,上线就翻车。
3.1 VOC 转 YOLO 的转换脚本与坐标归一化
如果体检发现是.xml,用下面这个脚本转。转换的关键是 VOC 的xmin/ymin/xmax/ymax是左上右下的绝对像素,YOLO 要的是中心点加宽高的归一化值,中间任何一步算错,框就会整体偏移。
import os, glob import xml.etree.ElementTree as ET from PIL import Image xml_dir, img_dir, out_dir = "plate_dataset/annotations", "plate_dataset/images", "plate_dataset/labels" os.makedirs(out_dir, exist_ok=True) # 类别名到 id 的映射,车牌单类就写一个 classes = {"plate": 0} for xml in glob.glob(os.path.join(xml_dir, "*.xml")): tree = ET.parse(xml) root = tree.getroot() stem = os.path.splitext(os.path.basename(xml))[0] # 图片真实宽高优先从文件读,别信 xml 里的 size,经常标错 img_path = None for ext in (".jpg", ".png", ".jpeg"): p = os.path.join(img_dir, stem + ext) if os.path.exists(p): img_path = p break if not img_path: continue W, H = Image.open(img_path).size lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in classes: continue bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 裁剪到图像边界,防止标注越界 xmin, xmax = max(0, xmin), min(W, xmax) ymin, ymax = max(0, ymin), min(H, ymax) xc = (xmin + xmax) / 2 / W yc = (ymin + ymax) / 2 / H bw = (xmax - xmin) / W bh = (ymax - ymin) / H lines.append(f"{classes[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, stem + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:三个细节决定成败。第一,宽高从图片文件读而不是从 xml 的<size>读,因为标注工具经常把 size 写错,用错尺寸归一化,框会整体缩放偏移。第二,max(0, xmin)这类裁剪是后悔药,标注越界在人工标注里太常见,不裁的话归一化后会出现负值或大于 1 的值,训练时直接报错或产生诡异梯度。第三,保留 6 位小数,精度足够且文件不会太大。参数上,classes字典按你的实际类别改,如果数据集里车牌还分了蓝牌黄牌,就加两个 id,但单类检测通常合并成一类更稳。
3.2 训练验证集划分:别让连拍图泄漏
划分不是random.shuffle就完事。车牌数据很多来自视频抽帧,相邻帧几乎一样。如果随机划分,训练集里的第 100 帧和验证集里的第 101 帧是同一辆车同一个角度,验证 mAP 会虚高十几个点,你以为模型很强,换条视频就原形毕露。
import os, glob, shutil, random random.seed(42) src_img, src_lbl = "plate_dataset/images", "plate_dataset/labels" dst = "plate_dataset/split" for sub in ("train/images", "train/labels", "val/images", "val/labels"): os.makedirs(os.path.join(dst, sub), exist_ok=True) imgs = sorted(glob.glob(os.path.join(src_img, "*"))) # 按文件名前缀分组,假设连拍图前缀相同,如 cam01_0001.jpg groups = {} for p in imgs: stem = os.path.splitext(os.path.basename(p))[0] key = stem.rsplit("_", 1)[0] if "_" in stem else stem groups.setdefault(key, []).append(p) keys = list(groups.keys()) random.shuffle(keys) val_ratio = 0.15 n_val = max(1, int(len(keys) * val_ratio)) val_keys = set(keys[:n_val]) for key, paths in groups.items(): split = "val" if key in val_keys else "train" for p in paths: stem = os.path.splitext(os.path.basename(p))[0] lbl = os.path.join(src_lbl, stem + ".txt") shutil.copy(p, os.path.join(dst, split, "images", os.path.basename(p))) if os.path.exists(lbl): shutil.copy(lbl, os.path.join(dst, split, "labels", stem + ".txt"))逻辑说明:核心是「按组划分」而不是「按图划分」。key = stem.rsplit("_", 1)[0]假设文件名用下划线分隔机位和帧号,如果你的命名是cam01-0001,把分隔符改成-。参数上val_ratio = 0.15对一千张的数据集比较合适,验证集一百多张足够看趋势,又不至于让训练集太瘦。如果你的文件名没有可分组的前缀,退而求其次按时间排序后分段划分,也比纯随机强。划分完记得核对 train 和 val 的图片数,以及标签是否一一对应。
4. 训练参数怎么设:1019 张图上的 YOLO 配置清单
数据就绪,进入训练。一千张图属于小数据集,参数设错很容易过拟合或者根本学不动。这一章把关键参数逐个说清楚,包括为什么这么设、改了会怎样。
4.1 从预训练权重出发:小数据集的必选项
一千张图从零训,几乎必然过拟合。正确姿势是加载 COCO 预训练权重做迁移学习。YOLOv8 系列的常见做法是yolov8n.pt或yolov8s.pt,n 更轻适合边缘部署,s 精度略高。车牌是规整目标,n 通常够用。
# 用 ultralytics 训练,配置文件 data.yaml 指向划分好的目录 yolo detect train \ model=yolov8n.pt \ data=plate_dataset/split/data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ cache=True \ name=plate_yolov8n逻辑说明:model=yolov8n.pt加载预训练权重,这是小数据集能收敛的前提。epochs=150配合patience=30,意思是连续 30 轮验证指标不提升就早停,避免无效训练。imgsz=640是通用起点,如果你的车牌在原图里很小(比如 1080p 里只占 100 像素宽),可以提到 960 或 1280,但显存和速度要重新权衡。cache=True把图片缓存到内存,一千张图完全放得下,能明显加快每个 epoch。lr0=0.01是初始学习率,小数据集别设太大,否则前期 loss 震荡。
data.yaml长这样:
path: /abs/path/to/plate_dataset/split train: train/images val: val/images nc: 1 names: ["plate"]path一定写绝对路径,相对路径在不同工作目录下会找不到数据,这是新手最常翻的车之一。
4.2 数据增强:车牌场景该开哪些、该关哪些
YOLO 默认开了一堆增强,但车牌场景有它的脾气。水平翻转可以用,因为车牌左右翻转后仍是合理车牌(虽然文字反了,但检测框任务不关心文字)。垂直翻转要关,倒过来的车牌现实中不存在,开了只会引入噪声。马赛克增强(mosaic)对小数据集有帮助,能提升小目标鲁棒性,但车牌通常不是极小目标,开到默认的 1.0 即可,不必额外加强。
# 在训练命令里覆盖增强参数 yolo detect train \ model=yolov8n.pt \ data=plate_dataset/split/data.yaml \ epochs=150 imgsz=640 batch=16 \ fliplr=0.5 flipud=0.0 \ mosaic=1.0 mixup=0.0 \ hsv_h=0.015 hsv_s=0.7 hsv_v=0.4 \ name=plate_aug逻辑说明:flipud=0.0关掉垂直翻转,这是车牌场景的硬规则。mixup=0.0关掉,mixup 把两张图线性叠加,对车牌这种需要清晰边界的任务弊大于利,容易让框定位变糊。hsv_h/s/v是色调饱和度明度扰动,车牌在不同光照下颜色变化大,这三个值保持默认或略调即可,hsv_h=0.015控制色调扰动幅度小一点,避免把蓝牌调成绿牌这种离谱颜色。参数没有绝对最优,但方向是:几何增强保守,光照增强适度。
4.3 看训练曲线判断过拟合还是欠拟合
训练跑起来后,别只盯着最后的结果,看曲线。results.csv里有每轮的 loss 和 mAP。判断标准:训练 loss 持续下降但验证 mAP 早早见顶然后下滑,是过拟合,该加增强或减模型容量;训练 loss 和验证 loss 都居高不下,是欠拟合,该加轮数或提学习率。一千张图最常见的是前者。
import pandas as pd df = pd.read_csv("runs/detect/plate_yolov8n/results.csv") df.columns = [c.strip() for c in df.columns] # 找验证 mAP 最高那轮,看它和最后一轮差多少 best = df["metrics/mAP50(B)"].idxmax() print("最佳轮次:", best, "最佳 mAP50:", df.loc[best, "metrics/mAP50(B)"]) print("最后一轮 mAP50:", df["metrics/mAP50(B)"].iloc[-1]) print("训练框 loss 首末:", df["train/box_loss"].iloc[0], df["train/box_loss"].iloc[-1])逻辑说明:idxmax()找到验证指标峰值轮次。如果最佳轮次远早于最后一轮,且最后一轮指标明显更低,说明后面都在过拟合,应该用最佳轮次的权重而不是最后一轮。YOLO 训练默认保存best.pt和last.pt,部署时用best.pt。参数上,列名可能因版本略有差异,跑之前先print(df.columns)确认,别硬套。
5. 避坑与排查:车牌检测训练里最容易翻车的五件事
这一章是我自己踩过和帮别人排查过的真实问题,按「现象 → 原因 → 解决」写,遇到对应症状直接对号入座。
现象一:训练 loss 正常下降,但验证 mAP 一直是 0 或极低。原因通常是标签路径或格式不对,模型根本没学到有效监督信号。排查顺序:先确认data.yaml里的train/val路径下确实有图片,再确认每张图有同名.txt,最后抽查 txt 内容坐标是否在 0~1。常见错误是 txt 里是像素坐标没归一化,或者类别 id 从 1 开始而nc=1只认 0。解决:用第 2 章的体检脚本重新核对,归一化和类别 id 对齐。
现象二:模型在验证集上 mAP 很高,实际用摄像头一测全是漏检。原因是数据泄漏或场景不匹配。连拍图同时进了训练和验证,验证指标虚高;或者训练数据全是白天,实际场景有夜间。解决:按第 3.2 节的分组划分重做数据集,并补拍目标场景的图,哪怕只补一两百张,混进去微调,效果立竿见影。
现象三:小车牌检测不到,大车牌正常。原因是输入分辨率不够,小目标在下采样中丢失。解决:提高imgsz,或者用带 P2 小目标检测层的模型结构。但提分辨率会拖慢推理,要结合部署硬件权衡。另一个办法是把摄像头拉近或换更高清的头,从源头让车牌占更多像素。
现象四:框位置整体偏移,比如统一往右下偏。原因是宽高归一化用错了尺寸,常见于从 xml 的<size>读宽高但标注工具写错了。解决:一律从图片文件实际读取宽高,重跑转换脚本。这个坑很隐蔽,因为 loss 会下降,只是框永远差一点。
现象五:训练到一半显存爆了。原因是batch或imgsz设太大,或者cache=True在图片很大时把内存吃满。解决:先降batch到 8 或 4,再考虑降imgsz。如果开了cache且图片是 4K 级别,关掉它。一千张 1080p 图开 cache 一般没问题,但如果是几千张 4K 图就要小心。
6. 训完之后:验证、导出与一个提升召回的小技巧
训练出best.pt只是开始,真正决定能不能用的是验证和导出。先说验证,别只看 mAP 一个数。用验证集跑一次预测,把结果可视化出来,人眼过一遍。重点看三类:漏检的(尤其小目标和遮挡)、误检的(把广告牌、相似矩形当车牌)、框不准的(框住半个车牌)。这三类问题对应的改进方向完全不同,漏检补数据,误检加负样本,框不准检查标注质量。
# 在验证集上跑预测并保存可视化结果 yolo detect predict \ model=runs/detect/plate_yolov8n/weights/best.pt \ source=plate_dataset/split/val/images \ conf=0.25 \ save=True \ name=plate_val_vis逻辑说明:conf=0.25是置信度阈值,验证阶段可以设低一点,把可疑框都显示出来,方便看漏检。实际部署时这个值要按业务调,宁可误检不可漏检的场景(比如道闸)设 0.1~0.2,宁可漏检不可误检的场景设 0.4~0.5。save=True把画了框的图存下来,去runs/detect/plate_val_vis里逐张看。
导出环节,如果目标是边缘设备,通常导出 ONNX 或 TensorRT。导出时注意imgsz要和训练一致,否则精度会掉。
# 导出 ONNX,动态 batch 方便部署时调 yolo export model=runs/detect/plate_yolov8n/weights/best.pt format=onnx imgsz=640 dynamic=True simplify=True逻辑说明:dynamic=True允许推理时变 batch,simplify=True做图优化,去掉冗余算子。导出后务必用同一张图对比 PyTorch 和 ONNX 的输出,确认框位置一致,差异超过一两个像素就要查导出参数。
最后说一个提升召回的小技巧,也是我自己常用的:测试时增强(TTA)。推理时把图水平翻转一次,两次预测结果做 NMS 融合。车牌水平翻转后仍是合理目标,TTA 对召回有稳定提升,代价是推理时间翻倍。对一千张图训出来的模型,这个技巧能把漏检压下去一截,尤其对模糊和低对比度的车牌。
# 简化版 TTA 思路:原图 + 水平翻转各预测一次,框坐标翻转回来后合并 # 实际用 ultralytics 可直接在 predict 里开 augment=Trueyolo detect predict model=best.pt source=test.jpg augment=True conf=0.2augment=True就是内置的 TTA 开关,一行搞定。代价是速度,实时性要求高的场景要测一下帧率够不够。
我自己的习惯是:任何一千张级别训出来的模型,上线前一定用真实机位录一段视频跑一遍,看连续帧的稳定性,而不是只看静态图的 mAP。视频里框的抖动、偶发漏检、光照突变时的表现,才是真正决定这个模型能不能用的东西。数据集再干净,也替代不了真实场景的最后一公里验证。希望帮到你。
本文还有配套的精品资源,点击获取