
简介这份资源是面向计算机视觉初学者与目标检测实践者的YOLOv5自行车检测数据集源自PASCAL VOC 2012训练验证集专门抽取自行车类别可用于智能交通、安全监控等场景下的模型训练与算法验证。压缩包共1810个文件包含603张jpg原始图像、603个xml详细标注文件与604个txt标签文件xml记录边界框、类别及目标尺寸等信息txt则提供YOLOv5训练所需的简洁坐标与类别标注整体约77.49MB目录结构清晰便于直接转换后投入训练。目前已有846人学习下载适合希望快速上手目标检测项目、验证数据增强与迁移学习效果的读者。借助该数据集可完成从标签格式转换、模型配置到训练评估的完整流程并对照mAP、召回率等指标迭代优化为自行车检测系统的搭建提供扎实的数据基础。1. 从一堆 2009 年的老照片说起这份自行车数据集到底能干什么如果你手头正好有一批交通监控截图或者想给树莓派小车加一个看到自行车就减速的逻辑第一道坎往往不是模型结构而是没有标注好的数据。这份bicycle_VOCtrainval2012-自行车数据集.rar解决的就是这个问题它把 PASCAL VOC 2012 里所有含自行车的图像挑出来整理成一份单类别检测数据集同时给了 txt 和 xml 两套标签。文件名里那些2009_004444.jpg、2010_000462.jpg就是 VOC 原始的图像编号年份加序号一眼能看出数据来源。它适合三类人一是刚接触 YOLOv5、想跑通训练自己的数据集完整流程的新手二是做智能交通、共享单车停放识别、骑行安全监控的从业者需要一个干净的自行车单类基线三是拿它当模板把同一套转换脚本套到锥桶、行人等其他单类任务上。数据集本身不大但胜在标注规范、类别单一训练时不容易被多类别干扰收敛曲线也好看。下面我按先看懂标签 → 再转格式 → 配训练 → 排坑 → 进阶的顺序拆一遍。2. 拆开压缩包先看标签VOC xml 与 YOLO txt 的字段对照2.1 两种标签格式各自装了什么VOC 的 xml 是一份图像一个文件根节点annotation下面挂着filename、size宽高通道、以及若干个object。每个object里有name类别名、pose、truncated、difficult还有最关键的bndbox里面是xmin/ymin/xmax/ymax四个绝对像素坐标。这份数据集里name基本只会出现bicycle一个值所以类别映射表写起来很省事。YOLO 的 txt 则是一份图像一个 txt每行一个目标格式是class_id x_center y_center width height后四个值全部是归一化到 0~1 的相对值相对的是图像自身的宽和高。这就是两种格式最本质的差别xml 存绝对坐标、信息全txt 存相对坐标、喂给 YOLOv5 直接能用。很多人第一次转格式翻车就是忘了除图像宽高或者把xmax当成了宽。2.2 用脚本统计一下类别和框的分布在动手转格式之前我习惯先跑一遍统计确认没有空标注、没有越界框。下面这段脚本遍历所有 xml输出图像数量、目标总数、每张图的平均框数以及宽高比的分布区间。import os import glob import xml.etree.ElementTree as ET from collections import Counter xml_dir ./bicycle_VOCtrainval2012/Annotations xml_files glob.glob(os.path.join(xml_dir, *.xml)) cls_counter Counter() box_per_img [] ratios [] for f in xml_files: tree ET.parse(f) root tree.getroot() objs root.findall(object) box_per_img.append(len(objs)) for obj in objs: name obj.find(name).text.strip() cls_counter[name] 1 bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) w xmax - xmin h ymax - ymin if h 0: ratios.append(round(w / h, 2)) print(图像数:, len(xml_files)) print(类别分布:, dict(cls_counter)) print(平均每图框数:, round(sum(box_per_img) / len(box_per_img), 2)) print(空标注图像数:, box_per_img.count(0)) print(宽高比样本:, Counter(ratios).most_common(8))逻辑说明ET.parse逐文件解析findall(object)拿到所有目标cls_counter用来确认是不是真的只有 bicycle 一类如果冒出motorbike或person说明数据没筛干净得回头处理。box_per_img.count(0)是重点空标注图像如果混进训练集YOLOv5 会把它当纯背景图少量可以当负样本多了会拉低召回。宽高比分布则决定你 anchor 要不要重聚类——自行车普遍是扁长形比例集中在 1.5~3 之间和 COCO 默认 anchor 差异不小。参数说明xml_dir换成你解压后的实际路径ratios保留两位小数是为了看分布别拿它做精确计算。跑完如果发现空标注占比超过 5%建议单独列一个清单训练时用--noval之外的方式排除或者干脆删掉。3. 把 xml 转成 YOLOv5 能吃的 txt转换脚本与目录组织3.1 转换的核心逻辑与边界处理转换本身不复杂难的是边界。核心公式就三行x_center (xmin xmax) / 2 / img_wy_center (ymin ymax) / 2 / img_hwidth (xmax - xmin) / img_wheight (ymax - ymin) / img_h。但真实数据里总有xmax超出图像宽度、xmin xmax、坐标是浮点字符串这些情况不裁剪就会在训练时触发NaN或者框跑到图外。import os import glob import xml.etree.ElementTree as ET from PIL import Image xml_dir ./bicycle_VOCtrainval2012/Annotations img_dir ./bicycle_VOCtrainval2012/JPEGImages out_dir ./labels os.makedirs(out_dir, exist_okTrue) classes [bicycle] # 单类别class_id 固定为 0 for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text img_path os.path.join(img_dir, filename) # 用真实图像尺寸别信 xml 里的 size偶尔会对不上 with Image.open(img_path) as im: img_w, img_h im.size lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in classes: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 裁剪到图像范围内防止越界框 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue # 退化框直接丢 xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f0 {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) if lines: txt_name os.path.splitext(filename)[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines))逻辑说明先读真实图像尺寸而不是 xml 里的size这是血泪经验——VOC 里偶尔有 size 和实际图像对不上的样本用错尺寸会让所有框整体偏移。裁剪那四行是必须的min(xmax, img_w - 1)保证不越界。if lines:这一句让没有有效目标的图像不生成空 txt避免训练时读到空文件报错。坐标保留 6 位小数够用且不会让文件膨胀。参数说明classes列表决定 class_id 的映射顺序单类别就写[bicycle]将来加锥桶就往后追加顺序一旦定了别乱改否则旧标签全废。out_dir是输出目录建议和图像目录分开方便后面按 8:2 划分。3.2 目录结构怎么摆才不返工YOLOv5 对目录结构有约定摆错了训练脚本直接找不到图。常见做法是建一个datasets/bicycle下面分images/train、images/val、labels/train、labels/val四个子目录图像和标签同名不同后缀靠文件名对应。划分脚本我一般这么写# 假设所有图像在 JPEGImages标签在 labels mkdir -p datasets/bicycle/images/train datasets/bicycle/images/val mkdir -p datasets/bicycle/labels/train datasets/bicycle/labels/val # 随机抽 20% 做验证集 ls JPEGImages/*.jpg | shuf | head -n $(( $(ls JPEGImages/*.jpg | wc -l) / 5 )) val_list.txt ls JPEGImages/*.jpg | grep -v -f (sed s#JPEGImages/## val_list.txt) train_list.txt # 按清单搬运 while read p; do base$(basename $p .jpg) cp $p datasets/bicycle/images/train/ cp labels/$base.txt datasets/bicycle/labels/train/ done train_list.txt逻辑说明shuf打乱后取前 1/5 做验证grep -v -f取补集做训练保证不重叠。搬运时用basename去掉路径和后缀拼出对应的 txt 名。这一步别偷懒用mv保留原始 JPEGImages 一份后面调参想换划分比例还能重来。参数说明验证集比例 20% 是单类别小数据集的常用值数据量特别小可以降到 10%但别低于 5%否则 mAP 波动大到没法判断模型好坏。4. 配 YOLOv5 训练data yaml、模型选型与超参数4.1 写对 data yaml 和类别数YOLOv5 训练入口认的是 data yaml里面四个字段必须对train、val指向图像目录nc是类别数names是类别名列表。自行车单类别就写nc: 1names: [bicycle]。这里最常见的翻车是nc和标签里的 class_id 对不上——标签里全是 0nc却写了 80模型输出层维度错了训练能跑但 loss 不降。# datasets/bicycle/bicycle.yaml train: ./datasets/bicycle/images/train val: ./datasets/bicycle/images/val nc: 1 names: [bicycle]逻辑说明路径用相对路径时注意是相对于你执行训练命令的工作目录不是相对于 yaml 文件本身这点和很多框架不一样容易踩。names的顺序必须和转换脚本里classes的顺序完全一致。参数说明如果后面要加类别nc和names同步改同时旧标签的 class_id 也要重映射别只改 yaml。4.2 模型选型与关键超参数模型从yolov5s.pt起步最稳参数量小、在单类别任务上够用树莓派 4B 部署也扛得住。命令大致长这样python train.py \ --data datasets/bicycle/bicycle.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --cache逻辑说明--weights yolov5s.pt用 COCO 预训练权重做迁移学习单类别小数据集从零训几乎不可能收敛好。--img 640是输入尺寸自行车在监控图里往往偏小如果显存够可以上 960但推理速度会掉。--cache把图像缓存进内存小数据集能明显加快每个 epoch。参数说明--batch 16是 8G 显存的安全值爆显存就降到 8 或加--accumulate。--hyp选hyp.scratch-low.yaml是因为小数据集用低增强更稳默认的强增强mosaic、mixup 拉满在几百张图上容易过拟合到记图。--epochs 100配合早停实际往往 60~80 轮就收敛。提示第一次训练先把--epochs设成 3 跑通全流程确认数据加载、标签匹配、验证都不报错再开长训练。直接上 100 轮中途发现标签有问题白等几小时。5. 训练中的避坑与排查五条真实踩坑记录5.1 现象loss 一直是 nan 或 inf原因标签里有退化框宽或高为 0或者坐标越界导致归一化后出现除零或超出 [0,1]。解决回到第 3 章的转换脚本确认裁剪和if xmax xmin那两处判断生效再跑一遍统计脚本把所有w0或h0的样本列出来删掉。5.2 现象mAP 卡在 0.1 上不去原因多半是类别映射错了或者验证集和训练集图像重叠导致评估虚高后又崩。解决先确认nc和标签 class_id 一致再用diff比对 train/val 清单有没有交集。单类别任务正常 mAP0.5 应该能到 0.7 以上长期 0.1 基本是数据问题不是模型问题。5.3 现象训练时提示找不到标签原因YOLOv5 默认按图像路径把images替换成labels、后缀换成.txt去找标签如果你的目录不是这个命名它就找不到。解决要么严格按images/和labels/平行目录摆要么在 data yaml 里显式写labels路径。别用自定义的Annotations目录名直接喂。5.4 现象显存够但速度奇慢原因没开--cache每个 epoch 都在反复读盘解码或者--workers设成了 0数据加载单线程。解决小数据集加--cache--workers设成 CPU 核数的 2 倍左右比如 8 核设 8~16但别超过 16否则进程调度反而拖慢。5.5 现象验证集指标忽高忽低原因验证集太小几十张图里错一两个框mAP 就跳一大截。解决把验证集比例提到 20%或者用 K 折交叉验证取平均。单类别小数据集别指望单次评估稳定看趋势不看单点。6. 进阶从训练到部署以及一个验证模型是否真学到东西的技巧训练完拿到best.pt别急着上板子。先做两件事一是用detect.py在几张没参与训练的图上跑一遍肉眼看框准不准尤其是远处小目标和被遮挡的自行车二是用val.py单独跑一次验证集把--save-json打开拿到每个类别的 PR 曲线看召回在哪个置信度阈值下掉得最快。python detect.py --weights runs/train/exp/weights/best.pt \ --source ./test_imgs --img 640 --conf 0.25 --save-txt python val.py --weights runs/train/exp/weights/best.pt \ --data datasets/bicycle/bicycle.yaml --img 640 --save-json逻辑说明detect.py的--conf 0.25是默认阈值实际部署时这个值要按场景调——智能交通怕漏检就把阈值降到 0.15怕误报就提到 0.4。--save-txt把预测框存下来方便和真值对比。val.py的--save-json输出 COCO 格式结果能直接喂给 pycocotools 算更细的指标。参数说明--img必须和训练时一致训练用 640 推理用 960 会导致精度异常因为模型学到的特征尺度变了。如果非要改推理尺寸用--img 640 --half配合 TensorRT 导出别直接改。一个判断模型是不是真学到东西的技巧把验证集里所有含自行车的图按框面积从小到大排序挑最小的 20 张单独跑一遍。如果小目标召回明显低于大目标说明模型主要靠看到大片车架在判而不是学到自行车的结构特征。这时候要么提高训练分辨率要么在数据增强里加scale和小目标复制粘贴。我一般会在训练前就把这个小目标子集划出来训练完必跑一遍比看整体 mAP 更能暴露问题。从那以后我每次拿到新的单类别数据集都强制先跑一遍类别统计和退化框检查再开始转格式——省下的返工时间远比这几分钟多。希望帮到你。本文还有配套的精品资源点击获取