茶叶病害检测:基于883张VOC+YOLO数据集训练目标检测模型

发布时间:2026/10/19 2:04:21
茶叶病害检测:基于883张VOC+YOLO数据集训练目标检测模型
简介目标检测是计算机视觉中的基础任务其原理是通过边界框回归与类别分类让模型学会定位和识别图像中的目标物体。在工程落地中标注数据的组织方式至关重要VOC与YOLO是两种主流标注格式前者结构清晰后者更贴合YOLO系列模型的训练要求。对于农业场景下的茶叶病害识别病斑小、背景复杂高质量的标注数据与合理的格式转换流程能显著提升模型效果。一套包含883张茶叶病害图像、8个类别的VOCYOLO格式数据集覆盖了从数据校验、格式转换、类别映射、训练参数调整到常见踩坑经验的完整流程为算法工程师和农业AI开发者提供了一条可复现的目标检测模型落地路径。1. 茶叶病害数据集883张图能训练出一个能用的检测模型吗对做农业视觉的人来说茶叶病害检测一直是个“看着简单、做起来头疼”的方向。茶园背景乱、叶片遮挡多、病斑面积小通用目标检测模型直接上去经常出现漏检、误检。这套“茶叶病害数据集VOCYOLO格式883张8类别.zip”的价值在于它把数据整理成了开箱即用的格式同时提供VOC与YOLO两套标注省掉了最枯燥的标注转换和类别核对工作。883张图、8个类别数量不大但作为验证算法路线、跑通训练流程、甚至做数据增强后的生产前验证是够用的。这套数据适合三类人第一是刚接触目标检测、想用农业场景练手的开发者第二是做茶叶品质或茶园管理的算法工程师需要一个现成的起点第三是想跑对比实验的研究者VOC格式便于接进老框架YOLO格式便于直接开训。接下来我按自己拿到数据集后的真实处理顺序来讲先看格式再做校验与转换再调训练参数最后讲踩坑。整个过程不依赖任何付费工具纯命令行加Python脚本就能跑通。2. 压缩包里到底是什么VOC与YOLO两种格式的目录结构和标签细节2.1 解压后的标准目录长什么样我一般拿到压缩包后第一件事不是解压而是先看压缩包里的顶层目录结构避免解压出一堆散乱文件。这个数据集在压缩包内通常保持着清晰的三段式组织Annotations放VOC格式的XML文件、images放JPG图片、labels放YOLO格式的TXT标注文件。有的打包者还会多放一个classes.txt用于记录类别名或者一个train.txt/val.txt做划分。我建议你先执行这条命令确认结构unzip 茶叶病害数据集VOCYOLO格式883张8类别.zip -d tea_disease find tea_disease -maxdepth 2 -type d | sort解压后看到类似tea_disease/Annotations、tea_disease/images、tea_disease/labels这样的顶层目录说明组织规范。如果没有labels目录只有XML也不要慌YOLO标注可以由VOC标注脚本化生成后面第3章我会给转换脚本。这里有个容易忽略的点图片数量与标注文件数量必须严格一一对应。我用一行命令快速核对ls tea_disease/images/*.jpg | wc -l ls tea_disease/Annotations/*.xml | wc -l ls tea_disease/labels/*.txt | wc -l三个数字一致说明标注没有缺漏如果不一致说明打包时漏了文件或者有空的标注文件这会在训练时直接报错。常见做法是训练前先跑一次完整性校验把缺失标注的图片从训练列表里剔除而不是硬着头皮训。2.2 VOC标注里能读出的关键信息VOC格式的XML是树状结构一个文件对应一张图片。我通常用Python的ElementTree解析几个关键字段filename、size里边的width和height、以及每个object的name和bndbox。这张图告诉你模型需要学什么、标注框是否越界、类别名是否统一。import xml.etree.ElementTree as ET tree ET.parse(tea_disease/Annotations/000001.xml) root tree.getroot() print(文件名:, root.find(filename).text) print(图片尺寸:, root.find(size/width).text, x, root.find(size/height).text) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) print(f类别: {name}, 坐标: ({xmin}, {ymin}) - ({xmax}, {ymax}))这段代码跑完后你该重点关注三件事第一xmax、ymax是否小于图片宽高大于则说明标注越界第二所有XML里出现的name是否与classes.txt完全一致不一致后面转YOLO时会错位第三坐标是否出现xmin xmax这种非法情况这类XML属于脏数据需要直接丢弃或者人工修正。我在某次处理茶叶病害数据时遇到过几处xmax等于width的边界标注转成YOLO归一化后没多大问题但老版本的YOLO对越界框很敏感轻则警告重则loss变成nan。2.3 YOLO标注的格式细节与归一化陷阱YOLO格式的TXT文件每行代表一个目标五列数据依次是class_id x_center y_center width height。前四个坐标值是归一化后的数值范围在0到1之间由像素坐标除以图片宽高得到。这个设计让模型不关心输入分辨率但也带来了一个坑如果你用VOC转YOLO时没有统一除以XML里的size.width和size.height而是除以别的值坐标全部错位训练时loss能降但预测框全偏。def voc_to_yolo(xml_path, out_path, class_map): 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.findall(object): name obj.find(name).text cls_id class_map[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 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))这里class_map是一个字典键是类别名值是类别ID必须与训练用的数据集YAML中names列表顺序一致。我建议你生成TXT后随机抽几张图验证写一个小脚本把归一化坐标乘回图片宽高画框人眼看一下是否贴住病斑。这一步叫“标注可视化校验”花5分钟能省掉训练3小时后才发现数据错位的血泪时间。3. 把VOC转成YOLO格式类别映射脚本与四个边界坑3.1 类别映射为什么要单独维护一份文件8个类别的数据集中最怕的是类别顺序不一致。举个例子classes.txt里写的是[blight, rust, mildew]但转换脚本里硬编码成{blight: 0, mildew: 1, rust: 2}前两个反正能跑第三个就错位了。由于是打包好的数据集你拿到的classes.txt就是唯一可信的类别顺序所有脚本和配置都应以它为基准。我一般会先读一遍classes.txt再核对XML里出现的所有name生成一份完整映射with open(tea_disease/classes.txt) as f: class_names [line.strip() for line in f.readlines()] class_map {name: idx for idx, name in enumerate(class_names)} print(类别映射:, class_map)如果XML里出现classes.txt之外的类别名说明数据集来源混合了其他病害类型需要决定是合并还是剔除。这个决定会影响最终模型的类目一定要在训练前做不要训练中再改配置。3.2 完整批量转换脚本从XML目录到TXT目录把第2章的转换函数扩成批量版本遍历所有XML文件输出到labels目录。这是我在本地跑转换时的常用做法。import os import glob import xml.etree.ElementTree as ET def convert_all(xml_dir, txt_dir, class_map): os.makedirs(txt_dir, exist_okTrue) xml_files glob.glob(os.path.join(xml_dir, *.xml)) for xml_path in xml_files: basename os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(txt_dir, basename .txt) try: voc_to_yolo(xml_path, out_path, class_map) except Exception as e: print(f转换失败: {xml_path}, 原因: {e}) convert_all(tea_disease/Annotations, tea_disease/labels, class_map)跑完后统计TXT文件数量确认与XML数量一致。如果某张图有目标但转换后TXT为空大概率是XML里bndbox字段缺失或格式异常需要人工检查该XML是否损坏。转换过程中的异常处理很重要我一向建议把异常信息打印出来而不是直接吞掉因为毒数据往往集中在几张图上错误提示能帮你快速定位是哪张图、什么原因。3.3 边界坑一图片尺寸写错导致归一化偏移VOC的XML里size字段不一定真实可信比如标注工具导出时把宽高写反或者图片被压缩过但XML没更新。一种有效的校验办法是直接用Python读取图片真实尺寸和XML的size对比。from PIL import Image img Image.open(tea_disease/images/000001.jpg) print(图片真实尺寸:, img.size)如果图片真实尺寸与XML不一致归一化坐标就会错位模型训练时看起来一切正常实际预测框偏移。解决方法是优先以图片真实尺寸为准忽略XML中的尺寸字段若打包者保证XML尺寸与图片一致那这一步就不需要额外处理。我实际处理过的某茶叶数据集中就有一个子集图片被统一resize过而XML未同步更新导致那一批标注全歪。3.4 边界坑二类别ID错位与遗漏类别YOLO训练时如果标签文件里出现了超过类别总数的ID训练直接崩溃如果类别ID顺序与YAML不一致训练不报错但模型学的是错的东西。这个坑最有隐蔽性因为人眼很难从预测结果上立刻看出类别顺序反了。我的处理方式是转换完统计一遍所有ID范围cat tea_disease/labels/*.txt | awk {print $1} | sort -n | uniq输出结果应当是从0开始、最大值等于类别总数减1的连续整数序列。如果出现空缺或跳变说明有类别没被分配到任何样本或者转换时映射表漏了项需要回去修。3.5 边界坑三空标签文件与全黑背景图茶园的病害图里偶尔会有完全没有标注的图片可能因为病斑太小无法人工标也可能因为图片本身对焦模糊。这类图片如果留在训练集里YOLO会把它们当作背景负样本但如果你把校验做严格点发现TXT文件大小为0我建议直接从训练列表里删掉省得影响采样。3.6 边界坑四不同来源数据集的类别名写法不统一这类打包数据集有时会混入多个来源的数据比如同一个病害有的标成leaf_blight有的标成blight还有的标成Blight。大小写不同在字典映射时不敏感但下划线和缩写差异会导致类别数量比你预期多。我遇到过某数据集标称5类实际XML里出现8种不同name的情况前3类正常后两类是重复标注的不同写法。处理办法是在转换前做一次名称清洗把同义变体统一映射到同一个ID。4. 训练前的数据准备划分训练集、配置YOLO数据文件与增强参数4.1 按病害类别分层划分训练集与验证集883张图划分时如果直接随机打乱容易造成某个类别全部落在验证集里训练时模型没见过验证时全是漏检。正确做法是按类别分层采样保证每个类别在训练集和验证集里的占比相近。import random from collections import defaultdict random.seed(42) # 读取所有图片名及对应的类别集合 img_classes defaultdict(set) for txt_path in glob.glob(tea_disease/labels/*.txt): img_name os.path.basename(txt_path)[:-4] with open(txt_path) as f: for line in f: cls_id line.strip().split()[0] img_classes[img_name].add(cls_id) # 按类别分层划分 train_ratio 0.85 train_imgs, val_imgs [], [] for cls in range(8): imgs_of_cls [img for img, clses in img_classes.items() if cls in clses] random.shuffle(imgs_of_cls) cut int(len(imgs_of_cls) * train_ratio) train_imgs.extend(imgs_of_cls[:cut]) val_imgs.extend(imgs_of_cls[cut:]) # 去重并写文件 train_imgs list(set(train_imgs)) val_imgs list(set(val_imgs)) with open(tea_disease/train.txt, w) as f: for name in train_imgs: f.write(ftea_disease/images/{name}.jpg\n) with open(tea_disease/val.txt, w) as f: for name in val_imgs: f.write(ftea_disease/images/{name}.jpg\n)抽样策略上一张图可能包含多个病害类别按类别分层时它会被抽中和去重最终训练集和验证集的比例并非严格的85%但每个类别在两边都有代表样本这是保证评估结果稳定的关键。4.2 写对YOLO数据配置文件是训练的第一道关卡以YOLOv8为例数据集配置是一个YAML文件内容包含训练路径、验证路径、类别数和类别名。这里最难的地方在于类别名顺序必须与前面class_map完全一致顺序错了模型权重和类别ID就对齐不上。path: tea_disease train: train.txt val: val.txt names: 0: blight 1: brown_blight 2: charcoal_rot 3: grey_blight 4: helopeltis 5: red_spider 6: rust 7: white_scabtrain和val支持直接写TXT文件路径列表。如果你用的是新版YOLO训练入口推荐写成train: images加val: images的形式框架会自动去找同名的labels目录。一旦你用了TXT列表就需要保证TXT里的图片路径与labels目录里的标签文件同名同目录否则训练时一堆“image not found”或者“label not found”。4.3 数据增强参数怎么设病害小目标场景的调整方向茶叶病害检测的特殊性在于病斑小、边缘模糊、背景噪声大。默认的增强参数不是不能用但针对性调整能明显提升小目标召回率。我常用的配置思路是hsv_h、hsv_s、hsv_v这组颜色增强保持默认因为叶片颜色本身是重要特征过度改变色调反而让模型学到不真实的颜色degrees旋转设为10度以内茶园的叶片方向相对固定不需要大角度旋转translate设为0.1模拟拍摄位置的轻微偏移scale设为0.5配合马赛克增强让模型看到不同尺度下的病斑mosaic设为1.0合成四张图会让小目标被裁掉所以需要配合copy_paste让目标在不同背景上重新组合。这个方向在同类农业病害数据上的效果通常是正向的但每个数据集最优值不同建议跑两组长短对比再决定。4.4 训练命令与关键超参数参考选模型大小时我会先看标注框的大小分布。如果绝大多数框的长宽都在图片尺寸的10%以下属于典型的小目标场景直接用yolov8n大概率效果不够建议从yolov8s开始试。yolo train modelyolov8s.pt datatea_disease.yaml epochs150 imgsz640 batch16 patience20imgsz640是速度和精度的折中如果病斑很小可以试试imgsz800但显存占用和训练时间会同步上升。batch按显存调16是8G显存能承受的值。patience20表示验证集指标连续20个epoch不提升就提前停止能在模型收敛过慢时省时间。epochs150对于883张图的数据集来说偏多但配合早停机制实际训练到60到80轮就会停不影响效率。5. 训练中的四个典型坑从loss不降到mAP为05.1 坑一loss正常下降但验证集mAP一直为0现象训练时loss稳定下降但每个epoch结束后的验证mAP为零预测结果全无。原因验证集的标注文件与图片文件命名不匹配框架找不到标签于是把所有图片当作背景模型预测结果无从计算。解决检查验证集图片名与labels目录下的TXT文件名是否一一对应重点看是否有.jpg和.JPG大小写不一致的情况。我在某个数据集上遇到过Windows系统打包导致的文件名全小写Linux上解压后图片名与XML名对不上的问题当时排查了很久才发现是大小写差异。5.2 坑二训练时报“AssertionError: labels are not consistent”现象训练进程刚加载数据就报错退出提示标签文件与图片不匹配。原因存在没有对应TXT标注的图片或者TXT里出现了不存在的类别ID。解决用脚本扫描所有图片和标签找出只有图片没有标签、以及标签类别ID超范围的样本删除或修正。这条我不建议跳过硬跳过去后面验证集评估时还会爆雷。5.3 坑三训练正常但效果极差预测框全画在叶片边缘现象验证loss不高但实际预测时框总在叶片边缘或者叶片间的缝隙处。原因标注框贴合度差数据里混入了将整片叶子框进去的大框模型被带偏学会了框叶子而不是框病斑。解决检查标注统计信息如果框的宽高比普遍大于10:1或面积占比过大需要用脚本过滤掉这类异常框或者人工修正。从实践角度看某团队在我参与的模拟项目X中遇到过整片茶园叶部病斑标注框过大的问题把异常框过滤后mAP提升了10个点以上。5.4 坑四类别不均衡导致少数类几乎不检现象8个类别中有一个类别样本量明显少于其他类训练后该类别召回率极低。原因茶叶病害数据集中不同病害发病率天然不均衡有些类别只有几十个实例模型学不到足够的特征。解决优先调整cls_loss权重其次考虑对少数类做针对性增强比如对该类样本做随机裁剪放大、重复采样拼接。还可以用weight参数给少数类更高损失权重让模型更关注少数类。这些坑的共同特点是训练日志里不一定会报错但结果一看就不对。我的习惯是训练前写一个数据体检脚本把图片数量、标签数量、类别分布、框大小分布、异常框比例全部统计一遍5分钟能跑完能挡掉至少一半的翻车场景。6. 训练完怎么验证用混淆矩阵和热力图判断模型是否可信训练结束后不要只看验证集mAP数字。mAP是全局指标它掩盖了类别间的差异一个mAP 0.5的模型可能对其中3类效果很好、5类全部漏检。我会先跑验证命令然后重点看confusion_matrix.png和results.png两张图。yolo val modelruns/detect/train/weights/best.pt datatea_disease.yaml验证结束后输出目录里会有混淆矩阵。看混淆矩阵时我关注三个地方对角线上的数值是否足够高哪一类的预测被错分到背景哪两类之间互相混淆严重。茶叶病害里白星病和茶饼病外观相似若模型经常把后者误判为前者说明特征区分度不够可以考虑增加这两类的裁剪图做专门训练。再看热力图。对模型输出的特征图做可视化你能直观看到模型是根据病斑还是根据叶片纹理做的判断。如果热力点集中在叶片边缘的高光区域而不是病斑中心说明模型学到了错误模式需要回到数据清洗和标注修正环节。如果验证结果达到预期下一步是把模型导出为ONNX做边缘端推理。8类茶叶病害在手机端或嵌入式设备上跑推理模型大小控制在10M以内几乎不影响精度这对茶园实地检测场景更实用。导出命令很简单yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出ONNX后可以再转NCNN或者TensorRT按部署设备选。如果只是想快速验证模型在实际茶园的拍摄效果直接用yolo predict跑一批未经处理的实拍图看漏检率和误检率更真实。883张图的数据集训练出的模型不是终点而是验证方案的起点。我自己的做法是先用这套数据跑通整条流水线再把模型部署到边缘设备上收集真实茶园数据用半自动标注的方式持续扩充训练集。数据量从883涨到3000张后小目标类别的召回率会有一次肉眼可见的提升这个规律在多个农业场景数据集上都成立。最后提醒一句数据集的标注质量直接决定模型上限半吊子的标注清洗流程会让再好的模型也白搭。希望这篇笔记能帮你少走几趟弯路顺利跑通自己的茶叶病害检测项目。本文还有配套的精品资源点击获取