
简介面向农作物病虫害检测与分类任务压缩包内提供2000个基于Pascal VOC格式的XML标注文件是IP102数据集的拓展子集。IP102原始数据涵盖水稻、玉米、小麦、甜菜、苜蓿、葡萄、柑橘、芒果等8类作物共75000余幅图像这一拓展聚焦其中与具体虫害识别相关的图像标注可识别稻纵卷叶螟、二化螟、玉米螟、黏虫、蚜虫、红蜘蛛等数十种常见害虫。压缩包共2000个文件全部为XML标注文件总大小约493.45MB每份标注包含图像中害虫目标的类别与边界框位置可直接用于YOLO、Faster R-CNN等基于Pascal VOC格式的训练流程。与原始图像配合即可构建完整训练集省去从零标注的成本XML结构规范便于后续数据清洗与格式转换。目前已有449人学习下载适合从事农业病虫害识别研究的学生、工程师及数据科学从业者使用。1. IP102 数据集VOC XML 标注下的 18975 张昆虫原图究竟能做什么IP102 是一个面向农业害虫识别的大规模图像数据集199 个类别其中常用于目标检测任务的版本截取了 95 类保留了 18975 张原始图片。和 ImageNet 这种通用分类集不同IP102 的标注文件明确使用了 Pascal VOC XML 格式意味着拿到数据后可以直接喂给 YOLO、SSD、Faster R-CNN 这类检测框架不必先做格式转换。这也是它在农业 AI、植保无人机、病虫害预警系统里被频繁引用的原因——标注质量和数据分布直接关系着模型落地的可信度。但真正动手之后会发现几个绕不开的问题XML 里某些字段缺省了怎么办类别编号从 1 开始而训练框架要求从 0 开始害虫类别天然长尾部分类只有几十张图训练时很容易过拟合。这篇文章就把从原始 XML 到可直接训练的 YOLO 数据集这条路完整走一遍顺便把标注解析和格式转换中常见的坑逐一说明。2. IP102 的目录结构与 XML 标注格式解析2.1 拿到数据后的第一件事确认目录和类别清单IP102 的检测版解压后最常见的目录结构是这样IP102_detection/ ├── JPEGImages/ # 18975 张原图 ├── Annotations/ # 18975 个 XML 文件 ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── classes.txt打开classes.txt会发现类别顺序直接决定了后续 YOLO 格式里的class_id。这里有一个常见的误区VOC 格式的 XML 中name字段写的是类别字符串而 YOLO 训练时类别必须是整数索引两者之间的映射必须完全依赖这个 txt 的顺序。如果类名里有空格或者特殊字符比如 locust 和 Locust 这种大小写不一致解析时最好做一个 strip 和去重避免映射表错位。ImageSets/Main/下的三个 txt 文件是官方划分好的训练、验证、测试集。但实际使用时建议重新划分因为官方版本的划分未必符合你的业务场景。比如做迁移学习时你可能希望保证每个类别在训练集里至少有 10 张图这时候就要写脚本按类别去重采样。2.2 XML 标注的核心字段与真实样本IP102 的 XML 遵循 Pascal VOC 2012 的 schema但个别字段可能缺失。一个完整的标注文件长这样annotation folderJPEGImages/folder filename0001.jpg/filename size width1024/width height768/height depth3/depth /size object namerice_leaf_roller/name bndbox xmin220/xmin ymin180/ymin xmax580/xmax ymax420/ymax /bndbox /object /annotation字段含义注意事项folder图片所在目录很多标注工具会按原路径写死迁移时需忽略filename图片文件名注意与 JPEGImages 目录实际文件名的大小写一致性size.width/height图片宽高这是 YOLO 归一化的关键依据优先使用降低 IO 开销object.name类别名必须与 classes.txt 中的名称完全一致bndbox目标框左上/右下坐标像素值必须为整数有些工具会写成浮点数注意一个 XML 里可能有多个object但 IP102 原版中大部分图片只标注了一个主目标多目标的情况集中在小虫群类比如蚜虫。解析时不能用find必须用findall。2.3 Python 解析 XML 的完整代码与防错处理用xml.etree.ElementTree是最稳妥的方案不依赖额外库Python 3.6 都内置。下面这段代码不仅解析标注还会过滤掉空标注和坐标越界的文件import xml.etree.ElementTree as ET import os def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() img_name root.findtext(filename) size root.find(size) width int(size.findtext(width)) height int(size.findtext(height)) objects [] for obj in root.findall(object): name obj.findtext(name).strip() box obj.find(bndbox) xmin int(float(box.findtext(xmin))) ymin int(float(box.findtext(ymin))) xmax int(float(box.findtext(xmax))) ymax int(float(box.findtext(ymax))) # 过滤无效框坐标非正或宽高为 0 if xmax xmin or ymax ymin: continue # 过滤越界框边界留 1 像素余量 if xmin 0 or ymin 0 or xmax width or ymax height: continue objects.append({ name: name, bbox: [xmin, ymin, xmax, ymax] }) return { filename: img_name, width: width, height: height, objects: objects, num_objs: len(objects) }逻辑说明findtext和find的区别在于前者直接取值、后者返回 Element 对象没有子节点时findtext返回None所以用int(float(...))强行转换会报错但这种情况在 IP102 里极少见。int(float())这种双重转换是为了兼容 580.0 这种由标注工具产生的浮点字符串。提示经典的 XML 解析报错大多发生在文件编码上。IP102 的 XML 通常是 UTF-8但 Windows 下用记事本改过就可能变成 UTF-8-BOMET.parse会直接抛ParseError。批量处理时建议统一用open(xml_path, r, encodingutf-8-sig)读入字符串再交给ET.fromstring。2.4 XML 文件打开与编辑的两个实用路径如果你只是想看一眼标注内容不需要写代码两个工具足够浏览器直接打开现代的 Firefox/Edge 对 XML 有基本的树形展示结构一目了然但无法编辑。VS Code XML 插件XML Language Support by Red Hat支持 schema 校验、节点折叠、自动补全。改标注时不推荐手改坐标但改name字段比如合并相似类非常高效。真正进入标注环节时labelImg和 CVAT 是对本数据集最顺手的工具。前者是单机版打开 XML 后可以重新调整框再保存后者是 Web 版支持多人协作和自动标注模型介入。IP102 原图里害虫尺寸小、背景杂如果用 CVAT 对补充图片做人工标注建议把opacity调大并使用track mode做视频帧标注效率会高很多。3. 把 IP102 的 VOC XML 转换成 YOLO 格式一次跑通3.1 为什么要转格式VOC 与 YOLO 的坐标体系差异Pascal VOC 记录的是绝对像素坐标xmin/ymin/xmax/ymax四个整数。YOLO 需要的是归一化后的相对坐标x_center, y_center, w, h且全部是 0~1 的浮点数。直接用绝对坐标训练 YOLOloss 会爆炸因为模型输出的范围是固定的。转换公式x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height这里的width/height必须来自 XML 里的size字段而不是用cv2.imread去读。原因有两点一是 IO 效率18975 张图全部用 PIL 读一遍要额外耗时数十秒到数分钟二是万一原图被缩放或裁剪过但 XML 没有同步更新用图像实际尺寸去归一化不会报错但会悄然产生坐标偏差排查起来极难。3.2 类别映射表的生成规则IP102 的类别编号在不同来源的版本里存在两种起点论文原始定义从 1 开始1~95而 YOLO 要求从 0 开始0~94。所以必须自己拉一张映射表with open(classes.txt, r) as f: classes [line.strip() for line in f if line.strip()] # 构建名称 - 从0开始的索引 class_map {name: idx for idx, name in enumerate(classes)} # 验证打印前5个映射 for i, (name, idx) in enumerate(class_map.items()): if i 5: break print(f{idx}: {name})在写转换脚本之前先打印映射表确认没有空行、重复项、尾部特殊字符。常见错误是 classes.txt 是 Linux 换行符Windows 下读出来每行末尾带\r导致匹配不上。strip()一劳永逸。3.3 转换脚本从 Annotations 目录到 labels 目录下面是一个可以直接落地的完整脚本同时生成images/和labels/两个目录import os import glob import random import shutil import xml.etree.ElementTree as ET from tqdm import tqdm def convert_annotation(xml_path, output_label_path, class_map): tree ET.parse(xml_path) root tree.getroot() width int(root.findtext(size/width)) height int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name not in class_map: print(f[WARN] Unknown class {name} in {xml_path}) continue box obj.find(bndbox) xmin int(float(box.findtext(xmin))) ymin int(float(box.findtext(ymin))) xmax int(float(box.findtext(xmax))) ymax int(float(box.findtext(ymax))) # 归一化并限制在 0~1 之间 x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height w (xmax - xmin) / width h (ymax - ymin) / height # 防止浮点越界极端情况下 xmax width 时 x_center 略大于 1 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) w min(w, 1) h min(h, 1) lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: with open(output_label_path, w) as f: f.write(\n.join(lines) \n) # 路径配置 annotation_dir IP102_detection/Annotations image_dir IP102_detection/JPEGImages output_image_dir yolo_dataset/images output_label_dir yolo_dataset/labels os.makedirs(output_image_dir, exist_okTrue) os.makedirs(output_label_dir, exist_okTrue) xml_files glob.glob(os.path.join(annotation_dir, *.xml)) print(fTotal XML files: {len(xml_files)}) for xml_path in tqdm(xml_files): basename os.path.splitext(os.path.basename(xml_path))[0] # 转换标注 label_path os.path.join(output_label_dir, basename .txt) convert_annotation(xml_path, label_path, class_map) # 复制图片保持文件名一致 img_src os.path.join(image_dir, basename .jpg) if os.path.exists(img_src): shutil.copy(img_src, os.path.join(output_image_dir, basename .jpg)) else: print(f[WARN] Image not found: {img_src})逻辑和参数说明class_map由上一小节的classes.txt生成数据量小直接读入内存。归一化坐标计算时先算中心点再除以宽高和先除再算中心点在浮点结果上几乎一致但中间溢出风险不一样碰到超大图超过 10000px时先算坐标更安全。{:.6f}保留 6 位小数对 4K 图以内的 bbox 足够精确6 位对应约 1e-4 的误差换算成像素不到 0.1px不用再提升精度。复制图片用shutil.copy而非copy2后者会复制元信息导致 md5 不一致在数据版本管理时会造成无意义的差异。3.4 验证集的抽取策略按类别等比例抽样官方的ImageSets/Main/val.txt可以直接用但更好的做法是重新按类别等比例抽样尤其是针对长尾类别。简单随机抽样在 IP102 上会导致部分冷门类在验证集里只有 1~2 张图评价指标的方差极大。推荐每个类别至少保证验证集有 5 张train_files, val_files [], [] val_count_per_class {} image_ids [os.path.splitext(f)[0] for f in os.listdir(annotation_dir)] random.seed(42) random.shuffle(image_ids) for img_id in image_ids: # 从 XML 读取该图的类别列表 tree ET.parse(os.path.join(annotation_dir, img_id .xml)) cls_names list({obj.findtext(name).strip() for obj in tree.findall(object) if obj.findtext(name)}) # 判断该图是否应该进入验证集 priority False for cls in cls_names: if val_count_per_class.get(cls, 0) 5: priority True break if priority: val_files.append(img_id) for cls in cls_names: val_count_per_class[cls] val_count_per_class.get(cls, 0) 1 else: train_files.append(img_id)这套策略保证了冷门类不会从验证集消失代价是热门类进入验证集的比例略大。对于 IP102 这种 95 类的长尾分布我认为这种偏好是合理的——比随机划分更能反映真实检测能力。4. 用 YOLOv8 训练 IP102配置、损失分析与长尾处理4.1 推荐的模型选型和 yaml 配置以 Ultralytics YOLOv8 为例因为它是目前对自定义数据集最省心的检测框架不需要自己写 DataLoader。显存 8GB 以下选yolov8n.yaml16GB 选yolov8s.yaml追求精度且显存充足可选yolov8m.yaml。对 IP102 这种目标小、背景复杂的农业数据yolov8s是性价比最高的起点。先创建一个数据集描述文件ip102.yamlpath: /path/to/yolo_dataset train: images/train val: images/val nc: 95 names: 0: rice_leaf_roller 1: rice_leaf_caterpillar 2: paddy_bug # ... 按 classes.txt 顺序依次列出共95行注意names列表必须完整不能省略。YOLOv8 在训练时会用这个列表生成类别名映射如果数量与nc不一致会直接抛错。顺序必须与转换脚本里的class_map完全一致否则某个类会被静默地训练成另一个类而 mAP 曲线毫无异常。4.2 训练命令与关键超参数说明yolo detect train \ dataip102.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ momentum0.937 \ weight_decay0.0005 \ warmup_epochs3 \ augmentTrue \ mixup0.1 \ copy_paste0.1 \ projectip102_runs \ nameexp1参数推荐值说明imgsz640IP102 原图多为 1024 宽640 是速度和精度的平衡点升到 800 对小目标友好但训练时间增加约 50%batch16~32取决于显存batch 32 以上时 BN 统计更稳定lr00.01从预训练权重起步0.01 是安全范围不要直接上 0.1mixup / copy_paste0.1IP102 长尾严重mixup 加多一点能提升尾部类别的泛化能力但超过 0.2 会让小目标彻底糊掉warmup_epochs3冷启动阶段让优化器平稳进入状态在 18975 张图上按 batch 16 训练 200 epoch单张 RTX 3090 大约需要 8~12 小时。如果只是验证流程先用 5 个 epoch 跑通确认 loss 下降趋势正确再拉长。4.3 长尾类别与类别不均衡的处理IP102 最大问题就是类别极度不均衡。少量类比如稻飞虱、玉米螟占了图片总数的大半而尾部的 30 多个类加起来可能不到 5%。直接训练会出现头部类 mAP 0.7、尾部类 mAP 0.1 的巨大落差。三个务实的处理手段按优先级排列类别权重加权损失。YOLOv8 本身不直接在配置里暴露cls_loss权重但可以通过修改 loss 计算逻辑实现。常见做法是在loss.py的BCEWithLogitsLoss传入pos_weight权重为训练集每类图片数的倒数。过采样尾部类别。训练时把出现频率低于 100 次的图片复制 2~3 份加入数据列表。实现上最简单的方式是在 4.2 数据准备阶段对尾部类的 txt 行做重复写入不影响验证集。使用数据增强中的mosaic和copy_paste将多个小目标拼接到同一张图。YOLOv8 的copy_paste配合实例分割 mask 使用效果最好目标检测模式也能用但要注意拼接后的框重叠问题。提示不要随手用class_weightbalanced。该参数对分类问题有效但对检测任务来说它等价于把所有类的置信度阈值整体压低头部类会大量误检整体 mAP 反而下跌。IP102 这种小目标多的数据集误检比漏检的负面影响更大。4.4 小目标检测的特殊策略IP102 中很多害虫在 640×640 尺度下只有 20×20 像素甚至更小。YOLOv8 的默认 stride 是 8/16/3220 像素的目标在 P3 层还有特征响应但如果小目标占比极高建议yolo detect train \ dataip102.yaml \ modelyolov8s.pt \ imgsz1280 \ rectTruerect参数允许同 batch 内的图片使用不同长宽比保持纵横比缩放在提升小目标分辨率的同时不引入太多填充浪费训练速度损失小于直接放大 imgsz。如果硬件扛不住 1280另一个折中方案是imgsz960并开启cacheTrue用内存换插入时间。5. 验证、排错与一个数据增强的进阶技巧5.1 训练后的验证命令与指标解读yolo detect val \ modelip102_runs/exp1/weights/best.pt \ dataip102.yaml \ imgsz640 \ batch32 \ conf0.001 \ iou0.6conf0.001是关键验证时要把置信度阈值放到最低这样才能绘制出完整的 PR 曲线和 mAP0.5。如果验证时用了默认的conf0.25mAP 会被高估而实际部署推理时又必须重新调阈值两头不讨好。iou0.6表示 IoU 大于 0.6 才算正样本比 COCO 的 0.5 更严格对 IP102 这种密集小目标更合理。训练完成后建议做的事用yolo predict跑 10 张验证集图片人工比对框的贴合度。mAP 是统计指标框偏移 20% 在 mAP 里可能只扣 0.02 分但实际农药喷洒路径规划就完全错位了。画混淆矩阵from ultralytics.utils.plotting import plot_confusion_matrix重点观察哪些类互相混淆。IP102 里很多害虫外形极度相似如果混淆集中在某几个近亲类考虑合并类别比调参更有效。5.2 常见的 XML 转换坑与排错清单症状可能原因解法XML 解析报错invalid token文件编码不是 UTF-8读取时用encodingutf-8-sig或批量iconv -f GBK -t UTF-8训练时 Loss 为 NaN归一化坐标出现负值或超宽检查 XML 中 xmax 是否小于 xmin用 2.3 的过滤逻辑提前剔除某张图检测不到任何目标对应的 txt 是空文件确认 XML 里没有object或name不匹配 class_map所有框偏移固定方向width/height 取值错误检查是否用了图片实际尺寸而非 XML 的size两者可能不一致类别数量对不上classes.txt 顺序与映射不一致打印前 10 行映射表逐个人工核对最后一类隐藏问题Annoations目录下存在重复文件名。改标注输出时标注工具自动保存了.xml.bak后缀文件glob(*.xml)不会匹配这种文件但shutil.copy生成 label 文件名时用原 basename多个版本覆盖会产生 label 与 image 错位。解决方法是转换前先检查重复名from collections import Counter dup_names [name for name, cnt in Counter(ids).items() if cnt 1] assert len(dup_names) 0, fDuplicate names: {dup_names}5.3 进阶技巧使用标注工具回读 YOLO 格式修改样本当模型在某个类别上表现差时最有效的手段是检查标注质量而不是加大模型。用labelImg回读 YOLO 格式后直接修正框再重新导出 XML形成 VOC 与 YOLO 双格式闭环。labelImg yolo_dataset/images yolo_dataset/labels classes.txt把classes.txt内容按行排好labelImg 会按照文件顺序映射到快捷键 1~95。实测中 IP102 最常见的标注问题是小目标框偏大把害虫触角也框进去这在检测精度上影响不大但会影响切割分类任务。用labelImg的W键和A/D键快速切换到前后图做一轮 500 张的校对模型 mAP 的提升通常比换更大的 backbone 更明显。这是个高性价比的做法发布数据集时也建议附上这轮人工反馈记录。本文还有配套的精品资源点击获取