简介这份以5229张水稻害虫图像为基础的目标检测数据资源面向农业植保、计算机视觉研究与深度学习开发者覆盖褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆等常见害虫类别可直接用于训练和评估害虫检测模型。压缩包共2000个文件全部为VOC格式的XML标注文件包体约109.96MB每个XML记录对应图像的类别、目标框坐标等关键标注信息便于直接接入YOLO、Faster R-CNN等主流检测框架显著节省人工标注成本。已有1218人浏览学习适合需要开展水稻害虫智能识别、田间监测预警等方向的团队或个人使用。借助这批标注数据研究者可快速构建检测模型复现论文实验或落地农业植保应用并可用于模型训练、迁移学习与数据增强等环节具有较高的实用参考价值。1. 先看清这份水稻害虫数据集能做什么害虫检测真正难的不是模型而是标注数据本身。这份数据集提供了 5229 张田间实拍图像覆盖褐飞虱、绿叶蝉、叶夹、稻蝽、蛀干虫、轮生蛆六类水稻主要害虫标注格式为 Pascal VOC XML。拿到手之后能直接用于训练 YOLOv5/YOLOv8/RT-DETR 等主流检测模型也可以转成 COCO JSON 喂给 Mask R-CNN 这类实例分割框架——虽然标注框是矩形框但坐标信息足够支撑迁移。适合做智慧农业巡检、昆虫自动识别系统、植保无人机图像分析的同学直接用。需要提醒的一点这份数据出自 Roboflow 导出流程文件名里保留了采集时的原始命名预处理时不能只靠文件名分类必须解析 XML 里的name字段才能真正恢复标签。理解了这一点后面处理任何 VOC 数据集都能少踩一个坑。2. 数据集构成与 VOC 标签结构解剖2.1 文件命名规则透露的信息从样本文件名的模式可以反推出数据集的原始结构。以BROWN-PLANTHOPPER_original_bhopper--89--jpg_74816510-a595-4b40-965c-1a01944d6711_jpg.rf.3afd557efe4b9a42539e2207c55b4e2c.xml为例分段拆解BROWN-PLANTHOPPER是类别名大写形式original表示原始图片未增强bhopper--89--jpg是源文件名中间那串 UUID 是 Roboflow 为每张图片生成的唯一标识末尾.rf.加一段哈希是导出批次标记。这说明该数据集很可能经过 Roboflow 平台的清洗和版本管理已经是整理好的状态。但正因为保留了.rf.标记直接按文件名前缀做类别划分会出问题——BROWN-PLANTHOPPER_original只是源目录名不代表图里只有褐飞虱。一张图完全可能同时出现褐飞虱和水稻蝽文件名只记录第一个类别。正确的做法是扫描 XML 内容。2.2 VOC XML 的字段含义与定位逻辑VOC 格式的核心是annotation根节点关键字段集中在size和object两块。size里的width、height、depth是图像原始尺寸做坐标归一化时必须从这里读取不能依赖代码里硬编码的值。每个object包含name类别名、bndbox边界框四角坐标和difficult是否为难例。田间害虫数据集里difficult字段通常为 0因为拍摄角度和遮挡本来就多标起来已经够费劲。整个数据集的类别映射大致如下表所示文件内类别名中文常见名危害部位BROWN-PLANTHOPPER褐飞虱稻株茎基部汁液GREEN-LEAFHOPPER绿叶蝉叶片汁液RICE-BUGS稻蝽类穗部、嫩茎RICE-LEAF-FOLDER叶夹稻纵卷叶螟叶片卷叶危害STEM-BORER蛀干虫螟虫类茎秆内部蛀食WHORL-MAGGOT轮生蛆稻潜蝇类心叶、生长点注意RICE-BUGS是复数形式说明数据集中稻蝽类没有细分到种。训练时如果业务需要区分稻绿蝽和稻黑蝽得自己在原图上补充标注。BROWN-PLANTHOPPER全部大写也是 Roboflow 导出的常见状态——原始标签名可能带着空格或小写导出时被统一规范化了。2.3 六类害虫标注的关键差异这六类害虫在图像上的尺度差异非常大。褐飞虱和绿叶蝉体长只有 2~5 毫米在一张 1920x1080 的田间照片里往往只占十几个像素而稻蝽和蛀干虫造成的危害症状茎部枯白、穗头变色范围大得多标注框可能覆盖上百像素。这种极端的尺度分布直接决定了训练时的 anchor 配置和数据增强策略后面第四章会讲具体怎么调。另外褐飞虱和绿叶蝉在低分辨率下形态接近都是小虫体趴在稻株上类间混淆是预期内的难点。训练前建议先用labelImg或X-AnyLabeling打开几十张图人工核对一遍边界框——特别是确认bndbox是否出现xmin xmax或者坐标超出图像边界的脏数据。3. 从 VOC 到 YOLO训练前必须完成的格式转换3.1 为什么要转格式YOLOv5/YOLOv8 原生的标签格式是每张图对应一个同名.txt文件每行内容为class_id x_center y_center width height四个坐标值均相对于图像宽高做归一化取值在 0~1 之间。而这份数据集提供的是绝对值像素坐标差的不是格式而是坐标系语义。更关键的是YOLO 的标签文件不包含图像尺寸信息归一化坐标一旦算错模型训练时边界框会直接偏移出特征图范围。因此VOC 转 YOLO 本质上不是改后缀而是重新计算坐标并重构目录结构。常见做法是转换成如下目录dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/图片和标签文件保持同名不同后缀YOLO 训练时会根据图片路径自动寻找同名的.txt标签。这是 YOLOv8 默认的布局比单独维护一个临时文件列表省事得多。3.2 转换脚本的完整实现下面这个脚本是完整的转换实现直接针对文件名里带.rf.标记的 XML 文件。核心思路是遍历所有 XML解析出图像尺寸和每个边界框的绝对坐标然后统一除以宽高做归一化。同时把类别名映射为整数 ID避免直接拿字符串做标签。脚本最后按 7:2:1 的比例做随机划分保证训练集、验证集、测试集互不重叠。import os import glob import random import xml.etree.ElementTree as ET from pathlib import Path # 类别与整数 ID 的映射按类别名的字典序排列保证可复现 CLASS_MAPPING { BROWN-PLANTHOPPER: 0, GREEN-LEAFHOPPER: 1, RICE-BUGS: 2, RICE-LEAF-FOLDER: 3, STEM-BORER: 4, WHORL-MAGGOT: 5, } def convert_voc_to_yolo(xml_path, output_dir): 转换单个 VOC XML 文件为 YOLO txt 标签。 返回 (image_filename, width, height) 或 None解析失败时 tree ET.parse(xml_path) root tree.getroot() # 优先取 filename 字段若为空则从 XML 文件名反推 image_name root.findtext(filename) if not image_name: image_name Path(xml_path).stem .jpg size root.find(size) if size is None: return None width int(size.findtext(width)) height int(size.findtext(height)) if width 0 or height 0: return None yolo_lines [] for obj in root.iter(object): # 跳过 difficult 标记为 1 的样本严重遮挡或边界不清 difficult int(obj.findtext(difficult, 0)) if difficult 1: continue name obj.findtext(name) if name not in CLASS_MAPPING: continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 数值容错坐标超出图像边界时裁剪到合法范围 xmin max(0, min(xmin, width)) xmax max(0, min(xmax, width)) ymin max(0, min(ymin, height)) ymax max(0, min(ymax, height)) # 过滤掉面积过小的框小于 5 像素的框通常为噪声标注 if (xmax - xmin) 5 or (ymax - ymin) 5: continue x_center ((xmin xmax) / 2.0) / width y_center ((ymin ymax) / 2.0) / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height yolo_lines.append(f{CLASS_MAPPING[name]} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) if not yolo_lines: return None output_file os.path.join(output_dir, Path(image_name).stem .txt) with open(output_file, w, encodingutf-8) as f: f.write(\n.join(yolo_lines) \n) return image_name, width, height def split_dataset(image_paths, ratio(0.7, 0.2, 0.1), seed42): 按比例划分数据集固定随机种子保证每次运行结果一致 random.seed(seed) shuffled image_paths[:] random.shuffle(shuffled) n len(shuffled) n_train int(n * ratio[0]) n_val int(n * ratio[1]) train_set shuffled[:n_train] val_set shuffled[n_train : n_train n_val] test_set shuffled[n_train n_val :] return train_set, val_set, test_set def build_dataset(xml_root, output_root): 主流程遍历 XML - 转换标签 - 划分数据集 - 复制图片到对应目录 xml_files glob.glob(os.path.join(xml_root, *.xml)) if not xml_files: raise FileNotFoundError(f在 {xml_root} 下未找到任何 XML 文件) # 创建目标目录结构 for split in [train, val, test]: os.makedirs(os.path.join(output_root, images, split), exist_okTrue) os.makedirs(os.path.join(output_root, labels, split), exist_okTrue) converted [] for xml_file in xml_files: result convert_voc_to_yolo(xml_file, os.path.join(output_root, labels, tmp)) if result: converted.append((result, xml_file)) # 用所有成功转换的图片做 7:2:1 划分 image_keys [item[0] for item in converted] train_set, val_set, test_set split_dataset(image_keys) # 根据划分结果写图片和标签 for (image_name, width, height), xml_file in converted: if image_name in train_set: split train elif image_name in val_set: split val else: split test # 这里需要自行指定图片所在目录或从 XML 同级目录查找同名文件 src_img os.path.join(os.path.dirname(xml_file), image_name) dst_img os.path.join(output_root, images, split, image_name) if os.path.exists(src_img): os.system(fcp {src_img} {dst_img}) # 移动标签文件到对应划分目录 for split in [train, val, test]: label_tmp os.path.join(output_root, labels, tmp) label_dst os.path.join(output_root, labels, split) for img_name in os.listdir(os.path.join(output_root, images, split)): stem Path(img_name).stem txt_src os.path.join(label_tmp, stem .txt) if os.path.exists(txt_src): os.rename(txt_src, os.path.join(label_dst, stem .txt)) print(f转换完成训练集 {len(train_set)} 张验证集 {len(val_set)} 张测试集 {len(test_set)} 张) if __name__ __main__: build_dataset(./annotations, ./yolo_dataset)代码里有几个点值得展开。CLASS_MAPPING用了字典序排列这样新增类别时不会打乱已有 ID如果后面接着训练大模型或做增量学习这个顺序稳定性很关键。difficult 1时直接跳过这是我喜欢保留的策略——田间数据里被遮挡的害虫很难学硬让模型拟合反而是给 loss 注入噪声。坐标裁剪那句max(0, min(xmin, width))防的是标注工具手滑导致框越界这类脏数据在 VOC 数据集里比想象中常见。3.3 转换后的验证手段转完不能直接开训先用一段简单脚本验证标签数据是否合法# 统计所有 txt 文件的类别和数量 find labels -name *.txt | xargs cat | awk {print $1} | sort | uniq -c | sort -rnawk取第一列类别 IDuniq -c统计每个类别的标注框总数。如果发现某个类别的数量是 0大概率是 XML 里的name跟CLASS_MAPPING的 key 不一致。再去查一下 XML 里真实的类别拼写grep -rh name annotations/ | sort | uniq -c这一步虽然简单但能把标签拼写错误、多余空格这类问题在训练前一网打尽。实际项目中我最常碰到的坑就是这里——手动改过 label某个类名带了尾随空格结果该类别在所有训练轮次里都是空拉。4. YOLOv8 训练配置与易混类调参策略4.1 先做劣化再调参正式训练前建议把数据集按 6:2:2 用yolo train内部划分跑一次快速验证epochs30用预训练权重拿到基线指标。这里不追求 mAP目的是确认数据链路是通的——损失函数在下降、验证集指标有波动、每个 batch 里能看到图像。链路不通的话后面所有调参都是白搭。# data.yaml path: ./yolo_dataset train: images/train val: images/val test: images/test nc: 6 names: 0: BROWN-PLANTHOPPER 1: GREEN-LEAFHOPPER 2: RICE-BUGS 3: RICE-LEAF-FOLDER 4: STEM-BORER 5: WHORL-MAGGOT训练命令yolo detect train \ modelyolov8s.pt \ datadata.yaml \ epochs120 \ imgsz640 \ patience20 \ batch16 \ lr00.005 \ mosaic1.0 \ close_mosaic10 \ project./runs/pest_detection参数说明imgsz640是速度和精度的折中如果田间图像里褐飞虱普遍在 20 像素以下建议改成 960 甚至 1280代价是显存和训练时间翻倍。mosaic1.0在数据集只有 5229 张图时是必要的它能把四张图拼成一张变相增加小目标出现的频率但最后 10 个 epoch 必须关闭close_mosaic10否则模型适应不了真实分布的图像。patience20控制早停害虫检测任务通常不会在 120 轮内完全收敛但验证集 mAP 连续 20 轮不涨就该停了防止过拟合。4.2 易混类褐飞虱 vs 绿叶蝉的针对性优化褐飞虱和绿叶蝉的类间差异主要在触角形态和体色深浅低分辨率下人眼都容易搞混模型更容易在特征图上把两者混为一谈。两个手段一是增加这两种小虫的采样权重二是用 Copy-Paste 增强把小虫贴到背景里新的位置。采样权重可以通过重写 data.yaml 配合loss_scale实现但更省事的做法是先看基线数据的类别分布from collections import Counter import glob counts Counter() for txt in glob.glob(labels/train/*.txt): with open(txt) as f: lines f.read().strip().splitlines() for line in lines: counts[int(line.split()[0])] 1 total sum(counts.values()) for cls_id, cnt in sorted(counts.items()): print(fclass {cls_id}: {cnt} boxes, {cnt/total*100:.2f}%)如果褐飞虱的框数量是稻蝽的 5 倍以上就要考虑训练时的类别权重。YOLOv8 没有直接暴露类别权重参数常见做法是用class weights做后处理在model.predict时传入conf0.1而非默认的0.25再对置信度低于 0.3 但类别为褐飞虱的框做二次确认——因为褐飞虱本来就小模型对其置信度天然偏低。我一般在推理阶段用两档阈值yolo detect predict \ modelruns/pest_detection/weights/best.pt \ source./test_images \ conf0.1 \ iou0.5 \ max_det300iou0.5对应标准 mAP50 的判定条件预测阶段放宽到 0.5 是为了在密集虫害场景下保留更多候选框如果发现漏检多把conf降到 0.05 再看代价是误检增加后面用 NMS 或类别过滤兜底。对这类小目标密集场景我更建议开tile切图推理yolo detect predict modelbest.pt source./field_imgs/ imgsz1280 conf0.1 tile640 tile_overlap0.2这里tile640把 1280 的原图切成四块分辨率不变的子图等效于不放大原图的情况下让模型看到更多细节。tile_overlap0.2控制切块重叠比例避免害虫正好被切缝截断。这个参数组合比单纯调imgsz1280省显存效果接近。4.3 超参调整参考表参数默认值推荐值调整原因imgsz640960 或 1280小目标占比高需要更多像素支撑lr00.010.005数据量偏小学习率过大会导致类别 loss 震荡mosaic1.01.0增强小目标多样性close_mosaic010最后让模型适应真实分布的图像fliplr0.50.0害虫图像左右翻转不影响语义但保留原方向有助于稳定学习scale0.50.3缩小缩放范围避免小目标被过度压缩hsv_h/hsv_s/hsv_v0.015/0.7/0.40.01/0.5/0.3颜色抖动太强会破坏褐飞虱与叶蝉的体色区分依据batch16尽量大类别不均衡时大 batch 能稳定梯度方向表格里的fliplr0.0值得一提。害虫检测的左右对称性确实存在但褐飞虱和绿叶蝉的触角弯曲方向在某些角度下是不对称的翻转增强可能在无意中混淆这些特征。我在这类昆虫数据集上都会关掉水平翻转用scale0.3限制缩放来弥补数据多样性。5. 迈向实用清洗、重训与 k-fold 验证5.1 挖掘标注噪声的两种方法训练到中后期模型对每张图的类别预测置信度分布会呈现出规律如果某个标注框的类别被模型以 0.9 以上的置信度判成另一个类别而且多轮训练都是同样的结果那大概率是原标注错了而不是模型错了。把训练集里所有被预测成其他类且置信度大于 0.85 的样本捞出来用labelImg过一遍能手动修正不少历史标注错误。另一个方法是把训练好的模型放到测试集上做 hard example 挖掘找出 detector 的 false negative 集中出现的图片框数量多但召回率低这些图往往有严重遮挡或目标过小的极端情况单独补充数据比整体扩增效率高得多。5.2 用 k-fold 而不是固定划分验证泛化性5229 张图做固定 7:2:1 划分的隐患是随机性可能导致某一类害虫在验证集上的分布与训练集差异过大。对于小数据集建议直接用 5-fold 交叉验证替代固定划分。第一次跑 fold 时把数据按图像级而不是 XML 级划分防止同源图像串到多个 fold 里。YOLOv8 官方提供了yolo detect train接受多个验证集的入口但更实际的做法是写一个 10 行左右的 Python 循环每次生成不同的 data.yaml 然后调用yolo模块from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(kf.split(image_paths)): write_data_yaml(fold, train_idx, val_idx) os.system(fyolo detect train modelyolov8s.pt datafold_{fold}.yaml epochs60 imgsz960)每一轮的best.pt对应当前 fold 的验证集表现5 轮结束后取每个类别的 mAP 均值。这个均值比单次划分的结果更可信从里面能直观看出哪个类别在不同 fold 间波动最大——波动大的类别就是数据最缺的类别后续做数据扩充时优先补它。如果时间紧张至少跑 3-fold别用单次随机划分的 mAP 作为结论依据。5.3 推理链路的最终形态模型训练稳定后实际部署时建议把推理封装成一个带 MQTT 上报的函数摄像头每 10 秒抓一帧模型输出所有检测框按类别聚合统计——褐飞虱框数量超过 30 就触发预警推送。后端用 FastAPI 包一层把predict的streamTrue打开支持图片流式上送。这里要特别注意部署时输入图像的压缩方式RGB 顺序和训练一致、不要加额外锐化滤镜预处理不同会让精度掉 3~5 个百分点。模型权重用best.pt转为 ONNX 后推理单张 1280 分辨率的推理耗时能控制在 8~12 毫秒足够支撑田间实时监测的需求。本文还有配套的精品资源点击获取
