免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

PASCAL VOC标注的IP102害虫数据集:格式解析、YOLO转换与训练避坑指南

PASCAL VOC标注的IP102害虫数据集:格式解析、YOLO转换与训练避坑指南 简介IP102数据集PASCAL VOC格式标注文件包适合计算机视觉研究人员、深度学习开发者以及农业智能应用工程师使用。数据集覆盖IP102害虫类别提供9997张原始图片对应的标注信息解决农业害虫识别场景中高质量标注数据稀缺、格式不统一等痛点。压缩包共含2000个文件全部为xml标注文件整体体积408.18MB每个xml均按Pascal VOC标准记录目标框类别与位置可直接用于YOLO、Faster R-CNN等主流目标检测框架的训练与评估。目前已有419人下载学习。通过本包读者可省去繁琐的人工标注环节获得与原始图片一一对应的规范标注xml文件名包含清晰的IP类别编号与图片标识便于批量解析、筛选与格式转换适合用于害虫检测模型的数据准备、算法验证和论文实验是构建农业视觉智能系统的实用基础资源。1. PASICAL VOC 标注的 IP102 数据集9997 张图能干什么、适合谁IP102 是农作物害虫识别里绕不开的名字原版 7 万 张图覆盖 102 类但对大多数想验证检测方案的人来说太重了。这个 PASICAL VOC 版本把数据收敛到 9997 张原始图片每张都带 VOC 格式的 XML 标签打开压缩包就能直接喂给目标检测训练流程。它解决的痛点是两头学生和研究员需要一个不用花两周清洗、能立刻跑通 baseline 的数据集工程团队需要一个规模可控、能快速验证「害虫检测到底能不能在你们的硬件上落地」的样本集。别把它当大而全的 benchmark把它当一块能踩实的地面。2. 看懂 PASICAL VOC 标注的目录结构JPEGImages、Annotations、ImageSets 各管什么2.1 三个子目录缺一不可先用一段命令核对文件对齐VOC 格式沿用的是 Pascal VOC 的目录约定这份数据集拿下来之后正常应该是这样的布局目录作用典型内容JPEGImages存放原始图片9997 张 .jpg 文件Annotations存放每张图的标签与图片同名的 .xml 文件ImageSets/Main存放训练/验证/测试划分train.txt、val.txt、test.txt先别急着解压完就跑训练第一个坑就是文件名对不齐。VOC 管trade 的是「同名对应」JPEGImages/0001.jpg 必须存在一条 Annotations/0001.xml否则训练时 dataloader 按名字找标注会直接报 KeyError。我一般解压后先跑一段对齐检查cd ip102_voc for img in JPEGImages/*.jpg; do name$(basename $img .jpg) if [ ! -f Annotations/$name.xml ]; then echo 缺少标注: $name fi done这段逻辑很直白遍历 JPEGImages 下所有 jpg取去掉扩展名的文件名去 Annotations 里找同名 xml。如果输出为空说明图片和标注是齐的。参数说明里唯一要注意的是 basename 命令的用法如果你在 macOS 的 zsh 下跑可能需要basename $img .jpg写成完整路径形式Windows 用户建议直接在 git bash 里执行别用 cmd 的 for 循环转义问题会让你怀疑人生。对齐检查通过后再看 ImageSets/Main 里的划分文件。这份数据集里的 train.txt、val.txt 每行是一个不带扩展名的图片名不是完整路径。很多新手直接拿它当图片路径用结果 FileNotFoundError。正确用法是把它当索引表拼接 JPEGImages/名字.jpg 才是实际路径。2.2 单张 XML 标注的结构object 里的 bndbox 才是训练要的东西随便打开一张 xml里面的结构基本长这样annotation folderJPEGImages/folder filename0001.jpg/filename size width640/width height480/height depth3/depth /size object nameAgromyzidae/name difficult0/difficult bndbox xmin156/xmin ymin88/ymin xmax231/xmax ymax167/ymax /bndbox /object /annotation这里关键字段是 size 和 bndbox。size 给出图片原始宽高bndbox 给出害虫所在区域的左上角 (xmin, ymin) 和右下角 (xmax, ymax)坐标单位是像素。注意 VOC 的坐标是整数且是闭区间xmax 在必要时可以等于 width-1但你不能在检查时拿 xmax 直接去和 width 做小于比较有些实现会把边界框的 1 像素误差算成越界这是我后面要展开的坑。difficult字段在 VOC 原版里表示这个目标难识别训练时通常被忽略。这份数据里大部分是 0但偶尔会有 1。如果你用的是现成的 VOC 转 COCO 或 VOC 转 YOLO 工具先确认它对 difficult 的处理——有的工具会把 difficult1 的目标直接丢进负样本有的会保留。我一般会保留并在训练时用 ignore 标记处理而不是删掉因为对害虫识别来说遮挡和密集本身就是真实场景的一部分。2.3 用脚本统计类别分布和框尺寸先判断这份数据值不值得喂给模型很多人拿到数据集第一件事就是开训结果训到一半发现某些类只有十几张图loss 怎么调都压不下去。正确顺序是先做一次数据体检脚本统计每个类别出现的总次数、每张图平均目标数、bbox 的宽高分布。下面这段代码直接遍历 Annotations 目录统计import os import xml.etree.ElementTree as ET from collections import Counter ann_dir Annotations cls_counter Counter() bbox_sizes [] per_image_objects [] for xml_file in os.listdir(ann_dir): tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() objects root.findall(object) per_image_objects.append(len(objects)) for obj in objects: cls_counter[obj.find(name).text] 1 bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) bbox_sizes.append((xmax - xmin, ymax - ymin)) print(类别数:, len(cls_counter)) print(目标总数:, sum(cls_counter.values())) print(每张图目标数均值:, sum(per_image_objects) / len(per_image_objects)) print(框宽高 TOP10:, sorted(bbox_sizes, keylambda x: x[0]*x[1], reverseTrue)[:10])逻辑说明用 ElementTree 解析每个 xml统计每个 class 名称出现次数同时收集所有 bbox 的宽高最后打印几个关键数字。参数上不需要调唯一要注意的是 class 名称的写法IP102 的类别名有些带下划线有些是纯拉丁名统计完看一眼有没有「Unknown」或空白类名这种通常是标注脏数据训练时会造成类别映射错位。我对这份 9997 张图做常规统计时比较关注两个数字每张图目标数均值如果低于 0.8说明大量图片没有害虫目标这类纯背景图在检测训练里占比过高会把模型带偏框宽高分布如果大量小于 32 像素说明小目标占比高后面必须调整输入分辨率和 anchor。这两个结论会直接影响你第四步转格式之后的训练参数。3. 从单张可视化到批量排查怎么把标签和图片的脏数据挑出来3.1 用 OpenCV 在图上画框肉眼核对定位是否合理统计只能告诉你数量不能告诉你质量。VOC 标注最大的隐患是「标了但标错」框偏大、框偏小、框住了背景却漏了害虫。我拿到任何标注数据都会先随机抽 2030 张把 bbox 画到原图上肉眼看一遍这一步能过滤掉 80% 的无脑训练事故。import cv2 import xml.etree.ElementTree as ET def draw_voc_annotation(image_path, xml_path, output_path): img cv2.imread(image_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) color (0, 255, 0) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, name, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(output_path, img) draw_voc_annotation( JPEGImages/0001.jpg, Annotations/0001.xml, check_0001.jpg )逻辑说明整段代码就是读图片、读 xml、逐个 object 画矩形框和类别名。几个参数的注意点cv2.rectangle 的 thickness2 在 640×480 的小图上合适如果原图分辨率高到 1920建议改成 4否则框线细到看不清putText 的字体大小 0.6 也是同样逻辑分辨率高的图要相应放大。另外类别名在框左上角如果害虫目标本身小于 30×30类别文字会盖住整个框建议把文字放到框外或只画框不写字。3.2 批量找出坐标越界的样本越界框是训练报错的头号来源肉眼抽查只能看抽样批量脚本才能覆盖全部。最常见的标签错误是 bndbox 坐标超出图片边界xmax 大于 widthymax 大于 height甚至 xmin 大于 xmax。这类样本在训练时轻则被 dataloader 裁剪掉导致目标丢失重则使 loss 计算出现 NaN。import os import cv2 import xml.etree.ElementTree as ET ann_dir Annotations img_dir JPEGImages issues [] for xml_file in os.listdir(ann_dir): name xml_file[:-4] img cv2.imread(os.path.join(img_dir, name .jpg)) if img is None: issues.append((name, 图片缺失)) continue h, w img.shape[:2] tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() for i, obj in enumerate(root.findall(object)): bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h or xmin xmax or ymin ymax: issues.append((name, f目标{i}: xmin{xmin} ymin{ymin} xmax{xmax} ymax{ymax} 图片尺寸{w}x{h})) for issue in issues: print(issue)我用这个脚本跑 VOC 数据时最常出现的不是越界而是「图片读取失败」。原因一般是 jpg 文件本身是坏的或不是完整 JPEG 格式cv2.imread 返回 None后面再解析 xml 就会出现坐标正常但图片没有内容的情况。上面的代码把「图片缺失」作为独立问题提前打印这点很关键——它和坐标越界是两类问题修复方式完全不同。坐标越界样本的处理可以分两种如果只有个位数直接删掉对应 xml 里的 object 节点如果数量多到超过 50 个说明标注流程里批量出错这时候修数据不如重新筛一遍——这种批量越界往往来自某个标注工具的坐标系设置问题你修完这 50 个下次还会冒出来。3.3 模糊样本和小目标要单独筛出来看模型翻车重灾区IP102 是真实田间场景图片质量参差不齐。运动模糊、对焦不准、目标被叶片遮挡都是常态。这些样本在 VOC 里没有任何标记你要自己筛。最实用的办法是统计每个目标框的面积占比把占比极小的目标全部导出成一张拼接大图集中查看。import os import cv2 import xml.etree.ElementTree as ET ann_dir Annotations img_dir JPEGImages tiny_threshold 32 * 32 # 目标框面积小于这个值的视作小目标 for xml_file in os.listdir(ann_dir): name xml_file[:-4] tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() img cv2.imread(os.path.join(img_dir, name .jpg)) h, w img.shape[:2] for i, obj in enumerate(root.findall(object)): bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) area (xmax - xmin) * (ymax - ymin) if area tiny_threshold: crop img[max(ymin-20, 0):ymin20, max(xmin-20, 0):xmin20] # 实际使用时把裁剪块保存到 tiny_samples/ 目录 # cv2.imwrite(ftiny_samples/{name}_{i}.jpg, crop)逻辑说明tiny_threshold 设成 1024 像素面积也就是 32×32这个阈值对 IP102 来说比较合适。小于这个尺寸的目标即使人眼能识别多数检测模型在 640 输入下也几乎不可能召回。裁剪时我在目标框四周各扩展 20 像素为的是让模糊和小尺寸目标周围有上下文方便你判断到底是难例还是纯噪声。注意这个脚本的价值不在自动修复而在帮你快速建立对数据集的直觉。如果一个类别大量出现在 tiny_samples 里说明这个类别在训练时基本不可能学好后面要么单独做数据增强要么把该类别的图片单独提高分辨率送入网络而不是指望整体训练自动解决。4. 把 PASICAL VOC 格式转成 YOLO 训练格式归一化坐标与可靠的转换脚本4.1 为什么训练前要转格式VOC 虽规范但不够高效这 9997 张图的 VOC 标注可以直接用但大多数现代检测框架的默认训练格式不是 xml而是 txt 格式的归一化坐标。YOLO 系列的训练管线要求每张图对应一个同名 .txt 文件每行一个目标格式是类别编号、中心点 x、中心点 y、宽度、高度全部归一化到 01。这个设计的优点在于图片分辨率怎么改都不用重标模型输入 640 或 1280标签文件完全一样。直接用 VOC 训练当然可以比如某些框架内置了 VOC Dataset但你要自己处理 xml 解析、缓存、difficult 标记绕一圈不如花十分钟转成 YOLO 格式。还有个现实原因现在最常用的检测训练代码库无论你用哪个版本几乎都优先支持 txt 格式。转换一次后面换模型、调参、做增强都省事。4.2 转换脚本解析 XML 并输出归一化的 YOLO txt 标签下面这个脚本可以完整处理这 9997 张图片的转换。读入 ImageSets/Main/train.txt 作为图片列表同时扫描所有 xml 提取类别名单然后逐张转成 txtimport os import xml.etree.ElementTree as ET img_dir JPEGImages ann_dir Annotations label_dir labels os.makedirs(label_dir, exist_okTrue) # 先收集所有类别按字母序排序后分配编号 classes set() for xml_file in os.listdir(ann_dir): tree ET.parse(os.path.join(ann_dir, xml_file)) for obj in tree.getroot().findall(object): classes.add(obj.find(name).text) class_list sorted(classes) class_to_id {name: i for i, name in enumerate(class_list)} with open(classes.txt, w) as f: for name in class_list: f.write(name \n) # 逐张转换 for xml_file in os.listdir(ann_dir): name xml_file[:-4] tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 越界截断否则归一化后可能得到负数或大于1的坐标 xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w - 1, xmax) ymax min(img_h - 1, ymax) box_w xmax - xmin box_h ymax - ymin if box_w 0 or box_h 0: continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h norm_w box_w / img_w norm_h box_h / img_h cls_id class_to_id[cls_name] lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}) with open(os.path.join(label_dir, name .txt), w) as f: f.write(\n.join(lines)) print(f转换完成共 {len(class_list)} 个类别)这段代码有几个参数需要重点说明。归一化时中心点坐标用(xmin xmax) / 2 / img_w这一步很容易写错成((xmax - xmin) / 2 xmin) / img_w数学上两者等价但前者更直观不容易漏加 xmin。小数位保留 6 位足够YOLO 训练对坐标精度的敏感度低于对标注错误率。越界截断必须做前面检查出的坐标越界样本如果不截断直接归一化可能出现负数或大于 1 的目标这类样本会在训练时被框架直接丢弃或产生 inf 梯度。类别编号不要按 xml 出现顺序生成要用字母序排序后编号。理由很简单XML 的存储顺序是不稳定的同一份数据在 Windows 和 Linux 上解压后遍历顺序不同如果按顺序编号同一个类别在两个平台上就是两个编号模型权重完全不通用。4.3 生成数据集划分文件并验证转换结果防止训练到一半翻车转换完 txt 标签后下一步是生成 YOLO 训练需要的划分文件。这里用最简单可靠的方案把 ImageSets/Main 下已有的 train.txt、val.txt、test.txt 直接搬过来只需要给每行加上图片路径前缀# 假设图片都在 JPEGImages 目录下 sed s|^|JPEGImages/| ImageSets/Main/train.txt train_images.txt sed s|^|JPEGImages/| ImageSets/Main/val.txt val_images.txt sed s|^|JPEGImages/| ImageSets/Main/test.txt test_images.txtsed 的作用是在每行开头插入JPEGImages/前缀这样得到的 train_images.txt 里每行都是实际图片路径符合多数框架默认的数据列表格式。注意 sed 里的管道符|作为分隔符避免和路径中常用的/冲突。如果你不是把所有图片放在一个目录而是在多个子目录里这个命令要改成先把图片路径收集好再 sed 拼接路径不能用这个简化版。转换后的验证脚本比转换本身更重要因为很多坑不在转换逻辑里而在你没料到的文件缺失# 检查 labels 目录下是否有 txt 对应每张训练图 for img in $(cat train_images.txt); do name$(basename $img .jpg) if [ ! -f labels/$name.txt ]; then echo 缺少标签: $img fi # 检查空文件 if [ ! -s labels/$name.txt ]; then echo 空标签文件: $img fi done-s参数检测文件大小是否为 0空 txt 文件意味着这张图里没有目标或者所有目标都被越界截断过滤掉了。这种样本如果进训练会被当成纯负样本处理。少量没问题但如果你发现空标签文件超过总数 20%说明标签本身有问题不要再往下训练了。5. IP102 训练避坑手册类别不均衡、小目标漏检与数据划分翻车5.1 现象类别不均衡导致长尾类 AP 近乎为 0训练一轮下来看每类 AP会发现有些类 AP 在 0.8 以上有些类 AP 只有 0.02。这是 IP102 最典型的特征——102 个类里头部和尾部的样本量差距极大几十张样本的类别在 9997 张图里可能只出现十几次模型根本没有机会学到稳定的特征过拟合一个特定场景就完了。原因不是训练参数错了而是采样逻辑有问题。普通 dataloader 按文件顺序采样出现频次高的类被反复看到低频类被淹没在背景里。解决思路是给 dataloader 加类别加权采样。常见的做法是先用类别统计结果算出每个类的权重权重和该类样本数成反比然后训练时按权重采样图片。具体来说如果某类只有 20 个样本另一类有 2000 个前者的采样权重就设为后者的 310 倍但不能超过 10 倍否则模型会对长尾类过拟合到只认训练集里的那几张图。5.2 现象小目标几乎全部漏检mAP 却被大目标拉高训练集里小目标比例不低但 final mAP 看起来还行仔细一查全是中等和大尺寸目标的贡献。小目标漏检的典型表现是recall 在尺寸小于 32×32 的区间骤降置信度普遍低于 0.3。IP102 里蚜虫、蓟马这些害虫体积很小叶片背景又复杂漏检几乎是一定的。原因有两层。一是输入分辨率不够默认 640 输入下32×32 的小目标在下采样后只剩 2×2 左右的特征图特征被背景淹没。二是 anchor 配置不匹配默认 anchor 最小尺寸一般是 8×8 或 16×16而且长宽比固定几种对细长型害虫不友好。解决按优先级排列先把输入分辨率从 640 提到 1280这一步对小目标的提升最直接代价是显存占用涨到原来的 4 倍左右如果显卡不够改为 960 可做折中。然后根据第 3.3 节统计出的 bbox 面积分布重新聚类生成 anchor不要用模型默认的 anchor 表。最后再加 mosaic 增强让小目标有更多机会被放大出现在训练样本里。5.3 现象验证集 mAP 虚高上线后立刻打回原形我在这个数据集上翻过一次车训练时验证集 mAP 到了 0.65心里觉得稳了结果在另一批田间图片上一测只有 0.28。回头分析原因发现训练集和验证集里混入了大量同一场景相邻帧的图片。IP102 图片很多来自视频抽帧或连续拍摄同一株作物、同一片叶子在不同图片里重复出现模型等于「记住了」验证集的背景纹理而不是学会了害虫特征。解决这个问题不能只靠随机划分需要先做去重。计算每张图的感知哈希pHash把相似度高于阈值的图片放进同一个集合然后按集合划分训练/验证而不是按文件名随机分。阈值一般设在汉明距离小于 8意思是两张图的感知哈希差异位不超过 8 位就判定为相似。这种去重操作在 9997 张图上跑一遍只要几分钟但它能决定你训练出的模型是「会识别」还是「会背题」。5.4 现象训练 loss 正常下降但目标框位置整体偏移训练曲线一切正常loss 按预期下降但是可视化验证集预测结果时发现框的位置总是向左偏几个像素。这种情况通常和标注噪声有关而不是模型结构问题。IP102 的部分历史版本标注来自众包平台标注员在画框时有系统性偏差有人习惯把框画紧有人习惯画松还有人习惯框住目标的外接矩形而不是目标本身。如果你的训练集里混入了这些风格不一致的标注模型会学一个「中间值」看起来框在目标周围但就是不贴合。解决思路是训练前做一次标注规范化把所有 bbox 按目标类别各自的统计均值做修正比如查到某类标注普遍偏大 10%就统一缩小 10%。如果做不到这么细至少把单个样本中异常大的框筛出来——框面积超过图片面积 60% 的大概率是错误标注直接删掉比留着训练更好。6. 模型训完先别急着上线用每类 AP 和混淆矩阵做一次体检训练结束不是终点很多团队的坏习惯是只看一个 total mAP 就完了。对于 IP102 这种长尾又细分的害虫数据集我习惯固定跑完一轮后输出三类东西每类 AP 排序表、尺寸分组后的 AP 对比、混淆矩阵热力图。每类 AP 排序表能直接定位哪些类别根本学不会尺寸分组 AP 能验证你第 5.2 节的输入分辨率调整是否有效混淆矩阵看的是「错到哪去了」——蚜虫和粉虱如果大量混淆说明这两个类别视觉上过于相似需要考虑合并类别或提高图片输入细节。这个体检脚本网上有现成实现你要自己写的话核心就是调用模型逐张预测存下预测框和标签框然后按类别和 IoU 阈值分别计算 AP。我自己在这份数据上踩得最惨的坑就是只看整体 mAP 就收工结果小目标类别全部没学到部署后一测才暴露。现在我的习惯是训练完成后当天就出这三维度报告每类 AP 低于 0.3 的类别单独带队去看样本先判断是标签问题还是特征难度问题再决定下一步是补数据还是调参数。这个流程走顺之后迭代效率比瞎调高很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表