简介这份资源面向从事植物病害识别、农业智能检测方向的目标检测学习者与开发者提供一套可直接投入训练的大型叶片病害缺陷数据集覆盖葡萄叶黑腐病、番茄叶菌斑、苹果锈叶病、马铃薯晚疫病等29个类别省去自行采集与标注的成本。包内共约2000个文件以1999个VOC格式xml标注文件和1个可视化py脚本为主压缩包约934.99MBdata目录下按训练集与验证集分文件夹存放训练集含2345张图片及对应xml验证集含239张图片及对应xml另附类别标签json字典文件。已有256人学习关注。可视化脚本无需修改即可运行随机传入一张图片便能绘制边界框并保存标注结果便于快速核验数据质量配合类别json与规范的目录结构可直接对接YOLO等主流检测框架开展训练与改进实验适合作为课程设计、科研验证或算法调优的现成数据基础。1. 植物叶片病害检测数据集29类VOC标注从拿到手到跑通第一条baseline植物叶片病害检测这个方向过去两年我从温室巡检机器人到手机端拍照问诊都碰过。真正卡住项目进度的往往不是模型结构而是数据集类别定义混乱、标注框贴边、训练集和验证集里同一片叶子反复出现导致指标虚高。这次要拆的是一份29类植物叶片病害缺陷检测数据集VOC标注格式自带训练集、验证集、类别json文件和可视化脚本。它解决的是「从零标注几千张叶片图」这个最耗人力的环节适合做农业AI质检、病害识别课程设计、以及想拿目标检测练手但缺真实场景数据的工程师。VOC格式意味着你能直接对接YOLO系列、Faster R-CNN、DETR等主流框架不用先写转换器。下面按「先看懂标注体系 → 再跑通可视化 → 再转YOLO训练 → 最后避坑」的顺序讲透。2. 拆解VOC标注与29类目录先搞清楚手里的数据长什么样2.1 VOC格式的三个核心文件和它们的关系VOC标注格式不是单个文件而是一套约定。你拿到手的数据集通常包含三个部分JPEGImages/放原图Annotations/放同名XMLImageSets/Main/放划分列表。XML里记录图片尺寸、每个目标的类别名和边界框坐标xmin、ymin、xmax、ymax。29类意味着XML里的name字段有29种取值比如leaf_spot、rust、powdery_mildew、bacterial_blight这类。类别json文件的作用是把这29个字符串映射成0到28的整数索引训练时模型只认数字。先做一件事确认XML数量和图片数量是否一致。常见翻车是图片有但XML缺失或者XML里filename和实际文件名对不上。用下面这段脚本快速体检import os import xml.etree.ElementTree as ET img_dir JPEGImages xml_dir Annotations imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))} xmls {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} print(图片数:, len(imgs), XML数:, len(xmls)) print(有图无XML:, list(imgs - xmls)[:10]) print(有XML无图:, list(xmls - imgs)[:10]) # 统计每个类别的框数量 from collections import Counter cls_counter Counter() for xf in os.listdir(xml_dir): if not xf.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xf)) for obj in tree.findall(object): cls_counter[obj.find(name).text] 1 for k, v in cls_counter.most_common(): print(k, v)逻辑说明第一段用集合差集找出不配对的文件这是最容易被忽略的脏数据来源。第二段遍历所有XML统计类别分布你会立刻看出哪些类是长尾。参数上img_dir和xml_dir按实际路径改如果图片是.JPG大写后缀lower()已经处理了。类别分布出来后如果某类少于50个框训练时要么过采样要么直接合并到相近类否则mAP会被这一类拖死。2.2 类别json文件的正确读法和索引对齐类别json一般长这样{0: leaf_spot, 1: rust, ...}或者[leaf_spot, rust, ...]。两种都要能处理。关键是训练框架里的names列表顺序必须和json完全一致差一位整个标签就全错。我一般会写一个校验函数把json读进来后和XML里出现过的类别名做交集比对import json with open(classes.json, r, encodingutf-8) as f: classes json.load(f) # 兼容dict和list两种写法 if isinstance(classes, dict): # 按key排序后取value避免json里key乱序 names [classes[k] for k in sorted(classes.keys(), keylambda x: int(x))] else: names classes print(类别数:, len(names)) print(前5类:, names[:5]) # 和XML中实际出现的类别比对 xml_names set(cls_counter.keys()) json_names set(names) print(XML有但json没有:, xml_names - json_names) print(json有但XML没有:, json_names - xml_names)逻辑说明sorted那行是血泪经验有些json的key是字符串0到28但写入顺序不保证直接list(classes.values())可能得到乱序。比对结果里如果出现「XML有但json没有」说明标注里混入了未定义类别必须手动清理或补进json。参数上int(x)要求key必须是数字字符串如果key是类别名本身改成直接取values即可。2.3 可视化脚本怎么用先看框再谈训练数据集自带可视化脚本是好事但很多人直接跑完看一眼就过了。我建议把可视化当成标注质量检查工具。核心逻辑是读XML画矩形和类别名用OpenCV或PIL都行。下面是一个可复用的最小版本import cv2 import xml.etree.ElementTree as ET import os def draw_voc(img_path, xml_path, save_path): img cv2.imread(img_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bbox obj.find(bndbox) x1 int(float(bbox.find(xmin).text)) y1 int(float(bbox.find(ymin).text)) x2 int(float(bbox.find(xmax).text)) y2 int(float(bbox.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, name, (x1, max(y1 - 5, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(save_path, img) # 批量跑前20张 for i, xf in enumerate(os.listdir(Annotations)[:20]): stem os.path.splitext(xf)[0] for ext in [.jpg, .png, .jpeg]: ip os.path.join(JPEGImages, stem ext) if os.path.exists(ip): draw_voc(ip, os.path.join(Annotations, xf), fvis_{i}.jpg) break逻辑说明float()转换是因为部分XML里坐标写成123.0这种浮点字符串直接int()会报错。max(y1-5, 10)防止类别名画到图片外面。跑完重点看三种情况框是否贴边到像素级贴边框在缩放增强后会丢目标、同一片叶子是否被拆成多个框、类别名是否和视觉症状对得上。参数上矩形颜色和线宽随意但建议固定方便批量对比。3. 从VOC转YOLO格式转换脚本、路径规则和三个必调参数3.1 转换的核心公式和目录结构YOLO格式每张图对应一个.txt每行是class_id cx cy w h全部归一化到0到1。转换公式cx (xmin xmax) / 2 / img_wcy (ymin ymax) / 2 / img_hw (xmax - xmin) / img_wh (ymax - ymin) / img_h。目录结构建议直接按YOLO惯例来images/train、images/val、labels/train、labels/val。训练集和验证集的划分如果数据集已经给了ImageSets/Main/train.txt和val.txt直接读不要自己随机分否则验证集里出现训练集同株叶片的近重复图指标会虚高。import os import xml.etree.ElementTree as ET import shutil def voc_to_yolo(xml_path, img_w, img_h, class_names): tree ET.parse(xml_path) lines [] for obj in tree.findall(object): name obj.find(name).text if name not in class_names: continue cid class_names.index(name) b obj.find(bndbox) x1 float(b.find(xmin).text) y1 float(b.find(ymin).text) x2 float(b.find(xmax).text) y2 float(b.find(ymax).text) # 裁剪到图像边界内 x1, y1 max(0, x1), max(0, y1) x2, y2 min(img_w, x2), min(img_h, y2) cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h if w 0 or h 0: continue lines.append(f{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines # 读取图片尺寸需要PIL from PIL import Image def get_size(img_path): with Image.open(img_path) as im: return im.size # (w, h)逻辑说明max(0, x1)和min(img_w, x2)是必须的VOC标注里偶尔出现负坐标或超出图像宽高的框不裁剪会导致归一化后值大于1YOLO训练时直接报错或学出诡异框。w 0过滤掉零面积框。参数上class_names必须和json顺序一致:.6f保留6位小数足够再多没必要。3.2 划分训练集和验证集时最容易犯的错如果数据集没有给划分文件自己分的时候千万别用random.shuffle一刀切。植物叶片病害有个特点同一株、同一片叶、同一发病阶段可能拍了多张。正确做法是按「株」或按「拍摄批次」分组后再分。简单可操作的方案是如果文件名里带株号或日期按这个字段分组如果没有至少保证验证集占20%且类别分布和整体接近。下面这个分层抽样脚本可以抄import random from collections import defaultdict def split_by_class(xml_dir, val_ratio0.2, seed42): random.seed(seed) # 每张图的主类别 框最多的类别 img_main_cls {} for xf in os.listdir(xml_dir): if not xf.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xf)) cnt defaultdict(int) for obj in tree.findall(object): cnt[obj.find(name).text] 1 if cnt: img_main_cls[xf] max(cnt, keycnt.get) # 按主类别分组 groups defaultdict(list) for xf, c in img_main_cls.items(): groups[c].append(xf) train, val [], [] for c, files in groups.items(): random.shuffle(files) n_val max(1, int(len(files) * val_ratio)) val.extend(files[:n_val]) train.extend(files[n_val:]) return train, val逻辑说明max(cnt, keycnt.get)取框最多的类别作为图片主类别这样分层后每个类别在验证集里都有代表。max(1, ...)保证长尾类至少有一张进验证集否则那一类的mAP无法计算。参数上val_ratio一般0.2数据量小于500张时用0.3seed固定住方便复现。3.3 生成YOLO训练用的data.yaml转换完最后一步是写data.yaml这是YOLOv5/v8训练的入口文件。格式如下path: /home/user/leaf_dataset train: images/train val: images/val nc: 29 names: 0: leaf_spot 1: rust 2: powdery_mildew # ... 补齐29类path是数据集根目录train和val是相对路径。nc必须等于names长度。常见翻车是names写成一行列表但缩进错了YAML对缩进敏感。另一个坑是path用了相对路径但训练时工作目录变了建议写绝对路径。改完用python -c import yaml; print(yaml.safe_load(open(data.yaml)))验证一下能否解析。4. 用YOLOv8跑通第一条baseline命令、参数和指标解读4.1 环境准备和最小训练命令假设你已经装好ultralytics一条命令就能起训yolo detect train \ data/home/user/leaf_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ workers4 \ projectleaf_runs \ namebaseline逻辑说明modelyolov8n.pt用nano版先跑通别一上来上x版显存和时间都吃不消。imgsz640是默认值叶片病害目标通常不大如果标注框普遍小于32像素考虑提到imgsz1024但batch要相应降到8或4。workers4在Linux上够用Windows上设0避免多进程报错。project和name决定输出目录跑完在leaf_runs/baseline/weights/best.pt拿最优权重。4.2 三个必调参数imgsz、batch、学习率imgsz直接决定小目标能否被检测到。叶片病斑在整图里可能只占几十像素640下经过下采样后特征几乎消失。我的经验是统计所有标注框的宽高分布如果中位数小于50像素imgsz至少1024。代价是显存翻倍batch要减半。batch不是越大越好。小数据集上大batch容易过拟合且BN层统计不稳定。29类、几千张图的规模batch16或8比较稳。如果显存不够用batch8配合accumulate2模拟16。学习率默认lr00.01对YOLOv8的SGD优化器偏高小数据集上常见loss震荡。改成lr00.001配合cos_lrTrue余弦退火收敛曲线会平滑很多。如果10个epoch后mAP还不涨先查学习率再查数据。4.3 看指标时别只看mAP50训练日志里重点看四个box_loss、cls_loss、mAP50、mAP50-95。box_loss降不下去说明框回归有问题多半是标注框质量差或imgsz太小。cls_loss高但box_loss低说明框对了但类别分不清29类里可能有视觉相似的类比如不同真菌引起的斑点这时候要么合并类要么加数据。mAP50高但mAP50-95低说明框的位置不够准IoU阈值一提高就掉。这在叶片病害里很常见因为病斑边界本来就模糊不同标注者对边界理解不一致。解决办法不是调模型而是统一标注规范病斑框到底包不包含边缘黄晕这个必须在标注阶段定死。验证集指标异常高比如mAP500.95时先别高兴检查验证集是否和训练集有重复图。用图片哈希去重import hashlib def file_hash(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest() train_hashes {file_hash(os.path.join(images/train, f)) for f in os.listdir(images/train)} val_hashes {file_hash(os.path.join(images/val, f)) for f in os.listdir(images/val)} print(重复图数量:, len(train_hashes val_hashes))逻辑说明MD5对图片内容做哈希完全相同的图会得到相同值。如果重复数大于0说明划分有问题必须重新分。参数上如果图片经过不同压缩MD5不同但视觉相同这种情况要用感知哈希但VOC数据集一般不会。5. 避坑与排查29类叶片病害数据集上最容易翻车的5件事5.1 现象训练loss正常但mAP一直是0原因类别json的索引和data.yaml的names顺序不一致模型学的是错位标签。或者XML里类别名有空格、大小写差异比如Leaf_Spot和leaf_spot被当成两类。解决跑一遍2.2节的比对脚本确保XML类别集合和json完全相等。再用print(names)确认data.yaml里的顺序和json一致。大小写问题统一用.strip().lower()清洗。5.2 现象验证集mAP远高于训练集原因验证集里混入了训练集的近重复图或者验证集太小且类别分布偏斜。植物叶片同一株拍多张的情况极常见。解决用4.3节的哈希去重再检查划分是否按株分组。如果数据集自带的ImageSets/Main/val.txt有问题自己按3.2节重新分层抽样。5.3 现象小病斑全部漏检原因imgsz太小病斑经过骨干网络下采样后特征消失。或者标注框本身太小YOLO的anchor匹配不上。解决先统计框尺寸分布中位数小于50像素就把imgsz提到1024。如果显存不够改用YOLOv8的rectTrue矩形训练减少padding或者换用带P2小目标检测层的模型配置。标注框小于8像素的建议直接删掉这种框连人都标不准。5.4 现象某些类别AP为0但该类明明有样本原因该类在验证集里没有实例或者实例数太少1到2个AP计算不稳定。也可能是该类在训练集里被过采样后模型过拟合验证集上反而崩。解决检查3.2节分层抽样是否保证每类至少一张进验证集。如果某类总样本少于30考虑合并到语义相近的类或者用重复采样把训练集补到50以上。验证集里该类至少要有5个实例AP才有参考意义。5.5 现象训练到一半loss突然变NaN原因学习率太高导致梯度爆炸或者某张图的标注框归一化后值异常比如w1.5。VOC转YOLO时如果没做边界裁剪就会出现这种情况。解决先把lr0降到0.0005加grad_clip10.0。然后跑一遍转换后的labels检查用下面脚本找出非法值import os for lf in os.listdir(labels/train): with open(os.path.join(labels/train, lf)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(格式错误:, lf, line) continue vals [float(x) for x in parts[1:]] if any(v 0 or v 1 for v in vals): print(越界:, lf, line)逻辑说明YOLO格式要求所有坐标在0到1之间越界值会让损失函数计算出NaN。参数上len(parts) ! 5还能抓出行数不对的脏标注。跑完把有问题的图从训练集剔除或重新标注。6. 进阶技巧用可视化脚本反查标注质量把mAP再提5个点数据集自带的可视化脚本大多数人只用来「看一眼」其实它可以变成标注质量审计工具。我的习惯是训练前把训练集全部可视化一遍用脚本自动标记出三类可疑框——面积占比小于0.1%的、宽高比大于5的、以及和其他框IoU大于0.7的。这三类分别对应「太小无意义」「细长误标」「重复标注」。下面这个审计脚本可以直接跑import os import xml.etree.ElementTree as ET def audit_xml(xml_path, img_w, img_h): tree ET.parse(xml_path) boxes [] issues [] for obj in tree.findall(object): name obj.find(name).text b obj.find(bndbox) x1 float(b.find(xmin).text) y1 float(b.find(ymin).text) x2 float(b.find(xmax).text) y2 float(b.find(ymax).text) w x2 - x1 h y2 - y1 area_ratio (w * h) / (img_w * img_h) if area_ratio 0.001: issues.append(f过小框: {name} area{area_ratio:.4f}) if max(w / h, h / w) 5: issues.append(f细长框: {name} w{w:.0f} h{h:.0f}) boxes.append((x1, y1, x2, y2, name)) # 检查重复框 for i in range(len(boxes)): for j in range(i 1, len(boxes)): ax1, ay1, ax2, ay2, an boxes[i] bx1, by1, bx2, by2, bn boxes[j] ix1, iy1 max(ax1, bx1), max(ay1, by1) ix2, iy2 min(ax2, bx2), min(ay2, by2) if ix2 ix1 and iy2 iy1: inter (ix2 - ix1) * (iy2 - iy1) union (ax2 - ax1) * (ay2 - ay1) (bx2 - bx1) * (by2 - by1) - inter if inter / union 0.7: issues.append(f重复框: {an} 与 {bn} IoU{inter/union:.2f}) return issues # 批量审计 from PIL import Image for xf in os.listdir(Annotations): if not xf.endswith(.xml): continue stem os.path.splitext(xf)[0] for ext in [.jpg, .png, .jpeg]: ip os.path.join(JPEGImages, stem ext) if os.path.exists(ip): with Image.open(ip) as im: w, h im.size iss audit_xml(os.path.join(Annotations, xf), w, h) if iss: print(stem, iss) break逻辑说明area_ratio 0.001对应640图中约20x20像素以下的框这种框在训练中贡献极小但会拉低精度。max(w/h, h/w) 5抓细长误标叶片病斑一般是团状细长框多半是把叶脉或边缘当成了病斑。IoU大于0.7的重复框直接合并或删一个。参数上0.001和5这两个阈值可以根据你的数据调整但建议先跑一遍看数量如果过小框占比超过10%说明标注规范有问题不是模型能救的。审计完把问题图挑出来重新标再训练一轮mAP50通常能涨3到5个点。这个提升不是模型带来的是数据质量带来的。我自己的习惯是任何新数据集到手先跑可视化审计再跑baseline最后才调参。顺序反了就是浪费时间。另外29类里如果有个别类始终学不好别急着加数据先看那类的框是不是普遍偏小或边界模糊有时候合并两个视觉相近的类比硬分29类更划算。希望帮到你。本文还有配套的精品资源点击获取