简介:针对农业病害识别和YOLO目标检测初学者,这份葡萄叶片病害检测数据集提供了1000张真实场景拍摄的高质量图片,覆盖不同病害类型与复杂背景,所有标注经LabelImg人工精修,并同步输出VOC(xml)、COCO(json)和YOLO(txt)三种格式标签,可直接对接YOLOv5、YOLOv8等主流模型训练,大幅简化数据准备环节。压缩包共2000个文件,主体为xml与txt标签,还包含环境搭建和训练案例的html教程、用于划分训练集/验证集/测试集的py脚本以及yaml模型配置,整体仅34.79MB,目录结构清晰,适合本地快速部署。目前已有457人学习下载,是课程设计或课题研究的实用资源。随附的划分脚本可一键生成自定义数据集,教程区分Windows与Linux两种环境,从环境搭建、参数配置到替换数据训练均有操作说明,即使刚入门也能按步骤完成葡萄叶片病害检测的完整项目实践。
1. 拿到 YOLO 葡萄叶片病害检测数据集,先搞清楚 1000 张图能做什么
解压这个 rar,你会发现里面不是一堆散图,而是一套能直接开跑的完整工程:图片、三种格式标签、划分脚本、训练教程都齐了。做葡萄叶片病害检测,最烦的不是训练本身,而是数据从哪来、标签怎么打、格式怎么对齐。这套数据包把前两步替你做了,你拿到手应该先想清楚一个问题:1000 张图,对 YOLO 目标检测来说到底够不够用?答案是,够跑通流程、够做原型验证,但离“大田里的高鲁棒性模型”还有距离。
所以这篇笔记我按自己的使用习惯来写,顺序是:先把三种标签格式看懂、验证坐标对不对,再跑划分脚本,然后改训练配置,最后才是训练和部署实战。这个流程也是 YOLO 训练自己的数据集时最标准的路线。读者里如果是刚接触目标检测的新手,跟着走一遍就能出模型;如果是熟手,重点看第三章的划分边界和第五章的踩坑记录,能省不少调试时间。
2. 三种标签格式一次给齐:VOC、COCO、YOLO 的结构差异与校验脚本
2.1 三套坐标格式,本质是同一批框的三种写法
数据包标题里写了“对应 VOC、COCO 和 YOLO 三种格式标签”,这三者到底差在哪,很多新手拿到手就懵。我拆开说。
VOC 格式是一张图对应一个 XML 文件,里面用<bndbox>节点记录xmin / ymin / xmax / ymax,单位是像素。COCO 格式是把所有图的标注汇总到一个 JSON 文件里,bbox字段记录的是[x, y, width, height],同样是像素单位,但有专门的categories和annotations结构。YOLO 格式最简单粗暴——每张图一个 txt 文件,每行五个数字:class_id x_center y_center width height,区别在于这里的前四个值全部做了归一化,除以了图片宽高。
换算关系就是:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height训练 YOLO 系列模型时,底层加载的就是 txt 格式。数据包同时给了三份,等于把 labelimg 打标、格式转换、模型训练之间的所有准备工作都做完了。你不需要自己写转换工具,但要能看懂、能验证,因为后面训练报错时,问题往往就出在这个环节。
2.2 用校验脚本核对数据包:框能不能对上原图
我拿到这种数据包,第一件事从来不是直接训练,而是先抽几张图,把三种格式解析出来的框画在原图上,肉眼确认坐标没写错。这步十分钟能做完,却能避免后面训了几个小时发现坐标全飘了的悲剧。
以 YOLO 格式为例,我一般写这样一个快速校验脚本:
import cv2 def draw_yolo_boxes(image_path, label_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, 'r', encoding='utf-8') as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: print(f'[警告] {label_path} 中存在异常行: {line.strip()}') continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) # 反归一化回像素坐标 x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite('check_result.jpg', img) print(f'已保存可视化结果: check_result.jpg')这段脚本的逻辑很直白:把 YOLO 的归一化坐标乘以图片宽高,还原成像素坐标,然后用 OpenCV 画框。异常行检测那块要留意,标签文件里如果出现空行、列数不等于 5、类别编号超出范围,都会在这一步暴露出来。常见做法是随机抽个五六张图看,不用全查。
参数说明:class_names是一个列表,下标对应类别编号,顺序必须和训练时的data.yaml完全一致,否则画出来的名字就是错的。校验 COCO 和 VOC 格式时,只需把解析段换成对应的 JSON / XML 解析逻辑,核心判断标准是一样的——框的位置和病斑位置要能对齐。
2.3 什么时候需要自己转换格式:从 VOC 转 YOLO 的通用脚本
数据包虽然三种格式都备好了,但你自己用 labelimg 打标新数据时,往往只会存成 VOC 或 YOLO 一种格式。如果只有 VOC 标注,又想在 YOLO 上训练,就得自己转换。这里给一个从 VOC XML 转 YOLO txt 的通用脚本,以后扩充数据集时直接改路径复用:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) yolo_lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) 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) # 像素坐标 -> 归一化 YOLO 坐标 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 yolo_lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') base = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(out_dir, base + '.txt') with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(yolo_lines)) print(f'已生成: {out_path}')这里要注意,class_names列表顺序就是训练时的类别编号,顺序一旦定下来就不要轻易改,否则旧标签全部作废。转换过程中如果遇到不在列表里的类名,脚本直接跳过,这是为了兼容“有些 XML 里混了背景框”的情况。实际训练时发现某类框数量异常少,优先检查是不是被这行continue过滤掉了。
3. 划分脚本:按图像切 train/val/test,保证三类标签跟着走
3.1 为什么必须按图像划分,而不是按标注文件划分
YOLO 训练前,要把数据切成训练集、验证集,测试集看需求可选。划分的基本原则是:同一张图片只能出现在一个集合里,不能既进 train 又进 val。这个道理说出来谁都懂,但用脚本批量操作时容易翻车——尤其是当标注文件的文件名和图片文件名不完全一致时,按标注文件去划分,会出现训练集里有图无标签、验证集里有标签无图的情况。
数据包自带的划分脚本,我推测是按文件名前缀做同步划分的,这是最稳妥的做法。因为 VOC 格式里 XML 文件名对应图片名,COCO 格式里image_id对应图片,YOLO 格式里 txt 文件名也对应图片名。三者能关联起来的关键就是“同一张图的文件名前缀相同”。所以划分时要同时处理 images 和 labels 两个目录,保证集合严格一致。
另外,1000 张图的数据量,划分比例我建议按 8:1:1 或者 7:2:1。不要用 9:1,因为葡萄叶片病害类内差异大,验证集太小会让 mAP 指标的波动非常剧烈,你很难判断模型是真的变好了还是随机涨跌。
3.2 一套可复用的划分脚本,支持固定随机种子
这里给一套我常用的划分脚本,逻辑是先把所有图片文件读出来,打乱后按比例切片,再同步搬运图片和三种格式标签:
import os import random import shutil from collections import defaultdict random.seed(42) # 固定随机种子,保证结果可复现 dataset_root = 'grape_dataset' images_dir = os.path.join(dataset_root, 'images') yolo_labels_dir = os.path.join(dataset_root, 'labels') train_ratio, val_ratio = 0.8, 0.1 # test_ratio 自动 = 0.1 # 1. 收集所有图片文件名(不含扩展名) all_bases = [] for fname in os.listdir(images_dir): if fname.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): base = os.path.splitext(fname)[0] all_bases.append(base) random.shuffle(all_bases) total = len(all_bases) train_end = int(total * train_ratio) val_end = train_end + int(total * val_ratio) splits = { 'train': all_bases[:train_end], 'val': all_bases[train_end:val_end], 'test': all_bases[val_end:], } # 2. 按集合分别搬运图片和 YOLO 标签 for split_name, bases in splits.items(): img_out = os.path.join(dataset_root, 'images', split_name) lbl_out = os.path.join(dataset_root, 'labels', split_name) os.makedirs(img_out, exist_ok=True) os.makedirs(lbl_out, exist_ok=True) for base in bases: for ext in ('.jpg', '.jpeg', '.png', '.bmp'): src_img = os.path.join(images_dir, base + ext) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(img_out, base + ext)) break src_lbl = os.path.join(yolo_labels_dir, base + '.txt') if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(lbl_out, base + '.txt')) else: print(f'[警告] {base} 缺少 YOLO 标签文件') print(f'{split_name}: {len(bases)} 张')代码逻辑分两步:第一步收集全部图片文件名前缀,打乱后按 8:1:1 切成三段;第二步按集合同步复制图片和 YOLO 标签到新目录。random.seed(42)这行非常关键,不固定种子的话,两次运行划分结果不同,模型对比实验就没有意义了。
3.3 类别平衡:划分后看一眼各类别分布
很多人在划分阶段就吃了一个暗亏:训练集里某个病害类别占了 70% 的框,验证集里又恰好全是另一个类,导致训练时模型严重偏科,指标虚高。我建议划分完成后,必须统计每个集合里的类别框数量。
做法很简单,遍历训练集和验证集的 txt 文件,统计每个类别编号的出现次数。如果发现某个类在验证集里数量极少甚至为零,重新设置随机种子划分一次,或者手动调整图片分配。对于 1000 张图的小数据集,类别不平衡是大概率存在的,不要等到训练完看曲线时才追悔莫及。
这个阶段还应该顺手做一个动作:确认标签里的类别编号是连续的 0 到 n-1。YOLO 训练时类别编号必须是连续的,如果原数据集标注时跳过了某个编号,比如只用了 0 和 2,训练时会直接报错或静默错位。
4. 训练教程:数据配置、参数选择和 1000 张小数据集的训练策略
4.1 写对 dataset yaml:路径和类名是训练前最后一道坎
YOLO 系列训练自己的数据集,第一步就是写数据配置文件。很多时候环境配好了、权重下了,但训练一启动就报错,九成是 yaml 写错了。yaml 的内容结构如下,以 YOLOv5 和 YOLOv8 通用为例:
# grape.yaml path: C:/datasets/grape # 数据集根目录,绝对路径最稳 train: images/train # 训练集图片目录,相对于 path val: images/val # 验证集图片目录,相对于 path test: images/test # 测试集目录,可选 names: 0: black_rot # 黑腐病 1: esca # 轮斑病 2: leaf_blight # 叶枯病配置里的坑有两个。第一,path建议写绝对路径,相对路径在换了工作目录或者用 IDE 启动训练时容易找不到文件。第二,names的类别顺序必须和标签 txt 文件里的class_id严格一致。数据包如果自带训练教程,里面大概率已经给了现成 yaml,但你别直接抄,先打开一个标签文件对照看一下:第一列数字是几,对应类别名是什么,务必确认无误。
注意,train和val指向的是图片目录,不是标签目录。YOLO 框架会自己根据图片路径去找同名的 txt 标签文件——在 images/train 同级目录下的 labels/train 里。目录结构千万别改动,否则标签找不到,训练时所有图片会被当成背景。
4.2 训练命令与参数说明:小数据集下不要盲目加 epochs
环境配置完成后,训练命令本身不长。以 YOLOv8 为例:
yolo detect train data=grape.yaml model=yolov8s.pt epochs=150 imgsz=640 batch=16 device=0 patience=20如果是 YOLOv5,命令格式略有差别,但参数含义一致:
python train.py --data grape.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 150 --patience 20这几个参数我用下来,最值得细说的是:
epochs不要一上来就设 300。1000 张小数据集,模型很容易在 80 到 100 轮就把训练集的框全记住了,后面纯属过拟合。我一般先 150 轮,配合早停机制观察。
patience是早停耐心值,表示验证集指标连续多少轮不提升就停止训练。小数据集上patience设 20 比较合适,太大会浪费时间,太小可能错过后面 mAP 50-95 缓慢爬升的机会。
imgsz用 640 是默认值。葡萄叶片病斑有不少是小目标,如果你发现模型对早期小病斑漏检严重,可以试试把推理尺寸提到 960,但代价是训练速度明显变慢,显存占用也上涨,2G 以下显存会直接爆。
batch在显存允许的范围内尽量大。6G 显存跑 YOLOv8s 建议 16,12G 以上可以到 32。batch 太小会导致 BN 层统计不稳定,loss 曲线震荡明显。
预训练权重的选择上,我一般用yolov8s起步。n太小,特征提取能力对小病斑不够;m以上在 1000 张图上容易过拟合,而且训练时间翻倍。
4.3 训练过程怎么盯:loss 曲线和 val 指标的读法
训练启动后,终端会持续打印 loss 和指标。你需要重点看两类信息:训练集 box_loss / cls_loss 是否在下降,验证集 mAP50 是否在缓慢上升。正常情况下,一次成功的训练大概在第三个 epoch 后能看到 box_loss 明显下降,mAP50 在 20 轮内从 0 快速跳到 0.6 以上。
有一个非常常见的误读:训练集 loss 还在降,就以为模型还在变好。实际上训练集 loss 肯定会降,关键是看验证集指标的走势。当 val/box_loss 开始回升或者 mAP50 连续十几轮纹丝不动,训练就该停了。YOLO 训练会自动保存best.pt和last.pt两个权重,best.pt是验证指标最好的一版,最后部署全部用它。
训练日志里如果出现大量nanloss,先别直接来问我为什么,九成是学习率太大或者数据里有异常的标注坐标——回到第二章的校验脚本,抽几张图重新看框。
5. 避坑记录:小样本病害检测最容易翻车的五个地方
5.1 现象:loss 不降、坐标框全飘,训练白跑几个钟头
我见过不止一个新手上路时,拿着 COCO 格式的 JSON 标注,用脚本草草转成 txt 就开始训练,结果 box_loss 居高不下,推理时框要么糊成一团,要么跑到叶片外面去。原因多半是坐标没有归一化,或者归一化时除以了错误的宽高。VOC/COCO 的像素坐标直接当成 YOLO 的归一化坐标用,模型当然学不会。
解决的办法就是回到第二章,随机抽三到五张训练图,用校验脚本把标签画回图上。框和病斑重合度在视觉上能接受,再开始训练。这个步骤真的值得做,它花的时间远小于一次失败训练浪费的时间。
5.2 现象:训练一启动就报文件读取错误,路径里带中文
数据包下载解压后,如果路径里带了中文目录名或者图片文件本身是中文名,YOLO 框架在读取时经常报cannot find image或者直接崩。这是因为框架底层部分依赖的库对中文路径支持不好。
解决的办法很粗暴:把整个数据包解压到纯英文路径下,比如C:/datasets/grape,所有文件夹名、文件名一律用英文和下划线。这是 YOLO 环境配置里最土但最有效的经验,血泪教训。
5.3 现象:测试集 mAP 很高,拿到大田里一拍就漏检
这是 1000 张小数据集最典型的过拟合表现。模型把训练集的背景、光照、叶片角度都背下来了,换个环境立即原形毕露。原因就是数据量太小,模型没有见过足够多的环境扰动。
解决思路有两条:一是数据增强,训练时将hsv_h、hsv_s、hsv_v扰动适当调大,模拟不同光照条件,配合随机翻转和多尺度训练,相当于把单张图的价值放大数倍。二是扩充数据集,用手机在不同天气、不同果园补拍一批图片,哪怕每类只加几十张,对泛化能力的提升都立竿见影。
5.4 现象:总 mAP 还看得过去,但某一类病斑的精度惨不忍睹
葡萄叶片病害数据里,有的病害早期症状只有几个小点,标注框很小,模型很容易忽略;有的病害样本本身就少,训练时模型对它的权重更新贡献小。这两者叠加,就会看到某类的 precision 和 recall 明显低于其他类。
解决的办法分两步。第一步是类别权重平衡,YOLO 训练时可通过修改损失函数中每个类别的权重系数来缓解,但配置繁琐,我一般不做。第二步更实际:把该类的小目标用复制粘贴增强或 mosaic 增强来强化。YOLOv8 的 mosaic 增强默认开启,它对小目标检测有明显帮助,但如果你的数据里小目标特别多,还可以设置mosaic=0.5之类让部分训练轮次用原始图,避免训练过头。
5.5 现象:labelimg 打标后训练,类别总是错一位
用 labelimg 打标时,如果你新建了多个类,它的类别编号是按创建顺序从 0 开始排的。但如果你中间删过类、重新排过序,或者导入过别人的标签文件,编号就可能错位。训练时模型输出的类别和实际病害类型对不上,你还没察觉,直到验证阶段才发现框是对的,名字全乱了。
解决的办法是在训练前写一个脚本,遍历所有标签文件,统计每个class_id的出现次数,并和data.yaml的类别表人工核对一遍。这步做完,类别错位问题基本能杜绝。相信我,在验证阶段看一张图上写着错误的病害名,比训练报错更让人崩溃。
6. 验证与部署:调阈值、导出权重,最后用肉眼抽审收尾
6.1 用 val 模式评估最佳权重,别信训练日志里的数字
训练完毕后,第一步是用best.pt在独立的测试集上跑一次评估,得到 mAP50、mAP50-95、precision、recall 四项指标作为基准线。命令非常简单:
yolo detect val data=grape.yaml model=runs/detect/train/weights/best.pt batch=32注意 val 和 test 的差别。训练过程会拿 val 集合做早停和选 best 权重,所以 val 指标天然带一点水份。如果数据包划分出了 test 目录,一定要用 test 集做最终评估,这才是模型真实水平的反映。
6.2 推理时调整 conf_thres 和 iou_thres:根据误检率做取舍
部署阶段,conf_thres(置信度阈值)和iou_thres(NMS 交并比阈值)是线上表现的两大旋钮。默认置信度阈值是 0.25,但如果场景是监控摄像头大面积扫检,误检率高得让人头疼,我一般会调到 0.4 甚至 0.5,代价是牺牲一部分小病斑的召回。反过来,如果是人工复查场景,宁可多框出来让专家筛,那阈值就降到 0.1 到 0.15。
iou_thres影响的是相邻框去重力度。葡萄叶片上病斑密集时,同一个斑可能被多个锚框击中,iou 阈值设太低会导致同一个目标画好几个框,设太高又可能把相邻的两个真实病斑合并成一个。我经验值是 0.45 到 0.5 之间,具体看密集程度适度调整。
6.3 导出 ONNX 做部署,留一套输入输出验证脚本
训练和验证都满意之后,部署到服务器或边缘设备,建议先导出 ONNX 格式,再用 ONNX Runtime 做推理验证:
from ultralytics import YOLO # 导出 ONNX 格式权重 model = YOLO('best.pt') model.export(format='onnx', imgsz=640, dynamic=True)导出后最好做一次 ONNX 输出和 PyTorch 输出的对比,确保转换没有精度损失。实际操作中如果发现两者指标差异超过一个点,优先检查输入图像的预处理是否一致——这一步看着不起眼,翻车率却很高。
我最后习惯做的一件事是用一组新拍的照片批量跑推理,把全部预测结果拼成一张大图,肉眼逐一过目。模型指标再漂亮,都不如亲眼看一下它对真实果园图片的表现。这些小图单张看没问题,拼图后能暴露出不少规律性问题,比如某个时间段的光线下叶片反光引起系统性误判。这个习惯帮我避过好几次部署后出丑的场面。
希望帮到你。
本文还有配套的精品资源,点击获取