简介面向石榴成熟阶段检测任务的目标检测数据集覆盖开花、结果、初期幼果、中期生长与成熟五个阶段适合农业视觉研究者、目标检测开发者用于模型训练与效果验证。数据包同时提供VOC与YOLO两种标注格式压缩包共2000个文件以XML标注文件与TXT标签文件为主整体约406.5MB资源内部按图片、XML、TXT三类目录组织便于直接接入常见检测框架无需额外格式转换。全局共包含11482个矩形标注框五个生长阶段均有覆盖其中flower阶段框数最多2827个、bud次之2433个类别分布相对均衡图片清晰且未经增强标注准确规范适合作为石榴成熟度识别的基础训练数据。目前已有79人学习/下载适合需要高质量农业场景标注数据、快速搭建YOLO或VOC流程检测模型的读者使用。1. 石榴成熟阶段检测数据集一份能直接开训的标注资产如果你在做果园自动采摘或成熟度分选方案前期最磨人的不是模型选型而是数据自己拍石榴、逐张画框、按成熟阶段分桶一个月下来也未必攒够一千张干净样本。标题这套石榴成熟阶段检测数据集就是冲着这个痛点来的——5855 张图片、5 个成熟阶段、同时给 YOLO 与 VOC 两种标注格式解压后可以直接当训练集用。它适合两类人一类是刚起步做农业目标检测的工程师想跳过标注环节快速跑通 YOLO 训练流程另一类是已经跑通检测但某些阶段总是漏检想找一批真实果园样本做增量训练的人。先说我的结论数据集质量的关键不在张数而在标签一致性所以拿到包后别急着训练先按下面的流程体检。2. 数据集解压后先做体检YOLO与VOC双格式的目录组织与标签校验拿到的包里同时出现 YOLO 和 VOC不是作者多此一举。Pascal VOC 是标注工具LabelImg 那一类的通用输出每张图对应一个 xml记录目标的 xmin、ymin、xmax、ymaxYOLO 则是训练框架直接吃的格式每行一个目标内容是“类别 中心点x 中心点y 宽 高”全部归一化到 0~1。同一个标注同时导出这两种格式能让你在 LabelImg 里复查、在 YOLO 里直接训练不用自己写转换脚本。但双格式也意味着错误可能只出现在其中一边xml 是对的、txt 导出时越界txt 是对的、xml 漏了目标。所以解压后的第一件事不是翻 README而是对两个目录分别过一遍体检脚本。2.1 两种格式的差异从 xml 的绝对坐标到 txt 的归一化坐标先看 VOC 的 xml 长什么样。每个 object 节点对应一个框坐标是像素绝对值并且用红绿灯式的命名把阶段标签写在 name 里annotation folderimages/folder filenamepomegranate_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namestage_1/name bndbox xmin512/xmin ymin340/ymin xmax783/xmax ymax610/ymax /bndbox /object /annotation而同一张图的 YOLO 标注在 txt 里只有一行数字分别是“类别编号、归一化中心点 x、归一化中心点 y、归一化宽、归一化高”1 0.337239 0.439815 0.141146 0.25把这两份放到一起对比你就明白为什么转换工具一旦在边界处浮点出错txt 里就会出现负数或大于 1 的值。物体贴边时xmax 除以宽度得到 0.99999四舍五入后变成 1.0000001训练框架读数据时虽然不一定崩但会让模型的回归目标出现一个永远学不到的极值。下面脚本重点查的就是这类问题。2.2 一键体检统计类别、空标签与越界框拿到数据集目录后我一般先写一个无依赖的 Python 脚本把 labels 目录整体扫一遍。它只做四件事统计每个类别有多少框、找空标签、找格式错误行、找坐标越界或宽高非正的行。import os import glob IMG_DIR images # 图片目录按实际解压路径改 LABEL_DIR labels # YOLO txt 标注目录 CLASS_FILE classes.txt # 数据集的类别清单一行一个类名 # 读类别清单真实类名以解压后的文件为准这里只拿来做编号映射 with open(CLASS_FILE, encodingutf-8) as f: class_names [line.strip() for line in f if line.strip()] counts [0] * len(class_names) empty_labels [] bad_lines [] overflow [] for txt_path in glob.glob(os.path.join(LABEL_DIR, *.txt)): lines [line.strip() for line in open(txt_path, encodingutf-8) if line.strip()] if not lines: empty_labels.append(txt_path) continue for idx, line in enumerate(lines): parts line.split() if len(parts) ! 5: # YOLO 格式必须是 5 个数字 bad_lines.append((txt_path, idx, line)) continue cls, xc, yc, w, h parts cls int(cls) vals [float(xc), float(yc), float(w), float(h)] if cls len(class_names): bad_lines.append((txt_path, idx, line)) continue counts[cls] 1 if any(v 0.0 or v 1.0 for v in vals) or vals[2] 0.0 or vals[3] 0.0: overflow.append((txt_path, idx, line)) print(类别统计:, dict(zip(class_names, counts))) print(空标签文件:, len(empty_labels)) print(格式错误行:, len(bad_lines)) print(坐标越界或非正宽高的行:, len(overflow)) for p, i, l in overflow[:10]: print(p, 第, i, 行:, l)脚本的核心逻辑是txt 文件和图片分别在两个目录下类别编号从 0 开始递增越界检查用 0~1 的闭区间同时把 w、h 小于等于 0 的行也归为异常。参数上需要你按实际情况改 IMG_DIR、LABEL_DIR 和 CLASS_FILE 三个路径其中 CLASS_FILE 必须和数据集 labels 目录对应否则类别统计会错位。空标签和越界行不会直接让训练崩溃但会变成 mAP 的暗伤尤其是越界框会让损失函数在训练早期震荡表现为前二十个 epoch 的 box_loss 始终不降。2.3 图片和标注对齐缺图缺标、坏图与 EXIF 旋转体检脚本跑完没有异常不等于能直接训练还要查一遍图片和标注的配对关系。常见做法是写一个 stem 对比取图片文件名去掉后缀的主名去 labels 目录找同名 txt反过来再查一遍两边都缺才算异常。注意石榴数据集经常有 .jpg 和 .JPG 混用的情况Linux 下大小写敏感Windows 下不敏感如果你跨平台拷贝目录很容易出现“我明明有这张图训练时却报 FileNotFoundError”的问题。还有两个在农业数据里很高频的坑坏图和 EXIF 旋转。手机或相机拍摄的 JPG 可能带着 EXIF 里的 Orientation 字段缩略图看着正但训练框架读出来是旋转 90 度的框全错位。解决办法是统一转正再训练用 ImageMagick 或 Python 的 PIL 做一次性重导出别指望训练框架帮你处理。坏图更隐蔽图片能打开但不完整OpenCV 读出来是 NoneYOLO 训练到一半才崩。体检阶段直接把所有图片过一遍cv2.imread读不出来的单独挑出来删掉或补拍。检查项怎么查失败迹象图片-标注同名对比两边文件的 stem 集合训练时报 labels 或 images 缺失后缀大小写检查 .jpg/.JPG/.jpeg跨平台拷贝后找不到文件坏图cv2.imread 读不到训练中断报 NoneTypeEXIF 旋转看 Orientation 字段mAP 正常但框全部偏移这组检查做完数据集才算“可训练”。别嫌这 20 分钟浪费后面的翻车大多能在这一步找到源头。3. 把5855张图切好train/val/test划分原则与三个边界坑体检通过后就是划分数据集。很多人直接random.shuffle然后按比例切这在果园场景里是典型的翻车操作。石榴成熟度数据的采集通常是按“日期、地块、树号”成组拍的同一组的相邻帧背景、光照、果面几乎一样。如果同组的相似图片同时被塞进 train 和 val模型在验证集上的 mAP 会虚高到离谱换到新果园立刻掉点。我现在的习惯是划分的基本单位不是单张图片而是“采集组”。3.1 直接 shuffle 为什么在果园场景里会翻车假设一组照片是同一棵树下朝四个方向拍的其中两张进 train、两张进 val。模型在训练时已经记住了这棵树的叶子纹理和光照方向val 里剩下的两张几乎是开卷考试mAP 高不代表它能认新树。更麻烦的是成熟阶段在时间上是连续的同一棵树采收前十天拍的照片果实颜色每天都在变如果不同日期的同棵果树被分到 train 和 val模型实际上在“背日期”而不是在学阶段特征。所以划分前先确认分组键文件名公共前缀、子目录名、采集时间戳任选一种能代表“同一采集单位”的键来做组级划分。3.2 按组划分的脚本与三种分组键下面这个脚本按“文件名前 8 位”作为分组键把整组图片分配到一个集合里再用固定随机种子洗牌按 70/20/10 输出三个 txt 文件。输出的是图片绝对路径列表符合 YOLO data.yaml 对 train/val/test 的约定不用移动任何文件。import os import random from collections import defaultdict IMG_DIR images SEED 42 # 固定种子保证划分可复现 RATIO (0.7, 0.2, 0.1) random.seed(SEED) # 按采集组收集图片分组键按实际文件名规则改常见是前缀、日期或目录名 groups defaultdict(list) for img_name in os.listdir(IMG_DIR): stem os.path.splitext(img_name)[0] key stem[:8] groups[key].append(os.path.join(IMG_DIR, img_name)) group_keys list(groups.keys()) random.shuffle(group_keys) n len(group_keys) n_train int(n * RATIO[0]) n_val int(n * RATIO[1]) assigned {} for i, key in enumerate(group_keys): if i n_train: assigned[key] train elif i n_train n_val: assigned[key] val else: assigned[key] test for split in [train, val, test]: out_path f{split}.txt with open(out_path, w) as f: for key, group in groups.items(): if assigned[key] split: for img_path in group: f.write(os.path.abspath(img_path) \n) print(split, 组数:, sum(1 for k in assigned if assigned[k] split))逻辑上是两层循环先按分组键洗牌再按比例切组数最后展开成图片路径写入文件。有两个参数要按数据实际情况调一是分组键的截取长度文件名前 8 位只是示例如果你的文件名是20241015_tree03_001.jpg这种建议直接用前 11 位或者按目录名分组二是 RATIO底座数据量够大用 70/20/10偏小就改成 80/10/10并考虑用五折交叉验证代替单次划分。注意SEED 必须固定。固定种子后每次跑脚本生成的 train/val/test 完全一致这对后面调参、复现结果、写实验记录都是“后悔药”。不固定种子你的实验可能因为一次重跑换了验证集所有对比都失去意义。3.3 划分后立刻回读每个阶段的 val 数量不能太少划分脚本输出完别急着进训练先回读统计。最简单的方式是复用第 2 章的体检脚本把 val 的 labels 目录单独扫一遍确保 5 个阶段在 val 里都有足够样本。我的经验阈值是每个阶段的框不少于 50 个少于这个数量mAP 的方差会大得没法看同一个模型换一次划分结果能差 5 个点以上。如果发现某个阶段在 val 里只有十几框说明分组键粒度太大或者该阶段本身样本就少这时候有两个选择一是把该阶段在 train 里分一部分出来补二是改用按类别分层划分先按 stage 分组、再在每组内部按比例抽 val保证每个阶段的 val 占比一致。分层划分要改的地方不多把 labels 读进来建立“图片 stem - 类别集合”的映射然后对每个类别单独做随机划分最后合并去重。代价是代码复杂度上去一点但对多阶段检测任务是值得的。另一个边界坑是测试集同样要检查很多项目只有 train/val 没有 test最后用 val 当 test 报 mAP等你上线才发现泛化能力没那么好。规划时就把 test 留出来哪怕只占 10%它也是你未来判断模型能不能上线的唯一可信参考。4. 基于YOLO训练成熟度检测模型从yaml配置到损失曲线判读很多人拿到这份数据集的第一反应是套 yolov8 训练自己的数据集这确实是目前最稳的路径。农业场景下果实检测对实时性有硬要求采摘机械臂跟前跑两阶段检测器算不过来而成熟度只有 5 类不需要重型分类头。YOLO 系列在嵌入式设备上的部署生态也最成熟导出 ONNX 或 TensorRT 都有现成管线。模型选型上我建议先用 s 级模型做基线跑通全流程确认数据没问题后再考虑 m 或 l 级压精度别一上来就开大模型烧显卡。4.1 生成数据集的 yaml 配置路径、类别数与类别名YOLO 训练的第一步是把数据组织成它认识的 yaml。你只需要告诉它数据集根目录、三个 txt 的路径和类别信息。注意第 3 章生成的 train.txt/val.txt/test.txt 要放在数据集根目录下yaml 里的路径都相对根目录写。# granet5.yaml 石榴成熟阶段检测配置 path: /path/to/dataset # 改成你的数据集根目录 train: train.txt val: val.txt test: test.txt nc: 5 names: [stage_0, stage_1, stage_2, stage_3, stage_4]names 里我用的是占位名目的只是先把流程跑通。实际数据集里如果带 classes.txt以那个文件里的真实类名为准如果类名是中文直接写进 yaml 也能跑但要注意文件编码必须是 UTF-8。nc 必须和 classes.txt 的行数一致多写或少写会在训练启动时报维度不匹配。4.2 训练命令与一组起步超参imgsz、batch 与 mosaic配置写好后训练命令本身很简短。下面这组参数是我在类似果实检测任务上的起步值适合单卡 16GB 左右显存yolo detect train \ dataconfigs/granet5.yaml \ modelyolov8s.pt \ epochs200 \ imgsz1280 \ batch16 \ patience40 \ projectruns/granet \ nameexp1几个关键参数的解释imgsz 设 1280 是因为石榴在远景照片里可能只有几十像素640 的输入会把小目标压缩成几个像素召回率掉得很快如果你想先快速验证管线可以先用 640 跑通再上 1280。batch 是显存弹性项16GB 显存跑 1280 输入时 batch16 接近上限显存不足就降到 8 或 4同时可以用梯度累积补回来。patience40 意思是 40 个 epoch 内 val 指标没有提升就早停这里我习惯给大一点因为成熟度任务后期 mAP 的爬升很平缓给太少会在拐点前被掐断。提示imgsz 从 640 提到 1280显存占用接近 4 倍。如果你的卡跑不动优先保持 1280 降 batch而不是降 imgsz——果实这类小目标对分辨率太敏感。mosaic 增强默认是开的它能大幅提升模型对遮挡和密集场景的鲁棒性但也有代价石榴相互遮挡严重mosaic 把四张图拼在一起时边框处的果实会被裁一半标签却没被裁模型被迫学一堆“半截目标”。如果训练时发现 box_loss 后期不降可以试试把 mosaic 降到 0.5 或在最后 10 个 epoch 关闭它。不同版本 YOLO 的 CLI 参数名略有差异训练前先跑一次yolo detect train --help确认参数写法别让命令在参数名上报错。参数起步值调整建议imgsz1280显存不够先降 batch不要降 imgszbatch16按显存向下调 8/4配合梯度累积mosaic1.0遮挡严重导致边角目标学不动时降到 0.5patience40后期提升慢不要小于 30epochs200配合早停实际多数在 100 epoch 内收敛4.3 从损失曲线判断模型是没学够还是学歪了训练开始后我会盯三个指标box_loss、cls_loss、dfl_loss。box_loss 是框的回归误差它降不动说明定位没学好常见原因是标签里框太松或太紧cls_loss 是分类误差石榴成熟度之间本来就是连续过渡cls_loss 降到一定程度不再降是正常的不必强求。更值得警惕的是 val/box_loss 在某个 epoch 后反弹而 train/box_loss 还在降——这是典型的过拟合说明模型在背训练集的光照和背景。训练输出目录里会同时保存 best.pt 和 last.pt。best 是按验证集指标选的last 是最后一个 epoch 的权重。如果你开了 mosaic 且最后 10 个 epoch 才关掉 mosaiclast 往往比 best 更值得拿去测试因为它的训练分布更接近真实推理场景。我通常两个都留下来最后用混淆矩阵对比选出真正能落地的那个。5. 避坑五条让石榴成熟度训练翻车的标注与预处理细节这份数据集整体质量可能不错但多阶段成熟度任务有它自己的一套暗坑跟常规目标检测不一样。下面五条是我在不同成熟度项目里踩过的每一条都按“现象 - 原因 - 解决”讲清楚。它们不一定全部发生在你身上但发生了以后你知道往哪个方向查。5.1 同画面多阶段混生成熟度边界不是“非黑即白”现象训练出来的模型在单果图像上表现正常一到整树照片就大量误检同一个石榴相邻两帧被标成不同阶段。原因石榴成熟是连续过程同一棵树上早熟果和晚熟果并存标注员对“半熟”和“可采”的理解不一致标签边界本身是模糊的。解决拿到数据集后先做一次标签一致性抽检重点看每个阶段相邻两类之间是否有明显可分的颜色或纹理特征如果两个相邻阶段的框有一半以上看起来差不多考虑把 5 类合并成 3 类未熟、可采、过熟重新训练精度通常比强行分 5 类高。5.2 反光和高光让“成熟阶段”跟着光影走现象同一颗石榴上午拍的被识别为 stage_2下午补光后变成 stage_3置信度都很高。原因成熟度判断高度依赖表皮颜色而石榴表皮有蜡质反光阳光直射下高光区域的颜色饱和度和色相都变了。解决训练前对全数据集做光度统计把每张图的亮度均值和标准差打出来如果样本间差异过大在数据增强里把 hsv_h、hsv_s、hsv_v 的扰动范围调大一点让模型不过度依赖某一光照条件下的绝对颜色。更好的办法是采集时统一使用漫射光或背光面拍摄但这超出数据集本身能控制的范围。5.3 VOC 转 YOLO 后中心坐标越界一个 1.0000001 引发的崩溃现象第 2 章体检脚本跑出几十条坐标大于 1 的行单独看每一条只超了一点点删除后训练正常了但 mAP 比预期低。原因VOC 的 xmax 是像素值除以图片宽度时浮点四舍五入可能得到 1.0000001更隐蔽的是目标完全贴边时xmin 为 0中心点算出 0.0000001看似合法但宽高比已经失真。解决不要直接删这些标注而是做夹逼处理把越界值截断到 0~1 区间同时过滤掉 w 或 h 小于 0.0001 的极端小框处理完后再跑一遍体检脚本确认干净。这类问题在后续增量训练中还会出现建议把它写进数据管线的固定环节。5.4 val 里某个阶段样本太少mAP 高得像玄学现象训练结束打印的 mAP 0.5 有 0.92但换一批测试图过熟阶段的误检率惨不忍睹。原因五阶段里未熟和可采样本多过熟样本少随机划分后 val 里过熟只有十几个框模型只要把所有深色果实都判成过熟这个类别的 AP 也不会太难看整体 mAP 被多数类撑住了。解决训练前必须按类别分层划分如果某些阶段总数确实太少先在 train 里做该阶段的过采样复制再做分层划分。判断标准很简单打印混淆矩阵看最后一类的行和列如果多数预测落在相邻阶段而不是对角线说明这个阶段的“学会”是假象。5.5 训练中断后没有存档没有后悔药的跑路现象训练到 150 个 epoch 时断电或显存炸了重启后发现项目里没有可用的断点权重只能从头再来。原因默认配置下 last.pt 会定期覆盖保存但很多人把 project 和 name 参数设成了临时目录跑完手工拷贝忘了中断恢复这件事。解决训练命令里固定 project 和 name让每次实验的输出留在原地中断后用yolo detect train resumeTrue projectruns/granet nameexp1恢复训练。这里的坑是 resume 必须和原实验的配置完全一致改了 imgsz 或 batch 可能导致优化器状态加载失败所以实验启动前把命令完整记到实验笔记里别只记一个结果。6. 用混淆矩阵和置信度阈值把五个阶段调到能落地6.1 五阶段任务要盯混淆矩阵而不是单看 mAP成熟度检测和普通目标检测还有一个不同错误不是对称的。把 stage_1 误判成 stage_2 可能只是提前两天采摘损失不大把 stage_1 误判成 stage_4 会让整批果实进废品线。所以训练结束后我不会只盯着 mAP而是直接看混淆矩阵——每一行是真实类别每一列是预测类别对角线的占比决定了这个模型能不能上产线。相邻阶段混淆是成熟度任务的常态但“隔阶段”的混淆一旦超过几个百分点说明模型学到的主要是颜色深浅而不是更具体的成熟特征。6.2 跑一次带混淆矩阵的验证并调整置信度用训练好的 best.pt 对 val 集重新验证顺手把混淆矩阵和每类的 PR 曲线打出来from ultralytics import YOLO model YOLO(runs/granet/exp1/weights/best.pt) metrics model.val( dataconfigs/granet5.yaml, splitval, imgsz1280, conf0.25, ) matrix metrics.confusion_matrix.matrix print(matrix)metrics.confusion_matrix.matrix 是一个形状为 (nc1, nc1) 的矩阵最后一列代表预测了目标但没有对应真值也就是误检框。你可以把 conf 从 0.25 改成 0.5 和 0.1 各跑一次看误检率和漏检率怎么变conf 调高误检变少但小果漏检变多调低则反过来。产线场景我更愿意牺牲一点 recall 换 precision因为漏检可以由多帧检测或传感器补充误检直接送错分选通道更麻烦。6.3 把成熟度变成产量统计检测只是第一步我个人会把这类模型往产量预估上再推一步统计同一批果树照片里每个阶段的框数量按时间序列绘制成熟度比例变化用来指导采收排期。这就对置信度阈值的一致性提出了更高要求——同一批照片必须用同一套 conf 和 IOU 参数跑否则数量没法对比。我的习惯是把调好的 conf 写进部署配置里固定下来而不是每次手填。这套流程走完你会发现解决“石榴是什么阶段”只是开始真正有价值的是稳住阈值后对果园做的统计决策。我从第 2 章的体检脚本一直走到混淆矩阵唯一的体会是数据集的每一分细致都会在训练结果里连本带利还给你。希望帮到你。本文还有配套的精品资源点击获取