ARTICLE DETAIL

资讯详情

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

VOC数据集转YOLO实战:公交车子集格式转换与训练

VOC数据集转YOLO实战:公交车子集格式转换与训练 简介面向目标检测实战的YOLO公交车检测数据集从PASCAL VOC2012训练验证集中筛选出所有含公交车实例的图像专供训练与评估YOLO模型。压缩包共1402个文件包含467张jpg原图、467个xml详细标注及468个txt简洁边界框标签整体55.82MB解压即可用于模型训练适合正在学习YOLO算法或从事交通监控、自动驾驶研究的开发者。已有644人学习利用这份数据可省去手动标注的繁琐过程直接进行格式转换、训练集划分、模型训练和mAP指标评估能够快速掌握从数据准备到目标检测落地的完整流程。1. 拿到bus_VOCtrainval2012.zip后离YOLO训练还差几步解压bus_VOCtrainval2012.zip那一刻你手上是PASCAL VOC 2012的trainval子集里和公交车相关的部分JPEGImages里是图Annotations里是XML但YOLO训练要的是txt格式的归一化框。很多新手在这一步就卡住也有人直接把XML丢进YOLO训练直接报错。这个包的价值在于它把目标检测领域最经典的数据组织方式——VOC目录结构和bus类别——原样保留下你可以在上面完整走一遍数据清洗、格式转换、划分训练验证集、训练YOLOv8的流程。适合刚拿到VOC数据集不知从哪里下手的初学者也适合做车辆检测想快速验证方案的工程师。先把目录结构看清楚再转格式别急着套脚本。2. 拆解VOC目录结构与bus类分布先摸清数据再动手2.1 JPEGImages、Annotations、ImageSets各管什么先确认压缩包有没有缺目录拿到压缩包之后第一件事不是写转换脚本而是先确认目录结构是否完整。常见做法是解压后直接find看层级。很多VOC子集压缩包在二次打包时把ImageSets目录丢了只留JPEGImages和Annotations这会直接影响后续是否能用官方train/val划分所以这一步值得花两分钟。unzip bus_VOCtrainval2012.zip -d bus_voc cd bus_voc find . -maxdepth 2 -type d | sort这个命令把压缩包解压到bus_voc目录-maxdepth 2限制只看到两层目录避免Annotations里几千个XML刷屏。正常情况你会看到Annotations、JPEGImages和ImageSets三个目录如果ImageSets不存在后面划分训练验证集就要自己重切多一步风险。bus_VOCtrainval2012这个名字里的trainval来自PASCAL VOC 2012 Challenge的训练加验证全量数据。VOC 2012官方划分里train和val是两批不重叠的图片trainval就是把两者合起来这个子集包就是把其中和bus有关的图抽了出来。PASCAL VOC 2012定义了20个对象类别bus是其中之一类别表顺序固定后面转YOLO标签时要用到这个顺序来算类别ID。ImageSets/Main目录下放着按类别划分的清单文件比如bus_train.txt、bus_val.txt、bus_trainval.txt每一行由“图片名空格1或-1”组成1表示这张图里存在bus目标-1表示明确不含。利用官方清单切分的好处是能和论文里的Baseline对齐VOC上大量公开结果是按官方train/val跑出来的你换了划分数字就没法横向比较。如果只图省事随便shuf可能得到虚高的mAP后面部署到真实场景再翻车折腾的是自己。JPEGImages负责存原图Annotations负责存同名XML数量对不上的情况我遇到过不止一次。ls JPEGImages | wc -l ls Annotations | wc -l两个数字对不上说明打包时丢过文件后面转换脚本会生成一批指向空XML的标签YOLO加载时会跳过这些样本等于训练集悄悄变少。VOC 2012的trainval合计大约一万张图但bus只占其中很小一部分单类训练时代价很低几块消费级显卡就能跑起来。2.2 从XML里数bus实例分清“含bus的图”和“只有bus的图”VOC的标注是XML而不是给人看的JSON先看一条典型记录后面所有转换逻辑都围绕这几个节点annotation filename000001.jpg/filename size width500/width height375/height /size object namebus/name difficult0/difficult bndbox xmin100/xmin ymin200/ymin xmax300/xmax ymax400/ymax /bndbox /object /annotationfilename对应JPEGImages下的原图size里的width和height是归一化坐标的分母object节点可以出现多次代表一张图里的多个目标name是类别名bndbox是像素级的左上右下坐标difficult1表示这个目标本身很难认官方评测不计入正样本转换时通常直接丢掉。注意XML里不写类别IDbus到底是多少号要靠外部类别表映射这一步错后面全错。先不急着写转换脚本用两条grep把家底数清楚grep -l namebus/name Annotations/*.xml | wc -l grep -o namebus/name Annotations/*.xml | wc -l第一条统计的是“含bus的图像数”第二条统计的是“bus实例总数”因为一张图里可能有多个bus两个数字一般不相等。这两条命令的结果要记下来转完YOLO格式后再对一遍总计行数能立刻发现转换脚本是否丢框。接着写一个识别哪些图是纯bus的脚本。小数据集训练时我一般保留多类图不删除等会说明。import glob import xml.etree.ElementTree as ET xml_files sorted(glob.glob(Annotations/*.xml)) bus_images, bus_only_images [], [] for xml_file in xml_files: root ET.parse(xml_file).getroot() classes {obj.findtext(name) for obj in root.findall(object)} if bus not in classes: continue img_name root.findtext(filename) bus_images.append(img_name) if len(classes) 1: bus_only_images.append(img_name) print(f含bus的图像: {len(bus_images)}) print(f只有bus的图像: {len(bus_only_images)})这段脚本用xml.etree遍历每个XML把每张图里出现的所有类别收集进一个set。set的长度等于1说明这张图只有bus目标属于纯bus图否则说明图里还混着car、person、truck之类。选择保留多类图对训练有实际价值模型能学到bus常见的道路上下文误检率更低而把多类图全部删掉会让数据集很干净但遇到车流混合场景时模型反而变脆。我一般会保留多类图转换时只写出bus那一行其余目标当背景。不过如果评估时想严格看单类AP验证集最好保持只有bus的图避免其它目标干扰指标解释。3. VOC标注转YOLO标签转换脚本与四个边界坑3.1 最小可跑的VOC2YOLO转换脚本过滤bus并归一化坐标VOC用像素坐标加图片宽高描述框YOLO要的是相对图片宽高的中心点坐标和宽高缩放和归一化是转换的核心。下面这个脚本只保留bus类别并且跳过difficult1的困难目标import glob import os import xml.etree.ElementTree as ET VOC_CLASSES [ aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor, ] KEEP_CLASSES {bus} def convert_one(xml_path, label_path): root ET.parse(xml_path).getroot() size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in KEEP_CLASSES: continue if obj.findtext(difficult, 0) 1: continue bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{VOC_CLASSES.index(name)} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(label_path, w) as f: f.write(\n.join(lines) \n) os.makedirs(labels, exist_okTrue) for xml_file in glob.glob(Annotations/*.xml): label_path labels/ os.path.basename(xml_file).replace(.xml, .txt) convert_one(xml_file, label_path)代码逻辑对应VOC到YOLO的标准换算中心点x(xminxmax)/2再除以图宽得到一个0到1之间的比例宽度同理用(xmax-xmin)除以图宽。之所以要归一化而不是直接存像素值是因为YOLO训练时会把输入resize到640x640之类的固定尺寸模型回归的是相对坐标像素坐标在resize之后会失效。类别ID用的是上面VOC_CLASSES的索引bus在这个表里是第6位索引5这个顺序不是随便排的是VOC官方和YOLO生态里通用的类别顺序。注意obj.findtext(difficult, 0)有些XML没写difficult节点默认按0处理也就是正常正样本。文件名用os.path.basename把Annotations里的XML文件名映射到labels目录下的同名txt保证图片、XML、txt三者同名这是YOLO数据组织的基本约定缺一个字母训练都找不到对应标签。3.2 转出来的txt闹鬼坐标越界、difficult目标和类别错位转换脚本跑完只是第一步因为VOC标注本身不保证干净以下三个问题我在实际转换中几乎每次都会遇到。第一个是坐标越界。VOC里偶尔会出现xmax比图片宽度还大几个像素的标注或者xmin小于0的情况直接转出来的中心点可能略大于1有些框架会报AssertionError有些会静默丢掉这个框不管哪种行为都算不上稳定。常见做法是在转换前做一次clipxmin 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 or ymax ymin: continueclip之后如果框变成没有宽高直接continue跳过不留空行。这段保护逻辑对训练稳定性的提升很明显尤其是当转入YOLOv5或YOLOv8时坐标大于1的标签会导致loss异常上涨曲线图一开头就是一个大尖峰很多人第一反应是改学习率实际是数据问题。第二个是difficult目标。上面代码里已经把difficult1跳过但要注意这只是转换层面的选择。如果你做的是纯bus单类检测difficult里的目标要么严重遮挡要么非常小训练时硬学容易把模型带偏跳过反而干净如果未来要参加VOC评测对齐官方AP官方明确不计入difficult正样本保留它们毫无意义。所以跳过的行为不用犹豫。第三个是类别ID错位这是最隐蔽的翻车点。有些人写转换脚本时图省事直接把类别名按字母序排列然后取index比如把20类排序后bus从第6位变成第5位或者数据集包里有别人转好的txt但类别表丢失你按自己的表去读第5行正好是car训练出来的模型把所有公交都当小车。对付这种问题唯一可靠的办法是保留原始XML始终从XML重新生成txt并且生成后抽样打开labels下的txt人工看一眼第一列数字和框坐标量级别把黑匣子带到训练里。3.3 切分训练验证集沿用官方划分还是重新shuf转换完之后要决定train和val怎么切。VOC原始ImageSets/Main里已经有bus_train.txt和bus_val.txt优先沿用这两个文件原因前面说过官方划分能对齐论文Baseline并且train和val在场景上天然分离不容易出现训练时见过、验证时又见一次的相似图。从官方清单转成图片列表只需要两条命令grep 1$ ImageSets/Main/bus_train.txt | awk {print $1} train_imgs.txt grep 1$ ImageSets/Main/bus_val.txt | awk {print $1} val_imgs.txtbus_train.txt每一行末尾是1或-1grep 1$匹配以空格加1结尾的行也就是明确含bus的正样本awk {print $1}取第一列图片名。如果压缩包把ImageSets目录丢了或者官方清单本身不全才考虑全量重切。重切的常规做法是把trainval全部bus图片合起来shuf打乱后按比例劈开cat train_imgs.txt val_imgs.txt | sort -u all_bus_imgs.txt shuf all_bus_imgs.txt -o shuffled.txt total$(wc -l shuffled.txt) head -n $((total * 9 / 10)) shuffled.txt train_imgs.txt tail -n $((total - total * 9 / 10)) shuffled.txt val_imgs.txtsort -u先去重避免trainval里同一张图出现两次shuf是随机打乱保证分配不带顺序偏差9:1是单类小数据集常用的经验值样本本身少验证集不用留太大。重切的问题在于随机shuf不看场景可能把同一场景不同视角、同一车型不同车牌的相似图分到两边导致验证集虚高这一点留到第5章专门说。如果时间充裕建议两种切法都各自训练一次对比val mAP差异差异超过5个点基本可以判定数据和切分里有猫腻。切完之后顺手查一下两边有没有交集comm -12 (sort train_imgs.txt) (sort val_imgs.txt) | wc -l输出0才算干净输出大于0说明同一张图同时进了训练和验证必须回去清理。4. 配好data.yaml跑YOLOv8 Baseline三个必调参数4.1 组织成Ultralytics工程目录train/val与labels的摆放规矩YOLOv8用的是Ultralytics的目录约定图片和标签分开train与val分开不靠文件名区分。先建目录再把前面的清单并进去mkdir -p datasets/bus/voc/{images/{train,val},labels/{train,val}} while read img; do cp JPEGImages/${img}.jpg datasets/bus/voc/images/train/ lablabels/${img}.txt if [ -f $lab ]; then cp $lab datasets/bus/voc/labels/train/ else touch datasets/bus/voc/labels/train/${img}.txt fi done train_imgs.txt这段循环一边拷图片一边拷标签train_imgs.txt里是图片名不存在的标签文件用touch生成空文件保证每个图片都有对应txtUltralytics加载时不会因为缺标签而跳过整张图。val集同理把命令里的train换成val再跑一遍。这里有个容易忽略的点图片名带不带扩展名要和你实际文件名一致如果清单里是000001而你图片叫000001.jpg直接拼出jpg名是对的如果文件名里本身已含.jpg脚本会拼出jpg.jpg拷贝时报错。解压后先ls看一眼再写循环。4.2 data.yaml的nc与names1类检测为什么最容易写错索引Ultralytics用data.yaml描述数据集它是训练和验证两个阶段的入口路径和类别配置都在这一个文件里。最小可用配置如下path: /home/user/datasets/bus/voc train: images/train val: images/val nc: 1 names: 0: buspath我习惯写绝对路径Ultralytics在处理相对路径时偶尔会和当前工作目录拼接出奇怪的结果绝对路径省心。train和val相对path写不需要写images/train前面的完整路径。nc是类别总数单类就填1names列表的顺序和索引必须严格对应YOLO标签txt里的第一列数字。这里最常见的错误是数据集只有一个类别但names写成VOC全量20类或者txt里第一列是5而names只有[bus]一项训练要么报错要么把这些行当背景处理loss看着正常但val mAP永远接近0。转完格式后先抽查三四个txt再配data.yaml两个动作连在一起做减少错位概率。4.3 用yolov8n先跑通再换大模型imgsz/batch/epochs怎么定单类小数据集的Baseline不需要上大模型我用yolov8n起步训练命令如下yolo detect train \ datadatasets/bus/voc/data.yaml \ modelyolov8n.pt \ imgsz640 \ batch16 \ epochs120 \ patience20model指定预训练权重的名字yolov8n.pt是官方在COCO上预训练的最小版本单类bus这样的小任务足够起步先跑通看损失曲线再决定提升方向。imgsz640是显存和精度的平衡点bus这类目标不算极端小640能覆盖大部分场景batch设16如果你的显卡是T4这类16G显存yolov8n跑到32问题也不大batch大小影响的是BN统计和收敛稳定性显存允许的条件下大一点更好但不建议为了凑batch把imgsz降下来。epochs给120配合patience20早停即连续20个epoch验证集mAP没有提升就自动停小数据集一般跑不到120就停了不用死等。参数为什么这样选一句话说清单类bus数据集一般只有几百张训练图对比COCO级别的数据量模型容量和训练轮数都不能按通用模板来yolov8n容量小不容易过拟合120轮足够充分收敛。如果你去查YOLO不同版本的损失函数和网络结构会发现每个版本都有改动但对一个单类小数据集结构差异带来的影响远小于数据质量问题先跑出稳定曲线再谈换结构。训练完看runs/detect/train目录下的results.pngbox_loss和cls_loss两条曲线前30轮应该在快速下降如果loss不降反升先回头查标签而不是调学习率。5. 公交车检测的避坑清单从bus/truck混淆到小目标漏检5.1 bus被识别成truck与car用混淆矩阵定量看误检现象训练完跑验证集检测结果里公交被框成truck甚至car从单张可视化看好像还能解释但混在视频流里就是一路误报。原因VOC数据里bus、truck、car三类外观接近长途大巴和厢式货车正面看几乎没有区别要靠车身上的窗户、涂装和车顶轮廓细节才能分开。单类数据集只用bus做正样本truck和car的图全部被当成背景模型没有见过“长得像bus但不是bus”的反例自然倾向把相近外观全判成bus或随机判成相近类。解决第一步不是改网络而是看验证集输出的混淆矩阵。Ultralytics训练完会在runs/detect/val下生成confusion_matrix.png和归一化版本行代表真实类别列代表预测类别对角线是正确率最后一列background代表漏检。重点看bus那一行走进truck列的比例如果误检集中在相似类而不是background把验证时的置信度阈值从默认0.25提到0.4很多低置信误报直接就被滤掉代价是召回略降如果误检大量落在background那才是真正的漏检阈值提高只会更漏。血泪经验是先看混淆矩阵再调阈值不看矩阵就调参属于玄学调参。5.2 远距离小目标漏检imgsz加到960之前先算显存账现象在道路场景里远处驶来的公交只有几十个像素高整张图resize到640后目标区域连10x10都不到模型直接丢掉验证集mAP50看着还行一上实车视频就漏检。原因YOLO的输入尺寸决定了能感知的最小目标尺度imgsz640下小目标特征图上的响应很弱加再强的损失函数也救不回来。bus在VOC里的标注框通常偏大训练集里小目标样本本来就少模型没有见过足够多小尺寸bus。解决常用做法有两档。离线精度优先时把imgsz提到960或1280小目标响应明显改善但显存占用按面积增长imgsz从640到960同一batch的显存开销接近翻倍batch要跟着降。这里要算部署环境的账如果你后面用TensorRT在T4这类卡上做实时检测imgsz640时并发路数还够用提到960后单路耗时变长支持路数直接掉先想清楚系统瓶颈是精度还是帧率。另一种做法是离线切图检测把大图切成有重叠的patch分别检测再合并结果小目标问题缓解明显代价是后处理复杂度上了一个台阶。我一般建议先试着把imgsz提到960这档其它不动跑完看召回变化再决定要不要上切图。5.3 训练集验证集混入相似场景mAP虚高的隐患现象训练时loss稳定下降val mAP也一路涨到很高但把模型放在实际路段视频里一试表现远不如验证数字差距大到怀疑自己训练了个寂寞。原因验证集与训练集中存在高度相似的图。VOC是图像数据集不是视频流但同一场景多视角、同一车型同款式不同车牌的照片在随机重切时很容易一边分一半。模型在训练时已经记住了这类车的纹理和构图验证时等于开卷考试mAP虚高属于数据划分泄漏不是模型能力。解决优先沿用官方ImageSets里的bus_train.txt和bus_val.txt官方划分在设计时已经避开了同源图片这是最便宜的保障。如果必须自己重切用shuf之后至少看一眼两边文件名分布发现有大量同前缀或同场景的图按场景分组再划分不要把相似帧拆进两侧。这条建议对车辆类数据集尤其重要同一个岔路口的公交停在同位置角度稍有差别随机切分下几乎必然泄漏。判断泄漏的经验阈值是改切分方式后mAP波动超过5个点多半就是数据和划分有问题不是模型问题。6. 用混淆矩阵验收模型把eval做在发布之前6.1 从best.pt导出val指标和badcase训练结束后第一件事不是发模型而是对验证集做一次完整评测yolo detect val \ datadatasets/bus/voc/data.yaml \ modelruns/detect/train/weights/best.pt \ conf0.25这里conf0.25表示按实际部署阈值评估Ultralytics默认用更低的阈值收集全部预测算完整mAP两者各有用途。跑完去看runs/detect/val目录results.csv里有mAP50、mAP50-95、precision、recallconfusion_matrix.png给出误检结构。如果你要部署把best.pt导出为ONNX或TensorRT engine后再用同一份验证集跑一遍导出前后指标差超过零点几个点要查导出配置别拿PyTorch的mAP当上线指标。6.2 最后一道自检统计标签是否丢框和越界在训练前我还习惯对labels目录做两个快速检查成本几乎为零能拦下90%的转换事故awk {if ($21.000001 || $31.000001 || $41.000001 || $51.000001) print} labels/val/*.txt for l in labels/val/*.txt; do n$(wc -l $l); if [ $n -gt 4 ]; then echo $l $n; fi; done第一条awk检查中心点坐标和宽高有没有超过1超过说明归一化时分母用错或者原标注越界第二条统计单张图框数正常一张图里bus实例很少超过4个如果出现一批6个甚至10个框的txt多半是XML解析时把其它类别也写进来了。这两条命令在train和val上都跑一遍比训练完再去分析曲线节点要快得多。我自己的习惯是转换脚本跑完后立刻对原始XML实例总数和labels总行数做一次对账对不上的地方逐条看。记得有一次为了赶进度直接重切数据把相似场景图分进了训练和验证两侧val mAP好看得让人兴奋换官方划分后掉了6个点教训是数据划分这件事永远不要图省事乱打乱切。这个方案本身不复杂难的是把每一步都做扎实。希望帮到你。本文还有配套的精品资源点击获取
返回列表