简介:面向农业目标检测与YOLO模型训练者的12种水果检测数据集,包含苹果、香蕉、哈密瓜、无花果、葡萄、芒果、橙子、梨、菠萝、石榴、草莓、西瓜共12个常见品类。图片采集自不同角度、成熟度与光照环境,并专门收录香瓜/哈密瓜等易混淆样本,有助于模型学习细粒度特征,可直接支撑农业自动化分拣、果园监测、成熟度检测、产量预估与食品质量分级等应用场景。数据集已按YOLO格式完成边界框标注,训练集1556张、验证集151张、测试集434张;压缩包共2000个文件,其中1998个txt标注文件对应每张图片的类别与框坐标,1个yaml配置文件用于定义模型类别,1个docx说明文档介绍数据集结构与使用方式,整体大小113.11MB,导出结构清晰,主流YOLO框架导入后即可训练。当前已有138人学习,适合目标检测入门者、农业AI研究者以及智能农业项目开发者直接用于模型训练、算法验证和方案预研。
1. 为什么12种水果目标检测数据集值得从zip包处理开始
一个目标检测项目,最容易翻车的往往不是模型结构,而是数据集的初始状态。十二种水果的目标检测数据集,听起来只是“简单场景”,但它包含的遮挡、相似类别、小目标、标注噪声,几乎能覆盖通用目标检测流程里的大部分坑。拿到一个 zip 包的时候,很多人的第一反应是直接解压然后跑脚本开始训练,结果要么标签和图像对不上,要么类别 id 顺序错位,要么压缩包里混进了损坏文件导致训练中断。与其到处找现成脚本,不如把 zip 到可训练状态的整个过程走顺。这篇文章就围绕“12种水果目标检测数据集.zip”这个场景,把完整性校验、标注解析、可视化、训练、验证评估这条链路完整打通。
2. 解压前的完整清单:校验zip包完整性与目录结构
目标检测数据集用 zip 分发是常态,原因无非是文件数量多、整体压缩率高。但 zip 在传输过程中出现文件截断或部分损坏并不少见,所以解压前做一次机械性检查,成本最低,收益最直接。
2.1 用Python的zipfile模块做CRC完整性检测
zip 格式内部为每个文件保存了 CRC-32 校验值,压缩包解压时会做完整性比对。用 Python 标准库的 zipfile 模块,可以不用解压工具就完成全量检测:
import zipfile from pathlib import Path def inspect_zip(zip_path: str): with zipfile.ZipFile(zip_path, "r") as zf: # testzip() 会解压每个成员并与内部 CRC-32 比对 corrupt = zf.testzip() if corrupt is not None: print(f"[FAIL] 文件损坏: {corrupt}") else: print(f"[OK] {len(zf.infolist())} 个文件全部通过 CRC 校验") for info in zf.infolist(): if info.is_dir(): continue size_kb = info.file_size // 1024 print(f"{info.filename}\t{size_kb} KB") if __name__ == "__main__": inspect_zip("12种水果目标检测数据集.zip")testzip()的返回值只有两种情况:None代表全部文件校验通过,字符串则代表第一个损坏文件的路径。需要注意,这里说的“损坏”主要指 CRC 对不上,也就是文件内容已经变化;如果压缩包能打开但内部文件零散损坏,训练脚本通常会在读取某个样本时突然崩溃,报错位置随机,非常难排查。另一种情况是压缩包设置了密码,读取时会抛出RuntimeError。遇到带密码的数据集压缩包,规范做法是回到发布页面找密码说明,而不是到处找所谓“zip压缩包密码移除”的工具,后者既不可靠也可能带来安全风险。
2.2 读懂目录布局:YOLO分集和COCO分年两种主流结构
目标检测数据集压缩包里的目录结构,90% 以上属于两种布局之一。提前识别出它属于哪种,后续脚本才能走对。
| 布局类型 | 图像路径 | 标签位置 | 标注文件格式 |
|---|---|---|---|
| YOLO 分集 | images/train/xxx.jpg | labels/train/xxx.txt | 每个 txt 一行一个真值框:class cx cy w h |
| COCO 风格 | train2017/xxx.jpg | annotations/instances_train2017.json | 单个 JSON,内含 images、annotations、categories |
| 平铺结构 | 全部图像在一个目录 | 标签同名但无子目录 | 需要自行划分 train/val/test |
YOLO 布局下,标签文件与图像文件严格同名,只有扩展名不同,这是后续配对脚本的基本前提,也是最常被破坏的前提。COCO 风格没有独立 txt,全部实例存在于一个 JSON 文件中,要按image_id关联到对应图像,处理步骤多一层。平铺结构最省事但最危险,因为没有现成的训练验证划分,直接拿去训练会出现验证集与训练集重叠的问题。拿到压缩包先打印目录层级前两层,就能判断属于哪种。
2.3 标签-图像配对校验:识别孤儿标签与无标签样本
目录结构确认后,第二步是严格配对。这个步骤能找出两类典型脏数据:没有标签的图像,以及没有图像的孤儿标签。
from pathlib import Path IMG_EXTS = {".jpg", ".jpeg", ".png"} def check_pairs(img_dir: str, label_dir: str): imgs = {p.stem for p in Path(img_dir).iterdir() if p.suffix.lower() in IMG_EXTS} labels = {p.stem for p in Path(label_dir).iterdir() if p.suffix.lower() == ".txt"} no_label = imgs - labels orphan = labels - imgs print(f"图像总数: {len(imgs)} 标签总数: {len(labels)}") print(f"缺少标签的图像: {len(no_label)} 孤儿标签: {len(orphan)}") for name in sorted(no_label)[:10]: print(f" 无标签: {name}") for name in sorted(orphan)[:10]: print(f" 孤儿: {name}") # 示例调用 check_pairs("fruit12/images/train", "fruit12/labels/train")无标签图像放进训练集,模型每次看到它们都会产生一个空的 loss 项,单个样本问题不大,但数量一多会拉低整体收敛速度。孤儿标签则是图像丢失或命名不一致造成的,直接忽略即可,不需要保留。另一个隐蔽问题是文件名编码,部分 zip 在 Windows 下打包时对中文字符使用了非 UTF-8 编码,解压后文件名变成乱码,配对脚本会误判大量“孤儿标签”。处理方式是先用zipfile读原始文件名列表,确认编码后再批量重命名,不要让乱码样本进入训练。
2.4 统一图像扩展名与色彩通道
数据集里经常同时混有 jpg、png 甚至 bmp,编译器切图时无感,但后续可视化某一步会因为通道数不一致而报错。常见做法是统一转成 jpg:
import os import cv2 from pathlib import Path def normalize_images(img_dir: str): for p in Path(img_dir).iterdir(): if p.suffix.lower() in {".png", ".jpeg", ".bmp"}: img = cv2.imread(str(p)) if img is None: print(f"无法读取: {p.name}") continue out = p.with_suffix(".jpg") cv2.imwrite(str(out), img, [cv2.IMWRITE_JPEG_QUALITY, 95]) if out.name != p.name: p.unlink()这段脚本先读取图片,写为 jpg 后再删除原文件。如果源图带 alpha 通道,png 转 jpg 时会丢失透明信息,水果数据集中透明背景图不多见,但遇到时要把透明区域先填充为白色背景再转,避免黑边进入训练。统一扩展名不只为了规范,更关键的是让后续逻辑可以安全假设“同一个文件名的.txt就是它的标签”。
3. 12类水果的YOLO标注格式解析与可视化验证
标注格式解析是目标检测数据集中最容易出错也最容易被跳过的环节。很多人拿到 txt 就直接开训,直到模型出现系统性误检,才回头找是不是标注坐标出了问题。
3.1 每行五个数字的几何含义
YOLO 格式的标注文件非常简单,每行五个数字:
2 0.506667 0.641667 0.180000 0.253333 2 0.766667 0.426667 0.130000 0.200000第一个数字是类别 id,后四个依次是边界框中心点的 x、y、宽度、高度。关键点是这四个值全部做了归一化,即除以了图像宽度和高度,取值应在[0,1]区间。中心点和宽高都是相对比例,所以模型训练时把图像缩放到任意分辨率,标注都不需要跟着改动,YOLO 系列能够跨分辨率训练,从根本上依赖这一点。坐标系是图像左上角为原点,x 向右增大,y 向下增大。
一个常见错误是有人把像素坐标直接写进 txt,比如中心点写成了812,而所有其他值都在零点几,模型训练时会在 loss 中出现巨大数值。这类越界错误要用脚本排查,不能靠肉眼。
3.2 将归一化坐标投影到像素并绘制检测框
可视化验证是标注质量检查中最直观的一步。把归一化坐标还原为图上像素坐标并画框,能同时暴露好几类问题:框是否贴紧目标、类别 id 是否对得上、坐标是否越界。
import cv2 CLASSES = ["apple", "banana", "orange", "grape", "watermelon", "strawberry", "peach", "pear", "mango", "pineapple", "kiwi", "lemon"] def draw_yolo_box(img_path: str, label_path: str, out_path: str): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split() if len(parts) != 5: print(f"非法行: {line}") continue cls, cx, cy, bw, bh = map(float, parts) if not (0 <= cx <= 1 and 0 <= bw <= 1): print(f"坐标越界: {line}") continue x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) color = (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, CLASSES[int(cls)], (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(out_path, img) # 使用示例 draw_yolo_box("fruit12/images/train/apple_001.jpg", "fruit12/labels/train/apple_001.txt", "check_vis/apple_001.jpg")代码里有一个容易忽略的细节:将归一化坐标还原为像素时,中心点减半宽、减半高得到左上角,加回得到右下角,全部要转成整数才能交给 OpenCV 绘图。类别 id 直接用于索引CLASSES列表,如果索引越界,多半说明 names 顺序与标注不一致。随机抽二十张训练图画框,如果超过半数出现“框明显偏向目标一侧好几像素”的情况,基本可以判定归一化坐标的计算基准有问题。
3.3 类别直方图:判断12类不平衡程度的办法
训练前做一次类别分布统计,成本极低,却常常被跳过。在十二类水果这种数据集中,不同类别的目标数量往往差异巨大,例如苹果、香蕉这类常见品类可能上千个实例,猕猴桃、柠檬可能只有几十个。类别不均衡会导致模型对样本量小的类别学习不充分,出现系统性漏检。
from collections import Counter from pathlib import Path def count_classes(label_dir: str, nc: int = 12) -> Counter: counter = Counter({i: 0 for i in range(nc)}) for p in Path(label_dir).glob("*.txt"): for line in p.read_text(encoding="utf-8").splitlines(): parts = line.strip().split() if len(parts) != 5: continue cls = int(float(parts[0])) if 0 <= cls < nc: counter[cls] += 1 return counter dist = count_classes("fruit12/labels/train") total = sum(dist.values()) for cls in range(12): ratio = dist[cls] / total if total else 0 print(f"{cls:02d} {CLASSES[cls]:<10} {dist[cls]:>6} {ratio:.3f}")这个脚本输出的就是一张最简单的类别分布表。判断标准可以放宽:如果某个类别目标数不足平均数的五分之一,训练时就要重点关注。但只有目标总数还不够,还要看“包含某类的图像数”而不是“某类的目标数”。一张图里同一个水果出现几十个实例和几十张图各出现一个,训练效果完全不同。更合理的指标是统计包含每个类别的图像张数,这个量直接决定了模型见过多少种该类别的外观变化。
4. 用YOLOv8从零训练12类水果模型
数据校验通过之后,才真正进入 yolo 目标检测流程的核心训练环节。用 YOLOv8 训练自己的数据集,绕不开三个文件层面的事:数据集配置文件、训练命令行参数、结果指标解读。
4.1 编写 data.yaml:路径语义与names顺序的坑
YOLOv8 需要一份 YAML 文件描述数据集位置和类别名。路径选择上有个常见误区:path字段如果写绝对路径,换机器就必须改;如果写相对路径,又相对当前工作目录解析,命令行在哪个目录执行就很容易跑偏。我通常把path写成相对路径,并在执行训练前用cd固定工作目录。
path: /workspace/fruit12 train: images/train val: images/val test: images/test nc: 12 names: 0: apple 1: banana 2: orange 3: grape 4: watermelon 5: strawberry 6: peach 7: pear 8: mango 9: pineapple 10: kiwi 11: lemon这里最关键的是names列表的顺序。YOLO 标签文件里存的是类别数字,数字含义完全取决于这个列表。如果实际标注文件里0是苹果,但 yaml 里0写成了香蕉,训练不会报任何错误,只会默默学习错误映射。检查方式很简单:随机挑几张图,用 3.2 节的画框脚本把类别名打印出来对照一次。
4.2 训练命令与关键参数选择
环境准备好后,训练命令如下:
pip install ultralytics yolo detect train \ data=fruit12.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ patience=20 \ project=./runs \ name=fruit12_baselinemodel=yolov8s.pt会自动下载 COCO 预训练权重,这对小规模数据集意义重大。12 类水果的数据集通常只有几千到一两万张图像,从零训练很难收敛,迁移学习可以让模型用 COCO 里学到的通用特征快速适配水果任务。几个关键参数的影响如下:
| 参数 | 默认值 | 影响与调整建议 |
|---|---|---|
| imgsz | 640 | 输入分辨率,越大越能检出小目标,但显存和训练时间线性上涨 |
| batch | 16 | 受显存限制,批量太小归一化统计不稳定,可配合梯度累积 |
| patience | 50 | 早停耐心轮数,数据集小建议降到 20,避免过拟合 |
| lr0 | 0.01 | 预训练权重下这个值偏大,我通常改成 0.001~0.002 |
| mosaic | 1.0 | 已默认开启,对密集场景有效,但最后 10 轮建议关闭 |
关于lr0多说一句:使用 COCO 预训练权重时,初始学习率不建议沿用默认的 0.01。因为预训练模型已经收敛到较好的局部最优,过大的学习率会破坏已有特征,常见做法是降到 0.001 左右,再用余弦退火调度缓慢下降。
4.3 从训练日志判断模型收敛:P、R、mAP50怎么读
训练完成后,runs/fruit12_baseline目录下会有weights/best.pt、last.pt和results.png。results.png 里最该看的是 Box P、Box R、mAP50、mAP50-95 四条曲线。
| 指标 | 全称 | 实际含义 |
|---|---|---|
| P | Precision | 预测出的所有框中正确框的占比 |
| R | Recall | 所有真实框中被找回的占比 |
| mAP50 | mAP at IoU 0.5 | IoU 阈值取 0.5 时的平均精度,偏宽松 |
| mAP50-95 | mAP at IoU 0.50:0.95 | 多个 IoU 阈值的均值,标准更严 |
常见情况是 mAP50 很高但 mAP50-95 明显偏低,这说明模型框得“差不多”,但精确位置没对齐。水果数据集里相邻果实互相遮挡时,框的位置会更松一些,可以用下面命令单独跑验证集看详细结果:
yolo detect val \ data=fruit12.yaml \ model=runs/fruit12_baseline/weights/best.pt \ project=./runs \ name=fruit12_eval验证过程会输出一个完整的评测表,包含每个类别的 P、R、mAP50、mAP50-95。逐个类对比,能快速排除“模型整体差”和“只有某几类差”这两个完全不同的原因。
5. 类别易混淆与低召回率场景的验证技巧
训练出一个 mAP50 在 0.9 左右的模型并不难,难的是验证它到底在哪些场景下不可用。十二类水果里苹果与桃子、橙子与芒果,这类颜色和形状都接近的类别,是检验模型泛化能力的好样本。
5.1 用混淆矩阵定位易混淆类别
YOLOv8 的验证环节会自动生成confusion_matrix_normalized.png。打开这张图,找到行列颜色接近的两个类别,比如苹果一行的错误预测里若有相当比例落在了桃子列,说明模型在两者之间摇摆。定位到具体类别后,可用的修正手段是检查这些混淆样本的标注质量,常见情况是标注框本身把两种果实混标了,或者训练集中这类别的样本数确实太少。
5.2 滑窗切分提升小目标召回
水果在画面中占比较小时,提升召回率的常见做法不是盲目放大 imgsz,而是对高分辨率图像做滑窗切分,把原图切成若干块分别训练。这里的难点是切分后目标可能与边界相交,标签需要同步裁剪。
def sliding_crop_anns(img, anns, tile=640, stride=320): h, w = img.shape[:2] tiles = [] anns = [(cx * w, cy * h, bw * w, bh * h, cls) for cx, cy, bw, bh, cls in anns] for y in range(0, max(h - tile + 1, 1), stride): for x in range(0, max(w - tile + 1, 1), stride): new_anns = [] for cx, cy, bw, bh, cls in anns: x1, y1 = cx - bw / 2, cy - bh / 2 x2, y2 = cx + bw / 2, cy + bh / 2 ix1, iy1 = max(x1, x), max(y1, y) ix2, iy2 = min(x2, x + tile), min(y2, y + tile) if ix2 > ix1 and iy2 > iy1: new_cx = (ix1 + ix2) / 2 - x new_cy = (iy1 + iy2) / 2 - y new_bw = ix2 - ix1 new_bh = iy2 - iy1 new_anns.append((new_cx / tile, new_cy / tile, new_bw / tile, new_bh / tile, cls)) tiles.append((tile, x, y, new_anns)) return tiles这段代码算的是裁剪后的新坐标与原坐标在裁剪区域内的交集。如果目标的可见部分不足全框的百分之五十,裁出来的标注框会包含大量背景,反而干扰训练。实践上通常要加一层过滤:目标的new_bw * new_bh相对原始面积不足 0.3 时丢弃该标签。滑窗推理时,再用原始坐标把多个裁剪窗口的检测结果合并回原图做非极大值抑制。
5.3 固定随机种子做可复现验证
最后是很多人忽略的验证纪律:目标检测训练引入的随机性来自数据加载顺序、增强参数、模型初始化等多处,不固定随机种子,两次训练结果会有几个百分点的浮动。
yolo detect train \ data=fruit12.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ seed=42seed=0固定了训练和验证的随机数生成器。对比实验结果时,只有同样 seed 下产生的差值才真正反映配置改动的影响。更进一步,可以在验证时用同一个 seed 跑三次取均值,避免单次结果抖动干扰判断。
本文还有配套的精品资源,点击获取