ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

559张三轮车数据集:YOLO/VOC双格式目标检测训练实战

559张三轮车数据集:YOLO/VOC双格式目标检测训练实战 简介数据集包含559张三轮车实景图片配套Pascal VOC与YOLO两种格式的标注文件由labelImg手工绘制矩形框完成类别统一为tricycle共659个标注框。面向计算机视觉入门学习者和目标检测算法研究人员尤其适合需要快速准备训练数据、验证模型效果的场景。资源包内共1679个文件以jpg图像、xml标注和txt标注三类为主其中VOC格式便于使用传统检测工具YOLO格式可直接接入Darknet、YOLOv5等训练流程压缩包约25.22MB轻量易下载。目前已有642人学习下载。数据集中仅对三轮车单类别进行框标注标注信息准确合理可直接用于模型训练、精度对比或数据增强实验省去自行采集和清洗数据的环节。1. 559 张三轮车数据集一份能直接喂给 YOLO 的交通类样本库搞目标检测的人应该都经历过这种尴尬教程看懂了代码也跑通了却找不到一份合适的数据集把手里的模型跑出第一条正常的 loss 曲线。这份三轮车数据集就是用来干这个的——559 张 jpg559 张同名 Pascal VOC 的 xml559 张同名 YOLO 的 txt单类别 tricycle用 labelImg 手工画框总共 659 个标注框。平均每张图里有 1 个出头一点的目标比那种单张单框的玩具数据集要真实比动辄上万张的公开大集又好上手得多。它没有预置 train/val 路径清单需要自己动手划分正好把“如何训练自己的数据集”这套流程完整走一遍。适合想跑通 yolov8/yolov5 训练闭环的入门者也适合拿真实交通样本做迁移学习微调的人。数据量不大但标注格式里的坑一个不少。2. 双格式交付VOC 的 xml 与 YOLO 的 txt 怎么对齐2.1 目录结构与文件命名规律拿到数据集先别急着训练先认清手里有什么。文件就三类tricycle_xyxr_xxx.jpg、tricycle_xyxr_xxx.xml、tricycle_xyxr_xxx.txt同名不同后缀。命名里的tricycle_xyxr是统一的后面的数字编号从个位数到五百多都有应该是采集时按顺序落盘的。这样设计的最大好处是省掉了匹配环节jpg、xml、txt 三件套靠文件名一一对应后面的检查脚本也能直接用文件名做主键去比对。文件类型数量坐标体系说明jpg559无RGB 原始图片三通道xml559VOC 像素绝对坐标Pascal VOC 格式xmin/ymin/xmax/ymaxtxt559YOLO 归一化坐标class_id 中心点 x/y 宽/高均除以图片宽高类别上只有 1 个tricycle总框数 659也就是所有标注框都落在这个类上。摘要里特意说明“不包含分割路径的 txt 文件”意思是没有常见的train.txt、val.txt这种记录图片路径清单的文件——它交付的是干净的标注划分得自己来。这个设计其实很合理因为每个项目对训练集/验证集的比例和划分方式要求不一样预置路径清单反而绑手绑脚。打开一个 xml 看内部结构节点就是标准 VOC 字段annotation根节点下面挂folder、filename、size、object。object里的name是类别名bndbox里存的是左上角和右下角的像素坐标。对应 yolo 的 txt 则是每行五个浮点数class_id x_center y_center width height其中后四个值全部是相对图片宽高的比例取值在 0 到 1 之间。这两个体系看着简单混着用就出事——VOC 给的是绝对像素值YOLO 给的是相对值同一个框在 xml 里写xmin512/xmin在 txt 里可能就变成0.5333 0.4200 0.1200 0.2500。2.2 两个格式的坐标换算手工对一遍更放心为了确认两份标注描述的是同一个框常见做法是自己写个小脚本把两种格式读出来画到同一张图上对比。这步不是浪费时间新手阶段最容易犯的错就是把x_center当成xmin直接用结果框全部漂移到左上角。下面这个脚本就是干这个的import cv2 import xml.etree.ElementTree as ET def read_voc_box(xml_path): tree ET.parse(xml_path) root tree.getroot() boxes [] for obj in root.findall(object): name obj.find(name).text bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) boxes.append((name, (xmin, ymin, xmax, ymax))) return boxes def read_yolo_box(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() cls, cx, cy, bw, bh (int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])) xmin int((cx - bw / 2) * img_w) ymin int((cy - bh / 2) * img_h) xmax int((cx bw / 2) * img_w) ymax int((cy bh / 2) * img_h) boxes.append((cls, (xmin, ymin, xmax, ymax))) return boxes img_path tricycle_xyxr_341.jpg img cv2.imread(img_path) h, w img.shape[:2] for _, box in read_voc_box(tricycle_xyxr_341.xml): cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) for _, box in read_yolo_box(tricycle_xyxr_341.txt, w, h): cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), 2) cv2.imwrite(check_341.jpg, img)逻辑说明read_voc_box把 xml 里的object全取出来坐标原样返回read_yolo_box则是把归一化的中心点坐标和宽高还原成像素坐标用的是中心点转左上角的经典公式(cx - bw / 2) * img_w。绿框是 VOC 读出来的红框是 YOLO 读出来的如果同一辆三轮车两个框严丝合缝说明这份 txt 的换算逻辑和 xml 对得上。参数说明img_w和img_h必须来自同一张图的真实尺寸否则框会整体偏移parts里第一个值转int是类别 id后面四个转float如果全部用整数除法小于 1 的归一化坐标会直接变 0框全部缩在左上角。我建议抽查至少 10 张不同编号的图而不是只看一张。2.3 用前先盘点文件完整性检查脚本训练时报“cant open image”或者“no labels found”十有八九是文件缺失或路径写错而不是模型的问题。我一般先写一个盘点脚本建立基线确认三个目录数量一致且没有配不上对的孤儿文件import os from pathlib import Path root Path(tricycle_dataset) jpg_files sorted(root.glob(*.jpg)) xml_files sorted(root.glob(*.xml)) txt_files sorted(root.glob(*.txt)) print(fjpg: {len(jpg_files)}, xml: {len(xml_files)}, txt: {len(txt_files)}) name_jpg {p.stem for p in jpg_files} name_xml {p.stem for p in xml_files} name_txt {p.stem for p in txt_files} print(缺xml:, name_jpg - name_xml if name_jpg - name_xml else 无) print(缺txt:, name_jpg - name_txt if name_jpg - name_txt else 无) print(多xml:, name_xml - name_jpg if name_xml - name_jpg else 无) print(多txt:, name_txt - name_jpg if name_txt - name_jpg else 无)检查逻辑分两层第一层用glob按后缀把所有文件捞出来排序打印三个数量正常都是 559第二层对文件名取stem做集合差jpg 缺对应 xml 或 txt 会直接打印出来。反过来xml 和 txt 比 jpg 多出来的部分说明原始目录里有残留标注文件训练前最好清理掉否则打乱后可能出现同名的旧标注覆盖新标注。参数说明root指向数据集根目录脚本默认文件都在一层目录里如果后续你把图片和标注分到images/、labels/子目录把glob(*.jpg)改成glob(images/*.jpg)即可。划分完训练集之后建议对两个子集各自再跑一遍这个脚本确保增强、剪切、移动文件后三件套依然完整。这种“分完再查一遍”的习惯能挡住后面至少一半的玄学报错。3. 手工标注复盘labelImg 的配置习惯与被忽略的边界3.1 标注工具的配置与操作习惯这份数据集的标注工具是 labelImg一个开源标注工具被用得非常广。很多刚接触目标检测的人以为数据集是从天而降的实际上像这种小样本数据集大部分就是拿 labelImg 一框一框画出来的。把工具的行为摸透后面用起来才不虚。labelImg 默认在PascalVOC模式左下角显示 1 的地方就是格式切换按钮点一下会在PascalVOC和YOLO之间切换。这里有个非常经典的坑如果你先以 VOC 模式画了一部分中途切到 YOLO 模式再保存VOC 模式生成的 xml 还在但下次打开时标注框会读不到因为两种模式的坐标存储结构完全不一样。我一般建议拿到这类双格式数据后把目录里所有文件都假定为已按 labelImg 的默认规则落盘xml 和 txt 与图片同名、同目录类别名读的是classes.txt或predefined_classes.txt名字顺序直接决定 txt 里 class_id 的顺序。对这个数据集来说只有 tricycle 一个类所以 txt 里所有行的第一个数字都应该是 0如果出现 1十有八九是标注时类别列表顺序出了问题。标注规则本身不复杂对属于 tricycle 的目标画一个紧贴车体外轮廓的矩形框从左上角拉到右下角尽量别把路沿、行人、阴影包进去。难处理的是两类情况一是三轮车被其他车或树遮挡这种我一般按可见部分画框框紧贴可见边缘二是远处的小目标整辆车在图中可能只有二三十个像素高这种也要标哪怕最后训练时它很难被检出来漏掉反而会让模型学到“小的不用管”的错误先验。3.2 框数分布659 个框背后的标注密度摘要里统计了每个类别的框数tricycle 659 框559 张图平均每张 1 18 个框。这个密度说明大部分图是单目标但也有相当一部分图是两辆、三辆三轮车同框。用框数反推标注场景可以判断这是真实道路采集数据不是摆拍因为真实街景里目标分布就是离散的偶尔扎堆。这个平均框数还能帮你判断要不要做数据增强。如果目标检测里平均每张只有 1 个目标模型在训练时正样本数量就少mAP 容易波动。常见做法是先用 659 这个数估算正样本总量再决定离线增强的倍数——比如做 2 倍增强让每张图平均有 2 个以上的样本参与训练类别不平衡的焦虑会小很多。但注意增强必须同步修改标注文件这个问题后面专门讲。另外659 个框也可以用来做标注质量抽检的基准如果你自己重新统计一遍发现某张 xml 里的框数比图上肉眼可见的三轮车少很多那大概率是漏标了如果某个框的宽或者高小于 10 像素那大概率是误标或者说画得太贴边这类框在缩放、增强后很容易变成无效标注。3.3 标注质量自查越界框、极小框与重复框拿到数据集之后别急着训练先写一个质量抽查脚本重点查三类问题越界框、极小框、重复框。越界框是标注时手抖拉出了图片边界YOLO 训练时会以负坐标报错极小框是画得太极限缩放后特征完全丢失重复框是同一个目标被不小心画了两次造成 loss 计算时对小目标重复计数。import glob import xml.etree.ElementTree as ET from PIL import Image for xml_path in glob.glob(tricycle_dataset/*.xml): img_path xml_path.replace(.xml, .jpg) img_w, img_h Image.open(img_path).size tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: print(f[越界] {img_path} {name} ({xmin:.0f},{ymin:.0f})-({xmax:.0f},{ymax:.0f})) if (xmax - xmin) 10 or (ymax - ymin) 10: print(f[极小] {img_path} {name} w{xmax-xmin:.1f} h{ymax-ymin:.1f})逻辑说明逐张读 xml把 bndbox 坐标与图片实际宽高做比较越界的直接打印再算框的宽和高小于 10 像素的单独标记。这里的 10 像素是一个经验阈值在 640 的输入尺寸下10 像素的框小于一个 stride下采样 32 倍后只有 0.3 个像素模型基本学不到东西属于无效标注。参数说明脚本用 PIL 读图片尺寸比 cv2 轻量glob 路径和 xml 后缀替换成 jpg 时要注意目录结构如果图片和标注不在同一层replace会失效。跑完脚本正常情况应该只有极少数告警如果告警超过总量的 5%说明这份标注需要返工不能硬训。4. 常见问题排查从拿到数据到跑通训练的五次翻车4.1 路径错位图片与标注“同名不同命”现象yolo 训练刚开始就报found no labels或者FileNotFoundError: xxx.jpg但手动打开路径明明有文件。原因最常见的是文件被移动过比如你把图片单独复制到images/子目录但 xml 和 txt 还留在原来的根目录。YOLO 训练器的查找逻辑默认是按训练集图片路径在相同目录下找同名的.txt目录一变就找不到。还有一种情况是 xml 里写的filename字段和磁盘上实际文件名大小写不一致Linux 下这种问题会直接暴露。解决建立标准的images/与labels/目录结构按文件名做映射不要依赖 xml 里的 filename。我一般会先把所有文件按 stem 排序再批量移动到对应目录移动完成后立刻跑一遍第 2.3 节的完整性检查脚本确保两边数量一致。从那以后我再也没有因为路径问题半夜起来调。4.2 类别索引错乱tricycle 被识别成别的类现象训练不报错loss 也正常下降但预测时框是准的类别却显示成莫名其妙的标签或者只显示 class 0/1 的数字。原因data.yaml 里的names顺序和 txt 里 class_id 的语义对不上。比如你在 yaml 里写了names: [tricycle]但某个 txt 里第一行第一列写的是 1模型就把 id 为 1 的框当成了第二类。VOC 格式因为有name字段所以不会错YOLO 的 txt 只存数字类别顺序一旦错了模型静默地学到错误映射。解决这类纯单类数据集必须保证所有 txt 每行第一个数字都是 0。批量检查一段grep -c ^1 *.txt | grep -v :0只要 grep 输出非空就说明有错乱文件重新跑一遍标注格式转换即可。检查行首数字这个习惯我在任何多类别数据集上都会先做因为这是 yolo 数据里最常见、最隐蔽的黑匣子错误。4.3 VOC 转 YOLO 坐标除错框全部漂移现象可视化脚本里YOLO txt 画出来的框和标注对象偏差很大有的框跑到图片外面有的框宽高比严重不对。原因归一化的时候除了错误的尺寸。最常见的是把x_center除以图片宽度时写成了除以图片高度或者把width除以了图片宽度、把height也除以了图片宽度。再有一种低级错误是部分边角坐标忘了转 float整数除法把 0.5 直接干成 0。解决坐标换算必须严格按公式x_center (xmin xmax) / 2 / img_w、width (xmax - xmin) / img_w高度同理除以img_h。我常用上一章的对比脚本做回归验证换一张没抽查过的图绿框红框叠得上才算过。这个步骤枯燥但它是后续所有训练的地基。4.4 离线数据增强只增强了图片现象做了翻转、平移、缩放增强后训练 loss 变成 nan或者验证集 mAP 直接掉到接近 0。原因增强是离线做的只对 jpg 做了处理生成的新图片没有对应的 txt/xml 标注或者增强里对图片做了裁切但标注坐标没跟着一起变换。yolo 在训练时会认为这张增强图“没有目标”一遍遍把梯度往“无目标”方向推模型很快就废了。解决离线增强必须以“图和标注是同一个变换对象”为前提。翻转时 xmin/xmax 要同步翻转缩放时坐标和图片尺寸同时乘以系数裁切还要做坐标裁边。如果不熟悉这套几何变换我一般建议优先用在线增强mosaic、mixup 这类由训练框架处理的增强源码级的同步不容易出错。只有在进度条显示正样本严重不足时才值得动手做离线增强并且每生成一张新图都要跑一遍标注越界检查。4.5 验证集 mAP 为 0不是模型的锅现象训练结束验证集 mAP 一直显示 0.000但训练集上的 loss 已经收敛得很好。原因十有八九是验证集划分方式出了问题。比如按文件顺序取出最后 20% 当验证集而采集顺序恰好是按路段排的验证集全是某一种光照或背景下的难例模型没见过mAP 为 0 不意外。另一个常见原因是验证集路径含中文或特殊字符opencv 读图失败后标记全部丢弃评估时所有图片都是“无目标”mAP 自然为 0。解决划分时必须随机打散用固定 seed 保证可复现这个我在第 5 章专门讲。路径里不要带中文项目目录统一用英文字母和数字。你自己可以留一条检查线验证集里挑 10 张图手动跑一遍检测如果模型肉眼可见能框住三轮车但 mAP 是 0问题一定出在评估的数据加载上而不是模型。5. 三种落地玩法直接训练、迁移微调与可视化体检5.1 按帧打散划分固定 seed让每次结果可复现第 4 章说了按文件顺序划分会翻车正确做法是随机打散。这里有一个细节容易被忽略划分的最小单位是“帧”也就是单张图片而不是“视频段”或“连续帧批次”。如果采集来源是连续视频抽帧相邻帧高度相似把它们全放进训练集、或者全放进验证集都会让验证结果虚高或虚低。现在这份数据集是独立图片不涉及这个风险但顺手把 seed 固定下来仍是好习惯。import random from pathlib import Path random.seed(42) root Path(tricycle_dataset) imgs sorted(root.glob(*.jpg)) random.shuffle(imgs) val_ratio 0.2 val_cut int(len(imgs) * val_ratio) val_imgs imgs[:val_cut] train_imgs imgs[val_cut:] print(ftrain: {len(train_imgs)}, val: {len(val_imgs)}) for img in train_imgs: print(img.name) for img in val_imgs: print(img.name)逻辑说明先对所有图片名做排序保证不同机器上glob返回的顺序一致再shuffle打乱取前 20% 当验证集其余当训练集。打印出的文件名清单可以接着去生成 YOLO 需要的train.txt和val.txt路径列表也可以直接用脚本去移动文件到子目录。参数说明seed42是固定种子你可以换任何整数但只要固定别人跑同一个脚本就会得到完全一样的划分val_ratio0.2在 559 张图上划分出约 112 张验证样本量足够评估又不至于抢训练量。如果你有强迫症想保证验证集里每一类都有分布可以用按类别统计的 StratifiedSplit但单类数据集没必要。5.2 YOLOv8 训练闭环data.yaml 与第一条曲线新一代 yolo 系列把训练配置收敛到了一个 yaml 加一条命令行对新人友好很多。这份数据集因为只有单类data.yaml 可以写得非常干净path: /home/yourname/tricycle_dataset train: images/train val: images/val names: 0: tricyclepath是数据集根目录的绝对路径train和val是相对path的图片目录names的 key 是 class_id从 0 开始value 是类别名。这个顺序和第 4.2 节讲的坑直接相关yaml 里0对应tricycle所以所有 txt 里第一个数字必须是 0。目录结构要先建好images/train、images/val放图片labels/train、labels/val放 txt两者文件名一一对应。准备完成后训练命令是yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz640 batch16参数说明modelyolov8s.pt表示用小规模的 s 权重作为起点首次跑流程时建议用 s 而不是 x显存占用低、迭代快epochs100在当前数据量下够用了新手验证流程可以先用epochs10跑通再拉长imgsz640是标准输入尺寸如果原图普遍超过 1280可以尝试imgsz960但显存和训练时间会上升。第一个 epoch 的 loss 通常比较大不要慌先看前 20 个 epoch 有没有稳定下降趋势而不是盯着 mAP 看。5.3 迁移学习用小数据微调而不是从零训练559 张图、659 个框从头训练一个小模型未必不能收敛但容易过拟合尤其体现在验证集 mAP 上蹿下跳。更稳的路线是拿在 COCO 上预训练好的权重来 finetune。COCO 里有车、卡车、自行车这类高度类似的目标模型的底层特征边缘、纹理、轮子结构可以直接复用这也是modelyolov8s.pt这个参数存在的原因——它会自动下载预训练权重。yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz640 batch16 freeze10freeze10表示冻结前 10 层 backbone让它们在微调时保持 COCO 学到的特征不动。小数据集最容易翻车的就是 backbone 被少量新数据带偏冻结前几层是常用的后悔药。如果你用的是自己从零训练好的权重把model参数换成本地权重路径即可pretrainedTrue这种写法在旧版 yolo 里常见新版本直接用modelxxx.pt控制。我个人的习惯是先用不冻结的版本跑 30 个 epoch 观察 loss再对比冻结版本哪个验证集表现好留哪个。对比是为了确认这个数据集到底是“缺通用特征”还是“缺任务专属特征”前者冻结有效后者冻结反而拖后腿。5.4 可视化体检把 txt 框画回图片最后一步体检很朴素把训练用的 txt 画回原图肉眼确认标注质量。很多人跳过这步直接训练然后被 mAP 的忽高忽低折磨。画回图片不需要多复杂的代码用 opencv 就能批量做import glob import cv2 def draw_yolo_boxes(img_path, txt_path, color(0, 0, 255)): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() cls, cx, cy, bw, bh (int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])) 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), color, 2) cv2.putText(img, fclass:{cls}, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img img_path tricycle_xyxr_179.jpg txt_path tricycle_xyxr_179.txt result draw_yolo_boxes(img_path, txt_path) cv2.imwrite(vis_179.jpg, result)逻辑说明读图片宽高解析 txt把归一化坐标换算回像素坐标再画矩形框和类别 id。putText里的max(0, y1 - 5)是防止框贴着图片顶部时文字写到画面外去。参数说明color(0, 0, 255)是红色BGR 顺序opencv 里别写反如果要批量检查把img_path和txt_path换成 glob 遍历即可。抽检时重点看两个地方一是有没有把石头、树影当成三轮车二是两辆三轮车挨得很近时是一个框包住两辆还是两个框独立。前者说明标注颗粒度不够后者才是合格的画法。6. 吃满 559 张用尺度分布调 anchor靠置信度门限收口数据集只有 559 张想在有限样本里压榨出更好的检测效果最值得做的一件事是先统计所有真实框的像素宽高分布再决定要不要调整模型默认的 anchor。YOLO 系列虽然已经能自适应锚框但默认设置是在 COCO 上统计出来的COCO 里包含大量大中型目标而街景里的三轮车通常是有特定尺寸范围的。import glob import numpy as np from PIL import Image wh [] for txt in glob.glob(tricycle_dataset/*.txt): img_path txt.replace(.txt, .jpg) iw, ih Image.open(img_path).size with open(txt) as f: for line in f: parts line.split() bw float(parts[3]) * iw bh float(parts[4]) * ih wh.append([bw, bh]) wh np.array(wh) print(框宽高均值:, wh.mean(axis0)) print(宽高比 p10/p50/p90:, np.percentile(wh[:, 0] / wh[:, 1], [10, 50, 90]))这段脚本遍历所有 txt把归一化宽高还原成像素宽高再算宽高比的十分位、中位数和九十分位。如果 p50 在 1.2 到 1.8 之间说明三轮车多是横向偏宽的目标默认 anchor 里细长条的比例就浪费了如果整体框偏小可以适当把 imgsz 调大或者在训练时减少输入缩放带来的下采样损失。理解这个分布比机械地套用 anchor 调参脚本更有用。训练完成之后还有最后一个收口动作调整置信度门限。YOLO 预测时会输出每个框的置信度默认conf0.25在 559 张这类小数据集上这个门限往往偏低会带出一堆误检框。跑验证时把门限提到 0.35 或 0.4同时打印出每个置信度区间里的误检数量找一个“召回不掉太多、误检又压得住”的点。这个操作不改变模型权重只改变输出策略但视觉上对交付结果的影响比调几个 epoch 还明显。从那以后我每次拿到新数据集都会强制先走一遍“文件完整性核对→格式可视化比对→标注质量抽检→随机打散划分→小步训练验证”的流程再开始正式调参。这套流程看起来又笨又慢但能挡掉大部分低级翻车让精力真正花在模型本身。希望这份 559 张的三轮车数据能帮你把这条路走顺。本文还有配套的精品资源点击获取
返回列表