首页 / 资讯中心 / 文章详情

苹果叶病害检测数据集VOC转YOLO训练避坑指南

苹果叶病害检测数据集VOC转YOLO训练避坑指南 ★ FEATURED ARTICLE
简介面向苹果叶病害识别与检测任务这份数据集包含21119张实地采摘、拍摄并经过数据增强处理的果园叶片图像覆盖苹果黑星病、褐斑病、花叶病、灰斑病和锈病5类常见病害。资源采用VOC与YOLO双格式标注其中XML文件保存目标边界框的详细坐标与类别信息TXT文件提供YOLO训练所需的类别编号及归一化坐标图片、XML、TXT一一对应合计约6.3万个文件压缩包大小713.92MB文件命名规律清晰便于按图像快速检索对应标注。博主强调数据均来自实际项目标注精准、场景多样且会持续优化更新目前已有472人学习下载。这套数据可直接用于目标检测、图像分类等模型的训练与验证尤其适合计算机视觉学习者、农业AI开发者在模型实验、算法对比或毕业设计中使用。1. 苹果叶病害识别检测数据集先看它能不能接住你的训练任务「5类苹果叶病害识别检测数据集」这种压缩包在植保 AI 项目里属于硬通货。21119 张图五类病害同时给出 VOC 的 XML 和 YOLO 的 TXT 两套标签意味着拿到手就能直接开工不用先花几天在 LabelImg 里补标注。但拿来就训往往要翻车类别顺序、标签目录结构、坐标归一化方式任何一个和训练脚本对不上损失曲线就会用玄学的方式惩罚你。这篇笔记从拆包开始把文件结构、格式转换、训练配置和常见的标注坑一次讲透适合准备做作物病害检测、又不想从零标数据的团队也适合想拿现成数据验证 YOLO 改进点的研究者。2. VOC 与 YOLO 双标签怎么落地XML 到 TXT 的映射与转换脚本2.1 标签文件命名规则图片、XML、TXT 三者的对应关系拿到压缩包解压后第一件事不是看图片而是先理清目录结构。常见做法是图片放在 images 或 JPEGImages 目录VOC 格式的 XML 放在 AnnotationsYOLO 格式的 TXT 放在 labels。三个目录里的文件名一一对应前缀相同、扩展名不同比如 IMG_20210703_001.jpg 对应 IMG_20210703_001.xml 和 IMG_20210703_001.txt。命名对齐是整个数据集的命脉。YOLO 训练时读取标签的逻辑很简单拿到一张图片路径去找同名的 TXTVOC 转 YOLO 时也依赖同名匹配。如果 XML 里写的 filename 字段和实际文件名不一致或者图片是 .JPG 大写扩展名、标签是 .jpg 小写转出来的 TXT 就会对不上。我一般会先跑一个文件清单比对脚本把只有图没有标签、只有标签没有图的文件全部列出来。这一步能筛掉压缩包在传输、拷贝过程中产生的残缺文件也顺带发现那些被重复命名的图片。花五分钟做这个检查比训练到一半发现样本数少了上千张再回头排查要划算得多。2.2 读 XML 标签object 里的 name、bndbox、difficult 是关键VOC 格式的 XML 是树形结构训练真正需要的字段只有几个。根节点 annotation 下有 filename、size 和若干个 objectsize 里记录图片宽高决定 YOLO 坐标归一化的分母object 里的 name 是类别名bndbox 是四个整数坐标分别表示 xmin、ymin、xmax、ymax。difficult 字段在 VOC 检测任务里表示目标难以辨识比如被遮挡严重或太小。很多转换脚本直接忽略它但遇到苹果叶病害标注时这会成为隐患——如果标注员把早期小病斑标成了 difficult转换后这些样本要么被当成普通目标参与训练要么直接丢失两种结果都会干扰模型对小目标的判断。find . -name *.xml | head -5 | xargs -I{} sh -c echo {} ; head -20 {}这条命令用来快速抽查 XML 内容。head -20 能看到 filename、size 和第一个 object 的完整结构确认标注格式是否符合预期。如果 XML 里嵌了 verification 之类的额外字段不必惊慌转换时只取需要的键就行。2.3 转换脚本VOC XML 转 YOLO TXT 的归一化计算YOLO 标签格式是每行一个目标类别 id 加四个归一化浮点数分别是中心点 x、中心点 y、框宽 w、框高 h全部除以图片宽高。转换逻辑不复杂但 CLASSES 列表的顺序必须和之后 data.yaml 里的 names 完全一致否则类别就错位了。import os import xml.etree.ElementTree as ET CLASSES [apple_scab, black_rot, cedar_apple_rust, healthy, frog_eye_leaf_spot] def convert_xml_to_yolo(xml_path, txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) if img_w 0 or img_h 0: print(f[skip] bad size in {xml_path}) return lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f[warn] unknown class {name} in {xml_path}) continue cls_id CLASSES.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 过滤掉异常框坐标反了或宽高为负 if w 0 or h 0: print(f[warn] bad box in {xml_path}: {name} {xmin},{ymin},{xmax},{ymax}) continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n) if __name__ __main__: xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for fname in os.listdir(xml_dir): if fname.lower().endswith(.xml): xml_path os.path.join(xml_dir, fname) txt_path os.path.join(txt_dir, fname[:-4] .txt) convert_xml_to_yolo(xml_path, txt_path)转换脚本里有两个容易被忽略的细节。第一个是宽高为 0 或坐标反了的异常框标注工具偶尔会产出 xmin 大于 xmax 的脏数据不过滤会让 loss 里出现 NaN 或者训练震荡。第二个是 unknown class 的告警如果 XML 里有 healthy 之外的健康叶类别名或者拼写不一致脚本会跳过它此时必须回到原始数据确认到底有几类。这段脚本输出的 TXT 直接放进 YOLO 的 labels 目录就能用。如果数据集已经自带了 TXT我会拿这个脚本对同一批 XML 重新生成一份放到临时目录做 diff 对比目的不是怀疑数据集作者而是确认压缩包里的 TXT 是否和 XML 完全同步——两份标签一旦出现不一致后期排查会非常痛苦。2.4 转换后自检越界、空文件、重复标签的排查脚本转换完成不等于数据可用。YOLO 对标签的合法性要求很严坐标必须在 [0, 1] 区间内每行只能有五个字段类别 id 必须在 names 范围内。任何一行出问题训练时轻则跳过该样本重则直接报错中断。import os IMG_DIR images LABEL_DIR labels IMG_EXTS (.jpg, .jpeg, .png, .bmp) NUM_CLASSES 5 def check_label_sanity(img_dir, label_dir): problems [] for img_name in os.listdir(img_dir): if not img_name.lower().endswith(IMG_EXTS): continue stem os.path.splitext(img_name)[0] label_path os.path.join(label_dir, stem .txt) if not os.path.exists(label_path): problems.append((img_name, missing label)) continue with open(label_path, r, encodingutf-8) as f: lines f.readlines() if not lines: problems.append((img_name, empty label file)) continue for line in lines: parts line.strip().split() if len(parts) ! 5: problems.append((img_name, fbad field count: {line.strip()})) continue try: cls_id int(parts[0]) x, y, w, h map(float, parts[1:]) except ValueError: problems.append((img_name, fnon-numeric: {line.strip()})) continue if cls_id 0 or cls_id NUM_CLASSES: problems.append((img_name, fclass out of range: {cls_id})) if not (0 x 1 and 0 y 1): problems.append((img_name, fcenter out of [0,1]: {x},{y})) if w 0 or h 0 or w 1 or h 1: problems.append((img_name, fbox size invalid: {w},{h})) for item in problems[:50]: print(item) print(ftotal problems: {len(problems)}) if __name__ __main__: check_label_sanity(IMG_DIR, LABEL_DIR)这个自检脚本会把所有有问题的标签打印出来限制显示前 50 条避免刷屏。中心点越界是常见问题之一通常由 XML 中的 bndbox 超出图片尺寸导致空标签文件则说明那张图上没有标注任何目标后续划分数据集时应该把这类图片单独存放别丢进训练集干扰正负样本比例。自检通过后我才会把 labels 目录交出去给训练脚本用。到这里VOC 和 YOLO 两套格式的同步问题就被彻底钉死了后面不管换哪个框架都只需要重新写一个读取函数不用再碰坐标计算。3. 用这份数据集跑通 YOLOv8目录划分、data.yaml 与首次训练3.1 训练集与验证集的划分按目录复制还是按文件生成数据准备好了下一步是决定怎么划分训练集和验证集。很多人在这一步图省事直接把所有图片放在一个文件夹里让训练脚本随机抽 20% 当验证集。对苹果叶病害这类数据我不会这么做因为同一株树拍摄的多张照片往往同时出现在训练和验证里导致指标虚高。合理的做法是先把所有图片和标签按训练集、验证集分成两个目录再用脚本生成对应的 images/train、images/val、labels/train、labels/val 结构。YOLOv8 官方约定就是这种布局目录里放图片labels 目录放同名 TXT训练时 data.yaml 指向父目录即可。import os import random import shutil random.seed(42) IMG_DIR images LABEL_DIR labels TRAIN_RATIO 0.8 train_img_dir dataset/images/train train_label_dir dataset/labels/train val_img_dir dataset/images/val val_label_dir dataset/labels/val for d in [train_img_dir, train_label_dir, val_img_dir, val_label_dir]: os.makedirs(d, exist_okTrue) all_files [f for f in os.listdir(IMG_DIR) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(all_files) split int(len(all_files) * TRAIN_RATIO) train_files all_files[:split] val_files all_files[split:] for img in train_files: stem os.path.splitext(img)[0] shutil.copy(os.path.join(IMG_DIR, img), os.path.join(train_img_dir, img)) src_label os.path.join(LABEL_DIR, stem .txt) if os.path.exists(src_label): shutil.copy(src_label, os.path.join(train_label_dir, stem .txt)) for img in val_files: stem os.path.splitext(img)[0] shutil.copy(os.path.join(IMG_DIR, img), os.path.join(val_img_dir, img)) src_label os.path.join(LABEL_DIR, stem .txt) if os.path.exists(src_label): shutil.copy(src_label, os.path.join(val_label_dir, stem .txt)) print(ftrain: {len(train_files)}, val: {len(val_files)})固定随机种子是关键。random.seed(42) 保证每次执行得到相同的划分结果方便复现实验。我习惯把没有标签的纯背景图片也复制进目录但会在后面 yaml 里通过配置过滤或者单独标注避免模型把「没有病斑」和「漏标」混淆。这里用 shutil.copy 而不是 move保留原始数据完整。万一划分有问题还能重新跑一次脚本不需要重新解压压缩包。如果你确定原始目录不再需要改成 move 能省一半磁盘空间但 21119 张图对现代硬盘来说不算压力稳妥优先。3.2 data.yaml 配置path、train、val、names 的顺序与坑YOLOv8 的 data.yaml 是训练入口的灵魂文件字段不多但顺序敏感。path 指向 dataset 根目录train 和 val 是相对 path 的图片目录路径names 则是类别名列表顺序必须和转换脚本里的 CLASSES 完全一致。path: ./dataset train: images/train val: images/val names: 0: apple_scab 1: black_rot 2: cedar_apple_rust 3: healthy 4: frog_eye_leaf_spot这个 yaml 的坑有两个。第一个是 path 的写法用相对路径时 YOLOv8 会以当前工作目录为基准解析如果你在别的目录下执行训练命令path 就指向错误位置报错提示却只说找不到图片。第二个是 names 的顺序写反了不会报错但训练出来的模型预测结果会张冠李戴——把 black_rot 当成 apple_scabmAP 照样很高实际完全不可用。建议在训练前先写一段读取 yaml 并打印类别对应关系的校验代码确认 names 索引和 TXT 首列数字一致。这一步虽然不起眼但是避免「训练完才发现类别全错」的唯一后悔药。3.3 启动 YOLOv8 训练关键超参怎么定目录和 yaml 就绪后训练命令本身很简洁。第一次跑通建议用 yolov8n.pt 作为预训练权重参数量小、速度快先验证数据链路是否畅通再换 s 或 m 模型提精度。yolo detect train dataapple_leaf.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0这几个参数各有讲究。imgsz640 是速度和精度的平衡点苹果叶病斑普遍偏小想提精度可以试试 imgsz1280但显存占用会翻四倍。batch16 取决于显卡显存6GB 显存跑 640 分辨率配 batch 16 会比较紧张遇到 CUDA out of memory 就降到 8 或 4。device0 指定第一张显卡多卡环境可以写 device0,1。epochs100 对于 21119 张图的数据集偏保守但安全。病害检测任务里模型通常在前 50 个 epoch 内收敛到平台期后 50 个 epoch 主要看早停机制能不能帮你省时间。YOLOv8 默认不开启早停建议加上 patience20让模型在连续 20 个 epoch 验证集 mAP 不提升时自动停止省下的时间足够做多组对比实验。3.4 训练日志里看什么mAP50、混淆矩阵与过拟合信号训练启动后终端会滚动输出每一轮的指标很多人只盯着 mAP50 一个数这是不够的。我一般同时看三个信号mAP50-95 的曲线是否平滑上升、P 和 R 是否同步改善、以及 loss 的分项变化。过拟合在苹果叶病害数据上很典型。如果训练 loss 持续下降验证集 mAP 却停滞甚至下降说明模型开始死记训练集的背景纹理。这时候不要急着加数据增强先看 train/val 的划分是否泄漏了同株树照片再看是不是模型太大而数据量不够。混淆矩阵是训练结束后最值得翻的图。它会把每个类别的误判关系清楚地列出来——比如 black_rot 经常被误判成 frog_eye_leaf_spot说明这两个病害的视觉特征高度重叠这时候要考虑是不是标注边界画得太随意而不是急着换模型结构。我习惯把混淆矩阵导出来给植保专家看一眼他们往往能指出哪两类在田间本来就容易混淆这比任何调参建议都管用。4. 苹果叶病害数据集避坑清单标签、类别与小目标的四个大坑4.1 图片和标签文件名对不上样本量直接蒸发两成现象训练开始后日志提示大量图片找不到标签文件或者训练集总样本数和压缩包里图片数量对不上少了将近 20%。原因压缩包在分卷压缩或传输过程中部分文件名被系统改写更常见的是同一批图片里混有 .jpg 和 .JPG 两种扩展名Linux 环境下标签匹配是大小写敏感的导致一半标签失踪。解决先跑一遍文件系统检查把所有图片扩展名统一成小写再按 2.1 节的思路比对三个目录的文件集合。我用过一个笨但有效的办法写一段脚本统计图片名和标签名的差集直接输出缺失列表然后根据原始压缩包重新解压缺失部分而不是手动补标签。4.2 五类样本量悬殊loss 被大头类别带偏现象训练完成后查看每类的 mAP样本多的类如 apple_scab 能到 0.95样本少的 healthy 只有 0.6 左右整体 mAP 被平均得虚高。原因五类病害在田间的发生频率天然不同加上标注成本差异压缩包里各类别图片数量可能从几千到几百不等。模型在训练时把绝大多数梯度用在了大类别上小类别学不充分。解决先统计每个 cls_id 的标签行数画出类别分布柱状图。如果最大类和最小类差距超过五倍就在训练时给少数类提高 loss 权重。YOLOv8 的 class 参数可以直接传权重列表也可以对少数类做简单的复制粘贴增强多复制两遍就能明显改善。我不会推荐过度重采样病斑特征和拍摄环境强相关复制太多反而让模型过拟合背景。4.3 小病斑被标成 difficult 或漏标模型学不到小目标现象模型在验证集上的 mAP 还不错但实际检测时只能框出大块病斑早期针尖大小的病斑全都漏掉。涸泽而渔地提高置信度阈值也没用因为模型根本没把那些位置当成目标。原因苹果叶病害的早期病斑往往只有十几到几十像素标注员在 1x 缩放视图下很容易漏标另一部分被标成 difficult转换脚本又把 difficult 目标过滤掉了训练样本里压根没有小目标的正例。解决转换脚本里不要把 difficult 一刀切过滤而是单独保留用于后续分析。训练时把 imgsz 从 640 提到 1280小目标在缩放后能保留更多像素。如果显存不够可以试试把图片切成两半分别推理再合并结果代价是推理速度变慢。更激进的做法是用 SAHI 这类切片推理工具但先确认数据里小目标占比是否值得投入。4.4 同一张图同时进 train 和 val数据泄漏导致指标虚高现象训练集 mAP 和验证集 mAP 都逼近 1.0模型拿到新拍摄的果园照片却一塌糊涂检测框乱飘。原因这是划分数据集时最隐蔽的坑。同一棵树的病害照片通常是一组连拍如果直接对全部图片随机洗牌划分同株树的兄弟照片大概率同时出现在 train 和 val模型等于提前见过答案。真实场景里这些连拍照片之间只有拍摄角度和光照差异病斑位置几乎一样。解决按拍摄会话或果树个体来划分数据集而不是按单张图片。具体做法是如果文件名里有拍摄批次信息比如 IMG_20210703_001、IMG_20210703_002 这种时间前缀就把同批次的图片全部放在同一侧如果文件名没有规律只能靠聚类图像相似度来做划分。我当时在苹果叶数据上吃过这个亏重建划分后验证集 mAP 掉了十几个点但田间实测效果反而明显变好——虚高的指标是骗自己模型真正见过的数据分布差异才是它能力的度量。5. 让数据集发挥余热迁移微调与预测结果人工抽检5.1 拿训好的模型回测训练集反向清洗脏标签训练结束后用训练好的模型对全部 21119 张图做一次推理把置信度高于 0.9 但和原始标签不一致的检测框导出成图片和文本列表。这些位置大概率是标注错误比如漏标、框偏了半个身位、或者类别标错。人工快速过一遍修正后重新训练一轮通常比直接换更大的模型更提点。这一步成本不高但需要写一段推理脚本把预测结果和 GT 做 IoU 匹配只挑 IoU 小于 0.3 且置信度高的差异框。5.2 迁移到其他作物病害冻结 backbone 与按比例微调这套五类苹果叶病害的类别结构拿来训其他作物病害时可以当预训练资源但直接全量微调风险大。我的习惯是冻结前 10 层 backbone只训练 head 和 neck用较小学习率跑 30 个 epoch等验证集 mAP 不再上升后再解冻全部层用更低学习率微调。这样做的好处是避免作物叶片纹理差异太大时预训练特征被破坏同时保留数据集里学到的病斑通用视觉模式。5.3 把预测结果导出成可抽查的标注文件最后一步是把验证集预测结果导出成 XML 或者 JSON交给植保人员做人工抽检。不要只给一张画了框的图片他们看不出来哪里错了应该给出表格包含图片名、预测类别、置信度、框坐标让他们直接在表格上标出「对」「错」「漏了哪个」。这样积累两周就能统计出模型在哪些病害、哪些生长阶段最容易犯错再针对性地补拍和补标数据。这个习惯我一直保留着因为模型迭代最大的瓶颈从来不是网络结构而是对真实田间数据的理解深度。希望这套从数据到训练再到验证的流程能帮你少走几个我走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站