尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

IP102病虫害数据集VOC转YOLO:从XML标注到YOLOv8训练实战

IP102病虫害数据集VOC转YOLO:从XML标注到YOLOv8训练实战 简介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.xml。rf后缀常见于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根节点下面按层级组织图像信息和每个目标的信息。上面给出的是数据集里常见字段的对照说明。字段路径类型含义转换时是否必须folderstr图像所在目录名否可忽略filenamestr图像文件名是用于与原图关联sourcestr图片来源否sizewidthint图像宽度是坐标归一化依赖sizeheightint图像高度是坐标归一化依赖sizedepthint通道数通常为3否objectnamestr目标类别名称是objectdifficultint是否难例0或1建议保留并单独处理objectbndbox4个intxmin, ymin, xmax, ymax是需要注意difficult字段。VOC原版约定difficult1表示目标模糊难辨认评测时不计入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(fXML文件总数: {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, keylambda 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里坐标是绝对像素值比如xmin120, ymin340这种表示对尺寸固定的输入图没问题但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), encodingutf-8) xml_dir Path(path/to/xml) out_dir Path(path/to/labels) out_dir.mkdir(exist_okTrue) 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, encodingutf-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, keyclass_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_trainn_val]) test_set.extend(items[n_trainn_val:]) print(ftrain: {len(train_set)}, val: {len(val_set)}, test: {len(test_set)})逻辑说明脚本先按一张图中出现次数最多的类别作为该图的层标签再进行分层切分。seed42固定随机种子保证可复现。比例上训练集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 \ dataip102_ext.yaml \ modelyolo11s.pt \ epochs80 \ imgsz960 \ batch16 \ lr00.005 \ mosaic0.8 \ cos_lrTrue \ projectruns/ip102_ext参数表格与推荐取值参数推荐值说明imgsz960病虫害小目标多640输入下帧宽不足960能明显提升小目标recallepochs80-120迁移学习下80轮可收敛观察val loss不降后early stopbatch按显存调16GB显存配960输入建议batch为16OOM就降到8lr00.005比默认0.01保守农业数据噪声大学习率过大容易过拟合背景mosaic0.8保留一定真实比例全mosaic会让模型过度依赖拼接纹理训练到一半时重点看两个曲线val/box_loss和metrics/precision。box_loss持续下降但precision抖动不升说明模型对小目标回归不准优先调anchor或增大imgsz而不是加epochs。4.4 评估阶段怎么看懂mAP训练结束后运行yolo detect val \ modelruns/ip102_ext/weights/best.pt \ dataip102_ext.yaml \ imgsz960 \ conf0.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 \ modelruns/ip102_ext/weights/best.pt \ dataip102_ext.yaml \ epochs40 \ imgsz960 \ copy_paste0.5 \ mosaic0.6这里要注意copy_paste对密集小目标很有效但对大目标容易贴出反逻辑画面比如把螟虫贴到水稻叶片之外。所以copy_paste0.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_typeyolov8, model_pathruns/ip102_ext/weights/best.pt, confidence_threshold0.3, image_size960, devicecuda:0, ) result get_sliced_prediction( test_images/rice_pest_001.jpg, detection_model, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.2, )切片尺寸用640配合原图960训练窗口能覆盖完整虫体。overlap_ratio0.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 \ modelruns/ip102_ext/weights/best.pt \ dataip102_ext.yaml \ conf$conf done比较每个阈值下的mAP50和F1再结合实际推理场景选。害虫巡检一般更在意recall而不是precision多检错一个框的成本低于漏检一个虫害爆发点所以阈值别设太高。这套流程走完后这个VOC标注的IP102扩展集训练出来的模型在细粒度小虫目标上的mAP50表现会明显好于拿原版分类数据硬转的模型。本文还有配套的精品资源点击获取
返回列表