☰
车牌检测实战:1695张YOLO标注数据集从训练到部署全攻略
2026/10/5 3:56:48 网站建设 项目流程

简介:这份车牌图像数据集收录了1695张不同环境下的车辆图片,均附精确边界框标注,以YOLO文本格式存储,含类别标签与坐标,可直接用于车牌检测、识别等对象检测任务的模型训练与验证。适用对象包括计算机视觉初学者、算法工程师以及智能交通、停车管理、安防监控等领域技术人员。

资源包共2000个文件,1696个txt为YOLO标注文件,303个jpg为原始图像,1个yaml为配置文件,整体约147.47MB,目录结构清晰。已有132人学习,适合在真实场景图像上开展目标检测实验或验证新算法。

图像来源多样,涵盖不同光照、角度与车型,可提升模型泛化能力;配合规范标注,使用者能快速接入YOLO训练流程,节省数据清洗与格式转换时间,为车牌定位、字符识别等环节提供扎实训练基础。

1. 车牌图像数据集实战:1695张YOLO标注图能做什么

车牌检测是对象检测里一个很有意思的细分场景,目标小、长宽比固定、拍摄角度千奇百怪,跟COCO那种大目标检测完全是两种手感。这个车牌图像数据集一共1695张图片,全部带YOLO格式的边界框标注,拿来做YOLOv5、YOLOv8甚至YOLO11的训练测试都够用。对刚入门YOLO的开发者来说,它最大的价值不是数据量,而是标注格式干净、拿来就能跑通全流程,不用自己折腾标注工具。对已经有模型基础的人来说,它适合做迁移学习的起点——先在车牌这种强结构目标上把模型训收敛,再往其他检测任务上移植。

我拆这个数据集的时候,重点看了标注坐标的归一化精度、类别分布和图像来源这几个维度,下面把数据格式解析、训练前准备、参数调优和踩坑记录一条条讲清楚。不管你是第一次接触YOLO格式,还是已经在跑自定义数据集,这篇都能让你少走几趟弯路。

2. 数据格式拆解:YOLO标注到底在描述什么

2.1 YOLO文本标注的五列数字

拿到数据集后,每张jpg图片对应一个同名txt文件,这就是YOLO格式的标准组织方式。我一般会先随手打开一个标注文件看看内容,常见格式是下面这样:

0 0.651042 0.495370 0.104167 0.055556

这行五个数字分别代表:

  • 第1列:类别ID。当前数据集里车牌是唯一的检测目标,所以全部是0。如果未来自己扩展类别(比如加上车脸、驾驶员),这里的数字就会从0递增。
  • 第2列:边界框中心点的x坐标(归一化值)
  • 第3列:边界框中心点的y坐标(归一化值)
  • 第4列:边界框宽度(归一化值)
  • 第5列:边界框高度(归一化值)

归一化意味着所有数值都在0到1之间,计算方式是用像素坐标除以图像的宽和高。例如某张图是1920x1080,车牌在像素坐标系下的中心点是(1250, 535),宽200,高60,那么对应的YOLO坐标就是:

x_center = 1250 / 1920 # 0.651 y_center = 535 / 1080 # 0.495 width = 200 / 1920 # 0.104 height = 60 / 1080 # 0.056

使用归一化坐标的好处是模型训练时不需要关心输入图像的绝对尺寸,不管原图是720p还是4K,标注格式完全一致。这个数据集里的标注精度我抽查过十几个文件,小数点后保留6位,定位偏差基本在一个像素以内,属于可以直接喂给模型的质量。

2.2 用Python快速核对标注与图像的匹配度

拿到数据集以后,第一件事不是急着训练,而是确认标注有没有错位。我习惯写一段脚本,把标注框直接画回图上,肉眼检查十张八张,马上就能发现坐标归一化是否出错、类别ID是否混乱。

import cv2 import numpy as np # 读取图像和对应标注文件 image_path = "video11_3350.jpg" label_path = "video11_3350.txt" img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() class_id = int(parts[0]) # 反归一化:把相对坐标转回像素坐标 x_center = float(parts[1]) * w y_center = float(parts[2]) * h box_w = float(parts[3]) * w box_h = float(parts[4]) * h x1 = int(x_center - box_w / 2) y1 = int(y_center - box_h / 2) x2 = int(x_center + box_w / 2) y2 = int(y_center + box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(class_id), (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite("check_annotation.jpg", img) print("标注检查图已保存,请确认边界框是否贴合车牌边缘")

这段脚本做的事情很直接:读取标注坐标并反算回像素坐标,然后画出矩形框覆盖在原始图像上。参数方面,cv2.rectangle的thickness设为2,在1080p图上足够醒目;如果要从多张图里抽检,建议用一个子文件夹放20到30张图批量生成检查结果,比一张一张翻效率高得多。

提示:检查时重点看两个地方——框是不是紧贴车牌边缘,以及同一张图里有没有出现明显错位的框。如果框偏移超过10个像素,说明标注存在系统性偏差,训练前需要整体修正。

2.3 数据集的图像来源与多样性分析

翻文件名能看出不少信息。这个数据集里既有video前缀的连续视频帧抽取图,也有nissan-terrano-official-review这种带车型标识的静态图。视频帧来自不同的监控或行车记录仪场景,意味着同一辆车会在连续帧里反复出现。这对训练来说是把双刃剑:优点是模型能看到同一车牌在不同光照、角度下的形态变化,缺点是如果把连续帧同时分进训练集和验证集,会造成数据泄漏,评估指标虚高。

建议对视频帧做时间维度的采样分析,确认两张相邻帧之间相隔了多少个frame index。以video11_3350.jpg和video11_2180.jpg为例,中间隔了上千帧,出现在同一数据划分里的概率较低;但video9_360.jpg到video9_390.jpg这种只隔了30帧的连续图,就很容�易被同时抽进训练集和验证集。后面避坑章节我会给出处理方案。

3. 训练前准备:数据划分与YOLOv8训练文件配置

3.1 按比例划分训练集、验证集与测试集

拿到YOLO格式数据集后,第一步永远是数据划分。1695张图不算多,我建议按8:1:1的比例拆分,也就是约1356张训练、170张验证、169张测试。关键在于划分时引入随机种子,保证多次运行结果可复现。

import os import random import shutil random.seed(42) # 固定随机种子,确保每次划分结果一致 image_files = [f for f in os.listdir("images") if f.endswith(".jpg")] random.shuffle(image_files) train_ratio, val_ratio = 0.8, 0.1 train_count = int(len(image_files) * train_ratio) val_count = int(len(image_files) * val_ratio) train_files = image_files[:train_count] val_files = image_files[train_count:train_count + val_count] test_files = image_files[train_count + val_count:] for split, files in [("train", train_files), ("val", val_files), ("test", test_files)]: os.makedirs(f"dataset/{split}/images", exist_ok=True) os.makedirs(f"dataset/{split}/labels", exist_ok=True) for fname in files: # 复制图片,同时复制同名txt标注文件 shutil.copy(f"images/{fname}", f"dataset/{split}/images/{fname}") label_name = fname.replace(".jpg", ".txt") shutil.copy(f"labels/{label_name}", f"dataset/{split}/labels/{label_name}") print(f"训练集: {len(train_files)} 验证集: {len(val_files)} 测试集: {len(test_files)}")

这里有几个参数需要说明。random.seed(42)不限定必须用42,任何整数都可以,但一旦选定就不要改,否则后续实验对比就失去意义。8:1:1是数据量在2000张以下时的常见做法;如果图片总量上万,7:2:1甚至9:0.5:0.5也可以,验证集只要能稳定反映模型表现即可。

划分后的目录结构是YOLOv8的标准输入格式:

dataset/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/

3.2 编写data.yaml配置文件

YOLOv8训练时通过YAML文件指定数据路径、类别数量和类别名称。写这个文件的时候有两点容易翻车:路径必须使用绝对路径或相对于运行目录的相对路径,类别名称必须和标注文件里的类别ID对应上。

# dataset.yaml path: /home/user/plate_dataset # 数据集的上级目录,改成你自己的绝对路径 train: train/images # 训练集图片路径,相对于path val: val/images # 验证集图片路径 test: test/images # 测试集图片路径,可选 nc: 1 # 类别数量,当前只有车牌一个类别 names: ["plate"] # 类别名称列表,索引0对应标注文件中的0

配置里最容易被忽略的是path字段。如果训练时提示找不到图片,八成就是这里写成了相对路径,而YOLOv8对相对路径的解析基准是当前进程的工作目录,不是YAML文件所在目录。建议直接填绝对路径,一劳永逸。

nc: 1和names: ["plate"]要特别注意顺序关系。names列表的长度必须等于nc的值,且列表索引就是标注文件中的类别ID。手工标注数据时如果忘了把类别改成0,训练时就会报AssertionError: class id > num_classes之类的错误。

3.3 用YOLOv8从头训练车牌检测模型

数据划分完成、YAML配置就绪,接下来就是训练命令。我用的是ultralytics包,如果还没装,先执行pip install ultralytics。训练命令如下:

yolo detect train \ data=/home/user/plate_dataset/dataset.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ workers=4 \ device=0 \ project=plate_yolo \ name=exp_plate_base \ seed=42

这个命令里每个参数都有讲究。model=yolov8n.pt是加载COCO预训练权重做迁移学习,n代表nano版本,模型最小、显存占用最低。如果你的显卡显存低于8G,nano版本是起步选择;显存在12G以上可以换yolov8s.pt,检测精度会更高,但训练时间也相应拉长。

imgsz=640是训练时把输入图像缩放到640x640。车牌属于小目标,拉伸到640后细节损失在可控范围内;强行上1280分辨率会显著增加显存压力和训练时间,对1695张这种量级的数据集来说提升有限。batch=16代表每轮迭代用16张图,显存不足时优先降batch而不是降imgsz。

workers=4是数据加载的进程数,Windows下建议设为0避免多进程报错,Linux下4或8都正常。device=0表示用第一块GPU训练,没有GPU就改成device=cpu,但训练速度会慢一个数量级,1695张图可能要跑几个小时。

4. 训练调参与评估:把mAP提上去的关键细节

4.1 学习率、批次大小与损失函数的关系

YOLOv8默认使用SGD优化器,初始学习率lr0设为0.01,最终学习率lrf设为0.01。这个参数组合在大多数检测任务上表现稳定,但车牌检测有个特殊性:车牌在图像中占比普遍很小,边界框回归的损失占比相对低,容易出现分类分支已经收敛、但定位分支还在震荡的情况。

常见的调整思路是把lr0从0.01降到0.005,让模型在早期就谨慎更新权重,避免定位分支被分类分支带偏。我在类似数据集上测试过,调低学习率后mAP50大概能提升1到2个百分点,代价是训练收敛稍微慢一些。

损失函数方面,YOLOv8默认的组合是分类损失(BCE)+ 定位损失(CIoU)+ 置信度损失。对车牌这类方形目标,CIoU本身已经考虑了长宽比一致性,不需要额外修改。除非你发现预测框的高度系统性偏大或偏小,才考虑替换成SIoU或EIoU——这是后话。

4.2 看懂验证结果中的mAP50与mAP50-95

训练结束后,YOLOv8会在验证集上输出一组评估指标,我最常看的是这两个:

指标含义参考范围
mAP50IoU阈值设为0.5时的平均精度,衡量框的重合程度是否达标车牌检测0.95以上算优秀
mAP50-95IoU阈值从0.5到0.95按步长0.05计算的平均精度,更加严格0.7以上算可用

mAP50-95比mAP50苛刻得多,因为它要求边界框在极高重合度下依然准确。如果你的目标只是做违停检测这类不需要像素级定位的场景,mAP50跑到0.9就够用;但如果是停车场出入口的自动计费,需要精确知道车牌的四角位置,那mAP50-95至少要0.75。

训练日志里还会输出precision和recall两个单值指标。precision是预测出的所有框中真正检测到车牌的比例,recall是所有真实车牌中被成功检出的比例。车牌检测场景里漏检的代价远高于误检,所以我的习惯是看recall为主,precision只要不低于0.85就能接收。

4.3 数据量不足时的Trick:数据增强参数调整

1695张图片对深度学习模型来说算中等偏少,容易过拟合。YOLOv8自带数据增强参数,默认的增强策略已经考虑了翻转、缩放、色彩抖动,但针对车牌检测可以做两个针对性调整。

首先是hsv_h、hsv_s、hsv_v这三个颜色增强参数。车牌的颜色在不同光线下的变化非常大——白天是蓝底白字,夜间在路灯下会偏黄,在监控补光灯下会反白。把hsv_v的默认0.4调高到0.6,hsv_s默认0.7调高到0.9,让模型见过更多亮度饱和度的组合,能有效提升不同光照场景下的泛化能力。

其次是degrees旋转参数。监控摄像头拍摄的车牌通常水平,但停车场入口的相机偶尔会有5到10度的安装偏差。把degrees设为10,每次训练随机旋转最多10度,能让模型对安装角度不那么敏感,代价是训练时间增加约10%。

yolo detect train \ data=/home/user/plate_dataset/dataset.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ hsv_h=0.02 \ hsv_s=0.9 \ hsv_v=0.6 \ degrees=10 \ project=plate_yolo \ name=exp_plate_aug

注意:增强参数不是越多越好。旋转角度超过15度后,车牌本身的字形特征会被破坏,模型反而学不到稳定的模式。手感上,10度是个安全值。

5. 避坑指南:车牌数据集训练最常见的五个问题

5.1 标注坐标越界或非数值,训练直接崩

现象:训练进行到中途,loss突然变成NaN,之后所有指标全部失效。

原因:标注txt文件里有非法值,比如坐标大于1(说明归一化时使用了错误的图像宽高),或者存在空行、负值、非数字字符。

解决:训练前用脚本批量校验所有标注文件,发现异常直接删除对应图片和标注,宁缺毋滥。

import os label_dir = "labels" invalid_count = 0 for fname in os.listdir(label_dir): path = os.path.join(label_dir, fname) with open(path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"无效行数: {path}, 内容: {line.strip()}") invalid_count += 1 break try: vals = [float(v) for v in parts] except ValueError: print(f"非数值: {path}, 内容: {line.strip()}") invalid_count += 1 break if not (0 <= vals[1] <= 1 and 0 <= vals[2] <= 1): print(f"坐标越界: {path}, 内容: {line.strip()}") invalid_count += 1 break print(f"发现 {invalid_count} 个异常文件")

这段脚本逐行读取每个标注文件,检查三件事:是否正好五列、是否全部能转成float、中心点坐标是否在0到1之间。宽度和高度没有做上限校验是因为YOLO允许框超出图像边缘一部分,但实践中超过1.2就说明标注出错了,可以按需加强校验。

5.2 验证集指标虚高,实际部署翻车

现象:训练时mAP50在验证集上表现优异,达到0.97,但部署到真实场景后漏检严重。

原因:视频连续帧同时出现在训练集和验证集里。模型在训练时见过几乎相同的帧,验证时等于开卷考试,指标自然好看,真实场景没见过的新角度立刻露馅。

解决:按视频来源划分数据,而不是按单张图片划分。同一个video前缀下的所有帧要么全进训练集,要么全进验证集,绝不能混在一起随机切分。

import os from collections import defaultdict # 按文件名前缀分组 groups = defaultdict(list) for fname in os.listdir("images"): prefix = fname.split("_")[0] groups[prefix].append(fname) # 输出每个组的样本数,便于手动决定哪些组进验证集 for prefix, files in sorted(groups.items()): print(f"{prefix}: {len(files)}张,示例: {files[:3]}")

执行这段脚本后,你会看到video11、video9等组各有多少张图。如果某组包含300张,这300张必须全部放在同一划分里。这个数据集里不同video前缀代表了不同路段的拍摄,正好可以用这种方式避免数据泄漏。

5.3 小目标检测不到,imgsz设置成罪魁祸首

现象:训练收敛后测试单张图片,远处的车牌完全检测不到,近距离车牌正常。

原因:imgsz=640时,远处的车牌在原图中可能只有30x10像素,缩放到640后变成10x3像素,特征几乎丢失。

解决:把推理时的imgsz提高到960或1280,同时训练时对应提高。YOLOv8允许训练和推理用不同的imgsz,但差异过大会导致精度下降。

yolo detect predict \ model=runs/detect/exp_plate_aug/weights/best.pt \ source=test/images/ \ imgsz=1280 \ conf=0.25 \ device=0

如果GPU显存不支持1280分辨率推理,可以用YOLOv8自带的yolov8n配合slice inference功能,自动把大图切块再拼接结果。缺点是耗时翻倍,但对监控场景的离线分析完全够用。

5.4 白天效果好、夜间效果差,光照增强缺位

现象:模型在晴天白天场景表现优秀,一到夜间或逆光场景,召回率下降30%以上。

原因:数据集中白天图像占多数,模型学到了对亮度敏感的特征。夜间车牌在补光灯下会产生反光和高光溢出,与白天图像分布差异巨大。

解决:常用的做法是在训练时对图像做灰度化和亮度抖动强化。YOLOv8里可以用hsv_v参数控制亮度变化幅度,同时别忘了一个容易被忽视的预处理——把输入图像做自适应直方图均衡化再喂给模型。

import cv2 img = cv2.imread("night_plate.jpg") # CLAHE增强对比度,参数clipLimit控制对比度上限,tileGridSize控制分块大小 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) lab[:, :, 0] = clahe.apply(lab[:, :, 0]) enhanced = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) cv2.imwrite("night_plate_enhanced.jpg", enhanced)

这段代码把图像转到LAB色彩空间,只对亮度通道做CLAHE增强,避免颜色失真。clipLimit=2.0是个常用值,数值越大对比度增强越明显,但会引入噪点;对夜间监控画面,我一般从1.5开始尝试,最高不超过3.0。

5.5 类别不平衡导致模型偏向某一类车牌

现象:数据集中蓝色车牌占80%,黄色和白色车牌各占10%,训练后模型对蓝牌检测很准,黄牌经常漏检。

原因:深度学习模型天然偏向样本量大的类别,这是所有检测任务共有的问题,不限于车牌。

解决:两个方案,一是复制少样本类别的图片做离线增强,二是用YOLOv8的mosaic增强把不同类别的样本合成到一张图里。Mosaic默认开启,但mosaic概率是0.5,可以提高到0.8让模型在训练早期见到更多类别组合。

yolo detect train \ data=/home/user/plate_dataset/dataset.yaml \ model=yolov8n.pt \ epochs=100 \ batch=16 \ imgsz=640 \ mosaic=0.8 \ project=plate_yolo \ name=exp_plate_mosaic

提示:mosaic在epoch后期建议调低,因为合成图与原图分布差异太大会干扰最后的收敛精度。YOLOv8目前没有直接支持按epoch动态调整mosaic参数的配置项,实践中可以训练80轮后再把mosaic关掉续训20轮。

6. 部署与泛化:从验证集走向真实场景的最后一公里

训练完模型、验证集指标好看,离真正能用还差两步:模型导出和场景适配。

模型导出。YOLOv8支持导出成ONNX和TensorRT格式。ONNX是中间格式,适合跨平台部署和调试;TensorRT是NVIDIA GPU上的加速引擎,能显著提升推理速度。如果你的部署环境是英伟达显卡,直接导出TensorRT:

yolo export \ model=runs/detect/exp_plate_aug/weights/best.pt \ format=engine \ imgsz=640 \ device=0

导出后的engine文件可以直接用于下一步推理。注意导出时的imgsz必须与推理时一致,否则TensorRT会重新编译引擎,首次加载会特别慢。

场景适配方面,车牌检测最大的坑是训练数据里看不到的极端角度。这个数据集里的图像多数是正面或小角度倾斜拍摄,但实际停车场的相机安装角度五花八门,有些甚至是从上往下俯拍。我在实际部署中遇到过最典型的翻车是:模型在测试集上mAP50有0.96,一到停车场出口,因为车牌在画面中只有3度左右的小角度俯视,立刻漏检。

后来我养成了一个习惯,拿到任何车牌数据集,先检查有没有俯拍样本,没有的话用透视变换做离线增强补足这一环:

import cv2 import numpy as np img = cv2.imread("front_plate.jpg") h, w = img.shape[:2] # 模拟俯拍视角:将矩形车牌映射到梯形区域 src_pts = np.float32([[w*0.2, h*0.3], [w*0.8, h*0.3], [w*0.85, h*0.7], [w*0.15, h*0.7]]) dst_pts = np.float32([[w*0.3, h*0.3], [w*0.7, h*0.3], [w*0.75, h*0.7], [w*0.25, h*0.7]]) matrix = cv2.getPerspectiveTransform(src_pts, dst_pts) warped = cv2.warpPerspective(img, matrix, (w, h)) cv2.imwrite("front_plate_topdown.jpg", warped)

getPerspectiveTransform的四个点分别对应变换前后的四边形顶点,warpPerspective完成映射。通过调整src_pts和dst_pts的坐标,可以生成多种俯拍角度的合成样本,把这些图混入训练集后再训练一轮,模型对相机安装角度的适应能力会有质的提升。

从那以后,我每次拿到新的车牌数据集,都强制走一遍固定流程:先校验标注合法性,再按视频来源划分数据,然后检查类别比例和拍摄角度多样性,最后才进入训练环节。这套流程帮我在多个项目上避免了数据泄漏和场景不适配的问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询