简介:面向葡萄生长阶段识别与目标检测任务的图像数据集,包含v0.1和v0.2两个版本,图片统一为1280×720分辨率,分别提供1212张和2099张图像,并配有3993个与6641个边界框标注。数据覆盖不同生长阶段,可支撑深度学习模型训练、农业视觉算法验证及教学实验等多种场景。压缩包整体约314.1MB,共2000个文件,主要文件类型为jpg图像、txt标注文件与xml标注文件,分别对应原图、归一化坐标标注及Pascal VOC格式标注,便于直接接入YOLO、Pascal VOC等主流框架。目前已有180人学习下载,适合计算机视觉初学者、农业智能化研究人员及算法工程师使用,可作为葡萄长势监测、成熟度判断等任务的直接训练数据。
1. 葡萄图像数据集:拿它跑目标检测前,先把这两个版本的差异搞清楚
做农业视觉检测的人,手头最缺的往往不是模型,而是干净、带框、能直接喂给 YOLO 的数据。这套葡萄不同生长阶段的图像数据集,包含 v0.1 和 v0.2 两个版本:v0.1 有 1,212 张图、3,993 个边界框,v0.2 有 2,099 张图、6,641 个边界框,统一 1280x720 分辨率。换句话说,它覆盖了从幼果、转色到成熟的不同发育阶段,每张图平均 3 个以上的框,密度适中,不会出现一张图上几十个框挤在一起的情况。适合做葡萄检测模型的微调、农业场景的迁移学习,或者拿来当作物图像数据集的基准测试。但注意:文件是以 txt 清单形式给出的,不是现成的图片文件夹,第一步必须先把数据清单理顺,否则直接当普通数据集用会翻车。
2. 数据格式与目录结构:txt 清单背后是标注信息的入口
2.1 两个版本的文件组织逻辑
拿到手是一串 UUID 命名的 .txt 文件,每个文件名对应一个文本索引。常见做法是先写一个脚本,把这些 txt 逐行读出来,看里面是图片路径、标注信息还是两者的混合。我的经验是:先不要猜,直接跑一段探测代码,把文件内容打印出来,确认结构后再决定用什么方式解析。
import glob txt_files = sorted(glob.glob("./*.txt")) for tf in txt_files[:3]: with open(tf, "r", encoding="utf-8") as f: lines = f.readlines() print(f"文件: {tf}, 行数: {len(lines)}") for line in lines[:5]: print(line.strip())这段代码的作用是快速查看前三个文件的前五行内容。glob.glob("./*.txt")匹配当前目录下所有 txt 清单文件,encoding="utf-8"是为了防止中文路径或注释乱码。如果行数差异很大,比如有的文件几百行、有的就几行,说明清单分配不均匀,后续需要合并处理;如果每行都是图片路径加空格加坐标信息,那就是标准的目标检测标注格式。
2.2 理解边界框与图像尺寸的匹配关系
数据集标注了边界框,边界框数量与图片数量不相等——因为一张图上有多个葡萄簇。假设标注格式是class_id x_center y_center width height(YOLO 格式),那么归一化坐标必须基于 1280x720 的尺寸。为什么强调这一点?因为如果标注文件里存的是绝对像素值,转 YOLO 格式时需要除以宽高;如果是归一化值,则直接可用。两个版本之间这个细节可能不一致——v0.1 和 v0.2 采集时间不同,标注工具也可能不同,常见做法是先统一成归一化坐标再进训练管线。
在写转换脚本前,我先做一步统计:看一下每个类别的框数量分布。如果某个类别的框非常少,训练时容易欠拟合,需要加大该类别的 loss 权重,或者做针对性增强。
import numpy as np counts = {} with open("label_example.txt", "r") as f: for line in f: parts = line.strip().split() cls = int(parts[0]) counts[cls] = counts.get(cls, 0) + 1 print(counts.items()) for k, v in counts.items(): print(f"类别 {k}: {v} 个框")这段代码统计每个类别出现的次数。line.strip().split()按空白字符切分,第一个字段是类别 ID,后面是坐标信息。如果这里发现类别 0 有 3000 个框、类别 1 只有 500 个框,那就是严重的数据不平衡,训练时要注意cls_loss加权。我一般会把类别分布贴到终端里,作为后续训练参数的依据。
2.3 图片资源与标注资源的对应关系
1,212 张图对应 3,993 个框,说明图片和框不是一一对应的,存在一图多框。这一步要做的,是把 txt 里的图片路径与本地图片文件做匹配,确认没有缺失或多余文件。常见坑是 txt 清单里写了train/images/xxx.jpg,但实际目录结构是images/train/xxx.jpg,路径前缀不一致,导致加载时报 FileNotFoundError。
import os img_root = "./images" with open("train_list.txt", "r") as f: img_paths = [line.strip() for line in f if line.strip().endswith(".jpg")] missing = [p for p in img_paths if not os.path.exists(p)] print(f"缺失图片数量: {len(missing)}") if missing: print(missing[:10])这段代码检查图片是否存在。endswith(".jpg")过滤出图片路径,os.path.exists(p)判断相对或绝对路径是否有效。如果缺失数量大于 5%,就要检查是清单路径前缀错误还是某些图片在采集时损坏被剔除了。检查完毕后,把 img_paths 以换行写入train.txt,作为后续训练的图片索引文件。
3. 从 txt 到 YOLO 训练管线:格式转换与训练参数调优
3.1 把标注统一成 YOLO 格式
拿到数据后第一件事不是训练,而是把标注统一成 YOLO 能吃的格式。YOLO 格式要求是class x_center y_center width height,全部是相对坐标。即使原始数据已经是归一化的,我也建议用 OpenCV 或 PIL 读图验证一遍,读取框坐标画出来看看能不能贴合葡萄区域。这一步很重要,很多数据集标注质量不高,框偏大或偏小,直接训练会影响 mAP。
import cv2 image = cv2.imread("sample.jpg") h, w = image.shape[:2] with open("sample.txt", "r") as f: for line in f: cls, xc, yc, bw, bh = map(float, line.strip().split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("visualized.jpg", image)这段代码把归一化坐标还原成像素坐标。(xc - bw / 2) * w将中心坐标转换为左上角 x 值,宽高乘以图像的宽和高得到实际像素宽高。画框后如果框没有紧贴葡萄簇,要么是标注本身有偏移,要么是坐标中心点定义不同——有的标注工具用左上角,有的用中心点,这一步能直接暴露格式问题。
3.2 划分训练集与验证集,并生成 data.yaml
划分时要注意:同一张图像可能出现在 txt 的不同清单中,必须先去重,再按 8:1:1 或 9:1 划分。由于葡萄生长阶段在 v0.1 和 v0.2 中可能重叠(同一棵葡萄在不同天拍的照片),如果两个版本要一起训练,先把图片的哈希值算出来,删除跨版本的相同图片,防止数据泄露导致验证集虚高。
import hashlib import random import os def file_hash(path): with open(path, "rb") as f: return hashlib.md5(f.read()).hexdigest() imgs = glob.glob("./all_images/*.jpg") hash_map = {} unique_imgs = [] for imp in imgs: h = file_hash(imp) if h not in hash_map: hash_map[h] = imp unique_imgs.append(imp) random.seed(42) random.shuffle(unique_imgs) n = len(unique_imgs) train_imgs = unique_imgs[:int(n * 0.8)] val_imgs = unique_imgs[int(n * 0.8):] for split, lst in [("train", train_imgs), ("val", val_imgs)]: with open(f"{split}.txt", "w") as f: f.writelines([p + "\n" for p in lst])这段代码用 MD5 去重,random.seed(42)固定随机数种子,保证每次运行划分结果一致,便于复现。int(n * 0.8)取 80% 作为训练集,剩下的 20% 作为验证集。如果算力允许,可以改成 90% 训练、10% 验证,因为该数据集属于小规模数据集,验证集太大反而让训练数据减少。
3.3 在 YOLOv8 上训练:参数选择与停止条件
YOLOv8 是当前物体检测常用的模型,在农业场景中,尤其是果实检测这类目标不算极小、背景复杂的任务,v8n 和 v8s 是首选。如果显存有限,我一般用yolov8n.pt作为预训练权重。训练时最关键的三个参数是imgsz、batch和epochs。这个数据集的图片是 1280x720,直接输入 640x640 会丢失部分细节;如果尺寸设为 1280,显存占用大幅升高。折中方式是设成 768 或 896,缩放比例接近整数,信息损失可控。
yolo detect train data=data.yaml model=yolov8n.pt epochs=150 imgsz=768 batch=16 device=0epochs=150是因为数据量只有两千多张,150 轮足够模型收敛而不至于严重过拟合。batch=16在 8GB 显存的 GPU 上跑 768 分辨率接近上限,如果显存不足就降为 8。训练过程中重点看验证集 loss 曲线:如果 val loss 在 80 轮后不再下降而 train loss 持续下降,说明开始过拟合,应当停止训练。另一个观察点是mAP50指标,农业检测一般不追求 mAP50-95 特别高,mAP50 达到 0.85 以上基本能满足实际需要。
3.4 类间混淆与大目标尺度问题
葡萄在不同生长阶段的形态差异对检测模型有显著影响。幼果期葡萄果粒小、颜色与叶子接近,转色期颜色变红但叶片遮挡多,成熟期果串大但密集量大。模型容易把转色期的葡萄误检为成熟期,因为颜色特征存在连续性。解决方式有两种:一是训练时开启 mosaic 增强和 mixup,增强模型对遮挡和颜色变化的鲁棒性;二是如果发现 val 集在某一阶段(如幼果期)的 mAP 明显低于其他阶段,需要单独提取该阶段的图片,复制后做 HSV 色彩增强再放进训练集,人为平衡各阶段比例。
4. 避坑指南:五个常见问题与排查方法
4.1 训练时 loss 为 NaN
现象:训练到第 3 个 epoch 左右,训练 loss 突然变成 NaN,终端输出大量 warning。
原因:最常见的是学习率设置过高,尤其是在加载预训练权重后,模型前期梯度更新过大;另一个原因是标注文件中存在非法坐标,比如负值或大于 1 的归一化坐标,导致 loss 计算出错。
解决:先下调学习率,从默认的 0.01 降到 0.002;再用脚本扫描所有标注 txt,检查是否有坐标值不在 [0,1] 区间内,包含则删除对应标注行。
bad_count = 0 with open("all_labels.txt", "r") as f: for line in f: parts = line.strip().split() coords = [float(x) for x in parts[1:]] if any(c < 0 or c > 1 for c in coords): bad_count += 1 print(f"异常标注行数: {bad_count}")检查坐标范围的代码。parts[1:]跳过类别 ID,只看坐标部分。if 条件判断任一坐标超出归一化区间,则该行标注非法。这种行如果不清理,轻则 mAP 下降,重则训练崩溃。
4.2 训练完 mAP 低但训练集表现好
现象:训练集 loss 很低,mAP 接近 0.9,但验证集只有 0.5 左右。
原因:数据划分时没有把同一棵葡萄树、同一时间段的图片分开,导致相似图片同时出现在训练集和验证集,验证集失去真实性;或者划分时随机种子未固定,每次实验结果不可比。
解决:划分前先给每张图片打上拍摄时间戳或葡萄品种标签,按时间分组,保证同一天的图片都在同一集合中。如果 txt 中没有这些元数据,可以按文件夹名称分组——比如名为2024-06-12的目录下所有图片全部进入训练集或验证集,不允许拆分。
4.3 多类别训练时某一类完全检测不到
现象:训练完成后,用模型预测新图,某一阶段(比如幼果期)的葡萄完全没有检出框。
原因:幼果期样本数量少,且在数据集中占比远低于成熟期;训练时模型倾向于学习样本量大的类别特征。另一个原因是幼果期的标注框过小,YOLO 训练时下采样倍数导致小目标特征丢失。
解决:针对小目标,可以开启 YOLOv8 的scale增强,让训练时随机缩放图片模拟更小尺寸目标;如果目标太小(如 20x20 像素以下),考虑使用 SAHI 切片推理,把大图切成 512x512 的小块后分别推理再合并结果。
4.4 图片路径包含中文导致加载失败
现象:报错UnicodeDecodeError或图片读取后为 None。
原因:数据清单中某些图片路径包含中文字符(如“葡萄_成熟期_01.jpg”),部分工具链在 Windows 环境下默认编码不一致。
解决:统一把图片重命名为纯英文加数字格式,例如grape_01.jpg。写一个重命名脚本:遍历所有图片文件,用os.rename把旧路径映射成新路径,同时更新标注文件中的文件名引用。
import os img_paths = glob.glob("./images/*.jpg") for idx, old_path in enumerate(img_paths): new_path = f"./images/img_{idx:06d}.jpg" os.rename(old_path, new_path)img_{idx:06d}生成六位数的序号,保证文件名长度一致且按字典序排列就是时间顺序。重命名后记得同步修改 txt 清单中的路径和标注文件中的对应字段。
4.5 数据增强过度导致验证集失真
现象:训练时 mAP 稳定上升,但实际部署到果园监控摄像头上,效果比验证集差很多。
原因:训练时开了过强的 mosaic 和 copy-paste,将多张图拼接、目标被复制到其他位置,模型学到的是“拼接图”的特征,而不是真实葡萄的生长特征。
解决:验证测试时把增强关闭,确保验证集完全采用原始图片;如果测试效果仍差,需要减小增强概率,把mosaic=0.5、mixup=0.2作为起点,逐步调高,而不是一上来就拉满。
5. 迁移学习的正确姿势:先用 v0.1 预训练,再在 v0.2 上微调
很多人拿到数据集后直接把两个版本合并训练,其实这是一种浪费。v0.1 和 v0.2 是不同拍摄条件下采集的,相当于两个域的图片。合并训练容易让模型同时面对两种光照和角度分布,收敛变慢。更合理的方案是用 v0.1 训练一个基线模型,然后冻结主干网络,只在 v0.2 上微调检测头。这样 v0.1 学习到的通用特征(葡萄轮廓、叶片纹理、果梗结构)可以作为先验,v0.2 的图片专门用于适应新域的光照和分辨率差异。微调的具体做法:先用 v0.1 训练得到best_v1.pt,然后把它作为预训练权重,训练时在 YOLOv8 中设置freeze=10,冻结前 10 层。如果 v0.2 与 v0.1 的采集设备差异很大,只微调 50 轮就够了;如果差异小,微调 30 轮即可。注意微调时的lr0要比从头训练低一个数量级——比如从头训练用lr0=0.01,微调就用lr0=0.001,这样模型不会在一个新域上快速覆盖掉已经学到的通用特征。
验证迁移学习效果时,我会在 v0.2 上同时跑两个实验:一个是用yolov8n.pt从头训练,另一个是用 v0.1 预训练权重微调。对比两者的 mAP50 和收敛速度。常见的结果是微调版本在 30 轮就达到从头训练 100 轮的效果,且最终 mAP50 高 3% 到 5%。这个收益在数据量小的时候尤其明显——因为 v0.2 只有 2,099 张图,单靠它的原始分布信息不足以学到足够深的特征。如果业务场景中图片数量长期无法扩充,这个迁移学习方案几乎是用这套数据集的标准做法。
调用微调的命令与普通训练差别不大,只是权重路径换成 v0.1 的产物,多一个冻结参数:
yolo detect train data=data_v2.yaml model=best_v1.pt epochs=50 imgsz=768 batch=16 freeze=10 lr0=0.001freeze=10表示冻结模型前 10 层——YOLOv8 的主干网络(backbone)主要集中在前几层,负责提取边缘、纹理和颜色特征,这些特征对葡萄本身依然有效,不需要重新学习。lr0=0.001保证检测头在新数据上平滑适应,不会出现 loss 振荡。微调完成后第一时间用验证集测试单张图,看看在 v0.2 图像光照条件下是否有大量的误检框出现在叶片暗部。
最后分享一个个人习惯:每次训练结束后,我都会从验证集里随机挑 20 张图,把模型预测框和 GT 框画在一起,按类别、目标尺寸分别统计漏检率。光看 mAP 是不知道模型在哪一类上翻车的——可能成熟期非常好,幼果期一团糟。可视化这一步必须强制走一遍,因为测试集里一张图的预测错误往往就对应着某个阈值的崩溃。从那以后我每次换数据集,都会先在训练前把可视化脚本写好,训练完直接出图,而不是等推理阶段才发现模型对某个生长阶段完全没有反应。这套葡萄图像数据集的分辨率和边界框质量都比较理想,转换脚本准备好后基本半小时内能跑通第一轮训练,希望帮到你。
本文还有配套的精品资源,点击获取