ARTICLE DETAIL

资讯详情

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

VOC标注转YOLO格式实战:基于YOLOv8的公交车检测训练指南

VOC标注转YOLO格式实战:基于YOLOv8的公交车检测训练指南 简介面向公交车目标检测任务的数据集基于PASCAL VOC 2012训练验证集筛选而成只保留含有公交车的图像与标注适合用来训练和评估YOLOv3/v4/v5等实时检测算法也能服务于交通监控、车流统计、自动驾驶辅助等场景。压缩包共1402个文件内有467张jpg原图、468个txt标签和467个xml标注txt提供简洁的边界框坐标xml记录更精确的标注细节便于转换格式或核对数据整体大小仅55.82MB轻量方便。已有644人学习使用。对目标检测学习者和工程开发者来说这套数据集免去了从VOC全量数据里挑图、转格式的重复劳动拿到手即可配置YOLO完成训练与验证同时可借两种标注文件深入理解YOLO的网格化预测和边界框回归原理实验中还能用验证集统计mAP等指标是公交场景检测任务中可直接上手的基准数据。1. 这个 zip 到底改了什么从 VOC 标注到 YOLO 格式的一次偷懒拿到bus_VOCtrainval2012.zip这个名字第一反应应该是这包数据不是给你送去训练 VOC 分类器的而是已经帮你把 PASCAL VOC 2012 里公交车相关的图像和标注重新整理成了 YOLO 系列模型能直接吃的格式。很多人卡在yolov8训练自己的数据集第一步不是模型不会跑而是标注文件不对——原版 VOC 给的是 xmlYOLO 要的是 txt格式不对训练器连数据都读不进去。这个 zip 解决的就是这个衔接问题它保留了 VOCtrainval2012 的原始图像划分但把标注转换成了 YOLO 的归一化坐标格式。适合谁适合想快速验证公交车检测方案、又不想花一晚上写转换脚本的人。也适合刚入门 YOLO、需要一份“标注没错”的干净数据来跑通全流程的初学者。2. 从 VOC 的 xml 到 YOLO 的 txt转换脚本与归一化坐标2.1 VOC 标注到底长什么样YOLO 又想要什么VOC2012 的标注文件是 xml 格式核心信息是一组object节点每个节点里有name和bndboxbndbox 里是 xmin、ymin、xmax、ymax 四个绝对像素坐标。比如一张 1024x768 的图里有一辆公交车xml 里可能写着 xmin100, ymin200, xmax600, ymax700。这个格式人眼看得懂但 YOLO 训练器不认。YOLO 系列包括 YOLOv5、YOLOv8、YOLO11要的标注是每个目标一行纯文本格式是class_id center_x center_y width height注意这里的 center_x、center_y、width、height 全部是相对于图片宽高的归一化值范围在 0 到 1 之间。拿上面那辆车举例中心点 x (100 600) / 2 350归一化后是 350 / 1024 ≈ 0.3418中心点 y (200 700) / 2 450归一化后是 450 / 768 ≈ 0.5859宽 600 - 100 500归一化后是 500 / 1024 ≈ 0.4883高 700 - 200 500归一化后是 500 / 768 ≈ 0.6510。所以对应行是5 0.3418 0.5859 0.4883 0.6510。这里 class_id 是 5不是 0 也不是 1。为什么因为 PASCAL VOC 的 20 个类别有固定排序aeroplane0, bicycle1, bird2, boat3, bottle4,bus5然后是 car6、cat7 以此类推。这一步最容易翻车——有人把 bus 当成第 0 类训练出来的模型把公交车框出来但标签全错或者干脆不收敛。如果你的项目里 bus 的类别编号不是 5记得改。2.2 一个能用的 xml 转 txt 脚本常见做法是写一个 Python 脚本遍历Annotations目录下的所有 xml解析出目标框再按 YOLO 格式写入同名 txt。下面这个脚本我一般在拿到 VOC 数据后直接跑几分钟就能把整个 trainval 集转完import xml.etree.ElementTree as ET import os # VOC 20类的固定顺序bus 在第5位从0开始数 VOC_CLASSES [ aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor ] def convert_xml_to_yolo(xml_path, output_dir, img_width, img_height): 解析单个VOC xml文件输出YOLO格式的txt标注。 img_width/img_height 必须和实际图片尺寸一致否则归一化全错。 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in VOC_CLASSES: continue # 跳过VOC以外的类别 class_id VOC_CLASSES.index(name) 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) # 计算中心点和宽高再归一化到[0,1] center_x ((xmin xmax) / 2) / img_width center_y ((ymin ymax) / 2) / img_height box_width (xmax - xmin) / img_width box_height (ymax - ymin) / img_height # 过滤掉宽高为0的异常标注 if box_width 0 or box_height 0: continue lines.append(f{class_id} {center_x:.6f} {center_y:.6f} {box_width:.6f} {box_height:.6f}) if not lines: return # 没有有效目标时不生成空txt避免训练时误判 output_path os.path.join(output_dir, os.path.basename(xml_path).replace(.xml, .txt)) with open(output_path, w) as f: f.write(\n.join(lines)) # 使用示例遍历Annotations目录 if __name__ __main__: xml_dir VOCdevkit/VOC2012/Annotations out_dir VOCdevkit/VOC2012/labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue # 实际使用时需要从JPEGImages读取对应图片的宽高这里用占位值 # 正确做法是调用PIL或cv2读图获取宽高 convert_xml_to_yolo( os.path.join(xml_dir, xml_file), out_dir, img_width1024, # 占位必须换成实际图片宽高 img_height768 )逻辑说明脚本核心是第三个参数和第四个参数——图片宽高。如果拿占位宽高去归一化真实图片坐标会整体偏移模型训练时 loss 降不下去但也不爆 NaN属于“看起来很顺利、实际全错”的隐蔽翻车。正确做法是先读图片拿宽高再转标注。另一个细节是空标注处理一张图如果 xml 里没有目标脚本不生成 txt这在 YOLO 里是允许的——训练时这张图会被当成背景图参与负样本学习。但如果你转出来一堆空 txt要检查是不是类别过滤写错了。2.3 转换后必做的三件事转完别急着训练。第一件事是随机打开几个 txt 和对应图片肉眼看坐标对不对画个框对比原图确认框是贴在车身上而不是偏移半个车身。第二件事是统计类别分布数一数 bus 类有多少个框如果只有几百个训练出来的模型很容易过拟合后面要加数据增强。第三件事是检查图片文件名和 txt 文件名是否一一对应YOLO 训练时是按前缀匹配 image 和 label 的文件名差了哪怕一个字符这张图就被静默跳过。3. 训练前先验证目录结构三类文件配对检查与标注体检3.1 标准目录布局与 train/val 划分bus_VOCtrainval2012.zip解压后典型目录结构应该长这样如果作者按常见习惯打包bus_VOCtrainval2012/ ├── images/ │ ├── train/ │ │ ├── 2007_000027.jpg │ │ └── ... │ └── val/ │ ├── 2007_000129.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 2007_000027.txt │ │ └── ... │ └── val/ │ ├── 2007_000129.txt │ └── ... └── data.yaml注意2007_000027.jpg这个文件名——PASCAL VOC 的数据其实混合了 2007 和 2012 两个年份的采集内容VOCtrainval2012 这个集合里经常能看到 2007 前缀的文件这是正常的不影响训练。data.yaml 是给 YOLO 训练入口用的内容大致是path、train、val三个字段加上names类别列表。如果压缩包里没有 data.yaml自己建一个也很简单五秒钟的事。train/val 的划分比例常见做法是 8:2 或者 9:1。VOCtrainval2012 官方划分已经把 trainval 拆好了但转换成 YOLO 格式后很多人会重新按 9:1 随机划分一次目的是让 val 集和 train 集在场景分布上更均匀。注意重新划分时一定要保证图片和标签同步移动别图过去了 txt 没过去。3.2 标注体检脚本检查越界、空文件、类别漂移训练前跑一遍标注体检能省掉后面好几个小时的排错时间。下面这个脚本专门挑标注问题我在 yolov8 训练自己的数据集 之前必跑import os from PIL import Image IMG_DIR images/train LABEL_DIR labels/train CLASS_NAMES [bus] # 按实际类别数改 # 1. 检查图片与标签是否一一对应 img_files set(f.split(.)[0] for f in os.listdir(IMG_DIR)) label_files set(f.split(.)[0] for f in os.listdir(LABEL_DIR) if f.endswith(.txt)) print(f图片数: {len(img_files)}标签数: {len(label_files)}) print(f缺少标签的图片: {len(img_files - label_files)} 张) print(f没有图片的标签: {len(label_files - img_files)} 个) # 2. 逐行检查坐标是否越界或为负 for label_file in os.listdir(LABEL_DIR): path os.path.join(LABEL_DIR, label_file) img_name label_file.replace(.txt, .jpg) img_path os.path.join(IMG_DIR, img_name) if not os.path.exists(img_path): continue # 用PIL拿真实宽高 with Image.open(img_path) as im: w, h im.size with open(path) as f: for line_num, line in enumerate(f.readlines(), 1): parts line.strip().split() if len(parts) ! 5: print(f{label_file}:{line_num} 字段数不对: {line.strip()}) continue cls, cx, cy, bw, bh parts cx, cy, bw, bh float(cx), float(cy), float(bw), float(bh) # 归一化坐标换算回像素值 xmin (cx - bw/2) * w ymin (cy - bh/2) * h xmax (cx bw/2) * w ymax (cy bh/2) * h if xmin 0 or ymin 0 or xmax w or ymax h: print(f{label_file}:{line_num} 越界: xmin{xmin:.1f}, ymin{ymin:.1f}, xmax{xmax:.1f}, ymax{ymax:.1f}, 图宽{w}, 图高{h})逻辑说明这个脚本做三件事——查配对、查字段数、查越界。最容易被忽略的是越界检查。转换脚本在归一化时如果遇到浮点精度问题或者 xml 里本身就标出了图片边界生成的 txt 里可能出现 1.02 这种大于 1 的坐标。YOLO 训练时读到这种标注不会报错但会当作非常离谱的目标框参与损失计算轻则拉高 loss重则让模型学出偏移的框。字段数检查也很关键YOLO 的 txt 格式每行必须正好 5 个值多一个少一个都会导致训练中断或整行被跳过。3.3 用可视化脚本最后看一眼标注体检脚本过了不代表标注质量就高。最靠谱的验证方式是画框可视化——随机抽 50 张图把标注框画上去人眼扫一遍。这一步不用写复杂的可视化代码直接用 OpenCV 的 rectangle 画框就能看重点看三类问题框是不是紧贴车身VOC 原始标注偶有框偏大的情况有没有漏标图里明显有公交车但没框重名文件导致框画在错误的图上。4. 用 YOLOv8 把 bus 类训起来配置、损失函数与 Efficient Head4.1 data.yaml 与训练命令的最小配置当你确认标注没问题下一步就是写 data.yaml 并启动训练。这里给一个最少字段的配置# data.yaml path: /your/absolute/path/bus_VOCtrainval2012 train: images/train val: images/val # names 列表的顺序必须和 txt 里的 class_id 一一对应 names: 0: bus然后启动训练yolo detect train \ modelyolov8s.pt \ datadata.yaml \ imgsz640 \ epochs100 \ batch16 \ workers4 \ device0 \ patience20 \ projectruns/detect \ namebus_exp1参数说明imgsz640是 YOLO 系列的默认输入分辨率公交车属于中大目标640 够用往上提到 960 对远处小目标有改善但显存占用翻倍epochs建议 100 起步VOC 数据量不大100 轮足够收敛过拟合反而是主要风险patience20是早停机制连续 20 轮 val loss 不降就自动停能省时间batch16要看显存8G 显存跑 YOLOv8s 的话 batch 设 8 更稳。device0指定第一张 GPU没有 GPU 的用devicecpu但训练时间会很长新手建议直接上云 GPU 或者用 colab。4.2 YOLO 损失函数对公交车检测的影响YOLOv8 的损失函数由三部分组成box loss默认 CIoU、cls loss二元交叉熵、dfl loss分布焦点损失。对公交车检测来说重点调的是 box loss 的权重。公交车的特点是宽高比极端侧面看是扁矩形、遮挡严重公交站台场景多车重叠、尺度单一但大。CIoU 本身对长宽比敏感如果训练时发现 loss 曲线降得慢或者最终 mAP 不高可以试试把 box loss 的权重从默认的 7.5 调低到 5给分类损失更多权重让模型先学会“认得出公交车”。# 在 YOLOv8 的模型配置里调整 loss 权重或者用命令行参数 # yolo detect train ... box5.0 cls1.5 dfl0.5这里提一下nwd 改进 yolo这个方向NWDNormalized Wasserstein Distance损失专门针对小目标和遮挡场景设计它用高斯分布建模目标框对重叠和尺度变化更鲁棒。如果你的数据集里公交车经常被树、行人、其他车辆挡住一半CIoU 算出来的损失可能不准确——因为 CIoU 对框的重合度非常敏感遮挡时两个框明明是同一个人眼能识别的目标但 IoU 很低导致梯度惩罚过重。NWD 可以替代 CIoU 作为 box loss实现方式是在 loss 计算函数里加一个分支训练时选用 NWD 计算回归损失。这个改进对 640 分辨率下远处的小公交车尤其有效。4.3 Efficient Head 加在 YOLOv8 的哪个位置efficient head yolo是最近讨论度挺高的一类轻量化检测头改进思路是去掉原 YOLOv8 检测头里的冗余卷积用共享参数的分组卷积或深度可分离卷积替代。具体到实现层面YOLOv8 的检测头是解耦头——分类分支和回归分支各走各的卷积。Efficient Head 的常见做法是把两个分支的前几层共享只在最后几层分开这样参数量能降 20% 左右推理速度快一截。公交车检测是单类检测任务对检测头的表达能力要求不高Efficient Head 这种瘦身方案在这个场景下效果显著——模型变小、速度变快精度几乎不掉。提示改检测头不是改配置就能完事的需要动模型定义文件。YOLOv8 的模型结构在ultralytics/nn/modules/head.py里Detect 类的 forward 方法定义了特征图处理流程。如果你只是想把数据跑通先别碰检测头等基线模型训出来、确认精度满足需求再考虑用 Efficient Head 做加速。5. 训练公交车检测的五个常见坑现象、原因与解决5.1 坑一loss 正常下降但 val 集 mAP 极低现象训练日志里 box loss 和 cls loss 都在降看起来一切正常但 val 集的 mAP0.5 只有 0.1 左右。原因最常见的是 class_id 对错了。VOC 的 bus 在类别列表里的索引是 5如果你把 bus 当成第 0 类txt 里写着0 x y w h模型会把 bus 学成第 0 类但 val 集的真实标签如果你自己用另一套脚本生成用的又是5两边对不上mAP 必然崩。解决用第 3 章的体检脚本统一检查 train 和 val 两个目录里 txt 的 class_id 范围必须小于names列表长度。然后把第 1 行的类别号打印出来和 data.yaml 里的 names 对照看。5.2 坑二训练时图片被静默跳过总数对不上现象启动训练时日志显示train: 4000 images但你的 images/train 目录里明明有 5000 张图。原因YOLO 训练时按图片前缀匹配同名 txt。有 1000 张图的 txt 文件不在 labels 目录里可能因为转换脚本的过滤逻辑也可能因为文件名大小写不一致这些图就被当成无标签图片跳过了。解决把 labels 和 images 分别ls出来用comm -23或 Python 集合差集找出缺少标签的图片。如果是转换时漏了重新补转如果是原本就没目标确认这些图确实是空场景再保留。5.3 坑三训练到一半 loss 突然变 NaN现象第 30 轮之前 loss 正常第 31 轮开始 loss 变成 nan之后一直是 nan。原因大概率是数据里有损坏图片或异常标注。YOLOv8 的数据加载器会把读取失败或标注数值异常的样本过滤掉但有时读取失败发生在训练中期比如内存压力大、文件 IO 异常或者标注里有inf这种非正常数值。解决用 PIL 遍历所有图片做一次“能否打开并转成 RGB”的检查损坏的直接剔除。同时用脚本检查 txt 里有没有inf、nan字符串有就删掉对应行。如果还是 NaN把 batch 调小一半再试排除显存溢出的可能。5.4 坑四公交车和卡车、小汽车混淆严重现象模型把公交车框出来了但置信度只有 0.3–0.5在包含卡车、客车的场景里框会跳来跳去分类标签在 bus 和 car 之间反复横跳。原因VOC 数据集里 bus 类的样本量本身就少trainval2012 全部图片约 5700 张其中含 bus 的图可能只有几百张。在类别不平衡的情况下模型很容易把“大车”这个视觉特征学到位但分不清 bus 和 truck 的边界——两者视觉上确实很像都有大车厢、多轮子、方正的轮廓。解决第一招是针对性做数据增强——随机旋转 ±10 度、裁剪缩放 0.5–1.5 倍、HSV 色域扰动把有限的 bus 样本“变形”出更多变体。第二招是去网上找公开的公交车图片补标注常见做法是拿 OpenImages 数据集的 bus 类做补充。第三招是降低置信度阈值val 时用conf0.1看实际召回率如果召回率上来了只是精确率低说明分类边界模糊重点调分类损失权重。5.5 坑五验证集 mAP 高但实际视频场景里框不住公交车现象在 VOC 测试集上 mAP 有 0.85看起来很漂亮但拿到真实道路视频里公交车一进画面就漏检或者框偏到旁边的树上去。原因VOC 数据是 2008–2012 年拍摄的画面里公交车的特点是车身完整、光照均匀、背景多为欧洲城市街道。真实部署场景里公交车可能被遮挡、车身上有大幅广告、天气是雨天或夜晚这些在 VOC 里都少见。所谓“训练集分布和测试集分布不一致”mAP 高不代表部署能用。解决把数据集按场景拆分——白天晴天一批、傍晚一批、夜间一批分别跑 val 看每个子集的 mAP。如果夜间那批 mAP 掉得厉害去补充夜间公交车图片做第二轮微调。微调时用小学习率lr00.001、冻结 backbone 只训检测头避免灾难性遗忘。6. 验证不只是 mAP混淆矩阵、PR 曲线与 Efficient Head 落地6.1 用 val 命令跑出完整指标训练完别只盯着 mAP 一个数。用下面的命令评估然后打开results.png和confusion_matrix.png仔细看yolo detect val \ modelruns/detect/bus_exp1/weights/best.pt \ datadata.yaml \ conf0.25 \ iou0.5 \ imgsz640打印出来的指标要按优先级看第一看混淆矩阵里 bus 到底和谁的混淆度高——是 background漏检还是 car误检。第二看 PR 曲线在召回率 0.8 附近有没有“膝盖”——如果召回率一超过 0.7 精确率就断崖下跌说明模型对 partial occlusion部分遮挡场景没有泛化能力。第三看不同 confidence 阈值下的 F1 分数找到这个模型实际部署时最合适的 conf 阈值——我常用做法是取 val 集 F1 最高的那个阈值YOLOv8 训练日志会自动在results.csv里记录每一轮的最优结果。验证完如果效果满意Efficient Head 的落地就顺理成章了。单类检测场景下把 YOLOv8s 的检测头换成 Efficient Head常见做法是只在模型定义文件里替换 Detect 类然后正常训练 50 轮、用同样的 val 命令对比 mAP。如果 mAP 掉点不超过 0.02但推理帧率从 60 提到了 75这笔交换就值得做。最后说一个我的习惯拿到任何数据包不管名字多正规第一件事不是解压训练而是花 30 分钟做标注体检——数类别、查越界、看配对、画框目测。这个习惯帮我躲过了无数次“训练了三天发现标签全错位”的翻车。很多看起来像是模型调参的问题根源其实在数据进模型之前就埋下了。希望帮到你。本文还有配套的精品资源点击获取
返回列表