ARTICLE DETAIL

资讯详情

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

VOC数据集转YOLO训练车牌检测全流程拆解

VOC数据集转YOLO训练车牌检测全流程拆解 简介该数据集提供534张欧盟车牌实拍图像配套一一对应的VOC格式XML标注文件标注工作基于VOTT工具完成面向目标检测、车牌定位与识别等计算机视觉任务的学习者及算法工程师。图像内容涵盖白天、夜晚及不同行车场景有助于检验检测模型在光照变化和复杂背景下的鲁棒性。包内共有1070个文件主体为jpg图像与XML标注的成对组合另附加1份txt说明与1份md文档方便快速了解标注规范与数据组织方式整体压缩包约428MB可直接用于YOLO、Faster R-CNN等主流检测框架的训练或微调。目前已有185人学习下载适合需要真实欧洲车牌数据开展模型训练、算法效果对比或毕业设计的开发者使用。1. 拆一个 534 张欧盟车牌数据集VOTT 标的 VOC 能不能直接训练做车牌检测的同行可能都有过这种经历从项目里翻出一个标注好的数据集压缩包名字写着“带VOC格式标签、VOTT标注”解压后却不知道能不能直接拿去训练。这个五百多张的欧盟车牌包就是一个典型例子图片来自白天和夜间行车视频的抽帧标注用 VOTT 完成标签走的是 Pascal VOC 格式。我能确认的是这类数据做欧盟车牌检测够用但直接丢进 YOLO 十有八九会翻车——白天效果好、夜间漏检、框整体偏移、XML 和图片对不上这些问题我在复现时都实际遇到过。这篇笔记就把拆包的整个过程写清楚文件命名透露了什么、VOC 字段怎么读、VOC 转 YOLO 的脚本长什么样以及训练前必须做的避坑排查。适合两类人一是手头只有 VOC 标注数据想转 YOLO 训练的车牌检测开发者二是刚接触标注工具选型、想知道 VOTT 标出来的数据该怎么落地的初学者。2. 这包数据的家底文件命名、VOC 标签字段与 VOTT 的选型理由很多数据集拿到手第一反应是看图片、数张数我一般会先看文件名。文件名在标注工具导出时往往会保留原始信息而这个信息对后续训练拆分验证集非常关键。2.1 文件名里的信息量时段、批次与视频时间戳这个包里图片的命名长这样nightride_type3_001.mp4#t352.jpg。拆开看前面部分nightride_type3_001说明它来自夜间行车视频type3 是拍摄批次或路段编号001 是视频序号#t352表示抽帧点在视频播放到第 352 秒的位置。同理dayride_type1_001.mp4#t1199.jpg就是白天视频在第 1199 秒抽出的帧。这层信息对训练很有用夜间和白天的样本天然分开不需要再去翻拍摄日志。拿到数据后我习惯先按前缀做一次数量统计确认昼夜比例避免验证集随手一划就把某个时段的数据全划走。# 按白天/夜间前缀分别统计数量 ls *.jpg | grep -c ^dayride ls *.jpg | grep -c ^nightride # 看 type 字段的分布了解拍摄批次 ls *.jpg | awk -F_ {print $2} | sort | uniq -c第一条和第二条分别输出白天、夜间图片张数第三条用awk按下划线切分文件名统计 type1、type3 这类批次字段的数量。操作上要注意grep -c统计的是行数如果文件只有名字没有换行符输出可能不准但这在常规ls输出下没影响。后面划分 train/val 时我会按前缀比例分层抽样而不是纯随机这样白天夜间在验证集里都能有代表。2.2 VOC 标签里到底写了什么bndbox、difficult 与 truncatedVOC 格式的标签是 XML 文件每张图对应一个。打开看一下结构并不复杂但字段之间主次分明转换到 YOLO 时哪些留哪些丢需要心里有数。annotation foldernightride_type3_001/folder filenamenightride_type3_001.mp4#t352.jpg/filename size width1280/width height720/height depth3/depth /size object nameplate/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin842/xmin ymin356/ymin xmax1013/xmax ymax428/ymax /bndbox /object /annotationfilename记录的是原始图片名注意它带着#t352这一段后面做文件配对时容易在这里出问题size里是图片宽、高、通道数转换 YOLO 坐标要靠它做归一化分母object下的name是类别名bndbox四个值是左上角和右下角的像素坐标。truncated表示目标是否被图像边界截断difficult表示这个目标是否属于难例。对车牌检测这个场景最需要关注的是bndbox和difficult。欧盟车牌大多是白底黑字的长条形占图面积不大但对比度差异明显白天和夜间的框质量差别也大。difficult字段在 VOC 比赛里表示“人类都难辨认”的样本转 YOLO 时如果你没有意图做难例挖掘我一般建议直接过滤掉否则模型会在模糊样本上浪费拟合能力。下面这个表格可以用来对照字段在转换时的去留字段含义转 YOLO 时是否使用folder图片所在目录不使用filename图片文件名用于配对需注意特殊字符size/width图片宽作为归一化分母size/height图片高作为归一化分母object/name类别名映射到 class idobject/bndbox左上角与右下角坐标换算成中心点、宽高object/truncated是否截断可用于过滤极端截断样本object/difficult是否难例建议过滤2.3 为什么是 VOTT从视频抽帧到导出一气呵成这个包标注用的是 VOTT标注工具选型上它和 LabelImg、CVAT 走的路线不太一样。VOTT 这类工具专门处理视频标注时可以按时间戳抽帧像我前面提到的#t352这种帧在 VOTT 里能直接定位到对应时间点框完一帧继续走下一帧不需要先自行把视频拆成图片再导入。这种“边看视频边标注”的流程对行车记录仪素材很友好。LabelImg 是纯图片标注工具更通用但需要先完成抽帧步骤CVAT 功能更强、支持多人协作但要部署服务端对小规模单机标注来说有点重。个人感受是几百张规模的视频抽帧标注VOTT 在“能看视频”和“导出 VOC”这两点上刚好够用学习成本也不高。它导出 VOC 时还会保留原始文件名这也是这个包里图片名带mp4#t后缀的原因之一。这里也顺带提醒一句VOTT 导出的 XML 命名不保证和图片完全一致解压后第一步应该是做清单配对后面避坑章节会再展开。我习惯的做法是先列目录树确认 JPEG 和 XML 是不是一一对应再进入格式转换环节。3. 把 VOC 喂给 YOLO一份可改的 Python 脚本与类名映射VOC 和 YOLO 的标签坐标系不一样不能直接复制粘贴。VOC 存的是左上角和右下角的像素坐标YOLO 要的是归一化后的中心点坐标与宽高。这一步出问题模型训练出来框全是歪的而且很难从 loss 曲线看出来。3.1 转换思路坐标系与两个容易忽略的点转换公式本身不复杂中心点x_center (xmin xmax) / 2 / 图片宽宽w (xmax - xmin) / 图片宽高度方向同理。但有两个点我每次都会强调一是分母必须用图片的实际宽度和高度而不是 XML 里size字段写的值。视频抽帧后如果有人对图片做过缩放、裁剪黑边XML 里的尺寸就可能和实际图不一致强行用它做分母所有框都会整体偏移。二是difficult和truncated字段要提前定好策略是保留还是丢弃。我在转换脚本里会加一层兜底解析完 XML 的 size 之后再用 OpenCV 读一次图片真实尺寸如果对不上就打印警告并以实际尺寸为准计算归一化坐标。这个小动作不少项目能省下一轮返工。3.2 转换脚本XML 解析、合法性检查与输出下面这份脚本可以直接用按你的实际类别名改一下CLASSES就行。import xml.etree.ElementTree as ET from pathlib import Path import cv2 # 按实际情况改成你的类别列表单类车牌就这一行 CLASSES [plate] def parse_voc(xml_path: Path): 解析单个 VOC XML返回图片尺寸和所有目标框。 tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) boxes [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue difficult_el obj.find(difficult) difficult int(difficult_el.text) if difficult_el is not None else 0 if difficult 1: continue # 直接过滤难例 bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) boxes.append((xmin, ymin, xmax, ymax, name)) return img_w, img_h, boxes def convert_voc_to_yolo(xml_dir: Path, img_dir: Path, out_dir: Path): 遍历目录下所有 XML逐个转成 YOLO 格式的 txt。 out_dir.mkdir(parentsTrue, exist_okTrue) for xml_path in sorted(Path(xml_dir).glob(*.xml)): img_w, img_h, boxes parse_voc(xml_path) # 兜底以图片实际尺寸为准 img_path Path(img_dir) / (xml_path.stem .jpg) if img_path.exists(): h, w cv2.imread(str(img_path)).shape[:2] if (w, h) ! (img_w, img_h): print(f[warn] size mismatch: {xml_path.name}, use image size) img_w, img_h w, h txt_path out_dir / (xml_path.stem .txt) with open(txt_path, w, encodingutf-8) as f: for xmin, ymin, xmax, ymax, name in boxes: # 防御 XML 里坐标写反的情况 xmin, xmax min(xmin, xmax), max(xmin, xmax) ymin, ymax min(ymin, ymax), max(ymax, ymax) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w_norm (xmax - xmin) / img_w h_norm (ymax - ymin) / img_h if not (0 x_center 1 and 0 y_center 1): print(f[warn] x_center out of range: {xml_path.name}) if not (0 w_norm 1 and 0 h_norm 1): print(f[warn] width/height out of range: {xml_path.name}) cls_id CLASSES.index(name) f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) if __name__ __main__: convert_voc_to_yolo( xml_dirPath(Annotations), img_dirPath(JPEGImages), out_dirPath(labels), )逻辑上分三步先解析 XML 拿到尺寸和所有框再对每张图做坐标换算最后写入 txt。文本里每一行对应一个目标格式是类别id 中心x 中心y 宽 高全部是 0 到 1 之间的小数。参数上xml_dir放 VOC 的 XML 目录img_dir放同级图片目录out_dir是输出目录。脚本运行时如果终端打印了size mismatch或out of range说明数据里存在脏样本不要忽略后续要回源头检查。3.3 参数怎么改多类别、难例保留与 data.yaml 的同步如果你这个包里的 XML 不只包含plate一个类别直接把CLASSES扩成多类即可顺序决定 id比如[plate, car]后续训练配置里的names必须和这个顺序一致。想保留difficult的框就把脚本里if difficult 1: continue这一行注释掉。想做缩框处理可以在算完xmin/xmax后乘一个收缩系数比如向内收缩 5%这在检测框普遍偏大时很有效。转完 txt 之后还要同步一份 YOLO 训练用的数据配置。下面这个dataset.yaml是常见写法路径按自己的目录结构改path: ./dataset train: images/train val: images/val nc: 1 names: [plate]nc是类别总数names的顺序严格对应CLASSES列表。如果图里既有白天又有夜间建议 train 和 val 都按时段比例抽样YOLOv5/v8 仓库里常见的启动命令类似python train.py --data dataset.yaml --epochs 100批次大小再按显存调整。我自己习惯先跑 50 轮只看验证集 mAP 的走势确认标签没有大问题后再加轮数。4. 训练前的避坑排查VOC 转 YOLO 时最容易翻车的四类问题很多训练效果差不是模型玄学而是标签在转换时悄悄变形了并且这类问题几乎不会在 loss 曲线上直接暴露。以下四类问题是我在这个车牌数据集上实际撞到过的按“现象 → 原因 → 解决”整理。4.1 XML 和图片对不上训练提示 No labels found现象训练时日志里频繁出现No labels found in ...或者某几张图完全没有对应的 txt但数据包里的 XML 明明存在。原因VOTT 导出的 VOC XML 文件名和图片名不是简单的一一对应关系尤其当原始图片名里带着mp4#t352这种字符串时Windows 会把#当特殊字符处理某些工具解析路径时直接截断另外 XML 后缀也可能因为导出选项不同而变成xxx.jpg.xml和图片的 stem 对不上。解决解压后先跑一遍配对脚本把 JPEG 和 XML 的 stem 列出来对比确认所有图片都有对应标注。不一致时统一重命名成纯数字编号比如000001.jpg同时把 XML 也改成同名再跑转换脚本。从那以后我每拿到一个 VOC 数据集第一步永远是做这个配对检查不然后面全是连锁反应。4.2 白天效果不错夜间几乎全漏检现象模型在白天验证集上 AP 很高夜间帧上基本检不出车牌或者框住的是车灯而不是车牌。原因这个数据包的白天和夜间抽帧比例本身就不均衡夜间图片占比如果再被随机划分冲淡模型看到的夜间样本更少另一个隐藏原因是夜间图片里车牌对比度差标注时容易把亮斑和牌照边缘混在一起框的真实性不如白天帧。解决用第 2 章里的统计命令确认 dayride 和 nightride 各自占比按前缀分层划分 train/val。训练时对夜间样本做数据增强比如亮度抖动、直方图均衡或者把夜间图片单独复制一份进训练集做样本均衡。如果夜间样本实在太少退一步的做法是白天夜间各训一个模型推理时输入端按亮度判断走哪个模型。4.3 框整体偏大或贴在图片边缘坐标没有真正归一化现象转出来的 txt 可视化后框比车牌大一圈或者框的一条边紧贴图片边界像是被人为拉长了。原因最常见的是分母用错。XML 的size字段写的是 1280x720但实际 jpg 因为去黑边、缩放变成了 1248x704直接用旧尺寸归一化所有框都会偏移另一种可能是有目标本身就超出画面边缘xmin或xmax出现负值或超过图宽。解决转换脚本里我已经加了实际尺寸兜底读到size mismatch告警时回到原图确认宽高。更稳妥的方式是把所有 XML 里超出边界的坐标先裁剪到[0, 图片宽/高]范围内再算归一化值我个人会在脚本里加一步 clamp 处理避免一条坏数据拖累整个训练。注意不要只看一两个框正常就跳过全部检查这类问题往往是按批次出现的某个时段抽帧的图会集体中招。4.4 中心点全挤在角落宽高比变成细长条现象可视化标签时发现某个时段图片的框中心点全部堆在图像左上角或右下角框的形状变成很细的长条。原因坐标写反了。XML 里如果xmin和xmax的顺序不统一或者混入了归一化和未归一化两种坐标系的数据换算后中心点会跑到奇怪的位置。还有一次我发现是分割文件名时把type3当成了类别 id导致类别编号错位视觉上就成了一个个乱跳的框。解决写一个简单的合法性自检遍历所有 txt 检查0 x_center 1、0 w 1不满足就打印文件名。另外每次转换完我都会挑 20 张图把框画回原图上肉眼扫一眼比对框和车牌边缘的距离。画框脚本放在下一章这个动作虽然原始但比看任何统计数字都直接。5. 验证动作画框回归脚本与数据质量体检转换完标签正式训练前我建议你先花十分钟做一个回归校验把标签和图片重新对齐一遍。5.1 校验脚本把 txt 和图片尺寸再对一遍import cv2 from pathlib import Path for txt in Path(labels).glob(*.txt): img_path Path(images) / (txt.stem .jpg) if not img_path.exists(): print(f[missing] {txt.stem}) continue img cv2.imread(str(img_path)) h, w img.shape[:2] for line in txt.read_text().splitlines(): cls_id, x, y, bw, bh map(float, line.split()) x1 int((x - bw / 2) * w) y1 int((y - bh / 2) * h) x2 int((x bw / 2) * w) y2 int((y bh / 2) * h) if x1 0 or y1 0 or x2 w or y2 h: print(f[out-of-bound] {txt.stem}: ({x1}, {y1}, {x2}, {y2})) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(fcheck_{txt.stem}.jpg, img)这段脚本读回 txt 里的归一化值乘以实际图片尺寸画框同时检查框是否越界。它验证的不是模型而是你手里的标签是否还能对应回原图。跑完后随便打开几张生成的check_*.jpg看框和车牌的贴合程度框比车牌大一圈的、框斜着跨到隔壁车道的、盖在车灯上的都要回源头重新处理。5.2 画框抽查看清标注的边界和时段的差异夜间抽帧的图尤其建议多抽几张看框是不是明显比白天的松。欧盟车牌本身是长方形蓝边白底光照不足时边缘位置标不准很正常如果单张图偏差超过 10%我会把那批夜间图打回重新标注而不是靠模型硬扛。这个动作也能帮你判断要不要做缩框处理抽查 20 张里超过三分之一框的外边距明显大于车牌本身就值得在转换脚本里加一个 5% 的收缩系数。从那以后我每次换数据集哪怕是已经标注好的包也会强制走一遍配对统计、合法性检查和画框抽查这三步再进训练流程。这套动作不复杂但能省下大量调参和排错的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表