简介:这套水稻害虫检测数据集面向目标检测与农业植保智能化应用,覆盖褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆等常见害虫类别,整体任务规模为5229张图像,采用VOC格式进行标注,可服务于虫情监测、防治决策等模型的训练与效果验证。压缩包共2000个文件,全部为xml标注文件,合计109.96MB;xml中记录每个目标的类别与边界框坐标,便于转换为YOLO、SSD等主流检测框架所需的标签结构。已有1218人学习下载。对于缺少田间标注数据的开发者,这份资料可直接进行数据划分与格式转换,减少人工采集和标注成本;若后续需要扩展新害虫类别,也可参考xml字段规范快速生成自定义检测数据集。
1. 水稻害虫数据集 5229 张图:VOC 标记的褐飞虱、绿叶蝉等类别能直接训练吗
收到这份数据集后的第一反应,很多人都是把 zip 解压后直接丢进训练脚本跑一把,翻车概率不小。它标注的是水稻害虫检测场景,共 5229 张图片,格式为 VOC 标记(XML),描述里写了褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆六个类别。这些类别大多是稻株上的小虫或小虫危害状,目标框偏小、单图目标数量多,对数据质量的要求比常规目标检测更高。建议拿到 zip 后先做三件事:统计 XML 里真实出现的<name>标签,核对 XML 和 jpg 文件是否一一对应,抽查坐标是否越界。这三步做完,再决定训练参数,顺序不能反。
2. 从 VOC XML 看水稻害虫数据的组织:六个类别、五个字段和一个命名坑
2.1 XML 标注文件里的五个必读字段
Pascal VOC 的标注文件是单个 XML,和图片同名,只是后缀从.jpg换成.xml。打开任意一份,根节点是<annotation>,里面依次有<folder>、<filename>、<path>、<source>、<size>,然后是一组<object>。对训练脚本来说,真正必须读对的字段是五个。
<filename>决定这张标注对应哪张图片。字段值与实际 jpg 文件名不一致,是转换流程里最常见的问题来源,我见过两份来源不同的 XML 写同一个文件名,导致后写的那份直接覆盖前一份。<size>给出width、height、depth,坐标归一化时要用这组宽高做分母,不少转换脚本只读bndbox不读size,结果坐标系完全错乱。<object>是核心对象块,每个目标一个节,节点里的<name>是真实类别名。<bndbox>内四个子节点xmin、ymin、xmax、ymax是目标框的左上角和右下角坐标,单位像素,没有归一化。<truncated>表示目标被图片边界截断,<difficult>表示难例;这两个字段在第三方平台导出时多数为 0,但转换时不能默认忽略,要先确认自己的训练逻辑是否把难例单独加权。
这个数据集的 XML 文件名以.rf.哈希结尾,是典型图像标注平台导出产物,XML 里的<folder>和<path>通常记录着原始目录。这两个字段对训练没有用处,转换时必须剔除,否则 YOLO 会把路径里的斜杠当成额外信息一起读进去,轻则出现警告,重则训练中断。
2.2 文件名命名规律与标签统计
文件名本身就是信息,比如BROWN-PLANTHOPPER_original_bhopper--89--jpg_..._jpg.rf.3afd....xml可以拆出三层意思:BROWN-PLANTHOPPER是工作区标签,对应褐飞虱;bhopper--89是采集现场的俗名和序号;rf.3afd...是平台为规避同名冲突生成的指纹。文件名前缀可以参考,但不能当作类别权威,因为导出时经常会把多个来源的数据合并进同一批次。
我一般会先对 XML 里的真实标签做一次全量去重,再决定映射表。命令如下:
grep -h -o "<name>.*</name>" annotations/*.xml \ | sed 's/<\/\?name>//g' \ | sort | uniq -c | sort -rngrep -h -o只输出匹配的标签内容而不带文件名,sed去掉<name>和</name>外壳,uniq -c统计频次,最后按次数降序排列。执行结果是一份能看清六类之间比例差异的清单,同时也能发现不属于白名单的杂项标签。
统计完标签再核对文件对应关系:把 jpg 和 xml 按文件名主体取交集,看两边各有多少孤立文件。常见做法是用find加comm,但更省事的是让转换脚本把找不到 XML 的图片和找不到图片的 XML 全部打印出来,一次跑完直接看日志。
2.3 类别映射与白名单表
根据资源标题,这份数据覆盖褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆六类。中文名和 XML 里的英文标签之间需要一张映射表,下表是常见方案,实际名称以统计结果为准:
| 中文类别 | 建议标签 | 说明 |
|---|---|---|
| 褐飞虱 | BROWN-PLANTHOPPER | 文件名与 XML 中均出现过 |
| 稻蝽 | RICE-BUGS | 文件名与 XML 中均出现过 |
| 绿叶蝉 | GREEN-LEAFHOPPER | 按 XML 实际值核对 |
| 叶夹 | LEAF-FOLDER | 按 XML 实际值核对 |
| 蛀干虫 | STEM-BORER | 按 XML 实际值核对 |
| 轮生蛆 | WHORL-MAGGOT | 按 XML 实际值核对 |
映射表的意义是给转换脚本设白名单。不在表里的标签打印日志后跳过,而不是中断整个转换。最常见的命名噪音包括大小写不一致、连字符位置不一、单复数混用,都要在这一层统一掉。映射关系一旦确定,后续训练配置里的类别 ID 顺序就必须锁定,训练到一半再改顺序,模型输出含义会全部错位。
3. 把 VOC 转成 YOLO 格式:坐标归一化脚本、类别映射与五分钟自检
3.1 为什么离线转换比运行时解析更省事
YOLO 系列训练器接收的标注格式是每张图片一个 txt,一行一个目标,内容为类别id x_center y_center width height,后四个数值要除以图片宽高,归一化到 (0,1)。VOC 的 XML 给的是绝对像素坐标,直接训练只有两条路:一是在数据加载器里写 VOC 解析逻辑,每次迭代都读 XML;二是离线把所有 XML 转成 yolo txt。第一条路不是不能走,但每次迭代重读文件,坐标解析错误只会在训练中途冒出来,定位成本高。离线转换能把全部解析问题提前到训练之前暴露,这是数据准备里收益最高的一步。
坐标小数位保留六位足够。对 640 分辨率图片来说,0.000001 的归一化误差远小于一个像素,模型感受野上区分不出来。类别 ID 从 0 开始递增,不能从 1 开始,YOLO 的损失函数和评估脚本都按 0 基计数。
3.2 Python 转换脚本
import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path("annotations") yolo_dir = Path("labels") yolo_dir.mkdir(exist_ok=True) CLASS_NAMES = [ "BROWN-PLANTHOPPER", # 褐飞虱 "RICE-BUGS", # 稻蝽 # 其余类别名以 XML 统计结果为准,列表顺序决定类别 ID ] def voc2yolo(xml_path: Path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") if size is None: print(f"跳过 {xml_path.name}: 缺少 size 字段") return w = float(size.find("width").text) h = float(size.find("height").text) if w <= 0 or h <= 0: print(f"跳过 {xml_path.name}: 非法图片尺寸") return lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_NAMES: print(f"未映射类别 {name} @ {xml_path.name}") continue cls_id = CLASS_NAMES.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) if xmin >= xmax or ymin >= ymax: print(f"无效框 {name} @ {xml_path.name}") continue x_c = ((xmin + xmax) / 2) / w y_c = ((ymin + ymax) / 2) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}") if lines: out = yolo_dir / (xml_path.stem + ".txt") out.write_text("\n".join(lines), encoding="utf-8") for xml_file in sorted(voc_dir.glob("*.xml")): voc2yolo(xml_file)脚本逻辑是先拿 size 再遍历所有 object。size字段缺失或宽高非法直接跳过,这种文件进训练会引发除零;name not in CLASS_NAMES是白名单过滤,只打印日志不中断;xmin >= xmax的判断用来防止宽高为负的框进入模型。最后一段if lines:很关键,没有合法目标的 XML 不生成 txt 文件,而是留到训练前统一清点。
float()转换必不可少,XML 里的坐标是文本,不转类型直接除会报错。f-string里的.6f控制输出六位小数,训练时可复现性更好。
3.3 五分钟自检
转换完成先做两组低技术含量检查,不要直接开训。
# 空标注统计:数量应该与纯背景图数量吻合 find labels -name "*.txt" -empty | wc -l # 越界检查:归一化坐标的四项都应落在 [0, 1] 区间 awk '{for (i=2; i<=5; i++) if ($i<0 || $i>1) print FILENAME": "$0}' labels/*.txt | head -20第一条命令数出空 txt 文件数,如果超过图片总数的 5%,先回头确认 XML 结构,多半是白名单漏了类别。第二条命令用 awk 遍历每行第 2 到第 5 个字段做区间判断,出现越界说明 XML 里的size与实际图片分辨率不一致,常见于二手标注平台的数据。两条都通过,才进入数据划分。
4. 训练前排查与避坑:五个标注高频坑的处理方法
4.1 标签统计结果和描述对不上
现象:统计出十几个<name>,而资源描述只说六类。
原因:同一虫害在不同批次采集时用了别名,或者平台在工作流里重建类目时生成了重复名称,比如GreenLeafhopper和green-leaf-hopper被当成两个类别。
解决:以 XML 里出现频率最高的名称为规范标签,把别名写进映射表统一到规范名。我每次都在转换脚本里保留未映射日志,确认没有丢掉真正需要的类别再开训,日志文件就是这个数据集的真实类别字典。
4.2 目标框坐标越界
现象:第 3.3 节的 awk 检查出现1.034567或负数,YOLO 训练时会直接报数据错误或静默丢弃该目标。
原因:粘贴图像后画布尺寸变了,但标注框没有跟着偏移;或者标注时拖框出了画布边缘。
解决:除法之前先把绝对像素坐标 clip 到[0, W]、[0, H]区间。更关键的是输出诊断日志,对比原始框宽高和 clip 后的宽高,损失比例超过 10% 的样本圈出来复查,这类框即使不越界也往往是位置标错了。
4.3 转换后出现大量空 txt
现象:脚本跑完,labels目录里出现几十个空文件,对应图片看起来也有虫。
原因:有些 XML 里的difficult=1或truncated=1在转换逻辑里被统一过滤掉了;也有一部分是采集初期拍了纯背景图,本就没有目标。
解决:先用空文件清单做人工抽查,区分“该有框但没有”和“本来就没目标”两类。纯背景图可以单独保留成背景样本库,在训练配置里以负样本形式参与;该有框但没有的,要回原始数据集补标注,不能跳过。
4.4 类别不均衡:褐飞虱多、轮生蛆少
现象:统计结果显示某几类占了大半,轮生蛆这类样本可能只有几十个,直接训练时少数类几乎学不出来。
原因:田间采集生态分布就是如此,褐飞虱常成簇出现,蛀干虫和轮生蛆在稻株上很少单独露头。
解决:不要简单复制少数类图片,那样过拟合风险极高。优先做法是按类别分层划分训练集和验证集,保证验证集里每个类别都有代表;训练时给少数类单独调分类损失权重,观察每轮混淆矩阵里少数类的召回率变化,比盲目复制图靠谱得多。
4.5 小目标在预处理后消失
现象:轮生蛆这类目标在原图上可能只有十几个像素宽,缩放到 640 分辨率后,目标框面积占比降到百分之几以下,训练出的模型几乎检测不到。
原因:归一化坐标本身没错,但letterbox缩放过程把小目标框内像素抽样丢失,小目标检测本身又很吃分辨率。
解决:先把imgsz从 640 调到 960,再观察 mAP@50 变化。增强阶段不要对这类图做重马赛克混叠,mosaic 会进一步把小目标缩小;关闭旋转和上下翻转,保留水平翻转。小目标调试没有通用最优解,每一轮只改一个变量,才能知道是哪一步把小目标弄没了。
5. 用 YOLOv8 验收这批 VOC 数据:分层划分、训练配置与难例分析
5.1 按类别分层划分 train 和 val
数据划分用纯随机,很容易出现某类只在训练集出现、验证集完全没有的情况。先按每个 txt 文件的主类别归档,再对每个类别桶做比例切分:
import random from pathlib import Path from collections import defaultdict random.seed(42) txt_files = [p for p in Path("labels").glob("*.txt") if p.stat().st_size > 0] bucket = defaultdict(list) for f in txt_files: cls = open(f).readline().split()[0] # 取首个目标的类别 id 做分层依据 bucket[cls].append(f) train, val = [], [] for cls, items in bucket.items(): random.shuffle(items) cut = int(len(items) * 0.8) train += items[:cut] val += items[cut:] train.sort(), val.sort() Path("train.txt").write_text("\n".join(str(p) for p in train)) Path("val.txt").write_text("\n".join(str(p) for p in val))多标签文件按首个目标类别近似分层,虽然粗,但比纯随机安全。之后写一份rice_pests.yaml,指定数据根目录、train/val 清单和六个类别名称,直接交给训练脚本读取。
5.2 训练配置与命令
首次验证用轻量权重跑一轮,先把数据链路跑通再谈精度:
yolo detect train \ data=rice_pests.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16imgsz先用 640,遇到小目标类别表现太差再上 960。batch按显存调整,16G 显存可以开到 32。训练完看results.csv里的mAP50和mAP50-95两条曲线,水稻害虫这类小目标场景更关心mAP50,不必强求高 IoU 指标。
5.3 混淆矩阵和低置信度样本分析
训练结束打开runs/detect/train下的confusion_matrix.png,VOC 数据最容易暴露的就是两类问题:相邻类别互相错分,说明标签噪声或类别边界不清晰;某一整列偏淡,说明该类别样本量太少。再对验证集做一轮推理,把预测置信度低于 0.4 的结果存下来:
yolo detect predict model=runs/detect/train/weights/best.pt \ source=val_images \ conf=0.25 \ save_txt=True save_conf=True低置信度样本里通常藏着两种问题:目标太小被漏检,或者标注框偏移导致 IoU 一直上不去。从那以后,我每次拿到 zip 里带 VOC 标记的数据集,都会强制走一遍标签统计、坐标越界检查和分层划分三件事,再谈训练曲线。小目标检测里的玄学已经够多了,能提前消掉的变量就别留给后处理,希望帮到你。
本文还有配套的精品资源,点击获取