简介:这份训练数据集面向计算机视觉开发者与深度学习学习者,聚焦车牌检测与识别任务,可用于交通监控、智能停车、自动驾驶等场景的算法开发与模型优化。包内共1296个文件,以650张jpg车牌图片和646个xml标注文件为主,图片与标注一一对应,覆盖蓝牌、黄牌、新能源绿牌及少量白牌、黑牌,兼顾不同颜色与字体差异带来的识别难点;压缩包约228.08MB,已整理打包,省去自行采集与标注的成本。目前已有1862人学习下载。读者可直接用于YOLO、SSD等检测模型与CRNN、LSTM等识别模型的训练,并按训练集、验证集、测试集划分开展实验,结合数据增强与正则化手段调优,快速搭建从预处理到字符识别的完整流程。
1. 车牌检测识别数据集:从“能跑通”到“敢上线”之间差了什么
做车牌识别项目,最容易被低估的环节不是模型结构,而是数据。很多人拿到一份车牌检测识别训练数据集,第一反应是直接丢进 YOLO 跑一遍,mAP 看着还行就以为成了,结果一上车载视频或者路口抓拍就翻车——蓝牌白天识别率 98%,一到新能源绿牌、夜间逆光、大角度倾斜就掉到六成以下。这份数据集的价值不在于“有图有标注”,而在于它把车牌检测和车牌字符识别两个任务的数据组织在一起,覆盖蓝牌、黄牌、新能源绿牌等常见类型,适合用来做检测框回归加字符序列识别的联合验证。它适合两类人:一是刚接手车牌识别 Demo、需要一份能直接训练的数据起步的开发者;二是已经有模型、想补绿牌和复杂光照样本做增量迭代的熟手。下面按“数据长什么样、怎么转格式、怎么训、坑在哪”的顺序拆开讲。
2. 先看清数据组织:检测框与字符标注是两套逻辑
2.1 检测任务和识别任务对标注的要求完全不同
车牌检测识别训练数据集这类资源,通常同时包含两类标注信息:一类是车牌在整图中的位置框,用于训练检测器;另一类是车牌区域内的字符内容,用于训练识别器。这两套标注的坐标系和粒度不一样,混在一起处理是新手最常见的翻车点。检测标注关心的是x_min, y_min, x_max, y_max这样的绝对像素坐标或归一化坐标,识别标注关心的是裁剪后的车牌小图上“京A12345”这样的字符序列,以及字符级或序列级的对齐信息。
我一般会先把数据集按任务拆成两个视图:检测视图保留原图和框,识别视图保留裁剪图和文本标签。不要试图用一套标注文件同时喂两个模型,除非你用的是端到端的多任务网络,那也需要在数据加载层做字段映射。判断一份数据集是否“干净”,先看它的目录结构是否把images、labels_det、labels_rec分开,再看标注文件里有没有混入空框、越界框、字符数与图片不匹配的记录。常见做法是写一个统计脚本,先跑一遍分布,再决定怎么划分训练集和验证集。
2.2 用脚本做一次数据体检,别急着开训
拿到数据后第一件事不是配置训练参数,而是体检。下面这段脚本做三件事:统计图片数量与标注数量是否对齐、检查框是否越界、统计车牌类型分布(蓝牌、黄牌、绿牌)。假设检测标注是 YOLO 格式的 txt,每行class cx cy w h为归一化值。
import os from collections import Counter from pathlib import Path IMG_DIR = Path("dataset/images") LBL_DIR = Path("dataset/labels_det") CLS_NAME = {0: "blue", 1: "yellow", 2: "green"} # 按数据集实际类别顺序改 img_files = {p.stem for p in IMG_DIR.glob("*.jpg")} lbl_files = {p.stem for p in LBL_DIR.glob("*.txt")} print("图片数:", len(img_files), "标注数:", len(lbl_files)) print("有图无标:", len(img_files - lbl_files)) print("有标无图:", len(lbl_files - img_files)) cls_counter = Counter() bad_boxes = 0 for txt in LBL_DIR.glob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: bad_boxes += 1 continue c, cx, cy, w, h = int(parts[0]), *map(float, parts[1:]) cls_counter[CLS_NAME.get(c, c)] += 1 # 归一化框必须在 (0,1) 内,且宽高不能为 0 if not (0 < cx < 1 and 0 < cy < 1 and 0 < w <= 1 and 0 < h <= 1): bad_boxes += 1 print("类别分布:", dict(cls_counter)) print("异常框数量:", bad_boxes)这段脚本的逻辑很直白:集合差集找图文不匹配,逐行解析找格式错误和越界框,Counter 统计类别。参数上唯一要改的是CLS_NAME的映射,必须和数据集data.yaml里的names顺序一致,否则统计出来的“绿牌数量”其实是别的类。跑完如果发现绿牌样本占比低于 10%,而你的实际场景绿牌占比很高,那就别指望直接训出来的模型在绿牌上表现好,得先做重采样或者针对性增广。这一步花十分钟,能省掉后面几小时的无效训练。
3. 转成 YOLO 与 CRNN 可用的格式:两条流水线各走各的
3.1 检测标注转 YOLO:坐标归一化和类别映射
如果数据集原始标注是 VOC 的 XML 或者 COCO 的 JSON,需要转成 YOLO 的 txt。转换的核心是绝对坐标转归一化中心点坐标,公式是cx=(xmin+xmax)/2/W、w=(xmax-xmin)/W,y 方向同理。下面以 VOC XML 为例,转换时顺手做类别名到索引的映射。
import xml.etree.ElementTree as ET from pathlib import Path W, H = 1920, 1080 # 若每张图尺寸不同,需用 PIL 逐图读取真实宽高 CLS_MAP = {"blue": 0, "yellow": 1, "green": 2} def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLS_MAP: 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) cx = (xmin + xmax) / 2 / W cy = (ymin + ymax) / 2 / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f"{CLS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out = Path(out_dir) / (Path(xml_path).stem + ".txt") out.write_text("\n".join(lines)) for xml in Path("dataset/annotations").glob("*.xml"): voc_to_yolo(xml, "dataset/labels_det")这里有个血泪经验:W, H千万别写死成固定值。很多数据集里图片尺寸不统一,写死会导致框整体偏移,训练 loss 看着在降但检测框就是不准。正确做法是用PIL.Image.open(img_path).size逐图取宽高。另外CLS_MAP里没出现的类别直接跳过还是报错,取决于你的业务——如果数据集里有“双层黄牌”“使馆牌”这类长尾类,建议先保留并单独统计,别一上来就丢,丢了后面想补还得重新对齐。
3.2 识别标注整理成 CRNN 的“图片路径 + 文本”清单
字符识别这条线,常见做法是 CRNN 或 CTC 系网络,输入是裁剪后的车牌小图,标签是字符序列。需要生成一个清单文件,每行图片相对路径\t文本标签。如果数据集给的是整图加框加文本,得先按框裁剪出小图再生成清单。
import cv2 from pathlib import Path def crop_and_build_list(img_dir, lbl_dir, out_img_dir, list_path): Path(out_img_dir).mkdir(parents=True, exist_ok=True) records = [] for txt in Path(lbl_dir).glob("*.txt"): img_path = Path(img_dir) / (txt.stem + ".jpg") img = cv2.imread(str(img_path)) if img is None: continue h, w = img.shape[:2] for i, line in enumerate(txt.read_text().strip().splitlines()): parts = line.split() if len(parts) < 6: # 假设格式: cls cx cy w h text continue cx, cy, bw, bh = map(float, parts[1:5]) x1 = int((cx - bw / 2) * w); y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w); y2 = int((cy + bh / 2) * h) crop = img[max(0, y1):min(h, y2), max(0, x1):min(w, x2)] if crop.size == 0: continue out_name = f"{txt.stem}_{i}.jpg" cv2.imwrite(str(Path(out_img_dir) / out_name), crop) records.append(f"{out_name}\t{parts[5]}") Path(list_path).write_text("\n".join(records)) crop_and_build_list("dataset/images", "dataset/labels_rec", "dataset/rec_images", "dataset/rec_list.txt")裁剪时max(0, ...)和min(...)的边界保护不能省,标注框偶尔会超出图片边缘,不裁剪直接切片会得到空数组,写文件时静默失败,最后清单里少了一堆样本还查不出原因。文本标签里如果含空格或特殊分隔符,清单的分隔符要换成制表符并确保标签本身不含制表符。生成完清单后,统计一下字符集大小和每个字符的出现频次,字符集里出现频次个位数的生僻字符,要么合并要么剔除,否则 CTC 训练时这些类基本学不动。
4. 训练配置与验证:检测和识别分开调,别一锅炖
4.1 检测侧的关键参数与绿牌召回
检测模型用 YOLO 系是性价比最高的选择。配置文件里真正影响车牌场景的几个参数:imgsz建议不低于 640,车牌在整图中占比小,分辨率太低框会糊;cls损失权重可以适当调高,因为类别不均衡(蓝牌多、绿牌少);数据增广里mosaic对车牌有帮助,但mixup要慎用,两张图叠在一起车牌字符会互相污染,识别侧会学到脏标签。下面是一个训练命令的示例,参数按实际路径改。
yolo detect train \ data=plate_det.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16 \ cls=1.2 \ mosaic=1.0 \ mixup=0.0 \ project=runs_plate \ name=det_v1cls=1.2是提高分类损失权重,缓解类别不均衡;mixup=0.0是主动关掉,理由上面说了。训练完别只看总体 mAP,要按类别看召回率,尤其是绿牌那一类的recall。如果绿牌召回明显低于蓝牌,两个方向:一是对绿牌样本做过采样,在data.yaml里用重复路径或者写个采样权重文件;二是针对绿牌做颜色增广,调整 HSV 的色调范围,让模型对绿色区域的敏感度上来。验证时用混淆矩阵看蓝牌和绿牌有没有互相误判,这两种牌在低分辨率下颜色特征接近,误判很常见。
4.2 识别侧的字符集与序列长度
识别模型训练前,先把字符集固定下来。中文车牌字符集包括省份汉字、字母、数字和少量特殊字符,新能源绿牌是 8 位,比蓝牌多一位。序列长度max_len要按最长车牌设,设短了长车牌直接被截断,设长了 CTC 解码时空白符占比高、收敛慢。常见做法是统计清单里标签的最大长度,向上取整再加 1 作为max_len。训练时batch可以比检测侧小,因为裁剪图尺寸小;学习率用 1e-3 起步,CTC loss 前期下降慢是正常的,别急着调大学习率,容易震荡。
验证识别效果不能只看整体准确率,要拆成“整牌完全正确率”和“单字符准确率”两个指标。整牌完全正确率才是业务真正关心的,单字符准确率高但整牌正确率低,说明错误集中在某几个易混字符上,比如0和O、8和B。把验证集里识别错的样本导出来看,按错误字符对做统计,比盲目加数据有效得多。
5. 避坑与排查:这几处翻车我见过太多次
5.1 现象:训练 loss 正常下降,验证集检测框整体偏移
原因:坐标转换时图片宽高写死,或者训练时imgsz与标注归一化基准不一致。解决:转换脚本里逐图读取真实尺寸,训练配置的imgsz只影响缩放,不影响归一化基准,但数据加载时的 letterbox 填充要确认没把框算错。排查方法是取一张验证图,把预测框和标注框画在同一张图上目视对比,偏移是系统性的还是随机的,一眼能看出来。
5.2 现象:绿牌识别率远低于蓝牌,混淆矩阵里绿牌被大量判成蓝牌
原因:绿牌样本占比低,且绿牌底色在低光照下偏暗,颜色特征和蓝牌接近。解决:先统计绿牌占比,低于 15% 就做过采样;再做 HSV 色调增广,把绿色区间的样本做亮度扰动;最后检查标注里绿牌类别索引有没有和蓝牌写反,这种低级错误在手工整理的数据集里并不少见。
5.3 现象:识别模型训练时 loss 不降或者降到某个值卡住
原因:字符集里混入了标签中不存在的字符,或者清单文件里有空标签、标签含分隔符。解决:训练前跑一遍字符集与标签的交叉校验,列出“字符集有但标签无”和“标签有但字符集无”的字符,前者删掉,后者补进字符集并重新生成索引。空标签行直接过滤,别指望模型学会输出空序列。
5.4 现象:检测框裁出来的车牌小图边缘被切掉,字符识别错位
原因:检测框本身贴得紧,裁剪时没有留边距,字符笔画被切。解决:裁剪时按框宽高的 5% 到 10% 向外扩一圈,再裁。扩边后如果超出图片边界,用边缘像素复制填充,别用黑边,黑边会让识别模型学到“黑边等于车牌边界”的伪特征。
5.5 现象:验证集指标很好,实际视频流上帧间结果跳变严重
原因:单帧检测没有做时序平滑,视频里相邻帧的框位置和识别结果独立输出。解决:在推理侧加简单的跟踪或投票,连续 N 帧里同一位置的车牌识别结果取众数,检测框做滑动平均。这不是数据集的问题,但拿这份数据训出来的模型直接上视频流,不做后处理一定会遇到。
6. 增量迭代的一个实用技巧:用错例反哺数据集
数据集不是训完就扔的一次性资源。我习惯在验证阶段把识别错的样本按“检测漏检”“框偏移”“字符错认”三类归档,每类攒够一定数量后,回到数据集里找同类场景补样本。具体做法是给验证脚本加一个错例导出逻辑:预测框与标注框 IoU 低于阈值记为漏检或偏移,识别文本与标签编辑距离大于 0 记为字符错认,导出时保留原图、标注和预测结果三份信息。
def iou(box_a, box_b): # box: [x1, y1, x2, y2] xa = max(box_a[0], box_b[0]); ya = max(box_a[1], box_b[1]) xb = min(box_a[2], box_b[2]); yb = min(box_a[3], box_b[3]) inter = max(0, xb - xa) * max(0, yb - ya) area_a = (box_a[2]-box_a[0]) * (box_a[3]-box_a[1]) area_b = (box_b[2]-box_b[0]) * (box_b[3]-box_b[1]) return inter / (area_a + area_b - inter + 1e-6) def collect_bad_cases(preds, gts, iou_thr=0.5): bad = [] for p, g in zip(preds, gts): if iou(p["box"], g["box"]) < iou_thr: bad.append(("det", p, g)) elif p["text"] != g["text"]: bad.append(("rec", p, g)) return bad这段逻辑的关键在阈值iou_thr,车牌场景建议设 0.5 而不是通用的 0.45,因为车牌框本身窄长,IoU 对轻微偏移很敏感,阈值太低会把正常框也判成错例,导出量爆炸反而没法看。导出的错例按场景打标签(夜间、逆光、倾斜、遮挡),攒到几十条就做一轮增量训练,学习率调小到原来的十分之一,只训几个 epoch。从那以后我每次拿到新的车牌数据,都强制先跑一遍体检脚本和错例归档,再决定是补数据还是调模型——数据的问题用模型补,往往是白费力气。希望这套流程能帮你在自己的项目里少走几段弯路。
本文还有配套的精品资源,点击获取