简介:面向茶叶病害检测与识别任务的数据集,内含9591张茶叶图像的Pascal VOC与YOLO双格式标注,覆盖茶黑腐病、茶褐枯病、茶锈病、红蜘蛛为害叶、茶小绿叶蝉为害叶、健康茶叶、茶白星病及未分类病害共8个类别,可直接用于YOLO系列目标检测模型的训练与评估。资源包共2000个文件,以VOC格式XML标注文件为主体,并搭配YOLO格式txt标注及说明文本,压缩后约102MB,目录结构清晰便于批处理解析。已有1223人学习浏览,适合从事茶树病害监测、智慧农业视觉分析场景的开发者或研究者使用。下载后可快速获得图像与标注的对应关系,省去从零整理标签格式的耗时,也有助于训练多类别病害分类器,支撑病害预警与田间管理决策。
1. 茶叶病害检测数据集:9591 张 VOC+YOLO 双格式,拿来就能训
做农业视觉检测的人都有体会:找数据集最折磨人的往往不是图不够,而是标注格式不统一。有的给 VOC XML,有的只给 COCO JSON,还有的给一堆剪裁好的图,类别名全是拼音缩写。这份茶叶病害检测数据集把这事省了——9591 张叶片图像,8 个类别,压缩包里同时放了 VOC 和 YOLO 两套标注,解压后 YOLOv5、YOLOv8 直接开训,不用再写格式转换。适合做茶叶病害识别落地的工程人员、想换一份规整数据练目标检测流程的新手,以及需要标注基准做算法对比的研究者。下面我把目录结构、标签对应、训练参数和踩坑记录逐条拆开讲。
2. 八类病害标签与双格式对应:看清标注再动手
2.1 8 个类别怎么定义,先背下来
拿到数据集第一件事不是解压,而是先看类别表。这份数据的 8 个类别对应茶叶生产里最常见的叶部病害,外加一个健康类,具体如下:
| class_id | VOC/YOLO 标签名 | 中文名 | 病斑特征 |
|---|---|---|---|
| 0 | healthy | 健康叶片 | 无明显病斑 |
| 1 | anthracnose | 茶炭疽病 | 褐色近圆形病斑,边缘有黄晕 |
| 2 | blister_blight | 茶饼病 | 叶面凹陷、叶背凸起,幼叶高发 |
| 3 | white_scab | 茶白星病 | 灰白色小圆点,散生 |
| 4 | red_leaf_spot | 茶赤叶斑病 | 不规则红褐色斑块 |
| 5 | gray_blight | 茶轮斑病 | 同心轮纹,外围有晕圈 |
| 6 | sooty_mold | 茶煤病 | 黑色煤污层,多在叶面 |
| 7 | brown_blight | 茶云纹叶枯病 | 大型云纹状枯斑 |
这个顺序不是随便排的,它在整套数据里是固定的。VOC 的 XML 里<name>字段写的是字符串,比如 anthracnose;YOLO 的 txt 里写的是对应编号 1。两者靠上面这张表对齐。我自己拿这种双格式数据的第一习惯,是把这张表截图贴在项目笔记最前面,后面所有脚本、yaml、训练日志都基于它,避免中途改类别顺序导致全部标注错位。
健康叶片这个类很多人会忽略,但它恰恰是田间落地的关键。实际巡检时,大部分框选出来的是没病的叶子,如果训练集里没有足够多的健康样本,模型就会把一切叶片都当病叶报,误报率高到没法用。所以看到数据集里包含 healthy,说明做标注的人考虑了真实场景分布,这是加分项。
2.2 同一个目标在 XML 和 TXT 里的两种写法
VOC 格式的核心是一个 XML 文件对应一张图,框的信息以像素坐标存在<bndbox>里。随便抽一张图,它的标注长这样:
<annotation> <folder>JPEGImages</folder> <filename>tea_01234.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>anthracnose</name> <bndbox> <xmin>128</xmin> <ymin>96</ymin> <xmax>340</xmax> <ymax>312</ymax> </bndbox> </object> <object> <name>healthy</name> <bndbox> <xmin>400</xmin> <ymin>220</ymin> <xmax>590</xmax> <ymax>430</ymax> </bndbox> </object> </annotation>一个<object>就是一个目标,<name>是类别字符串,<bndbox>里是左上角和右下角的像素坐标。如果一张图里有多个病害区域,就会有多个 object 节点。要注意filename只写文件名不带路径,实际读取时靠目录去定位。
YOLO 格式则完全不同,它是每张图对应一个同名 txt,每行五个数:类别编号、中心点 x、中心点 y、框宽、框高,全部归一化到 0~1。上面这个 XML 转成 YOLO 后是两行:
1 0.365625 0.425000 0.331250 0.450000 0 0.773438 0.677083 0.296875 0.437500第一行 1 对应 anthracnose,中心点 x=(128+340)/2/640=0.3656,中心点 y=(96+312)/2/480=0.425。第二行 0 对应 healthy,算法一样。这就是为什么 YOLO 训练时看不到像素坐标,所有框都被压进了 0~1 区间,模型预测的也是归一化坐标,推理后再乘回原图尺寸。
2.3 目录结构:两套标注怎么对应
解压后你会看到两个顶层目录,一个 VOC 风格,一个 YOLO 风格,大致是这样:
VOC/ ├── Annotations/ # 每张图一个 xml ├── JPEGImages/ # 全部原图 └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txt YOLO/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/两套结构的逻辑不同。VOC 的ImageSets/Main里的 txt 只存图片文件名(不带扩展名),标注全部堆在Annotations,靠 XML 里的 filename 关联。YOLO 则是按 train/val/test 分目录,图片和标签目录一一对应,找标签的规则就是:把images/train/a.jpg的路径替换成labels/train/a.txt。
这份数据的两套划分是对齐的,也就是说 VOC 里ImageSets/Main/train.txt列出的图和 YOLO 里images/train下的图是同一批。这个细节在做算法对比时很重要——如果你自己重新抽样,两套格式就不等价了,后面比出来的结果差异到底是标注格式造成的还是抽样造成的,说不清。
提示:拿到手先核对一下两套划分的文件数是否一致。方法很简单,分别对两个 train 列表排序后 diff,几秒钟的事。
3. 从 7z 到能跑训的环境:解压、数量校验与格式转换脚本
3.1 先解压:Windows 和 Linux 两条路
压缩包是 7z 格式,不是 zip。Windows 上 7-Zip 应该都有了,右键压缩包选「解压到当前文件夹」就行。这里有个细节:如果下载下来是好几个分卷,比如.7z.001、.7z.002,必须保证所有分卷躺在同一目录再解压,只拖第一个文件进去会提示缺少分卷。
Linux 服务器上更常见的情况是没装 p7zip,直接7z命令会报 command not found。按照下面两步走:
# 安装 p7zip 工具集 sudo apt install p7zip-full # 先测试压缩包完整性,再解压 7z t tea_disease.7z 7z x tea_disease.7z -o/home/user/datasets/参数说明:7z t是 test 模式,只校验压缩包有没有损坏,不实际写文件;7z x会保留压缩包里的目录层级结构,解压出来的目录和打包时一致。很多人会手滑用7z e,e是把所有文件平铺到一个目录,不保留子目录,对这份数据来说会把 JPEGImages 和 Annotations 混在一起,后面脚本全乱。我习惯把 test 和 x 连着跑,test 过了再 x,不然后面训练到一半才发现某个文件损坏,返工成本太高。
另外要注意 7-Zip 和 p7zip 的版本。老版本对较新的压缩算法支持不全,解压理论上应该成功的包也会报错。Windows 下把 7-Zip 升到 21.x 以上,Linux 下确保 p7zip-full 是最新源里的版本,这类莫名报错基本能消掉八成。
3.2 解压后的数量校验
数据集的简介写的是 9591 张,解压完第一件事就是对账。一个很简单的思路:分别数图片和标签,两者数量一致,再和 9591 对,对不上就说明解压或传输有问题。脚本不用复杂,几行 Python 就够:
import os for split in ['train', 'val', 'test']: img_dir = f'YOLO/images/{split}' lab_dir = f'YOLO/labels/{split}' # 只数 jpg,按实际情况改后缀 n_img = len([f for f in os.listdir(img_dir) if f.lower().endswith('.jpg')]) n_lab = len([f for f in os.listdir(lab_dir) if f.endswith('.txt')]) print(f'{split}: images={n_img}, labels={n_lab}, matched={n_img == n_lab}')这个脚本就是最朴素的对账逻辑,三个 split 各输出一行。如果 images 和 labels 数量不等,说明某张图缺标注,或者标注文件是空文件但还在目录里。出现这种情况,直接删掉对应图片和空标签,不要让空标注流进训练——YOLO 训练时会把空 txt 当成「无目标」图,数据分布被悄悄带偏。
数量对完还要看后缀大小写。部分图像可能是.JPG或.png,上面的 lower() 已经把大小写差异抹掉了,比直接 endswith('.jpg') 稳。后缀大小写不一致是压缩包传输时最常见的隐性坑,macOS 和 Linux 之间尤其容易碰到。
3.3 VOC 转 YOLO 脚本:顺序固定、坐标边界兜底
其实这份数据已经给了 YOLO 格式,正常情况下不需要再转。但做工程的人经常会遇到三种情况需要自己转:换类别顺序、重新划分 train/val、或者把 VOC 目录里新增的图片合并进来。所以脚本还是要备一份,核心逻辑就是把 XML 里的像素框归一化成 YOLO 的 cx、cy、w、h:
import os import xml.etree.ElementTree as ET # 顺序写死,禁止用 set 去重后转 list CLASSES = ['healthy', 'anthracnose', 'blister_blight', 'white_scab', 'red_leaf_spot', 'gray_blight', 'sooty_mold', 'brown_blight'] def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in CLASSES: continue # 未知类别直接跳过,避免污染 cls_id = CLASSES.index(name) box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 像素坐标转归一化中心点+宽高 cx = ((x1 + x2) / 2) / w cy = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h # 越界兜底,防止训练时报 out of bounds 警告 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) if bw < 0.005 or bh < 0.005: continue # 面积过小的框没有训练价值 lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') with open(out_path, 'w') as f: f.write('\n'.join(lines) + '\n') xml_dir = 'VOC/Annotations' out_dir = 'YOLO/labels/train' os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): stem = os.path.splitext(xml_file)[0] voc_to_yolo(os.path.join(xml_dir, xml_file), os.path.join(out_dir, stem + '.txt'))逻辑说明:CLASSES列表的顺序就是目标类别编号,这个顺序必须写死,千万不能用set()去重后转 list——集合是无序的,每次跑出的映射都不一样。root.iter('object')遍历一张图里的所有目标,一个目标生成一行。归一化公式前面说过,中心点=两端坐标均值除以图宽高。后面那段 clamp 和面积过滤是保命用的:很多第三方标注工具会导出略微越界的框,比如 xmax 比图宽还大 1 像素,不兜底的话 YOLO 训练时会刷一堆 out of bounds 警告,严重的直接把 loss 带歪。
参数说明:xml_dir和out_dir按实际路径改;CLASSES要和你最终训练用的 names 完全一致。如果数据集原始标签里某个名字不在列表里,代码会静默跳过,日志不会报错,所以转完后建议抽 5~10 个 txt 人工看一下首列数字是否合理,别偷懒。
4. 用它训练 YOLOv8:dataset.yaml、损失函数与参数经验
4.1 dataset.yaml 的写法和路径陷阱
拿到一份规整的 YOLO 格式数据,训练第一步是写数据集描述文件。YOLOv8 用的是 YAML,核心就两件事:告诉框架图片在哪、标签对应的类别名是什么:
# tea_disease.yaml path: D:/datasets/tea_disease train: images/train val: images/val test: images/test nc: 8 names: 0: healthy 1: anthracnose 2: blister_blight 3: white_scab 4: red_leaf_spot 5: gray_blight 6: sooty_mold 7: brown_blightpath是数据根目录,train/val/test相对它定位。这里有三个很常见的坑。第一,path用相对路径时是相对「当前工作目录」解析的,不是相对 yaml 文件所在目录,如果你在项目根目录跑命令、yaml 放在数据集目录里,路径会解析错。我习惯直接用绝对路径,或者把 yaml 丢到数据集根目录下再写相对路径。第二,names的顺序必须和标签 txt 第一列的数字对齐,一旦错位,训练不报错,但预测结果全乱。第三,nc必须等于 names 的长度,多写少写都会触发对齐错误。
如果是在 Docker 或远程服务器上训练,这份数据的双格式价值就体现出来了——VOC 目录可以作为备份存档,YOLO 目录直接挂载进容器,不需要在镜像里再装转换工具。数据不动、环境只读,跑完一个实验销毁容器,下一轮重新挂载,保证实验可复现。
4.2 训练命令与参数范围
YOLOv8 的命令行训练很直接,一条命令跑到底:
yolo detect train \ data=tea_disease.yaml \ model=yolov8m.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=40 \ lr0=0.001 \ device=0 \ project=runs/tea_diseasemodel=yolov8m.pt表示从预训练权重开始微调,而不是随机初始化。9591 张图对农业细粒度病害来说数量不算多,迁移学习收益明显,尤其前几十个 epoch 收敛速度快很多。预训练权重会从官方自动下载到~/.cache,第一次跑需要联网,后面都走本地缓存。如果网络受限,手动下载对应的.pt文件放到工作目录再指定路径也行。
关键参数我给一个参考范围,具体按显存和任务调:
| 参数 | 参考值 | 说明 |
|---|---|---|
| imgsz | 640 | 数据原图接近这个分辨率,不宜再放大 |
| batch | 8~32 | 8G 显存用 m 模型建议 8~16 |
| epochs | 150~300 | 农业病害类 imgsz 小,epoch 可以给足 |
| patience | 30~50 | 早停轮数,配合 epochs 用 |
| lr0 | 0.001~0.01 | 迁移学习建议从 0.001 起步 |
| optimizer | AdamW / SGD | 小数据集上 SGD+momentum 更稳 |
显存估算有个粗公式:显存占用大约正比于 batch × imgsz²。batch 16、imgsz 640 用 m 模型,8G 显存基本顶满,再往上就 out of memory。此时降 batch 到 8、换 n 或 s 模型更保险,不要硬上大模型——农业病害这种细粒度分类,模型容量不是瓶颈,数据和标注质量才是。
4.3 训练时看什么:损失曲线与混淆矩阵
训练启动后,很多人只盯着 mAP 那个数,其实最有信息量的是三条损失曲线。YOLOv8 的损失函数拆成三块:box_loss是边界框回归损失,用 CIoU 变体,衡量预测框和真实框的重合度;cls_loss是分类损失,二值交叉熵;dfl_loss是分布焦点损失,负责框边界的精细回归。训练过程中results.png里这三条曲线都会画出来。
判断标准有两个。第一,看 train 和 val 曲线的间距:train loss 一直降、val loss 降到某点开始回升,是过拟合的信号,此时加数据增强或提前早停;val loss 全程剧烈抖动,大概率是 lr 太高或 batch 太小,先降 lr 再看。第二,看混淆矩阵是不是对角占优。训练结束后runs/tea_disease/下会生成confusion_matrix.png,对角线亮、其余位置暗就是正常状态。茶叶病害里最容易混淆的组合是 anthracnose 和 red_leaf_spot,两者都是褐红色斑,如果这对格子很亮,说明特征差异在 640 分辨率下不够明显,考虑截取病斑局部图做二次分类,或者叠加一个细粒度分类头。
提示:训练完别急着删 runs 目录。
weights/best.pt和weights/last.pt都要留,best 是验证集上最优的权重,last 是最后一轮的,有时候 last 反而在真实场景上更稳。
5. 避坑指南:双格式数据集最容易翻车的五个位置
5.1 7z 解压到一半报错:分卷、版本与完整性
现象:解压到 30% 或 80% 弹出「数据错误」或「无法作为自解压文件运行」,有时还提示密码错误,但包根本没设密码。 原因:最常见的是分卷没下全,或者下载过程中文件被截断,7z 校验文件头时就中止了。其次是 7-Zip 版本太老,不支持较新的压缩算法。密码提示多半是文件本身损坏导致的误报,压缩包损坏时错误信息会乱跳。 解决:先把所有分卷放进同一目录,跑一遍7z t看完整性;Windows 升级 7-Zip 到 21.x 以上,Linux 用 p7zip-full 最新版;确认没问题再7z x。这套流程我现在拿到任何 7z 包都先走一遍,属于最低成本的后悔药。
5.2 类别编号对不上:loss 正常但 mAP 为 0
现象:训练曲线非常正常,loss 稳定下降,验证时 mAP 却是 0,预测结果类别全乱,比如把煤病全预测成健康。 原因:类别编号错位。txt 里第一列是数字,yaml 里 names 是字符串,两者靠顺序对齐。只要转换脚本的 CLASSES 顺序和训练 yaml 的 names 顺序不一致,整个标签体系就整体平移一位。 解决:以 VOC 的 XML 为基准,抽 3~5 个文件人工比对一次。具体做法是找一个 XML 里name=anthracnose的框,看对应 txt 同索引行首列是不是 1。这个检查 30 秒做完,能省掉一整轮无效训练。
5.3 空标注与越界框
现象:训练时刷大量WARNING: label file ... out of bounds,或者数据里存在 0 字节 txt,训练日志里对应图片永远没有 loss 贡献。 原因:第三方标注工具导出时,贴边框的 xmax 会略大于图宽;空 txt 是标注员标完又删掉目标后留下的残留文件。 解决:把 3.3 节脚本里的 clamp 逻辑跑一遍,强制把坐标压回 [0,1];同时清理 0 字节标签和对应图片。检查命令很简单:find YOLO/labels -name "*.txt" -size 0把空文件全列出来,核对后删除。
5.4 训练 loss 变 NaN 与 BN 崩溃
现象:训练一两个 epoch 后 loss 突然变成 NaN,或日志里出现 BN running statistics 相关的异常,后续 epoch 全部无效。 原因:三个高频诱因——学习率过高导致梯度爆炸、batch 太小让 BN 统计值不稳、预训练权重和被改过的模型结构不匹配(比如手动改了 nc 但没对应调整网络头)。 解决:先把 lr0 降到 0.001 试探 20 个 epoch,能跑再拉回来;batch 至少 8;换预训练权重时直接用对应版本的 yolov8n.pt 或 yolov8m.pt,不要自己改结构。BN 崩溃这种问题,降 lr 永远是最快的止血手段。
5.5 混淆矩阵数值总和不是 1
现象:训练完打开 confusion_matrix.png,标题写着 normalized,但每行加起来不是 100%,甚至矩阵里所有数字加起来不到 1。 原因:这个图默认是「行归一化」,每个真实类别单独归一,每行之和为 1。但背景列也算作一个预测类,所以某类被误判为背景的比例也占一行里的份额。如果不确认归一化方向,光凭直觉觉得「总和不对」很容易误判模型好坏。 解决:读图前先看 colorbar 范围,normalized 版的最大值是 1.0;要算自己关心的指标,直接看 val 输出的 precision、recall、mAP50 数字,不要拿图上的色块猜。这个属于「看着像 bug 其实是特性」的典型,我第一次看到也差点以为数据有问题。
6. 把模型接进真实场景:推理脚本与帧间投票
6.1 推理脚本
训练结束后,先用没进过训练集的现场照片验证一遍,再看真实视频流。推理脚本很短:
from ultralytics import YOLO model = YOLO('runs/tea_disease/weights/best.pt') results = model.predict( source='field_samples/', # 放几张现场实拍图 conf=0.25, iou=0.6, save=True, save_txt=True, project='outputs/field_test', )conf是置信度阈值,低于 0.25 的框不输出;iou=0.6是 NMS 合并阈值,同一位置的多余框会被压掉。田间巡检场景我会把 conf 抬到 0.4 左右,宁可少框不可乱框,因为误报框出现在健康叶片上,工人得专门跑过去看一次才知道是错的。
6.2 帧间投票
单张图稳定不代表视频稳定。扫茶园时同一片叶子会出现在连续多帧里,我给每帧输出加一个投票窗口,连续几帧出现同一类别才保留:
from collections import deque window = deque(maxlen=5) # 滑动窗口,保留最近 5 帧 def vote(frame_boxes, min_votes=3): window.append(frame_boxes) if len(window) < min_votes: return [] counter = {} for boxes in window: for cls_id, _ in boxes: counter[cls_id] = counter.get(cls_id, 0) + 1 return [c for c, n in counter.items() if n >= min_votes]窗口 5 帧、阈值 3 次,相当于目标要被持续检出约一秒才上报,无人机晃动和叶片反光造成的偶发误报能压掉大半;窗口太长又会让快速移动镜头的响应变迟钝,3~5 帧是我试过的平衡点。
这轮做下来我养成了一个固定习惯:拿到任何数据集压缩包,先7z t再解压,解压完先数图片和标签、抽查三五个标注,然后才谈训练参数;模型上了现场,也不看单帧效果,先跑一遍帧间投票看稳定性。这份双格式数据把最麻烦的格式转换省掉了,剩下的坑基本都在解压完整性和标签对应上,按这个流程走都能提前挡住。希望帮到你。
本文还有配套的精品资源,点击获取