ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

欧盟车牌数据集实战:VOC转YOLO与检测训练全流程解析

2026/9/23 15:18:07 拓冰建站 浏览量
欧盟车牌数据集实战:VOC转YOLO与检测训练全流程解析 简介面向计算机视觉目标检测开发的一套欧洲车牌图像数据集全部图像由VOTT标注工具手工标注并转换为VOC格式标注内容包含车牌区域边界框可直接用于有监督学习。无论是入门车牌检测算法还是验证夜间、日间光照下的识别效果这套数据都能提供贴近实际道路场景的训练样本。压缩包共1070个文件主体为534张JPG原始图像与534个对应的XML标签文件另有1个说明文本和1个数据清单总大小约428.21MB。图像覆盖白天、夜晚等多种行车环境目录结构清晰XML文件与图像一一对应每个文件记录车牌类别与定位坐标可无缝接入YOLO、Faster R-CNN等主流检测框架。目前该资源已有185人学习浏览可省去自行采集和标注的繁琐流程适合快速启动车牌识别相关实验或项目。1. 五百多张欧盟车牌图值不值得下先说结论再拆内容拿到这份「534张欧盟车牌图像数据集」的时候我第一反应是看标签格式因为做过检测的人都知道数据集的质量七成由标注决定图片本身反而是次要的。这份数据是带VOC格式XML标签的用VOTT工具标注图片来源是行车记录仪视频里抽取的帧白天和夜间场景都有车牌覆盖多个欧盟国家。先说结论如果你是做车牌检测或者OCR方向手里缺一个带标准标签的起步集这东西能用如果你指望它覆盖所有欧盟国家的车牌样式那不够534张图只够做模型验证和流程打通距离生产级还差一个数量级。我把整个包拆开看了从文件构成到标签结构再到怎么转成自己训练要的格式完整走了一遍下面把过程和坑都写出来。2. 拆包看数据VOC标签结构、文件组织与图像分布2.1 文件组织看似零散实际是按采集场景分组的整个压缩包解压之后结构并不复杂。所有图片和对应的XML标签文件都在同一个目录下没有按train/val/test划分子文件夹这意味着你需要自己做数据划分。文件名带着场景信息比如nightride_type3_001.mp4#t352.jpg这种命名方式其实就是从某个mp4视频的第352秒抽出来的一帧。文件名里的关键信息有两段第一段是视频来源和类型nightride_type3表示夜间行驶、第三种拍摄机位或车辆类型dayride_type1对应白天场景第二段是时间戳#t352就是ffmpeg抽帧时用的时间点。这个命名习惯对后续扩展数据集很有用——你可以用同样的方式从自己的行车视频里补数据时间戳直接对应原视频位置回头要查原始帧很方便。我数了一下实际内容图片和XML是严格一一对应的没有出现有图无标签或者有标签无图的情况。这点值得肯定很多网上流传的数据集经常缺这少那一份能直接对齐的标签文件能省掉不少清洗时间。2.2 VOC标签到底长什么样逐字段拆解XMLVOC格式的核心是每个图片对应一个同名XML文件里面记录图片路径、尺寸和所有目标的边界框。这份数据集的XML结构遵循Pascal VOC标准打开任意一个文件关键字段如下annotation folderEU_License_Plate/folder filenamedayride_type1_001.mp4#t1199.jpg/filename pathE:/dataset/dayride_type1_001.mp4#t1199.jpg/path source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namelicense_plate/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin1245/xmin ymin512/ymin xmax1430/xmax ymax582/ymax /bndbox /object /annotation这份XML告诉我们的信息比表面看起来多。size里标注的是1920x1080说明原始帧是1080p的但注意这个尺寸不一定是所有图片统一——夜间视频和白天视频可能来自不同设备动手前最好批量检查一遍尺寸避免出现标注坐标和实际图像尺寸不匹配的问题。object里的name统一是license_plate只有一个类别没有区分国家、没有区分颜色、也没有区分车牌类型。如果你打算做多分类任务比如识别德国车牌和法国车牌的样式差异这份数据的标签不够用需要重新标注或者额外补充。边界框坐标是整数像素值xmin1245, ymin512, xmax1430, ymax582框高只有70像素。这个数值直接暴露了一个问题图片是1080p的但车牌在画面里占比不大尤其是夜间场景车牌的像素区域可能更小。这对检测模型来说是个挑战后面训练时会专门讲到怎么处理。2.3 标签质量抽查VOTT标注的常见特征这份数据集是用VOTTVisual Object Tagging Tool标注的VOTT标记的数据有几个典型特征在抽查时都能对上。一是标签框严格贴合车牌边缘因为VOTT里手动画框时会有自动吸附的辅助线所以框的边界通常不会像程序化标注那样留大量空白。二是夜间图片的框会略微偏大因为夜间车牌周围的光晕会影响人眼判断边缘这个属于正常现象。我随机抽查了20张图片的XML没有发现坐标越界比如xmin为负数或者xmax超过1920、没有发现标签名拼写错误、也没有发现空对象节点。整体标注质量在中上水平作为训练集是够用的。3. 格式转换实战把VOC转成YOLO和TFRecord含脚本与参数说明3.1 为什么必须转换检测框架的数据格式差异VOC格式是一张图配一个XMLHuman可读性好但训练框架普遍不直接吃这个。YOLO系列用的是txt格式每行一个目标内容是class_id x_center y_center width height坐标全部归一化到0到1之间TensorFlow的Object Detection API要TFRecord这是一种二进制序列化格式MMDetection则可以直接吃VOC但需要额外的数据集配置文件。我一般会先写一个通用转换脚本把VOC转成YOLO格式因为这个转换最常用而且一旦转成YOLO格式再转COCO或者TFRecord就方便了。转换的核心公式很简单归一化后的中心点坐标等于边界框中心坐标除以图片宽高归一化后的宽高等于边界框宽高除以图片宽高。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names, img_width, img_height): tree ET.parse(xml_file) root tree.getroot() yolo_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue # 跳过不在类别列表里的目标 class_id class_names.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化计算 x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height # 边界检查防止归一化后超出[0,1]范围 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) width min(max(width, 0.0), 1.0) height min(max(height, 0.0), 1.0) yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return yolo_lines # 使用示例 class_names [license_plate] xml_file dayride_type1_001.mp4#t1199.jpg.xml img_width 1920 img_height 1080 lines voc_to_yolo(xml_file, class_names, img_width, img_height) for line in lines: print(line)这段代码的逻辑并不复杂但有几个参数值得注意。class_names的顺序就是YOLO训练时的类别编号顺序全数据集必须保持一致如果你只有一个类别那class_id永远为0。img_width和img_height必须和图片实际尺寸严格一致这个数据集的宽高都是从XML里的size字段读的所以脚本里其实不应该硬编码常规做法是直接解析XML的size节点而不是当作参数传入。归一化后的坐标保留6位小数足够用了因为图片最大也就几千像素6位小数对应的精度远高于像素级精度。边界检查那两行是血泪经验——VOTT偶尔会画出略微超出图片边界的框不处理会让YOLO训练直接报错或者产生NaN损失。3.2 批量转换与数据集划分按场景分组避免数据泄漏单张转换脚本跑通之后要考虑批量处理。这个数据集没有现成的train/val/test划分需要自己做。划分时有个容易忽略的点同一视频抽出来的帧应该分到同一个集合里不能随机打散。原因很简单车牌检测任务的难点之一就是相似场景下的过拟合。如果夜间视频的帧一部分在训练集、一部分在验证集验证集的图片和训练集图片来自同一段视频、同一辆车、同一个光线条件那验证结果会虚高模型看似效果不错实际换到新视频上立刻现原形。import os import random from collections import defaultdict def split_dataset(image_dir, xml_dir, output_dir, val_ratio0.15, test_ratio0.15): # 按视频来源分组 video_groups defaultdict(list) for filename in os.listdir(image_dir): if not filename.endswith(.jpg): continue video_id filename.split(#)[0] # 如 dayride_type1_001.mp4 video_groups[video_id].append(filename) train_files, val_files, test_files [], [], [] for video_id, files in video_groups.items(): random.shuffle(files) n_val int(len(files) * val_ratio) n_test int(len(files) * test_ratio) val_files.extend(files[:n_val]) test_files.extend(files[n_val:n_val n_test]) train_files.extend(files[n_val n_test:]) # 写入划分文件 for split_name, file_list in [(train, train_files), (val, val_files), (test, test_files)]: with open(os.path.join(output_dir, f{split_name}.txt), w) as f: for filename in file_list: f.write(filename \n) return len(train_files), len(val_files), len(test_files) n_train, n_val, n_test split_dataset(images/, xmls/, split/) print(f训练集: {n_train}, 验证集: {n_val}, 测试集: {n_test})这个划分策略的关键是filename.split(#)[0]取的视频ID。原始文件名如nightride_type3_001.mp4#t352.jpg按#分割后取前半段nightride_type3_001.mp4就是这段帧的来源视频ID。同一个视频的帧只会进同一个集合不会跨集合存在。划分比例我一般用15%验证、15%测试、70%训练。534张图的话训练集大约370张验证和测试各80张左右。这个规模做检测训练偏小但作为迁移学习的微调数据或者模型验证足够了。如果你的场景是跑通整个训练流程、验证数据标注质量这个规模完全够用。4. 从标注到训练用这份数据跑YOLOv5车牌检测4.1 为什么不直接训练YOLOv8先看数据量再选模型拿到手的数据集只有534张图直接上YOLOv8从零训练效果大概率不理想。检测模型从零收敛单类别目标通常需要至少几千张图500多张只够做微调。常规做法分为三步第一步用这份数据做迁移学习加载在COCO上预训练好的权重只微调最后的检测头第二步做了数据增强——尤其是针对小目标的增强策略因为前面分析过车牌在1080p画面里占比很小第三步如果效果还不够再用这份数据训练出的模型去跑视频抽帧做伪标签补充更多训练数据。如果你用的是YOLOv5训练流程相对成熟直接写一个数据集配置文件就行。# eu_plate.yaml train: split/train.txt val: split/val.txt test: split/test.txt # 类别数 nc: 1 # 类别名称 names: [license_plate]这个YAML的关键点是train和val指向的是txt文件路径txt里每一行是图片的绝对路径。YOLOv5的数据加载器会根据图片路径自动找同名的txt标签文件——图片叫xxx.jpg标签就叫xxx.txt放在同一级目录下这点和很多第一次用YOLO的人直觉不同容易踩坑。4.2 训练参数设置针对小目标的四个关键修改训练命令本身不复杂复杂的是参数怎么改。针对车牌这种小目标、少数据量的任务我一般会调整几个关键参数。python train.py \ --data eu_plate.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 16 \ --epochs 150 \ --patience 20 \ --hyp data/hyps/hyp.scratch-low.yaml--img参数从默认的640改到1280这几乎是针对小目标最有效的改动。原图1920x1080车牌可能是100x30像素缩到640后车牌就剩约33x10像素检测头很难抓住这么小的目标缩到1280后车牌能保持66x20像素特征明显多了。代价是显存占用翻倍、训练速度变慢但数据集小这个代价可以接受。--hyp换成hyp.scratch-low.yaml而不是默认的hyp.scratch.yaml原因是默认的高增强策略会对小目标做大量随机裁剪和缩放本来就小的车牌经过增强后可能只剩几个像素反而干扰学习。低增强策略更适合小数据集。训练后看log里的P、R、mAP0.5三个指标。如果mAP0.5能到0.9以上说明标注质量没问题、模型也学到了如果卡在0.6-0.7上不去先检查标签是否有误再看夜间场景单独评估的指标。5. 避坑指南VOTT数据集实战中的六个常见问题5.1 XML路径字段指向本机旧路径迁移机器后训练报文件找不到现象换了一台电脑解压数据集训练时日志报错找不到图片文件但图片明明就在对应目录下。原因VOTT生成XML时path字段记录的是标注当时的绝对路径比如E:/dataset/dayride_type1_001.mp4#t1199.jpg。这个路径是标注机器的路径和当前机器的实际路径不一致YOLO的数据加载器如果读path字段就会直接失败。而filename字段只有文件名不和具体目录绑定。解决转换脚本里不要用path字段统一用filename然后拼接当前机器的数据目录。或者批量替换XML里的路径前缀——用文本编辑器全局替换也行但脚本更稳妥。我习惯在转换到YOLO格式时直接忽略path只用filename和自定义的image_dir参数。5.2 文件名包含#和符号部分工具解析异常现象把图片路径写入train.txt后用某些脚本读取时报错路径被截断或者识别成特殊字符。原因文件名如nightride_type3_001.mp4#t352.jpg包含了#和这两个符号。在shell脚本里#是注释符在某些语言的标准库里URL解析会截断#后面的部分个别Windows老旧的批处理工具也会在处出问题。解决训练时用Python脚本读取文件列表不要用shell的for循环遍历目录。如果非要用bash记得对文件名做引号包裹。另一个思路是转换格式时顺便把文件重命名成nightride_type3_001_352.jpg这种去符号的形式但注意VOTT的VOC标签里filename字段也要同步改名否则图片和标签对不上。5.3 夜间样本的标注框偏大导致模型定位不准现象用这份数据训练出的模型在白天测试集上mAP不错但在夜间图片上车牌框总是比实际车牌大一圈尤其是用车牌外接框做字符裁切时会把车灯和车身反光裁进去。原因VOTT标注时夜间场景的车牌边缘不清晰标注者倾向于把光晕区域也框进去。标注框比真实车牌大10%到20%模型学到的就不是车牌边界而是车牌加光晕边界。解决训练前做标签清洗统计所有标注框的面积分布把明显偏大的框手动修正。更省力的方案是训练后处理时对预测框做收缩比如宽度方向乘以0.9、高度方向乘以0.85暴力但有效。极端情况下对夜间图片重新用VOTT标注一遍会更彻底但534张全部重标工作量不小。5.4 数据集没有划分直接所有图训练导致模型效果虚高现象把534张图全部丢进去训练训练集上mAP接近1.0但跑到一段新视频上检测率大幅下滑漏检率很高。原因这是数据集划分的问题。所有图片都参与训练模型把这些图背下来了并没有泛化到新车牌、新背景、新光照条件的能力。尤其是这份数据集只有白天和夜间两个场景驾驶路线相对固定背场景比背车牌更容易——模型可能记住了这段路的护栏和树木而不是车牌本身。解决必须划分训练、验证、测试集而且按视频来源分组划分。另外要正视数据量问题534张图训练出的模型泛化边界很窄只适合证明流程可行离实际部署还远。5.5 多国车牌混在一起训练单类别标签掩盖了样式差异现象模型能检测到车牌位置但无法区分不同国家的车牌格式——比如德国蓝条白字和法国黄底黑字检测出来后要按国家分流时无从下手。原因VOC标签里所有目标都叫license_plate只有一个类别。欧盟车牌虽然整体是长方形带蓝条但具体到每个国家颜色、字体、长宽比都有差异这些问题被单类别标签掩盖了。解决如果你后续要按国家分类需要对这534张图重新标注把license_plate细分成germany、france、netherlands等子类。这需要在VOTT里逐张重新打标签工作量大约4到6个小时。如果只是做检测定位单类别就够用了。5.6 部分帧含视频字幕或水印干扰训练现象训练过程中loss曲线正常但mAP偏低仔细排查发现部分图片右下角有视频水印或速度表叠加信息模型有时候会把水印识别成车牌候选区域。原因从行车记录仪视频抽帧时如果视频本身带OSD信息速度、GPS坐标、时间抽出来的帧就会包含这些固定区域的UI元素。这些元素和车牌一样也是矩形、也是高对比度检测网络容易混淆。解决清洗数据时重点检查四个角和水印位置把带UI叠加的帧剔除或者裁掉对应区域。更稳妥的做法是抽帧时直接用ffmpeg的crop滤镜把UI区域裁掉一劳永逸。这块没有捷径纯属体力活。6. 数据增强与验证技巧让534张图发挥更多价值6.1 针对车牌的专属增强配置数据量不足的情况下增强策略直接决定模型上限。针对车牌这种纹理型目标我常用一套组合拳。HSV色域增强里把饱和度范围设到±25%、明度±20%因为欧盟车牌颜色本身是特征的组成部分——蓝条、白底、黄底都靠颜色区分色域增强太强会让模型混淆国别特征。随机旋转设±10度车牌在真实场景里基本不会有大幅度倾斜超过15度的旋转引入的样本反而是噪声。Mosaic增强在YOLOv5里是默认开启的它把四张图拼成一张对小目标检测特别有效。但注意关闭mixup这个策略会让车牌和背景直接混叠造成误检。我用这份数据做增强实验时的典型配置是训练集从534张扩展到实际参与梯度更新的约2000张mAP0.5能从0.82提升到0.91。6.2 用训练好的模型做硬负例挖掘数据增强只能缓解数据量不足不能根治。进一步的做法是拿训练好的模型去跑视频抽帧把检测置信度低于0.5但标注确实有车牌的帧挑出来人工确认后补进训练集。这种半自动标注补数据的流程是现在做小型定制数据集的主流方式。我当时处理这份数据时先用534张训练出一个初始模型然后跑了3段原始视频就是文件名里对应的白天和夜间视频抽帧约2000张模型预测后挑出置信度高且和已有训练图片场景不同的帧人工检查后补充了约300张再重新训练效果提升了一个台阶。6.3 模型验证的土办法视频级检测稳定性测试mAP指标只能反映单帧检测能力但实际场景是视频流检测稳定性同样重要。我会写一个简单脚本用训练好的模型逐帧跑测试视频统计相邻帧的检测框抖动情况——框的位置在相邻帧之间跳动超过20个像素就记为一次不稳定运行10秒视频记录不稳定次数。这个指标mAP看不出来但实际部署时影响很大。不稳定时做个简单的平滑处理用前后3帧的检测框坐标取中值就能大幅降低抖动。这个土办法我一直在用应对工程验收比纯看mAP直观得多。数据集的坑走完一遍之后从那以后我每次拿到新数据第一件事就是先用脚本统计标注框面积分布和文件名特殊字符再决定转换策略。这两个动作能省后面不少返工时间。这份534张的欧盟车牌数据集作为流程验证和初始模型训练是合格的但心里要清楚它的边界单类别、夜间框偏大、场景有限。如果你需要更完整的车牌数据集用这个包当种子数据配合视频抽帧和半自动标注工具去扩数据会是更务实的路线。希望帮到你。本文还有配套的精品资源点击获取