简介面向油茶儿成熟度检测的YOLO数据集专为农业视觉与实时目标检测场景设计适合需要训练成熟度识别模型的算法工程师、研究人员及农业智能化开发者可有效解决人工检测效率低、高质量标注数据稀缺等问题。资源共220个文件包含110张不同季节、光照条件下的油茶儿果实图像与110份配套XML标注文件整体大小约276.03MB。XML标注由专业人员手工完成以边界框标定果实位置并记录青涩、转色、成熟等阶段的状态标签格式规范、与YOLO系列算法兼容性好可直接用于模型训练、验证与推理测试。已有85人浏览学习图像覆盖多阶段外观特征和多种拍摄环境数据划分灵活可帮助使用者省去大量采集与标注成本快速搭建油茶儿成熟度检测基线同时为后续优化模型结构、调整超参数或迁移至其他农作物成熟度识别提供了可靠的数据基础。1. 油茶儿成熟检测数据集这份资源到底解决什么问题油茶儿成熟检测数据集名字听起来只是个农业课题的附属品但它在 YOLO 目标检测这条线路上踩的坑一点不比通用物体检测少。这份资源给的是图片加 xml 标签的 VOC 格式标注也就是说你拿到手的不是一句建议你用 YOLO 检测成熟度的废话而是一批已经框好边界的真实样本。想自己从头标一版得爬树、换光照、分成熟度分别框一周起步。这份数据解决的是果实成熟度检测里有数据可训这个前置问题适合正在做智慧农业、油茶种植监测或者需要一个密集小目标检测练手集的人。接下来我按自己复现的流程把从解压、转格式到训练、避坑的过程完整拆开讲照着走能少走很多弯路。2. 先把 VOC 的 xml 看清目录结构、标签字段与一致性体检很多人在第一步就翻车压缩包解压出来看一眼有图片有 xml直接拿第三方脚本转 YOLO 格式结果训练出来的模型框偏移、loss 不收敛回头排查才发现 xml 里的 size 和实际图片尺寸对不上或者类别名里混进了前缀空格。所以拿到这份数据集先用半小时做一次数据体检比急着训练重要得多。2.1 解压后先不要急着训练目录结构长什么样解压之后第一件事是用命令把结构打出来而不是双击打开随便翻。常见做法是先用unrar或者你本地的图形化解压工具解压然后通过命令行统计数量和目录层级。# 解压具体文件名以你下载到的为准 unrar x YOLO目标检测-油茶儿成熟检测数据集图片xml标签.rar # 看目录层级 find . -maxdepth 2 -type d | sort # 统计图片和xml数量判断是否一一对应 echo jpg数量: $(find . -name *.jpg | wc -l) echo xml数量: $(find . -name *.xml | wc -l) # 找出有xml没图片、有图片没xml的文件对防止数据不齐 for xml in $(find . -name *.xml); do img${xml%.xml}.jpg [ -f $img ] || echo 缺少图片: $img donefind和wc -l的组合是快速摸清数据集体量的基础操作建议把统计结果记下来后续划分 train/val 时能估算出大致比例。.jpg是这份资源里最可能的图片格式但如果你解压后发现是.png或者.jpeg把代码里的后缀统一改掉再跑。2.2 xml 里的每个标签都在说什么object、bndbox、difficult这份数据用的是 VOC 风格标注也就是 LabelImg 默认输出的格式。一个 xml 文件对应一张图片里面最关键的信息都集中在下表这几个字段里。字段含义对 YOLO 训练的影响filename图片文件名与其他文件关联的主键改文件名必须同步改这里size/width、height、depth图片原始宽高和通道数转换标签时要拿它做分母数值错误会导致框偏移object/name目标类别名类别映射表靠它生成名字里多个空格都算不同类object/bndboxxmin、ymin、xmax、ymax像素整数坐标转 YOLO 格式的原始输入必须保证在图片范围内object/difficult0 或 1是否难以辨认YOLO 训练会把它当普通目标处理容易引入噪声object/truncated目标是否被截断截断样本对遮挡检测有帮助一般保留difficult这个字段在 VOC 时代的语义是这框太模糊我不打算考它但 YOLO 系的训练脚本默认不去读它。也就是说如果这份数据里存在difficult1的框转换之后它照样参与训练而这些框往往是角度刁钻或遮挡严重的样本反而成了模型学习的干扰项。我的建议是转换前先统计一下这类框的数量占比超过 5% 就考虑要不要过滤。2.3 用脚本快速体检这组 xml 的完整性和一致性接下来写一个小脚本遍历所有 xml检查坐标是否越界、size 是否异常。这一步能提前暴露标注质量问题省得训练到一半才发现。import xml.etree.ElementTree as ET from pathlib import Path for xml_path in Path(annotations).glob(*.xml): root ET.parse(xml_path).getroot() w int(root.findtext(size/width)) h int(root.findtext(size/height)) if w 0 or h 0: print(f[异常] {xml_path.name}: size 无效 {w}x{h}) for obj in root.iter(object): box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) if xmin 0 or ymin 0 or xmax w or ymax h: print(f[越界] {xml_path.name}: {xmin},{ymin},{xmax},{ymax} 图片 {w}x{h}) if xmax xmin or ymax ymin: print(f[无效] {xml_path.name}: 框面积为0或反向坐标)这段代码的逻辑很直白先把 size 读出来再去校验每个 bndbox 是否落在图片范围内。findtext如果找不到节点会返回None所以当 xml 结构被标注工具改过时这里会抛异常你反而能第一时间发现是哪几个文件出了问题。越界框常见原因是标注过程中图片被 resize 过而 xml 未同步更新这类文件要在转换阶段单独处理。2.4 统计类别分布与目标密度决定训练策略的依据在动手转换之前还有一件事值得做统计类别名和目标密度。这决定了后面的训练是单类还是多类也决定了数据增强里要不要开 copy-paste。from collections import Counter from pathlib import Path import xml.etree.ElementTree as ET cnt_name Counter() cnt_per_image [] for xml_path in Path(annotations).glob(*.xml): root ET.parse(xml_path).getroot() boxes [obj for obj in root.iter(object)] cnt_per_image.append(len(boxes)) for obj in boxes: name obj.findtext(name) cnt_name[name.strip()] 1 print(类别分布:, dict(cnt_name)) print(单张图目标数均值:, round(sum(cnt_per_image) / len(cnt_per_image), 2)) print(单张图目标数最大值:, max(cnt_per_image))name.strip()这一步千万别省我遇到过类别名写成mature末尾带空格的情况肉眼完全看不出来但转出来的标签类别 ID 全乱了。目标密度这个数字直接关系到训练配置如果均值超过 30说明是密集小目标场景输入尺寸尽量不低于 640如果均值只有三五个说明目标相对稀疏可以优先考虑 416 或者更轻量模型。3. 把 xml 转成 YOLO 格式归一化公式、转换脚本与三个边界坑VOC 格式的 xml 没法直接喂给 YOLO 的 dataloaderYOLO 需要的是与图片同名的 txt 文件每行一行规范化的目标信息类别 ID、中心点坐标和宽高。这一段是整个流程里最容易出错的地方公式本身简单但三个边界坑几乎每个人都踩过。3.1 为什么转YOLO 不认 xml 的像素坐标框VOC 用绝对像素坐标描述框的位置xmin/ymin/xmax/ymax 的单位是像素YOLO 则要求四个值全部归一化到 0 到 1 之间且这组值代表的是目标中心坐标和框尺寸不是左上和右下角。归一化的好处是图片被缩放到任意分辨率标签都不用重新算。转换公式是center_x (xmin xmax) / 2 / image_width center_y (ymin ymax) / 2 / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_height注意所有除法都用同一组 image_width 和 image_height也就是 xml 里 size 字段的值或者实际图片的宽高。哪一步用了不同的尺寸基准框就会往某个方向偏移。3.2 转换脚本从 xml 目录到 labels 目录一次搞定下面这个是可用版本直接保存成voc2yolo.py就能跑注意按实际目录调整路径。import xml.etree.ElementTree as ET from pathlib import Path IMG_DIR Path(images) # 图片目录 XML_DIR Path(annotations) # xml目录 OUT_DIR Path(labels) # 输出yolo标签目录 OUT_DIR.mkdir(exist_okTrue) # 类别顺序直接决定标签第一位的数字必须与训练时的data.yaml保持一致 CLASSES [mature] # 按xml里实际出现的name填写顺序一旦确定不要改 def convert_one(xml_path: Path): tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) lines [] for obj in root.iter(object): name obj.findtext(name) if name not in CLASSES: print(f[跳过] {xml_path.name}: 未知类别 {name}) continue class_id CLASSES.index(name) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 归一化保留6位小数足够 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines for xml_path in sorted(XML_DIR.glob(*.xml)): lines convert_one(xml_path) if lines: out_path OUT_DIR / (xml_path.stem .txt) out_path.write_text(\n.join(lines) \n) print(f[OK] {xml_path.name} - {out_path.name})代码逻辑分三层最外层遍历所有 xml中间层读取图片尺寸并遍历 object里层做坐标归一化和文本格式化。六个小数位在处理 6000 像素级别的大图时误差小于一个像素够用不要用round(..., 2)那会把小目标的框精度毁掉。CLASSES 列表的写法有个细节值得单独说它既负责过滤未知类别又决定了类别 ID。如果你这份数据里有成熟和未成熟两类就按[mature, immature]这样的顺序排列训练时的 data.yaml 必须保持完全一致否则模型训练和验证时标签对不上。3.3 转换后的强制校验把标签画回图上转完格式不算完还要验证转换过程没把框弄丢。最直观的做法是把 txt 里的归一化坐标还原成像素框画到原始图片上肉眼抽查一遍。import cv2 from pathlib import Path for txt_path in sorted(Path(labels).glob(*.txt))[:10]: # 先抽查前10个 img_path Path(images) / (txt_path.stem .jpg) img cv2.imread(str(img_path)) if img is None: print(f[缺图] {txt_path.stem}) continue h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[格式错] {txt_path.name}: {line}) continue _, cx, cy, bw, bh map(float, parts) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(fcheck/{txt_path.stem}_check.jpg, img)这里的换算正好是归一化公式的逆运算中心坐标减半宽得到左上角加半宽得到右下角。同一个 txt 画出的框如果和原 xml 里的像素坐标对不上说明转换脚本或数据集有问题而不是标注错了。3.4 三个边界坑踩过的人才懂为什么单独列第一个坑是取整时机。有人图省事先把 xmin、xmax 取整再相减结果小目标的宽直接变成 0转出来的标签是废的。正确做法是全程用浮点数最后格式化输出时才限制小数位数。第二个坑是 size 字段和实际图片不一致。标注工具记录的是标注当时的尺寸如果后来有人批量压缩过图片xml 里的宽高就成了过期数据。我一般会在转换脚本里用 OpenCV 读一次真实图片宽高以此为准做分母而不是迷信 xml 里的 size。第三个坑是类别 ID 的稳定性。转换脚本多跑一次不会改变 ID但如果你中途改了 CLASSES 顺序或者把过滤未知类别的逻辑写晚一步整个 labels 目录的语义就变了。建议转换时把 CLASSES 写成常量并加注释训练前再跑一次类别统计确认标签里的第一个数字没有异常值。4. 训练参数怎么设输入尺寸、anchors 与数据增强的取舍数据格式修好了接下来面对的是训练配置。油茶儿检测的本质是密集小目标问题果实挂在树上叶片遮挡、光照变化、成熟度过渡这些因素直接决定了你不能照搬 COCO 的默认配置。这一章的参数我按实践中的优先级排开先说影响最大的输入尺寸。4.1 输入尺寸选 640 还是 416从目标占比倒推很多人纠结输入尺寸是玄学其实有判断依据。用第 2 章统计出的框宽高中位数除以图片尺寸得到目标占比。如果中位数占比在 3% 以下属于典型小目标用 640 保证缩放后果实还能保留足够像素如果占比超过 8%说明果实本身就大416 可以换来更快的训练速度。油茶儿这种场景我默认建议 640 起步。理由很直接果实密集且互相遮挡缩到 416 后相邻果实的边界在特征图上几乎融为一体anchor 匹配会变得不稳定。显存不够时与其降到 416不如保持 640 但把 batch 减到 8前者牺牲的是检测能力后者只是牺牲一点训练速度。4.2 anchors 先重算autoanchor 不是玄学是省时间的捷径YOLOv5 和 YOLOv8 训练时会默认执行 autoanchor也就是根据你这份数据集的标签自动聚类出合适的 anchor 尺寸。这个机制平时不用管但它有两个前提标签格式必须完全正确且数据集中在同一类目标上。油茶儿果实的尺寸分布相对集中autoanchor 能算出一组贴近真实框的初始值收敛更快。需要手动介入的情况是物体尺寸极度不均匀比如既有小果实又有被无人机拍到的大簇果实聚类出的 anchors 可能会偏向某一端。我一般会跑一次训练前检查看日志里 autoanchor 输出的 k-means 结果如果最大 anchor 和最小 anchor 差距超过 10 倍再考虑分两阶段训练。其他情况不用手动调这个环节真的不是玄学。4.3 我这套跑下来比较顺的超参组合以下参数是针对油茶儿类似场景的一套起始配置环境是单卡 8G 显存YOLOv8s 权重。参数值不是绝对的但是按重要性排序的改的时候优先动后面的。参数取值说明imgsz640输入尺寸密集小目标场景不建议低于 640batch168G 显存跑 YOLOv8s 的稳妥值显存大可以到 32epochs150单类检测任务 100 个 epoch 够想看稳定收敛就 150lr00.01从预训练权重续训建议降到 0.001防止破坏已有特征lrf0.01余弦退火末段学习率不需要动optimizerSGDYOLOv8 默认 AdamW小数据集上 SGD 泛化更稳seed0固定随机种子方便复现对比实验close_mosaic10最后 10 个 epoch 关闭 mosaic避免增强分布干扰收敛训练命令用 YOLOv8 的 CLI 启动最省事yolo detect train \ dataoiltea.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs150 \ lr00.01 \ optimizerSGD \ close_mosaic10 \ seed0dataoiltea.yaml指向你的数据集配置文件里面至少包含train、val路径、类别数量和类别名称。close_mosaic10这个参数在 YOLOv8 里是官方推荐的它在最后 10 轮把 mosaic 增强关掉让模型在接近真实分布的图片上收尾能在最后几轮把 mAP 稳住甚至再提一点。modelyolov8s.pt加载的是 COCO 预训练权重不是随机初始化对收敛速度影响很大。4.4 数据增强密集果实场景下哪些开关要动数据增强不是全开就好。针对油茶儿数据集中果实密集、互相遮挡、背景复杂三个特点最值得关注的是这几个增强项mosaic 是 YOLOv5 以来提升最明显的增强策略把四张图拼成一张变相扩大 batch 的上下文多样性。但在密集目标场景里四张图拼接时会随机裁剪果实被裁掉一半的情况很常见所以建议用close_mosaic在训练后期关掉它。hsv 增强调整亮度饱和度和色相对果实的颜色变化有帮助但注意饱和度增强幅度别太大否则会把青色叶子和青色未成熟果实混在一起。翻转向量和角度默认参数在树冠场景足够不需要额外调。copy-paste 增强如果数据集目标数不够多可以开它能把一个果实框复制到另一张图上对密集场景有正向帮助但前提是你的目标框标注足够准确否则复制过去的是一堆噪声。5. 训练与转换中绕不开的坑从标注到 val 的四条排查记录这一章写的是我自己在复现这类数据集时真实踩过的坑按出现频率排序。每一条都按现象 → 原因 → 解决的格式讲你在训练时如果撞上同样的问题直接对号入座。5.1 loss 正常下降但 mAP 一直上不去先从标签找问题现象训练日志里 box_loss 和 cls_loss 都在降但 val 的 mAP50 卡在 0.3 左右不动。换预训练权重、调学习率都不管用。原因最常见的是类别映射错位。xml 里的类别名列表和 data.yaml 里的 names 顺序不一致转出来的标签第一列数字指的类别和训练时 YOLO 理解的类别不是一个东西。另一种可能是 xml 里存在difficult1的框这些框本身标注质量差参与训练后模型的置信度被反复拉低。解决先做一次标签验证把所有 txt 文件里的第一列数字统计出来和你的类别数对比。如果数字范围超出了类别数减一说明转换脚本过滤逻辑没生效。然后处理 difficult 框在转换脚本里加一行过滤条件# 过滤difficult框后再写txt if obj.findtext(difficult) 1: continue这两步做完重训一般情况下 mAP 会有肉眼可见的提升。5.2 训练中途 loss 变成 nan先查标签和显存现象训练跑到第 20 到 40 个 epoch 之间loss 突然打印出 nan然后一直回不来。原因90% 的情况是标签文件有问题常见的是空 txt 文件或者某一行坐标出现无穷值。少数情况是学习率设太大触发梯度爆炸尤其当 batch 也开得比较大时。解决先跑一次全量标签检查不要手动翻。# 找出所有空txt find labels -name *.txt -size 0 # 找出坐标越界的行任何字段超出[0,1] awk $20 || $21 || $30 || $31 || $40 || $41 || $50 || $51 {print FILENAME, $0} labels/*.txt-size 0会列出所有 0 字节文件这些文件对应的图片没有目标训练时读到空标签会计算出无效损失。awk 那行检查五个字段是否都在合法区间输出里只要出现任何一行直接定位到对应的 xml 手工修。如果标签没问题再把lr0降到 0.001、batch减半重试。5.3 检测框比实际果实大一圈或整体偏移size 字段不一致现象验证集上检测框能框到目标但整体向右上方偏移或者框明显比真实果实大一圈。训练集上也有同样问题说明不是后处理的问题。原因转格式时用了 xml 里的 size 作为分母但 xml 记录的是标注工具的原始尺寸图片后来被压缩过实际宽高和 size 不一致。归一化算出的 txt 标签虽然值在 0 到 1 之间但还原到实际图片上时真实图片坐标系变了框就错位了。解决转换脚本里不要读 xml 的 size直接用 OpenCV 读实际图片尺寸。改动很小但效果天差地别import cv2 img cv2.imread(str(IMG_DIR / (xml_path.stem .jpg))) img_h, img_w img.shape[:2] # 以实际图片为准忽略xml里的size5.4 训练集验证集都高、换到新场景立刻翻车划分方式有 bug现象同源测试集上 mAP 很漂亮一旦换到另一片果园或另一台设备的视频检测效果明显下降。原因随机划分 train/val 时同一棵树、同一时段的照片同时出现在两边数据高度相似。模型学到的是这片果园的特征而不是油茶儿本身的特征这叫数据泄漏。解决划分时按序列或按目录分不要按单张图片随机分。常见的做法是先把所有图片按拍摄时间或地点做分组然后用组为单位划分# 按目录维度划分每个子目录里的图片要么全进train要么全进val python - EOF import random from pathlib import Path groups sorted([p for p in Path(images).iterdir() if p.is_dir()]) random.seed(0) random.shuffle(groups) split int(len(groups) * 0.8) with open(train.txt, w) as f: for g in groups[:split]: for img in sorted(g.glob(*.jpg)): f.write(str(img.resolve()) \n) with open(val.txt, w) as f: for g in groups[split:]: for img in sorted(g.glob(*.jpg)): f.write(str(img.resolve()) \n) EOF这段脚本会把每个子目录当作一个整体整个目录的图片要么全进训练集要么全进验证集避免同一地块的照片同时出现在两边。如果你的数据没有按目录分好至少要做到按拍摄时刻分桶比如把间隔 1 秒内的照片分到同一边。6. 验证与进阶读混淆矩阵与调阈值把模型推到能用6.1 看混淆矩阵别只看总行数YOLO 的矩阵计数规则训练完第一件事不是看 mAP而是打开 val 输出的 confusion_matrix.png。这张图在主对角线上的值越高越好但有一个容易误判的细节矩阵里的数值是归一化比例不是样本数而且行总和往往不等于该类的真实样本数因为验证时 YOLO 会做多尺度推理和 TTA同一个目标会被多个预测框命中重复计入。所以正确的读法是看背景帧被误判成目标的概率和类别间的混淆程度不要纠结对角线上为什么不等于 1。如果背景那一列有超过 10% 的误检说明模型把树叶纹理当成了果实优先检查数据增强里 hsv 的饱和度设置。6.2 conf 与 NMS 阈值从测试视频里找漏检和重框验证集指标不直接等于上线效果把 best.pt 拿到真实视频里跑一遍边看边调两个参数。# conf0.25是默认值先跑一遍看漏检和误检分布 yolo detect predict modelruns/detect/train/weights/best.pt sourcetest.mp4 conf0.25 iou0.45 saveTrueconf 决定什么样的框算检出调低漏检减少但误检变多调高则反过来。视频里同一颗果实在连续帧中被反复点亮是正常的如果同帧内相互重叠的框太多把 iou 从 0.45 往 0.6 方向调NMS 会合并掉冗余框。油茶儿这类密集场景我一般会从 conf0.25 和 iou0.45 出发先降 conf 补漏检再看误检是哪一类如果是同类果实重复框就升 iou如果是树叶被误判就回到数据增强找原因。前阵子我图省事默认阈值直接部署到果园 demo结果模型把满地落叶当成了成熟果实当场翻车。从那以后我每次训练完都会强制走一遍视频实拍 → 扫 conf/iou → 看误检类型的流程再决定要不要回到训练侧调参养成习惯后踩坑次数明显少了很多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?