简介:目标检测任务中,石榴成熟阶段识别是农业智能化与果园管理的重要环节。这份数据集面向计算机视觉初学者及农业AI项目开发者,提供5855张清晰标注的成熟阶段检测图片,覆盖花蕾、早果、盛花、中果、成熟五个阶段,共11482个矩形框,可直接用于YOLO系列和Faster R-CNN等检测模型的训练与验证。压缩包整体约406.53MB,共2000个文件,以1999个XML标注文件与1个TXT说明文件为主,分别对应VOC格式与YOLO格式的标签文件,图片、XML、TXT按目录分类存放,便于工程化读取与格式转换。数据未做增强处理,标注严谨、质量可靠,可有效减少数据采集与清洗耗时。除检测模型训练外,还可用于农业场景下的果实计数、成熟度分析等任务。目前已有79人学习浏览,适合作为目标检测项目的基础数据资源。
1. 石榴成熟阶段检测:比普通目标检测更考验工程判断的一类任务
把“找到石榴”和“判断石榴到了哪个阶段”放在一起,难度不是叠加,是乘出来的。普通目标检测的类别差异是离散的,而成熟度检测的五个阶段之间没有硬边界,同一个果实换个角度、换个光照,视觉特征就可能滑到相邻类别里去。这个标题给的是 5855 张石榴图像、5 个成熟阶段、YOLO 与 VOC 双格式标注的现成数据集,适合两类人用:一类是做农业自动化采摘或分级的工程师,需要快速验证一个检测方案;另一类是刚入门目标检测,但想避开“只跑通官方示例”这个舒适区的研究者。拿到这种数据,第一件事不是直接训练,而是先把标注格式、阶段分布和类别边界梳理清楚。
2. 5855张石榴图像与5阶段标注:数据集构成与格式拆解
2.1 五个成熟阶段的划分逻辑与边界问题
石榴从坐果到采收的周期里,果皮颜色由绿转黄绿、再转红、最后变暗红或开裂,标注这五个阶段时不同标注员的主观判断差异很大。常见做法是把“全绿且果实体积未定型”标为阶段一,“绿中带红晕”标为阶段二,“红绿相间但红色未连片”标为阶段三,“全红且果皮有光泽”标为阶段四,“果皮颜色发暗、出现裂纹或明显过熟斑”标为阶段五。这套划分在农业上对应“不可采、暂缓采、可试采、适采、过熟”五个商业动作。
阶段三和阶段四是最容易混淆的区域,因为红色占比是一个连续量,不是开关量。有些石榴在遮阴面还是绿色,但向阳面已经全红,模型很难判。另一个常见坑是类别不平衡:果园巡检数据里,适采期的图像往往最多,转色期和过熟期的样本明显偏少。训练前先统计类别分布,如果少数类占比低于 10%,就需要考虑重采样或者在损失函数里加权重,否则最终模型对临界阶段的召回会很难看。
2.2 YOLO格式与VOC格式的字段差异
这个数据集同时给了 YOLO 和 VOC 两种标注,但两者转换时容易丢信息,所以先把字段差异说清楚。VOC 格式是每个图像对应一个 XML 文件,框坐标是像素绝对值,以左上角为原点,记录 xmin、ymin、xmax、ymax 四个整数;YOLO 格式是每个图像对应一个 txt 文件,每行一个检测目标,记录类别索引、中心点 x、中心点 y、宽度 w、高度 h,五个数值全部相对图像宽高做了归一化。表面看只是坐标系不同,实际使用中有三个影响训练结果的点。
| 维度 | VOC(.xml) | YOLO(.txt) |
|---|---|---|
| 坐标基准 | 像素绝对坐标,整数 | 相对图像宽高的浮点数,0~1 |
| 框表达方式 | xmin, ymin, xmax, ymax | x_center, y_center, width, height |
| 类别表示 | <name>节点中的字符串 | 行首整数,对应 data.yaml 类别索引 |
| 单图多目标 | 多个<object>节点 | 每行一个目标,行数等于目标数 |
YOLO 格式不保存图像尺寸信息,单独看 txt 文件无法还原像素坐标,所以转换工具里必须有原图尺寸。反过来,VOC 的 XML 里有<size>节点保存宽高,转 YOLO 时直接取值即可,不需要额外读图。另一个细节是类别索引必须和训练时的 data.yaml 声明保持一致,否则模型会把阶段一当成阶段二来学。
2.3 用脚本给5855张图做一次标注体检
拿到的标注不一定干净,常见问题包括:坐标值越界、负宽高、类别索引超过声明范围、空 txt 文件、同一条记录重复出现。我一般会先跑一个健康检查脚本,把这些问题全量扫一遍,再决定是否直接使用或挑出脏数据重新标注。
# inspect_yolo_labels.py # 用法: python inspect_yolo_labels.py --labels ./labels --num_classes 5 import argparse from pathlib import Path from collections import Counter import numpy as np def main(): parser = argparse.ArgumentParser() parser.add_argument("--labels", type=Path, required=True, help="YOLO标签目录") parser.add_argument("--num_classes", type=int, default=5, help="类别总数") args = parser.parse_args() cls_counter = Counter() box_wh = [] # 收集归一化框宽高,查看尺度分布 malformed = [] # 格式异常文件 empty_files = [] # 空标签文件 for txt_path in sorted(args.labels.glob("*.txt")): lines = [ln.strip() for ln in txt_path.read_text().splitlines() if ln.strip()] if not lines: empty_files.append(txt_path.name) continue for ln in lines: parts = ln.split() if len(parts) != 5: malformed.append((txt_path.name, ln)) continue cls_id, xc, yc, w, h = map(float, parts) if cls_id >= args.num_classes or w <= 0 or h <= 0: malformed.append((txt_path.name, ln)) continue if not (0 <= xc <= 1 and 0 <= yc <= 1): malformed.append((txt_path.name, ln)) continue cls_counter[int(cls_id)] += 1 box_wh.append((w, h)) print("类别分布:", dict(sorted(cls_counter.items()))) if box_wh: wh_arr = np.array(box_wh) print("框宽均值: %.4f, 框高均值: %.4f" % (wh_arr[:, 0].mean(), wh_arr[:, 1].mean())) print("空标签文件数:", len(empty_files), empty_files[:5]) print("异常标注行数:", len(malformed), malformed[:5]) if __name__ == "__main__": main()脚本里对 cls_id 做了整数比较,避免类别索引写成"2.0"这种浮点字符串导致转换后匹配不上。框宽高均值能帮助判断是整棵树级别的检测还是果实级别的检测,如果框太小,后期做 NMS 或数据增强时要考虑裁剪边界。空标签文件要重点排查,尤其是图像里确实有果实但标注遗漏的情况,这种情况最影响训练——模型会以为“无目标”才是正确输出。
3. VOC与YOLO互转:坐标换算与数据集划分实战
3.1 从四角坐标到中心点坐标的换算逻辑
VOC 转 YOLO 时的核心公式不复杂,但容易写错符号。VOC 记录的是左上角 (xmin, ymin) 和右下角 (xmax, ymax),YOLO 需要的是中心点坐标和宽高,并且全部要用图像宽高做归一化。中心点 x 的计算是(xmin + xmax) / 2 / img_width,框宽是(xmax - xmin) / img_width,y 和 h 同理。这里最容易踩坑的是把除法的分子写成xmax - xmin但忘了除以图像宽度,导致一个像素值混进归一化空间,训练时框位置严重偏移。转换后拿一张图做可视化核对,比看一百条数字管用。
3.2 数据集划分要考虑光照和拍摄批次
划分训练集、验证集、测试集时,不能直接对所有文件做随机 shuffle。果园采集数据通常是一棵树上连续拍几十张,同一棵石榴树下相邻照片的背景、光照、果实位置高度相似,随机划分会把同株果实的照片同时分进训练集和验证集,导致验证指标虚高。更稳妥的做法是把同一个采集批次或同一棵树的图像视为一个组,整组划入训练或验证。如果文件命名里带拍摄时间或树号信息,可以按这个前缀分组;如果没有任何分组信息,那就只能退回到随机划分,但要意识到评估结果会比真实场景乐观。
类别分层也是一个要点。阶段一和阶段五的样本通常远少于阶段四,验证集里少数类可能只有几十条,mAP 波动会非常大。建议按类别比例做分层抽样,确保验证集里每个阶段都有足够样本。常见比例是 7:2:1,数据量大且类别均衡时也可以用 8:1.5:0.5。
3.3 一个可直接改写的VOC转YOLO转换脚本
# voc2yolo.py # 用法: python voc2yolo.py --voc_dir ./annotations --image_dir ./images --out_dir ./labels import xml.etree.ElementTree as ET from pathlib import Path import argparse CLASSES = ["immature", "color_turning", "half_ripe", "ripe", "overripe"] # 顺序与data.yaml一致 def voc_to_yolo(xml_path: Path, img_w: int, img_h: int) -> list[str]: tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): cls = obj.find("name").text.strip() if cls not in CLASSES: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) xc = (xmin + xmax) / 2.0 / img_w yc = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h cls_id = CLASSES.index(cls) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") return lines def main(): parser = argparse.ArgumentParser() parser.add_argument("--voc_dir", type=Path, required=True) parser.add_argument("--image_dir", type=Path, required=True) parser.add_argument("--out_dir", type=Path, required=True) args = parser.parse_args() args.out_dir.mkdir(parents=True, exist_ok=True) for xml_path in sorted(args.voc_dir.glob("*.xml")): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = voc_to_yolo(xml_path, img_w, img_h) out_txt = args.out_dir / (xml_path.stem + ".txt") out_txt.write_text("\n".join(lines)) if __name__ == "__main__": main()这里有一个容易忽视的问题:转换时读的宽高来自 XML 里的<size>节点,不是实际图像文件。如果标注工具写入的 size 和图像真实尺寸不一致,转换出来的 YOLO 坐标全部错位。稳妥做法是在转换前后随机抽样,用 OpenCVimread读图对比实际宽高,确认一致再批量执行。CLASSES 列表顺序改动会导致所有标签重新映射,所以训练前把它和 data.yaml 里的 names 字段对齐是最高优先级。
4. YOLOv8训练配置:成熟度边界的数据集该怎么做超参数调优
4.1 用data.yaml声明五个阶段并启动训练
拿到整理好的标签后,先写训练用的 data.yaml。路径建议用绝对路径或相对 data.yaml 文件的路径,避免不同机器上目录结构不一致导致训练中断。
# data.yaml # 数据集根目录 path: ./datasets/pomegranate train: images/train val: images/val test: images/test nc: 5 names: 0: immature 1: color_turning 2: half_ripe 3: ripe 4: overripe启动训练的命令如下:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ imgsz=640 \ batch=16 \ epochs=120 \ optimizer=AdamW \ lr0=0.001 \ device=0model=yolov8s.pt是在 COCO 预训练权重上继续微调,迁移学习对农业场景通常有帮助。imgsz=640是常规选择,但如果石榴在图像里占比大,可以降到 512 提升推理速度;如果一棵树上果实小且密集,可以升到 768。batch的值以显存不爆为下限,16 到 32 是常选区段。epochs设在 100 到 150 之间,成熟阶段类别边界模糊,收敛比普通检测慢一些,只跑 50 轮往往欠拟合。
4.2 五个关键超参数与不同场景的推荐值
| 参数 | 推荐区间 | 说明 |
|---|---|---|
| imgsz | 512 / 640 / 768 | 果实占比大用小值,小目标多用大值 |
| batch | 16 / 32 | 受显存限制,小 batch 配小数据增强 |
| epochs | 100 / 120 / 150 | 看验证损失是否还有下降趋势 |
| optimizer | AdamW / SGD | 数据量小用 AdamW,数据量大用 SGD 更稳 |
| lr0 | 0.001(AdamW)/ 0.01(SGD) | 预训练权重迁移时不宜过大 |
显存不足时优先降 batch 而不是降 imgsz,因为降 imgsz 会直接损失小目标的框精度。训练过程中要盯着 val 损失,如果训练损失持续下降但验证损失在第 40 轮后不再变化甚至回升,就是过拟合信号,可以提前停止或把数据增强里的翻转概率调高。
4.3 YOLOv8损失函数里和阶段边界相关的两个细节
YOLOv8 的损失由三部分构成:边界框损失box_loss、分类损失cls_loss、分布式焦点损失dfl_loss。默认权重是 box=7.5、cls=0.5、dfl=1.5,但成熟阶段检测里最需要关注的不是默认权重,而是相邻类别的误判模式。阶段二和阶段三、阶段四和阶段五之间的视觉差异小,模型输出概率会在这两对类别上出现明显的高分纠缠,这时如果少数类样本不足,分类损失会主导训练方向,导致模型把边界样本一路推向多数类。
常见做法是在训练时把类别权重传入损失函数,让少数类的分类错误产生更大的梯度。Ultralytics 提供class_weights参数,也可以自己在损失函数外部做一个小技巧:对每个类别的损失乘一个权重系数,权重取总样本数除以各类别样本数的平方根再归一化。类别差别不超过 3 倍时,这个操作带来的收益有限;一旦超过 5 倍,几乎必须做。
另一个细节是dfl_loss,它负责让框的分布更精准。如果标注框本身边界画得松,比如有的标注员把石榴外圈果皮算进去,有的只包住果肉区域,dfl 会把框学得不够收敛。遇到这种情况,先锁模型权重在 0.6 以下,用一个辅助脚本统计同一张图不同标注员的框交并比,框质量太差不做清洗就训练,后期无论怎么调参都要从标注重来。
5. 评估不只看mAP:用混淆矩阵定位成熟阶段误判方向
5.1 从验证命令到mAP指标的读取方式
训练完成后跑验证的命令很简单:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml \ conf=0.001 \ iou=0.7 \ device=0conf=0.001是把置信度阈值压到最低,保证所有检测框都参与评估,这样算出的 mAP 更客观;iou=0.7是指预测框和真实框的交并比大于 0.7 才视为匹配,这个值在果实密度高的图片里可以调到 0.5,因为果实相互遮挡时标注框本身就很难完全对齐。训练过程会自动生成results.png,里面有 PR 曲线、F1 曲线和混淆矩阵热力图。
5.2 mAP50和mAP50-95怎么穿插使用
mAP50 衡量的是预测框和真实框 IoU 大于 0.5 时的平均精度,对定位误差容忍度较高;mAP50-95 是从 0.5 到 0.95 按 0.05 步长取十个阈值分别计算 mAP 再取平均,对框的精度要求严苛。成熟阶段检测场景里,如果只关心果实能不能被采摘执行器对准,mAP50 够用;如果要做精细分拣,mAP50-95 更能反映框边界的质量。两个指标需要同时看,不能只追某一个。
5.3 混淆矩阵里的阶段误判怎么定位
混淆矩阵的横轴是真实类别,纵轴是预测类别,对角线越亮越好。注意看对角线两侧的邻值方块,阶段二和阶段三之间的误判,以及阶段四和阶段五之间的误判,方向不同处理方式不同。如果阶段四被误判成阶段五,多半是颜色过深的样本过多,模型被带偏;如果阶段五被误判成阶段四,通常是过熟期的特征不够明显,比如裂果样本太少。前者通过加重少数类权重解决,后者要补充或合成过熟样本。
| 误判模式 | 可能原因 | 建议处理 |
|---|---|---|
| 阶段四 → 阶段五 | 适采期不可采样本偏多 | 降低多数类权重或用难例挖掘 |
| 阶段五 → 阶段四 | 过熟样本量不足 | 补充标注或做颜色增强 |
| 阶段二 ↔ 阶段三 | 转色期视觉边界本身模糊 | 局部类别重定义,合并或细分 |
6. 采摘机器人上的成熟度稳定输出:置信度时间滑动平均
训练和评估之后,实际部署时的一个常见痛点是单帧检测结果不稳定。相机是在移动中拍摄石榴的,角度、遮挡、光照变化会让同一个果实连续几帧里阶段判断来回跳,直接影响后端执行器该不该摘。一个有效做法是让模型输出连续帧的置信度向量,做指数滑动平均,再取最终类别。
# confidence_smoother.py # 用法: 初始化后逐帧输入YOLO输出的5维置信度向量 import numpy as np class MaturitySmoother: """对连续帧的成熟度置信度做指数滑动平均""" def __init__(self, alpha: float = 0.4): self.alpha = alpha self.smooth = None def update(self, probs: list[float]) -> int: arr = np.asarray(probs, dtype=float) if self.smooth is None: self.smooth = arr else: self.smooth = (1 - self.alpha) * self.smooth + self.alpha * arr return int(np.argmax(self.smooth))alpha取值在 0.2 到 0.5 之间比较稳,值越大对当前帧响应越快,但抗抖动能力弱;值越小输出越平滑,但会引入 2 到 3 帧的延迟。在采摘执行器动作速度为每秒 2 次的场景里,0.4 基本能兼顾响应和稳定。除了平滑,还可以加一个最小置信度门限:当平滑后的最高置信度仍低于 0.6 时,输出“待定”而不是硬选一个阶段。这个门限在过熟阶段尤其有用,模型犹豫时的默认动作应该是“不摘”,而不是误判成适采期造成损失。
最后的经验是,平滑器的输入务必用 softmax 或 sigmoid 归一化后的置信度,不要用原始的 logits。YOLO 输出头的数值分布在不同训练轮次差异较大,归一化后滑动平均才能对齐。
本文还有配套的精品资源,点击获取