ARTICLE DETAIL

资讯详情

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

PASCAL VOC转YOLO格式实战:467张公交车数据集训练指南

PASCAL VOC转YOLO格式实战:467张公交车数据集训练指南 简介针对YOLO公交车检测任务此数据集由VOC2012训练验证集中的“bus”单类别图像筛选而成面向公交识别、交通监控与自动驾驶等场景可支撑YOLO系列模型的训练与评估。压缩包共1402个文件包含467张jpg原图、468个txt标签与467个xml标注文件txt记录边界框坐标与类别IDxml补充细致的目标实例信息整体大小55.82MB。467张图片均含有至少一个公交车实例场景和角度覆盖较广能为YOLOv3、YOLOv4等算法提供直接可用的训练语料也可作为专项评测基准。已有644人学习该资源两种标签格式省去手动转换成本可基于数据划分快速完成训练、验证与mAP指标调优。无论是学术实验还是工程落地这套数据都能为公交车检测系统的数据准备和效果验证提供完整支持。1. 拆这个公交车数据集467张VOC图、双标签与YOLO训练入口做交通监控或自动驾驶感知时最烦的不是模型选型而是手里没有一张带干净标注的公交车图片。这个 bus_VOCtrainval2012.zip 就是在 PASCAL VOC2012 的 trainval 全集里把所有不含 bus 的图像筛掉最终留下 467 张含公交车实例的图片和成对标签。它解决的不是“认识 VGG 还是 ResNet”而是“单类别、小数据量、能完整走一遍 YOLO 训练流程”这件事。适合两类人一类是刚碰 YOLO、想把标签格式、数据划分、训练、评估串起来的新手另一类是在用 YOLOv8/YOLOv5 做车辆检测、需要单独补一组公交车专项数据做微调的工程师。包内每张图同时带 txt 和 xml 标签实际转换时有两个坑比想象中深下面展开说。2. 两种标签的真实差异txt 负责快读xml 负责准确2.1 文件命名规律与单类别约束解压后你会看到一长串编号图片比如 2009_003018.jpg、2008_008080.jpg。这是 VOC2012 的原始文件名规则前四位是年份中间是采集流水号后两位是帧序号。文件名本身没有业务含义但它承担了一个关键职责——txt、xml、jpg 三者靠同名关联。这意味着后续做数据划分时只要保证三个文件的主文件名一致就不会出现标签对不上图的情况。这个数据集是单类别的原始 VOC2012 里有 20 类目标这个包只保留了 bus 相关的图像和 annotation。我在实际项目中遇到过一种误解有人以为“单类别”就是把 xml 里的其他 object 删掉但图片里仍然残留 car、person 等目标。这样训练出来的模型会在评估时把没标注的 car 也当成背景导致误检率虚高。所以拿到包第一件事是抽查 xml 的 object.name确保只有 bus。2.2 txt 标签格式拆读打开任意一个 txt你可以看到每行对应一个框字段顺序是图像文件名、左上角 x、左上角 y、右下角 x、右下角 y、类别 ID。注意这不是 YOLO 标准训练格式YOLO 标准格式是 class x_center y_center width height且坐标经过归一化。所以这份 txt 更像是“给人快速看的数据清单”不能直接丢进训练。一份真实的 txt 行可能长这样2009_003018.jpg 42 101 460 308 5我来拆一下这个字段42 和 101 是框左上角在原始图片中的像素坐标460 和 308 是右下角像素坐标5 是 bus 在 VOC 20 类里的索引。注意这个 5 是从 0 开始数的VOC 按字母序排列类别aeroplane0bicycle1bird2boat3bottle4bus5。这是一个非常容易写错的地方我见过有人把 bus 直接写成 6结果训练出来的模型把 car 当成 bus。字段含义示例文件名图像名称用于关联 jpg 和 xml2009_003018.jpgxmin左上角 x 像素坐标42ymin左上角 y 像素坐标101xmax右下角 x 像素坐标460ymax右下角 y 像素坐标308类别 ID目标类别索引5用这份 txt 做模型训练前必须先确认它是不是 YOLO 格式。如果是上面这种“文件名 左上角 右下角”的结构就得先做归一化转换。2.3 xml 标签格式拆读xml 是 PASCAL VOC 的标准标注格式信息量比 txt 大得多。除了框坐标它还记录了图片宽高、通道数、目标是否被截断、是否难检。最核心的是 size 节点和 object 节点size 里的 width 和 height 是归一化换算必需的参数object 里的 bndbox 给出绝对像素坐标。下面是一段典型的标注结构annotation folderVOC2012/folder filename2009_003018.jpg/filename size width500/width height335/height depth3/depth /size object namebus/name truncated0/truncated difficult0/difficult bndbox xmin42/xmin ymin101/ymin xmax460/xmax ymax308/ymax /bndbox /object /annotation解析这段 xml 时我一般直接读 size/width 和 size/height而不是从 txt 里取。因为有些图片被第三方工具重写过尺寸但 xml 没有同步更新以 xml 的 size 为准做归一化至少能保证转换脚本内部自洽。truncated 和 difficult 字段如果为 1在评估 mAP 时通常会被忽略这也是 VOC 评测的默认规则。提示txt 和 xml 并存时优先用 xml 做格式转换。txt 适合快速筛查数量xml 适合做严谨的标签解析两者不一致时以 xml 为准。3. VOC 坐标转 YOLO 归一化解析脚本与可视化验证3.1 YOLO 坐标格式与 VOC 坐标格式的本质区别YOLO 标签文件每一行只有五个数字类别索引、中心点 x、中心点 y、框宽、框高。前四个值全部归一化到 01 之间也就是除以图片宽高。这么设计是为了让模型不关心输入图片的绝对尺寸——同一个框在 1920×1080 和 416×416 下归一化后的值是相同的。而 VOC 标签是绝对像素值如果直接用 VOC 坐标训练 YOLO模型在不同分辨率下看到的目标尺度完全不一致损失函数会震荡到没法收敛。换算公式很直接cx (xmin xmax) / 2 / widthcy (ymin ymax) / 2 / heightw (xmax - xmin) / widthh (ymax - ymin) / height注意除数一定是图片的原始宽高不是框的宽高。之前有人把 width 写成了检测框的宽导致归一化结果全部变成 1 附近的大值训练直接跑飞。3.2 一份可跑的 VOC 转 YOLO 脚本我一般把这个脚本放在数据集根目录下Annotations 存 xmllabels 输出转换后的 yolo 标签。脚本支持自定义类别映射当前数据只有 bus所以把 bus 映射到 0# voc2yolo.py import os import xml.etree.ElementTree as ET def voc2yolo_one(xml_path, class_map, out_txt_path): # 解析 xml 文件 tree ET.parse(xml_path) root tree.getroot() # 读取图片真实宽高归一化必须用这两个值 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue # 跳过未在映射中的类别 cls_id class_map[name] 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) # YOLO 格式中心点 宽高全部归一化 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines) \n) if __name__ __main__: class_map {bus: 0} # 单类别数据集bus 映射到 0 os.makedirs(labels, exist_okTrue) for xml_name in os.listdir(Annotations): if not xml_name.endswith(.xml): continue stem xml_name[:-4] # 去掉 .xml 后缀保留主文件名 voc2yolo_one( os.path.join(Annotations, xml_name), class_map, os.path.join(labels, stem .txt) )这个脚本会遍历 Annotations 目录下所有 xml为每张图生成同名 txt 到 labels 目录。ET.parse 负责把 xml 加载成树结构root.iter(object) 遍历所有目标框而不遗漏嵌套节点。img_w 和 img_h 来自 size 节点是整个转换的关键如果这两个值读取错误后面所有坐标全部错位。注意labels 目录下生成的 txt 是 YOLO 训练格式不要和原包里的 txt 混在一起。原包 txt 在另一个目录内容里带着文件名YOLO 格式标签没有文件名两者结构完全不同。3.3 转换正确性的可视化验证格式转换后第一件事不是急着训练而是把生成的标签画回图片上肉眼确认框的位置。这一步能挡掉 80% 的坐标换算错误。我用 OpenCV 快速验证# check_labels.py import os import cv2 img_dir JPEGImages label_dir labels check_dir check os.makedirs(check_dir, exist_okTrue) for label_name in os.listdir(label_dir): stem label_name[:-4] img_path os.path.join(img_dir, stem .jpg) img cv2.imread(img_path) if img is None: print(f跳过无法读取的图片: {img_path}) continue h, w img.shape[:2] for line in open(os.path.join(label_dir, label_name)): cls, cx, cy, bw, bh map(float, line.split()) # 反归一化转回像素坐标系 x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, fbus:{cls}, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(os.path.join(check_dir, stem .jpg), img)这个脚本把归一化坐标乘以真实宽高换算成像素坐标去画矩形框。如果框偏上或偏下、宽度明显不对问题几乎出在归一化时除错了分母。我自己的习惯是抽 2030 张检查覆盖不同的图片分辨率确认高分辨率大图和低分辨率小图都能正确框住公交车。4. 数据划分与训练配置8:2 拆分、data.yaml 与 yolov8n 起步4.1 目录结构约定YOLO 训练对目录结构有强制约定images 和 labels 必须同级train 和 val 的图片、标签分别放在对应子目录下。标签后缀必须是 .txt且与图片主文件名完全一致。我常用的目录结构如下project/ ├── images/ │ ├── train/ │ │ ├── 2009_003018.jpg │ │ └── ... │ └── val/ │ ├── 2011_002227.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 2009_003018.txt │ │ └── ... │ └── val/ │ ├── 2011_002227.txt │ └── ... └── data.yaml注意 images 下放的是 JPEGImages 里的原图labels 下放的是转换脚本生成的 YOLO 格式标签。原包的 txt 和 xml 都不要直接放进这个目录YOLO 只认 labels 下的 .txt。4.2 划分训练集与验证集467 张图不算多我按 8:2 划分训练 373 张、验证 94 张。不推荐 9:1因为验证集只有 46 张mAP 波动会非常大。最关键的是加随机种子保证每次划分结果一致方便复现实验。下面这段脚本直接放在 data 根目录运行# split_data.py import os import random random.seed(42) # 固定随机种子保证可复现 img_dir JPEGImages imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(imgs) split int(len(imgs) * 0.8) train_imgs imgs[:split] val_imgs imgs[split:] def prepare(subset, names, dest): for name in names: stem name[:-4] # 分别拷贝图片和标签到对应目录 os.system(fcp {img_dir}/{name} images/{dest}/{name}) os.system(fcp labels/{stem}.txt labels/{dest}/{stem}.txt) os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) prepare(train, train_imgs, train) prepare(val, val_imgs, val) print(f训练集: {len(train_imgs)} 张, 验证集: {len(val_imgs)} 张)这里注意 labels 目录必须先存在并且里面有转换好的标签否则 prepare 阶段会静默失败。随机种子选 42 还是 0 都行但一旦选定了就别改否则换了数据集分布前后两次实验没法对比。4.3 data.yaml 与模型选择data.yaml 是 YOLO 的配置文件告诉训练器数据在哪、有几个类别。这个数据集只有一个 bus 类所以 nc 写 1names 列表只有一项# data.yaml path: /home/user/yolo-bus-dataset # 改成你的实际项目路径 train: images/train val: images/val nc: 1 names: [bus]path 字段指向项目根目录train 和 val 都写相对 path 的路径。names 的索引顺序要和标签里的类别 ID 一一对应这里 bus 映射到 0所以 names 第一项是 bus。如果以后往里面加 car需要把 car 排在 bus 后面并且重新生成标签。模型我建议直接从 yolov8n 起步。原因是数据量只有 467 张单类别任务不需要大模型yolov8n 参数量小、训练快跑通流程后如果精度不够再往 yolov8s 升。如果一上来就用 yolov8x显存占用大训练速度慢而且小数据很容易过拟合验证集 mAP 甚至会低于小模型。训练命令如下yolo detect train datadata.yaml modelyolov8n.yaml epochs100 imgsz640 batch16 device0epochs 我一般先给 100数据集小几十轮就能看到损失下降趋势。imgsz 设 640 是 YOLOv8 的默认训练尺寸也是速度和精度的平衡点。batch 16 在 8GB 显存左右能跑如果爆显存就改成 8同时把 imgsz 降到 512效果差别不会太大。提示第一次训练重点看 loss 是否下降不要一上来就追求 mAP。loss 不降大概率是标签或路径问题和模型无关。5. 翻车排查从标签转换到训练时最常见的五个坑5.1 图片读不出来训练时报 “Could not load image”现象训练刚开始几秒就报错提示某个 jpg 打不开或者 OpenCV 返回 None。原因VOC 原始包里有少量图片后缀是 .JPG 大写或者文件本身完整但编码格式特殊cv2 读不出来。另外我遇到过网络下载后文件被截断的情况。解决先统一后缀再用 OpenCV 全部扫一遍把读不到的文件单列出来。我一般跑这段做体检import os import cv2 bad [] for f in os.listdir(JPEGImages): img cv2.imread(os.path.join(JPEGImages, f)) if img is None: bad.append(f) print(f无法读取: {len(bad)} 张) with open(bad_images.txt, w) as out: out.write(\n.join(bad))如果 bad_images.txt 有内容直接把这些文件从划分列表里剔除同时删掉对应的标签文件否则训练时还是会报错。5.2 坐标越界生成的框有一部分超出图片现象验证图上画的框明显超出边界或者训练时损失在初期出现 NaN。原因xml 里的 bndbox 是基于原始标注的但某些图片在数据集制作过程中被裁剪过标注没有同步更新导致 xmax 或 ymax 大于图片宽高。归一化后坐标超过 1YOLO 的损失计算就会崩掉。解决在转换脚本里做一次 clamp把坐标限制在图片范围内x1 max(0, min(int((cx - bw / 2) * w), w - 1)) y1 max(0, min(int((cy - bh / 2) * h), h - 1)) x2 max(0, min(int((cx bw / 2) * w), w - 1)) y2 max(0, min(int((cy bh / 2) * h), h - 1))把反归一化后的像素坐标用 min 和 max 夹到 [0, w-1] 和 [0, h-1] 区间保证标签不越界。更稳妥的做法是在转换脚本里同步检查凡是 out of range 的框都打印出行号人工复核后再进训练。5.3 类别 ID 错位bus 被写成 6现象训练能正常跑验证集 mAP 也不低但模型预测出的类别标签和真实目标对不上比如把轿车检成公交车。原因VOC 的 20 类按字母序排列bus 的索引实际是 5不是 6。把 bus 写在 6 这个位置的人本质上是数了“第几个”而不是“索引值”。解决转换脚本的 class_map 直接写死对应关系class_map {bus: 5} # 如果按 VOC 原始索引走 # 或者 class_map {bus: 0} # 如果只训练单类别重新从 0 映射两种写法都行关键是你有一条明确的映射规则并且 data.yaml 的 names 顺序和它严格对应。单类别场景我倾向重映射到 0这样 names 里只有 bus更直观。5.4 包内自带 txt 和 YOLO 格式混淆重复转换现象转换脚本生成的标签看起来一切正常但训练时损失一直不降打印标签发现坐标值全是 0.xxx 的小数且类别和宽高错乱。原因部分从网上下载的 VOC 数据包txt 里已经是 YOLO 标准格式也就是 class cx cy w h你再用“文件名 左上角 右下角”的逻辑去解析就会把中心点当成左上角把宽高当成右下角。解决拿一个标签看前几行先判断格式再决定走哪套解析逻辑。head -3 2009_003018.txt如果第一行是“2009_003018.jpg 42 101 460 308 5”这种带文件名的是 VOC 格式需要转换如果第一行是“0 0.5020 0.5776 0.8360 0.6179”这种五个数字全在 01 之间的已经是 YOLO 格式直接拷贝到 labels 目录不要再跑转换脚本。5.5 训练时提示 No labels found in images/train现象训练日志里出现 warning显示训练集标签数量为 0训练进程继续跑但 loss 不下降。原因目录结构不对或者标签后缀不是 .txt或者 data.yaml 里 train 路径写错。最常见的是把标签放到了 labels/train 下但文件名后缀是 .xml 或 .txt 但内容是 xml 结构。解决做一次文件计数对比确认图片和标签数量一致ls images/train | wc -l ls labels/train | wc -l两个数字应该一致。如果标签多出来多半是转换脚本重复生成如果标签少去看缺的是哪几个文件检查它们的 xml 里到底有没有 object 节点。没有 object 的图片本来就不生成标签这种文件要单独挪走别留在训练目录里。6. 验证与权衡mAP 指标、数据增强取舍与后续扩展6.1 用验证集算 mAP而不是只看训练 loss训练结束后用验证集单独跑一次评估命令如下yolo detect val datadata.yaml modelruns/detect/train/weights/best.pt重点看两个指标mAP50 和 mAP50-95。mAP50 是 IoU 阈值 0.5 下的平均精度反映模型能不能把公交框出来mAP50-95 把阈值从 0.5 提到 0.95 逐步计算再平均对框的定位精度更敏感。同一份模型mAP50 可能到 0.92但 mAP50-95 只有 0.61这说明框的位置还不稳需要更多轮次或更精细的 anchor。指标含义当前数据集的合理预期mAP50IoU0.5 时的平均精度0.880.95mAP50-95IoU0.50.95 平均0.550.70Precision预测为正样本中真正例占比0.90Recall实际目标中被召回的比例0.85467 张的单类别数据集如果 mAP50 不到 0.85优先检查标签画框是否准确而不是调模型参数。标签问题在数据量小的时候会被放大。6.2 数据增强的取舍和扩展思路数据量小增强必须开但要有取舍。YOLOv8 默认带 mosaic 和 HSV 扰动mosaic 能把四张图拼一起增加样本多样性对公交车这种中大型目标有效。唯一要关掉的是 90 度旋转公交车是长条形转 90 度后宽高比严重失真模型会学到错误的形状先验。我用这段增参控制yolo detect train datadata.yaml modelyolov8n.yaml \ epochs200 imgsz640 batch16 device0 \ degrees0 fliplr0.5 hsv_h0.015 hsv_s0.7 hsv_v0.4degrees0 关闭旋转fliplr0.5 保留水平翻转hsv 微调避免过拟合但不要拉太狠否则颜色失真后公交车的黄绿色特征会被破坏。epochs 从 100 提到 200主要看验证 loss 是否还在降不降就早停。后续如果要提升泛化从完整 VOCtrainval2012 抽出所有含 bus 的图像补充进来或者用爬取公交监控截图做二次清洗标注都比只在这 467 张里反复调参有效。现在拿到任何单类别数据集我都会先跑一遍标签体检脚本检查坐标范围、类别映射和标签格式三件事确认没问题再开训练。这个习惯帮我避掉了至少一半的翻车现场希望也能帮到你。本文还有配套的精品资源点击获取
返回列表