ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

猕猴桃检测数据集VOC/YOLO双格式解析与YOLO训练避坑指南

猕猴桃检测数据集VOC/YOLO双格式解析与YOLO训练避坑指南

简介:面向猕猴桃目标检测任务,这套数据集提供1838张真实图像的标注结果,同时采用Pascal VOC和YOLO两种通用格式,适合目标检测初学者验证流程,也适合农业视觉项目快速构建猕猴桃检测训练集。数据统一标注为mihoutao单一类别,矩形框标注总数达19278个,由labelImg标注完成,标注信息覆盖果实位置与类别,可平滑接入YOLOv5、YOLOv8、Faster R-CNN等常见模型的训练、迁移学习与精度对比。资源整体为7z压缩包,大小约84MB,文件总数2000个,核心文件以图像、xml标注文件和txt标签文件为主;其中xml面向VOC格式读取,txt面向YOLO系列训练使用,目录结构便于按图片索引。目前已有725人学习/下载,可作为果园巡检、农产品分拣等场景的预研数据;数据集保证标注准确合理,模型最终精度需结合实际训练策略评估。

1. 猕猴桃检测数据集:1838 张 VOC+YOLO 双格式,先验货再训练

做果园产量预估或分选线项目时,最磨人的往往不是模型结构,而是数据。你下载到一个 7z 压缩包,里面是 1838 张已经标注好的猕猴桃图片,同时给了 VOC 和 YOLO 两种标注格式,类别只有 1 个。对 yolo 入门的朋友来说,这种小规模、单类别、双格式的数据集是理想的起步材料:数据量不大,标注齐全,能快速把一条训练链路跑通。

但我的经验是:解压完千万别急着写训练脚本,先花十分钟验货,把目录结构、标签配对和坐标系检查一遍,不然后面所有报错基本都是从这里长出来的。下面直接按解压、校验、转换、训练、验证的顺序,把 1838 张猕猴桃数据真正用起来。

2. 看懂 VOC 与 YOLO 两类标注:目录结构、字段含义和转换原理

2.1 VOC 的 XML 里到底存了什么:从 JPEGImages 到 Annotations 的一一对应

PASCAL VOC 的老玩家都知道,VOC 格式不是一个文件,而是一整套目录约定。常见布局是这样:

dataset/ ├── JPEGImages/ │ ├── kiwi_0001.jpg │ └── kiwi_0002.jpg ├── Annotations/ │ ├── kiwi_0001.xml │ └── kiwi_0002.xml └── ImageSets/ └── Main/ ├── train.txt └── val.txt

这套布局里 JPEGImages 放原始图片,Annotations 放每张图对应的 XML 标注,ImageSets/Main 放划分好的训练验证名单。如果解压后看到的是这套结构,说明作者是按 pascal voc 惯例整理的;如果只有 JPEGImages 和 Annotations 两个目录也没关系,很多 YOLO 项目根本不依赖 ImageSets,直接用脚本按比例划分。

一个典型 XML 标注文件长这样:

<annotation> <folder>kiwifruit</folder> <filename>kiwi_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>kiwifruit</name> <bndbox> <xmin>100</xmin> <ymin>80</ymin> <xmax>210</xmax> <ymax>190</ymax> </bndbox> </object> </annotation>

字段含义:filename 是图片名,size 里的 width 和 height 是图片的像素宽高,object 代表一个目标实例,每个 object 带 name(类别名)和 bndbox(外接矩形)。xmin、ymin 是矩形左上角坐标,xmax、ymax 是右下角坐标,这四个值单位是像素,不是比例。如果一张图里有两颗猕猴桃,annotation 里就会有多个 object 节点,这是区分“1 类别”和“单张多个目标”最直观的位置。

这组字段是所有转换的源头。多数标注工具在 VOC 模式下导出的 XML 字段基本一致,但细节有差异:有的软件把 filename 写成不带扩展名,有的把 bndbox 写成 x1/y1/x2/y2,有的会自动生成 segmented 标签。这些差异本身不致命,致命的是拿到手没有抽查,默认它就是标准格式,然后转换脚本一跑,坑全冒出来。这种双格式封装在农业类数据集里很常见,因为标注成本高,导出方希望一份数据能同时照顾评测和训练两类用户,所以看懂两套格式的对应关系比记住几个命令更值钱。

2.2 YOLO 的 txt 为什么更适合丢给模型训练:归一化坐标与类别索引

YOLO 系列的训练原生吃纯文本标注:每张图一个 txt,文件名和图片完全同名。每一行的格式是固定的五个值:

class_id cx cy w h

class_id 是类别索引,从 0 开始;cx 和 cy 是目标中心点的横纵坐标;w 和 h 是宽和高。关键点是 cx、cy、w、h 都不是绝对像素值,而是归一化到 [0, 1] 区间的相对值,用绝对坐标除以图片宽高得到。

换算例子:假设图宽 1920、高 1080,框 xmin=100、ymin=80、xmax=210、ymax=190,中心点 x = (100+210) / 2 = 155,归一化后 cx = 155 / 1920 ≈ 0.0807;cy = (80+190) / 2 = 135,135 / 1080 = 0.125;宽 210-100 = 110,w = 110 / 1920 ≈ 0.0573;高 190-80 = 110,h = 110 / 1080 ≈ 0.1019。对应的一行 YOLO 标注就是 0 0.0807 0.1250 0.0573 0.1019。这些数字是随手算的,看公式即可。

如果手上只有 VOC XML 需要转成 YOLO,常见做法是用这个 Python 函数:

# voc_to_yolo.py import xml.etree.ElementTree as ET def voc_xml_to_yolo_line(xml_path, class_names): """把单个 VOC XML 转成多行 YOLO 标注""" root = ET.parse(xml_path).getroot() width = int(root.find('size/width').text) height = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) b = obj.find('bndbox') xmin = float(b.find('xmin').text) ymin = float(b.find('ymin').text) xmax = float(b.find('xmax').text) ymax = float(b.find('ymax').text) cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return lines

逻辑说明:先读 XML 里的 size 作为归一化分母,再遍历每个 object;name 不在 class_names 里的目标直接跳过,类别白名单可控。class_names 是类似 ['kiwifruit'] 的列表,name 拼写必须和 classes.txt 完全一致,否则 class_names.index(name) 会抛 ValueError。参数说明:最容易错的是读取 size 的顺序,width 和 height 读反会导致 cx 变成 cy,框的位置全部跑到对角线上去。

这个猕猴桃数据集同时给 VOC 和 YOLO 两份,训练时直接读 YOLO txt 就行;VOC 那份的用途是互验:把两种格式用脚本对拍一遍,能过滤掉相当一部分标注错误。两种格式的差异我用这张表记:

对比项VOC XMLYOLO txt
坐标基准绝对像素值相对图片宽高的比例
类别表达字符串 name整数索引 class_id
存储形式一个 XML 多个 object一个 txt 每行一个目标
训练适用性需转换或额外解析YOLO 系列原生读取
典型坑filename 缺扩展名、size 与实际图不符坐标越界、索引错位

3. 从 7z 解压到数据校验:三条命令与五个训练前必看项

3.1 Linux 下解压 7z 的正确姿势:x 和 e 不能混用

这类数据集压缩包大多用 7z 打包,后缀 .7z 说明它走的是 LZMA2 压缩算法,压缩率通常比 zip 高,但解压工具在 Linux 上不一定自带。Debian/Ubuntu 系先装工具再解压:

sudo apt install p7zip-full # Debian/Ubuntu 安装 7z 命令行工具 7z t kiwifruit_dataset.7z # 测试压缩包完整性 7z x kiwifruit_dataset.7z -odataset/ # 解压到 dataset 目录并保留内部目录结构

命令说明:7z t 只做完整性测试不释放文件。我拿到陌生压缩包的习惯是先跑一遍 t,网络传输导致压缩包损坏的概率不低,解到一半报错再排查更难受。-o 指定输出目录,注意 -o 和目录名之间没有空格,写成 -o dataset 会报未知选项。7z x 保留压缩包内部目录层级,7z e 会把所有文件平铺到一个目录;这个数据集内部大概率是按 JPEGImages/Annotations/labels 组织的,用 e 解压会把结构毁掉,后面脚本全废。另外作者打包时通常会用 -mx=9 这类高压缩比参数,所以解压速度会比普通 zip 慢一些,这是正常现象,不用怀疑机器性能。

Windows 上图形界面装 7-Zip 右键解压到指定文件夹即可;需要在脚本里批量解压时,用 py7zr 在 PyCharm 里一条龙处理:

import py7zr with py7zr.SevenZipFile('kiwifruit_dataset.7z', 'r') as archive: archive.extractall('dataset')

逻辑说明:with 语句确保解压完文件句柄正常释放;extractall(path) 的 path 是解压目标目录,目录不存在会自己建。py7zr 的好处是能和后面的校验脚本合在同一个 Python 工程里跑,不用来回切终端。

3.2 目录结构与配对校验:三行 Python 找出缺失标签

解压完第一件事看目录结构。常见做法是 tree 或 find 先过一遍,看清楚图片、XML、txt 三类文件到底摆在哪些目录。双格式数据集常见两种摆法:一种是把 VOC 系目录和 YOLO 系目录并列放,另一种是两个格式的文件揉在同一个平铺目录里。先看清再写脚本,比对着猜测的路径一顿操作要省事。

配对校验是整个流程里性价比最高的一步,用文件名去掉扩展名做键值,三方对照:

# check_dataset.py from pathlib import Path img_dir = Path('dataset/images') # 按实际布局调整 xml_dir = Path('dataset/annotations/voc') # VOC XML 目录 txt_dir = Path('dataset/annotations/yolo') # YOLO txt 目录 imgs = {p.stem: p for p in img_dir.glob('*.jpg')} xmls = {p.stem: p for p in xml_dir.glob('*.xml')} txts = {p.stem: p for p in txt_dir.glob('*.txt')} print('图片数:', len(imgs), 'XML数:', len(xmls), 'TXT数:', len(txts)) print('缺XML:', len(set(imgs) - set(xmls))) print('缺TXT:', len(set(imgs) - set(txts)))

逻辑说明:glob 按模式收集文件,p.stem 是文件名去掉扩展名的部分,用三个集合做差集可以快速统计“有图没标”和“有标没图”的数量,正常应全部为 0。参数说明:glob 模式里的扩展名要按实际数据改成 .png 或 .jpeg;如果目录结构不同,只改前三行路径即可。

图片损坏也顺带查一遍,压缩包在传输过程中偶尔会截断图片尾部,用 PIL 的 verify 全量扫一次:

from PIL import Image bad = [] for stem, path in imgs.items(): try: with Image.open(path) as im: im.verify() except Exception: bad.append(stem) print('损坏图片:', len(bad), bad[:10])

说明:im.verify() 只校验文件头和数据完整性,不解码全部像素,速度比 load 快一个量级,适合 1838 张这种规模。发现坏图后建议把对应文件名从划分名单里剔除,不要直接删文件,保留原样方便溯源。

3.3 类别文件和数据划分:classes.txt 和 train.txt/val.txt 里写了什么

配对没问题之后看两个文本文件:

cat dataset/classes.txt wc -l dataset/train.txt dataset/val.txt

classes.txt 决定类别名和索引的对应关系,单类别数据只有一行。把这一行内容和 XML 里的 字段比对,两处必须一致。wc -l 统计划分名单里的图片行数,1838 张按 8:1:1 分配的话,train 大约 1470、val 和 test 各约 184,比例相差太远就要警惕是不是划分脚本有 bug。

到这里,目录层级、文件配对、图片完整性、类别文件、划分名单五个必看项全部过了一遍,训练前的验货步骤才算结束。很多人直接跳过这些开始写配置文件,然后被 FileNotFoundError 和 All labels are empty 轮番教育,根子都在这一关。

4. 避坑:1 类别数据集训练前最容易翻车的五个细节

4.1 类别索引整体错位:XML 里是 kiwifruit,YOLO 里却成了 1

现象:训练正常启动,但验证阶段分类损失一直下不去,画出来的预测框类别名对不上;打开 YOLO txt,第一列写的是 1,而 classes.txt 里只有一个类别。

原因:VOC 转 YOLO 时把 object 序号直接当 class_id,或者 class_names 列表顺序和 classes.txt 不一致,索引整体平移。

解决:先确认 classes.txt 行数,一个类别只能一行;再全量检查所有 txt 的 class_id。单类别最小检查脚本:

# check_class_id.py from pathlib import Path for txt in Path('dataset/labels').glob('*.txt'): for line in txt.read_text().splitlines(): if not line.strip(): continue cls = int(line.split()[0]) if cls != 0: print('类ID异常:', txt.name, cls)

逻辑说明:line.split()[0] 取每行第一个空格分隔字段并转 int;单类别数据里所有 cls 必须为 0,任何非 0 输出都是错位索引。参数说明:多类别场景把 if cls != 0 改成 cls >= num_classes,num_classes 从 classes.txt 行数读取。

4.2 XML 的 filename 不带扩展名:配对失败从源头蔓延

现象:VOC 转 YOLO 的脚本跑完,生成的 txt 为空,或用 3.2 节的配对脚本一跑,缺 XML 的数量等于图片总数。

原因:标注工具写的是 kiwi_0001 ,没带 .jpg;或者磁盘上叫 kiwi_0001.JPG,XML 里写 kiwi_0001.jpg,大小写不一致。

解决:配对不要信 XML 里的 filename,以磁盘文件列表为准;转换前统一补扩展名:

# fix_filename.py import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path('dataset/annotations/voc') for xml_path in xml_dir.glob('*.xml'): root = ET.parse(xml_path).getroot() name = root.find('filename') if name is not None: text = name.text.strip() if not text.lower().endswith(('.jpg', '.jpeg', '.png')): name.text = text + '.jpg' ET.ElementTree(root).write(xml_path, encoding='utf-8', xml_declaration=True)

说明:这段脚本原地改写 XML,属于有侵入性的操作,跑之前务必保留一份原始未解压副本,给操作留后悔药。参数说明:xml_declaration=True 保留 XML 头部声明,一般不影响后续解析;扩展名可按数据集实际情况改。

4.3 图片被缩放后 size 没更新:归一化坐标整体漂移

现象:把标注框画回原图上,框的位置明显偏左上或框大小对不上果实轮廓;训练到一半 mAP 稳定在低水平,但数据集肉眼看不出问题。

原因:有人压缩数据把 1920×1080 的图缩成 960×540,忘了同步 XML 里的 size 字段,归一化时分母还是旧宽高,cx、cy、w、h 整体偏移。

解决:加一条尺寸比对,随机抽 20 张图验证 XML size 和真实尺寸是否一致,不一致就回源修正 XML。校验时用 PIL 读图宽高,与 ET 解析出的 size/width、size/height 逐项比较,跑完输出不一致的文件名清单。这个动作只花十秒,但能避免整个训练白跑。

4.4 自带 train.txt 写死路径:换目录就全盘失效

现象:解压后训练,读 train.txt 报 FileNotFoundError;vim 一看,里面写的是 /home/author/dataset/images/kiwi_0001.jpg 或 D:\old_path\kiwi_0001.jpg。

原因:作者在自己机器上生成的划分名单,存了绝对路径或旧环境相对路径,压缩包换机器后自然失效。

解决:别用自带划分,用固定随机种子重新生成 train/val/test,只存相对数据集根的路径。直接给完整脚本:

# make_splits.py import random from pathlib import Path img_dir = Path('dataset/images') # 实际图片目录 root_dir = img_dir.parent imgs = sorted(img_dir.glob('*.jpg')) random.seed(42) random.shuffle(imgs) n = len(imgs) val = imgs[: int(n * 0.1)] test = imgs[int(n * 0.1): int(n * 0.2)] train = imgs[int(n * 0.2):] for split_name, split_imgs in [('train.txt', train), ('val.txt', val), ('test.txt', test)]: lines = [p.relative_to(root_dir).as_posix() for p in split_imgs] (root_dir / split_name).write_text('\n'.join(lines))

逻辑说明:先排序让输入稳定,再用固定 seed 打乱,保证每次运行结果一致。按 8:1:1 切成三份,写入内容用 p.relative_to(root_dir).as_posix() 转成如 images/kiwi_0001.jpg 的相对路径,交给模型配置时由 path 字段拼出完整路径。参数说明:seed 定了就不要换,换 seed 等于换数据集划分,指标不可复现;img_dir 按实际目录改。

提示:任何原地改写 XML 或 labels 的操作,都建议保留一份原始压缩包未解压的副本。初始状态一旦被覆盖,想回头只能重新下载。

4.5 YOLO 坐标越界:cx 大于 1 或 w 为 0

现象:训练 loss 出现 nan,或预测框完全超出图片范围;打开某个 txt 看到 1.2567、-0.0034 这类值。

原因:标注框画到画布外、bndbox 被旋转框格式污染、或脚本把 xmin/xmax 顺序读反导致 w 为负。YOLO 格式要求每个值落在 [0, 1],宽度高度必须大于 0。

解决:全量检查越界,一条脚本抓出来:

# check_bounds.py from pathlib import Path for txt in Path('dataset/labels').glob('*.txt'): for line in txt.read_text().splitlines(): parts = line.split() cls = int(parts[0]) values = [float(v) for v in parts[1:]] if not all(0 <= v <= 1 for v in values): print('越界:', txt.name, cls, values)

说明:parts[1:] 把 cx、cy、w、h 一起取出来做范围判断,任何值超出 [0, 1] 都打印文件名和具体数值,方便回源修 XML。边界情况:1.000001 这类浮点余数可以容忍,建议写成 v <= 1.0001;如果出现 0.0 的 w 或 h,必须删掉这条标注重标。

提示:浮点越界不要只改 txt,要回头修 XML。YOLO txt 是标注的导出结果,源头错了改一百遍 txt 也没用。

5. 用 1838 张数据跑通一次 YOLO 训练:yaml 怎么配、loss 怎么判、onnx 怎么验

5.1 最小训练配置:PyCharm 里三行命令跑起来

我用 yolo 训练自己的数据集,最常用的一条命令是在 PyCharm 的终端里执行的。先建虚拟环境装 ultralytics,再准备 data.yaml:

path: D:/datasets/kiwifruit train: train.txt val: val.txt names: 0: kiwifruit

path 指向数据集根目录,train 和 val 用上一章生成的 txt 列表,names 的索引必须和标签 txt 的 class_id 对齐,这个数据集只有 1 类,所以只有 0。然后用最小命令启动:

yolo detect train data=data.yaml model=yolov8n.pt epochs=50 imgsz=640 batch=16

参数说明:yolov8n 是 nano 版本,1838 张单类别数据用 n 或 s 足够,上大模型只会加速过拟合;imgsz=640 是默认输入尺寸,batch=16 在 8G 显存也能跑,显存小就降到 8。

5.2 用损失曲线和 onnx 导出完成验收

训练完打开 runs/detect/train 下的 results.csv 或曲线图,重点看三个损失项:box_loss 是框回归损失,cls_loss 是分类损失,dfl_loss 是分布焦点损失。单类别数据集里 cls_loss 通常掉得非常快,这正常;真正要盯的是 box_loss 和 dfl_loss 是否平稳下降。如果最后十几个 epoch 还在剧烈震荡,优先把学习率降到 0.0005 再试。

验证模型能否落地,习惯先导出 onnx 再跑一张测试图:

yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 yolo predict model=runs/detect/train/weights/best.pt source=test.jpg

导出 onnx 能提前暴露输入尺寸写死、动态轴不兼容这类部署问题,predict 则是看框位置和置信度是否合理。两件事做完,这个数据集才算真正被用顺手。

我拿到任何新数据集,第一个习惯永远是先画十张原图加标注框叠起来看一眼,坐标对不对、类别对不对,一眼就能看出来。这个步骤不花时间,但能挡住后面一整晚的调参。文本标注永远是数据的最终解释权,别让转换脚本替你背锅。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进