中国车牌识别数据集1458张VOC标注,车牌检测与OCR实战指南
简介面向目标检测与OCR车牌识别开发者的中文车牌识别数据集适用于算法入门与工程落地提供1458张已标注车牌图像。图像标注采用VOC格式每张原图对应XML标注文件包含车牌位置框及数字和字母类别信息可直接接入YOLO、Faster R-CNN等常见检测框架用于车牌定位、字符识别模型的训练和验证。压缩包为zip格式整体大小19.49MB解压后即可获得规范标注的数据便于直接搭建训练流程。数据覆盖多种车牌类型、不同拍摄角度与光照条件可帮助算法工程师、研究生完成车牌检测及字符识别相关的项目实践、算法迭代与效果对比并比较不同网络结构下的识别性能。目前已有683人学习下载适合需要真实场景数据开展模型测试、课程设计或毕业设计的开发者使用。1. 中国车辆车牌号识别数据集1458张VOC标记图能撑起车牌检测与OCR两条任务线做车牌识别项目时最花时间的往往不是模型选型而是标注数据。这份中国车辆车牌号识别数据集把车牌号和数字字母的标记直接做到了VOC格式1458张标记图片解压即用不用自己拉网找标注工具重新标一遍。它适合两类人一是想快速跑通车辆或车牌检测流程的初学者二是有自己的摄像头数据、需要先在一个干净数据集上调通完整训练管线再迁移到业务场景的开发者。很多人拿到手先问“1458张够不够”。我的判断是做车牌整体检测它足够验证一条完整训练链路做数字和字母的字符级识别把标注框单独抽出来当字符切分的先验也顺手。别拿它和万级大规模数据集比要把它当成一个标得干净、格式统一、能直接进训练脚本的数据起点。下面从目录结构开始拆一步步把这份资源变成能运行的检测模型。2. 读懂VOC标注目录结构、XML坐标与标注字段的完整拆解2.1 拿到压缩包后先做三件事第一件事是解压并核对数量。VOC格式的常见组织方式是图片放在 JPEGImages标注放在 Annotations但也有人把图片和标注混在同一目录。先列目录再统计 jpg 和 xml 数量确认 1458 张标记图片这个说法是否成立。unzip 中国车辆车牌号识别数据集.zip -d car_plate_dataset cd car_plate_dataset find . -type f -name *.jpg | wc -l find . -type f -name *.xml | wc -l逻辑说明unzip -d 指定解压目标目录find 结合 -name 只匹配指定扩展名wc -l 统计行数也就是文件数。这一步能快速暴露两个问题图片和标注数量不相等说明有图片漏标或标注多余数量对不上时就别急着训练先回头补数据。参数说明-d 后面的目录名可以按自己习惯改。如果压缩包是分卷 zip需要先合并再解压这里不展开。第二件事是随机抽几个 XML 用文本编辑器打开先看 bndbox 落在图像的什么位置再确认框里包的是整块车牌还是单个字符。第三件事是统计 name 字段都有哪些值。这一步直接决定了你后面走的是“整体车牌检测”还是“字符级识别”路线。对这份资源来说name 如果直接是车牌号字符串说明每个框同时包含了“哪里是车牌”和“这车牌是什么”两层信息非常值钱。一个常见的理解误区是“VOC格式就是XML”。其实VOC标注的价值在于它保留了目标类别、位置和难易标记三层结构很多标注工具比如 labelImg 导出的就是这种格式。既然这份数据集支持VOC格式标记就意味着你可以直接拿 labelImg 打开它继续补充标注不需要重新做一个标注工具。2.2 一个典型XML标注的字段逐行拆解以下是这份资源里最常见的单目标XMLannotation foldercar_plate/folder filenameplate_0001.jpg/filename size width1920/width height1080/height depth3/depth /size segmented0/segmented object name京A12345/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin864/xmin ymin412/ymin xmax1064/xmax ymax472/ymax /bndbox /object /annotation逐个字段说明folder打包者写入的目录名不影响训练但转换脚本如果硬编码了路径要留意。filename图片文件名。转换脚本必须以这个字段为准去拼接图片路径而不是用列表下标去猜。size/width 和 size/height真实图像尺寸也是后面归一化坐标的分母。如果原图被压缩过而 xml 没更新坐标会整体偏掉。object/name类别名。如果 name 直接就是车牌号字符串说明这是一个既带检测框又带字符语义的标注。bndbox框的左上角 xmin、ymin 和右下角 xmax、ymax单位是像素。坐标可以超出图像范围标注工具手滑时就会这样清洗阶段要处理。difficult表示该目标是否难以辨认值为 1 时很多框架默认不参与训练。如果原始标注里大量 difficult1那可能是模糊样本被正确标记了这时不要一味删除反而可以用作难例挖掘。这份资源的一个细节在于“可识别车牌数字和字母”。如果 name 是完整车牌串那么框内字符就是车牌号本身检测下游可以直接接字符分类器如果 name 统一是 plate那就只适合做整体检测。2.3 用OpenCV把标注框画回图上做可视化体检拿到任何数据集我都有个固定动作把标注框画回原图保存成一张拼图。这个过程几行代码就能完成但能避免后面训练一路翻车。import cv2 import xml.etree.ElementTree as ET import os xml_dir car_plate_dataset/Annotations img_dir car_plate_dataset/JPEGImages for idx, xml_file in enumerate(os.listdir(xml_dir)[:16]): xml_path os.path.join(xml_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text img cv2.imread(os.path.join(img_dir, img_name)) if img is None: print(fimage missing: {img_name}) continue for obj in root.findall(object): name obj.find(name).text.strip() box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(0, ymin - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite(fcheck_{idx:02d}.jpg, img) print(check done)逻辑说明遍历 Annotations 下前 16 个 XML用 filename 找图像并读进来对每一个 object 的 bndbox 画矩形再把类别名画在框上方。这里直接使用变量名 name 表示类别文本如果 XML 里的 name 是车牌号字符串打印到图上后一眼就能核对“框是否框住整个车牌、字符标签是否正确”。参数说明切片 [:16] 只抽查 16 张实测建议抽查到 50 张以上rectangle 线宽在 1920 宽的大图上用 2在小图上用 1putText 的位置要防止 ymin 小于字体高度导致文字画出图像外所以我加了 max(0, ymin-10)。如果不做这个保护图像顶部的框会让 putText 直接坐标越界虽然 OpenCV 不崩溃但会丢失文字。可视化体检时重点看三类样本框和车牌边缘的贴合度如果框边离车牌字符太近说明标注把字符裁了一部分后续 OCR 准确率会受影响。同一张图多个框的情况如果有两个 bndbox 离得很近且重叠可能是同一块车牌的重复标注训练时会产生歧义。车牌类型分布蓝牌、绿牌、黄牌是否有足够覆盖这一步直接影响泛化能力。这一章的结论是别急着训练先把标注看明白。目录结构、XML 字段和可视化三关都过一遍数据集真实质量你就心里有数了。3. 把VOC转成YOLO训练集转换脚本、参数说明与四个边界坑3.1 为什么要转格式VOC与YOLO标注的空间差异VOC 给的是像素坐标系下的左上角与右下角YOLO 系列要求的是归一化后的中心点坐标与宽高。换算关系固定x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h这四行是把 XML 变成可训练标签的关键。归一化的好处是同一份标注可以在不同输入分辨率下复用模型读入图片时会按 data.yaml 配置的 imgsz 做缩放如果保留像素坐标缩放后框位就全错位了。所以转换脚本不只是格式翻译它同时承担坐标校准和类别映射两层职责。下面给一个可直接落地的版本。3.2 转换脚本从XML目录到txt标签目录import os import xml.etree.ElementTree as ET IMG_DIR car_plate_dataset/JPEGImages XML_DIR car_plate_dataset/Annotations OUT_DIR car_plate_dataset/labels os.makedirs(OUT_DIR, exist_okTrue) def xml_to_yolo_line(obj, img_w, img_h, class_id): box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) # 坐标裁剪到图像范围内 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h return f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f} for xml_file in os.listdir(XML_DIR): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(XML_DIR, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_name xml_file.replace(.xml, .txt) lines [] for obj in root.findall(object): lines.append(xml_to_yolo_line(obj, img_w, img_h, 0)) with open(os.path.join(OUT_DIR, txt_name), w) as f: f.write(\n.join(lines) \n) print(fdone, {len(os.listdir(XML_DIR))} xml files)逻辑说明先确保输出目录存在再对每个 XML 解析宽高xml_to_yolo_line 函数负责把单个 object 转成一行 txt。函数内部先做一次坐标裁剪把越界坐标拉回图像范围内然后按公式归一化并保留 6 位小数。这里 class_id 固定传 0是“整体车牌检测”的常用做法。参数说明如果你不是做整体检测而是做字符级识别class_id 就不能固定 0。需要先收集所有 name 并建立映射表把每个字符或每个车牌串映射成递增数字。用 ultralytics YOLOv8 训练时txt 第一列必须是整数 id不能直接写中文字符串这点要先想清楚再动手。我常用的是先收集全部 name排序后生成 classes.txt再回写进 txtnames [] for xml_file in os.listdir(XML_DIR): root ET.parse(os.path.join(XML_DIR, xml_file)).getroot() for obj in root.findall(object): names.append(obj.find(name).text.strip()) unique_names sorted(set(names)) name2id {n: i for i, n in enumerate(unique_names)} with open(car_plate_dataset/classes.txt, w) as f: f.write(\n.join(unique_names)) print(name2id)逻辑说明set 去重后必须排序保证类别顺序稳定不能依赖集合的随机顺序否则每次运行类别 id 都不一样训练与推理就会错位。classes.txt 就是后续 data.yaml 里 names 的蓝本。提示如果只做车牌检测更省事的办法是把所有 name 归并成 plate 一个类别转换时 class_id 写 0。归并前先统计一遍原始 name确认没有其他类别混杂。3.3 四个边界坑我在转这类车牌数据集时稳定会遇到四个坑。坑一filename 与文件名不一致。有的 XML 里 filename 写的是 plate_0001.jpg但图片实际叫 0001.jpg转换脚本按 IMG_DIR filename 拼接就会全部找不到图。解决转换时打印失败图片路径另一个做法是忽略 filename 字段改用 xml 文件名转成 jpg 名再去找图片但前提是先确认一一对应。坑二size 字段与真实图像宽高不一致。有些标注工具在图像被 resize 后没有更新 XML。解决转换前批量读取图片宽高和 XML 对照不一致就以图片实际宽高为准重新计算。坑三空 object。部分 XML 没有任何 object 节点转换出来是空 txt。空 txt 训练时会被当作背景图少量问题不大大面积出现就会让模型过度往背景方向优化。解决把空 xml 统计打印出来人工确认是漏标还是本来就是背景图。漏标就补标补不了就放进 val 里当负样本。坑四多类别但转换时写成固定 0。如果数据里包含不同车牌类型或不同字符串全部归 0 会丢失类别信息反过来如果没归并而某个 name 带了换行符它会变成一个独立类别模型输出类别数量瞬间膨胀。解决先统一任务再决定类别映射。只做检测就归并为 plate要做字符识别就按字符拆分并按映射表写 id。3.4 转换结果的批量校验转出来的 txt 必须先过一遍质量检查。一个实用脚本import os label_dir car_plate_dataset/labels for txt in os.listdir(label_dir): path os.path.join(label_dir, txt) for line in open(path): parts line.split() if len(parts) ! 5: print(fbad line in {txt}: {line}) break vals [float(p) for p in parts[1:]] if any(v 0 or v 1 for v in vals): print(fout of range in {txt}: {line}) break else: continue break print(validation finished)逻辑说明对每个 txt 逐行读取要求每行恰有 5 个字段即 class_id x_center y_center w h再检查归一化后的坐标是否都在 0 到 1 之间。任何越界值都说明坐标或宽高计算有问题要回到转换脚本排查。参数说明如果发现大量越界优先检查 img_w 和 img_h 是否在转换前被正确赋值。常见错误是图片是竖图XML 里却记录了横图的宽高归一化之后 y_center 直接超过 1。4. 跑通车牌检测训练数据划分、YOLOv8配置与训练命令4.1 数据划分决定你的mAP有没有水分划分不只在训练集与验证集之间而是 train/val/test 三层。1458 张实在不多我一般按 8:1:1 切折下来大约 1166 张训练、146 张验证、146 张测试。留独立测试集是因为车牌识别容易过拟合没有独立测试集看到的 mAP 会偏乐观。import os import random image_dir car_plate_dataset/JPEGImages images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.seed(666) random.shuffle(images) total len(images) val_n int(total * 0.1) test_n int(total * 0.1) val_files images[:val_n] test_files images[val_n:val_n test_n] train_files images[val_n test_n:] for split_name, split_files in [(train, train_files), (val, val_files), (test, test_files)]: with open(fcar_plate_dataset/{split_name}.txt, w) as f: for file_name in split_files: f.write(os.path.join(image_dir, file_name) \n)逻辑说明先把所有 jpg 读入并打乱再按比例截取三份。写出的 txt 每一行是完整图片路径YOLO 直接按行读取。没设随机种子的话每次运行顺序都不同前后两次实验无法对比所以 seed 必须固定。参数说明seed 写成 666 只是为了可复现你也可以用具体日期当种子。val_n 和 test_n 都是 10%如果觉得验证集太少改成 15% 也是普遍做法代价是训练集少几十张。更严谨的做法是按前缀分组。车牌图通常来自几段视频同一段视频的图片编号前缀一致。改成按前缀分组groups {} for f in images: prefix f.rsplit(_, 1)[0] # 按最后一个下划线切 groups.setdefault(prefix, []).append(f) group_keys list(groups.keys()) random.shuffle(group_keys)逻辑说明这是防止数据泄漏的关键。如果连续帧里的同一辆车同时出现在训练集和验证集验证分数完全失真部署到真实停车场视频时立刻现原形。4.2 data.yaml类别顺序是命门YOLOv8 需要一份 data.yaml。这里给出适配该数据集的配置path: /absolute/path/to/car_plate_dataset train: train.txt val: val.txt test: test.txt names: 0: plate如果转换脚本把 name 统一成了 0names 里只写 plate 一行。如果保留了原始车牌号字符串作为类别这里要按 classes.txt 里的顺序原样展开第 0 行对应第一类第 1 行对应第二类顺序错一位就是全盘错。注意data.yaml 不要用制表符缩进YOLO 的解析器只认空格。path 建议写成绝对路径避免不同机器上相对路径解析不一致。4.3 训练命令与参数详解yolo detect train \ datacar_plate_dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ device0 \ projectruns/detect \ nameplate_exp \ patience20逻辑说明model 用 yolov8n.pt 作为预训练起点而不是随机初始化patience20 表示验证集 20 轮没有提升就早停防止 1458 张小数据在后期发生过拟合。yolov8 训练默认会自动下载预训练权重网络慢时容易卡住建议提前手动下载 yolov8n.pt 放到工作目录命令里写本地文件名。参数说明imgsz输入分辨率车牌是中等尺寸目标640 是合理起点。提到 960 显存占用约涨 2.25 倍batch 要相应下调。batch16 对 Nano 级模型比较合理实测单张 6G 显存也能跑如果 OOM降到 8 或 4。epochs100 不是定律先按 100 跑日志里看到 val 指标还在涨就续跑。device多卡可以写 0,1小数据集单卡更稳。4.4 训练输出怎么读训练结束后的关键产出是 runs/detect/plate_exp/weights/best.pt 与 last.pt。results.png 里的字段需要逐个看字段含义我关注什么train/box_loss训练边框损失是否持续下降val/box_loss验证边框损失先降后升就是过拟合metrics/precision检出的框中真正含车牌的比例误检是否严重metrics/recall真实车牌中被召回的比例漏检是否严重metrics/mAP50IoU0.5 时的平均精度能否稳定在 0.85 以上metrics/mAP50-95多个 IoU 阈值的平均精度更严格虚高更少对 1458 张这种小数据集合理预期是 mAP50 不低于 0.85。如果明显低于这个值先回头检查转换脚本归一化是否出错再把 epochs 调到 150。loss 曲线前期波动大属于常见玄学现场主要看整体趋势不要被单轮反弹带偏。训练完用命令验证单图yolo detect predict \ modelruns/detect/plate_exp/weights/best.pt \ sourcecar_plate_dataset/JPEGImages/plate_0001.jpg \ conf0.25 \ saveTruesaveTrue 会把画了框的推断图保存到 runs/detect/predict 目录。逐张看预测图能发现哪些框是漏检、哪些是误检。低光照图把阴影误判成车牌是高频问题这时调高 conf或者在增强里加入亮度扰动。5. 避坑与排查车牌数据集实战里最常见的五个翻车现场5.1 训练顺利但预测框全空现象训练 loss 正常下降val mAP 也不差但 predict 一跑一张图都检测不出车牌。原因最常见是数据泄漏导致 val 虚高训练和验证里出现同一来源图片模型记住了而不是学到推理时面对真实现场就失效。另一个高频原因是 conf 阈值太高。解决先降低 conf 到 0.1 再预测如果依然全空说明模型确实没学到车牌特征回到数据集查类别 id 映射若只是 conf 不够调回 0.25 即可。更重要的是检查 train.txt 和 val.txt 里是否有相同文件名同一车牌出现在两个集合里时必须重新划分。5.2 XML里name带着换行导致类别数量爆炸现象打印 classes.txt 时出现很多带反斜杠的类别或者明明只有一个类别模型却预测出几十种类别名。原因name 字段首尾有换行或空格转换时没做 strip去重之后每种空白变体都成了一个类别。解决在转换脚本里统一处理name .join(name.split()) # 去掉所有空白字符这个处理必须在转换前做。转换后再处理就晚了txt 里的类别 id 已经按错误类别生成。5.3 bndbox坐标出界引发的nan现象训练日志出现 nan 损失或者 loss 直接变成 inf查看 labels 目录txt 里出现负数坐标有时 x_center 大于 1。原因XML 标注时框拖出了图像边界转换脚本没有做 clip归一化分母如果又取错负值更明显。解决转换时对坐标做 clip同时校验图片真实宽高。写一个异步检查脚本凡 XML 里 size 和图片实际尺寸不一致的一律列入异常清单人工修订后再进训练集。5.4 绿牌识别差到离谱现象蓝牌 mAP 很高绿牌基本漏检。中国车牌里有新能源绿牌如果数据集中绿牌占比很低模型天然会把所有车牌都按蓝牌特征来识别。原因小数据集上的类别不均衡。检测模型学到的是颜色与特征的强相关绿色样本太少颜色支路就把绿牌当背景。解决给绿牌样本做针对性增强。ultralytics 自带的 hsv_h、hsv_s 参数默认值分别为 0.015 和 0.7已经能缓解大部分颜色敏感问题。如果还不够用 albumentations 额外加 HueSaturationValuehue_shift_limit 设为 20。注意不要把整张图调成黑白那样会连反光特征一起丢掉。注意HSV 增强只能缓解颜色偏差不能替代补充真实绿牌样本。条件允许时尽量多收几种光照下的新能源车牌。5.5 同一辆车同时出现在训练集和验证集现象val mAP 接近 1.0但拿到停车场新拍画面完全崩盘。原因划分时按图片随机切同一辆车的连续帧被分到两个集合里。车还是那辆车光照角度只是微变模型等于在开卷考试。解决按拍摄片段或车牌唯一标识分组整组划入同一个集合。没有场景标识时按文件名前缀分组是可靠的近似做法。这也是 4.1 里为什么要把前缀分组单独拿出来写的原因。6. 进阶验证把检测框接到字符识别上顺带做数据增强6.1 用检测框驱动数字字母识别如果 name 直接是车牌号字符串这份资源的检测框能承担更重要的角色训练完车牌检测之后把预测框裁剪出来接一个轻量 OCR 模型实测能完成“检测车牌 → 识别数字和字母”的完整链路。这个做法最妙的地方是反向检验检测框质量OCR 识别准说明框贴得正OCR 识别乱说明框带着背景甚至只框住半个字符。一个可视化验证脚本骨架import cv2 from ultralytics import YOLO model YOLO(runs/detect/plate_exp/weights/best.pt) img cv2.imread(car_plate_dataset/JPEGImages/plate_0088.jpg) results model.predict(img, conf0.25) for r in results: for box in r.boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 [int(v) for v in box] crop img[y1:y2, x1:x2] cv2.imwrite(plate_crop.jpg, crop) # 后续把 crop 交给车牌OCR模型识别数字字母逻辑说明用训练好的 best.pt 预测单张图拿到框坐标后裁剪出车牌区域。后续接 OCR 时只要识别结果和 XML 中 name 对得上就说明检测框质量过关。参数说明conf0.25 是常见默认置信度如果框太小导致剪裁图模糊把 imgsz 提到 960 重新预测同一张图的框会更完整。6.2 数据增强与验证习惯数据增强方面我常用两个提高泛化的招数。一是随机透视变换模拟车辆从不同角度驶过摄像头画面二是把裁剪出的车牌随机错位拼回新背景增加背景多样性。两种操作都必须对标签同步做相同的变换否则车牌框会错位。从那以后我拿到任何新数据集都强制走一遍同样流程先可视化核对、再统一检测目标类别、再划分三个集合、最后才跑训练。顺序不乱项目就不会白跑。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →