ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

IP102害虫检测VOC数据校验与YOLO训练格式转换实战

IP102害虫检测VOC数据校验与YOLO训练格式转换实战 简介IP102数据集的PASCAL VOC格式标注文件包专门面向农业害虫识别、目标检测等计算机视觉方向的开发者与研究者。压缩包内共包含2000个XML文件整体容量约408.18MB每个XML文件对应一张害虫图片的标注信息其中记录着目标的类别名称与边界框坐标等标准字段可直接用于Faster R-CNN、YOLO、SSD等主流检测模型的训练也容易通过脚本转换为COCO等标注格式。IP102数据集涵盖102类常见农业害虫这套标注可以大幅节省人工标注与整理标签的时间成本非常适合用于课程设计、科研实验以及农作物病虫害智能检测项目。需要特别说明压缩包内只提供标注文件原始图片与完整标签可在配套博文链接中查看文件命名与原始图片一一对应便于批量匹配与数据划分。目前已有421人学习下载适合正在研究害虫检测、目标检测算法复现的读者无论是初学者还是中高级算法工程师都能快速部署使用。1. IP102做到PASCAL VOC9997张标注图能否直接开训做农业害虫检测的同行都有过这种体验IP102图片量很大但原始标注只到图像级类别想跑目标检测还得自己画框。所以当有人把其中9997张原始图片按PASCAL VOC格式标注好、图片和标签打包在一起时最该想清楚的是这份数据能不能直接进训练流程还是得先做一轮清洗。我按处理这类标注数据时的顺序讲清楚VOC目录怎么读、标注怎么校验、怎么转成YOLO训练格式以及最容易翻车的几个位置。新手可以按章节顺序一步步跟下来熟手直接跳到第3章和第5章那里是数据质量最容易出问题的地方。2. 先看懂VOC的底细目录结构、XML字段与IP102类别映射2.1 一个标准VOC数据集的文件布局长什么样PASCAL VOC的目录结构比较固定拿到手先执行一遍tree把全貌摸出来。常见布局是三个主要目录加一个类别清单文件IP102_VOC/ ├── JPEGImages/ # 原始图片jpg格式 ├── Annotations/ # 每张图一个同名XML ├── ImageSets/ │ └── Main/ # 放train.txt、val.txt等划分文件 └── labels.txt # 类别清单一行一个类别名JPEGImages放的是训练要用的原始图片文件名通常是六位数字编号这套数据有9997张文件名可能是延续IP102原始编号也可能是重新编号过。Annotations里每张图对应一个同名XMLXML记录了这张图有哪些目标、每个目标的类别名和边界框坐标。ImageSets/Main下面是划分文件每行一个不带扩展名的图片名决定哪些图进训练集、哪些进验证集。labels.txt是这份数据集的附加产物标准VOC里没有它但转YOLO时离不开它决定了类别名到编号的映射顺序这个顺序后面会反复踩坑先留个印象。拿到数据集之后第一件事不是看图片而是确认这三个目录的数量关系。简单统计一下JPEGImages里的jpg数量、Annotations里的xml数量两个数应该完全一致。ImageSets/Main里有没有现成的train.txt和val.txt有的话先看划分比例没有的话后面自己划。另一点要注意的是JPEGImages里的图片是否全部是RGB三通道IP102这类田间采集数据偶尔混入灰度图或损坏图这会直接影响到后面训练时的数据加载。2.2 XML里的关键字段size、object、bndbox到底记了什么VOC的XML结构本身不复杂但训练框架对它的读取是固定的缺一个字段都可能直接报错。打开一个XML核心结构是这样annotation folderJPEGImages/folder filename000001.jpg/filename size width960/width height540/height depth3/depth /size object nameaphid/name bndbox xmin128/xmin ymin96/ymin xmax512/xmax ymax384/ymax /bndbox /object /annotation四个字段必须盯住。filename要和实际图片名完全一致包括扩展名size下面的width和height是图片的真实尺寸转YOLO算归一化坐标时全靠它object里的name是类别名必须能在labels.txt里找到拼写差一个字母后面就是类别静默错位bndbox的四个值是边界框左上角和右下角的像素坐标xmin必须小于xmaxymin必须小于ymax。关键字段对不上轻则训练崩重则模型学歪了还看不出原因。字段含义常见错误filename图片文件名含扩展名与JPEGImages实际文件名不一致size/width图片真实宽度像素与PIL读取结果不一致坐标整体偏移size/height图片真实高度像素同上object/name类别名必须在labels.txt中拼写不一致类别静默错位bndbox左上角与右下角坐标越界、反向框导致训练崩溃还有一个字段容易忽略depth。它表示通道数正常RGB图为3。有些标注工具导出时写1后续转YOLO虽然不读depth但部分框架做数据检查时会拿它判断灰度图该字段缺失或错误会引出莫名其妙的警告。校验脚本里顺手补成3即可。2.3 IP102原始标签怎么对应到VOC的类别名IP102原始类别有102个这份VOC版本是子集具体类别数以labels.txt为准。常见做法是保留类别的英文名或拼音名比如aphid、leafhopper也有直接用ip102_001这种编号的。两种命名风格影响的是后续转YOLO时的类别文件顺序不影响训练结果本身。我的习惯是拿到手先把labels.txt读一遍确认首行是不是类别0因为YOLO的类别编号从0开始labels.txt的每一行恰好对应一个编号。如果labels.txt不在压缩包里也可以自己生成遍历所有XML把object/name去重后按字典序排序写入文件。注意排序方式要固定否则每次生成的顺序不同类别编号就会漂移。顺序一旦确定后面所有转换脚本和训练配置都以这个文件为准不要再手动增删行。类别数量方面9997张图如果保留90多个类别长尾会非常严重绝大多数类别可能只有几十个框。这种情况下直接训练mAP会被高频类别拉高低频类别基本学不到。这个风险在第5、6章会具体展开。3. 动手校验这份标注数据图片一致性、字段完整性与可视化抽检拿到标注数据集不校验就直接开训等于把质量检测交给损失函数。VOC格式的好处是校验脚本很好写十几分钟就能把整个数据集摸一遍。下面按三个步骤来每一步都针对一类实际问题。3.1 第一步核对JPEGImages与Annotations的一一对应最容易出的问题就是图片和标注对不上JPEGImages里有图但Annotations里没XML或者反过来。用一段脚本快速扫描import os img_dir JPEGImages ann_dir Annotations img_files {f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))} ann_files {f for f in os.listdir(ann_dir) if f.lower().endswith(.xml)} ann_basenames {os.path.splitext(f)[0] for f in ann_files} img_missing_ann [ f for f in img_files if os.path.splitext(f)[0] not in ann_basenames ] img_basenames {os.path.splitext(f)[0] for f in img_files} ann_missing_img [ f for f in ann_files if os.path.splitext(f)[0] not in img_basenames ] print(图片无XML:, img_missing_ann) print(XML无匹配图片:, ann_missing_img)代码把两边的文件名取basename后做差集两边互为参照。注意一个隐含约定有些数据集的图片扩展名不统一有的jpg有的png而XML里的filename字段写的是原始文件名。这里按basename匹配更稳妥不会因为jpg和png同名而误判。扫描结果如果不为空先看是缺标注还是扩展名不一致导致的误报处理办法是把不一致的图片名统一批量重命名而不是手工改XML。这个环节还要顺带确认一件容易被忽略的事图片文件名是否都符合训练框架的命名规则。有些数据集文件名里带括号、中文或空格在Windows下看着正常放到Linux服务器上训练就会出现随机路径错误。建议在第一步就把文件名统一成纯字母数字加下划线的格式后面能省掉大量排错时间。3.2 第二步检查XML字段完整性与bbox是否越界图片和XML数量对上之后还要读每一个XML检查关键字段有没有缺失、bbox是否越界。这一步用ElementTree就能完成import os import xml.etree.ElementTree as ET from PIL import Image ann_dir Annotations img_dir JPEGImages bad_files [] for xml_name in sorted(os.listdir(ann_dir)): if not xml_name.endswith(.xml): continue xml_path os.path.join(ann_dir, xml_name) root ET.parse(xml_path).getroot() size root.find(size) if size is None or size.findtext(width) is None or size.findtext(height) is None: bad_files.append((xml_name, size缺失)) continue img_name root.findtext(filename) if not img_name: bad_files.append((xml_name, filename缺失)) continue img_path os.path.join(img_dir, img_name) try: with Image.open(img_path) as im: w, h im.size except Exception: bad_files.append((xml_name, 图片无法解码)) continue for obj in root.iter(object): name obj.findtext(name) if not name: bad_files.append((xml_name, object缺name)) continue box obj.find(bndbox) if box is None: bad_files.append((xml_name, object缺bndbox)) continue xmin int(float(box.findtext(xmin))) ymin int(float(box.findtext(ymin))) xmax int(float(box.findtext(xmax))) ymax int(float(box.findtext(ymax))) if xmin 0 or ymin 0 or xmax w or ymax h or xmin xmax or ymin ymax: bad_files.append((xml_name, bbox越界或尺寸非法)) break print(问题文件:, bad_files[:20]) print(问题总数:, len(bad_files))这段脚本做了四件事检查size节点是否存在、检查filename是否填写、检查图片能否正常解码、检查每个bbox的坐标是否合法。核心是拿PIL实际读取的图片尺寸做基准不要直接信任XML里的size字段因为标注工具写错size的情况不在少数。这里特别加了xmin xmax这种反向框判断很多初版校验脚本只查越界不查反向框结果训练时loss出现nan排查半天才发现是几个框的坐标写反了。对bad_files里的文件数量在个位数可以直接修数量多就要考虑是不是标注流程出了系统性问题比如某批图片被批量压缩过导致尺寸变化。如果图片尺寸和XML的size大面积不一致最稳妥的做法是以实际图片尺寸为准批量重写所有XML的size节点而不是手工一个个改。3.3 第三步按类别分层抽样画框可视化抽查脚本校验只能保证格式合法不能保证框打得准。框偏了、框住了背景、漏标了目标这些都只能靠人眼判断。我会按类别分层抽样每个类别抽两到三张图把bbox画出来人工看。抽样和画框用这段代码import os import random import cv2 import xml.etree.ElementTree as ET from collections import defaultdict ann_dir Annotations img_dir JPEGImages out_dir check_vis os.makedirs(out_dir, exist_okTrue) samples defaultdict(list) for xml_name in os.listdir(ann_dir): if not xml_name.endswith(.xml): continue root ET.parse(os.path.join(ann_dir, xml_name)).getroot() for obj in root.iter(object): samples[obj.findtext(name)].append(xml_name) chosen [] for cls, xmls in samples.items(): chosen.extend(random.sample(xmls, min(3, len(xmls)))) for xml_name in chosen: root ET.parse(os.path.join(ann_dir, xml_name)).getroot() img cv2.imread(os.path.join(img_dir, root.findtext(filename))) if img is None: continue for obj in root.iter(object): 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))) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, obj.findtext(name), (xmin, max(0, ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(os.path.join(out_dir, xml_name.replace(.xml, .jpg)), img)抽样为什么按类别分层直接全局随机抽的话高频类别会占满抽查名额低频类别一次都抽不到等于没查。每个类别固定抽三张能保证样本覆盖到所有类别。画框时把类别名写在框上方字体大小固定为0.5便于快速浏览。这一步我重点看三类问题框有没有框住完整虫体、有没有把大片背景圈进来、同一张图里多个目标时是否漏标。虫体密集的图漏标很常见VOC格式下漏标不会让训练报错但会压制模型在该位置的召回率属于最隐蔽的质量问题。可视化检查结果如果发现大量框偏移建议先不要继续往下走回去查标注工具的导出参数或者标注规范。一个数据集如果存在系统性的框偏移后面所有训练和调参都是在一个有偏的数据上做无用功。4. 从VOC到YOLO训练格式转换脚本、归一化坐标与集划分VOC格式是交换格式不是训练格式。主流检测框架吃的是YOLO风格的txt格式一行一个目标格式是“类别编号 中心x 中心y 宽 高”坐标全部归一化到0到1。转换脚本本身不复杂但前提是经过第3章校验后XML足够干净这个顺序不能颠倒。4.1 转换脚本XML转TXT的关键代码与参数说明import os import xml.etree.ElementTree as ET with open(labels.txt) as f: classes [line.strip() for line in f if line.strip()] cls2id {name: i for i, name in enumerate(classes)} def convert(xml_path, txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.findtext(width)) h int(size.findtext(height)) lines [] for obj in root.iter(object): name obj.findtext(name) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls2id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) ann_dir Annotations label_dir labels os.makedirs(label_dir, exist_okTrue) for xml_name in os.listdir(ann_dir): if xml_name.lower().endswith(.xml): txt_name os.path.splitext(xml_name)[0] .txt convert(os.path.join(ann_dir, xml_name), os.path.join(label_dir, txt_name))这段脚本做的事情很直接读labels.txt构建类别编号映射然后对每个XML里的object把bndbox的左上角右下角坐标转成中心点坐标和宽高再分别除以图片的宽和高完成归一化。两个关键点cls2id的构建顺序直接来自labels.txt的行号所以这个文件的一行顺序都不要动增删行会让所有类别编号错位归一化时除的是size里的宽高这就是第3章强调要用真实图片尺寸校验的原因尺寸一旦错了所有坐标会整体缩放模型学到的框位置全部偏移。写txt时把类别编号和五个数值用空格分隔不要用逗号YOLO系列只认空格分隔。坐标保留六位小数足够再多也是浪费存储。转换完成后labels目录下的txt数量应该和Annotations里的xml数量完全一致这一步在4.3会做硬校验。4.2 训练/验证集划分与data.yaml配置转出来的labels目录和JPEGImages目录平级接下来要生成训练集和验证集的文件列表。如果数据集里没有现成的ImageSets划分我用固定随机种子来划分保证可复现import os import random random.seed(42) all_imgs [f for f in os.listdir(JPEGImages) if f.lower().endswith(.jpg)] random.shuffle(all_imgs) val_num int(len(all_imgs) * 0.15) val_imgs set(all_imgs[:val_num]) train_imgs all_imgs[val_num:] with open(train.txt, w) as f: for name in train_imgs: f.write(os.path.join(JPEGImages, name) \n) with open(val.txt, w) as f: for name in val_imgs: f.write(os.path.join(JPEGImages, name) \n)固定seed为42每次运行划分结果都一样方便复现实验。val比例设15%对接近一万张图的规模验证集有1500张左右评估mAP足够稳定。注意这里写进txt的是图片路径不是txt路径训练框架会自己根据图片路径找同名的标注txt。划分完还要确认一件事验证集和训练集不能有重复图片否则验证指标会虚高模型实际泛化能力根本没有看起来那么好。然后写data.yamlpath: /your/abs/path/to/IP102_VOC train: train.txt val: val.txt nc: 98 names: [aphid, leafhopper, caterpillar, ...]path必须是绝对路径train和val指向刚生成的txt文件nc就是labels.txt的行数names顺序必须和labels.txt完全一致。写data.yaml时最容易漏的就是names顺序很多人以为names只是显示用的实际上训练时类别编号直接对照names索引顺序错了等于类别错位模型挂在那边学你这边还看不出哪里不对。4.3 开训前的三个快检查数量对账、坐标范围、空txt转完格式不要急着起训练先跑三个快检查都是血泪经验换来的。第一数量对账确认三个数字一致find JPEGImages -name *.jpg | wc -l find Annotations -name *.xml | wc -l find labels -name *.txt | wc -l三个数字必须完全一样。第二抽查坐标范围随机打开几个txt确认五个数值都在0到1之间。如果出现大于1的值说明XML里bbox越界了回头修数据而不是继续训练。第三确保每个txt都有内容空txt在训练时会被框架跳过但如果某些图片确实没有目标空txt是合法的这里要区分的是“该有目标但没有标注”和“本身就没有目标”两种情况。三个检查都通过数据集就具备开训条件了。但别急着把训练跑起来下面这章的内容能帮你省掉至少三天的排查时间。5. 避坑指南标注数据最容易翻车的五个位置标注数据集的坑多半不是出在模型结构上而是出在数据本身。这张图没有XML、那个类别编号串了位、验证集里混进了训练图片……每一类问题都不会直接让训练报错但都会让最终模型的指标和实际表现对不上。这一章按现象、原因、解决的顺序列出我在处理类似标注数据时最常遇到的五个问题每一条都可能让你的训练静默变差甚至直接中断。5.1 XML里没写size字段训练直接崩现象训练数据加载到一半突然抛KeyError报错行指向读取size节点的宽高打开对应XML一看size节点整个不存在。更隐蔽的情况是size节点在但width或height其中一个缺失。原因标注工具不同版本导出的XML结构不统一有些旧版本根本不写size或者把尺寸信息放在其他节点下。另一种常见来源是图片在标注完成后被批量压缩替换XML没有跟着更新字段对不上。解决在3.2的校验脚本里加一条规则size缺失或width/height缺失的XML直接列入不合格清单。数量少就用PIL读取真实尺寸补写depth顺手补成3大多数检测框架按三通道读图depth写1会让部分工具误判灰度图。数量多就要回到数据源头确认图片是否被统一改过尺寸如果是所有XML的size都要按照实际图片尺寸批量重写。5.2 类别编号错位模型把“蚜虫”学成了“叶蝉”现象训练loss正常下降验证集却显示部分类别mAP为零。把预测结果可视化后发现框的位置基本正确但类别名全部串到了相邻编号的类别上比如把蚜虫的框标成叶蝉。原因labels.txt被人为重排过或者转换脚本里用了字典迭代顺序而不是固定列表顺序。类别编号整体偏移一位模型在训练时学的就是这个错位映射所以loss照样收敛验证指标却一片混乱。解决所有类别编号必须以labels.txt为准在转换脚本里显式构建cls2id不要依赖set或dict的迭代顺序。转换完成后输出一份“编号-类别名-样本数”的统计表人工核对高频类别的编号与labels.txt一致。这件事只能靠人眼确认但它不会报错只会让模型悄悄学歪是五个问题里最阴的一个。5.3 图片和XML数量对不上val指标忽高忽低现象训练过程中验证mAP波动很大同一个权重文件两次验证结果差三个点以上训练集和验证集各自的loss却都正常。原因训练列表和验证列表之间有重复图片或者验证列表里某些图片在Annotations里没有对应XML。数据加载器遇到缺失会静默跳过该样本实际参与验证的图片集每次都在变化指标自然不稳定。解决划分训练验证集之前先做一次全集去重保证一张图只出现在一个集合里。再跑一遍3.1的数量对账脚本确认验证列表里的每一张图都有XML。把这两项检查结果记下来作为这份数据集的体检记录后面每次训练前都过一遍省得指标抖动了才回头查数据。5.4 截断图片与零字节文件DataLoader随机报错现象训练到某个epoch随机中断报错信息指向Image.open或cv2.imread返回None重新启动训练可能跑更久才在另一个epoch崩掉位置完全不固定看起来像是玄学问题。原因数据集中存在下载不完整或传输损坏的jpg文件头正常解码到一半失败。本地图片浏览器能显示缩略图甚至预览因为解码器做了容错处理但训练框架的loader解码时直接抛异常。解决校验脚本里加一步用PIL打开每张图片并调用load()强制解码把抛异常的图片单独挑出来删除或重新下载。这一步在第一次处理数据集时只花几分钟但能避免训练到20个epoch时突然中断、前功尽弃。损坏文件的处理原则是直接删除而不是保留因为缺失一张图只会少一个训练样本损坏一张图会毁掉整个训练过程。5.5 长尾分布低频类别几乎不被模型关注现象训练结束后头部十几个类别的AP值很高尾部几十个类别的AP接近零。验证集整体mAP还可以但单独看低频类别的检测效果基本不可用。原因这份IP102子集如果保留了全部类别部分类别可能只有十几张图。模型的学习容量被高频类别占满低频类别的loss在总loss中占比太小梯度被头部类别淹没模型相当于没学过这些类别。解决先按XML里的object数量统计每个类别低于30个目标的类别在首轮训练前暂时剔除剔除时要同步改labels.txt和data.yaml类别重新编号后才能转YOLO。或者保留全类别在loss层面对低频类别做重加权初始权重设2根据验证集mAP逐步上调。低成本方案是先剔除再补标注不要指望模型凭空学会只有十几张图的类别。6. 把这批数据用出效果置信度清洗、长尾补偿与验收流程数据集在格式上跑通只是第一步想要模型在田间环境下真正可用还得做两件事清洗低质量框和补偿长尾类别。下面给出可以直接用的处理思路。6.1 先用置信度过滤清洗低质量框如果这份VOC标注里有一部分是伪标签或者人工标注的粗框最有效的办法是先用当前数据集训练一个初步模型然后拿这个模型对训练集自身做推理把置信度低于0.3的框标记为“可疑框”。人工抽看可疑框低质量的直接删除而不是修正因为粗框对训练的伤害比缺框更大。修正只针对坐标偏移不大的框整体偏移的框直接删掉重标。置信度阈值不是越高越好设太高会把真值框也过滤掉。我一般先用0.2到0.3扫一遍统计可疑框占比如果超过5%说明原始标注质量整体偏低优先考虑补标注而不是清洗。这一步的本质是用模型辅助人眼把人工复查的范围从9997张图缩小到几百个可疑框。6.2 长尾类别怎么补复制粘贴增强与类别重加权对低频类别常见做法是复制粘贴增强把该类别的目标从原始图上抠出来粘贴到背景图上同时生成对应的XML标注。粘贴时要随机缩放和旋转避免模型学到固定的上下文。如果不想自己写增强逻辑可以暂时只用类别重加权把低频类别的loss权重调高让模型在训练时更关注这些类别。重加权的权重系数一般从2开始往上调到验证集低频类别mAP不再明显变化为止。这个方案比复制粘贴省事但效果上限也低一些。两种方法可以叠加先用重加权稳住训练再用增强扩充低频样本量。低频类别的目标数量至少要补到50个以上才有机会在验证集上看到可用的AP值。6.3 一套可复用的数据验收流程把前面几章的内容串成一个固定流程以后每次拿到标注数据集都走一遍第一步统计图片、XML、类别分布第二步校验字段完整性和bbox越界第三步按类别分层抽样可视化第四步转YOLO格式并做三个快检查第五步训练一个短周期模型用验证集mAP和类别分布交叉验证数据质量。五步走完这份9997张图的VOC数据集能不能用、哪里需要补标注心里就有数了。这套流程我现在每个数据集都走吃过亏之后养成的习惯。标注数据集的真实质量往往写在指标之外类别分布、边界框贴合度、文件一致性这些看起来不起眼的东西反而决定了最终模型在田间的表现。希望这些经验能帮到你让你手里的数据和训练都少走点弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进