ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

苹果叶病害数据集实战:VOC/YOLO双格式标签处理与YOLOv8训练避坑

苹果叶病害数据集实战:VOC/YOLO双格式标签处理与YOLOv8训练避坑 简介这是一份面向农业目标检测与计算机视觉任务的高质量苹果叶病害识别数据集包含苹果斑点落叶病、褐斑病、花叶病、灰斑病、锈病5类常见病害适用于训练VOC与YOLO格式的目标检测模型也适合作为毕业设计、农业智能识别系统或算法benchmark测试的基准数据。资源包共整合2000个文件压缩包约713.92MB内含21119张JPG原图、21119个XML标注文件及21120个TXT标签文件分别对应VOC格式和YOLO格式的标注信息可满足不同检测框架的输入要求。全部图片来自果园实地采摘拍照并经过数据增强与精准标注覆盖不同光照、角度和生长阶段有助于提升模型的泛化能力。资源已有472人学习下载作者承诺持续更新优化目录按类别组织便于模块化调用适合农业AI开发者、科研人员及目标检测学习者直接使用。1. 21119张苹果叶病害图的底气VOC与YOLO双格式标签到手能跑做果园巡检或大棚病害识别第一道坎从来不是模型而是数据。这份5类苹果叶病害识别检测数据集21119张-vocyolo格式标签解压开就是jpg配xml再配txt五类苹果叶病害的边界框标注一套VOC一套YOLO图片总量21119张。它解决的是最磨人的采集和标注环节不用再顶着太阳去果园拍叶子也不用对着LabelImg一框框点到手酸拿到手就能直接喂给YOLOv5或YOLOv8开始训练。适合三类人准备做农业视觉落地的工程师、要快速跑通检测流程的研究生、以及需要一份现成数据做算法横向对比的算法岗。这里有个反直觉的点双格式不是平台送的冗余两套标签互相核对能提前暴露很多转换脚本才有的低级错误后面会展开说。2. 标签格式解剖VOC与YOLO的字段映射以及目录对齐第一关2.1 为什么同时给两套标签双格式不是冗余是互检Pascal VOC格式用xml存标注是人类可读的通用语言LabelImg、CVAT、labelme这些标注工具都能直接打开YOLO格式则是纯文本txt每行五个数字训练时读取极快。市面上不少数据集只给其中一种格式拿到手还得自己写转换脚本这一步耽误半天是常事。这份资源同时给了两套等于把转换环节替你做了。双格式在这里的真正价值是互检。同一张图xml和txt描述的是同一个目标只是坐标系和表达方式不同。如果某次转换脚本有bug比如忘了归一化、把x和y对调单看一种格式根本发现不了但两套一对照就露馅。我拿到任何数据集都会做这个对照动作后面3.2的脚本就是按这个思路写的。以下这张表可以快速建立两种格式的映射关系内容VOCxmlYOLOtxt文件名filename节点与图片同名仅后缀不同图像宽高size/width、size/height不需要训练时自动读取类别object/name字符串class_id整数从0开始目标坐标bndbox对角点绝对像素归一化中心点宽高单个目标表示一个object节点一行2.2 目录结构长什么样一张图在xml和txt里的两种身份解压后先用find命令把目录结构摸一遍避免凭经验猜路径。find . -maxdepth 2 -type d | sort常见结构是下面这样实际包内命名可能有出入但逻辑一致apple_leaf_dataset/ ├── images/ # 原始图片 jpg │ ├── train/ │ └── val/ ├── annotations/ │ ├── VOC/ # 每张图一个同名 xml │ └── YOLO/ # 每张图一个同名 txt ├── classes.txt └── data.yaml拿一张图举例它的VOC标签是这样的annotation filenameA_001.jpg/filename size width640/width height480/height /size object nameapple_scab/name bndbox xmin120/xmin ymin80/ymin xmax300/xmax ymax260/ymax /bndbox /object /annotation同一张图的YOLO标签只有一行0 0.328125 0.354167 0.281250 0.375000换算一下就懂了。x_center(120300)/2/6400.328125y_center(80260)/2/4800.354167宽度(300-120)/6400.28125高度(260-80)/4800.375。坐标原点都在图片左上角YOLO的归一化不涉及方向翻转这个映射关系是后面所有脚本的地基。2.3 文件名对齐第一关图像、XML、TXT三者必须数量一致YOLO训练时只按文件名stem配对图片和txt。如果某张jpg少了txt训练不会报错只是这张图不参与损失计算等于数据量悄悄变少还没人知道。更麻烦的是错位一张图配了另一张图的标签模型直接学歪。所以我拿到数据集第一件事就是做集合对齐检查。from pathlib import Path img_dir Path(images) xml_dir Path(annotations/VOC) txt_dir Path(annotations/YOLO) img_suffix {.jpg, .jpeg, .png, .bmp} imgs {p.stem for p in img_dir.rglob(*) if p.suffix.lower() in img_suffix} xmls {p.stem for p in xml_dir.glob(*.xml)} txts {p.stem for p in txt_dir.glob(*.txt)} print(图片数:, len(imgs)) print(xml数:, len(xmls)) print(txt数:, len(txts)) print(缺xml:, len(imgs - xmls)) print(缺txt:, len(imgs - txts)) print(多出来的txt:, len(txts - imgs))逻辑说明rglob会递归查找所有子目录避免图片散落在子文件夹里被漏掉后缀统一转小写再判断防止“JPG”和“jpg”这种大小写不一致导致的误判。参数上三个集合分别来自图片目录、VOC目录、YOLO目录差集为空的才是健康状态。如果缺txt的图片超过个位数大概率是导出标签时中断过这种情况直接去补对应的txt不要带着缺失开训练。3. 数据质量体检脏标签、坏框与类别失衡的三种排查脚本3.1 为什么先体检而不是直接丢给训练农业病害数据集有三大通病误标、漏标、坏框。标注员可能把虫咬的斑点当病斑框了进去叶片边缘的病斑被切了一半标注时没补框还有xmax比xmin还小的颠倒坐标。这类脏标签进了训练就是个黑匣子loss照样下降但模型学的是噪声等到部署时才发现某类目标就是识别不准回头排查数据要花更多时间。另外有个现实问题这类数据集的标注质量差异极大有的是作者认真核过有的是机器粗标不做体检等于开盲盒。所以我的习惯是任何数据集到手先跑一遍统计脚本看看各类别框数分布、有没有非法框再花十分钟抽看几十张图。这一步做完数据能不能用基本心里有数。3.2 解析XML统计类别分布与非法框下面这个脚本读全部xml统计每类目标的框数并把非法框导出成csv方便逐条回看。import csv import xml.etree.ElementTree as ET from collections import Counter from pathlib import Path xml_dir Path(annotations/VOC) out_csv Path(bad_boxes.csv) cat_counter Counter() bad_boxes [] total_boxes 0 for xml_file in sorted(xml_dir.glob(*.xml)): tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) for obj in root.iter(object): name obj.find(name).text cat_counter[name] 1 total_boxes 1 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) if xmax xmin or ymax ymin: bad_boxes.append([xml_file.name, name, 颠倒, xmin, ymin, xmax, ymax]) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: bad_boxes.append([xml_file.name, name, 越界, xmin, ymin, xmax, ymax]) print(总框数:, total_boxes) for cat, n in cat_counter.most_common(): print(f{cat}: {n} 个占比 {n / total_boxes:.1%}) if bad_boxes: with out_csv.open(w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([xml, class, 问题, xmin, ymin, xmax, ymax]) writer.writerows(bad_boxes) print(非法框已写入, out_csv) else: print(没有非法框)逻辑说明图像的宽高直接从xml的size节点读取不拿真实图片来比对是因为如果标注时分辨率记错了这里能直接暴露问题。非法框分两类判断坐标颠倒意味着标注时拖拽方向反了越界意味着标注或后期裁剪出错。输出方面类别占比低于5%的类要重点关注说明样本量太少总框数除以xml文件数就是平均每图框数如果只有0.5说明大量图片可能漏标。如果你只信txt也可以做一份txt维度的体检。YOLO格式比xml更精简检查字段数、类别ID范围和归一化坐标是否越界就行。from pathlib import Path nc 5 # 按包内实际类别数调整 bad_txt [] for txt_file in Path(annotations/YOLO).glob(*.txt): for i, line in enumerate(txt_file.read_text(encodingutf-8).strip().splitlines()): parts line.split() if len(parts) ! 5: bad_txt.append((txt_file.name, i, 字段数不为5)) continue cid int(float(parts[0])) cx, cy, w, h map(float, parts[1:]) if not (0 cid nc): bad_txt.append((txt_file.name, i, 类别ID超范围)) if w 0 or h 0: bad_txt.append((txt_file.name, i, 宽高非正)) if not (0 cx 1 and 0 cy 1): bad_txt.append((txt_file.name, i, 中心点归一化越界)) print(txt异常条数:, len(bad_txt)) for item in bad_txt[:20]: print(item)逻辑说明class_id必须在[0, nc)区间内否则训练时类别索引越界会直接报错w或h小于等于0是训练中loss变NaN的常见元凶这一条在坑1还会碰到中心点或宽高超出[0,1]说明转换时没有按原图尺寸归一化。txt和xml两边跑出来的异常在同一个文件上基本就能定位是哪一步出了问题。3.3 可视化抽检OpenCV批量画框人眼扫一遍统计脚本只能发现数值层面的问题漏标和误标必须靠肉眼。下面这个脚本把xml框画到图上按类别分层随机抽样输出到check目录。import random import cv2 import xml.etree.ElementTree as ET from pathlib import Path xml_dir Path(annotations/VOC) img_dir Path(images) out_dir Path(check) out_dir.mkdir(exist_okTrue) class_id_map { apple_scab: 0, cedar_apple_rust: 1, frogeye_leaf_spot: 2, mosaic: 3, healthy: 4, } def draw_one(xml_path, img_path, out_path): img cv2.imread(str(img_path)) if img is None: return tree ET.parse(xml_path) for obj in tree.getroot().iter(object): name obj.find(name).text box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) label fcls-{class_id_map.get(name, -1)} cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, label, (xmin, max(0, ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(str(out_path), img) xml_files list(xml_dir.glob(*.xml)) sampled random.sample(xml_files, min(50, len(xml_files))) for xml_file in sampled: img_path img_dir / f{xml_file.stem}.jpg if img_path.exists(): draw_one(xml_file, img_path, out_dir / img_path.name) print(抽检图已输出到, out_dir)逻辑说明画框颜色统一用绿色类别标签用红色文字位置放在框左上角上方ymin减5是为了避免文字和框线重叠。为什么类别显示成“cls-0”而不是中文名因为cv2.putText默认字体不支持中文写中文会渲染成问号要么映射成英文ID要么换PIL的ImageFont来写。人眼扫图时重点看三件事框是否贴合病斑边界、是否把健康叶片也框了进来、有没有明显的大面积病斑漏标。五十张图扫下来这套数据的标注水平基本就有结论了。4. VOC转YOLO与数据集划分转换函数、8:1:1切分和data.yaml组装4.1 转换核心逻辑归一化坐标与越界裁剪网上转换脚本不少但大多年久失修不处理越界框对中文路径支持也差。自己写一个二十行的函数最可控。核心点有两个一是坐标归一化二是越界裁剪必须先于归一化执行否则裁剪出来的可能是负值。import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, class_to_id, out_txt): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text 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) xmin max(0.0, xmin) ymin max(0.0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_to_id[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) Path(out_txt).write_text(\n.join(lines), encodingutf-8)逻辑说明根节点的size/width和size/height是归一化分母缺失这个节点直接报错所以转换前务必确认xml结构完整。裁剪用max/min夹紧把越界坐标拉回图像边界内如果裁剪后宽高变成非正说明这个框原始标注就是坏的直接跳过而不是生成零宽框否则训练时就是坑1。文本格式用f{:.6f}保留六位小数避免科学计数法写进txt。class_to_id字典来自classes.txt或data.yamlid必须是连续的整数从0开始。如果你以后要转DOTA这种旋转框格式核心思路一模一样只是每行多一个角度字段。4.2 训练集划分固定随机种子按8:1:1切分切分这事看着简单翻车点都在细节上。不固定随机种子跑两次实验数据分布不一样结果没法对比不按stem把图片和txt一起搬标签就丢了。下面这个脚本一次做完。import random import shutil from pathlib import Path src_img Path(images) src_txt Path(annotations/YOLO) dst Path(dataset) for split in [train, val, test]: (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) random.seed(42) all_stems [p.stem for p in src_img.glob(*.jpg)] random.shuffle(all_stems) n len(all_stems) train_stems all_stems[: int(n * 0.8)] val_stems all_stems[int(n * 0.8): int(n * 0.9)] test_stems all_stems[int(n * 0.9):] for split, stems in [(train, train_stems), (val, val_stems), (test, test_stems)]: for stem in stems: img_src src_img / f{stem}.jpg txt_src src_txt / f{stem}.txt if img_src.exists() and txt_src.exists(): shutil.copy2(img_src, dst / images / split / f{stem}.jpg) shutil.copy2(txt_src, dst / labels / split / f{stem}.txt)逻辑说明shuffle之后再切片避免按文件名排序导致同一批次的病种高度集中。seed固定为42下次重跑得到完全一样的划分这是实验可复现的基本要求。复制用shutil.copy2而不是shutil.move保留原始文件万一划分有问题还有后悔药。注意这里假设图片全部是jpg如果包内有png或jpeg把glob换成rglob加后缀过滤否则jpg之外的图会静默丢失。一个进阶细节如果图片文件名带树ID前缀比如tree01_leaf_001.jpg建议按前缀分组再切分防止同一棵树的叶片同时出现在训练集和验证集里。论文评审对这个数据泄露问题很敏感虽然这份数据是否按树组织不确定但文件名看一眼就知道有前缀就按分组切。4.3 组装data.yaml类别顺序决定训练结果切分完成后按YOLOv8的目录规范整理然后写data.yaml。mkdir -p dataset/images/{train,val,test} dataset/labels/{train,val,test}path: /home/user/apple_leaf_dataset train: images/train val: images/val test: images/test nc: 5 names: 0: apple_scab 1: cedar_apple_rust 2: frogeye_leaf_spot 3: mosaic 4: healthy逻辑说明path是绝对路径train/val/test写相对路径YOLOv8会拼成完整路径去读取。names顺序必须和txt里的class_id严格对应这里的class 0不是“第一个类别名”而是所有txt里标为0的那个类。最容易翻车的点就是把names顺序写错训练不报错但推理结果张冠李戴解释都解释不清。上面这组类别名是苹果叶病害常见的五类组合具体以包内的classes.txt为准先打开看一眼再抄进yaml。提示VOC和YOLO的坐标原点都在左上角归一化不涉及方向翻转不要画蛇添足做水平镜像之类的额外处理。5. 避坑指南训YOLOv8时这几个翻车点一次说清5.1 训练过程中的坑BN崩溃与混淆矩阵总和异常坑1loss变成NaN典型表现就是训练中bn崩溃。现象YOLOv8训练到20到40轮之间loss突然变成nan之后所有epoch的loss全是nanmAP直接归零看起来像模型炸了。 原因最常见的是标签里有宽度或高度为0的框这种框在损失计算时产生除零或梯度异常最终引爆BN层。第二常见是初始学习率太大、batch太大前几个epoch梯度就飞了。 解决先用3.2的txt体检脚本查一遍零面积框有就过滤或删除batch从16降到8学习率从0.01降到0.001并把warmup_epochs设到5让模型先小步试探再进入正式学习。如果还炸检查txt里有没有负数坐标YOLO格式里任何负值都是异常。坑2混淆矩阵总和怎么加都对不上。现象验证集GT框明明有三千个混淆矩阵每个类列的合计却对不上而且每次跑出来的数还不一样。 原因混淆矩阵默认按置信度阈值统计低于阈值的预测不会计入另一个常见原因是data.yaml的names顺序和txt的class_id错位同一个类别被拆成了两行显示。 解决先确认names顺序再打印混淆矩阵时把conf阈值调到0.001让它把低置信度预测也算进来同时注意横轴是真实标签还是预测标签YOLO混淆矩阵的行列语义在不同版本里有差异拿反了方向看到的数字自然对不上。5.2 数据层面的坑文件名错位、小目标漏检与类别不均衡坑3图片和标签文件名错位训练过程毫无察觉。现象loss下降正常但抽检验证集可视化时发现框画在错误目标上类别张冠李戴。 原因jpg和txt只靠stem配对复制文件时大小写不一致AppleScab.jpg配applescab.txt或xml转txt时漏生成了部分文件都会导致错位。YOLO训练不校验配对关系缺文件只是少算损失错位就变成噪声。 解决切分前跑一遍2.3的集合对齐脚本切分后再跑一遍做二次确认移动文件用脚本批量操作不要手工拖拽人一多手一滑就错位。坑4小目标漏检严重病斑只有几十像素。现象比如褐斑病这类小斑点的AP明显低于其他类漏检率居高不下。 原因苹果叶病害很多是小斑点而图片是整株叶片的高分辨率大图原图直接resize到640小目标特征被压缩没了。 解决先统计目标框面积占图片面积的比例如果低于0.5%就走tiling切图路线。把大图切成多个512x512子图子图边界处的框做裁剪并过滤掉面积损失过多的训练后推理时把子图坐标偏移量加回去拼成原图坐标。这套流程对苹果叶病害这类小目标是最有效的提升手段。坑5类别不均衡小类AP被大类压制。现象五类样本量差距拉大某类占总数四成多另一类只有4%小类的precision和recall都明显偏低。 原因模型见过太多大类样本决策边界天然偏向大类。 解决最省事的是对少数类做针对性数据增强HSV扰动、旋转、mosaic都可以工程上也可以按类复制少数类样本到训练集但复制后要确认验证集里没有同样图片否则指标虚高。YOLOv8没有直接的class weight字段想用权重方案得改Dataset里sampler的实现一般先试增强就够了。6. 用YOLOv8验证这份数据上手命令、三张图诊断与小目标切图数据和标签都检查完直接开跑。用nano版本先试30轮n模型参数量小、跑得快数据和标签有硬伤会在前30轮集中暴露。批次和学习率按常规配置先求一个“能收敛”的结果再谈精度。yolo detect train datadata.yaml modelyolov8n.pt epochs30 imgsz640 batch16参数说明model用yolov8n.pt而不是随机初始化COCO预训练权重已经学好了通用边缘和纹理特征迁移到叶片病斑上十轮内就能超过随机初始化三十轮。epochs先设30是让你快速看趋势的不是最终训练轮数。训练前提前把yolov8n.pt权重下载好避免跑起来才发现权重缺失中断作业。训练结束之后进到runs/detect/train目录看三张图就够了。第一张results.png关注train和val两条loss曲线是否同向下降如果val loss中途反弹而train还在跌说明标签噪声大或者已经过拟合回到第3章查非法框和漏标。第二张confusion_matrix.png看哪些类别互相混比如锈病和褐斑病混在一起说明这两个病斑在像素形态上太接近后续要么补细粒度标注要么考虑增加这一类的样本。第三张PR_curve.png看每类的AP50曲线五类如果都能越过0.7这套数据处理得就没问题某类低于0.5按第5章的坑4和坑5排查。确认数据没问题之后再谈进阶。一个做法是换大模型继续训练从yolov8s.pt或yolov8m.pt开始迁移学习的收益在细粒度分类上尤其明显。另一个做法是tiling上文提到的切图方案适合病斑占图面积很小的场景。它的代价是训练和推理时间成倍增加换取的是小目标召回率要不要用取决于你的部署设备算力。我自己在这类数据集上翻过车第一次跳过热身直接拿大模型跑结果某类AP低得没法看返工查数据花了两天最后定位到是零宽框把loss搞崩了数据体检十分钟就能拦住的事。从那以后我每次换数据集都强制走一遍流程——体检、对齐、小模型试跑、看三张图再上大模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表