简介:面向目标检测与农业害虫识别场景,此数据集提供10类水稻害虫的VOC格式标注,包含训练集与验证集,并附类别json字典和可视化脚本,可直接用于模型训练与评估。压缩包共2000个文件,以XML标注文件为主(1999个),另有一个无需修改即可运行的Python可视化脚本,整体大小约89.78MB。已有326人学习下载。图像为300×300分辨率RGB图片,边界框完整且每张含多个目标,部分样本经四图融合增强;目录按train/val划分,各含images与labels子文件夹,json字典便于类别映射,可视化脚本能随机读取图片绘制边界框并保存,整体结构清晰,可接入YOLO等主流框架,有效降低水稻害虫检测的数据准备成本。
1. 10类水稻害虫检测数据集:VOC格式看着简单,真正决定模型上限的是标注审计
从网上下过目标检测数据集的人基本都有同一种经历:解压出来目录齐全,一训练却发现mAP是零。标签能打开,但类别id对不上;可视化脚本能跑,一半框却画在背景上。这类问题在水稻害虫检测上尤其致命——害虫目标小、密度高、同类姿态差异大,标注歪上几个像素,模型学到的就是噪声。标题里这套10类别VOC数据集(训练集+验证集+类别json+可视化脚本)看起来常规,却是把数据从“能下载”变成“能训练”的完整链条。这篇文章就按这条链条拆开讲:VOC格式怎么读、json怎么对齐、可视化怎么审计、转YOLO训练时最常踩的坑在哪里。适合正在做农业植保检测的工程师,也适合第一次训练自己数据集的新手。
2. VOC标注格式与数据集构成:先读懂目录和xml,再谈模型
2.1 VOC标注格式为什么仍是农业数据集的主流选择
农业植保场景的检测数据大多是小团队采集、分批标注,很难一步到位做成大规模COCO格式。VOC格式至今被广泛使用,是因为它足够“透明”:每张jpg对应一个同名xml文件,xml里用纯文本记录了图片尺寸、目标类别和像素坐标,任何语言都能解析,而且肉眼可读。相比之下,YOLO的txt标注里只有归一化浮点数,一旦坐标出错,靠肉眼几乎无法定位问题。
另一个原因是标注工具兼容性。LabelImg、labelme、roboflow导出都支持VOC,田间作业人员转标注任务时,培训成本比COCO的json低得多。xml里还保留了truncated和difficult这类字段,虽然多数检测训练不会直接用,但它们能在数据清洗阶段帮我们筛掉“截断严重”和“难以辨认”的样本,这一点在害虫数据集里非常实用。
需要明确一点:VOC格式本身不包含类别清单。xml里object的name字段既是类别名也是唯一标识,同一个类在不同xml里写错大小写或多了空格,会被当成两个类。这就是标题里专门带上“类别json文件”的原因——json在这里起的是锁死类别字典的作用,而不是给模型提供额外特征。先把这一点想清楚,后面转YOLO格式时才不会乱。
2.2 10类水稻害虫的类别体系与边界划分
标题没有给出完整的10类清单,但水稻害虫检测数据集按常见做法会覆盖这几类:二化螟、三化螟、稻飞虱、稻纵卷叶螟、稻水象甲、稻蝗、稻蓟马、稻瘿蚊、稻秆蝇、稻负泥虫。这10类覆盖了螟虫类、刺吸类、食叶类和钻蛀类主要危害类型,是植保站和农药企业做虫情监测时最关心的对象。
类别体系设计上,最大的坑是近似种混淆。二化螟和三化螟成虫外观接近,稻飞虱和叶蝉在低龄若虫阶段几乎无法区分,稻蓟马更是只有一毫米级大小。如果标注人员没有植保背景,很容易把同一张图里的不同虫态标成两个类。做法上我会在标注规范里强制要求:以成虫和典型若虫为标注对象,虫态不明的不标;同一目标同时存在多个虫态时,以数量最多的虫态为准。类别json的排序也要固定,比如按危害部位分组排列,而不是字母排序,否则转换脚本读出来的类别顺序容易和txt标注错位。
2.3 训练集与验证集的目录组织方式
标题明确包含训练集和验证集。拿到手后我一般会先按下面结构重新组织一遍,让后续脚本无脑可用:
rice_pest/ ├── annotations/ │ ├── train/ │ │ ├── rice_001.xml │ │ ├── rice_002.xml │ │ └── ... │ └── val/ │ ├── rice_101.xml │ └── ... ├── images/ │ ├── train/ │ │ ├── rice_001.jpg │ │ └── ... │ └── val/ │ ├── rice_101.jpg │ └── ... ├── classes.json ├── visualize.py └── README.md这种“annotation与image按同名拆分”的布局是VOC数据集的常见形态。训练集和验证集的比例在害虫这类样本不均衡的任务里,我建议控制在8:2到9:1之间,不要为了验证集好看而切走太多数据。更重要的是按“田块/拍摄session”划分,而不是简单随机切分——同一个田块连续拍摄的帧高度相似,随机拆分会造成验证集“虚高”,这点后面避坑章节还会细讲。
目录对齐是从数据集到训练的第一道关。xml里的filename字段经常和真实图片名不一致:有的带路径前缀,有的扩展名大小写混乱,有的是jpg写成了jpeg。因此一切脚本都不该信任xml里的filename,而要自己拿着图片目录建一个stem索引表。这个习惯能帮你在处理任何VOC数据集时少踩一半的坑。
3. classes.json与可视化脚本:训练前先做一次标注质量审计
3.1 读懂classes.json的两种常见设计
标题里的“类别json文件”有固定作用,但json内部结构并不统一。常见做法有两种:第一种是字典映射形式,形如{"二化螟": 0, "稻飞虱": 1, ...};第二种是列表形式,形如[{"id": 0, "name": "二化螟"}, {"id": 1, "name": "稻飞虱"}]。前者简洁,后者保留了扩展信息,更像COCO风格。
问题在于,很多数据集作者只给其中一种,而可视化脚本默认读另一种,于是“json键名对不上”“读不到name字段”这类报错成了高频翻车现场。我写脚本时习惯写一个兼容读取函数,不管数据结构长什么样都能拿到类别名字典:
import json from pathlib import Path def load_class_map(json_path: str) -> dict: """读取类别json,统一返回 {name: id} 映射。""" with open(json_path, "r", encoding="utf-8-sig") as f: data = json.load(f) if isinstance(data, dict): # 形式一: {"二化螟": 0, "稻飞虱": 1} return {k: int(v) for k, v in data.items()} if isinstance(data, list): # 形式二: [{"id": 0, "name": "二化螟"}, ...] return {item["name"]: int(item["id"]) for item in data} raise ValueError(f"不支持的json结构: {type(data)}")这段代码做了两件事:用utf-8-sig打开文件,兼容带BOM的Windows记事本保存格式;用类型判断处理dict和list两种常见结构。name到id的映射是后续一切转换的唯一数据源,不要在训练脚本里再写一份手打类别表。
参数说明:json_path指向classes.json;返回值里映射关系必须与xml里的<name>标签一致。如果json里是"brown_planthopper"这类英文名,而xml里写的是"稻飞虱",需要先统一命名,否则转换后类别错位。
3.2 用可视化脚本把标注框画出来,肉眼审核
可视化脚本是这套数据集里最容易被人忽略的价值点。很多人拿到数据集直接转格式训练,等模型跑完才对着混淆矩阵猜数据问题。正确顺序是:训练前先把标注画在原图上,肉眼扫一遍。一个能用的可视化脚本通常长这样:
import argparse import json import xml.etree.ElementTree as ET from pathlib import Path import cv2 def parse_xml(xml_path: Path, class_map: dict): root = ET.parse(str(xml_path)).getroot() boxes = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in class_map: print(f"警告: {xml_path.name} 含未知类别 {name}") continue bnd = obj.find("bndbox") xmin = int(float(bnd.findtext("xmin"))) ymin = int(float(bnd.findtext("ymin"))) xmax = int(float(bnd.findtext("xmax"))) ymax = int(float(bnd.findtext("ymax"))) boxes.append((name, xmin, ymin, xmax, ymax)) return boxes def main(): ap = argparse.ArgumentParser() ap.add_argument("--img_dir", required=True, help="图片目录") ap.add_argument("--xml_dir", required=True, help="XML目录") ap.add_argument("--class_json", required=True, help="类别json路径") ap.add_argument("--out_dir", default="visual_check", help="输出目录") ap.add_argument("--show", action="store_true", help="逐张弹窗预览") args = ap.parse_args() class_map = json.load(open(args.class_json, encoding="utf-8-sig")) out_dir = Path(args.out_dir) out_dir.mkdir(parents=True, exist_ok=True) img_files = [p for p in Path(args.img_dir).glob("*.jpg")] img_files += [p for p in Path(args.img_dir).glob("*.jpeg")] img_files += [p for p in Path(args.img_dir).glob("*.png")] for img_path in img_files: xml_path = Path(args.xml_dir) / (img_path.stem + ".xml") if not xml_path.exists(): print(f"跳过: {img_path.name} 缺少xml") continue boxes = parse_xml(xml_path, class_map) img = cv2.imread(str(img_path)) for name, xmin, ymin, xmax, ymax in boxes: 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.7, (0, 0, 255), 2) out_path = out_dir / img_path.name cv2.imwrite(str(out_path), img) if args.show: cv2.imshow("check", img) cv2.waitKey(0) if __name__ == "__main__": main()这段脚本按图片stem找xml,不信任xml内的filename字段;对未知类别只警告不崩溃,方便一次跑完整个目录。运行方式:
python visualize.py --img_dir rice_pest/images/train --xml_dir rice_pest/annotations/train --class_json rice_pest/classes.json --out_dir check_output参数说明:--show开启弹窗预览,适合抽查;--out_dir批量输出带框图片,适合快速浏览。害虫数据集中小目标多,画框后一定要放大看,建议把输出图按“每张图的目标数”排序查看,目标数越多的图越容易暴露标注问题。
3.3 可视化之后重点审计的四类问题
第一,框与目标边缘是否贴合。害虫身体细长,很多标注框为了省事把虫体连同周边茎叶一起框进去,IoU只有0.5左右,这会直接拉低模型定位精度。
第二,类别是否错标。二化螟蛾子颜色偏灰褐,三化螟偏黄白,屏幕上一眼能分辨,但标注员连续工作两小时后很容易疲劳错标,可视化能把这类错误成批找出来。
第三,小目标是否有遗漏。稻蓟马、稻飞虱若虫只有几十个像素,漏标率最高。可视化时重点看那些“有大框无小框”的密集区域。
第四,是否存在重叠框。同一目标被标了两遍,常见于多人协作标注。重叠框会让模型在训练时收到互相矛盾的梯度。
这四类问题靠肉眼扫一遍可视化图就能发现,比任何自动化检查都直接。
4. 把VOC转成YOLO格式:转换脚本、data.yaml与训练参数
4.1 为什么yolov8训练前必须转换格式
yolov8和yolov5原生的训练管线默认读取Ultralytics格式:每个图片对应一个同名txt文件,txt里每行是“类别id 归一化中心x 归一化中心y 归一化宽 归一化高”。坐标全部是0~1的浮点数,不保留像素坐标和difficult标记。这意味着VOC的xml必须经过一次转换,否则数据进不了模型。
网上现成的xml转txt脚本很多,但大部分没处理三个关键问题:目标框坐标超出图像边界、xml里存在没有bndbox的残缺object、类别名称与json不一致。这些问题在害虫数据里很常见,因为田间照片可能经过裁剪或拼接,标注工具又偶尔会写出xmax比图片宽还大的非法框。所以我建议自己维护转换脚本,把输入检查做在源头。
4.2 一个带边界保护的XML转TXT脚本
下面这个脚本针对水稻害虫数据做了几个加固,直接按“annotations目录→labels目录”批量转换:
import argparse import json import xml.etree.ElementTree as ET from pathlib import Path def convert_one(xml_path: Path, class_map: dict, out_txt: Path, img_width: int, img_height: int): """转换单个xml为YOLO txt,坐标越界时按边界裁剪。""" root = ET.parse(str(xml_path)).getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in class_map: print(f"跳过未知类别 {name} 在 {xml_path.name}") continue bnd = obj.find("bndbox") if bnd is None: continue xmin = float(bnd.findtext("xmin")) ymin = float(bnd.findtext("ymin")) xmax = float(bnd.findtext("xmax")) ymax = float(bnd.findtext("ymax")) # 边界保护:防止标注框超出图片尺寸 xmin = max(0.0, min(xmin, img_width - 1)) xmax = max(0.0, min(xmax, img_width - 1)) ymin = max(0.0, min(ymin, img_height - 1)) ymax = max(0.0, min(ymax, img_height - 1)) if xmax <= xmin or ymax <= ymin: print(f"忽略非法框 {name} 在 {xml_path.name}") continue # 转归一化中心坐标 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height # 防止浮点误差导致坐标略超1.0 x_center = min(x_center, 1.0) y_center = min(y_center, 1.0) cls_id = class_map[name] lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_txt.parent.mkdir(parents=True, exist_ok=True) out_txt.write_text("\n".join(lines), encoding="utf-8") def main(): ap = argparse.ArgumentParser(description="VOC XML 转 YOLO TXT") ap.add_argument("--xml_dir", required=True) ap.add_argument("--label_dir", required=True) ap.add_argument("--img_dir", required=True) ap.add_argument("--class_json", required=True) args = ap.parse_args() class_map = load_class_map(args.class_json) img_dir = Path(args.img_dir) xml_dir = Path(args.xml_dir) label_dir = Path(args.label_dir) for xml_path in xml_dir.glob("*.xml"): # 从xml同级或图片目录读取尺寸 img_path = img_dir / (xml_path.stem + ".jpg") if not img_path.exists(): img_path = img_dir / (xml_path.stem + ".jpeg") if not img_path.exists(): img_path = img_dir / (xml_path.stem + ".png") if not img_path.exists(): print(f"跳过 {xml_path.name}: 找不到图片") continue import cv2 h, w = cv2.imread(str(img_path)).shape[:2] out_txt = label_dir / (xml_path.stem + ".txt") convert_one(xml_path, class_map, out_txt, w, h) if __name__ == "__main__": main()这段脚本的核心设计是:坐标裁剪加非法框检查,避免yolo训练时xywh出现负数或超界;用图片实际尺寸做归一化分母,而不是信任xml里的<size>节点——实测中xml的width和height经常和真实图片尺寸不一致,尤其当图片被标注工具压缩过。运行方式:
python voc2yolo.py --xml_dir rice_pest/annotations/train --label_dir rice_pest/labels/train --img_dir rice_pest/images/train --class_json rice_pest/classes.json转换完建议随手抽查几个txt,确认每行都是“整数 四个小数”的结构。常见问题是有些转换脚本把类别id从1开始编号,导致第一类永远学习不到,训练出来的模型漏检率奇高。
4.3 data.yaml的写法与关键参数
转换完标注,还要写一份data.yaml告诉yolo框架数据在哪、有几类。下面这份配置以yolov8为参照,同样适用于yolov5:
# rice_pest.yaml path: ./rice_pest # 数据集根目录,相对路径或绝对路径 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 10 # 类别数,必须与classes.json一致 names: # 顺序必须与classes.json的id一致 0: 二化螟 1: 三化螟 2: 稻飞虱 3: 稻纵卷叶螟 4: 稻水象甲 5: 稻蝗 6: 稻蓟马 7: 稻瘿蚊 8: 稻秆蝇 9: 稻负泥虫这里最容易出错的是names顺序。yolov8的txt标注里存的是类别id,data.yaml的names列表按下标对应类别名。假设classes.json里"二化螟"对应0,那names的第0项必须也是二化螟,两者任何一边不一致都会让训练在完全错误的目标上优化。
训练命令可以直接用yolov8的cli工具:
yolo detect train data=rice_pest.yaml model=yolov8s.pt epochs=120 imgsz=640 batch=16 lr0=0.01 workers=8参数建议按这个基准调整:epochs不要低于100,害虫类内差异大,训练不足容易欠拟合;imgsz=640是通用默认值,如果图里稻蓟马这类小目标占比高,可升到1280,但显存占用和训练时间会翻倍;batch按显存调,12G显存跑yolov8s建议8~16;workers在Windows上调小到2,否则dataloader容易报错。
训练结束后,用验证集评估:
yolo detect val data=rice_pest.yaml model=runs/detect/train/weights/best.pt验证命令会输出每个类别的precision、recall和mAP。先看每个类别的AP再调参数,不要只看总mAP。
5. 避坑与排查:VOC数据集训练最常见的5个翻车现场
5.1 现象:训练loss不降,验证mAP@0.5恒为0
原因分析:转换脚本里类别id和xml里的name对不上,比如按字母序排序生成了id,而classes.json按害虫危害部位分组排序,两类顺序完全不同。模型从头到尾都在用一个错误映射,学不到有效信息。
解决办法:强制以classes.json为唯一数据源,转换脚本不要自己做排序。在convert_one函数里加一行断言,检查class_map里的name集合与xml里出现的name集合是否一致,不一致立即报错并打印缺失项。这一步能拦住90%的类别错位问题。
5.2 现象:可视化脚本报“找不到文件”,训练时提示Dataset is empty
原因分析:xml里的filename和实际图片文件名不匹配。有的标注工具写的是带前缀的路径,有的把jpg写成了jpeg,还有的图片名是1.jpg而xml文件名是1_1.xml。可视化脚本和转换脚本都按stem匹配时,就会扑空。
解决办法:不要依赖filename字段,统一用Path(img_path).stem去xml目录里找同名xml。如果图片名和xml名整体对不上,说明数据集在打包时已经损坏,这时只能按xml的filename去重新映射图片,或者用图片内容哈希做匹配,没有捷径。
5.3 现象:json.load报编码错误,或类别名在可视化图上显示乱码
原因分析:数据集作者在Windows下用记事本保存json,默认编码是GBK,或者带BOM;另一些json里混入了中文引号。python的json.load默认按utf-8解析,遇到GBK直接抛错。
解决办法:读json统一用encoding="utf-8-sig",这能同时兼容带BOM和不带BOM的utf-8。如果还报错,用chardet探测文件编码,再把文件转成utf-8重存。另外脚本入口要加# -*- coding: utf-8 -*-,终端也切到utf-8环境,避免中文类别名在Linux和Windows两个平台间来回踩坑。
5.4 现象:某张图有目标但txt文件为空,训练时该图被当成背景
原因分析:xml里object节点缺失bndbox子节点,或所有框经过边界裁剪后变成非法框被脚本过滤掉了。这种情况常见于标注过程中误删框体坐标,只留了类别名。
解决办法:转换脚本里遇到“有object但无bndbox”的情况不要静默跳过,要单独输出到errored.txt清单。训练前检查清单,把这类xml从标注目录移出,或者人工补齐坐标。空txt本身没问题,yolov8会把空txt当负样本,但如果这张图本该有目标,模型就会学到“这里没虫”的错误信号。
5.5 现象:验证集mAP虚高,实田间测试一塌糊涂
原因分析:训练集和验证集出现重叠。最常见的是按文件名随机拆分,而同一田块连续拍摄的几十帧画面高度相似,被拆到了不同集合里,验证时模型相当于“看过答案再考试”。另一个来源是augmented图片没隔离,离线增强出来的图片被同时放进了train和val。
解决办法:写一个检查脚本,比对训练集和验证集的文件stem列表,找出同时出现在两边的图片名。更合理的划分方式是按“拍摄时间+田块编号”分组,一组整体进train或整体进val,不做逐张随机切分。数据集目录里如果带了采集日志,优先按日志字段划分。
6. 更进一步:数据增强、轻量模型选型与田间验收标准
6.1 离线增强怎么做效果最好
水稻害虫检测的难点集中在目标小、背景杂、光照多变。在线增强里mosaic和随机HSV抖动是默认项,此外我习惯加两项种子级增强:随机亮度扰动和随机遮挡模拟。亮度扰动要同时调高调低,模拟早晚田间光照差异;遮挡模拟可以用随机矩形块遮掉部分虫体,强迫模型学习局部特征。离线增强不要无脑复制,二化螟这类样本少时,把原始图做水平翻转、旋转30度、加高斯噪声各扩一份就够了。
6.2 模型选型建议
这类数据集我推荐从yolov8n或yolov8s起步,不要一上来跑yolov8x。稻蓟马和稻飞虱若虫目标小,但并不是模型越大效果越好,小模型配合1280输入分辨率往往比大模型配640更实际。如果要做田间边缘端部署,训练完把模型转成ONNX再量化成FP16,体积能压到十几MB,推理耗时在Jetson上可以到实时水平。
6.3 验收标准别只看总mAP
最后要有验收底线:至少让验证集mAP@0.5达到0.85以上,且逐类AP里没有低于0.6的类别。我最早做水稻害虫检测时,总mAP接近0.9,但稻蓟马那一类AP只有0.2,装在田间的设备对小型害虫几乎完全漏检。后来把可视化脚本输出的图片逐张重查,发现是标注框普遍偏大,目标只占框面积的六成,模型被背景噪声带偏了。重标那一类后,AP直接翻倍。数据干净这件事,永远比模型结构更值得投入时间。这套做法是我踩了多次坑之后沉淀下来的,希望帮到你。
本文还有配套的精品资源,点击获取