简介:水稻害虫目标检测数据集面向算法工程师、农业信息化研究者与机器学习初学者,覆盖褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆等典型水稻害虫,包含5229张田间实拍图,可支撑多尺度、密集小目标场景下的识别与定位,适用于无人机巡田、智能虫情测报灯等植保监测项目,也可用于YOLO、Faster R-CNN、SSD等模型训练与迁移学习。压缩包为VOC标记格式,共包含2000个xml标注文件,这类标注文件通常记录目标类别、边界框坐标与图像尺寸等信息,与LabelImg等工具导出的格式一致,可直接解析为COCO、YOLO等格式,便于做数据增强、难例挖掘或标签质量检查;资源包整体约109.96MB,文件命名与样本对应,结构清楚,便于批量训练与验证。数据集已有1218人学习下载,配合原始图像使用可以快速搭建害虫检测基线,降低数据采集与标注成本。对于高校农业AI课程、科研课题及智能植保产品研发而言,这份数据既能作为算法课堂的实战案例,也能为识别模型提供可靠的验证数据,帮助使用者聚焦建模与迭代。
1. 5229张水稻害虫图,VOC标记能不能直接扔进目标检测网络?
收到一份“水稻害虫数据集,5229张图,可检测褐飞虱、绿叶蝉、叶夹稻蝽、蛀干虫、轮生蛆,VOC标记.zip”这类压缩包,基本是农业目标检测项目的常态起点。5229张图听着不少,可六类害虫多数是小目标,稻叶背景又高度相似,直接拿VOC XML去训练通常得到的就是“指标好看、实地翻车”的模型。下面按我处理这类VOC标记数据集的顺序来:先摸清XML结构,再转成YOLO训练格式,跑通YOLOv8后回头验证模型到底学到了害虫还是背景。适合刚拿到数据集准备做病虫害识别的工程师,也适合想把老VOC数据迁到YOLO体系上的老手。
2. 拆开VOC标记的底细:XML结构、六类害虫难度与数据清洗
2.1 解压后先别急着训练:VOC目录结构与XML里到底存了什么
VOC标记是当前目标检测里最常见的中间数据格式,labelImg这类标注工具默认能导出这种结构。它把一张图的标注信息存成同名XML,常见目录大致分三块:JPEGImages放原图,Annotations放标注XML,ImageSets/Main放官方划分。这个zip如果只给了前两块,说明数据划分得自己写。如果你以前接触的是coco2017数据集结构,会发现那里用json存标注,而VOC用XML,两者转YOLO的路径完全不同,不能混用。
打开一个XML会看到filename、size、object三组关键信息。size里的width/height是归一化换算的唯一依据,object里name是类别名,bndbox里四个顶点是像素坐标。我先跑一个统计脚本,把全部XML的类别分布拉出来:
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("Annotations") stats = {} for xml_path in sorted(xml_dir.glob("*.xml")): try: tree = ET.parse(xml_path) root = tree.getroot() except (ET.ParseError, OSError) as e: print("bad xml:", xml_path, e) continue for obj in root.findall("object"): name = obj.find("name").text stats[name] = stats.get(name, 0) + 1 for name, cnt in sorted(stats.items(), key=lambda x: x[1], reverse=True): print(f"{name}: {cnt}")这段脚本按XML里的name字段累加每个类别框数,并把有问题的XML直接打印出来。看到统计结果后做三个判断:类别拼写是否统一,会不会同一个褐飞虱被标成两个名字;各类别框数量是否悬殊;有没有某个XML的object数为0。这些信息决定后面的转换脚本要不要加类别归并逻辑,也决定训练时要不要给少数类加权。
以5229张图这个规模来说,VOC标注本身已经是能直接用的数据资产,但离可训练还差两步:一是把XML里的像素坐标换算成YOLO需要的归一化坐标,二是把图片路径和标签路径按训练/验证拆开。 这两步做得不干净,后续所有训练问题都会反馈到“数据集不行”这个结论上,实际往往是格式转换埋的雷。
2.2 褐飞虱、绿叶蝉、叶夹稻蝽、蛀干虫、轮生蛆的检测难点
目标检测模型面对这六类害虫,和人类看图完全不是一回事。六类里面,褐飞虱是典型的“多而小”:褐色小虫聚集在稻株根部叶鞘,几十只挤在一起还互相遮挡,漏检通常从这里开始。绿叶蝉颜色与嫩叶高度接近,浅绿身体贴在叶脉上,特征是“背景即本体”。叶夹稻蝽这类稻蝽多数时间藏在叶鞘缝隙里,露出来的只有半个腹部。蛀干虫本身在茎秆内部,地面能看到的只有蛀孔和周围变色的组织,标注框和稻茎重合度很高,模型很容易学会框茎秆而不是框虫。轮生蛆是白色小蛆状幼虫,喜欢聚在心叶与穗部,目标尺度最小。
拿这六类去套小目标检测的经典问题,踩坑点都集中在两类:一类是背景混淆(绿叶蝉、叶夹稻蝽),一类是密集重叠(褐飞虱、轮生蛆)。我整理了一张对照表,训练前先对着它判断哪些类要特殊照顾:
| 类别 | 常见形态特点 | 聚集方式 | 最容易出的问题 |
|---|---|---|---|
| 褐飞虱 | 褐色小虫,多在稻株下部 | 密集群集 | 漏检多,重叠框多 |
| 绿叶蝉 | 浅绿,与嫩叶颜色相近 | 分散或少数聚集 | 贴背景,特征被叶子吃掉 |
| 叶夹稻蝽 | 稻蝽类,常藏叶鞘 | 单只为主 | 框出半截虫,学成叶鞘纹理 |
| 蛀干虫 | 幼虫在茎内,外露痕迹少 | 单只 | 框和茎重合,学的是茎节 |
| 轮生蛆 | 白色小蛆状,多在穗部/心叶 | 小簇聚集 | 目标太小,imgsz不足直接消失 |
表里列的是普遍现象,具体还要以zip里的图片为准。5229张图按六类分配,平均每类不到900张,再扣除不同拍摄角度和光照,每类真正能用来训练的有效样本可能只有几百张,这个量级基本离不开预训练权重做迁移学习。迁移学习能成,前提是数据的拍摄条件和真实田间场景接近;如果压缩包里的图是网图混拍的,训练完的模型对特定田块有效,换个生态区就明显掉点。
还有一个常被忽略的问题:图片分辨率和目标像素占比。假如褐飞虱在一张1920×1080的图里只有30×40像素,imgsz=640时缩到10像素左右,特征基本消失;而如果原图本身是800×600的近景,目标能占到80像素,模型学起来就轻松很多。先按类别统计标注框的宽度和高度分布,能直接告诉你该把imgsz定到多少,而不是盲目套默认值。
2.3 训练前先清一次底账:缺失标注、重复图片、读不出来的坏图
数据集里经常混着一些脏文件,不清理就训练,会在某个epoch突然让预处理中断,而且报错位置很难从日志看出来。我习惯写一个集合求差脚本,先把“有图无标注”和“有标注无图”两个方向都扫出来:
from pathlib import Path jpg_dir = Path("JPEGImages") xml_dir = Path("Annotations") jpg_stems = {p.stem for p in jpg_dir.glob("*.jpg")} xml_stems = {p.stem for p in xml_dir.glob("*.xml")} print("jpg without xml:", sorted(jpg_stems - xml_stems)) print("xml without jpg:", sorted(xml_stems - jpg_stems))集合差集的第一行,是那些会被送进训练列表但实际上没有标签的图;第二行是标注了但找不到原图的孤儿XML。两类都直接跳过或补对应文件。注意jpg_stems取文件名主体,不要带上扩展名,否则“.JPG”和“.jpg”会被当成两个文件。再做一步图片可读性检查:
import cv2 from pathlib import Path for p in Path("JPEGImages").glob("*.jpg"): img = cv2.imread(str(p)) if img is None: print("unreadable:", p)cv2.imread返回None说明文件头损坏或实际不是图片,这类文件在Ultralytics加载时会导致训练停止。清完这三个方向,再谈转换才有意义。
3. 把VOC转成YOLO格式:转换脚本、数据划分与三个标注边界坑
3.1 用一套voc2yolo.py完成坐标换算
VOC和YOLO的标注本质区别只有一点:VOC存的是像素坐标顶点(xmin、ymin、xmax、ymax),YOLO存的是归一化中心点和宽高(class xc yc bw bh),并且类别从0开始编号。转换就是一次简单坐标运算,但5229张图手工改不现实,写个脚本一次跑完:
import xml.etree.ElementTree as ET from pathlib import Path classes = ["brown_plant_hopper", "leafhopper", "daochun", "stem_borer", "grub"] # 这里按zip内的实际类别名和顺序为准,顺序决定label编号 def voc2yolo(xml_path: Path, out_dir: Path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) if w == 0 or h == 0: print("bad size:", xml_path) return lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: print("unknown class:", name, xml_path) continue cls_id = classes.index(name) b = obj.find("bndbox") xmin = float(b.find("xmin").text) ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text) ymax = float(b.find("ymax").text) xc = ((xmin + xmax) / 2.0) / w yc = ((ymin + ymax) / 2.0) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h xc = min(max(xc, 0.0), 1.0) yc = min(max(yc, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines)) xml_dir = Path("Annotations") out_dir = Path("labels") for xml_path in xml_dir.glob("*.xml"): voc2yolo(xml_path, out_dir)坐标换算的核心是中心点,算完做一次[0,1]截断,能避免边界框越界把训练拉崩。如果脚本输出的txt里出现整行都是1.000000,大概率是size节点读错或原始标注画出了图片边界,要回头修XML而不是硬训练。classes列表的顺序直接决定最终模型的类别编号,一旦定下来,data.yaml必须完全一致,中途改动会让已生成的标签全部作废。
写这个脚本时还要注意一个细节:每个XML只生成一个txt,文件名用XML的stem,确保和jpg一一对应。如果zip里出现了两个同名文件在不同目录,脚本会互相覆盖,需要提前用绝对路径去重。
3.2 按图划分train/val,并写好data.yaml
数据划分最忌讳直接copy整个目录,更忌讳按“框”抽样。同一张图里的多个目标天然相关,按框划分会把同图信息泄漏进验证集,让mAP虚高。正确做法是按图划分,把图片路径写进txt:
from pathlib import Path import random random.seed(42) jpg_files = list(Path("JPEGImages").glob("*.jpg")) random.shuffle(jpg_files) split = int(len(jpg_files) * 0.8) train_files = jpg_files[:split] val_files = jpg_files[split:] with open("train.txt", "w") as f: for p in train_files: f.write(str(p.resolve()) + "\n") with open("val.txt", "w") as f: for p in val_files: f.write(str(p.resolve()) + "\n")随机种子固定成42,每次跑脚本生成的划分一致,方便复现实验。划分比例按5229张图的经验,80/20足够;如果后续要调阈值,可以把有难度的类多留一些进验证集。
然后写data.yaml,注意names顺序必须和3.1里classes一致:
path: /home/you/rice_pests train: train.txt val: val.txt names: 0: brown_plant_hopper 1: leafhopper 2: daochun 3: stem_borer 4: grubtrain和val指向txt,不要求目录结构必须是images/train/val。yolo train读取train.txt里的图片路径后,会自动去同目录或指定labels目录找对应txt。如果图片和标签不在一个目录,需要在data.yaml里用names之外的方式手动指定,Ultralytics默认规则是图片路径的父目录换成labels,所以JPEGImages对应的标签目录最好就叫labels,否则要在脚本里做路径替换。
3.3 转换阶段最容易翻车的三个边界坑
第一个坑是XML里没有size节点。部分工具导出的标注不写图片尺寸,脚本读width时直接报NoneType错误。解决方法是先用PIL或OpenCV读原图尺寸回填,不要依赖XML里的size。第二个坑是bndbox坐标越界或反序。手绘标注偶尔会把xmin画在xmax右边,直接用会生成负宽度的框,训练时标签变异常。解决:读取后做一次min/max矫正,宽或高小于1像素的框直接丢弃。第三个坑是classes列表顺序和XML里的类别名对不上。比如XML里既有brown_plant_hopper又有褐飞虱,脚本会把第二个当unknown class跳过,导致这个类别整类消失。解决:先跑第2章的统计脚本,把所有name打出来,统一成一份类别映射表,再写进classes。
这三个坑的共同特点是:训练时不会马上报错,而是等模型跑几十个epoch后表现异常,排查成本远高于转换时多写几行防御。所以我现在的习惯是转换完立刻抽样可视化标签,直接看jpg上叠的框对不对,省得后面做无头排查。
4. 用YOLOv8训练水稻害虫模型:先跑通再调参,微调崩了也不怕
4.1 环境、目录布局和最小训练命令
转换完成后,数据集就算能被YOLO读了。我用Ultralytics YOLOv8做训练,先把环境装好:
pip install ultralytics装完后验证一行命令能找到yolo入口即可。目录布局用最简单的方式:图片放在JPEGImages,标签放在labels,data.yaml里写train.txt和val.txt的路径,不强制复制成images/train这类官方结构。第一次训练不要直接上大图长epoch,先用最小配置跑通管线:
yolo train data=data.yaml model=yolov8n.pt epochs=30 imgsz=640这段能把环境、数据路径、标签编号是否匹配全部验证一遍。如果这条命令报标签错误或类别数错误,说明问题出在前面转换阶段,先修再继续。第一次训练建议用yolov8n这个最小模型,不是因为它精度高,而是它迭代快,能快速暴露数据问题;等确认数据没问题再换yolov8m或yolov8l。
训练前快速看一遍目录结构能避免一半的路径问题:
rice_pests/ ├── JPEGImages/ │ ├── 000001.jpg │ └── ... ├── labels/ │ ├── 000001.txt │ └── ... ├── train.txt ├── val.txt └── data.yamllabels下每一行txt对应一张图,行数等于这张图里标注框的个数。没有txt的图片会被Ultralytics当成背景图处理,如果这类图混进训练集过多,模型会偏向输出空预测。
4.2 针对小目标害虫的四个必调参数
跑通之后,真正影响这5229张图效果的是下面四个参数。我按优先级排序列成表:
| 参数 | 默认值 | 这个数据集的调法 | 原因 |
|---|---|---|---|
| imgsz | 640 | 调到1280 | 褐飞虱、轮生蛆在640下只有十几个像素,特征被压缩掉 |
| batch | 自动 | 显存不够就降到4或8 | imgsz变大后显存翻倍,batch要跟着降 |
| mosaic | 1.0 | 视情况降到0.5 | 密集小目标被mosaic裁剪后出现大量半截虫 |
| close_mosaic | 10 | 提前到30 | 最后阶段让模型回到完整目标上精修 |
对应的训练命令如下:
yolo train \ data=data.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=1280 \ batch=8 \ lr0=0.001 \ mosaic=0.8 \ close_mosaic=30 \ patience=30imgsz调大是最直观有效的一步,但显存代价也最大。batch=8跑不动就降到4,再不行就换yolov8n或者把imgsz降到960,不要硬扛。mosaic这个参数在密集小目标场景要特别注意:mosaic会把四张图拼在一起随机裁剪,如果褐飞虱的框正好落在拼接缝上,模型见到的是半只虫,训练多了反而学偏。close_mosaic=30的意思是最后30个epoch关闭mosaic,让模型回到完整目标上精修边缘回归。
训练日志里要盯两个数:box_loss和cls_loss。小目标场景box_loss降得慢是正常的,真正危险的是cls_loss在后期反弹或恒定不动,那多半是类别不均衡导致模型在多数类上过拟合,少数类彻底放弃。
4.3 目标检测模型微调崩了?先查这三个原因
模型微调崩了这几年成了高频问题,拿5229张图微调YOLOv8,最常见的崩法有三种。
第一种是loss出现NaN。一般发生在第一个epoch,原因多为学习率太大或AMP半精度在浅模型上不稳定。解决:把lr0降到0.0005,并关掉AMP:
yolo train data=data.yaml model=yolov8n.pt epochs=50 imgsz=640 lr0=0.0005 amp=False第二种是训练正常,但mAP50全程为0。检查顺序:train.txt里的路径是否存在;labels目录里对应txt是否非空;data.yaml的names编号是否从0开始连续。这三个问题都属于黑匣子数据管线,日志只会报一句很暧昧的warning。第三种是loss掉得很快但验证集框全贴边,通常是归一化时用了XML里错误的size,或验证图片没有letterbox,模型学了个寂寞。
微调崩了的时候不要急着改网络结构,也别信“换更深的模型就能解决”这种玄学。按数据路径、标签内容、学习率、AMP顺序排查,九成以上的训练事故都出在这几层里。
5. 避坑:水稻害虫检测最常见的五次训练事故排查
5.1 现象:训练报错“class id out of range”
现象:Ultralytics启动时报标签编号越界,具体指向某个txt的第几行。原因:转换脚本的classes顺序和YOLO读取的标签编号对不上,常见的是XML里出现了统计时没发现的第6个类别名,转换时类别名没匹配上,标签文件里混进了脏数据。解决:回到第2章统计脚本,把所有name打印全,确认classes列表覆盖全部类别后重新生成labels。
5.2 现象:loss在降,但预测框全部贴边或覆盖整张图
现象:训练后期可视化预测结果,框在图像边缘堆积,或一个框覆盖大半张图。原因:XML坐标和图片尺寸不匹配,常见于原图被压缩后XML没有同步更新,归一化结果失真;也有的是bndbox反序未被矫正。解决:检查转换时读到的w/h和实际图片尺寸,写一个脚本逐个对比,不一致的图用真实尺寸重新转换。
5.3 现象:褐飞虱漏检率特别高,绿叶蝉几乎全被当成叶子
现象:混淆矩阵里褐飞虱的召回率明显低于其它类,而绿叶蝉那一路的false positive主要是背景。原因:这两个目标尺度太小或对比度太低,imgsz=640时特征已经消失;另一个原因是训练集里这两类的大目标居多,模型没机会看小的。解决:imgsz提到1280,数据增强里增加尺度抖动,必要时对密集区块做切图训练。
5.4 现象:验证集mAP很高,拿到大田拍的照片直接翻车
现象:在验证集上mAP50超过0.9,换一个相机或换一天光照就漏掉大半。原因:验证集和训练集来自同一批图片、同一拍摄环境,相当于测试集分布泄漏。解决:按拍摄时间、田块或光源条件划分验证集,不要让同一场景的连续帧同时出现在训练和验证;训练时打开光度扰动,让模型见过更多色彩偏移。
5.5 现象:调整参数后效果反而更差,且复现不了
现象:换一个batchsize或imgsz,mAP下降明显;同一份参数跑两次结果不一致。原因:数据量本来就少,随机初始化、mosaic随机裁剪、数据增强放大了验证集的偶然性;有时顺手改动了data.yaml里的path,模型实际加载的数据不全。解决:固定随机种子,把每次训练的超参数存成独立配置,比较实验时只改一个变量;复现不出来的先查数据目录是否有缓存。
6. 最后一步:验证模型学到的是害虫而不是背景
6.1 看懂混淆矩阵和PR曲线
训练结束后,不要只看mAP50一个数。去runs/detect/val下找confusion_matrix.png,重点看褐飞虱和绿叶蝉的互相串扰程度:这两类如果互相误判,说明模型在用小虫的“某个局部特征”分类,而不是完整虫形。PR曲线里那条陡降的线段对应着置信度阈值应该设在哪,密集害虫场景一般要把conf压低到0.2到0.3,才能把重叠漏检捞回来。
6.2 把预测框画回原图,逐类过目
这一步最笨但最有效:
yolo predict model=runs/detect/train/weights/best.pt source=val_images save=True conf=0.25保存下来的标注图按类别翻一遍,重点看两类:一是框住整片叶子的假阳性,说明模型学到的是叶型;二是框只有几个像素的极端小目标,说明imgsz还不够。我遇到过在验证集上正常的模型,画完图才发现叶夹稻蝽和蛀干虫的框都偏左上角几个像素,那是数据标注本身的系统性偏移。
6.3 小目标害虫的进阶验证:切图推理和TTA
如果画完图确认漏检集中在密集小目标,先别急着加epoch,试试切图推理:把大图切成512或640的patch,按patch分别预测再合并结果。常见做法是用SAHI这类切片推理库,它能保持原图坐标映射,几十行代码就能接入YOLOv8。切图后的召回通常比一次整图推理高不少,代价是推理时间变长。另一个低成本技巧是开启TTA,让模型对翻转、缩放后的图像各预测一次再投票,对小目标召回有实际帮助。
我现在的习惯是,训练前一定先花十分钟把标签可视化过一遍,训练后再把预测框画回原图过一遍。这一步看着笨,却能省掉后面所有参数上的玄学调参。5229张图能支撑的模型上限就在那,想往下走,还得靠切图、补拍和多尺度训练慢慢把精度抠出来。希望帮到你。
本文还有配套的精品资源,点击获取