IP102病虫害数据集VOC转YOLO:从XML标注到YOLOv8训练实战
2026/9/11 23:02:41 网站建设 项目流程

简介:IP102数据集Pascal VOC格式XML标注包,面向农作物病虫害目标检测与识别,适合算法工程师、科研人员及农业AI开发者使用。资源基于IP102扩展,覆盖水稻、玉米、小麦、柑橘等8种作物,可标注稻纵卷叶螟、二化螟、蚜虫、玉米螟等数十种害虫,适用于YOLO、Faster R-CNN等检测模型训练。压缩包内含2000个XML文件,总大小约493.45MB,每个XML对应一张图像中害虫的边界框与类别信息,标签规范,可直接转换为COCO等格式。目前已有449人学习下载。这份标注数据能显著降低数据准备门槛,帮助研究者在农业植保场景中快速开展模型训练与验证,为虫害智能监测及精准施药提供高质量数据支撑。

1. 为什么说原版IP102不适合直接训练检测模型,这套VOC扩展集值得从头看一遍

IP102是农业视觉里绕不开的基准数据集,75,000多幅图像覆盖水稻、玉米、小麦、甜菜等8种作物,类别跨度从稻纵卷叶螟到苜蓿广肩小蜂,做病虫害分类的论文几乎都在上面跑过。但真拿它做目标检测落地时,你会发现原版IP102最初按图像分类设计,很多类别只在文件名里区分,没有统一的目标级标注。想迁移到Faster R-CNN或YOLO做bbox预测,得先从分类标签反推目标位置、清洗重复图、统一命名,这几步往往要占掉两周时间。这套扩展集恰好补上了这个断层:18,975张原图,全部按Pascal VOC XML格式标注,覆盖稻飞虱、玉米螟、小麦红蜘蛛等80多个细分类别。对做农业植保检测的工程师来说,它省掉的是最脏的数据整理环节,让你把精力直接放在模型训练和漏检优化上。

2. 拆开一个Pascal VOC XML:标注结构与数据卫生检查

2.1 从文件名推断数据血缘,先建一个标注清单

数据集里单张样本的命名规则是IP078000377_jpg.rf.4677a8e3c14e2c092f313c1a776ed95c.xmlrf后缀常见于Roboflow等标注平台导出的文件,中间32位十六进制字符串是原始图片的哈希片段。这种命名的好处是多批次图片不会因为重名互相覆盖,坏处是文件名里没有标注工具版本和标注人信息,后续做多轮质检时查不了是谁改的框。

我一般会先扫描一遍全量XML,确认文件完整性和数量,再决定后面的转换策略。

find . -name "*.xml" | wc -l find . -name "*.xml" | head -n 5

这里wc -l统计XML总数量,head -n 5抽查前5个文件,确认没有损坏的空文件。如果数量与18,975张原图对不上,说明标注过程中有丢帧或漏导出的情况,需要先用文件名匹配原图目录,列出缺失清单再决定是否补标。

2.2 annotation标签树里藏着哪些关键字段

PaScaL VOC XML的核心结构是一个<annotation>根节点,下面按层级组织图像信息和每个目标的信息。上面给出的是数据集里常见字段的对照说明。

字段路径类型含义转换时是否必须
<folder>str图像所在目录名否,可忽略
<filename>str图像文件名是,用于与原图关联
<source>str图片来源
<size><width>int图像宽度是,坐标归一化依赖
<size><height>int图像高度是,坐标归一化依赖
<size><depth>int通道数,通常为3
<object><name>str目标类别名称
<object><difficult>int是否难例,0或1建议保留并单独处理
<object><bndbox>4个intxmin, ymin, xmax, ymax

需要注意difficult字段。VOC原版约定difficult=1表示目标模糊难辨认,评测时不计入AP,但很多深度学习框架读标签时会忽略这个字段,直接把框当背景处理,造成漏检假象。我在转YOLO格式前习惯先统计difficult数量,决定是过滤掉还是保留为普通正样本。

2.3 用ElementTree扫全量XML,摸清类别分布

这里是我的扫描脚本,输出每个类别的样本数和框数,顺便统计difficult占比。

import xml.etree.ElementTree as ET from pathlib import Path from collections import defaultdict def scan_voc_labels(xml_dir: str): xml_files = list(Path(xml_dir).glob("*.xml")) class_sample_cnt = defaultdict(int) class_box_cnt = defaultdict(int) difficult_cnt = 0 total_boxes = 0 malformed = [] for xml_path in xml_files: try: tree = ET.parse(xml_path) root = tree.getroot() except ET.ParseError: malformed.append(xml_path.name) continue seen_objects = set() for obj in root.iter("object"): name = obj.findtext("name", "").strip() if not name: continue box = obj.find("bndbox") if box is None: continue if name not in seen_objects: class_sample_cnt[name] += 1 seen_objects.add(name) class_box_cnt[name] += 1 total_boxes += 1 if int(obj.findtext("difficult", "0")) == 1: difficult_cnt += 1 print(f"XML文件总数: {len(xml_files)}") print(f"解析失败: {len(malformed)}") print(f"目标框总数: {total_boxes}, difficult框数: {difficult_cnt}") print("\n类别\t样本数\t框数") for name in sorted(class_box_cnt, key=lambda x: -class_box_cnt[x]): print(f"{name}\t{class_sample_cnt[name]}\t{class_box_cnt[name]}") scan_voc_labels("path/to/xml")

逻辑说明:先用glob拿到全部XML路径,逐个解析。seen_objects用于同一张图里相同类别只算一次样本数,而class_box_cnt统计的是该类别在所有图里的框总量,两者结合可以看出同一张图里密集出现的类别。比如稻飞虱这类目标往往一张图里有几十个框,样本数不高但框数很高,这直接影响后面对小目标推理策略的选择。

difficult的统计很重要,如果占比超过5%,建议在转换时统一过滤或单独建一个难例集做测试,不要混进训练集。

3. VOC转YOLO txt:坐标归一化、类别映射与脏数据清洗

3.1 为什么要把VOC转成YOLO格式再训练

VOC XML里坐标是绝对像素值,比如xmin=120, ymin=340,这种表示对尺寸固定的输入图没问题,但YOLO类模型训练时要对图片做letterbox缩放和随机裁剪,绝对坐标在增强后就不适用了。YOLO需要的标签格式是归一化后的中心点坐标和宽高,即class_id, x_center, y_center, width, height,五个值都缩放到0到1之间。另外VOC的difficult标记在YOLO格式里没有对应字段,必须在转换时显式决定去留。

转换前还要确认一件事:类别字符串到整数id的映射。这个映射表一旦定下来,后续训练和推理都要保持一致,不能中途修改。常用做法是所有类别按首字母排序生成id,这样新增类别时中间id不会变。

3.2 类别映射表与输出目录结构

这里给出一个类别映射示例,实际以你XML文件里出现的name为准。

整数id类别名称
0二化螟
1玉米螟
2稻纵卷叶螟
3稻飞虱
4红蜘蛛
5麦长管蚜
......

转换前要先跑一遍第2章的扫描脚本,把全量类别name收集出来,生成映射表,避免转换到一半遇到没见过的类别名。

3.3 转换脚本与边界情况处理

下面这个脚本完成VOC到YOLO的转换,并做三类清洗:越界框裁剪、超小目标过滤、空标签文件剔除。

import xml.etree.ElementTree as ET from pathlib import Path CLASS_MAP = { "二化螟": 0, "玉米螟": 1, "稻纵卷叶螟": 2, "稻飞虱": 3, "红蜘蛛": 4, "麦长管蚜": 5, } def voc_to_yolo(xml_path: Path, out_dir: Path, min_side: float = 2.0): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") width = int(root.findtext("size/width")) height = int(root.findtext("size/height")) lines = [] for obj in root.iter("object"): name = obj.findtext("name", "").strip() if name not in CLASS_MAP: continue if int(obj.findtext("difficult", "0")) == 1: continue box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) xmin = max(0, min(xmin, width)) ymin = max(0, min(ymin, height)) xmax = max(0, min(xmax, width)) ymax = max(0, min(ymax, height)) if xmax - xmin < min_side or ymax - ymin < min_side: continue if xmax <= xmin or ymax <= ymin: continue x_center = ((xmin + xmax) / 2.0) / width y_center = ((ymin + ymax) / 2.0) / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height lines.append(f"{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") if lines: out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") xml_dir = Path("path/to/xml") out_dir = Path("path/to/labels") out_dir.mkdir(exist_ok=True) for xml_file in xml_dir.glob("*.xml"): voc_to_yolo(xml_file, out_dir)

参数说明里需要注意三点。difficult过滤逻辑直接跳过难例,如果你的模型在难例上漏检严重,可以改成保留并单独设一组难例验证集,而不是和普通样本混在一起。min_side是边长阈值,小于该像素数的框会被丢弃,目的是清掉标注误差导致的1到2像素噪声框,但密集小虫目标本身可能只有8到10像素,这个值设太大会误杀。最后的坐标转换公式里,中心点和宽高都除以图像原始宽高,保证归一化到0到1区间。

3.4 转换后校验:把标签画回原图

格式是否转对,光看数字不够,我的习惯是抽查20到50张转换后的图片,把txt标签叠加画回原图,确认框和虫体位置对齐。

import cv2 def draw_yolo_label(image_path: str, label_path: str): img = cv2.imread(image_path) h, w = img.shape[:2] for line in open(label_path, "r", encoding="utf-8"): parts = line.strip().split() if len(parts) != 5: continue cls_id, x_c, y_c, bw, bh = parts x_c, y_c, bw, bh = map(float, (x_c, y_c, bw, bh)) xmin = int((x_c - bw / 2) * w) ymin = int((y_c - bh / 2) * h) xmax = int((x_c + bw / 2) * w) ymax = int((y_c + bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) return img

这段代码按反向公式把归一化中心坐标还原成像素坐标,再画框。如果发现大量框偏移半个身位,通常是VOC坐标读错或者原图分辨率与XML里size字段不一致,回查XML的<size>节点和实际图片尺寸比一比即可。

4. 分层划分数据集,再用YOLOv8把18,975张图训起来

4.1 为什么不能直接random_split

很多分类任务直接按10%切验证集没问题,但检测任务不行。这个数据集里水稻害虫类别多、框密集,小麦和苜蓿类别少,直接随机切很可能让某些类别在训练集和验证集里分布不均,验证集出现某类只有一个框的情况,mAP波动剧烈。正确做法是按类别框数分层划分,保证每个类别在训练集、验证集里的比例接近总体比例。

4.2 按类别的分层划分脚本

下面脚本以图为单位,按图中占比最高的类别做分层抽样。

import random from pathlib import Path from collections import defaultdict random.seed(42) xml_dir = Path("path/to/xml") train_ratio, val_ratio = 0.85, 0.10 class_to_files = defaultdict(list) for xml_path in xml_dir.glob("*.xml"): tree = ET.parse(xml_path) root = tree.getroot() class_cnt = defaultdict(int) for obj in root.iter("object"): class_cnt[obj.findtext("name")] += 1 major_class = max(class_cnt, key=class_cnt.get) class_to_files[major_class].append(xml_path.stem) train_set, val_set, test_set = [], [], [] for cls, items in class_to_files.items(): random.shuffle(items) n_train = int(len(items) * train_ratio) n_val = int(len(items) * val_ratio) train_set.extend(items[:n_train]) val_set.extend(items[n_train:n_train+n_val]) test_set.extend(items[n_train+n_val:]) print(f"train: {len(train_set)}, val: {len(val_set)}, test: {len(test_set)}")

逻辑说明:脚本先按一张图中出现次数最多的类别作为该图的层标签,再进行分层切分。seed=42固定随机种子,保证可复现。比例上训练集85%、验证集10%、测试集5%,如果你的任务更关注最终泛化,可以把测试集扩大到10%,但训练集不能低于80%,检测模型很吃数据量。

划分结果建议写入三个txt文件,每个文件一行一个图片文件名(不带扩展名),方便后面YOLO训练直接索引。

4.3 data.yaml配置与YOLOv8训练

YOLOv8或YOLOv11的ultralytics接口都支持YOLO txt标注,只需准备一个yaml描述数据集路径和类别列表。

path: /your/dataset/root train: images/train.txt val: images/val.txt test: images/test.txt names: 0: 二化螟 1: 玉米螟 2: 稻纵卷叶螟 3: 稻飞虱 4: 红蜘蛛 5: 麦长管蚜

训练命令:

yolo detect train \ data=ip102_ext.yaml \ model=yolo11s.pt \ epochs=80 \ imgsz=960 \ batch=16 \ lr0=0.005 \ mosaic=0.8 \ cos_lr=True \ project=runs/ip102_ext

参数表格与推荐取值:

参数推荐值说明
imgsz960病虫害小目标多,640输入下帧宽不足,960能明显提升小目标recall
epochs80-120迁移学习下80轮可收敛,观察val loss不降后early stop
batch按显存调16GB显存配960输入建议batch为16,OOM就降到8
lr00.005比默认0.01保守,农业数据噪声大,学习率过大容易过拟合背景
mosaic0.8保留一定真实比例,全mosaic会让模型过度依赖拼接纹理

训练到一半时重点看两个曲线:val/box_lossmetrics/precision。box_loss持续下降但precision抖动不升,说明模型对小目标回归不准,优先调anchor或增大imgsz,而不是加epochs。

4.4 评估阶段怎么看懂mAP

训练结束后运行:

yolo detect val \ model=runs/ip102_ext/weights/best.pt \ data=ip102_ext.yaml \ imgsz=960 \ conf=0.25

输出里mAP50表示IoU阈值0.5下的平均精度,mAP50-95是0.5到0.95的平均。农业检测场景建议以mAP50为主要指标,因为虫害检测的最终目的是发现并防治,对框的像素级定位精度的要求低于自动驾驶等领域。如果mAP50和mAP50-95差距过大,说明框定位偏但没漏检,可以先不管;如果两者都低,就要回到类别分布和图片质量找原因。

5. 小目标漏检与类别失衡:mAP50后面的优化顺序

5.1 先用混淆矩阵定位短板

训练结束后,把runs/ip102_ext/confusion_matrix.png调出来看。我通常关注两类问题:一是某些类预测成背景,说明正样本特征没学够,需要增强或补充数据;二是某两类互相混淆,比如稻飞虱和白背飞虱,说明类别间视觉差异太小,单纯加数据效果有限,考虑合并成超类或改用细粒度分类头。多数情况下,这个数据集里漏检最严重的是单张图里几十个框的密集小虫,比如稻飞虱。

5.2 数据端补偿:Copy-Paste增强的适用边界

对少样本类别,YOLO训练配置里可以打开copy_paste参数,它会把某张训练图里的小目标复制粘贴到另一张图的随机位置,变相增加该类别的样本量。

yolo detect train \ model=runs/ip102_ext/weights/best.pt \ data=ip102_ext.yaml \ epochs=40 \ imgsz=960 \ copy_paste=0.5 \ mosaic=0.6

这里要注意,copy_paste对密集小目标很有效,但对大目标容易贴出反逻辑画面,比如把螟虫贴到水稻叶片之外。所以copy_paste=0.5意思是50%的概率应用该增强,留一半真实样本让模型维持对自然分布的认知。

5.3 推理端SAHI切片:不做额外训练也能提升小目标recall

如果训练端优化完,稻飞虱这类小目标漏检还是严重,常见做法是用SAHI做切片推理。SAHI把大图切分成多个重叠小图,分别推理再合并结果,等效于用小输入尺寸跑高分辨率推理。

from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="yolov8", model_path="runs/ip102_ext/weights/best.pt", confidence_threshold=0.3, image_size=960, device="cuda:0", ) result = get_sliced_prediction( "test_images/rice_pest_001.jpg", detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, )

切片尺寸用640,配合原图960训练,窗口能覆盖完整虫体。overlap_ratio=0.2是为了避免目标正好卡在切片边界被切成两半。如果切得太碎导致单窗口里只有半只虫,就调大overlap到0.3。SAHI的代价是推理时间随切片数量线性增长,批量推理时建议先按框数量排序,只对框数超过50的密集图走切片流程,普通图直接全图推理。

5.4 最后的兜底:conf阈值和NMS参数小网格搜索

模型训练到位后,在验证集上做一个简单的conf阈值网格搜索,通常能再把mAP50提升零点几个点。

for conf in 0.15 0.2 0.25 0.3; do yolo detect val \ model=runs/ip102_ext/weights/best.pt \ data=ip102_ext.yaml \ conf=$conf done

比较每个阈值下的mAP50和F1,再结合实际推理场景选。害虫巡检一般更在意recall而不是precision,多检错一个框的成本低于漏检一个虫害爆发点,所以阈值别设太高。这套流程走完后,这个VOC标注的IP102扩展集训练出来的模型,在细粒度小虫目标上的mAP50表现会明显好于拿原版分类数据硬转的模型。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询