目标检测实战:5855张石榴图像五阶段成熟度标注与YOLO训练

发布时间:2026/10/10 17:50:40
目标检测实战:5855张石榴图像五阶段成熟度标注与YOLO训练 简介面向目标检测学习与农业视觉应用的开发者压缩包内提供一套石榴成熟阶段检测数据集完整覆盖开花结果期五个关键节点bud花芽、early-fruit幼果、flower花朵、mid-growth中期生长、ripe成熟可满足幼果识别、花期统计、成熟度评估等农业AI项目的数据需求。包体约406.53MB文件清单共2000项以xml标注文件与txt说明文件为主解压后形成JPEGImages、Annotations、labels三个目录分别存放5855张清晰图像、5855份VOC格式xml标注和5855份YOLO格式txt标签矩形框总数达11482个分辨率清晰且未做增强便于后续扩展。该数据集不承诺模型精度但每个目标框均经准确且合理标注可直接用于YOLOv5/YOLOv8等主流检测器训练与验证省去格式转换与重新整理标注的环节。目前已有79人学习适合作为农业视觉课程设计、毕业设计或目标检测基线实验的支撑数据也可在此基础上进行数据增强、迁移学习与类别均衡优化。1. 石榴成熟阶段检测为什么是数据集的活先看清 5855 张和 5 个阶段意味着什么做农业目标检测落地的人几乎都会撞上同一个问题模型结构不是瓶颈数据才是。这个标题里的数据集5855 张石榴图像、5 个成熟阶段同时给出 YOLO 和 VOC 两种标注格式本质上是在告诉你——从采摘机器人识别“能不能摘”到分级流水线判断“什么成熟度”你缺的不是算法而是带明确阶段标签的样本。适合正在做果蔬检测、成熟度分级或者想用自己的数据复现一遍完整训练链路的人。我第一眼关注的是两个数字5855 张不算海量但足够检验一个模型从数据准备到部署的完整能力5 个阶段则意味着标注边界一定比二分类更暧昧。2. 解包与文件盘点看清 YOLO 格式和 VOC 格式各自的目录结构与标注文件拿到压缩包我习惯先解压到一个干净目录用tree -L 2 -d看目录树。如果是同时带 YOLO 和 VOC 的双格式数据集目录里多半会出现JPEGImages、AnnotationsVOC 系以及images、labelsYOLO 系这四种目录。很多初学者直接跑训练结果发现 Ultralytics 找不到标注问题就出在没看清目录结构上。这一章把两种格式的目录和标注文件结构拆开讲顺便给一个核对数量的脚本。2.1 先认目录JPEGImages 与 images 不是一个东西VOC 系的目录结构来自 Pascal VOC 标准图片统一放在JPEGImages标注放在AnnotationsImageSets/Main里是划分清单。YOLO 系特别是 Ultralytics 生态则默认图片在images/train、images/val标注在labels/train、labels/val。有一种常见误解是JPEGImages就是images其实它们可以被同一份数据复制成两份但组织规则完全不同。解压后先用tree看树$ unzip pomegranate.zip -d ./pomegranate $ tree -L 2 -d ./pomegranate逻辑说明unzip解压到项目目录tree -L 2 -d只看两层目录忽略文件方便一眼看出有没有ImageSets、images、labels这些关键目录。如果没有安装 tree可以用find . -maxdepth 2 -type d -print代替。如果看到JPEGImages和Annotations说明 VOC 格式是原始档同时存在images和labels说明 YOLO 格式已经由作者或工具生成好。挑战是这两份很可能不同步——比如labels是从Annotations转出来的但有个别 XML 没有对应图片而images里的图片又比JPEGImages多了几张。这就是为什么我会先做一次数量核对。2.2 读懂标注文件VOC 的 XML 是一种树YOLO 的 TXT 是一种归一化编码先看一段 VOC 的 XML片段前面也出现过但这里重点看它的结构annotation filenamepomegranate_001.jpg/filename size width1280/width height720/height /size object namesemi/name bndbox xmin420/xmin ymin180/ymin xmax620/xmax ymax360/ymax /bndbox /object object namemature/name bndbox xmin700/xmin ymin220/ymin xmax900/xmax ymax470/ymax /bndbox /object /annotation而对应的 YOLO 格式labels/pomegranate_001.txt1 0.40625 0.37500 0.15625 0.25000 3 0.62500 0.47917 0.15625 0.34722参数说明第二行开头的1表示semi第三行开头的3表示mature这个 ID 完全取决于class_to_id映射表。VOC 的name是字符串YOLO 的类别是整数这就是为什么需要一个固定的 names 顺序。两者可以互相转换但转换是单向不可逆的一旦把字符串映射成整数原字符串就丢了。下面是对照表维度VOC XMLYOLO TXT坐标形式左上角与右下角像素中心点与宽高的归一化值类别表示字符串整数 ID多框多个object多行图像尺寸写在size内部不写想反算必须读图文件后缀.xml.txt2.3 先做一次数量核对图片与标注必须 1:1 对应无论哪种格式我最先做的事是写一个数量核对脚本统计图片总数、标注总数、有图片没标注、有标注没图片的数量。这一步能拦住大量后面训练时莫名其妙的报错。import os from glob import glob img_ext (.jpg, .jpeg, .png) img_names set() for ext in img_ext: img_names.update(os.path.splitext(os.path.basename(p))[0] for p in glob(fJPEGImages/*{ext})) img_names.update(os.path.splitext(os.path.basename(p))[0] for p in glob(fimages/train/*{ext})) img_names.update(os.path.splitext(os.path.basename(p))[0] for p in glob(fimages/val/*{ext})) xml_names {os.path.splitext(os.path.basename(p))[0] for p in glob(Annotations/*.xml)} txt_names set() for sub in (train, val): txt_names.update(os.path.splitext(os.path.basename(p))[0] for p in glob(flabels/{sub}/*.txt)) print(fimages: {len(img_names)}, XML: {len(xml_names)}, TXT: {len(txt_names)}) print(f图片无XML: {img_names - xml_names}) print(fXML无图片: {xml_names - img_names}) print(f图片无TXT: {img_names - txt_names}) print(fTXT无图片: {txt_names - img_names})逻辑说明把三种来源的文件名前缀收集成集合集合减集直接得到差异。图片可能同时存在于JPEGImages和images所以用update多次累加。输出缺失列表后再决定补图片还是删标注。这套脚本不依赖任何第三方库在任何机器上都能跑。有差异的话优先处理“TXT无图片”因为 YOLO 训练时如果labels下某个 txt 没有对应图片会直接报错而“图片无TXT”只会造成漏训不会中断。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑虽然这个标题下的数据集可能已经自带 YOLO 格式但我不建议直接拿去训练。原因有两个一是现有 YOLO 标注很可能由别人的转换脚本生成不校验就等同于把别人的坑背在身上二是你自己总会往这个数据集里补充图片新增的 VOC 标注迟早要转。所以跑通一个转换脚本是第一步正常操作。3.1 一个能跑通的 VOC 转 YOLO 脚本我一般会写一个最简脚本只依赖标准库不引入额外依赖方便在任何机器上直接跑。下面这个脚本遍历Annotations目录里的所有 XML生成同名.txt到labels目录同时生成class_names.txt。import os import xml.etree.ElementTree as ET from glob import glob voc_root Annotations yolo_root labels os.makedirs(yolo_root, exist_okTrue) # class_to_id: 按成熟阶段顺序定义必须和训练时的 data.yaml 保持一致 class_to_id {unripe: 0, semi: 1, half: 2, mature: 3, overripe: 4} for xml_path in glob(os.path.join(voc_root, *.xml)): tree ET.parse(xml_path) size tree.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) name os.path.splitext(os.path.basename(xml_path))[0] lines [] for obj in tree.findall(object): class_name obj.find(name).text if class_name not in class_to_id: continue cls_id class_to_id[class_name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # YOLO 需要中心点坐标和宽高全部归一化 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(yolo_root, name .txt), w) as f: f.write(\n.join(lines))逻辑说明ET.parse解析 XML取size里的宽高作为归一化分母。遍历所有object取出类名和边界框换算成 YOLO 的格式。class_to_id需要你手动定义顺序就是训练时类别 ID 的顺序。注意x_center用的是(xminxmax)/2不是(xmax-xmin)/2写错的话所有框都会偏到左上角。输出文件每行 5 个值用空格分隔。生成后我还建议把class_names.txt也写出来后面核查 data.yaml 时直接 diff省得靠脑子记。3.2 边界坑一归一化坐标经常除错了分母现象是模型能训练但预测框明显偏大或偏小。常见原因有人用标注框的宽高做分母比如(x_center - xmin) / w_box也有人把img_w和img_h写反导致竖图变横图。解决每次转完随机挑一张图把生成的 YOLO 坐标反算回像素坐标画框对比一次。这个动作只花两分钟能挡住大部分低错。我习惯这样反算验证import cv2 line open(labels/pomegranate_001.txt).readline().split() cls_id, xc, yc, w, h int(line[0]), float(line[1]), float(line[2]), float(line[3]), float(line[4]) img cv2.imread(images/pomegranate_001.jpg) H, W img.shape[:2] x1 int((xc - w / 2) * W) y1 int((yc - h / 2) * H) x2 int((xc w / 2) * W) y2 int((yc h / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check.jpg, img)这段代码把归一化坐标乘回图像宽高还原成像素框。如果框没贴住石榴第一嫌疑就是分母错了第二嫌疑是find(size)拿到的尺寸不是这张图的实际尺寸。有些标注工具在 XML 里写的 size 和原图不一致遇到这种只能逐个读图取真实宽高。3.3 边界坑二类别 ID 和字符串对不上数据集里如果class_to_id与训练配置文件里的 names 顺序不一致模型会训练得很“正常”但推理输出对不上语义。比如我把mature放在unripe前面XML 里unripe就会被映射成 1而 data.yaml 里unripe是 0整个标签全错。解决把class_names.txt写到磁盘训练前再读一遍确认和 data.yaml 一致。这里有个小技巧打印每个类别的样本数看看类别分布是否合理。如果half只有几十张说明要么原始标注少要么class_to_id漏了这个类后者会让该类的所有框被continue静默跳过。3.4 边界坑三一个 XML 里有多个 object只取第一个很多简化脚本用find(object)而不是findall(object)结果一图多果只保留了第一个框。石榴图像经常两三个果实挤在一起漏框会直接导致召回率暴跌。解决用findall并且对每个object都做一次归一化。我还会在脚本里加一个计数器统计每个 XML 的平均 object 数量。如果平均不到 1.0说明有 XML 是空标注要警惕如果平均超过 3.0那这数据集密度不低训练时nms阈值要放宽一点否则密集果实会互相抑制。3.5 边界坑四只转标注不检查图片是否存在训练时报AssertionError: Label not found是因为 labels 目录里有 txt但 images 目录里找不到同名图片。常见原因是某些 XML 是从别的数据集拷贝来的图片没跟过来。解决转换脚本末尾加一个校验循环对每个生成的 txt检查对应的 jpg/png 是否存在缺失的打出来。顺手把没有 txt 的图片也列出来放到missing.txt。这样处理完再跑一遍第 2 章的数量核对脚本确认 1:1 了再进训练环节。4. 用 YOLOv8 训练石榴成熟度检测数据划分、配置与关键参数格式验证完毕进入真正的训练。这里以 YOLOv8 为例因为它是目前“yolov8训练自己的数据集”最主流的入口命令也足够简洁。但同样的数据划分和 data.yaml 配置思想适用于 YOLOv5、RT-DETR。4.1 十行脚本划分 train/val/test 并写入 data.yaml目标检测数据集需要一个data.yaml里面写明path、train、val、names。下面的脚本按 8:1:1 划出三个集合并把结果写进data.yaml。import os import random import yaml random.seed(42) def split_ttv(img_dir, out_dir): files [f[:-4] for f in os.listdir(img_dir) if f.endswith((.jpg, .jpeg, .png))] random.shuffle(files) n len(files) tr, va files[:int(n*0.8)], files[int(n*0.8):int(n*0.9)] te files[int(n*0.9):] for name, subset in [(train, tr), (val, va), (test, te)]: with open(f{out_dir}/{name}.txt, w) as f: for item in subset: f.write(f{item}\n) return tr, va, te tr, va, te split_ttv(images, ImageSets/Main) yaml_data { path: ./pomegranate, train: ImageSets/Main/train.txt, val: ImageSets/Main/val.txt, test: ImageSets/Main/test.txt, names: {0: unripe, 1: semi, 2: half, 3: mature, 4: overripe} } with open(data.yaml, w) as f: yaml.dump(yaml_data, f, allow_unicodeTrue)逻辑说明先对全部图片文件名不带扩展名做随机打乱按比例切分。names的 key 是整数顺序必须和class_to_id一致。path建议写绝对路径或相对路径train 和 val 指向的是 txt 文件YOLO 会自动按 txt 里的名字去找images/和labels/下的对应文件。这里有个极易踩的坑train.txt里写的是“不带扩展名的文件名”而不是完整路径Ultralytics 默认会在path下拼接images/train/去找图片所以图片目录名必须是images标注目录必须是labels。划分完成后检查一下混合矩阵式的对应关系用for line in open(ImageSets/Main/train.txt)逐个读确认labels/train/{line}.txt存在且非空。空 txt 会导致该图片被过滤但不报错这种静默问题最烦人因为 mAP 会在不知不觉中变虚高。4.2 训练命令与三个必调参数epochs、imgsz、batchyolo train modelyolov8n.pt data./data.yaml epochs80 imgsz640 batch16 device0参数说明yolov8n是 nano 版本5855 张图用 nano 或 small 足够大模型容易过拟合。epochs80是起步值数据集规模不大30 轮就能看趋势80 轮能收敛。imgsz640是默认分辨率如果石榴在画面里偏小建议提高到 960但显存占用会翻倍。batch16在 12GB 显存上没问题8GB 显存就降到 8。device0指定第一张 GPU仅 CPU 就写devicecpu但训练速度会慢很多。第一次训练建议先跑 10 轮确认 loss 曲线在下降再完整跑。上面是基础参数。我还习惯开三个额外增强参数针对果蔬目标检测很有效yolo train modelyolov8n.pt data./data.yaml epochs80 imgsz640 batch16 device0 \ hsv_h0.02 hsv_s0.6 hsv_v0.4 fliplr0.5 scale0.3hsv_*控制颜色扰动石榴从红到黄的阶段差异很大适度扰动可以提升对光照的鲁棒性scale0.3做随机缩放模拟采摘机器人摄像头前后移动。注意scale过大会引入大量无意义背景反而拉低精度。4.3 从 loss 到 mAP训练时我盯着哪些输出训练日志里有一堆指标新手容易只盯 box_loss。更值得关注的是mAP50、mAP50-95和precision/recall。mAP50 反映框定位是否合格mAP50-95 反映边界框精度对 IoU 的敏感度后者才是实际部署时更接近真实难度的指标。每轮结束会输出混淆矩阵和 PR 曲线存到runs/detect/train/我会在训练中段就打开混淆矩阵看“哪个成熟阶段之间在打架”而不是等训练结束。训练结束后我第一件事不是看 chart而是把best.pt拿出来跑一次验证集推理打印出每个类的 recall。有些类别 mAP 看着不错但 recall 不足 0.5集中在少数难分样本上。对石榴成熟度检测来说漏检一个成熟果比误检一个未熟果代价更高所以我会以“每类 recall 均不低于 0.7”作为可以进入部署验证的门槛。如果mature类的 recall 只有 0.55别急着调模型结构先回到数据里看那一类样本的标注质量八成是标注边界不一致。5. 从数据集到落地标注质量检查与三类常见问题排查训练完成后翻车才是常态。这一章按“现象 → 原因 → 解决”的排查思路写我在类似数据集上最常遇到的 5 个问题覆盖标注、坐标、显存三个维度。每一条都是黑匣子揭开的现场记录不是教科书上的理论推演。5.1 现象5 个成熟阶段互相混淆尤其是相邻阶段现象模型把半成熟semi识别成未成熟unripe把成熟mature识别成半成熟但两端unripe 和 overripe准确率还行。原因成熟阶段本身是连续变化的数据集的 5 阶段标注没有硬边界标注员可能把临界样本归到了不同类。解决把混淆矩阵打出来找到误判最多的两个相邻阶段用 subsample 方式查看它们的原图标注如果确实存在标注不一致就把这批样本单独拿出一份人工复核后重新跑一轮不改标注只调阈值是治标不治本。具体复检时我会定义一个“边界样本判定规则”果皮底色偏绿但可见局部红色 → semi超过 50% 面积转红但果形瘦 → half。规则写清楚复核才有依据不然第二次标注还会歪。5.2 现象Loss 正常但 mAP 拉胯验证集上漏检严重现象训练 loss 稳步下降但mAP50只有 0.3 左右验证集上很多石榴根本没被框出来。原因最可能是标注框太小或太小目标太多默认imgsz640时小目标特征不足其次是类别不平衡某些阶段样本少。解决先统计每类样本数和每张图目标数labels/train.cache也会提示图例数量。样本少的类可以复制增强copy-paste或者把imgsz提到 960。还有一个经验参数anchor的宽高比YOLOv8 会自动聚类但如果果实都是近圆形问题不大。真遇到小目标漏检我一般是切图把 1280×720 的原图切成两半训练推理时再拼回效果比单纯拉高分辨率更稳。5.3 现象图片与标注文件名对不上训练前检查脚本直接报错现象执行yolo train时报Label x.jpg not found或者Image x.jpg not found。原因某些 XML 的filename写的是绝对路径或带.jpeg后缀而实际文件是.jpg。数据集里的ImageSets/Main/train.txt和labels/train/前缀也可能因为转换脚本用了不同扩展名而错开。解决用一个小脚本把所有图片统一重命名成six_digit.jpg同时同步修改对应 txt/XML 里的文件名。注意YOLO 训练不读 XML只读 txt所以只需要保证images里的图片和labels里的 txt 前缀一致。重命名后重新跑第 2 章的核对脚本确认两个集合完全相等。5.4 现象YOLO 坐标越界训练损失爆到 nan现象训练到某个 epoch 后 loss 变成nan重启后依旧。原因标注 txt 里出现负数或大于 1 的坐标比如-0.1 0.5 0.2 0.3这通常来自手动标注工具的导出 bug。解决在数据预处理阶段加一个 clip 操作把每个坐标值限制在 0 到 1 之间并过滤掉宽度或高度小于 0.001 的框。这个操作应该在转换脚本里完成而不是训练时。x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) w max(0.001, min(1.0, w)) h max(0.001, min(1.0, h))逻辑说明clip 是为了防负坐标宽度下限是为了防退化框。如果你不想过滤至少也要在训练前把所有 txt 扫一遍把越界行打印出来人工看。越界通常集中在少数几张图改掉后 nan 问题就消失。如果加入 clip 后仍然 nan下一步检查学习率是否过高那是另一类问题和坐标无关。5.5 现象显存溢出训练中断现象12GB 显存跑imgsz640, batch16没问题但一旦imgsz960就 OOM。原因特征图大小随分辨率平方级增加。解决优先降 batchbatch4也能收敛再降 imgsz如果还想用大图可以在训练配置里启用ampTrue默认开启它能在不损失太多精度的情况下显著省显存。另一个偏方是开启梯度累积但 Ultralytics 没有直接参数需要自己写 loop新手不建议搞不如直接换小模型。还有个大坑batch参数如果写成-1Ultralytics 会自动检测并尝试填满显存看起来聪明实际可能瞬间 OOM建议不要用。6. 用 5855 张图验证训练效果一个可复现的验证流程与成熟度阈值技巧训练结束后不要直接看 mAP 完事我会做一次接近部署的验证。做法是固定随机种子对 test 集跑一次yolo predict保存 JSON然后写脚本计算每个成熟阶段在不同conf阈值下的 precision/recall。石榴成熟度检测的特点是相邻阶段误判对业务影响不同把未成熟摘了损失小把过熟放过去损失大。所以我会单独为 overripe 和 mature 调高置信度阈值为 unripe 调低阈值让最终判定偏向安全侧。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(sourceimages/test, save_jsonTrue, conf0.25, imgsz640) for r in results: for b in r.boxes: cls int(b.cls[0]) conf float(b.conf[0]) # 记录每个框的类别和置信度用于后续阈值分析逻辑说明这段输出的 JSON 会包含image_id、category_id、bbox、score。我拿到后会按类别画 PR 曲线找到每类的“安全阈值”。实操中我会调conf到 0.35 左右把半成熟以下的目标尽量滤掉再对mature和overripe单独设nms参数避免密集果实的重复框。最后打开每张预测图看一眼高置信度框是否落在果皮表面而不是叶子或树枝上。这一步能暴露很多 mAP 看不到的问题比如背景误报。我习惯把 5855 张里容易翻车的 200 张另存为hard_set每次调参后用这个集合回归。这个习惯帮我挡住过不少“调 mAP 很爽、上线就翻车”的改动。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询