☰
油茶果成熟检测数据集实战:VOC转YOLO训练全流程详解
2026/10/1 16:26:55 网站建设 项目流程

简介:面向油茶儿果实成熟度自动检测任务,这份数据集提供了可直接用于YOLO系列模型训练与验证的完整图像和标签样本。压缩包共220个文件,其中110张JPG原图对应110个XML标注文件,包体大小约276MB,便于一次性下载与部署;XML内以边界框形式标定果实位置,并记录青涩、半成熟、成熟等不同状态,图像采集覆盖不同季节与光照条件,能够提升模型在实际果园环境中的泛化能力。所有标注均经专业人员手工校对,位置与类别信息具有较高一致性和可靠性,用户可据此直接开展目标检测实验,省去数据收集、清洗与标注重建等耗时环节。该数据集还可支撑油茶产量评估、品质分级等农业自动化场景,适合计算机视觉学习者、农业科研人员以及相关算法工程师用来验证模型性能。当前页面已有85人学习/下载,适合用于课堂项目、毕业设计或产业落地前的小规模验证。

1. 油茶果成熟检测数据集:一份图片+xml能帮你省下多少弯腰

油茶果采摘前要判断成熟度,果园里一棵树几百个果子,青果红果混在一起,靠人工仰头一个个看,效率低还容易漏。这份“油茶儿成熟检测数据集”做的就是把现场拍的果实图片,用xml标注框把每一个油茶果框出来,再标上成熟状态,交给YOLO训练一个自动检测模型。图片+xml标签是典型的Pascal VOC组织方式,也是YOLO生态里最常用的输入格式,转换后能直接喂给YOLOv5、YOLOv8和YOLO11。适合正在做农业视觉落地、手上有自己的果园照片但不知道标注和训练怎么串起来的人。这篇文章按我实际做过的流程,从拆包核对、转格式、训练参数到踩坑,完整过一遍。

2. 先拆开.rar看家底:图片、XML标签和类别分布怎么核对

拿到数据集合第一步不是急着写训练脚本,而是先搞清楚这批数据到底标注了什么、能不能支撑YOLO训练。油茶果成熟检测和通用目标检测有一个本质区别:同类目标在不同成熟阶段外观差异明显,而不同成熟阶段的果实又可能同时出现在一张画面里,所以标注质量直接决定模型上限。

2.1 先确认标注粒度:框果实还是框成熟区域

油茶果成熟检测数据集里,常见标注方式是把每一个可见果实单独画一个矩形框,类别名区分成熟状态,比如immature和mature。少数数据集会把整棵树的成熟区域画成一个大框,这种只适合判断“这棵树熟没熟”,没法定位单个果实,做采摘、估产都不够用。

打开一张xml看一眼object的数量和bndbox大小就能判断粒度。单张图十个以上小框,基本就是实例级标注;一张图就一个大框包住树冠,那就是区域级标注。实例级数据才能直接用YOLO做果实定位,区域级数据更适合做分类辅助。我一般会先用可视化脚本把标注框画到原图上,随机抽30张检查一遍,比直接看xml数字直观得多。

2.2 解析XML标签:VOC格式里有哪些字段

VOC格式xml每条object记录一个目标,核心字段是name和bndbox。一个标准xml长这样:

<annotation> <folder>images</folder> <filename>IMG_20241010_093212.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>immature</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>200</xmax> <ymax>160</ymax> </bndbox> </object> <object> <name>mature</name> <bndbox> <xmin>300</xmin> <ymin>200</ymin> <xmax>390</xmax> <ymax>280</ymax> </bndbox> </object> </annotation>

这里的size.width和height是整个图像的尺寸,bndbox是矩形框左上角和右下角的绝对像素坐标。YOLO训练不直接读xml,需要转换,但转换前必须确认两个隐患:一是size字段和实际图片分辨率是否一致,二是name字段是否和后面yaml里class id一一对应。这两处错位,转换出来的标签全是废的。

如果你之后要补标注,建议直接用labelImg或X-AnyLabeling这类工具打开xml,它们原生支持VOC格式,改完保存还是xml,不用自己手写坐标。

2.3 类别分布和图像质量核对:动手前的三件套

数据拿到手先跑一段统计脚本,把类别数、框数量、图片尺寸分布、有没有异常框一次查清楚。我一般用这个脚本扫一圈:

import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter xml_dir = Path("Annotations") total_boxes = 0 cls_counter = Counter() size_set = set() for xml_file in xml_dir.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") size_set.add((size.find("width").text, size.find("height").text)) for obj in root.findall("object"): name = obj.find("name").text cls_counter[name] += 1 total_boxes += 1 print("类别分布:", dict(cls_counter)) print("总标注框数:", total_boxes) print("图片尺寸种类:", size_set)

类别分布能告诉你成熟和未成熟样本是不是严重失衡,总框数能估算单类样本量,尺寸种类则暴露图片是统一分辨率还是混着手机拍摄、无人机拍摄的多分辨率。如果尺寸种类超过三种,训练时imgsz参数就得格外小心,后面第4章会讲。

尺寸核对应以实际图片为准,不要完全信任xml里的size字段。我遇到过压缩包内图片被批量改小但xml没跟着改的情况,训练出来预测框全部偏移。最稳的做法是转换脚本里直接用cv2读取图片的宽高来归一化,这个坑第3章会展开。

3. 把XML转成YOLO能吃的txt:转换脚本和四个边界坑

YOLO要求的标签是每张图片一个txt文件,每行格式为“class x_center y_center width height”,四个坐标值全部归一化到0~1。从xml转txt逻辑不复杂,但细节决定能不能顺利train起来,这章节给一个可以直接用的转换脚本,再把最容易翻车的四个边界坑讲透。

3.1 一个干净的VOC转YOLO脚本

转换脚本最核心的点是:框坐标必须除以图片实际宽高,而不是除以xml里的size,同时要做边界裁剪和无效框过滤。下面是我常用的版本:

import xml.etree.ElementTree as ET from pathlib import Path import cv2 class_names = ["immature", "mature"] # 顺序就是YOLO类别id,别乱换 def convert_xml_to_yolo(xml_path, image_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find("filename").text img_path = Path(image_dir) / filename img = cv2.imread(str(img_path)) if img is None: print(f"警告:找不到图片 {img_path}") return h, w = img.shape[:2] lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: print(f"跳过未知类别 {name} 在 {filename}") continue cls_id = class_names.index(name) 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) # 夹紧到图像边界内 xmin = max(0, min(xmin, w - 1)) ymin = max(0, min(ymin, h - 1)) xmax = max(0, min(xmax, w - 1)) ymax = max(0, min(ymax, h - 1)) if xmax <= xmin or ymax <= ymin: print(f"跳过无效框 {filename} {name}") continue x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h width = (xmax - xmin) / w height = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") out_path = Path(out_dir) / (Path(filename).stem + ".txt") out_path.write_text("\n".join(lines))

转换的核心逻辑是先读图拿真实宽高,再做边界裁剪,最后归一化。为什么必须夹紧坐标?因为有些标注工具会把框拉到图片外边界附近,xmax可能略大于width,不夹紧的话归一化后会出现大于1的坐标,YOLO在loss里对越界坐标会非常敏感,训练时经常报一堆warning甚至NaN。

3.2 归一化坐标的四个边界坑

第一个坑是“用xml尺寸还是图片尺寸”。必须用cv2读取的实际尺寸。xml里的size经常被标注工具写错或图片被压缩过,以它为准,框全部错位。如果图片读不出来,宁可跳过也不要硬转。

第二个坑是“类别名顺序漂移”。class_names的顺序就是YOLO的类别id,任何改名的操作必须在转换前统一。曾经试过把“mature”写成“mature1”,训练时类别id少了一位,预测和标签对不上,排查了半天。

第三个坑是“零宽高框”。油茶果在照片上很小,标注时手抖容易把xmin和xmax点成同一个像素,转换出来的width为0,训练时直接崩。解决办法就是上面脚本里的过滤逻辑,xmax <= xmin就放弃这个框。

第四个坑是“多行标签拼接”。如果一张xml里没有object,代码会写出一个空txt,YOLO训练时会认为“negative image”。如果空txt占比较高,模型会偏向输出“无目标”,导致检测不到任何果实。所以统计阶段就要看empty_xml的数量,空图片要么补标,要么从数据集里剔除。

3.3 数据集划分:按场景而不是随机划分

转换完毕后就要划分训练集和验证集。这里最常见的选择是random split,但对农业数据这是个陷阱:同一棵油茶树的照片可能被同时分进训练集和验证集,验证结果虚高,模型真正遇到没见过的树时效果大跌。

最合理的做法是按采集场景划分。油茶果数据集一般来自不同株树或不同日期拍摄,文件名里通常带着拍摄时间。我一般会把文件名中的日期或地点编号作为分组key,整组切分:

from pathlib import Path import random, shutil random.seed(42) images = sorted(Path("images").glob("*.jpg")) groups = {} for img in images: key = img.stem.split("_")[1] # 例如 IMG_20241010_093212 -> 20241010 groups.setdefault(key, []).append(img) train_imgs, val_imgs = [], [] for key, imgs in groups.items(): random.shuffle(imgs) cut = max(1, int(len(imgs) * 0.8)) train_imgs.extend(imgs[:cut]) val_imgs.extend(imgs[cut:]) print(f"train {len(train_imgs)} val {len(val_imgs)}")

如果文件名没有场景字段,可以按图片的拍摄时间元信息或者按目录分组。目标是保证同一株树、同一天拍的照片不跨集合。这样验证的mAP可信度才高,后面落地时不容易翻车。

4. 训练油茶果成熟检测模型:预训练选择、损失权重和必调参数

数据准备好了,进入训练环节。油茶果成熟检测比通用目标检测更需要精心设参数,因为果实小、密集、互相遮挡,成熟和未成熟外观又很像。盲用默认参数训练,mAP也许能看,但去果园实拍就会露馅。

4.1 预训练模型怎么选:三个档位

YOLO训练几乎不会从随机权重开始,大家基本都是用COCO预训练权重微调。目前常用的是YOLOv5、YOLOv8和YOLO11,训练接口基本一致,按硬件条件分三档选:

表格式文字描述:

  • 轻量档:YOLOv5s或YOLO11n。显存占用小,推理快,适合边缘设备,但小目标漏检明显。
  • 均衡档:YOLOv8s或YOLO11s。精度和速度兼顾,是农业检测的首选起点。
  • 精度档:YOLOv8m/l或YOLO11m/l。果实密集、遮挡严重时精度上限更高,但需要更大显存,训练时间翻倍。

第一次跑这份油茶果数据,我建议直接用YOLOv8s或YOLO11s,不要一上来就折腾大模型。数据量不超过几千张时,大模型容易过拟合,泛化反而不如中等模型。预训练权重会自动下载,如果网络不稳定,可以单独下载权重文件放在项目目录,训练命令里改成本地路径即可。

4.2 data.yaml怎么写:类别和路径决定成败

YOLO训练需要一份数据集描述文件,里面指定训练集、验证集路径和类别表。我一般把oiltea.yaml放在datasets目录下:

path: ./datasets/oiltea train: images/train val: images/val nc: 2 names: 0: immature 1: mature

这里的names顺序必须和转换脚本里的class_names顺序完全一致。很多人在这一步踩坑:转换脚本里名字是“mature/immature”,但yaml里写成了“成熟/未成熟”,序号对不上,训练不报错但精度完全错乱。

path字段建议写成相对路径,并且oiltea.yaml放在datasets/oiltea父目录下时,train用images/train即可。如果你把yaml放在别的目录,train和val就要改成绝对路径,或者以项目根目录为基准写。

4.3 训练命令与关键参数:imgsz、batch、epochs、patience

数据量和显存确定后,训练命令如下:

yolo detect train \ model=yolo11s.pt \ data=oiltea.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ project=runs/oiltea \ name=exp1

几个必须调的关键参数:

imgsz控制输入图像尺寸。油茶果在图片里可能只有几十个像素,imgsz=640是下限。如果显存允许,768或1024对密集小目标有明显提升,但训练时间也会变长。不要盲目上1280,对小数据集容易把模型拖进过拟合。

batch受显存制约。batch=16是8GB显存的常见选择,显存不够时优先减batch,不要减imgsz,因为小目标对图像分辨率更敏感。如果只有4GB显存,batch=8搭配imgsz=640勉强能跑。

epochs和patience配合。我一般设epochs=200,patience=30,意思是验证集指标连续30轮不涨就提前停。如果你的数据集只有几百张图,epochs可以加到300,但一定要开patience,不然模型会过拟合到训练集上。

4.4 类别不均衡与损失函数:成熟样本少怎么办

油茶果成熟度数据集常见的问题是不成熟样本远多于成熟样本,比如青果占八成、红果占两成。模型会偏向学“多数类”,把所有框都判成未成熟,mAP里的成熟类召回率低得可怜。

解决办法有三个方向,按效果排序:

第一种是数据层面过采样。把成熟样本的图片重复复制到训练集,让成熟框的数量和未成熟接近1:1。复制不是简单粘贴,最好对成熟样本做水平翻转、亮度变化,相当于扩充了一轮增强。

第二种是损失层面调权重。Ultralytics训练命令里有cls参数,它对应分类损失的系数。把cls从默认的0.5调到0.8或1.0,会让模型更重视分类正确性,对改善成熟类误判有一定帮助。但注意只调cls不够,数据过采样必须做。

第三种是调整置信度阈值。训练时不要动conf,验证时可以改成conf=0.1看模型真实的检出能力。很多时候不是模型没学到成熟特征,而是预测分数偏低被默认阈值卡掉了。这个在第6章验收部分会细讲。

5. 油茶果数据集训练避坑:五个常见问题的现象、原因与解法

训练油茶果数据这件事,算法本身不难,难的是数据里的坑一个个踩完才能跑出像样的结果。这一章把我遇到过的五类问题按“现象、原因、解法”拆开,你照着自查能省很多调试时间。

5.1 训练时报“No labels found”

现象:yolo命令启动几秒就报错,提示在labels/train目录找不到标签文件。 原因:转换脚本输出的txt没有和图片放在匹配的目录结构里。比如图片在images/train,标签在根目录labels/train但txt文件名与图片名不一致,或者转换后txt后缀写错成txt.txt。 解决:先检查单个对应关系是否成立。用下面命令扫一遍缺标签文件:

for f in datasets/oiltea/images/train/*.jpg; do base=${f%.jpg} tag="datasets/oiltea/labels/train/${base##*/}.txt" if [ ! -f "$tag" ]; then echo "缺标签: $f" fi done

如果少数图片没有标注,要么剔除,要么给它们单独建一个目录存起来,不要让空标签混进训练集。

5.2 转出的txt全是0或越界

现象:训练能跑,但日志里频繁出现“box coordinates越界”或loss不下降,验证集Precision和Recall都是0。 原因:标注框坐标超出图片实际宽高,或xml里的width/height与真实图片不同,归一化后中心点或宽高大于1。 解决:回到第3章的转换脚本,确认用了cv2.imread读取真实尺寸,并且对坐标做了clip。转换后可以做一个越界检查脚本,统计txt里任何一列小于0或大于1的行,必须零越界再进入训练。

5.3 模型把所有果实都判成未成熟

现象:验证集mAP整体看着不低,但单独看成熟类的混淆矩阵,成熟果实大量被预测成未成熟。 原因:类别严重不均衡,成熟样本太少,模型学到的“类先验”就是未成熟,输出置信度天然偏向多数类。另一种可能是标注本身漏标了成熟果,比如图片里远处模糊的红果没被标出来,模型反而学到了“成熟果不重要”。 解决:先统计成熟框占比,低于20%就做第4章说的过采样。再抽检标注,看成熟果漏标率是否超过10%。数据修完再调cls损失系数,顺序不能反。

5.4 开启增强后模型越训越差

现象:同一份数据,不开增强训练能到80% mAP,开了mosaic和翻转后反而掉到70%。 原因:油茶果图片里很多果实小到只有几十像素,mosaic增强会把果实裁成几块,导致小目标标签被切碎。另一个原因是增强尺度变化大,模型要同时适应多个分辨率,小数据量下学不稳。 解决:Ultralytics可以通过augment参数控制强增强策略,我一般把mosaic开到0.5而不是默认1.0,让一半batch保留完整图片。同时关掉hsv_h和hsv_s的剧烈色彩变化,因为油茶果成熟度判断依赖颜色特征,颜色增强过头会把青果和红果的特征搞混:

yolo detect train \ model=yolo11s.pt \ data=oiltea.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ mosaic=0.5 \ hsv_h=0.01 \ hsv_s=0.5

增强的目标是让模型对光照变化鲁棒,而不是改变果实颜色本身。成熟度本质是颜色特征,这个尺度必须控制住。

5.5 验证集mAP虚高,现场实拍却漏检

现象:测试集上mAP80%几,拿无人机或手机去果园拍一段视频,单帧漏检严重,成熟果经常没框。 原因:验证集和训练集来源高度一致,比如都来自同一批照片的随机划分;但现场实拍的光照、角度、背景完全不同。这是数据集划分没按场景切分导致的典型问题。 解决:严格按第3章的场景分组划分。另外落地时单独留一小组“现场实拍图”做最后验收,这一组不参与训练和验证,专门用来模拟真实使用条件。如果测试集mAP高但现场组漏检,优先扩充现场条件的图片,而不是调训练参数。

6. 模型到底能不能用:用线下测试和现场帧卡验收

模型训练完,最好的程度不是看train结束时的mAP,而是看它在一段现场视频里的表现。我习惯的做法是先把最优权重导出来,对一段没参与训练的视频做逐帧预测,再看两类漏检的分布:

yolo detect predict \ model=runs/oiltea/exp1/weights/best.pt \ source=field_video.mp4 \ conf=0.1 \ save_txt=True \ save=True

conf=0.1是关键,平时默认0.25会漏掉很多低置信度的成熟果实。线上使用可以回到0.25,但验收一定要用0.1看模型的真实上限。预测完成后,进入保存的labels,统计每个类别出现的次数:

from collections import Counter counter = Counter() with open("runs/detect/predict/labels/field_video.txt") as f: for line in f: cls = line.split()[0] counter[cls] += 1 print(counter)

如果成熟类计数明显低于人工估算,说明成熟果还是被漏了。我自己的验收标准是:青果类漏检率小于5%,成熟果漏检率小于10%,才算这个数据集和模型组合能去现场用。如果成熟果漏检超过两成,不要急着调训练参数,先回头抽图看是不是标注漏标、类别不均衡或者数据集场景不够。顺序搞反只会浪费时间。

最后留个习惯:把训练集、验证集、现场验收组三个目录固定下来,每一次重新训练都用同一套分组。这样不同版本之间的mAP才可比,模型迭代才有意义。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询