ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

番茄叶病害检测数据集实战:VOC与YOLO双格式解析及YOLO训练避坑指南

番茄叶病害检测数据集实战:VOC与YOLO双格式解析及YOLO训练避坑指南 简介这份番茄叶病害目标检测数据集面向农业智能化研究与深度学习目标检测实践者用于训练模型自动识别和定位番茄叶片上的多种病害区域适合具备一定YOLO或VOC标注基础的中级学习者。资源包共1438个文件以716张jpg图像为主体配套362个txt标签、358个xml标签另有1个yaml配置与1个cache缓存文件压缩包约23.27MB。数据集同时提供VOC与YOLO两套格式VOC以JPEGImages存放原图、Annotations存放xml边界框标注YOLO以images与labels配对并已划分train.txt和val.txt可直接投入训练与验证。目前已有130人学习下载。借助双格式兼容与完整标注读者可快速搭建病害检测流程对比不同框架下的训练效果省去数据整理与格式转换成本为农业病害识别研究提供即用型数据基础。1. 番茄叶病害检测数据集从拿到压缩包到跑通第一轮训练上周有个做智慧农业的朋友发来一个压缩包说是在棚里拍了一批番茄叶片想做个能自动框出病斑的检测模型问我有没有现成的标注数据能直接开训。我打开一看目录里躺着labels.cache、几张成对出现的 jpg还有 VOC 和 YOLO 两套格式的文件夹——这就是典型的「一份数据、两种吃法」的番茄叶病害目标检测数据集。它解决的核心问题很实在你不需要自己扛着相机去地里拍几千张、再一张张画框拿到就能喂给 YOLO 系列或任何支持 VOC 的检测框架。适合谁做农业 AI 落地的算法同学、带学生做课程设计的老师、想拿真实场景练手目标检测的新手。但别急着train.py这份数据集的目录结构和标签细节里有几个地方不先摸清楚第一轮训练大概率翻车。2. 拆开压缩包VOC 与 YOLO 双格式的目录结构与标签差异2.1 两种格式各自长什么样拿到数据集先别解压到桌面就开干我一般会先tree一下看结构。这份数据集的 VOC 部分走的是经典 PASCAL VOC 布局JPEGImages放原始 jpgAnnotations放同名 xml每个 xml 里有object节点记录类别名和bndbox的xmin/ymin/xmax/ymax。YOLO 部分则是imageslabels两个平行文件夹外加已经切分好的train.txt和val.txttxt 标签每行是class_id cx cy w h坐标全部归一化到 0~1。这里有个容易被忽略的点VOC 的坐标是绝对像素值YOLO 是归一化中心点加宽高。很多人转换完发现框全挤在左上角就是因为忘了除以图片宽高。数据集同时给两套格式本质是让你省掉格式转换这一步——但前提是你得知道自己在用哪套。2.2 标签文件与图片的对应关系labels.cache这个文件值得单独说一句。它是 Ultralytics 系框架在首次扫描labels目录后生成的缓存里面存了图片路径、标签路径和尺寸等索引信息。它的存在意味着这份数据集很可能已经被 YOLO 框架加载过一次缓存能加速后续训练的数据读取。但缓存也是双刃剑如果你手动改了标签或增删了图片缓存不会自动更新训练时读到的还是旧索引。对应关系上VOC 靠文件名匹配——Annotations/xxx.xml对应JPEGImages/xxx.jpgYOLO 靠路径约定——labels/xxx.txt对应images/xxx.jpg。正文里那串0FHTDmPsbO.jpg、sUSXE5otXd.jpg就是图片文件名成对出现说明每张图都有对应标签。检查对应关系最土但最有效的办法# 检查 YOLO 格式下图片和标签是否一一对应 for img in images/*.jpg; do base$(basename $img .jpg) if [ ! -f labels/${base}.txt ]; then echo 缺失标签: $base fi done这段脚本遍历images下所有 jpg去掉扩展名后去labels里找同名 txt找不到就打印出来。跑一遍如果输出为空说明对应关系完整如果有输出那些图就是「有图无标签」的脏数据训练前必须处理掉否则框架可能报索引越界或者直接跳过。2.3 类别定义与标注粒度数据集正文没有明确列出类别清单这是需要你自己确认的第一件事。VOC 格式下类别名写在 xml 的name标签里YOLO 格式下类别是数字 id真实名称通常在同目录的classes.txt或data.yaml里。我一般会先统计一遍类别分布import os from collections import Counter # 统计 YOLO 标签中的类别分布 counter Counter() label_dir labels for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: cls_id line.strip().split()[0] counter[cls_id] 1 print(类别分布:, dict(counter))逻辑很直白逐行读每个 txt取第一个字段就是类别 id用 Counter 累计。跑完你会得到类似{0: 320, 1: 180, 2: 95}的结果。如果某个类别只有个位数样本那基本可以预判训练时这个类别的召回率会很难看要么补数据要么在损失函数里做类别加权。参数上没什么可调的但这一步的产出直接决定你后面要不要做数据增强的针对性设计。3. 从零跑通 YOLO 训练环境、配置与第一轮结果解读3.1 环境准备与依赖安装这份数据集是 YOLO 格式最顺手的框架就是 Ultralytics 的 YOLOv8 或 YOLOv11。环境不用搞太复杂Python 3.8 以上、有 CUDA 的机器装 GPU 版 torch没 GPU 用 CPU 也能跑通小轮次验证流程。我一般用 conda 起个干净环境conda create -n tomato python3.10 -y conda activate tomato pip install ultralyticsultralytics这个包会把 torch、torchvision、numpy、opencv 这些依赖一起拉下来。装完yolo checks跑一下确认 CUDA 是否可用。如果显示CPU而你有显卡多半是 torch 版本和 CUDA 驱动不匹配去 PyTorch 官网按驱动版本重装对应 wheel。这一步的坑在于很多人 pip 装完直接开训结果发现用的是 CPU一个 epoch 跑半小时还以为是数据集太大。3.2 data.yaml 配置与路径陷阱YOLO 训练不认 VOC 那套必须给它一个data.yaml告诉它去哪找图、去哪找标签、有几个类。这份数据集已经切好了train.txt和val.txt所以配置可以走 txt 列表模式# data.yaml path: /home/user/tomato_dataset # 数据集根目录 train: train.txt # 训练集图片列表 val: val.txt # 验证集图片列表 nc: 3 # 类别数按实际统计结果改 names: [early_blight, late_blight, healthy] # 类别名按实际改关键参数说明path是根目录train和val写相对路径时是相对于path的nc必须和标签里的最大 class_id 1 一致写少了框架会报错写多了会浪费输出通道names的顺序必须和标签 id 严格对应id 0 对应 names 列表第一个。我见过最常见的翻车是nc写错——比如实际有 4 类但写了 3训练能启动但第 4 类的标签会被当成背景模型永远学不会那一类。3.3 启动训练与关键参数设置配置写好就可以开训了。第一轮不建议直接上大模型大 epoch先用yolov8nnano 版跑 50 轮看看 loss 曲线和 mAP 走势yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs50 \ imgsz640 \ batch16 \ lr00.01 \ patience10 \ projectruns/tomato \ nameexp1逐个说参数modelyolov8n.pt是预训练权重用 COCO 上训过的 backbone 做迁移小数据集上收敛快很多imgsz640是输入分辨率番茄叶病斑通常不大640 够用显存紧张可以降到 416batch16按显存调8G 显存跑 640 大概能到 16不够就减半lr00.01是初始学习率迁移学习场景下这个值比较稳patience10是早停验证集 10 轮不提升就停省时间。训练日志里重点盯三个指标box_loss是否稳定下降、mAP50是否在涨、cls_loss有没有震荡。如果box_loss降但mAP不涨多半是标注框质量问题如果 loss 直接 NaN检查学习率是不是被改大了或者数据里有非法坐标。3.4 验证集评估与结果解读训完在runs/tomato/exp1/下会生成weights/best.pt和results.csv。用验证集跑一次评估yolo detect val \ modelruns/tomato/exp1/weights/best.pt \ datadata.yaml \ imgsz640输出里会给出每类的 Precision、Recall、mAP50、mAP50-95。农业病害检测场景下我一般更关注 Recall——漏检一个病斑比误检一个更麻烦因为漏检意味着病害扩散没被发现。如果某一类 Recall 明显低回去看这一类在训练集里的样本量大概率是样本太少。mAP50 能到 0.7 以上对于这种叶片病斑数据集就算可用了要到 0.85 以上通常得靠更多数据或更精细的标注。4. 避坑与排查标注、缓存、路径上的五个血泪教训4.1 现象训练启动即报「No labels found」原因data.yaml里的train路径写的是文件夹但框架按 txt 列表模式去解析找不到文件。或者path用了相对路径而你在别的目录下执行命令相对路径解析基准不对。解决要么把train改成图片文件夹路径框架会自动扫描要么确保train.txt里每行是相对于path的图片路径。最稳的做法是path写绝对路径train.txt里也写绝对路径彻底避开相对路径的玄学。4.2 现象改了标签后训练效果没变化原因labels.cache缓存没更新。Ultralytics 首次扫描后会生成这个缓存后续训练直接读缓存不重新扫描目录。解决删掉labels.cache再训框架会重新生成。或者训练时加cacheFalse参数禁用缓存。我现在的习惯是每次动过标签就手动删一次缓存省得怀疑人生。4.3 现象VOC 转 YOLO 后框位置全偏原因转换时忘了归一化或者把xmin/ymin/xmax/ymax直接当cx/cy/w/h用了。VOC 是左上角加右下角绝对坐标YOLO 是中心点加宽高的归一化值两者差一个换算。解决转换公式是cx(xminxmax)/2/W、cy(yminymax)/2/H、w(xmax-xmin)/W、h(ymax-ymin)/H其中 W、H 是图片宽高。转换完随机抽几张用画框脚本可视化一遍肉眼确认框贴合病斑再开训。4.4 现象某类 mAP 始终为 0原因类别 id 和names顺序对不上或者该类样本在训练集里根本不存在只在验证集有。前者是配置错误后者是数据划分问题。解决先统计训练集和验证集各自的类别分布确认每类都有样本再核对data.yaml的names顺序和标签 id 是否一致。如果训练集某类为 0要么补数据要么在划分时做分层抽样。4.5 现象训练 loss 正常但验证 mAP 极低原因训练集和验证集分布差异大比如训练集全是晴天拍摄、验证集全是阴天或者两个集合的图片分辨率差异明显。解决检查train.txt和val.txt里的图片是不是随机划分的。如果是按拍摄批次划分的模型学到的可能是批次特征而不是病斑特征。重新做随机划分确保两个集合的拍摄条件混合。5. 进阶技巧用 VOC 格式做交叉验证与标注质量复核跑通第一轮之后别急着调参刷 mAP。这份数据集同时给了 VOC 和 YOLO 两套格式这其实是个被低估的优势——你可以用 VOC 的 xml 做标注质量复核再用 YOLO 格式训练两条腿走路。具体做法是写个脚本把 YOLO 的 txt 反算回绝对坐标和 VOC 的 xml 逐图对比。如果同一张图两种格式的框位置对不上说明其中一套标注有问题。我一般会抽 10% 的图做这个交叉检查import xml.etree.ElementTree as ET import os def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) W, H int(size.find(width).text), int(size.find(height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append((name, xmin/W, ymin/H, xmax/W, ymax/H)) return boxes def parse_yolo(txt_path): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() cls_id int(parts[0]) cx, cy, w, h map(float, parts[1:5]) boxes.append((cls_id, cx-w/2, cy-h/2, cxw/2, cyh/2)) return boxes # 对比同一张图的两种标注 base 0FHTDmPsbO voc_boxes parse_voc(fAnnotations/{base}.xml) yolo_boxes parse_yolo(flabels/{base}.txt) print(VOC:, voc_boxes) print(YOLO:, yolo_boxes)这段代码把 VOC 的绝对坐标转成归一化的左上右下和 YOLO 反算出来的左上右下放在一起对比。如果数值接近说明两套标注一致如果差得多就得人工去看原图到底哪个对。参数上注意 VOC 的size节点必须存在有些标注工具导出的 xml 缺这个节点得先补上。另一个进阶用法是把 VOC 格式直接喂给 MMDetection 或 Detectron2 做交叉验证。同一份数据用不同框架训出来的模型如果在验证集上表现差异很大说明数据本身有歧义样本。我习惯用 YOLO 快速迭代用 MMDetection 做最终验证两边 mAP 差距超过 5 个点就回去查数据。还有个实操细节番茄叶病害的标注粒度直接影响模型上限。如果标注是把整片叶子框进去而不是框病斑那模型学到的就是「叶子检测」而不是「病害检测」。拿到数据集先可视化几张确认框的是病斑区域而不是整叶。这个检查花五分钟能省掉后面几天调参的无效劳动。从那以后我每次拿到新数据集都强制走一遍「统计类别分布 → 可视化抽检 → 交叉核对格式」这三步再开始写训练命令。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进