ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

石榴成熟阶段检测数据集:5855张图+VOC/YOLO标注的实战指南

石榴成熟阶段检测数据集:5855张图+VOC/YOLO标注的实战指南

简介:面向石榴成熟阶段检测的目标检测数据集,覆盖花蕾、早果、花朵、中果、成熟5个生长阶段,标注框总数达11482个,适合农业视觉识别、智慧果园监测等场景下的模型训练与算法研究。压缩包整体约406.53MB,共2000个文件,以VOC格式的XML标注文件与YOLO格式的TXT标签说明为主,并配套对应JPG图片,数据清晰且未做增强,矩形框标注规范准确。压缩包内目录划分明确,JPEGImages、Annotations、labels分别存放图片、XML和TXT文件,可直接用于格式转换与模型输入。目前已有79人学习,且声明不保证训练精度,但数据集本身提供准确合理的标注,可帮助研究者快速完成数据准备、预处理和检测模型调优。

1. 石榴成熟阶段检测数据集5855张5个阶段:做农业目标检测的第一手标注资源

做果树生长监测的人最头疼的,不是模型选型,而是标注数据。石榴从开花到成熟,中间隔着bud、flower、early-fruit、mid-growth、ripe五个阶段,形态差异大,边界模糊,自己拍图加标注没两三周下不来。这份数据集把最耗时的那部分直接打包好了:5855张清晰图片,VOC和YOLO双格式标注,共11482个矩形框。下载解压后,JPEGImages、Annotations、labels三个文件夹齐整排列,能直接喂给YOLO系列训练。无论是做毕业设计、跑通目标检测流程,还是做石榴生长阶段识别的原型验证,它都是一份能省下大量重复劳动的起步资源。接下来从数据组织、格式转换到训练避坑,按实际使用顺序逐一拆开讲。

2. 拆开压缩包看数据结构:三个文件夹、5855张图与五类标签的对应关系

拿到压缩包后,先别急着跑训练,把文件夹结构和标签分布摸清楚,后面能少踩一半的坑。这套数据集的组织方式是目标检测任务中最常见也最稳妥的三目录结构,图片、VOC标注、YOLO标注各占一处,彼此通过文件名关联。

2.1 文件夹划分与双格式的工作方式

压缩包解压后,核心是三个平级文件夹。JPEGImages存放全部jpg图片,Annotations存放同名xml文件,labels存放同名txt文件。三者一一对应,比如xyxr_images_hehe4138.jpg对应xyxr_images_hehe4138.xml和xyxr_images_hehe4138.txt。

这种布局的直接好处是:用YOLO训练时,只需要指定images和labels两个路径,模型会自动按同名字找标注;用VOC格式做迁移学习或跑SSD、Faster R-CNN时,直接指向Annotations即可,不需要额外写转换脚本。我一般把整个解压后的目录结构保持原样,不重命名不挪动,避免路径字符串出幺蛾子。

三个文件夹统计如下:

文件夹文件格式文件数量用途
JPEGImages.jpg5855原始图像,分辨率清晰,未增强
Annotations.xml5855VOC格式标注,含对象类别与坐标
labels.txt5855YOLO格式标注,含归一化坐标

需要留意的是,labels文件夹内的txt是直接从VOC转换好的成品,但类别编号不一定和人类直觉顺序一致。数据集的说明文档里特别强调了一点:YOLO格式类别顺序以labels文件夹下的classes.txt为准,这个文件通常只有五行,分别对应五个阶段名。训练前务必打开看一眼,确认编号0到4分别映射到哪个阶段。

2.2 五类标签的框数分布说明什么

五个阶段标签分别是bud、early-fruit、flower、mid-growth、ripe,各标签的框数有一定差异:

标签名称阶段含义框数占比
bud花蕾期243321.2%
early-fruit幼果期130711.4%
flower开花期282724.6%
mid-growth中果期298626.0%
ripe成熟期192916.8%

总计11482个目标框,每张图平均约1.96个目标。从分布能看出两个实用信息:第一,early-fruit只有1307框,属于样本偏少的类别,训练时如果不做针对性处理,模型对它最容易漏检;第二,mid-growth和flower框数最多,模型对这两类的拟合会更充分。做类别平衡、数据增强或loss加权时,这个分布数据就是首要依据。

数据集图片分辨率整体清晰,未做增强处理。这意味着图像本身的亮度、翻转、噪声状态都是原始采集样貌。我建议训练时自己叠加mosaic、随机翻转和色彩抖动,不必担心数据被二次处理过。

3. 把VOC标注转成YOLO训练格式:解析XML并归一化坐标的完整脚本

虽然压缩包已经附带了YOLO格式的txt文件,但实际训练时仍然建议自己从VOC转换一遍。一个很直接的原因是:不同版本的YOLO工具对标注文件的解析细节不完全一致,与其被txt的格式差异卡住,不如把转换逻辑攥在自己手里,出了问题能立刻定位。

3.1 为什么最好自己转一次而不是依赖现成转换工具

网上能找到不少现成的VOC转YOLO脚本,但普遍有两个毛病。一是硬编码类别列表,把原本的猫狗类顺序写死,换到石榴五分类就得手工改数组;二是脚本默认输入图片路径和xml路径都在同一级目录,而这个数据集的Annotations里只有xml,没有图片。更隐蔽的问题是:某些转换工具在遇到坐标超出图像边界时会直接报错退出,导致转换中断,而手工脚本可以按需要做钳制或过滤,控制粒度更细。

亲手转换一次还有一个附带收获:你会天然知道每个txt的第一列编号到底对应哪个阶段。这对后续调试预测结果极其重要。

3.2 转换脚本:从XML读取边界框并生成labels文本

下面这个脚本以VOC xml为输入,逐文件读取对象类别和边界框,输出归一化坐标的YOLO txt文件。

import os import xml.etree.ElementTree as ET voc_dir = "Annotations" label_dir = "labels" class_names = ["bud", "early-fruit", "flower", "mid-growth", "ripe"] os.makedirs(label_dir, exist_ok=True) def convert_xml_to_yolo(xml_path, output_path): tree = ET.parse(xml_path) root = tree.getroot() img_width = int(root.find("size/width").text) img_height = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) bndbox = obj.find("bndbox") x_min = float(bndbox.find("xmin").text) y_min = float(bndbox.find("ymin").text) x_max = float(bndbox.find("xmax").text) y_max = float(bndbox.find("ymax").text) x_center = (x_min + x_max) / 2.0 / img_width y_center = (y_min + y_max) / 2.0 / img_height box_width = (x_max - x_min) / img_width box_height = (y_max - y_min) / img_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}") with open(output_path, "w") as f: f.write("\n".join(lines)) for xml_file in os.listdir(voc_dir): if not xml_file.endswith(".xml"): continue xml_path = os.path.join(voc_dir, xml_file) txt_name = xml_file.replace(".xml", ".txt") txt_path = os.path.join(label_dir, txt_name) convert_xml_to_yolo(xml_path, txt_path) print(f"converted: {xml_file} -> {txt_name}")

转换逻辑的核心是坐标归一化。VOC格式存的是像素绝对坐标,YOLO格式需要的是以图像宽高为分母的相对坐标。这里使用x_center、y_center、width、height四个字段表示边界框,全部控制在0到1区间内。类别编号直接按class_names的索引顺序生成,0对应bud,1对应early-fruit,依次类推。

代码中从xml中取值时,通过root.find("size/width")这类路径写法定位节点,比层层遍历更简洁。注意float()包裹了所有坐标值,避免遇到整数字符串时类型不匹配。生成结果没有包含置信度字段,纯检测任务不需要。

转换完成后,建议抽样打开几个txt文件检查,重点看坐标值是否都在0到1之间。如果出现大于1的情况,说明xml里的坐标超出了图像尺寸,需要追溯图片是否被裁切过。

4. 进入YOLO训练管线:数据集划分、配置文件与训练参数选择

标注格式就绪后,下一步是把数据喂给YOLO训练。这里涉及三个关键动作:划分训练集与验证集、编写数据集配置文件、启动训练并理解参数。

4.1 按“训练/验证”比例切分数据,而不是随机抽全部图片

训练集和验证集切分不能图省事直接random.shuffle全部文件。因为同一次拍摄中相邻图片往往高度相似,随机切分会导致验证集和训练集出现近亲样本,测试分数虚高。我一般按文件名排序后,先做分组,再按比例选取。

import os import random random.seed(42) image_dir = "JPEGImages" label_dir = "labels" train_ratio = 0.85 images = [f for f in os.listdir(image_dir) if f.endswith(".jpg")] images.sort() random.shuffle(images) split_idx = int(len(images) * train_ratio) train_images = images[:split_idx] val_images = images[split_idx:] with open("train.txt", "w") as f: for img in train_images: f.write(os.path.abspath(os.path.join(image_dir, img)) + "\n") with open("val.txt", "w") as f: for img in val_images: f.write(os.path.abspath(os.path.join(image_dir, img)) + "\n") print(f"train: {len(train_images)}, val: {len(val_images)}")

这段代码首先固定随机种子为42,确保每次运行划分结果一致。排序后shuffle,再按比例切分,可以避免连续文件名的图片全落在同一个集合里。写的train.txt和val.txt存的是图片的绝对路径,YOLO训练时会自动按路径前缀去匹配同名的txt标注文件。

seed参数可以按需要修改。有一点要注意:如果后续要复现实验,seed必须记录下来。如果换一台机器重跑,随机种子相同但Python版本不同,shuffle结果也可能变,这属于正常现象。

4.2 编写data.yaml并向模型指定路径

以YOLOv8为例,数据集配置文件是一个yaml文件,包含路径和类别信息。下面这份配置可以直接复制使用:

path: /absolute/path/to/dataset_parent train: train.txt val: val.txt names: 0: bud 1: early-fruit 2: flower 3: mid-growth 4: ripe

path字段填写数据集的上级目录绝对路径,train和val填写划分出来的txt文件名。names字段的编号顺序必须和转换脚本里的class_names一致,不能凭感觉排序。

两种常见错误:一是names顺序和labels里的txt第一列编号对不上,导致模型标签语义错乱;二是path字段写相对路径,换目录运行训练时直接报FileNotFoundError。我用这个配置时习惯把path写成绝对路径,一劳永逸。

4.3 训练启动命令与关键参数选择

训练命令本身不长,但参数影响显著。以下是一份在单卡GPU上可行的训练命令:

yolo detect train data=data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 patience=20

epochs建议在100到200之间选择。这个任务类别多但目标尺寸不小,150轮左右通常能收敛。imgsz选择一个适中值。如果显卡显存有限,降imgsz比降batch更划算,因为大幅降batch会影响BN层统计。patience设为20,表示验证集指标连续20轮不提升就早停,能节省时间。

对于这份数据集,还有一个参数值得调整:近邻匹配的IoU阈值。石榴果实的边界有时比较模糊,默认阈值可能漏匹配低重叠度的真值框。可以尝试在训练命令后追加close_mosaic=10,让最后10轮关闭mosaic增强,帮助模型稳定收敛。

5. 避坑阶段记录:标签顺序错位、坐标越界与小类样本漏检

训练接近落地时,问题才真正冒出来。我实际跑这套数据时,先后踩过几个坑,每一条都值得提前避开。

5.1 坑一:类别顺序与classes.txt不一致导致模型输出语义错乱

现象:模型训练完,推理结果里早该显示“ripe”的成熟石榴,输出标签变成了“bud”,但精度数值看着还算正常。

原因:训练时data.yaml的names顺序和labels文件夹下txt第一列的编号含义不一致。常见做法是直接把数据集作者给的VOC类名顺序抄进yaml,但YOLO格式的类别编号是转换脚本定的,两套顺序未必相同。

解决:以labels/classes.txt为准,先打开该文件查看每一行对应的类名,再据此编写data.yaml的names。我每次都会在训练前写一行校验代码,把每个txt的首列值范围打印出来,确保没有出现超出0到4的编号。

5.2 坑二:xml里的坐标超出图片边界导致YOLO报错或loss异常

现象:训练刚开始时,某些batch直接抛出坐标相关的断言错误,或者损失值出现NaN。

原因:xml文件中个别框的x_max大于图片宽度,或y_max大于图片高度。这类数据通常在标注时误框了边缘,原始VOC训练工具不敏感,但YOLO的训练管线对归一化结果有区间要求。

解决:转换脚本里加入钳制逻辑,把x_max钳到img_width,y_max钳到img_height。以下是修改片段:

x_max = min(float(bndbox.find("xmax").text), img_width) y_max = min(float(bndbox.find("ymax").text), img_height)

钳位不影响正常框,只修正边缘异常。执行转换后重新统计一遍坐标最大值,确保全部落在合法区间。

5.3 坑三:图片文件名带前缀导致划分后集合与标注文件不匹配

现象:训练时提示大量标注文件找不到,或标签文件对空。

原因:数据集的部分文件名以xyxr_images_hehe开头,但jpg、xml、txt三者的文件名主体一致。若在划分数据时用glob直接匹配*.jpg,再手动拼接txt路径,可能因为路径拼接方式不对导致找不到同名txt。

解决:划分脚本里统一使用os.path.splitext去掉扩展名,再分别追加.jpg和.txt后缀,保证三套文件始终基于同一stem。生成train.txt后,遍历每一行验证对应txt存在,缺一个就及时报警。

5.4 坑四:直接用全部数据做训练,验证集指标虚高假象

现象:训练完成后验证集mAP高达0.95,但换一批实拍石榴图预测,效果明显变差。

原因:未做数据划分,或划分时未打乱顺序。同类图片被聚在一起,验证时模型“记住”了邻近样本的特征,泛化能力被高估。

解决:严格按4.1的脚本先划分训练验证集,保证验证集样本时间跨度尽量分散。如果原始采集是连续帧,可以按数量间隔抽帧,而不是连续切分。

5.5 坑五:early-fruit样本偏少导致漏检率居高不下

现象:验证集里bud、flower的recall都不错,early-fruit的recall只有0.6左右,大量幼果被漏成背景或误判为bud。

原因:early-fruit框数只有1307个,占总量11.4%。模型整体学习到的该类别特征不足,且幼果与花蕾形态相近,特征混淆度高。

解决:在训练时对early-fruit类别调高loss权重,或在预处理阶段复制该类别样本生成额外的增强图。数据层面,可以将该类的mosaic比例调高,让每个batch中这类样本出现更多。训练完成后优先检查该类的recall指标,而不只看mAP均值。

6. 用小样本类别权重校准预测:recall分布检查与loss权重设置技巧

模型跑通后,真正考验效果的是细分类别能不能平衡。这里分享一个我常用的校准流程,特别适合处理early-fruit这类小样本标签。

第一步,训练结束后分别输出每个类别在验证集上的recall。YOLOv8默认训练结束会生成混淆矩阵和各类别指标曲线图,优先查看results.png里的recall-per-class柱状图。如果early-fruit明显低于其他类,就执行第二步。

第二步,在data.yaml同级目录下增加一个类别权重配置。YOLOv8的loss权重参数为cls,可以配合class_weights使用:

# 假设原始各框数为 bud:2433, early-fruit:1307, flower:2827, mid-growth:2986, ripe:1929 # 用小类权重 = 最大框数 / 当前类框数 weights = [ 2986 / 2433, # bud 2986 / 1307, # early-fruit 2986 / 2827, # flower 2986 / 2986, # mid-growth 2986 / 1929 # ripe ] print(weights) # 输出约 [1.23, 2.28, 1.06, 1.0, 1.55]

权重计算逻辑是让框数最少的类别获得最大惩罚系数。实际训练时,把最高权重限制在2.5以内,避免过拟合。

第三步,利用验证集混淆矩阵做阈值修正。YOLO推理时的conf阈值默认0.25,若early-fruit的漏检集中在低置信度预测,可以单独降低该类别阈值或增加一个后处理规则。以下是一个最小化验证脚本:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(split="val", conf=0.15, iou=0.5) print(results.box.mp, results.box.mr)

这里的conf从默认0.25降到0.15,观察整体recall变化。如果提升明显且precision没有崩,说明早期阈值过高,可以在推理阶段按此参数输出。

从那以后,我每次拿新数据集做检测,都会先统计每个类别的框数和计算类别权重,再进训练流程。这套顺序执行下来,漏检问题基本在训练阶段就能暴露,不用等部署上线才追悔。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进