1695张车牌图像也能训出可用YOLO检测模型?实战避坑指南
简介这份车牌图像数据集适用于目标检测与车牌识别方向的算法训练和验证面向计算机视觉研究者、深度学习开发者以及智能交通相关项目实践者。包内包含1695张真实场景车辆图像每张对应一个txt格式的YOLO标注文件内含类别标签与边界框坐标另有yaml配置文件便于直接接入YOLO系训练框架jpg图片覆盖不同拍摄角度、光线和背景可支撑模型从数据预处理到训练评估的完整流程。整个资源共2000个文件以txt标注、jpg图像和yaml配置为主要类型压缩包大小约147.47MB结构简洁、格式规范适合快速投入车牌检测与识别任务。目前已有132人学习下载对于需要高质量标注数据开展实验的开发者来说是一份可直接使用的数据集也能为高速公路、停车场、城市监控等场景中的车牌识别方案提供基础数据支撑。1. 用 1695 张车牌图像数据跑对象检测起步前先对任务难度有数同样是做车牌图像识别手里这套车牌图像数据集只有 1695 张图片标注是 YOLO 格式目标很明确跑对象检测任务把画面里的车牌框出来。很多人第一反应是量不够随手去找一个几万张的公开数据集来补结果场景对不上模型在自家摄像头下一路翻车。我的看法恰恰相反1695 张单类 YOLO 格式车牌数据足以训练出一个能用的检测 baseline前提是你先搞清楚任务边界、数据里的场景分布和标注质量再决定怎么训练、怎么调参、怎么部署。这篇文章按我实际做过的流程走一遍从确认任务类型、评估数据量到准备训练、避开高发坑最后落到测试集验证和边缘部署。2. 数据到手先想清楚三件事检测还是识别、1695 张够不够、为什么是 YOLO 格式2.1 车牌检测是车牌识别的前置步骤别把任务混在一起标题里同时出现了“车牌图像数据集”和“车牌图像识别”但 YOLO 格式标注对应的任务其实是对象检测而不是字符识别。对象检测的输出是一个个边界框和类别标签你要的是“画面里有没有车牌、车牌在哪里”车牌识别要输出的是“粤A12345”这种具体字符序列那是另一个任务。常见做法是把整个流程拆成两级先用检测模型定位车牌框再把框住的区域裁出来做透视矫正最后送进字符识别模型比如 LPRNet 或基于 CRNN 的方案读字符。有些项目图省事直接用 YOLO 检测 7 个字符的位置再逐字符分类这属于字符级检测需要给每个字符单独打框。1695 张图如果每张只标一个车牌框那做的是车牌定位不是字符级识别。拿到数据先确认标签里是几个类别、每张图几个框再判断该往哪个方向走。这个区分直接决定后续工作量和效果预期。检测模型在精心标注的 1695 张图上做到 mAP50 0.95 不是什么难事但如果目标是把整串车牌字符读出来这 1695 张图大概率不够还得叠加字符级数据和识别模型。做工程最怕在任务定义阶段就模糊后面所有调参都白费。2.2 1695 张意味着什么单类检测里属于“少而有用”的体量对比一下公开检测数据集COCO 的训练集有 11 万张图、80 个类别VOC 有约 2.5 万张图、20 个类别。按类别平均看COCO 每类大约几千张。你真正常见的训练基线是单类1695 张图按 80/20 划分大约 1350 张训练、340 张验证。对单类车牌检测来说这个量级确实偏小但不至于不可用。关键在于“多样性”而不是“数量”。同一个停车场同一批车牌的 100 张图信息量和 1 张图差不多反而是不同光照、不同角度、不同车牌颜色、不同遮挡程度的少量样本价值更高。你要是开着车在一个园区里绕两三圈一晚上也能攒出两三千张图但去重之后真正有效的正样本可能就一两百。从这个角度说1695 张整理干净、标注正确的图片含金量不一定比网盘里翻出来的几万张低。评估数据够不够时我一般先做两件事一是统计标注框的宽高比分布二是看每张图平均几个框。车牌是典型的细长目标宽高比在 3:1 左右如果标注框的宽高比标准差很大说明标注质量有问题如果平均每张图 1.2 个框以上说明存在多车牌场景训练时要考虑密集目标情况。这个统计也可以用一个脚本跑出来后面会给出代码。2.3 为什么偏偏是 YOLO 格式归一化坐标的取舍YOLO 格式的标注规则是每张图片对应一个同名 .txt 文件每一行一个目标格式为class_id center_x center_y width height其中坐标全部按图片宽高归一化到 0 到 1 之间。比如一张 1920x1080 的图里车牌中心在 (960, 540)、宽 400、高 140那么标注行就是0 0.500 0.500 0.208 0.130这种格式的好处首先是直观其次是不依赖图片绝对尺寸训练脚本拿到直接就能算。LabelImg 在 YOLO 模式下保存的就是这种 .txt和图片同名同目录或放在指定 labels 目录。对比一下 Pascal VOC 的 XML 和 COCO 的 JSONXML 要解析节点JSON 要维护 id 映射YOLO 格式一个文本文件搞定适合小项目人工抽检。它的代价是坐标写错时极难发现只要宽高算错一位小数训练时 loss 会莫名其妙地高而且这些问题往往积攒到训练完才发现。另一点要注意YOLO 格式的类别编号从 0 开始。如果你的数据里只有一类车牌那所有标注都应该是0。如果有两个类别比如蓝牌和黄牌分别标 0 和 1那 data.yaml 里的 names 列表就要和编号严格对齐。这个问题看似简单实际踩坑的人非常多后面避坑章节专门写。3. 从 1695 张到能用的检测模型数据检查、场景分层与 YOLOv8 训练3.1 训练前的完整性检查缺标注、坏图片、标注坐标越界很多数据集是从网盘或移动硬盘里倒出来的最常见的问题不是标注格式本身而是文件对不上号。图片有 1695 张labels 目录里也可能有 1695 个文件但里面可能有空标注、文件名后缀不一致、图片是坏的之类的情况。我在训练前一定会跑一遍完整性和尺寸分布检查花两分钟省得训练到一半被 batch 报错打断。# 检查数据集完整性图片、标注文件、尺寸分布 from pathlib import Path from PIL import Image img_dir Path(images/) # 图片目录按你的实际结构改 lbl_dir Path(labels/) # 标注目录 imgs sorted(img_dir.glob(*.jpg)) sorted(img_dir.glob(*.png)) print(f图片数量: {len(imgs)}) missing [] sizes set() for img_path in imgs: label lbl_dir / (img_path.stem .txt) if not label.exists(): missing.append(img_path.name) # 统计缺失标注的图片 continue with Image.open(img_path) as im: sizes.add(im.size) print(f缺少标注的文件: {len(missing)} 张, 示例: {missing[:5]}) print(f图片尺寸种类: {sorted(sizes)})这段代码只做两件事一是找出没有对应标注的图片二是统计所有图片的尺寸种类。逻辑很简单但非常实用。如果你发现有几百张图缺标注那训练时 YOLO 会把这些图当成背景图来算损失等于主动往模型里塞负样本结果就是误检变多。图片尺寸种类如果超过 5 种说明数据集里混了不同分辨率训练时 imgsz 设置要谨慎避免小图被拉伸变形。跑完脚本看输出通常要求缺标注文件数为 0。如果尺寸种类比较多我会建议统一缩放或剪裁把大部分图片处理到相近的分辨率再训练。车牌检测这种任务图大了信息丰富但也不代表 1920 的图一定要用 1920 训练后面会细说。3.2 划分数据集的正确姿势按场景分层而不是随机 shuffle很多教程告诉你 82 随机划分训练集和验证集这在车牌数据上会出问题。同一辆车在连续几帧里都会出现随机划分极可能把同一块车牌同时分进训练集和验证集验证指标虚高得离谱部署到新场景立刻现原形。我一般会先看文件名有没有规律比如命名是street_001.jpg、gate_002.jpg这种带前缀的就按前缀分组如果文件名没有规律就按拍照时间或者目录层级分。分组的本质是保证验证集里的车牌场景在训练里从来没出现过这才是检测模型真实的泛化水平。# 按场景分组后划分避免同一场景同时出现在 train 和 val from collections import defaultdict import random random.seed(42) # 固定随机种子保证结果可复现 # 假设文件名是中国车牌或区域前缀例如 hangzhou_01.jpg, beijing_02.jpg # 如果你没有前缀可以用图片所在子目录名作为分组键 def scene_key(path): return path.name.split(_)[0] groups defaultdict(list) for img in imgs: groups[scene_key(img)].append(img) train_imgs, val_imgs [], [] for scene, paths in groups.items(): random.shuffle(paths) cut int(len(paths) * 0.8) # 每个场景内 80% 训练、20% 验证 train_imgs paths[:cut] val_imgs paths[cut:] print(f训练集: {len(train_imgs)} / 验证集: {len(val_imgs)}) # 训练集: ~1350 / 验证集: ~340这里的核心逻辑是先在场景内部打乱再切 80/20。注意我的场景分组是按文件名首个下划线前的部分如果你的文件命名不是这个风格就改 scene_key 函数。每个场景取 20% 做验证能有效防止同一个停车场同一个车道的画面同时出现在两边。划分完之后把 train_imgs 和 val_imgs 分别拷到 train 和 val 目录或者直接把文件名列表存成 txt供后面做数据加载。3.3 用 YOLOv8 训练Anaconda 环境配置与最小命令先交代环境。YOLOv8 基于 ultralytics 包Python 版本建议 3.8 到 3.10Anaconda 下建一个干净环境最省心conda create -n yolo python3.10 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics这里的 PyTorch 我按 CUDA 11.8 装适配大多数 30 系和 40 系显卡。没有独显的话也可以装 CPU 版1695 张图用 yolov8n 跑 100 个 epoch速度慢一些但不是跑不动。训练之前把数据组织成 YOLO 期望的目录结构images 下放 train 和 val 两个子目录对应图片labels 下放对应标注。然后写数据配置# 在项目目录里生成数据配置文件 plate.yaml cat plate.yaml EOF path: /absolute/path/to/your/dataset train: images/train val: images/val nc: 1 names: [license_plate] EOF重点是 path 一定要换成绝对路径相对路径在换机器跑或者用 IDE 调试时经常翻车。nc 是类别数当前是 1 类names 要和标注文件里的 class_id 一一对应。然后训练命令非常短yolo train modelyolov8n.pt dataplate.yaml epochs100 imgsz640 batch16 patience20 device0模型我默认选 yolov8n因为单类车牌检测用不到大模型的容量n 模型训练速度快一半精度差距很小。imgsz 是训练输入尺寸640 是默认值batch 按显存调12GB 显存跑 16 没问题。patience 是早停 patience连续 20 个 epoch 验证指标不涨就停可以有效防止小数据集过拟合后期瞎跳。训练结束之后别急着开心weights 目录下会生成 best.pt 和 last.pt看 best.pt 就好。真正要先看的是 results.png 里 loss 曲线和验证指标曲线box_loss 和 cls_loss 是否稳步下降mAP50 是否稳定在高位。任何一条曲线如果在训练末尾还在剧烈震荡先回到数据层面找原因而不是继续加 epoch。3.4 训练日志怎么读box_loss、cls_loss、dfl_loss 各管什么YOLOv8 的损失函数由三个部分组成box_loss 管边界框的位置回归得准不准cls_loss 管类别判断对不对dfl_loss 是分布焦点损失负责边界框边缘的精细度。车牌检测只有一个类别所以 cls_loss 通常降得很快box_loss 和 dfl_loss 才是重点观察对象。小数据集上有个常见现象box_loss 看起来降得不错但 mAP50-95 上不去卡在 0.8 左右。这往往不是模型没学好而是标注框的边界本身就不准。车牌是规则矩形标注时稍微手抖一点框的左右边界差几个像素对 mAP50 影响不大对 mAP50-95 却很伤。遇到这种情况我会抽 20 张训练图出来人工看一眼标注框的贴合程度而不纠结于模型结构。归根结底数据集的标注质量决定了这个环节的天花板。4. 车牌数据集五个高频翻车点与排查记录坐标、类别、阈值、增强这块内容都是我实际跑过的血泪经验每条都按“现象 → 原因 → 解决”的方式来写。四个字先总结标注先行。后续 80% 的排查都回到数据上模型参数的调整反而靠后。4.1 训练 loss 不降、验证 mAP 为 0图片 EXIF 旋转把坐标带偏现象数据集是从手机或相机里导出的 JPG训练了 30 个 epochloss 始终在一个高位波动val 的 mAP 一直是 0。人工打开图片看内容完全正常标注框位置也对着车牌。原因手机拍的照片里有 EXIF 旋转信息图片查看器和 LabelImg 会自动按旋转后的方向展示所以肉眼看不出问题。但 ultralytics 的加载逻辑读的是原始像素数据不会自动应用 EXIF 旋转导致模型看到的画面是横着的而标注坐标是基于竖着的画面打的框和内容差了 90 度。解决训练前先统一处理一遍图片把 EXIF 旋转固化到像素里from PIL import Image, ImageOps from pathlib import Path for img_path in Path(images/).glob(*.jpg): with Image.open(img_path) as im: im ImageOps.exif_transpose(im) im.save(img_path)处理后重新检查一遍尺寸因为 EXIF 旋转会把宽高互换原来 1920x1080 的图变成 1080x1920尺寸统计里会出现和原文件不同的数字这是正常现象。如果标注是基于旋转前图片标的坐标需要重新生成如果标注本身是在 LabelImg 里按正确方向打的转换后直接对齐即可。最省事的办法是拿到数据集第一件事就先跑一遍这个脚本后续标注都基于处理后的图片进行。4.2 class_id 从 0 开始还是从 1 开始类别编号错位现象训练正常收敛验证 mAP 也不低但推理时发现所有检出的车牌都被标成了类别 1或者类别 2总之不是预期的那一类。原因YOLO 的类别编号从 0 开始但有不少标注工具或人工改动时把第一类写成了 1第二类写成了 2。如果你的 data.yaml 里 names 写成[blue_plate, yellow_plate]而标注文件里蓝牌写的是 1、黄牌写的是 2模型训练时就会把第 1 类当成蓝牌第 2 类当成黄牌实际完全错位。解决写个脚本扫描所有标注文件统计每个 class_id 出现的图片数和目标数# 统计标签文件里出现的所有类别编号 cat labels/*.txt | awk {print $1} | sort -n | uniq -c输出长这样1350 0或900 0、790 1。如果显示只有 0那没关系如果出现 1 和 2 而没有 0说明编号整体偏移了一位用 sed 批量替换把所有 1 改成 0、2 改成 1。这一步做完再检查 data.yaml 的 nc 是否等于实际类别数加 1也就是最大编号加 1。这是最廉价也最容易被忽视的错误。4.3 小数据集过拟合train_loss 很低但 val 指标卡住现象训练到第 60 个 epoch 时训练集的 box_loss 已经下降到 0.01但验证集 mAP50 从 0.9 上到 0.92 之后就不再动甚至出现回落。看上去模型“很会做题”但换一批新图就露馅。原因1695 张图片的多样性有限模型把训练集里的背景特征、光照条件都记住了而不是真正抽象出“车牌长得什么样”。这是小数据集上最典型的过拟合信号。解决分两步。第一步加强数据增强YOLOv8 支持直接通过训练参数控制yolo train modelyolov8n.pt dataplate.yaml epochs100 imgsz640 batch16 \ mosaic0.8 hsv_h0.015 hsv_s0.7 hsv_v0.4 degrees5 translate0.1 scale0.3注意 degrees 我故意给得很小车牌是规则矩形旋转超过 10 度在真实场景里很少见转得太多反而制造无效样本。第二步是换更小的模型yolov8n 已经足够小可以再把 imgsz 降到 416减少参数量迫使模型聚焦车牌结构而不是背景细节。训练时开 patience20让它提前停下别在验证指标已经不再上涨之后还硬跑。4.4 推理时置信度门限调反误检多还是漏检多现象模型训练完测试时发现两种极端。要么一张图里冒出三四个框要么明明有车牌的地方一个框都没有。原因多数情况不是模型问题而是推理时的置信度门限 conf 设得不合适。YOLO 默认推理参数是 conf0.25也就是说信心超过 25% 的框都会被保留。车牌这类目标纹理简单背景里圆形的排气孔、反光的保险杠都可能触发 30% 左右的置信度所以误检多反过来如果图里的车牌离得远、像素小置信度可能只有 20%低于阈值就漏检了。解决先分清你的需求偏误检还是偏漏检。搞停车场道闸宁可漏检一帧也不能乱框把 conf 调高到 0.5 以上yolo predict modelbest.pt sourcetest.jpg conf0.55 iou0.6iou 参数是 NMS 去重时的阈值框和框重叠超过 0.6 就保留置信度高的那个多框问题时调低 iou 也有效果。如果你发现多数正常图片可以检出只有暗光和远距离场景漏那 conf 降到 0.15后续再加一个目标跟踪器做时序过滤用连续多帧的一致性把假阳性压下去。阈值调参看起来像玄学但本质是在误检率和召回率之间找平衡点没有统一答案只能对着自己的测试集来回试。4.5 数据增强过头把车牌变成了平行四边形现象训练时 mAP50 一路冲到 0.95测试时对稍微倾斜的车牌反而检测失败对正对着镜头的车牌效果却很好。原因数据增强参数里开了透视变换或过大的旋转训练时模型天天看到各种夸张畸变的“车牌”反而对正常视角下的车牌特征学得不敏感。增强是为了模拟真实变化但一旦超过现实分布就变成了负优化。解决回到参数上把透视相关的增强关掉或降到最低只保留颜色抖动和轻微缩放。我常用的参数在上面 4.3 里已经给了。另一个更隐蔽的问题是标注框没有跟着增强同步变换尤其在自定义增强管线里如果只增强图片不改标注框模型学的全是错位样本。建议直接用 ultralytics 内置的增强逻辑它保证标注框与图片同步变换不要自己手写一套增强再手动改框那大概率漏改。数据增强是给小数据集续命用的不是把数据变难看用的把握好度。5. 验证与部署批量检一遍测试集并落到边缘设备5.1 用测试脚本批量验证别只看单张图效果单张图测得好不代表视频流里表现稳定。我会单独留一个测试集目录里面放训练和验证都没见过的图然后让模型批量跑一遍输出图片和结果再统计用得上的指标# 批量推理测试集统计每张图的检出框数和平均置信度 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetest/, conf0.45, # 置信度门限按漏检/误检情况调整 iou0.5, # NMS IoU 阈值 saveTrue, projectruns/test_out, nameplate_conf045, ) total_box 0 for r in results: total_box len(r.boxes) print(f平均每张图检出框数: {total_box / len(results):.2f})如果平均每张图框数超过 1.5而测试集大多数图片只有一个车牌说明 conf 偏低或场景里本来就有多车需要结合图片判断。批量推理的结果图最好每张都翻一遍车牌没框出来、框错位置、把车灯当车牌三种错误模式分别对应不同的解决方向第一个提高召回第二个检查标注质量第三个调高 conf。5.2 车牌是细长目标推理尺寸与矩形推理的取舍车牌宽高比接近 3:1在 640x640 的输入下车牌本身占的像素其实不多。训练时如果想提升小目标表现不要盲目把 imgsz 加到 1280那样的显存消耗和推理耗时会让部署成本翻倍。我一般按部署设备的算力反推RK3588 这类边缘设备跑 640 是常态就维持 640如果只做离线分析可以提高到 960。数据集检测框本来就是标注好的让模型在合适的输入尺寸下训练比强行塞大图更重要。5.3 边缘部署流程从 PyTorch 到 ONNX 再到 RKNN模型定下来之后导出和量化也有讲究。先用 ultralytics 导出 ONNXyolo export modelbest.pt formatonnx imgsz640 opset12然后把 ONNX 转成 RKNN 格式部署到 RK3588 的 NPU 上。这个环节最容易翻车的是 INT8 量化车牌字符是细线条纹理量化后容易掉精度。我的习惯是先用 FP16 跑一版验证效果如果精度可接受就不上 INT8如果必须 INT8校准集不要只挑清晰的近景图要把暗光、模糊、小尺寸车牌混进去不然量化后误检率突然飙升在监控画面里一屏全是框。5.4 数据不够时的“后悔药”把标注闭环跑起来1695 张毕竟是起点不是终点。最实用的办法是让模型先上线跑一阵把置信度在 0.3 到 0.5 之间摇摆的模糊样本挑出来人只需要快速确认是不是车牌确认过的图再回流训练集。这样迭代两三轮难点场景就会被逐步覆盖误检和漏检会肉眼可见地下降。这也是我在每个车牌检测项目里最后都会做的收尾动作模型能力是一方面数据回流机制才是精度持续爬升的引擎。希望这套思路对你帮得上忙少走我当年走过的弯路。本文还有配套的精品资源点击获取