
简介本资源为面向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_okTrue) # 类别名到 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_okTrue) 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.ptn 更轻适合边缘部署s 精度略高。车牌是规整目标n 通常够用。# 用 ultralytics 训练配置文件 data.yaml 指向划分好的目录 yolo detect train \ modelyolov8n.pt \ dataplate_dataset/split/data.yaml \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ cacheTrue \ nameplate_yolov8n逻辑说明modelyolov8n.pt加载预训练权重这是小数据集能收敛的前提。epochs150配合patience30意思是连续 30 轮验证指标不提升就早停避免无效训练。imgsz640是通用起点如果你的车牌在原图里很小比如 1080p 里只占 100 像素宽可以提到 960 或 1280但显存和速度要重新权衡。cacheTrue把图片缓存到内存一千张图完全放得下能明显加快每个 epoch。lr00.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 \ modelyolov8n.pt \ dataplate_dataset/split/data.yaml \ epochs150 imgsz640 batch16 \ fliplr0.5 flipud0.0 \ mosaic1.0 mixup0.0 \ hsv_h0.015 hsv_s0.7 hsv_v0.4 \ nameplate_aug逻辑说明flipud0.0关掉垂直翻转这是车牌场景的硬规则。mixup0.0关掉mixup 把两张图线性叠加对车牌这种需要清晰边界的任务弊大于利容易让框定位变糊。hsv_h/s/v是色调饱和度明度扰动车牌在不同光照下颜色变化大这三个值保持默认或略调即可hsv_h0.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 开始而nc1只认 0。解决用第 2 章的体检脚本重新核对归一化和类别 id 对齐。现象二模型在验证集上 mAP 很高实际用摄像头一测全是漏检。原因是数据泄漏或场景不匹配。连拍图同时进了训练和验证验证指标虚高或者训练数据全是白天实际场景有夜间。解决按第 3.2 节的分组划分重做数据集并补拍目标场景的图哪怕只补一两百张混进去微调效果立竿见影。现象三小车牌检测不到大车牌正常。原因是输入分辨率不够小目标在下采样中丢失。解决提高imgsz或者用带 P2 小目标检测层的模型结构。但提分辨率会拖慢推理要结合部署硬件权衡。另一个办法是把摄像头拉近或换更高清的头从源头让车牌占更多像素。现象四框位置整体偏移比如统一往右下偏。原因是宽高归一化用错了尺寸常见于从 xml 的size读宽高但标注工具写错了。解决一律从图片文件实际读取宽高重跑转换脚本。这个坑很隐蔽因为 loss 会下降只是框永远差一点。现象五训练到一半显存爆了。原因是batch或imgsz设太大或者cacheTrue在图片很大时把内存吃满。解决先降batch到 8 或 4再考虑降imgsz。如果开了cache且图片是 4K 级别关掉它。一千张 1080p 图开 cache 一般没问题但如果是几千张 4K 图就要小心。6. 训完之后验证、导出与一个提升召回的小技巧训练出best.pt只是开始真正决定能不能用的是验证和导出。先说验证别只看 mAP 一个数。用验证集跑一次预测把结果可视化出来人眼过一遍。重点看三类漏检的尤其小目标和遮挡、误检的把广告牌、相似矩形当车牌、框不准的框住半个车牌。这三类问题对应的改进方向完全不同漏检补数据误检加负样本框不准检查标注质量。# 在验证集上跑预测并保存可视化结果 yolo detect predict \ modelruns/detect/plate_yolov8n/weights/best.pt \ sourceplate_dataset/split/val/images \ conf0.25 \ saveTrue \ nameplate_val_vis逻辑说明conf0.25是置信度阈值验证阶段可以设低一点把可疑框都显示出来方便看漏检。实际部署时这个值要按业务调宁可误检不可漏检的场景比如道闸设 0.1~0.2宁可漏检不可误检的场景设 0.4~0.5。saveTrue把画了框的图存下来去runs/detect/plate_val_vis里逐张看。导出环节如果目标是边缘设备通常导出 ONNX 或 TensorRT。导出时注意imgsz要和训练一致否则精度会掉。# 导出 ONNX动态 batch 方便部署时调 yolo export modelruns/detect/plate_yolov8n/weights/best.pt formatonnx imgsz640 dynamicTrue simplifyTrue逻辑说明dynamicTrue允许推理时变 batchsimplifyTrue做图优化去掉冗余算子。导出后务必用同一张图对比 PyTorch 和 ONNX 的输出确认框位置一致差异超过一两个像素就要查导出参数。最后说一个提升召回的小技巧也是我自己常用的测试时增强TTA。推理时把图水平翻转一次两次预测结果做 NMS 融合。车牌水平翻转后仍是合理目标TTA 对召回有稳定提升代价是推理时间翻倍。对一千张图训出来的模型这个技巧能把漏检压下去一截尤其对模糊和低对比度的车牌。# 简化版 TTA 思路原图 水平翻转各预测一次框坐标翻转回来后合并 # 实际用 ultralytics 可直接在 predict 里开 augmentTrueyolo detect predict modelbest.pt sourcetest.jpg augmentTrue conf0.2augmentTrue就是内置的 TTA 开关一行搞定。代价是速度实时性要求高的场景要测一下帧率够不够。我自己的习惯是任何一千张级别训出来的模型上线前一定用真实机位录一段视频跑一遍看连续帧的稳定性而不是只看静态图的 mAP。视频里框的抖动、偶发漏检、光照突变时的表现才是真正决定这个模型能不能用的东西。数据集再干净也替代不了真实场景的最后一公里验证。希望帮到你。本文还有配套的精品资源点击获取