
简介面向目标检测与农业植保研究者的水稻害虫检测数据集包含10个常见害虫类别采用VOC格式标注并划分训练集与验证集。图像为300×300的RGB图片部分样本通过四图拼接做马赛克增强每张均含完整边界框可直接用于YOLO、Faster R-CNN等模型训练无需额外格式转换。资源共2000个文件其中1999个XML标注文件与1个可视化脚本压缩后约89.78MBtrain目录含6630张图片及对应XMLval目录含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、difficultVOC 格式的核心是把每张图的标注写在一个 xml 文件里。用 ElementTree 解析后结构大概长这样。annotation foldertrain/folder filenamebrown_plant_hopper_82_jpg.rf.9361af1603809eb101e6c5de3320dfa2.jpg/filename size width300/width height300/height depth3/depth /size object namebrown_plant_hopper/name bndbox xmin8/xmin ymin11/ymin xmax116/xmax ymax142/ymax /bndbox /object object namerice_leaf_roller/name bndbox xmin190/xmin ymin71/ymin xmax285/xmax ymax246/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-pythonpip 安装后再跑。注意脚本默认处理的是相对路径你最好在项目根目录下运行而不是先进到 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_hopperxml 里写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, encodingutf-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_hopperXML 的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 张。如果不想改变原始目录结构也可以在训练代码里设置seed42评估时对完整 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 的方法统计 xmlname集合确认等于 json 键集合。尺寸分布检查跑一遍上面的 Counter确认只有 300×300 和 600×600 两种尺寸没有异常空文件。空标注检查扫一遍 xml找到len(root.findall(object)) 0的文件并剔除。可视化抽样用 show.py 在 train 和 val 里各抽 5 张增强图确认框的位置和类别名符合直觉。这五步花不了二十分钟但能挡住大部分低级错误。从那以后我每次拿数据集训练都会强制先把这五步跑完确认 json 和 xml 的映射对得上再开训练希望帮到你。本文还有配套的精品资源点击获取