水果检测数据集实战:VOC/COCO/YOLO格式转换与YOLOv8训练全指南
2026/9/23 5:08:25 网站建设 项目流程

简介:这是一份面向目标检测初学者与算法工程师的YOLO水果检测数据集,适用于课程设计、模型训练与算法验证等场景。数据均为真实场景采集的高质量图片,场景丰富,经labelimg精细标注,同时提供voc、coco和yolo三种格式标签,可直接接入YOLO系列模型训练。压缩包共约2000个文件,以1000个xml标注文件和990个txt标签文件为主,另含少量html教程、py划分脚本与yaml配置文件,整体约107MB,目录结构清晰,便于按格式取用。资源附赠环境搭建、训练案例教程及数据集划分脚本,可自行划分训练集、验证集与测试集,帮助读者快速跑通从数据准备到模型训练的全流程,并掌握多格式标签转换与数据集组织方法。目前已有1522人学习下载,适合需要现成数据与配套脚本快速上手目标检测的读者。

1. 水果检测数据集怎么选:从1000张图到三种标签格式的完整链路

做过水果分拣、智能秤、采摘机器人的人都遇到过同一个问题:网上开源的水果数据集要么类别太少,要么标注格式不统一,拿到手先得花两天写转换脚本。这个标题指向的资源解决的就是这件事——1000张水果图片,同时提供VOC、COCO、YOLO三种格式标签,外加划分脚本和训练教程。它适合三类人:刚入门目标检测想跑通第一个自定义数据集的、做农业视觉产品需要快速验证原型的、以及带学生做课程设计不想在数据准备上耗时间的。我拿到类似数据集的第一反应从来不是直接训练,而是先验证三件事:图片质量是否一致、标注是否完整、三种格式之间能否无损转换。这三件事决定了你后面是花三天调模型还是花三周修数据。

2. 三种标签格式的差异与转换逻辑:VOC、COCO、YOLO到底该用哪个

2.1 格式差异不在文件后缀,而在坐标体系和字段语义

VOC格式用XML存储,每个目标一个<object>节点,坐标是绝对像素值xmin,ymin,xmax,ymax。COCO格式用单个JSON文件管理所有图片,坐标是[x,y,width,height]绝对像素值,外加category_idimage_id做关联。YOLO格式每张图一个TXT文件,每行class_id x_center y_center width height,全部是归一化到0到1的相对值。

这三种格式的转换坑集中在两个地方:一是VOC的xmax,ymax是包含边界的,转YOLO时宽高计算要用xmax-xmin而不是xmax-xmin+1,差一个像素在1000张图的规模下会让mAP掉0.5到1个点;二是COCO的category_id必须从1开始连续,而YOLO的class_id从0开始,中间少一个类别就会导致训练时标签越界报错。

我一般会先统一用VOC作为中间格式,因为XML可读性最好,出问题能直接打开看。确认无误后再批量转COCO和YOLO。

2.2 用Python脚本完成VOC到YOLO的批量转换

import xml.etree.ElementTree as ET import os # 类别列表,顺序决定class_id,必须与训练时的data.yaml一致 classes = ["apple", "banana", "orange", "pear", "grape"] def voc_to_yolo(xml_path, img_w, img_h, output_dir): tree = ET.parse(xml_path) root = tree.getroot() txt_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" txt_path = os.path.join(output_dir, txt_name) with open(txt_path, "w") as f: for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in classes: continue # 跳过未定义类别,避免class_id越界 cls_id = classes.index(cls_name) 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) # 归一化并计算中心点 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 裁剪到[0,1]防止标注越界导致训练报错 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) w = max(0, min(1, w)) h = max(0, min(1, h)) f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") # 使用示例:遍历所有XML,图片尺寸从XML的size节点读取 xml_dir = "annotations/voc" output_dir = "labels/yolo" os.makedirs(output_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) size = tree.getroot().find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) voc_to_yolo(os.path.join(xml_dir, xml_file), img_w, img_h, output_dir)

这段脚本的关键参数是classes列表,它的顺序直接决定YOLO训练时每个类别的编号。如果你后面用YOLOv8训练,data.yaml里的names必须和这个列表完全一致,否则模型学到的类别会错位。另一个参数是归一化后的裁剪操作,我见过太多因为标注框超出图片边界导致训练中途loss变NaN的案例,加一行max(0, min(1, ...))能省下大量排查时间。

2.3 COCO格式的生成与验证

COCO格式不适合手写,用pycocotools或自己拼JSON都行。核心结构是三个数组:images存图片id和宽高,annotations存每个框的image_idcategory_idbboxareacategories存类别id和名称。转换完必须做一次验证:用pycocotools加载后统计每个类别的实例数,如果某个类别数量为0,说明转换时类别名没对上。

from pycocotools.coco import COCO coco = COCO("annotations/instances_train.json") for cat_id in coco.getCatIds(): img_ids = coco.getImgIds(catIds=cat_id) ann_ids = coco.getAnnIds(catIds=cat_id) print(f"类别 {coco.loadCats(cat_id)[0]['name']}: {len(ann_ids)} 个标注")

如果输出里某个类别标注数为0,回去检查VOC的<name>字段是否和COCO的categories名称完全一致,大小写和空格都算差异。

3. 数据集划分脚本怎么写:训练集、验证集、测试集的比例与随机种子

3.1 划分比例不是拍脑袋,要看类别分布

水果数据集有个特点:不同类别的图片数量可能不均衡。苹果可能拍了300张,葡萄只有80张。如果直接按8:1:1随机划分,葡萄的测试集可能只有8张,评估结果波动极大。我一般会先统计每个类别的图片数,对少于100张的类别做分层抽样,保证每个子集里每个类别至少出现一次。

import os import random import shutil from collections import defaultdict def split_dataset(img_dir, label_dir, output_dir, ratios=(0.8, 0.1, 0.1), seed=42): random.seed(seed) # 固定随机种子,保证每次划分结果一致 images = [f for f in os.listdir(img_dir) if f.endswith((".jpg", ".png"))] # 按主类别分组,这里简化处理:读取对应YOLO标签的第一个类别作为分组依据 groups = defaultdict(list) for img in images: label_path = os.path.join(label_dir, os.path.splitext(img)[0] + ".txt") if os.path.exists(label_path): with open(label_path) as f: first_line = f.readline().strip() if first_line: cls_id = first_line.split()[0] groups[cls_id].append(img) else: groups["unlabeled"].append(img) train, val, test = [], [], [] for cls_id, imgs in groups.items(): random.shuffle(imgs) n = len(imgs) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) train.extend(imgs[:n_train]) val.extend(imgs[n_train:n_train + n_val]) test.extend(imgs[n_train + n_val:]) # 复制文件到对应目录 for split_name, split_imgs in [("train", train), ("val", val), ("test", test)]: img_out = os.path.join(output_dir, split_name, "images") lbl_out = os.path.join(output_dir, split_name, "labels") os.makedirs(img_out, exist_ok=True) os.makedirs(lbl_out, exist_ok=True) for img in split_imgs: shutil.copy(os.path.join(img_dir, img), os.path.join(img_out, img)) lbl = os.path.splitext(img)[0] + ".txt" lbl_src = os.path.join(label_dir, lbl) if os.path.exists(lbl_src): shutil.copy(lbl_src, os.path.join(lbl_out, lbl)) print(f"训练集: {len(train)}, 验证集: {len(val)}, 测试集: {len(test)}") split_dataset("images", "labels/yolo", "dataset", ratios=(0.8, 0.1, 0.1), seed=42)

seed=42这个参数看起来不起眼,但它是你复现实验的后悔药。没有固定种子,每次运行划分结果不同,你调参时根本分不清是模型改了还是数据变了。ratios参数按类别分层,保证小类别在验证集和测试集里也有样本,否则评估指标会剧烈跳动。

3.2 划分后必须检查的三个文件

划分完成后,检查dataset/train/imagesdataset/train/labels的文件数是否一致。不一致说明有图片没有对应标签,训练时YOLO会直接跳过这些图,你以为是1000张在训,实际可能只有800张。第二个检查是data.yaml的路径配置,YOLOv8要求trainval指向包含images子目录的父目录,写错一层路径就会报No labels found。第三个检查是随便打开一个TXT标签,确认坐标值在0到1之间,如果有大于1的值,说明转换时没做归一化。

4. YOLO训练教程的实操细节:从环境配置到第一次推理

4.1 环境配置的版本匹配比安装步骤更重要

YOLOv8用ultralytics包,一条pip install ultralytics就能装好。但真正的坑在PyTorch和CUDA的版本匹配上。如果你的显卡是RTX 30系或40系,需要CUDA 11.8以上,对应的PyTorch版本是2.0以上。我一般用conda建环境:

conda create -n fruit_yolo python=3.10 -y conda activate fruit_yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics

装完后用python -c "import torch; print(torch.cuda.is_available())"验证,输出True才算环境通了。如果输出False,先别急着训练,检查显卡驱动版本是否支持你装的CUDA版本。这一步翻车的人最多,症状是训练时loss正常下降但速度极慢,因为实际在用CPU跑。

4.2 data.yaml的五个必填字段

path: /home/user/fruit_dataset # 数据集根目录 train: train/images # 训练集图片路径,相对于path val: val/images # 验证集图片路径 nc: 5 # 类别数,必须与classes列表长度一致 names: ["apple", "banana", "orange", "pear", "grape"] # 类别名称,顺序即class_id

ncnames的长度必须一致,不一致会直接报错。path用绝对路径最稳妥,相对路径在不同工作目录下运行时容易找不到文件。

4.3 训练命令与关键参数

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/fruit \ name=exp1

model=yolov8n.pt用的是预训练权重,1000张图从零训练容易过拟合,用预训练模型微调收敛更快。imgsz=640是输入分辨率,水果目标通常较大,640够用,如果检测小目标比如单个葡萄粒,可以提到1280但显存占用翻倍。patience=20表示20个epoch验证指标没提升就早停,防止过拟合。batch=16在8GB显存下跑640分辨率刚好,显存不够就降到8。

训练过程中重点看mAP50mAP50-95两个指标。mAP50到0.9以上算收敛不错,如果卡在0.6上不去,先检查标签有没有问题,再考虑加数据增强或换更大的模型。

4.4 推理验证与置信度门限调整

yolo detect predict \ model=runs/fruit/exp1/weights/best.pt \ source=test_images/ \ conf=0.25 \ save=True

conf=0.25是置信度门限,低于这个值的检测框会被过滤。水果检测场景下,如果漏检多就降到0.1,如果误检多就提到0.5。我一般会先用0.25跑一批测试图,看误检和漏检的比例再调整。调整置信度门限不需要重新训练,直接改推理命令里的conf参数即可。

5. 避坑与排查:水果数据集训练中最容易翻车的五个地方

5.1 现象:训练loss正常下降但mAP始终为0

原因:data.yaml里的names顺序和YOLO标签里的class_id对不上。比如标签里苹果是0、香蕉是1,但names写成了["banana", "apple"],模型学到的类别完全错位。

解决:用脚本统计每个class_id对应的图片数,和names列表逐一核对。最直接的办法是拿一张训练图用yolo detect predict跑推理,看输出的类别名是否和图片内容一致。

5.2 现象:训练到一半报错NaN lossloss=inf

原因:标签文件里有坐标值超出0到1范围,或者宽高为0。VOC转YOLO时如果xmax=xmin,宽就是0,归一化后还是0,计算loss时除零导致NaN。

解决:在转换脚本里加过滤,宽高小于1个像素的框直接跳过。训练前用脚本扫描所有TXT文件,检查每行的后四个值是否都在0到1之间。

5.3 现象:验证集mAP比训练集低很多,差距超过0.3

原因:训练集和验证集的图片分布差异大。比如训练集全是白底拍摄,验证集有大量自然背景,模型没学过自然背景下的水果特征。

解决:检查划分脚本是否真的随机打散了数据。如果数据集本身按拍摄场景分文件夹,需要先合并所有图片再随机划分,不能按文件夹划分。

5.4 现象:推理时同一张图每次运行检测框数量不同

原因:没有设置随机种子,或者用了数据增强的推理模式。YOLO默认推理时不做增强,但如果augment=True,每次推理会随机变换输入。

解决:推理命令里不加augment参数,默认就是关闭的。如果还是不稳定,检查是否有其他随机因素,比如多GPU推理时的批次划分。

5.5 现象:训练速度极慢,GPU利用率不到30%

原因:数据加载成为瓶颈。1000张图如果放在机械硬盘上,每个epoch读取图片的时间可能超过计算时间。

解决:把数据集复制到SSD,或者在训练命令里加workers=8增加数据加载进程数。workers设成CPU核心数的70%左右比较合适,设太大反而会因为进程切换开销降低效率。

6. 从1000张到可落地模型:数据增强与模型选择的进阶技巧

1000张图训练一个水果检测模型,如果直接跑YOLOv8n,mAP50大概能到0.85左右。想再往上提,最有效的不是换更大的模型,而是做针对性的数据增强。水果检测有个特殊之处:颜色是核心特征,但光照变化会严重干扰颜色判断。我一般会在data.yaml同级目录建一个hyps.yaml,覆盖默认增强参数:

# hyps.yaml 自定义增强参数 hsv_h: 0.015 # 色调抖动,模拟不同光照下的颜色偏移 hsv_s: 0.7 # 饱和度抖动,水果成熟度不同饱和度差异大 hsv_v: 0.4 # 亮度抖动,模拟室内外光照变化 degrees: 15.0 # 旋转角度,水果在传送带上姿态随机 translate: 0.1 # 平移,模拟水果在画面中的位置变化 scale: 0.5 # 缩放,模拟不同拍摄距离 flipud: 0.0 # 上下翻转关闭,水果不会倒置 fliplr: 0.5 # 左右翻转开启,水果左右对称 mosaic: 1.0 # 马赛克增强,四张图拼接,对小目标检测有效

hsv_h设0.015是保守值,设太大会让苹果变成香蕉的颜色,模型学不到正确特征。degrees=15比默认的0更符合水果在分拣线上的实际姿态。flipud=0是因为水果检测场景下上下翻转没有物理意义,开了反而引入噪声。

用这个增强配置重新训练,mAP50通常能提升2到3个点。如果还想再提,可以试试YOLOv8s,参数量从3M涨到11M,推理速度在RTX 3060上从2ms涨到5ms,精度能再提1到2个点。但如果是边缘设备部署,比如RK3588或Jetson Nano,老老实实用YOLOv8n,精度够用,速度才是硬指标。

验证模型是否真的可用,我习惯用一批没参与训练的实拍图跑推理,统计漏检率和误检率。漏检率高于10%就加数据,误检率高于15%就提高置信度门限或加负样本。别只看mAP,那个数字在真实场景里经常骗人。

我自己的习惯是每做完一个数据集就写一个check_dataset.py,把图片数、标签数、类别分布、坐标范围全部打印一遍。这个脚本花十分钟写,能省下后面十个小时的排查时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询