简介这份数据集专为中国汽车车牌识别任务设计面向目标检测、OCR及智能交通方向的初学者、算法工程师与研究者。压缩包共包含两千个XML标注文件整体大小约三百五十三点五六MB对应八千四百九十三张真实车辆图片的车牌位置信息XML遵循VOC格式每条记录附带图片尺寸、目标类别、车牌边界框坐标等结构化字段可直接接入YOLO、SSD、Faster R-CNN等主流检测框架省去手动标注环节。数据采集自白天、夜晚、强光、逆光、倾斜、模糊、遮挡等复杂环境标注框细致准确且支持多车牌同时标注配合官方给出的99.4%超高识别率基准可有效支撑车牌检测、字符识别、多场景鲁棒性验证等实验。目前已有六百六十二人学习下载适合用于智慧停车、电子警察、高速收费等场景的模型预研与精度优化。1. 车牌识别数据集8493张VOC标注图为什么值得拿来训练做车牌识别的人应该都有过这种经历模型在公开测试集上跑得好好的一部署到现场夜里逆光、雨天反光、远处小目标直接打回原形。这个车牌识别数据集就是冲着多种环境下能识别来的8493张图片全部带VOC格式的XML标注类别是完整的车牌区域检测标称识别率99.4%。对正在做安防道闸、停车场管理、车辆出入统计的开发者来说它解决的是有数据可训、有标注可验、有场景可泛化三个实际问题既可以直接拿来做目标检测训练也可以作为VOC转YOLO格式、验证标注质量、排查训练翻车的一套完整素材。下面这套流程我按先摸清数据结构再转换格式最后训练排查的顺序给你完整过一遍。2. 数据底层结构VOC标注与图像文件的对应关系2.1 图像侧8493张图覆盖哪些环境这个数据集的图像来源基本是真实抓拍场景不是实验室摆拍。从样本分布看白天顺光、傍晚逆光、夜间补光、雨天、雾天、高速运动模糊都有覆盖车牌类型也分蓝底白字、黄底黑字少量新能源绿牌。这种多样性对训练结果的影响非常大因为车牌检测的难点不在看清字符而在在复杂背景下先找到车牌区域逆光下金属反光、夜间车灯过曝、雨雪遮挡都会让特征提取变得不稳定。我拿到数据集后的习惯是先看图像尺寸分布而不是直接开训。这个数据集里的图不是统一分辨率长边从几百到上千像素都有而且很多车牌在画面里是小目标——可能只占几十个像素。这个特点决定了后续训练时imgsz参数不能设得太小第4章会专门讲。2.2 标注侧VOC格式的XML字段与坐标约定VOC格式的核心是每个XML文件对应一张图片标注信息全部写在XML里。以其中一张图为例结构是这样的annotation folderJPEGImages/folder filenameIMG_20210305_142637.jpg/filename size width1920/width height1080/height depth3/depth /size object nameplate/name pose0/pose truncated0/truncated difficult0/difficult bndbox xmin732/xmin ymin514/ymin xmax1086/xmax ymax602/ymax /bndbox /object /annotation解析这份XML时有三个关键点需要知道filename里的文件名要和JPEGImages目录下的实际图片完全一致包括扩展名大小写。这个数据集里出现过.JPG和.jpg混用的情况后面第4章会讲它带来的坑。size里记录的是原始图片的宽高bndbox的四个坐标是像素绝对值。转换到YOLO格式时归一化必须用这个size里的宽高不能用脚本里读到的、经过解码的图片尺寸去算二者一旦不一致归一化坐标就全错了。每个object是一个车牌实例一张图里可能出现多个车牌比如双向车道抓拍转换时要注意循环遍历不能只取第一个。2.3 训练前先做数据体检VOC格式数据和图片之间的对应关系、坐标是否越界、有没有空标注这些问题靠肉眼看不出来训练到一半报错才回头查就晚了。我在正式转换前会先跑一遍统计脚本把异常样本提前揪出来cd /path/to/dataset echo 图片数量: ls JPEGImages | wc -l echo XML数量: ls Annotations | wc -limport os import xml.etree.ElementTree as ET xml_dir Annotations image_dir JPEGImages problems [] for xml_name in os.listdir(xml_dir): xml_path os.path.join(xml_dir, xml_name) root ET.parse(xml_path).getroot() # 检查filename字段对应的图片是否存在 fname root.find(filename).text if not os.path.exists(os.path.join(image_dir, fname)): problems.append(f缺图: {xml_name} - {fname}) width int(root.find(size/width).text) height int(root.find(size/height).text) for obj in root.findall(object): box obj.find(bndbox) xmin, ymin float(box.find(xmin).text), float(box.find(ymin).text) xmax, ymax float(box.find(xmax).text), float(box.find(ymax).text) # 坐标越界、坐标倒置、宽高为0都是无效标注 if xmin 0 or ymin 0 or xmax width or ymax height: problems.append(f越界: {xml_name} [{xmin},{ymin},{xmax},{ymax}] vs {width}x{height}) if xmax xmin or ymax ymin: problems.append(f无效框: {xml_name}) if problems: print(f发现 {len(problems)} 个问题:) for p in problems[:30]: print(p) else: print(数据体检通过)这段脚本做三件事检查XML的filename是否有对应图片、检查标注坐标是否超出图片边界、检查坐标是否出现倒置或者零宽高。这三个问题在VOC格式数据里最常见其中越界问题在转换到YOLO后会导致训练loss异常增大因为归一化出来的中心点和宽高是负数或大于1。体检通过后再进训练流程可以少翻很多车。3. VOC转YOLO格式转换脚本与四个关键参数3.1 为什么要转YOLO格式YOLO系模型训练时读取的标注不是XML而是和图片同名的txt文件每行一个目标格式是类别ID 中心点x 中心点y 宽度w 高度h四个坐标全部归一化到0到1之间。目前主流的ultralytics框架就是按这个约定去images目录下找labels目录。所以不管原数据集是VOC还是COCO落到YOLO训练前都得先做一次格式转换。转换本身不难难的是四个参数别搞错归一化分母用哪个尺寸、坐标是0-based还是1-based、类别ID从0开始还是从1开始、多类别时类别映射表怎么定。下面这个脚本把四个点全部处理到位。3.2 完整的VOC转YOLO脚本import os import glob import xml.etree.ElementTree as ET BASE /path/to/dataset # 数据集根目录 JPEG_DIR os.path.join(BASE, JPEGImages) ANNO_DIR os.path.join(BASE, Annotations) LABEL_DIR os.path.join(BASE, labels) # YOLO标签输出目录 class_map {plate: 0} # 类别映射多类别时按序扩展 os.makedirs(LABEL_DIR, exist_okTrue) def voc_to_yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() # 关键参数1从XML的size字段读宽高作为归一化分母 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] # 关键参数2difficult1的样本通常是遮挡严重或极小目标默认跳过 if int(obj.find(difficult).text) 1: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 边界裁剪防止标注越界导致归一化出现负数 xmin max(0.0, xmin) ymin max(0.0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) w xmax - xmin h ymax - ymin if w 0 or h 0: continue # 关键参数3中心点和宽高全部归一化到[0,1] cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h nw w / img_w nh h / img_h # 关键参数4输出固定6位小数避免精度损失导致框偏移 lines.append(f{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) xml_files glob.glob(os.path.join(ANNO_DIR, *.xml)) for xml_path in xml_files: base_name os.path.splitext(os.path.basename(xml_path))[0] out_txt os.path.join(LABEL_DIR, base_name .txt) voc_to_yolo(xml_path, out_txt) print(f转换完成共处理 {len(xml_files)} 个XML文件)这段脚本建议原样跑一遍跑完后再根据实际情况改三个地方class_map如果后续你把标注扩展成plate_front、plate_back或者字符级识别在这里加映射即可转换脚本不用改。difficult处理有些数据集把难样本标成difficult1我默认跳过是出于训练稳定性考虑。如果你想挑战难样本把continue改成pass逻辑让这些样本也进训练集。输出小数位.6f在1920x1080的图上误差不到0.002像素完全够用。不要用.2f否则框的边界会明显偏移。3.3 训练集与验证集划分按场景防泄漏划分数据集是另一个容易翻车的点。如果只按文件随机切分同一个摄像头同一辆车在相邻几帧的图片很可能一部分进了训练集、一部分进了验证集。验证集看起来精度很高实际上模型见过这些画面了部署到新场景立刻露馅。我一般按文件名前缀分组把同一时段、同一视角的连续帧分到同一个集合里import os import random import shutil random.seed(2025) # 把所有图片路径收集起来按文件名前缀分桶 image_files os.listdir(JPEG_DIR) buckets {} for f in image_files: prefix f.split(_)[0] # 按文件名前缀作为场景ID buckets.setdefault(prefix, []).append(f) groups list(buckets.values()) random.shuffle(groups) val_ratio 0.1 val_count max(1, int(len(groups) * val_ratio)) val_groups set(groups[:val_count]) # 按ultralytics标准目录结构输出 output_root /path/to/training_data for split in [train, val]: os.makedirs(os.path.join(output_root, images, split), exist_okTrue) os.makedirs(os.path.join(output_root, labels, split), exist_okTrue) for group in groups: split val if group in val_groups else train for img in group: base os.path.splitext(img)[0] src_img os.path.join(JPEG_DIR, img) dst_img os.path.join(output_root, images, split, img) src_lbl os.path.join(LABEL_DIR, base .txt) dst_lbl os.path.join(output_root, labels, split, base .txt) shutil.copy(src_img, dst_img) if os.path.exists(src_lbl): shutil.copy(src_lbl, dst_lbl)这里val_ratio我通常取0.1到0.158493张图的话验证集大约在850到1270张够统计出稳定的mAP了。如果某类场景的图特别多可以按组内图片总量加权后再切避免某个视角独占验证集。划分完别急着训练数一下labels/val目录下txt文件数如果比images/val少说明有图片没有对应标注要去查是不是XML缺失。3.4 生成dataset.yamlultralytics的训练入口需要一个YAML文件来告诉框架数据和类别信息。上面我们已经把数据整理成了标准目录结构YAML就非常简单path: /path/to/training_data train: images/train val: images/val names: 0: platepath建议写绝对路径因为训练时的工作目录可能和数据集不在同一层相对路径容易找不到。names的序号必须和转换脚本里的class_map一致比如class_map里plate是0这里0: plate就对应上了。如果改动了class_map两处要同步改。4. YOLOv8训练与常见问题排查四条高频坑4.1 训练命令与核心参数数据准备好后用ultralytics训练是最省事的路线pip install ultralytics yolo train modelyolov8n.pt dataplate.yaml epochs100 imgsz640 batch16 device0参数含义和我的建议如下参数值说明modelyolov8n.ptn是轻量版显存不够或想快速验证流程就用它追求精度换yolov8s.pt或yolov8m.ptimgsz640训练输入尺寸。这个数据集里小目标多显存够的话建议提到768epochs100早停开启后一般50轮左右就收敛了设100防止欠拟合batch16根据显存调16GB显存跑yolov8n可以到32device0指定GPU编号CPU训练把0去掉但会慢非常多训练产物在runs/detect/train目录下best.pt是按验证集mAP选出的最优权重last.pt是最后一轮权重。平时测试和部署都用best.pt这点不要搞混。4.2 坑一转换后的框整体偏移框不住车牌现象训练前可视化检查发现预测框或者标签框整体偏左上或偏右下有的框明显比车牌小一圈。原因大多数情况下是归一化分母用错了。比如XML里标注尺寸是1920x1080但加载图片做可视化时用了OpenCV的默认解码如果图片本身带有EXIF旋转信息读出来的实际数组尺寸可能是1080x1920坐标就全错位了。解决转换脚本里强制从XML的size字段读宽高不能从图像数组shape去推断。另外转换后务必写一个可视化脚本把txt坐标画回图上抽查30张。能画出贴合车牌的框再进训练这一步能省掉后面所有loss不收敛的排查时间。4.3 坑二验证集mAP很高但换场景识别率骤降现象训练时val mAP达到0.95以上模型一部署到陌生停车场识别率掉到80%以下。原因数据集划分时按单张图片随机抽样同一个摄像头连续抓拍的几十帧被拆到训练集和验证集里造成严重的数据泄漏。模型在验证集上见过了几乎相同的画面mAP虚高。解决回到第3.3节按文件名前缀分桶划分确保同一场景的连续帧全部落在同一个集合。部署前再用手机拍几张陌生场景的车牌图做盲测比任何指标都靠谱。4.4 坑三近处车牌识别正常远处小目标漏检现象测试视频里近距离车牌框得很准10米外的车牌直接漏掉逆光时更明显。原因这个数据集里的车牌大部分是中远景但训练时imgsz640会把整图压缩小车牌缩成十几个像素特征已经没了。还有一个常见误解是模型输入越大越慢实际对检测小目标来说加大输入尺寸是性价比最高的手段。解决先把imgsz提到768显存吃紧就减少batch。同时在data.yaml里给训练增强配置开启mosaic和scale增强让模型见到更多尺度变化。我在这类车牌数据集上的经验是imgsz从640提到768对mAP50的提升通常有2到4个点副作用是推理时间增加约15%在可接受范围内。4.5 坑四训练时大量图片报错label not found现象训练日志里频繁出现WARNING: image found but label missing一查发现很多图片被跳过了。原因转换脚本生成的txt文件名是取XML文件名而XML里filename字段的图片可能叫IMG_001.JPG实际目录里的文件叫IMG_001.jpg在Windows上不区分大小写看不出问题在Linux服务器上训练直接找不到对应文件。解决转换脚本里不要用XML的filename字段作为输出txt的命名依据而是用XML本身的文件名去扩展名作为主键。也就是Annotations/xxx.xml对应labels/xxx.txt再去JPEGImages里找xxx.jpg或xxx.png。配合第2.3节的体检脚本可以在转换阶段就把这类缺失文件暴露出来。5. 验证模型真实识别率单图推理与置信度阈值调优训练完拿到best.pt先别急着接入业务我习惯跑一遍单图推理把预测框直接画出来看from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) # 对测试集里的图片批量推理保存可视化结果 results model.predict( source/path/to/test/images, conf0.35, saveTrue, save_txtTrue, imgsz768 ) # 打印第一张图的检测结果确认框坐标和置信度 for r in results[:1]: print(框坐标:, r.boxes.xyxy) print(置信度:, r.boxes.conf) print(类别ID:, r.boxes.cls)conf是置信度阈值训练框架默认0.25但实际场景里0.25会带来不少误检。我一般先在验证集上跑几个档位的对比0.25、0.35、0.5分别看精确率和召回率的此消彼长。车牌识别场景对误检容忍度很低——系统把广告牌上的文字当成车牌会直接影响后续的字符识别和扣费逻辑所以我通常会选0.4到0.5之间的值宁可漏检一些模糊目标也不让错误框进入下游。验证泛化还有个狠招拿自己手机到地下车库、露天停车场、高速入口各拍十几张直接丢给模型跑。这些图和数据集分布有差异出来的效果才是模型真实实力的反映。我第一次跑这个数据集训出的yolov8n自己拍的夜间远距离车牌漏了一半调大imgsz、把conf降到0.3后好了很多这个折腾过程比任何指标都直观。从那以后我每次拿到带标注的数据集都会先花十分钟做一遍统计和可视化确认标注没有越界、划分没有泄漏再交给训练脚本。这个习惯帮我省下了很多训练一晚结果全是玄学的翻车时间。这份数据集本身质量不错按上面的流程走下来复现一个可用的车牌检测器不费劲希望帮到你。本文还有配套的精品资源点击获取