尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

石榴成熟度检测实战:YOLO与VOC数据集训练与评估指南

发布时间:2026/9/11 22:45:27

资讯中心
01
ARTICLE

石榴成熟度检测实战:YOLO与VOC数据集训练与评估指南

石榴成熟度检测实战:YOLO与VOC数据集训练与评估指南
简介目标检测任务中石榴成熟阶段识别是农业智能化与果园管理的重要环节。这份数据集面向计算机视觉初学者及农业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.xmlYOLO.txt坐标基准像素绝对坐标整数相对图像宽高的浮点数0~1框表达方式xmin, ymin, xmax, ymaxx_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, typePath, requiredTrue, helpYOLO标签目录) parser.add_argument(--num_classes, typeint, default5, 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_widthy 和 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, typePath, requiredTrue) parser.add_argument(--image_dir, typePath, requiredTrue) parser.add_argument(--out_dir, typePath, requiredTrue) args parser.parse_args() args.out_dir.mkdir(parentsTrue, exist_okTrue) 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 \ modelyolov8s.pt \ datadata.yaml \ imgsz640 \ batch16 \ epochs120 \ optimizerAdamW \ lr00.001 \ device0modelyolov8s.pt是在 COCO 预训练权重上继续微调迁移学习对农业场景通常有帮助。imgsz640是常规选择但如果石榴在图像里占比大可以降到 512 提升推理速度如果一棵树上果实小且密集可以升到 768。batch的值以显存不爆为下限16 到 32 是常选区段。epochs设在 100 到 150 之间成熟阶段类别边界模糊收敛比普通检测慢一些只跑 50 轮往往欠拟合。4.2 五个关键超参数与不同场景的推荐值参数推荐区间说明imgsz512 / 640 / 768果实占比大用小值小目标多用大值batch16 / 32受显存限制小 batch 配小数据增强epochs100 / 120 / 150看验证损失是否还有下降趋势optimizerAdamW / SGD数据量小用 AdamW数据量大用 SGD 更稳lr00.001AdamW/ 0.01SGD预训练权重迁移时不宜过大显存不足时优先降 batch 而不是降 imgsz因为降 imgsz 会直接损失小目标的框精度。训练过程中要盯着 val 损失如果训练损失持续下降但验证损失在第 40 轮后不再变化甚至回升就是过拟合信号可以提前停止或把数据增强里的翻转概率调高。4.3 YOLOv8损失函数里和阶段边界相关的两个细节YOLOv8 的损失由三部分构成边界框损失box_loss、分类损失cls_loss、分布式焦点损失dfl_loss。默认权重是 box7.5、cls0.5、dfl1.5但成熟阶段检测里最需要关注的不是默认权重而是相邻类别的误判模式。阶段二和阶段三、阶段四和阶段五之间的视觉差异小模型输出概率会在这两对类别上出现明显的高分纠缠这时如果少数类样本不足分类损失会主导训练方向导致模型把边界样本一路推向多数类。常见做法是在训练时把类别权重传入损失函数让少数类的分类错误产生更大的梯度。Ultralytics 提供class_weights参数也可以自己在损失函数外部做一个小技巧对每个类别的损失乘一个权重系数权重取总样本数除以各类别样本数的平方根再归一化。类别差别不超过 3 倍时这个操作带来的收益有限一旦超过 5 倍几乎必须做。另一个细节是dfl_loss它负责让框的分布更精准。如果标注框本身边界画得松比如有的标注员把石榴外圈果皮算进去有的只包住果肉区域dfl 会把框学得不够收敛。遇到这种情况先锁模型权重在 0.6 以下用一个辅助脚本统计同一张图不同标注员的框交并比框质量太差不做清洗就训练后期无论怎么调参都要从标注重来。5. 评估不只看mAP用混淆矩阵定位成熟阶段误判方向5.1 从验证命令到mAP指标的读取方式训练完成后跑验证的命令很简单yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml \ conf0.001 \ iou0.7 \ device0conf0.001是把置信度阈值压到最低保证所有检测框都参与评估这样算出的 mAP 更客观iou0.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, dtypefloat) 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 输出头的数值分布在不同训练轮次差异较大归一化后滑动平均才能对齐。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。