简介:面向目标检测与农业植保研究者的水稻害虫检测数据集,包含10个常见害虫类别,采用VOC格式标注并划分训练集与验证集。图像为300×300的RGB图片,部分样本通过四图拼接做马赛克增强,每张均含完整边界框,可直接用于YOLO、Faster R-CNN等模型训练,无需额外格式转换。资源共2000个文件,其中1999个XML标注文件与1个可视化脚本,压缩后约89.78MB,train目录含6630张图片及对应XML,val目录含631张图片及对应XML,另附10类别JSON字典文件,目录结构清晰。脚本show.py可随机读取图片绘制边界框并保存结果,便于快速预览标注质量。该资源已有327人学习下载,适合需要带验证集标注数据、快速开展害虫检测实验的入门与进阶开发者。
1. 水稻害虫检测数据集:10 类 VOC 资源能省两周标注时间
做目标检测数据集选型时,我最怕拿到一堆没标注的农田原图,最后还得自己补框。这份水稻害虫检测资源算是把准备工作做完了:10 个类别、VOC 标注格式、训练集 6630 张加验证集 631 张,每张图都有完整的边界框,还顺带给了类别 json 文件和一个能直接跑的可视化脚本 show.py。90 MB 的体积对现阶段深度学习训练来说很轻量,300×300 的 RGB 图像适合做小目标检测的 baseline。如果你在做智慧农业、病虫害巡检,或者想验证 YOLO、Faster R-CNN 在细粒度昆虫目标上的效果,这份数据能省下不少造数据的时间。
2. 目录结构与 VOC 标注:train/val 和 images/labels 的真实对应关系
2.1 data 目录的摆放逻辑:下载后不需要二次整理
先把目录结构摆出来很重要。解压后顶层是 data,下面两个目录:train 和 val。每个目录里面都是 images 和 labels 两个子文件夹,images 放图片,labels 放标注文件。labels 这个目录名容易让人误会成“最终类别标签”,实际上它存放的是和图片同名的 xml 解释文件。
data/ ├── train/ │ ├── images/ │ │ ├── brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg │ │ ├── rice_leaf_roller_99_jpg.rf.4ca0b4072fcee1cc5a572410c3d08011.jpg │ │ └── ... │ └── labels/ │ ├── brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.xml │ ├── rice_leaf_roller_99_jpg.rf.4ca0b4072fcee1cc5a572410c3d08011.xml │ └── ... └── val/ ├── images/ └── labels/训练集共 6630 张图片对应 6630 个 xml 文件,验证集共 631 张对应 631 个 xml,数量完全一致。说明这份数据不是随手丢过来的,至少文件配对是完整的。这里要强调一个自己的习惯:拿到任何数据集先数一遍文件数,而不是直接开训练。别看摘要里已经写了数量,我还是建议自己验一遍,三分钟的事。用 bash 就能做:
echo "train jpg: $(ls data/train/images/*.jpg | wc -l)" echo "train xml: $(ls data/train/labels/*.xml | wc -l)" echo "val jpg: $(ls data/val/images/*.jpg | wc -l)" echo "val xml: $(ls data/val/labels/*.xml | wc -l)"如果 images 和 labels 数量对不上,多半是某次搬运漏了文件,后面加载时就会报 no image found 一类的玄学错误。至于 json 文件,位置一般在 data 同级目录下,名字可能是 classes.json、category.json 之类,作用是告诉你 10 个类别名分别对应哪个数字 id,后面会单独讲。
2.2 VOC XML 关键字段:name、bndbox、difficult
VOC 格式的核心是把每张图的标注写在一个 xml 文件里。用 ElementTree 解析后,结构大概长这样。
<annotation> <folder>train</folder> <filename>brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg</filename> <size> <width>300</width> <height>300</height> <depth>3</depth> </size> <object> <name>brown_plant_hopper</name> <bndbox> <xmin>8</xmin> <ymin>11</ymin> <xmax>116</xmax> <ymax>142</ymax> </bndbox> </object> <object> <name>rice_leaf_roller</name> <bndbox> <xmin>190</xmin> <ymin>71</ymin> <xmax>285</xmax> <ymax>246</ymax> </bndbox> </object> </annotation>观察这个结构要注意三个点。
第一个是<filename>不一定和实际文件名完全一致,因为很多标注工具导出时会自动改名。写代码时不能依赖<filename>去找图,而应该用传入的图片路径来反推 xml 路径。这个问题在文件名带多段点号时尤其明显,第 5 章会详细说。
第二个是<object>节点可以重复出现,每出现一次就是一个目标。摘要里明确说“每张图像均有数个目标”,所以一张 xml 里通常不止一个<object>。如果你的数据加载器只取了第一个 object,等于把同一张图里其他害虫全丢掉了,mAP 会很难看。
第三个是bndbox存的是像素坐标,xmin、ymin 是框左上角,xmax、ymax 是右下角。拿去做 YOLO 时需要换算成归一化的中心坐标和宽高,很多转换脚本在这里翻车,主要是没有除以<size>里的宽高,直接把像素值当成了比例。对 300×300 的图像来说,一个 90×100 的害虫框,归一化后中心坐标约是 0.15 和 0.167,不是原数值。
VOC 标准里还可能有difficult字段,表示该目标是否难识别。如果 xml 里有difficult且值为 1,模型训练时通常应该忽略它。但我看过不少同类数据集导出结果,这个字段经常缺失,所以不能默认它存在。写解析脚本时先用find("difficult") is not None判断一下更稳。
2.3 300×300 与马赛克增强:小目标数据怎么影响训练策略
摘要里写图像分辨率是 300×300 的 RGB 图片,这个尺寸在目标检测数据集里不算大,通常是手机拍摄后缩小的结果。对害虫这种细节纹理丰富的目标,300×300 意味着一个目标可能只占几十乘几十像素,属于典型的小目标场景。
少部分图像做了马赛克增强,就是把四张图拼成一张。这种增强本身是 YOLOv4 之后流行的做法,目的是让模型看到上下文和不同尺度的目标。但注意,拼出来的图尺寸会变成原来的两倍,也就是大致 600×600,如果转换脚本还按 300×300 做归一化,坐标分母就错了。训练框架如果自动 resize 到 640×640,这种增强图上的目标会被拉伸变形,标注框还是原来的像素值,画出来就会对不齐。
所以我拿到这份数据的第一反应,是先跑可视化脚本看一下增强样本长什么样,确认框是不是仍然落在对应目标上。这种检查不要省,马赛克增强一旦在导出时坐标没换算,你训练出来的模型会学出一堆“框偏半边”的坏特征。
3. show.py 可视化脚本:一张图进去,一张带框图出来
3.1 运行入口与输出位置
资源里附带了一个 show.py,这个脚本的存在说明作者自己也意识到“光看标注文件不直观”。使用方式很简单,命令行传入一张图片路径即可。
python show.py data/train/images/brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg执行后脚本会找到同一路径下同名的 xml 文件,读取所有 object,把框和类别名画在原图上,然后保存到当前目录。保存文件名一般是在原图名基础上加后缀,比如brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2_vis.jpg,以实际脚本为准。如果你运行时报找不到模块,大概率是环境里缺 opencv-python,pip 安装后再跑。
注意:脚本默认处理的是相对路径,你最好在项目根目录下运行,而不是先进到 images 目录里再执行。否则它按 images 目录的路径找同名 xml,很可能找不到。
如果想连续看几张图,不要反复手敲长路径,直接写一个 for 循环,取前 8 张试试:
for f in $(ls data/train/images/*.jpg | head -8); do python show.py "$f"; done这种批量跑法能快速扫一遍标注质量,但前提是脚本每次输出的文件名都带原图名,否则第二张图会把第一张覆盖掉,最终只剩下最后一张,检查效果大打折扣。
3.2 等价的绘制逻辑:XML 定位、画框、写保存
如果 show.py 本身是个黑匣子,或者你想改成批量可视化,我一般会自己写一个最小版本。下面这段代码逻辑和常见实现一致,可以直接在命令行跑。
import sys, os, cv2 import xml.etree.ElementTree as ET def draw_boxes(image_path, xml_path, out_path): img = cv2.imread(image_path) if img is None: raise FileNotFoundError(f"读不到图片: {image_path}") root = ET.parse(xml_path).getroot() for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(round(float(bbox.find("xmin").text))) ymin = int(round(float(bbox.find("ymin").text))) xmax = int(round(float(bbox.find("xmax").text))) ymax = int(round(float(bbox.find("ymax").text))) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(0, ymin - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(out_path, img) if __name__ == "__main__": image_file = sys.argv[1] base = os.path.splitext(image_file)[0] draw_boxes(image_file, base + ".xml", os.path.basename(image_file).replace(".jpg", "_vis.jpg"))这段代码里有三个参数值得说明。
cv2.rectangle的 thickness 我设为 2。300×300 的图里框只有几十像素宽,粗一点才看得清;如果是马赛克增强后的 600×600 大图,2 像素也够用。颜色用绿色框、红色文字,如果你觉得原图背景偏绿看不清楚,换成蓝色,这不影响训练,只是方便肉眼检查。
cv2.putText的坐标写了max(0, ymin - 6),避免目标贴顶时文字跑到图像外面。很多脚本不处理这个,输出图上类别名消失一半,看起来像标注缺失。
路径解析用os.path.splitext而不是split(".")。画框后自动找 xml 时,brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg用splitext能得到前缀brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2,再接上.xml。如果换成split(".")[0],只会取到brown_plant_hopper_82_jpg,必然找不到 xml,这是最隐蔽的一个坑。
3.3 画完框后怎么判断数据质量
可视化不只是看“能不能画出来”,我更关心三件事。
第一,框是不是贴合目标轮廓。害虫这类目标边缘不规则,如果有些框大一圈或者只包住身体一半,说明标注标准不统一。少量不统一还能训,多了就得洗数据。
第二,有没有重叠框。两个不同类别对象离得很近时,像yellow_rice_borer和asiatic_rice_borer这类相近害虫,容易互相遮挡。如果<object>里的框重叠面积超过一半,要么是标注员漏分,要么是目标确实重叠,训练时模型会被两个互相矛盾的框干扰。
第三,马赛克增强图上的框是否正确。运行脚本时挑几张尺寸明显偏大的图片看,拼缝处如果一个物体被切成两半,但 xml 里只有一个框,说明拼接时坐标没同步。对这种样本,我的处理方式是直接删掉,而不是手工补框。一个样本带歪整个 batch,性价比太低了。
4. 类别 json 文件与训练前校验:让 XML 里的类名和模型 id 对齐
4.1 json 字典文件在检测框架里扮演什么角色
目标检测数据集的标注通常只有两种信息:目标在哪儿、目标是什么。VOC 的 xml 负责“在哪儿”,而“是什么”就写在<name>里。模型训练时不能直接消化字符串,它需要一个从类别名到整数 id 的映射,这个映射就是 json 文件。
在 MMDetection 的 class_names 配置、Ultralytics 的 data.yaml 里,最终都会变成“类别名到 id”的形式。资源自带的 json 是字典结构,加载后看起来大概是这样:
{ "brown_plant_hopper": 0, # 褐飞虱 "rice_leaf_roller": 1, # 稻纵卷叶螟 "rice_water_weevil": 2, # 稻水象甲 "small_brown_plant_hopper": 3, "yellow_rice_borer": 4, "asiatic_rice_borer": 5, # 其余类别键名和顺序以实际 json 文件为准 }这里我只列了项目文件名里能确认的 6 个键,实际 json 里应该是有 10 个键、编号 0 到 9。使用 json 文件有一个前提:xml 里的<name>必须和 json 的键完全一致。比如 json 用brown_plant_hopper,xml 里写Brown_Plant_Hopper,看起来差不多,但代码比较字符串时就是两个东西。对这类问题,最好的办法不是肉眼看,而是写脚本扫一遍。
4.2 写一个扫描脚本,把 xml 里的真实类别统计出来
import os, glob, json import xml.etree.ElementTree as ET def collect_classes(labels_dir): classes = set() for xml_file in glob.glob(os.path.join(labels_dir, "*.xml")): root = ET.parse(xml_file).getroot() for obj in root.findall("object"): name_node = obj.find("name") if name_node is None: continue classes.add(name_node.text.strip()) return classes train_cls = collect_classes("data/train/labels") val_cls = collect_classes("data/val/labels") all_cls = sorted(train_cls | val_cls) with open("classes.json", "r", encoding="utf-8") as f: class_map = json.load(f) print("XML 中出现的类别:", all_cls) print("类别数量:", len(all_cls)) print("json 键:", sorted(class_map.keys())) missing = set(all_cls) - set(class_map.keys()) extra = set(class_map.keys()) - set(all_cls) print("缺失映射:", missing) print("多映射:", extra)这段代码的输出能直接告诉你两个问题。一是 xml 里是否真的只有 10 个类别名。如果扫描出来是 13 个,说明某个.xml拼错类名,比如多了一个空格,或者手滑把rice_water_weevil写成了rice_water_wevvil。二是 json 里有没有多余的键。我遇到过 json 自带__background__的情况,如果不清掉,训练时会多算一个类别。
还有个细节:classes用的是 set,所以统计结束后看不到每类样本量。如果某个类别只有十几张,模型容易欠拟合。想做得细一点,把classes.add改成Counter累加,就能按类别计数。这一步对不平衡数据分析很有帮助,建议顺手加上。
4.3 用 hash 检查 train/val 是否真的互斥
训练集 6630 张、验证集 631 张,这个比例大约九比一,看起来合理。但目录分得干净不代表图像没有重复。有些导出工具会把全量数据集随机切分,如果随机种子没固定,train 里可能混着 val 的图。验证集里一旦出现训练图,评估指标就会虚高,这个坑特别隐蔽。
检查重复我一般用 md5,因为 300×300 的图读起来很快。代码如下:
import os, glob, hashlib def build_hashes(images_dir): result = {} for img_path in glob.glob(os.path.join(images_dir, "*.jpg")): with open(img_path, "rb") as f: digest = hashlib.md5(f.read()).hexdigest() result.setdefault(digest, []).append(img_path) return result train_hash = build_hashes("data/train/images") val_hash = build_hashes("data/val/images") overlap = set(train_hash.keys()) & set(val_hash.keys()) print("重复图像数量:", len(overlap)) for key in list(overlap)[:3]: print(train_hash[key], val_hash[key])如果 overlap 数量为 0,说明切分基本干净,可以直接用。如果大于 0,也别急着删。有些情况下所谓的重复只是同一地点不同角度拍的相似照片,md5 不同就不会被算出来;真正 md5 相同说明文件字节级一致,那就要考虑把 val 里的副本移到 train,或者从训练流程里排除。这类检查一定要在做标签映射之后跑,因为格式转换过程中最容易出错。顺序上,我习惯先跑 4.2 的类别统计,再跑 4.3 的 hash 检查,最后跑一遍 show.py 抽十张图肉眼确认。
5. 避坑指南:数据加载与格式转换中的五个翻车现场
5.1 XML 解析报错:编码和命名空间
现象:用xml.etree.ElementTree.parse(xml_file)直接抛 ParseError,或者解析成功但root.findall("object")返回空列表。
原因:VOC 文件虽然叫 xml,但来源不同编码也不一样。有的带 BOM,有的标签里混了非 UTF-8 字符,导出工具还可能在根节点加命名空间,导致真正的 tag 变成{http://some.url}annotation,按annotation找不到。这种问题在 Windows 下特别常见,csv 转 xml 的脚本一接管就容易带出乱码。
解决:先把文件按二进制读进来,去掉 BOM,再用ET.fromstring解析。命名空间则用 tag 的split("}")[-1]取最后一段。一个通用做法是这样:
import xml.etree.ElementTree as ET def parse_voc(xml_path): with open(xml_path, "rb") as f: data = f.read().lstrip(b"\xef\xbb\xbf") root = ET.fromstring(data) objs = [o for o in root.iter() if o.tag.split("}")[-1] == "object"] return root, objslstrip只处理 UTF-8 BOM,如果你遇到 UTF-16 编码,改用codecs.open(xml_path, "r", "utf-16")读取。看到报错别急着改 xml 内容,先定位是编码问题还是命名空间问题。
5.2 用 split(".") 解析文件名导致找不到对应 xml
现象:数据集里文件名是brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg,你用filename.split(".")[0]得到brown_plant_hopper_82_jpg,然后去 labels 目录找brown_plant_hopper_82_jpg.xml,结果 FileNotFoundError。
原因:这批文件来自 Roboflow 导出,原始文件名带.rf.<hash>这一段。文件名里有很多点号,split(".")会把它们切成好几段,取第一段等于把关键的 hash 信息丢掉了。xml 的真实前缀应该是brown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2,而不是_82_jpg。
解决:用os.path.splitext代替split(".")。os.path.splitext只去掉最后一个扩展名,保留中间的点号。如果 xml 后缀是.xml,直接把.jpg后缀替换成.xml就行。这是最省事、也最不容易翻车的写法。
5.3 马赛克增强图上的坐标整体偏移
现象:用 show.py 看增强样本时,有些框画在目标旁边,或者框明显比目标大一圈,尤其拼接缝附近的框错得最厉害。
原因:马赛克增强把四张 300×300 的原图拼成一张 600×600 的新图,每个子图里的目标坐标要按子图所在位置加上偏移量。如果导出脚本漏了这一步,bndbox还停留在子图坐标系里,画到大图上就整体偏到左上角。更隐蔽的是,有些增强会对每个子图先 resize 再拼,导致坐标分母不是原来的 300。
解决:检查增强图的size.width和size.height,如果标注尺寸是 600 而某个<object>的 xmax 小于 300,那基本可以判断它漏了偏移。对样本量小的局部错误,我一般直接过滤掉这些 object;如果错误比例超过 5%,直接找作者要原始导出脚本,手工修不划算。
5.4 类别名大小写不一致导致训练类别错位
现象:json 文件里键是brown_plant_hopper,XML 的<name>是Brown_Plant_Hopper。转换到 YOLO 格式后,类别 id 全部按默认顺序分配,褐飞虱被分到了别的 id 上,训练完模型输出完全对不上。
原因:很多标注工具导出时保留原始标签文本,而 json 是后写的,大小写没有统一。代码里最常犯的错误是直接用字符串去 json 里索引,没做 strip 和 lower。索引不到时,有些框架会静默设成 0,而不是报错,所以你会觉得训练正常,但结果全是错的。
解决:先让脚本输出 xml 里的原始 name 集合,再和 json 键做归一化比较。推荐在读 xml 时统一name.text.strip().lower(),在生成映射时也写小写。如果只是大小写差异,直接批量替换 xml 里的<name>文本即可,不要靠人工一个一个改。
5.5 验证集样本太少,评估指标不稳定
现象:同一份数据集训练两次,参数完全一样,mAP 却差了两个百分点以上。你以为是随机种子或 BatchNorm 的问题,最后发现是验证集只有 631 张,单张难样本会影响整体结果。
原因:631 张验证图对 10 类目标来说,平均每类只有六十多张样本,其中只要有一两张遮挡严重的,AP 就掉得厉害。如果数据源本身样本就少,重新划分也不一定有效。
解决:固定随机种子重新做一次划分,比如从 train 里抽出 15% 补进 val,保证每类至少 80 张。如果不想改变原始目录结构,也可以在训练代码里设置seed=42,评估时对完整 val 跑一遍,多次评估取平均。真正确认模型效果还是要跑田间实拍图,这个 val 只适合用来调参。
6. 进阶验证:用马赛克增强样本完成训练前最后一轮检查
6.1 从图像尺寸分布里找出增强样本
马赛克增强后的图像尺寸会从 300×300 变成 600×600,这个差异是识别增强样本最可靠的信号。下面这段代码可以统计整个训练集的尺寸分布。
import cv2, glob from collections import Counter sizes = Counter() for img in glob.glob("data/train/images/*.jpg"): im = cv2.imread(img) if im is None: continue h, w = im.shape[:2] sizes[(w, h)] += 1 for size, count in sizes.items(): print(size, count)跑完你会得到类似(300, 300) 5890和(600, 600) 740的结果。这时把 600×600 的图单独拿出来做一次可视化,重点看四张拼接子图的边界线。如果每条边界线附近的目标都能画出完整框,说明马赛克增强坐标处理没问题;只要发现一个目标被切断而框只有一个,就要回到 xml 检查坐标偏移。
6.2 训练前最终检查清单
进入训练前,我习惯把下面五件事跑完,顺序固定。
- 文件配对校验:遍历
images/*.jpg,确认每张图都存在同名 xml,数量差为 0。 - 类别扫描:按 4.2 的方法统计 xml
<name>集合,确认等于 json 键集合。 - 尺寸分布检查:跑一遍上面的 Counter,确认只有 300×300 和 600×600 两种尺寸,没有异常空文件。
- 空标注检查:扫一遍 xml,找到
len(root.findall("object")) == 0的文件并剔除。 - 可视化抽样:用 show.py 在 train 和 val 里各抽 5 张增强图,确认框的位置和类别名符合直觉。
这五步花不了二十分钟,但能挡住大部分低级错误。从那以后我每次拿数据集训练,都会强制先把这五步跑完,确认 json 和 xml 的映射对得上再开训练,希望帮到你。
本文还有配套的精品资源,点击获取