简介:本资源为芒果害虫检测数据集,面向从事农业虫害识别、目标检测算法训练与课程实践的研究者及学生,可解决芒果种植场景下多类别害虫图像样本不足的问题。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及一一对应的xml、txt标注文件,标注类别共10类,涵盖Weevil、beetle、grasshopper、mango_hopper、mango_mealybug、moth、sawfly、slug、stem_borer、wasp等常见芒果害虫。压缩包为7z格式,共约2000个文件,以1999个xml标注文件和1个说明txt为主,整体约191.04MB,目录结构清晰,便于直接接入检测框架进行训练与验证。目前已有223人学习下载,适合需要快速构建虫害识别模型、开展对比实验或完成课程设计的读者参考使用。
1. 芒果害虫检测数据集:3575 张双格式标注,10 类虫害一次说清
做农业视觉检测的同行大概都有过这种经历:模型结构调了一周,最后卡在数据上——要么类别不齐,要么标注格式对不上训练脚本。这次拆的这份芒果害虫检测数据集,就是冲着这个痛点来的。它包含 3575 张 jpg 图片,每张图都配了 Pascal VOC 格式的 xml 和 YOLO 格式的 txt,标注类别 10 类,覆盖象甲、甲虫、蝗虫、芒果叶蝉、芒果粉蚧、蛾、叶蜂、蛞蝓、茎螟、黄蜂这些芒果园里常见的害虫。换句话说,拿到手就能直接喂给 YOLO 系列训练,也能用 VOC 工具链做二次清洗。适合谁?做果园虫情监测的算法同学、想练手目标检测的在校生、以及需要快速搭一个农业检测原型的工程师。下面从格式、目录、转换、训练、避坑一路拆到验证技巧,尽量把能抄的作业都写出来。
2. 双格式标注拆解:VOC 与 YOLO 到底怎么对应
2.1 为什么同一批图要存两份标注
先讲清楚一个容易混淆的点:这份数据集里 xml 和 txt 是同一批标注的两种表达,不是两套独立标注。VOC 的 xml 用绝对像素坐标记录xmin/ymin/xmax/ymax,而 YOLO 的 txt 用归一化后的class_id x_center y_center width height,数值都在 0 到 1 之间。之所以两份都留着,是因为不同训练框架吃的东西不一样:YOLOv5/v8 官方仓库默认读 txt,而很多老一些的检测代码、可视化脚本、标注复核工具仍然认 VOC。你如果只留一份,换框架时就得自己写转换,反而多一道出错环节。
从项目正文能看到文件命名规律,比如firc_mangopets_74.xml、firc_mangopets_1816.xml,前缀统一是firc_mangopets_,后面跟数字编号。这个编号就是图片和标注的对应键——firc_mangopets_74.jpg对应firc_mangopets_74.xml和firc_mangopets_74.txt。实操里最怕的就是图片和标注对不上号,所以拿到数据集第一件事不是急着训练,而是先做一次配对校验。
2.2 目录结构与文件清单核对
解压后典型的结构是这样(不同打包方式可能略有差异,以实际为准):
mango_pests/ ├── 使用前必读.txt ├── JPEGImages/ # 3575 张 jpg │ ├── firc_mangopets_74.jpg │ └── ... ├── Annotations/ # 3575 个 VOC xml │ ├── firc_mangopets_74.xml │ └── ... └── labels/ # 3575 个 YOLO txt ├── firc_mangopets_74.txt └── ...先用一条命令核对三份文件数量是否一致,这是最省事的体检:
# 分别统计 jpg / xml / txt 数量,三个数字必须相等 find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l find . -name "*.txt" | wc -l如果三个数字都是 3575,说明文件没丢。要是某个数字对不上,八成是解压时文件名编码出问题,或者压缩包本身有损坏,重新解压一次通常能解决。这一步花不了一分钟,但能挡掉后面一堆莫名其妙的报错。
2.3 类别名与 class_id 的映射关系
10 个类别名是英文的:Weevil、beetle、grasshopper、mango_hopper、mango_mealybug、moth、sawfly、slug、stem_borer、wasp。YOLO 的 txt 里存的是数字 id,所以你必须先确定 id 到名字的映射顺序。常见做法是建一个classes.txt,一行一个类别名,行号(从 0 开始)就是 class_id:
Weevil beetle grasshopper mango_hopper mango_mealybug moth sawfly slug stem_borer wasp这里有个血泪经验:映射顺序一旦定了就不能改。我见过有人训练时用一套顺序,推理时又按字母序重排,结果模型把beetle认成Weevil,排查了半天才发现是类别表错位。建议把classes.txt和数据一起版本管理,训练、验证、部署全程共用同一份。
2.4 用脚本验证标注合法性
在正式训练前,写个小脚本扫一遍所有 txt,检查有没有越界坐标、空标注、类别 id 超范围的情况。这类脏数据不查出来,训练时 loss 会莫名其妙地抖。
import os # 类别总数,和 classes.txt 行数保持一致 NUM_CLASSES = 10 label_dir = "labels" bad_files = [] for name in os.listdir(label_dir): if not name.endswith(".txt"): continue path = os.path.join(label_dir, name) with open(path) as f: lines = [l.strip() for l in f if l.strip()] # 空标注文件:图片里没有目标,YOLO 允许,但值得记录 if not lines: bad_files.append((name, "empty")) continue for line in lines: parts = line.split() # YOLO 每行必须是 5 个字段 if len(parts) != 5: bad_files.append((name, "field_count")) break cid = int(parts[0]) coords = [float(x) for x in parts[1:]] # 类别 id 越界 或 归一化坐标不在 [0,1] if cid < 0 or cid >= NUM_CLASSES or any(c < 0 or c > 1 for c in coords): bad_files.append((name, "range")) break print(f"可疑文件数: {len(bad_files)}") for f in bad_files[:20]: print(f)逻辑说明:逐行读每个 txt,先判字段数是否为 5,再判类别 id 是否落在[0, NUM_CLASSES),最后判四个归一化坐标是否都在 0 到 1 之间。参数上,NUM_CLASSES必须和你的类别表一致,写错这个数会把正常文件误判。跑完如果可疑文件是 0,就可以放心进入下一步;如果有,先人工看几张,判断是标注错误还是脚本判据太严。
3. 从 VOC 到 YOLO:转换脚本与坐标换算细节
3.1 VOC 与 YOLO 坐标的换算公式
虽然这份数据集已经给了 YOLO txt,但你还是得懂换算,因为一旦你要自己增补标注、或者拿 VOC 工具改完再转回来,就得手动算。VOC 给的是左上角(xmin, ymin)和右下角(xmax, ymax),图片宽W、高H。YOLO 要的是中心点和宽高,全部归一化:
x_center = (xmin + xmax) / 2 / W y_center = (ymin + ymax) / 2 / H width = (xmax - xmin) / W height = (ymax - ymin) / H反过来,从 YOLO 还原 VOC:
xmin = (x_center - width / 2) * W ymin = (y_center - height / 2) * H xmax = (x_center + width / 2) * W ymax = (y_center + height / 2) * H坑就在W和H上:必须用当前这张图的真实尺寸,不能拿一个固定值套所有图。芒果害虫数据集里图片尺寸不一定统一,用错尺寸会导致框整体偏移。
3.2 批量转换脚本(VOC 转 YOLO)
下面这个脚本把 Annotations 里的 xml 批量转成 YOLO txt,同时生成classes.txt。它用xml.etree解析,不依赖额外库。
import os import xml.etree.ElementTree as ET from PIL import Image # 类别顺序决定 class_id,务必和训练时一致 CLASSES = ["Weevil", "beetle", "grasshopper", "mango_hopper", "mango_mealybug", "moth", "sawfly", "slug", "stem_borer", "wasp"] cls_to_id = {c: i for i, c in enumerate(CLASSES)} xml_dir = "Annotations" img_dir = "JPEGImages" out_dir = "labels_from_voc" os.makedirs(out_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() # 用图片真实尺寸做归一化,缺图就跳过并记录 img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): print("缺图:", stem) continue W, H = Image.open(img_path).size lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in cls_to_id: print("未知类别:", name, "in", xml_name) continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) xc = (xmin + xmax) / 2 / W yc = (ymin + ymax) / 2 / H bw = (xmax - xmin) / W bh = (ymax - ymin) / H # 坐标裁剪到 [0,1],防止个别越界标注污染训练 xc, yc = min(max(xc, 0), 1), min(max(yc, 0), 1) bw, bh = min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f"{cls_to_id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, stem + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:先按CLASSES建 id 映射,遍历每个 xml,用对应 jpg 的真实宽高做归一化,再按公式算中心点和宽高。参数上,CLASSES顺序就是 class_id 顺序,改它等于改标签含义;坐标裁剪那两行是保险,防止个别标注框超出图片边界导致训练异常。跑完把labels_from_voc和数据集自带的labels对比一下,如果内容基本一致,说明自带 txt 是可信的。
3.3 划分训练集与验证集
YOLO 训练需要train.txt和val.txt两个列表文件,每行是图片路径。按 8:2 划分:
import os import random img_dir = "JPEGImages" imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] random.seed(42) # 固定种子,保证每次划分一致 random.shuffle(imgs) split = int(len(imgs) * 0.8) train, val = imgs[:split], imgs[split:] for name, subset in [("train.txt", train), ("val.txt", val)]: with open(name, "w") as f: for im in subset: f.write(os.path.join(img_dir, im) + "\n") print("train:", len(train), "val:", len(val))逻辑说明:random.seed(42)是关键,固定种子后每次跑划分结果都一样,方便复现实验。参数上,0.8 是训练比例,数据量 3575 张时验证集约 715 张,够评估用。如果某类样本特别少,建议改成按类别分层抽样,避免验证集里缺类。
4. 训练落地:YOLOv8 配置与参数怎么设
4.1 数据集 yaml 配置
YOLOv8 用一份 yaml 描述数据路径和类别。在项目根目录建mango.yaml:
path: /abs/path/to/mango_pests # 数据集根目录,写绝对路径最稳 train: train.txt val: val.txt nc: 10 names: 0: Weevil 1: beetle 2: grasshopper 3: mango_hopper 4: mango_mealybug 5: moth 6: sawfly 7: slug 8: stem_borer 9: wasp参数说明:path用绝对路径能避免相对路径在不同工作目录下失效;nc必须等于类别数 10;names的键值顺序必须和 txt 里的 class_id 严格对应。这三处任意一处错位,训练都不会报错,但模型学出来的类别是乱的,属于最隐蔽的坑。
4.2 启动训练与关键参数
yolo detect train \ data=mango.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/mango \ name=exp1逐项说:model=yolov8n.pt是 nano 版预训练权重,3575 张图这个量级用 n 或 s 版就够,上大模型容易过拟合;imgsz=640是常见输入尺寸,显存吃紧可以降到 512;batch=16按显存调,8G 显存跑 640 一般能到 16;lr0=0.01是初始学习率,微调预训练模型时这个值比较稳;patience=20表示 20 轮没提升就早停,省时间。如果训练中 loss 一直不降,先别怀疑模型,回头查类别映射和标注质量。
4.3 训练过程该盯哪些指标
训练日志里重点看三个:box_loss、cls_loss、mAP50。box_loss管定位,cls_loss管分类,两者都应该整体下降。mAP50是 IoU 阈值 0.5 下的平均精度,是判断模型好坏的直接指标。如果box_loss降但mAP50不涨,通常是标注框质量差;如果cls_loss居高不下,多半是类别不平衡或映射错位。3575 张 10 类,平均每类 350 张左右,属于中等偏少,训练时建议开数据增强(YOLOv8 默认已开 mosaic、翻转等)。
4.4 推理与结果核对
训练完用验证集跑一次推理,肉眼核对几张:
yolo detect predict \ model=runs/mango/exp1/weights/best.pt \ source=JPEGImages \ conf=0.25 \ save=Trueconf=0.25是置信度阈值,低于它的框不显示。核对时重点看两类错误:把mango_hopper和grasshopper搞混(这两类形态接近,是这份数据集的难点),以及小目标漏检(mango_mealybug通常很小)。如果混淆严重,可以考虑在类别名上做更细的区分,或者补充这两类的样本。
5. 避坑与排查:标注、映射、训练里最容易翻车的地方
5.1 图片与标注对不上号
现象:训练时提示找不到某张图的标注,或者 loss 异常高。原因:jpg、xml、txt 三者文件名前缀不一致,常见于解压时文件名被截断或编码转换。解决:跑一遍配对校验,用集合运算找出缺失项:
import os stems = lambda d, ext: {os.path.splitext(f)[0] for f in os.listdir(d) if f.endswith(ext)} img = stems("JPEGImages", ".jpg") xml = stems("Annotations", ".xml") txt = stems("labels", ".txt") print("缺xml:", img - xml) print("缺txt:", img - txt) print("多余xml:", xml - img)三个差集都为空才算干净。
5.2 类别 id 与名字错位
现象:模型能检测到目标,但类别名全是错的,或者某类永远不出现。原因:classes.txt、yaml 里的names、txt 里的 id 三者顺序不一致。解决:以 txt 里的 id 为准,反查每个 id 对应的名字,确认三处一致。最稳的做法是只维护一份classes.txt,yaml 和转换脚本都从它读。
5.3 空标注文件被误删
现象:某些图片明明有虫,模型却学不会。原因:这些图的 txt 是空的(标注时漏标),有人清理数据时把空 txt 当垃圾删了,导致图片没有对应标注而被跳过。解决:空 txt 要保留,它表示"这张图无目标",是负样本,对降低误检有用。删了反而让模型没见过背景。
5.4 坐标越界导致训练崩溃
现象:训练几轮后 loss 变成 nan。原因:个别标注框的归一化坐标超出 [0,1],比如xmax大于图片宽度。解决:用 2.4 的校验脚本扫一遍,或者转换时统一做坐标裁剪(3.2 脚本里已经加了)。越界框不裁剪,梯度会爆。
5.5 验证集类别缺失
现象:mAP50波动大,某些类评估结果为空。原因:随机划分时某类样本全落进训练集,验证集里一个都没有。解决:改用分层抽样,保证每个类别在验证集里都有一定数量。样本少的类尤其要注意,必要时对这类做过采样。
6. 进阶技巧:用混淆矩阵定位难分类类别
训练跑通只是第一步,真正决定模型能不能上线的是对错误的分析。YOLOv8 训练结束会自动生成混淆矩阵(confusion_matrix.png),这张图比单看 mAP 有用得多。矩阵对角线是分对的,非对角线就是混淆。拿这份芒果害虫数据集来说,我一般会重点看mango_hopper和grasshopper那一格——这两类都是跳跃类昆虫,形态接近,混淆概率高。如果这一格数值明显,说明模型没学到区分特征,可以考虑三个方向:一是补充这两类的难例样本,二是检查标注时有没有把两者标混,三是适当提高输入分辨率,让小差异更容易被看到。
另一个实用技巧是按类别统计漏检和误检。YOLOv8 的验证输出里每个类都有 precision 和 recall,把 recall 低的类挑出来单独看。mango_mealybug这类小目标如果 recall 偏低,通常是输入尺寸不够,把imgsz从 640 提到 800 往往有改善,代价是显存和速度。下面这段代码可以快速把每个类的指标拉出来排序:
# 读取 YOLOv8 验证结果,按 recall 升序排列,优先处理差类 import pandas as pd # results.csv 在训练输出目录下,列名含 metrics/recall(B) 等 df = pd.read_csv("runs/mango/exp1/results.csv") df.columns = [c.strip() for c in df.columns] # 取最后一轮各类指标(需结合 val 输出的 per-class 结果,此处示意排序思路) print(df.tail(1).T.sort_values(by=df.tail(1).index[-1]))逻辑说明:这段是示意如何从训练日志里定位薄弱类别,实际 per-class 指标建议直接看验证时打印的表格。参数上,重点盯 recall 而不是 precision——农业检测里漏检一只虫的代价通常比误报高,宁可多报也别漏报。
还有一个容易被忽略的点:验证集和训练集的分布要一致。如果训练集里某类都是近距离大目标,验证集里却是远景小目标,mAP 会虚低。划分数据时按拍摄场景分层,比纯随机更靠谱。我现在的习惯是,每次拿到新数据集,先花十分钟跑配对校验和标注合法性检查,再花五分钟看一眼类别分布,确认没问题才开训。这个流程帮我挡掉过好几次"训练半天发现数据是坏的"的翻车。从那以后我每次开训前都强制走一遍这套检查,希望帮到你。
本文还有配套的精品资源,点击获取