首页 / 资讯中心 / 文章详情

车牌识别数据集实战:从数据体检到YOLO与CRNN训练避坑指南

车牌识别数据集实战:从数据体检到YOLO与CRNN训练避坑指南 ★ FEATURED ARTICLE
简介这份训练数据集面向计算机视觉开发者与深度学习学习者聚焦车牌检测与识别任务可用于交通监控、智能停车、自动驾驶等场景的算法开发与模型优化。包内共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(xminxmax)/2/W、w(xmax-xmin)/Wy 方向同理。下面以 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(parentsTrue, exist_okTrue) 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 \ dataplate_det.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ cls1.2 \ mosaic1.0 \ mixup0.0 \ projectruns_plate \ namedet_v1cls1.2是提高分类损失权重缓解类别不均衡mixup0.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_thr0.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。从那以后我每次拿到新的车牌数据都强制先跑一遍体检脚本和错例归档再决定是补数据还是调模型——数据的问题用模型补往往是白费力气。希望这套流程能帮你在自己的项目里少走几段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站