ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

芒果害虫数据集VOC+YOLO格式:YOLOv8训练与避坑指南

芒果害虫数据集VOC+YOLO格式:YOLOv8训练与避坑指南 简介这是一份面向目标检测与农业害虫识别的芒果害虫标注数据集覆盖Weevil、beetle、grasshopper、mango_hopper等10个害虫类别整体采用Pascal VOC格式组织可直接用于YOLO等主流检测框架的训练与评估。压缩包大小约191.04MB共包含2000个文件其中1999个为XML标注文件记录了每张图片中的目标类别、边界框坐标等关键信息另附1个txt说明文件方便使用者快速理解目录结构与标注规则本次发布对应3575张芒果害虫图片的标注数据量较为充足文件名采用统一编号便于批量读取和划分训练、验证及测试集整体组织清晰。该数据集适合农业智能监测、果园虫情预警、科研实验以及算法课程作业等场景当前已有224人浏览学习具备一定参考价值尤其适合需要现成标注数据、希望节省人工标注时间的研究者、算法工程师或相关专业学生直接取用。无论是快速搭建检测基线还是开展算法对比实验都能节省大量前期准备时间。1. 拿到「芒果害虫检测数据集VOCYOLO格式3575张10类别」这份压缩包先别急着训练第一次拿到「芒果害虫检测数据集VOCYOLO格式3575张10类别」这份压缩包多数人的第一反应是解压、改一下 yaml、然后直接把 yolov8 的训练命令甩上去。我在农业视觉项目里的习惯是先花十分钟把数据读干净——3575 张图片、10 个类别规模不算大但覆盖果园场景里常见的害虫形态与光照条件同时提供 VOC 和 YOLO 两种标注意味着你可以用 XML 做精细检查用归一化的 txt 直接训模型。这篇文章写给两类人正在做 yolo 入门学习、想拿真实农业数据跑通“yolov8 训练自己的数据集”全流程的初学者以及做智慧农业落地、需要评估这份数据能不能撑起一个可上线模型的工程师。前者照步骤抄作业后者重点看边界和踩坑。2. 先看货VOC 与 YOLO 双格式标注结构里藏着哪些约束2.1 3575 张、10 类别的数据规模对模型意味着什么3575 张图摊到 10 个类别上平均每类不到 360 张。做目标检测的人看到这个数字应该心里有数它属于典型的中小型数据集适合拿来验证算法流程、对比训练策略、跑通部署链路但不要指望它能支撑一个从零训练的大规模检测模型。对农业场景来说这个量级其实很真实——果园现场能采集到的有效样本有限人工标注成本又高能凑出 3500 多张已经是不小的工程。更关键的是 10 个类别这个设定。按虫种划分时芒果害虫之间存在大量形态相似的情况比如蓟马和叶蝉在低分辨率图像里几乎长得一样模型很容易把两个类别横跳如果混入了病害类别则更考验纹理细节的区分能力。所以这份数据做 yolo 入门学习非常合适因为它足够小、跑一轮训练不会让你等太久又足够让新手体会到“类别相似导致混淆矩阵不干净”的真实项目感觉。用这份数据练手 yolov8 训练自己的数据集你能完整经历从数据校验到模型推理的全过程这是纯合成数据集给不了的体验。还有一个容易忽略的点数据集里有没有负样本类。有些农业数据集会在 10 个类别之外单列一个“健康叶片”或者“背景”用来告诉模型哪些东西不该框。如果类别列表里没有负样本类训练时模型会把所有看起来像虫的东西都框出来误检率会明显偏高。拿到类别清单后先确认这一点比先跑训练更重要。2.2 双格式组织方式JPEGImages、Annotations、labels 的对应规则这类数据集解压后常见的目录结构这样分布虽然不同发布者组织方式略有差异但双格式数据集的核心骨架通常是相通的dataset/ ├── JPEGImages/ # 原图jpg 或 png共 3575 张 ├── Annotations/ # VOC 格式的 XML一个目标一个 object ├── ImageSets/Main/ # 官方划分文件train.txt / val.txt部分数据集有 ├── labels/ # YOLO 格式的 txt一行一个目标 ├── classes.txt # 类别名清单按行写 └── data.yaml # 给训练框架用的配置文件可选这条目录对应关系是整个数据集的第一条生命线JPEGImages/xxx.jpg必须精确对应Annotations/xxx.xml和labels/xxx.txt文件名除扩展名外完全一致。任何一级出现多余文件、缺失文件或者文件名前后有多余空格都会在训练时表现为“图片数量对不上”或者“读取标签失败”。拿到压缩包后的第一个动作先做文件数量核对命令很简单ls JPEGImages/ | sed s/\.jpg$// | sort /tmp/img_names.txt ls Annotations/ | sed s/\.xml$// | sort /tmp/xml_names.txt ls labels/ | sed s/\.txt$// | sort /tmp/txt_names.txt wc -l /tmp/img_names.txt /tmp/xml_names.txt /tmp/txt_names.txt comm -3 /tmp/img_names.txt /tmp/xml_names.txt comm -3 /tmp/img_names.txt /tmp/txt_names.txtcomm -3打印只在其中一个文件里出现的名字如果没有任何输出说明图片、XML、txt 三个集合一一对应。这一步看起来啰嗦但真能拦住至少三成翻车——尤其压缩包是从 Windows 或 macOS 环境打包的经常混入Thumbs.db、._xxx.jpg这类系统垃圾文件导致文件数对不上。注意sed s/\.jpg$//只处理了.jpg后缀如果图片是.png或.jpeg结尾记得把后缀替换改成对应的正则否则会出现一堆“缺失匹配”的假警报。2.3 VOC 坐标转 YOLO 坐标的换算公式与检查顺序VOC 的 XML 标注记录的是目标在图像上的绝对像素坐标核心字段是xmin、ymin、xmax、ymax和类别名name。YOLO 的 txt 标注则是归一化坐标五个数字分别是class_id cx cy w h其中cx、cy是中心点相对图像宽高的比例w、h是框宽高相对图像宽高的比例所有值都在 0 到 1 之间。两者换算公式如下w (xmax - xmin) / img_w h (ymax - ymin) / img_h cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h这里最容易踩的一个坑是类别编号。VOC 里目标通过name字符串标识YOLO 里则通过整数编号标识编号的对应关系由classes.txt的行号决定——第 0 行是编号 0第 1 行是编号 1依此类推。如果classes.txt的排序和发布者生成 YOLO 标注时的类别排序不一致整份标注的编号就会集体漂移。更隐蔽的情况是同一个类别在 XML 里叫“thrips”在 classes.txt 里写成了“thrip”转换脚本匹配不到这个字符串于是这条目标被静默跳过训练数据无声无息地少了一批框。检查顺序上我一般随机抽 3 张图做人工对照先看 XML 里目标数量再数对应 txt 里的行数接着把 XML 坐标代入公式算一遍确认和 txt 里的数值对得上。这三张图全部通过才做全量转换。这份数据既然同时给了 VOC 和 YOLO 双格式大概率已经有一套生成好的 YOLO 标签你仍然值得做这一步因为谁也无法保证发布者用的转换脚本没有边界 bug。3. 把压缩包变成可训练数据体检、转换、划分三件事3.1 数据体检先核对文件数量和文件名再用 OpenCV 读一遍图片文件数量核对通过只是第一关第二步必须用 OpenCV 把每张图真实读一遍。很多数据集发布时图片已经损坏但扩展名还在或者图片是零字节文件训练框架读取时直接报错。批量体检脚本不长但能把“训练到一半突然崩”这个隐性炸弹提前拆除import cv2 import glob for path in glob.glob(JPEGImages/*.jpg): img cv2.imread(path) if img is None: print(f[损坏] {path}) continue h, w img.shape[:2] if h 32 or w 32: print(f[过小] {path} {w}x{h})这段代码做了两件事cv2.imread返回None说明图片无法解码直接标记损坏能解码但尺寸小于 32×32 的图后面送入 YOLO 时会被 letterbox 放大插值出来的像素对训练贡献很小甚至起反作用提前过滤掉比让模型硬学更明智。注意glob的路径模式要和实际目录结构匹配如果图片是.png就把后缀改成*.png也可以用glob.glob(JPEGImages/*.*)一把抓再通过os.path.splitext判断扩展名。这一步失败时看什么如果大量图片报损坏优先怀疑压缩包传输不完整重传或者换解压工具如果只有个别图片损坏直接把这些文件名记录到一个broken.txt后续转换脚本和训练配置都避开它。顺便提一句某些数据集为了控制压缩包体积会把大图压得特别狠导致图片尺寸参差不齐这类问题在使用时也要留意。3.2 写一个稳妥的 VOC 到 YOLO 转换脚本带边界截断的版本如果发布者只给了 VOC 格式或者你手里的 YOLO 标签生成得不够可靠自己写转换脚本是最安心的方式。一个带边界截断和坐标过滤的版本如下import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class2id): root ET.parse(xml_path).getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in class2id: continue # 类别不在清单里跳过而不是报错 cid class2id[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.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax - xmin 2 or ymax - ymin 2: continue # 太小的框留着只会变成噪声 cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, os.path.basename(xml_path).replace(.xml, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))这段脚本有几个值得注意的细节恰好也是数据转换里最容易出问题的点。第一root.iter(object)比root.findall(object)更稳能兼容 XML 里嵌套结构第二name必须strip()否则类别名对不上第三框的坐标做了 clip 截断避免标注越界后生成的归一化宽高大于 1这种脏数据会让训练时的损失函数产生奇怪的行为第四宽高小于 2 像素的框直接丢弃这类目标经过下采样后基本不可见硬留着只会让模型学出一堆噪声梯度。调用这个函数前class2id的构建方式是从classes.txt逐行读取并建立字符串到编号的映射行号就是从 0 开始的编号不要用字典的插入顺序去替代行号。转换完成后随机挑 5 个 txt 文件人工核对一遍重点看坐标取值范围是否都在 0 到 1 之间、前几行的类别编号是否和classes.txt一致。3.3 训练集与验证集划分比例、随机种子、同源防污染数据划分看起来是最没技术含量的一步实际翻车率极高。最经典的错误是随机打乱后直接切分完全没考虑“同源图片污染”的问题——同一棵芒果树上同一批害虫拍的 30 张照片被拆成了 20 张训练、10 张验证模型在训练时已经见过高度相似的画面验证分数虚高得离谱一到现场就露馅。我一般按 8:1:1 切分训练、验证、测试并且固定随机种子保证可复现import random from pathlib import Path random.seed(42) # 固定种子复现实验 names [p.stem for p in Path(JPEGImages).glob(*.jpg)] if Path(ImageSets/Main).exists(): # 官方已有划分时优先用官方的 train [x.strip() for x in Path(ImageSets/Main/train.txt).read_text().split()] val [x.strip() for x in Path(ImageSets/Main/val.txt).read_text().split()] else: random.shuffle(names) n len(names) train names[:int(n * 0.8)] val names[int(n * 0.8):int(n * 0.9)] test names[int(n * 0.9):]这里有一个取舍优先采用发布者给的ImageSets/Main官方划分。因为发布者划分时通常会照顾到现场拍摄的分组关系不会把同一场景的照片拆到两个集合里这一点比你自己随机切更可信。如果官方划分不存在则自己切分时尽量按照片文件名里的场景前缀分组——比如文件名如果包含拍摄日期或果树编号把相同前缀的图片全部归入同一集合再用 sklearn 的GroupShuffleSplit做分组划分而不是简单random.shuffle。另外val和test的区别要有意识。val用于训练过程中的早停和调参会被模型间接“看到”test是最终验收只在模型定稿后跑一次。很多人只切了训练和验证最后拿验证集分数当上线指标这在数据量小的时候特别危险。建议从第一天就保留一个不参与任何决策的测试集。4. 用 YOLOv8 训练这份数据集最小可跑配置4.1 data.yaml类别顺序和路径必须对齐在 yolov8 里训练自己的数据集第一件事是写data.yaml。它的核心内容只有几行但错误往往出在最不起眼的地方# data.yaml path: /absolute/path/to/dataset # 数据集根目录建议用绝对路径 train: images/train # 训练图片目录相对 path val: images/val # 验证图片目录相对 path nc: 10 # 类别总数 names: # 类别名列表顺序就是编号 0: class_a 1: class_b 2: class_c 3: class_d 4: class_e 5: class_f 6: class_g 7: class_h 8: class_i 9: class_jnames里每一行的键值对顺序直接决定了类别编号的语义。假如labels/xxx.txt里一行写的是3 0.5 0.5 0.2 0.3那这个目标的类别就是names里编号为 3 的那个名字不是classes.txt里第 3 行的名字。这一步我没有写死成真实类别是因为不同发布者对 10 个类别的排列方式可能不同你要以压缩包内实际的classes.txt为准。目录结构方面如果数据集原本就是JPEGImages平铺组织YOLO 训练时你也可以直接指定train: JPEGImages、val: JPEGImages配合labels: labels的默认约定框架会自动去找和图片同名的 txt但更推荐在复制数据集时就顺手排成images/train、images/val、labels/train、labels/val的标准布局减少后续踩坑。4.2 训练命令与关键超参数epochs、imgsz、batch 的边界环境装好之后一个能直接从命令行跑的训练命令如下yolo detect train \ datadata.yaml \ modelyolov8s.pt \ imgsz640 \ epochs100 \ batch16 \ device0 \ patience20 \ cos_lrTruemodelyolov8s.pt选的是 small 版本。害虫检测属于小目标密集型任务用yolov8n这种轻量骨架很容易因为特征提取能力不足导致漏检反过来也不建议直接上yolov8x因为这个数据集只有 3575 张图大模型在这个量级上几乎必然过拟合收敛速度还慢。imgsz640是 YOLO 系列训练的常见输入尺寸但对芒果害虫这种小目标来说640 可能仍不够。显存允许时优先提到 960 或 1280代价是训练时间几乎翻倍显存不够时先减batch而不是减imgsz因为小目标检测对分辨率的敏感度远高于对 batch 的敏感度。batch16在 16GB 显存显卡上搭配imgsz640基本能跑动显存吃紧就降到 8千万不要硬顶。patience20表示验证集指标连续 20 个 epoch 不提升就早停对这个规模的数据集是合理的cos_lrTrue用余弦退火学习率比默认的线性下降更适合中等规模数据集让损失函数在训练后期收敛得更平滑。另外数据增强默认开着的mosaic1.0对这个数据集有利——它能把四张图拼成一张输入相当于把小目标出现的密度提高模型见过的目标上下文更丰富。如果害虫形态是长条形的细长身体建议顺手把fliplr0.5调成 0镜像翻转会让左右不对称的虫体形态学特征出现冲突。4.3 学会看训练日志损失函数曲线和混淆矩阵训练结束之后在你的输出目录runs/detect/train/下会生成results.png和confusion_matrix.png等图表。新手最容易犯的错是把results.png里的曲线当成广告看到一路走低就宣布胜利实际上至少要看懂三件事。第一是 yolo 损失函数的三条主要曲线box_loss代表边框回归损失cls_loss代表分类损失dfl_loss是分布焦点损失负责让框的分布更集中。三条曲线在训练集上都应该平滑下降如果cls_loss下降得很慢或者中途反弹大概率是类别编号错位或者数据里有冲突标注回第 5 章排查。第二是验证集上的mAP50和mAP50-95两者都是越高越好但mAP50-95对框的精度要求更苛刻在害虫这种小目标上通常比mAP50低不少。第三是混淆矩阵它能直接告诉你哪两个类别最容易互相认错这在后续做难例挖掘时是明确的方向指引。一个提醒这个数据集规模小训练 100 个 epoch 可能 40 个 epoch 就早停了。早停不是坏事brand 眼睁睁看着验证 mAP 开始下滑还不打断训练才是真的浪费时间。5. 避坑报告这份数据集最常见的 5 个翻车现场5.1 类别编号错位训练时 cls_loss 掉不下去现象是训练日志里cls_loss从头到尾几乎不下降验证集mAP50在 0 附近抖动像一条心电图。原因几乎都是labels里的类别编号和data.yaml里的names顺序对不上。比如转换脚本读取classes.txt时用了字典的哈希顺序而不是行号顺序导致编号整体漂移了一位。解决这个问题的第一刀是用一个命令打印标签文件里出现过的最大类别编号和nc对照cat labels/*.txt | awk {print $1} | sort -n | uniq -c输出结果应该是一列从 0 开始、到 9 结束的编号分布。如果最大编号是 9而nc: 10说明标签文件里没有越界的编号问题大概率出在语义顺序上如果最大编号是 10 或者出现了负数说明标签文件本身已经脏了需要回归 VOC 原始标注重新转换。之后再随机挑一个 label 文件按编号去classes.txt里查对应的名字再找到对应图片人工确认画框的是不是同一个东西。5.2 训练集指标不错手机照片测试时标注全歪现象是模型在验证集上表现正常但拿手机在果园里拍的照片测试时检测框整体平移或者旋转了一个角度看起来像是模型“喝醉了”。原因是手机拍摄的 JPEG 图片常带 EXIF Orientation 信息OpenCV 的imread默认不处理它而某些图片处理库比如 PIL 的exif_transpose会自动把图转正。如果数据发布环节有一步用过自动转正但训练和推理代码统一用 OpenCV 读图那一部分标注坐标就会和图像内容错位。解决方法是全流程统一图像读取策略。我个人的做法是禁用一切自动 EXIF 旋转在数据预处理阶段把所有图片先统一转正写回文件再生成标注。这样训练、验证、推理三条路径看到的图像方向始终一致。你甚至可以写一个脚本对比数据集里每张图在cv2.imread和PIL.ImageOps.exif_transpose两种方式下读出的尺寸如果宽高对调了说明这张图存在旋转信息需要重点检查。5.3 双格式转换后坐标漂移VOC 和 YOLO 各说各话现象是同一个数据集用 VOC XML 训练一个模型和用 YOLO txt 训练一个模型最终 mAP 能差出 10 个百分点。原因是两套标注并非完全等价——有些转换脚本为了数据清洗方便会把坐标 clip 到图像边界或者把过小目标过滤掉两个格式的数据自然就不一致了。训练时用 txt验证时用 XML前后标准都不一样分数低是必然的。解决思路是以 VOC XML 为基准对转换后的 txt 做一次逆向回读校验。读取 txt 里的归一化坐标反推回像素坐标再和对应 XML 里的bndbox计算 IoU把 IoU 低于 0.95 的文件全部打印出来。如果发现某个文件大面积漂移直接回到原始 XML 重新生成一份标签不要手改 txt。这一步虽然繁琐但能避免你在整个训练调参过程中反复被“玄学指标”误导。5.4 害虫目标太小mAP50 好看但现场漏检一大片现象是验证集上mAP50已经到 0.85 以上看起来不错但拿到果园拍摄的远距离画面里一测原先那些小虫全都检测不到。原因是害虫目标在图像里往往只有十几个像素经过 YOLO 的多次下采样后可能只剩一两个像素深层特征图里几乎没有可分辨的信息。目标越小对输入分辨率的依赖越强。解决思路分两步。第一步把imgsz从 640 提高到 960 甚至 1280让模型在更高的分辨率下看到目标。第二步如果显卡承受不了 1280 分辨率就改用切图策略把一张 4K 大图切成若干个 640 子图训练和推理都基于子图进行。这和遥感领域用 mmrotate 训练 DOTA 数据集时的切图方案是同一套思路先把大图切小解决目标在全局尺度上的稀疏性问题。注意切图推理时要保留每个子图在原图上的坐标偏移最后把检测框坐标加回偏移量再合并重叠框。5.5 类别严重不均衡损失函数曲线反复横跳现象是训练到 80 个 epoch 之后损失函数曲线没有继续平滑下降而是在一个区间里反复跳动验证集 mAP 也不见涨。原因很可能是 10 个类别里样本数量差异太大多的类别几百张少的只有几十张模型把多数类学会后就陷入了局部最优少数类的梯度被淹没在多数类的梯度里。解决做法有几个可选。一是给每个类别按样本数量的倒数设置权重YOLOv8 支持在data.yaml的names之外额外配置类别权重让少数类的分类损失放大。二是对少数类做离线过采样把它们的图片复制几份再参与训练注意复制时要做随机增强否则模型反而背下来了。三是干脆把样本数特别少的类别从 10 类里抽出来单独训练一个二分类模型往往比硬塞进多分类模型里效果好得多。我个人的经验是低于 50 张的少数类在多分类模型里基本是陪跑单独治理才是出路。5.6 使用了带方向的图片增强长条害虫形态学特征被破坏现象是训练集上损失函数下降很快验证集上却始终上不去两者差距越拉越大。原因是数据增强里的水平翻转对“左高右低”的害虫形态产生了语义冲突——某些害虫的头部朝向在镜像后和真实分布恰好相反模型学到的是矛盾的特征。解决方法是检查data.yaml之外训练命令里的增强参数把fliplr0.5改成fliplr0.0同时可以保留mosaic和mixup。很多农业检测项目里“翻转是否会破坏语义”这个问题被忽略直到验证集分数长期卡住才回头排查。为了确认问题你可以做一组对照实验同一份数据、同一个种子只改fliplr一个参数看验证集 mAP 的差异。6. 从训练集跑到果园盲测和难例挖掘才是落地关键模型在测试集上跑出不错的分数离真正“能用”还差一步。无论mAP50多漂亮我都建议你在第一次训练结束后独立留出一批模型从未见过的照片做盲测。做法是把所有图片按拍摄时间或者果树编号排序把某几棵树的照片全部抽出不参与训练和验证只做最终验收。这种按场景来源划分的盲测集比随机抽样更能暴露模型的真实泛化水平。我自己见过太多在验证集上全绿的模型一到真实果园就漏了一半原因就是验证集和训练集过于相似。第二个值得投入的动作是难例挖掘。把盲测集的推理结果导出专门看置信度在 0.3 到 0.7 之间的预测框这些框大多数是真目标只是模型信心不足。人工复核后把这些框补写进训练集对应的 txt 里下一轮训练的效果往往立竿见影比盲目调数据增强策略更高效。做法用一小段脚本就能完成from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(sourceblind_test/, conf0.3, save_txtFalse, saveTrue) for r in results: boxes r.boxes names [model.names[int(c)] for c in boxes.cls] confs boxes.conf.tolist() # 只保留犹豫的框人工复核 for name, conf in zip(names, confs): if 0.3 conf 0.7: print(f{r.path}: {name} {conf:.3f})这段脚本里conf0.3是保留低置信度预测的门槛数值越低漏掉的少但人工复核工作量会变大打印结果里的每一个难例都值得对应到原图看一眼。说句实话当年我第一次拿农业数据集做检测时只顾着盯验证集 mAP 调参模型在温室测试视频里看着还行拿到果园里测真实照片直接漏了一半小虫。那之后我把盲测当成验收门槛任何超参修改都必须过盲测这一关。数据集的 mAP 只能证明你的模型在“见过类似图”时表现不错证明不了它在现场也能扛住。这个习惯希望帮到你。本文还有配套的精品资源点击获取
返回列表