ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

欧盟车牌图像数据集VOC格式解析:从VOTT标注到YOLO训练的完整流程

欧盟车牌图像数据集VOC格式解析:从VOTT标注到YOLO训练的完整流程 简介这份资源是面向计算机视觉入门及进阶学习者的欧盟车牌图像数据集包含534张真实行车场景下的车辆图片均使用VOTT工具完成标注并输出VOC格式的XML标签文件适用于目标检测、车牌识别、OCR等模型的训练与效果验证。数据采集覆盖日间、夜间及多种路况图片取自行车记录视频帧场景差异明显有助于提升模型在光照变化和复杂背景下的鲁棒性。压缩包共1070个文件除534张jpg原图与534个xml标注文件外附带一个txt类别说明与一个md数据说明文档整体大小约428MB。目录结构简洁原图与标注一一对应可直接接入主流检测框架或作为自定义数据集的基础。目前已有185人浏览学习适合正在积累车牌类视觉数据或需要标准VOC格式标注样本的开发者快速取用省去自行采集与人工标注的时间成本。1. 五百张欧盟车牌图像数据集小样本检测的冷启动样本一个停车场改造项目甲方要求识别欧盟牌照车辆翻遍公开数据集要么是国内蓝牌要么是美国黄黑牌欧盟车牌的蓝底、国家代码和星环一个都不对。直到拿到这个 534 张欧盟车牌图像数据集带 VOC 格式标签使用 VOTT 标注zip 打包才从“找数据”回到“训模型”。这个 zip 是单类目标检测的冷启动样本534 张图每张标一个车牌框标签是标准 Pascal VOC XML适合直接做车牌检测训练或验证。直接客群是做车牌检测/OCR 前置的算法工程师、在边缘设备部署检测模型的嵌入式开发以及需要欧盟场景数据做验证的产品团队。结论先行534 张不多但单类、场景集中、目标特征清晰配合迁移学习足够跑通流程真正的成本在格式转换和标注一致性下面按实际处理顺序讲。2. 拆开 zip 看 VOC 格式目录结构、XML 字段与 534 张的数据底细2.1 解压后先确认目录VOC 三件套缺什么拿到 zip 不要急着解压在 Linux 下我一般先列压缩包内容确认目录结构再动手这一步能避免把未知文件散落到当前目录unzip -l 534张欧盟车牌图像数据集(带VOC格式标签使用VOTT标注.zip | head -30unzip -l只列出文件不真正解压head -30截断超长输出。看到的是带完整路径的文件列表能直接判断根目录下有几个文件夹。真正解压用这条unzip -q 534张欧盟车牌图像数据集(带VOC格式标签使用VOTT标注.zip -d eu_plate cd eu_plate find . -maxdepth 2 -type d | sort-q安静模式避免刷屏-d eu_plate指定解开后的根目录不让压缩包内部路径散落在外层。VOTT 导出 Pascal VOC 的典型结构是根目录下分 JPEGImages 和 Annotations 两个文件夹如果解压后发现缺 ImageSets/Main这并不意外VOTT 对 ImageSets 的生成一直很马虎后面会给补生成脚本。接着核对图片和标签是否一一对应这一步决定了后续所有操作能不能继续from pathlib import Path root Path(eu_plate) imgs sorted((root / JPEGImages).glob(*.jpg)) xmls sorted((root / Annotations).glob(*.xml)) print(图片数:, len(imgs), 标签数:, len(xmls)) img_stems {p.stem for p in imgs} xml_stems {p.stem for p in xmls} print(缺标签的图:, sorted(img_stems - xml_stems)[:10]) print(缺图的标签:, sorted(xml_stems - img_stems)[:10])用stem不含扩展名的文件名做主键配对比直接比文件名可靠因为 VOTT 导出的 XML 文件名可能带后缀也可能不带。如果数量差很多先查 JPEGImages 里是不是混了 png/tiff 等其他格式VOTT 导出时会把图片复制成 jpg偶尔也有漏转换的情况。这个核对脚本能提前暴露数据集干不干净缺标签的图超过几十张就说明标注过程有漏画后面转换时会表现为空标签文件早发现早处理。2.2 读懂一张车牌 XMLsize 和 bndbox 是训练的直接输入VOC 格式和那种一行一个框的 txt 不一样它把“这张图多大、框在哪儿、是什么类”用结构化字段写清楚典型的一张车牌标注 XML 长这样annotation folderJPEGImages/folder filenameeu_plate_0001.jpg/filename pathD:/datasets/eu_plate/JPEGImages/eu_plate_0001.jpg/path size width1920/width height1080/height depth3/depth /size object nameeu_plate/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin820.5/xmin ymin480.2/ymin xmax1106.0/xmax ymax562.8/ymax /bndbox /object /annotationsize 是原图宽高深bndbox 里的四个数字是画框时的绝对像素坐标。VOTT 导出的 bndbox 经常带小数因为画框记录的是鼠标位置的原始值不像早期 labelImg 会强制取整所以读取时一律转 float不要用 int否则坐标会整体偏移。对车牌这种几十像素宽的目标1 像素偏差在 loss 里不明显但转 YOLO 归一化后几位小数都会被污染。VOTT 导出的 XML 默认不写 pose/truncated/difficult 这几个字段。很多读取脚本写死root.find(object/truncated).text在 VOTT 数据上会直接抛 AttributeError。我一般用root.iter(object)遍历缺失字段给默认值兼容性最好第四章的转换脚本就是这个思路。2.3 534 张的数量级判断单类检测的底线数据量很多人看到 534 的第一反应是“这也能训练”这个直觉对通用检测几十类、背景杂乱是对的但对单类车牌不成立。车牌检测只有一个类形态高度固定矩形、高对比、蓝底白字或黑字、位置一般落在画面中下区域这类任务的有效信息密度高500 张足够让模型学会“哪里有一块车牌”。难点在雨天、遮挡、暗光下的稳定性那是数据分布问题不是数量问题。任务场景数据量参考策略单类固定目标车牌/二维码/表计从 500 张起步迁移学习 默认增强多类目标车辆行人标志从 3000 张起步按场景分层补样本小目标 复杂背景从 10000 张起步需要硬负例和难例挖掘534 张落在“够跑通但不够折腾”的区间做迁移学习、验证 pipeline、出 Demo 完全够用想挑战极端鲁棒性夜间、远距离、模糊就要自己补数据。所以这类数据集的正确用法不是直接上线而是当种子集先训练初版模型用模型去搜更多难例人机协同把它滚大。3. 用 VOTT 从原图到 VOC 标签标注流程与导出核对3.1 选 VOTT 而不是 labelImg 的理由数据集标题点明它是“使用 VOTT 标注”VOTTVisual Object Tagging Tool是微软开源的跨平台标注工具Electron 架构支持 Windows、Linux、macOS。和 labelImg 相比它在三个点上值得选一是导出预设里直接带 Pascal VOC不用自己写 XML 生成器二是视频抽帧和连续帧标注能力更强适合从监控视频批量抽车牌帧三是标签管理走 Tag 体系能导出 CSV 做二次筛检和坐标质量审计。缺点同样明显项目更新停滞、内存占用高、高分屏缩放支持一般部分版本导出 VOC 时不会自动生成 ImageSets/Main。如果只是“给 500 张图画框”labelImg 完全够两个工具产出的 VOC 字段基本一致。选 VOTT 的真正理由是协作和可回溯——项目文件是 JSON容易纳入版本管理出现漏标能回溯到具体操作这对需要长期维护的数据集很重要。3.2 建项目到导出的完整步骤如果你拿到 zip 后还想自己复标一批欧盟车牌比如补拍夜间图VOTT 的固定流程按下面这张表走基本不会翻车步骤操作注意事项新建项目打开 VOTT → New Project输入项目名名称用英文不要用中文或空格连接数据源Source 选 Local Folder指向图片目录数据量不大时别连 Azure本地最稳安全管理Connection Settings 确认 Source 是本地项目建错位置时导出的 path 会带无效地址画框打开一张图按住鼠标拖出矩形框贴车牌外沿留 2~3 像素余量打标签Tags 输入 eu_plate 回车点 Assign每个框必须挂标签空框导出会丢复查键盘左右箭头快速翻图重点看远景小框小目标漏标是车牌数据最常见问题导出Export Settings 选 Pascal VOC点 Export导出目录选空目录避免同名覆盖“安全管理”这一步很多教程不提。VOTT 启动时默认对非 HTTPS 资源有安全限制本地文件夹一般不受影响但如果图片路径带中文个别版本导出时 path 字段会乱码稳妥做法是图片放在纯英文路径下再建项目。画框还有一个细节车牌检测的下游通常是 OCR如果 OCR 需要完整车牌图框就要包含整块车牌加少量边缘不要把字符裁掉如果下游是车辆重识别框要含整个车头。同一个标注集给两个任务用标注规范必然冲突VOTT 的 Tag 体系支持多套标签并存eu_plate 和 vehicle_head但导出 VOC 时会变成两个类转换脚本里要记得分开处理。3.3 导出后必须做的三件事VOTT 导出 VOC 后默认不生成 ImageSets/Main或只生成空文件。这部分是训练集/验证集划分的关键需要手动补常见做法是按 8:2 划分提示在运行任何转换、把数据集喂给训练框架之前先保留一份原始 zip 的完整拷贝。import random from pathlib import Path random.seed(42) ann_dir Path(Annotations) img_dir Path(JPEGImages) out_main Path(ImageSets/Main) out_main.mkdir(parentsTrue, exist_okTrue) stems [p.stem for p in ann_dir.glob(*.xml)] random.shuffle(stems) split int(len(stems) * 0.8) (out_main / train.txt).write_text(\n.join(stems[:split]) \n) (out_main / val.txt).write_text(\n.join(stems[split:]) \n) print(train:, split, val:, len(stems) - split)random.seed(42)固定随机种子保证多次运行划分一致后面复现实验结果不会变列表存的是 stem 不带目录前缀因为 VOC 的 ImageSets 文件本来就只写名字读取时再拼路径。如果数据里有不同子目录的同名图这一步会暴露出来train 和 val 会互相污染所以务必先跑 2.1 的数量核对。第二件事是检查 XML 里的 filename 和实际文件名是否一致。VOTT 在文件名上有个历史包袱有时导出不带扩展名有时带。用一段 shell 快速扫for xml in Annotations/*.xml; do fname$(grep -o filename.*/filename $xml | sed s/[^]*//g) echo $xml - $fname done | head -20如果看到 filename 没带 .jpg统一修正我建议直接改 XML因为很多训练框架只认 filename 字段。第三件事是归档zip 本身就带 VOTT 标注说明标注过程是规范的但你拿到手要先留存一份“原样未动”的备份再动手转换。转换脚本跑坏 XML、覆盖标签、压缩包损坏这些事故我在项目里都见过备份是唯一的后悔药。3.4 VOTT 的中间产物CSV 和 JSON 的再利用VOTT 标注过程中会自动生成一个{项目名}_export.csv每条记录是一帧图片和一个 region 的对应关系包含标签、坐标、置信度人工标注默认 1.0。这个 CSV 有两个额外用途。一是按“标签名文件名”过滤可疑样本坐标越界的框在 CSV 里可以直接算出来不用解析 XML。二是如果 VOC 导出后发现 XML 批量损坏可以用 CSV 重建标签不必重新打开 VOTT 一张张画。我遇到过一次 XML 批量损坏就是用 CSV 重新生成的 Annotations省了大半天。如果 zip 里碰巧附带这个 CSV建议保留——它不是临时产物而是标注过程的可溯源记录。4. 把 VOC 转成 YOLO 训练格式转换脚本与最小训练命令4.1 为什么必须转 YOLO 格式VOC 的 XML 对人友好但对训练框架不友好解析慢、字段冗余、坐标是绝对像素一个目标一个 object 节点遍历成本高。YOLO 系列v5/v8 检测头统一吃单个 txt 文件每行一个目标class_id x_center y_center width height四个数值全部用图片宽高归一化到 0~1。训练时 Dataloader 直接读 txt不用反复解析文本节点读取效率高很多。所以训练前一般会把 VOC 转成 YOLO 格式这一步自己写脚本最可靠。目标检测的标签是硬标签——每个框一个确定性类别不携带置信度和分类任务的软标签是两回事坐标转归一化时一旦出错模型压根学不到正确位置。4.2 转换脚本XML 到 txt顺手过滤空标注和越界框下面这个脚本是我在类似数据集上常用的简化版直接放在解压后的根目录运行import xml.etree.ElementTree as ET from pathlib import Path classes [eu_plate] # 类别表顺序就是 YOLO 的 class id def voc_to_yolo(xml_path: Path, label_dir: Path) - bool: tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: 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) # 坐标越界修正VOTT 画框偶尔会拖出图片边界 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax - xmin 2 or ymax - ymin 2: continue # 过小的框一般是手抖直接丢弃 cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{classes.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if not lines: return False # 空标注由调用方统计后决定剔除还是补标 label_dir.mkdir(parentsTrue, exist_okTrue) (label_dir / (xml_path.stem .txt)).write_text(\n.join(lines) \n) return True # 对全部 XML 执行转换并统计 ann_dir Path(Annotations) label_dir Path(labels) converted skipped 0 for xml_path in sorted(ann_dir.glob(*.xml)): if voc_to_yolo(xml_path, label_dir): converted 1 else: skipped 1 print(f转换成功 {converted} 张空标签跳过 {skipped} 张)逻辑说明XML 里的绝对像素坐标被转换为归一化的中心点加宽高。三个容易出问题的点都已处理——第一坐标做了 clip最多让框贴近边界不会让训练 loss 变 NaN第二宽高小于 2 像素的框直接丢弃这种框大概率是标注时手抖画出来的第三空 XML 返回 False 单独统计事后回 VOTT 补标而不是让训练去读 0 字节 txt。4.3 目录整理与 data.yaml转换完成后把图片按 train/val 分到两个目录这里用复制而不是移动保留原始 JPEGImages 目录方便后面换其他框架重新组织import shutil from pathlib import Path img_dir Path(JPEGImages) for split_name in (train, val): dest Path(images) / split_name dest.mkdir(parentsTrue, exist_okTrue) for stem in (Path(ImageSets/Main) / f{split_name}.txt).read_text().split(): src_img img_dir / f{stem}.jpg if src_img.exists(): shutil.copy(src_img, dest / f{stem}.jpg)文件名拼接用的是.jpg如果原始图片里有 png这里要按实际扩展名匹配。再看 data.yaml喂给 ultralytics 框架# data.yaml path: /absolute/path/to/eu_plate # 改成你的实际路径 train: images/train val: images/val nc: 1 names: 0: eu_platepath强烈建议写绝对路径。不同机器上相对路径解析差异极大尤其在 Docker 或远程训练场景相对路径是翻车的高发地带。4.4 最小训练命令与参数设置用 YOLOv8n 做迁移学习命令非常短yolo detect train \ data/absolute/path/to/eu_plate/data.yaml \ modelyolov8n.pt \ epochs60 \ imgsz640 \ batch16 \ device0 \ patience10参数说明modelyolov8n.pt是大规模数据上预训练过的权重对 534 张的小数据集是必须项从零初始化训练基本训不出东西。epochs60对这个小数据集已经足够再多就是过拟合。imgsz640是 v8 默认值车牌属于小目标如果源图分辨率远高于 640比如 1920×1080可以试imgsz1280但显存占用会涨很多先 640 起步更稳。patience10指连续 10 个 epoch 的 val 指标没有提升就提前结束省时间。训练完看runs/detect/train目录weights/best.pt是验证集上表现最好的权重。先用它跑一遍推理再决定要不要调参yolo predict modelruns/detect/train/weights/best.pt \ source/absolute/path/to/eu_plate/images/val \ save_txtTrue save_imgTrue这一步的意义是建立直觉模型漏检的是大车牌还是小车牌远处的模糊车牌有没有被捡回来。先看坏例再行动比盲目调学习率靠谱得多。5. 避坑记录解压、路径、坐标与训练中的五个翻车点5.1 zip 解压后文件名乱码标签全对不上现象在 Linux 下用 unzip 解压这个带中文名的压缩包JPEGImages 里的文件名变成乱码XML 里的 filename 还是原始中文图片和标签完全对不上。原因压缩包里部分文件名在打包时用的是 GBK/GB18030 编码Linux 的 unzip 默认按 UTF-8 解码中文名就花了。解决用 unzip 的编码参数直接解压unzip -O GBK 534张欧盟车牌图像数据集(带VOC格式标签使用VOTT标注.zip或者用 Python 重命名逐个解压import zipfile from pathlib import Path z zipfile.ZipFile(your_dataset.zip) # 换成实际文件名 for info in z.infolist(): # zipfile 默认用 cp437 解码文件名先还原成 bytes 再按 gbk 解 name info.filename.encode(cp437).decode(gbk, errorsignore) target Path(name) target.parent.mkdir(parentsTrue, exist_okTrue) with z.open(info) as src, open(target, wb) as dst: dst.write(src.read())如果 zip 是伪加密——直接解压报错但用 7-Zip 却能打开——说明加密标志位是假的文件实际没加密直接用 7z 或 Python 无密码解压即可。5.2 XML 里的 filename 不带扩展名训练时找不到图现象VOTT 导出的 XML 里 filename 是eu_plate_0001而不是eu_plate_0001.jpg转换脚本按f{stem}.jpg拼文件名部分图片找不到训练中断。原因VOTT 导出 VOC 的已知行为部分版本导出的 filename 不补扩展名。解决转换前批量修正 XMLsed -i s#filename\([^]*\)/filename#filename\1.jpg/filename# Annotations/*.xml这个 sed 把所有 filename 节点末尾补上.jpg。如果部分文件本身就是.jpg会出现.jpg.jpg所以先跑 3.3 里的 grep 检查脚本确认受影响数量再统一处理。5.3 bndbox 坐标越界loss 直接变 NaN现象转换脚本没做 clip训练到某一步box_loss突然变成 nan整个训练报废重来。原因标注时手抖把框拖到图片外xmax 比图片宽度还大归一化后出现大于 1 的坐标值模型计算 IoU 时数值溢出。解决转换脚本里把坐标 clip 到[0, w]和[0, h]宽高小于 2 像素的框直接丢。另外可以先用脚本扫描所有 XML把越界框列出来python3 - EOF import xml.etree.ElementTree as ET from pathlib import Path for xml in Path(Annotations).glob(*.xml): root ET.parse(xml).getroot() size root.find(size) w float(size.find(width).text) h float(size.find(height).text) for obj in root.iter(object): b obj.find(bndbox) x1, y1, x2, y2 [float(b.find(k).text) for k in (xmin, ymin, xmax, ymax)] if x1 0 or y1 0 or x2 w or y2 h: print(xml.name, x1, y1, x2, y2, 图片宽高:, w, h) EOF扫描脚本在进训练前就能把脏数据揪出来省一次 NaN 重来。5.4 空标注 XML 导致 txt 为 0 字节dataloader 报错现象转换脚本统计后有十几张 skipped数量核对显示 XML 和图片一一对应但训练时 dataloader 报“空标签文件”。原因VOTT 里图片打开但没画框就导出或者画了框忘记 Assign 标签导出时 object 节点被忽略XML 里只有一个空 annotation。解决skipped 清单不要放过回 VOTT 对应图片补画框再导出。如果只是少数几张直接从训练集剔除也合理——它们不含目标对训练没有贡献反而会把 dataloader 搞崩。剔除时记得同步绕开对应的 XML 和 txt。5.5 小数据集过拟合val 指标死活上不去现象train loss 一路掉到 0.02val mAP50 卡在 0.4 上下训练曲线一开始就分叉。原因534 张数据对 YOLOv8n 这种百万级参数模型来说仍然偏少训练轮次一长模型开始背训练图的背景纹理。解决三招并行。第一用最小的 n 模型而不是 s 或 m。第二关掉 mosaic 增强mosaic 把四张图拼接成一张对这个小数据集增强太激进车牌容易被切成半截。第三前十几个 epoch 冻结主干先只训检测头loss 稳定后再解冻微调yolo detect train datadata.yaml modelyolov8n.pt \ epochs60 imgsz640 batch16 \ freeze10 patience10freeze10冻结前 10 个 layer对 v8n 来说基本冻结了主干网络让模型先在标注好的框上把检测头训出来再放开主干做精细调整。这是小数据集上防止过拟合最直接的一招。6. 把 534 张的余量榨干小目标检测的验证与调参技巧6.1 跑 val 时别只看 mAP用 F1 选置信度阈值534 张训练出来的模型mAP50 到 0.9 说明检测头基本学对了位置但真正上线前还要决定一个置信度阈值——高于它才输出。阈值直接影响误检和漏检的平衡v8 默认 0.25 是通用值不一定是这个数据集的最优值。跑一遍 val 看 F1 曲线yolo detect val modelruns/detect/train/weights/best.pt \ data/absolute/path/to/eu_plate/data.yaml \ conf0.25 iou0.5跑完后runs/detect/val目录里有F1_curve.png横轴是置信度纵轴是 F1峰值对应的置信度就是要用的阈值。如果峰值在 0.4 以上说明模型对车牌很自信用 0.25 会多出不少误检把阈值抬上去更干净。6.2 对漏检图做 TTA判断是模型没学会还是样本太少小数据集上分布偏差很大val 里漏检的图未必是模型差可能是训练里没见过类似样本。用测试时增强跑一遍如果能把漏检捡回来说明特征学进去了只是推理时对位置和清晰度敏感如果还是捡不回来那就是缺这类样本回去补数据更划算yolo predict modelruns/detect/train/weights/best.pt \ source/absolute/path/to/eu_plate/images/val \ augmentTrue save_imgTrue我的习惯是每轮调参前先跑一次 TTA 推理把漏检图分成“补数据能解决”和“调参能解决”两堆只调该调的不在 534 张上做无意义的超参穷举。小数据集训练本来就是玄学成分居多把调试方向分清能少走很多弯路。6.3 人机协同滚数据集初版模型当标注员这个 534 张的 zip 用完别丢它最大的价值是当种子。训练完初版模型后我一般把它对新采集的无标签图片跑一遍推理出一批带框预标注再人工修正边角而不是从零画。框的位置基本准只需要拖一拖效率大约是从零标注的三倍。这也是冷启动数据集的最终归宿——数据量滚到 2000 张之后用同一套脚本重新训练val 曲线提升会稳定很多。希望这套“解压→核对→转换→训练→验证→补数据”的流程能帮到你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
返回列表