大白菜叶片病害图像识别实战:从2800张标注数据到YOLOv8训练
发布时间:2026/10/1 5:17:37来源:尧图网络
简介这份数据集面向深度学习图像分类任务聚焦大白菜叶片上背蛾、潜叶虫、霉菌三类常见病害的识别覆盖了农业场景中容易混淆的叶部病害类型可直接用于植保相关的图像识别研究。包内共2000个文件其中主体为1998张已标注的jpg叶片图像已按训练集、测试集分目录存放另附1个Python可视化脚本和1个JSON类别配置文件压缩包整体约350.83MB。JSON文件写明三类的具体类别名与对应关系运行show脚本可快速预览各类样本便于核查标注结果、观察类间差异并发现可能的错标或样本不均衡问题。资源拿来即可用于CNN、YOLOv5等分类网络的训练和评估免去自行爬图、清洗与划分数据集的流程适合作为图像分类入门练手数据或算法对比的基准。目前已有154人学习下载相关CNN分类网络项目及YOLOv5分类实现也可在作者主页找到便于进一步参考完整训练思路。1. 大白菜叶片病害图像识别数据集2800张已标注图能做什么大白菜叶片病害图像识别数据集名字里已经交代了三件事任务是图像识别对象是大白菜叶片素材是约2800张已标注图片。农业团队拿到它通常有两个诉求一是快速搭一个叶片病害识别Demo手机拍一张叶子直接出诊断二是把病斑位置检测出来配合喷药设备做定点施药。2800张对深度学习训练来说不算大但胜在“已标注”足够把一个分类或检测模型训练到可演示、可小范围试用的程度。前提是你别把数据当黑匣子直接扔进训练脚本。这篇按我处理作物病害数据集的习惯从数据摸底、格式转换、模型训练到验收排错完整讲一遍能直接抄的参数会写在代码里会翻车的坑单独列一章。2. 拿到约2800张标注图先别训模型数据结构的四个摸底项很多人拿到数据集第一件事就是跑训练结果loss降得挺漂亮一到实拍就废。问题不在模型在数据没有被真正看懂。训练前花半小时做四个摸底项类别分布、标注格式、图像尺寸与光照、类别均衡度能省下后面几天的排错时间。2.1 用脚本统计类别分布每类图数决定任务上限大白菜病害数据集常见的组织方式是按类别分目录也有把所有图片放一个文件夹、用XML或JSON标注文件的。第一步先统计每个类有多少张图。# 假设数据集按类别分目录存放统计每一类的图片数量 find /path/to/cabbage_dataset -maxdepth 1 -type d | while read dir; do count$(find $dir -type f \( -name *.jpg -o -name *.png \) | wc -l) echo $(basename $dir): $count done这个命令遍历一级子目录并数出每类图片数量。重点不是脚本本身而是拿到三组信息类别总数、每类最少图数、有没有“正常叶”这个负样本类。健康叶的重要性经常被忽略模型在田间误报最多的就是健康叶如果数据集里没有这个类等于默认所有叶子都有病部署后根本没法用。先确认有没有负样本比确认哪个类有多少张更急迫。另外用 python 看一眼维度分布也很有必要from PIL import Image from pathlib import Path widths, heights [], [] for img_path in Path(images).glob(*.jpg): w, h Image.open(img_path).size widths.append(w) heights.append(h) print(width range:, min(widths), -, max(widths)) print(height range:, min(heights), -, max(heights))这一步是为了知道模型训练时统一缩放到什么尺寸合适。如果图片跨度从 640 到 4000 都有就不能强行全压到 640否则小图放大了还是模糊大图缩得太狠会丢掉病斑细节。2.2 看三张标注文件反推格式分类、检测还是分割“已标注”是个含糊的说法必须实际打开标注文件确认格式。常见三种情况labelimg 导出的 VOC XML、labelme 导出的 JSON、YOLO 系列的 txt。用一段小脚本判断import xml.etree.ElementTree as ET import json from pathlib import Path sample Path(labels/000001.xml) if sample.suffix .xml: root ET.parse(sample).getroot() objects root.findall(.//object) print(VOC XML 检测格式) for obj in objects: print(类别:, obj.findtext(name), 框:, [obj.findtext(fbndbox/{v}) for v in [xmin, ymin, xmax, ymax]]) elif sample.suffix .json: data json.loads(sample.read_text()) print(JSON 格式标签工具可能是 labelme 或 cvat 导出) print(shapes 数量:, len(data.get(shapes, []))) elif sample.suffix .txt: print(YOLO txt 格式每行: 类别 中心x 中心y 宽 高) print(sample.read_text().splitlines()[:3])XML 里有 bndbox 说明是检测标注JSON 里 shapes 带 points 可能是多边形分割txt 里是空格分隔的五个数字说明已经是 YOLO 格式可以直接开训。这一步骤的意义在于决定后面的工作流检测格式需要转成目标检测训练格式只有类别文件夹、没有框的就是纯分类数据集不需要转格式直接按文件夹训练即可。2.3 检查光照、背景与拍摄批次图片质量决定泛化上限作物病害图像的坑往往在标注之外。同一批叶子如果是在同一天、同一块地、同一个角度拍的背景和光照高度一致模型很容易学到“拍摄环境”而不是“病害特征”。训练时不觉得一到用户拿手机在真实大棚里拍立刻现原形。判断方法也很朴素用上一节的 Python 代码算出每张图的平均亮度再看文件名是否有批次规律。以下是一个快速亮度检查import numpy as np from PIL import Image from pathlib import Path for img_path in list(Path(images).glob(*.jpg))[:20]: img np.array(Image.open(img_path).convert(L)) print(img_path.name, 平均亮度:, round(img.mean(), 1))平均亮度相差超过 30 就说明拍摄时段或环境差异大。这类样本不能简单随机划分训练集和验证集要按批次划分否则验证分数会虚高。后面第五章会专门讲这个坑。2.4 类别均衡度的初步判断过采样、加权还是合并统计完每类图数后进行一个简单判断。假设 2800 张分到 6 个类平均每类约 460 张这个量对病害识别是够用的但如果某个子类只有 60 张整体数量再多也没用模型对少数类的输出基本不可信。每类图数建议处理方式300 张以上直接参与训练正常做增强150300 张考虑小幅过采样让每个 epoch 都能见到足够样本50150 张过采样 降低该类的学习率权重或合并相似病种50 张以下并入“其他病害”类单独输出该类意义不大这个判断要结合病害种类来做。大白菜上常见的霜霉病、软腐病、黑斑病在症状上差异明显不适合盲目合并但如果某类只有 40 张且形态和另一种高度相似硬要模型区分只会把两类都搞坏。先合并等数据积累够了再拆开是小数据集项目里最常见的务实做法。3. 把原始标注转成 YOLO 训练格式VOC 与 COCO 转换脚本和边界坑如果确认数据集是检测标注接下来的核心工作就是格式转换。无论手头是 VOC XML、COCO JSON 还是 labelme 的 JSON都要统一成 YOLO 的 txt 格式。这不是为了赶时髦而是 YOLO 系列的训练生态最成熟转换脚本、增强策略和部署导出都有人替你踩过坑。3.1 为什么统一到 YOLO 格式文件小、读取快、边界清晰YOLO 标注格式的核心是一张图片对应一个 txt 文件每行五个数字类别编号、归一化中心点 x、归一化中心点 y、归一化宽度、归一化高度。所有坐标除以图片宽高所以不同分辨率的图可以混在一个训练集里不用事先统一尺寸。对比下来VOC XML 需要解析 XML 树文件冗余字段多COCO JSON 一次性加载全部标注数据量稍大就吃内存而 YOLO txt 每张图独立读取训练时按 batch 加载基本没有 IO 瓶颈。另一个实际原因是cvat、labelimg、labelme 导出的检测标注最终都能通过脚本转成 YOLO 格式生态非常成熟。如果你的数据集已经是按文件夹分好类别的分类格式那就不需要做这一步直接跳到第四章用分类模式训练。3.2 VOC XML 转 YOLO TXT最小脚本与两个安全参数这是 labelimg 导出结果最常见的一步。脚本不长但有几个边界条件必须处理。# voc_to_yolo.py: 把 labelimg 导出的 VOC XML 转成 YOLO txt import xml.etree.ElementTree as ET from pathlib import Path def convert_xml(xml_path, out_dir, class_map): root ET.parse(xml_path).getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): cls obj.findtext(name) if cls not in class_map: continue # 跳过未纳入类别的标注避免脏数据进训练 box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 归一化像素坐标除以图像宽高得到相对坐标 cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{class_map[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_txt Path(out_dir) / (Path(xml_path).stem .txt) out_txt.write_text(\n.join(lines))逻辑说明代码先读 XML 里的图像宽高再逐个取 bndbox 的四个像素坐标换算成归一化的中心点和宽高最后写到与图片同名的 txt 文件。换算公式是 YOLO 格式的标准做法任何一张 1920 或 640 的图标注坐标都能落在 0 到 1 之间。两个参数需要重点说明。第一个是class_map也就是类别映射字典例如{霜霉病: 0, 软腐病: 1}这个顺序必须和后面训练用的 yaml 文件里的 names 完全一致否则模型学出来的类别全是错位的。第二个是xmin/xmax/ymax是否越界的判断如果发现 xmax 大于 img_w说明标注时框拖出了图片边界YOLO 格式对越界坐标容错很差要么裁剪到边界内要么直接丢弃这条标注不要硬算进 txt。3.3 COCO JSON 转 YOLO五个字段和一个大坑如果数据是 COCO JSON 格式转换逻辑类似但走法不同。COCO 的标注放在 annotations 数组里每个元素有 image_id、category_id、bbox 四个值其中 bbox 是[x, y, width, height]。转 YOLO 时要把 x、y 当作左上角先算出中心点再加一半宽高。这里有一个容易翻车的点COCO 的 category_id 是从 1 开始的而 YOLO 的类别编号是从 0 开始的。转换时如果直接用 COCO 里的 category_id 写入 txt第一类会变成 1而 yaml 里第一类是从 0 开始整个模型输出全部错位。转换时务必做yolo_id coco_category_id - 1或者干脆自己重建一个类别映射表。3.4 训练目录结构与 yaml 配置三个必配路径转换完成后把数据排列成 YOLO 常规目录结构后续训练命令才不用到处找路径。我一般这样组织cabbage_disease/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── cabbage_disease.yaml对应的 yaml 内容如下# cabbage_disease.yaml path: ./cabbage_disease train: images/train val: images/val names: 0: shuangmei # 霜霉病 1: ruanfu # 软腐病 2: heiban # 黑斑病 3: normal # 健康叶三个必调参数是path、train和val。path 指数据集根目录train 和 val 是相对于根目录的路径不要写绝对路径否则换机器后还得改一遍。names 的顺序必须与上一节转换脚本里的 class_map 编号严格对应这里错了前面所有转换都白做。建议把类别名保存成英文或拼音避免部分环境对中文路径和中文类名处理出问题展示层再映射回中文。3.5 转换后必须画框验证不抽检等于白转转换脚本跑完最常见的错觉是“txt 生成了格式对了”。格式对不代表坐标对。强烈建议随机抽五张图把标注框画出来看一眼。import cv2 img cv2.imread(cabbage_disease/images/train/000001.jpg) h, w img.shape[:2] with open(cabbage_disease/labels/train/000001.txt) as f: for line in f: cls, cx, cy, bw, bh map(float, line.split()) 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.imwrite(check_000001.jpg, img)这段代码读回 txt 里的归一化坐标乘以图像宽高还原成像素坐标再用 OpenCV 把框画出来。如果框没有包住病斑、框的位置跑到叶片外面说明转换脚本或原始标注本身有问题先回头查数据再谈训练。提示txt 转完但没画框验证就开训出问题时你分不清是标注错、转换错还是模型错排错成本翻倍。4. 用 YOLOv8 训练大白菜病害检测训练自己的数据集的最小命令和四个必调参数格式和目录都整理好后进入训练阶段。用 YOLOv8 训练自己的数据集已经是农业视觉项目里很常规的路线但 2800 张的体量意味着参数不能照搬大模型训练套路要用小数据专用的打法。4.1 数据集划分按拍摄批次切分别随机打乱这是整个训练环节里性价比最高的一步。很多人的数据划分代码长这样random.shuffle之后按比例切。对小数据集、尤其拍摄环境高度一致的数据来说这种随机切分是灾难。原因是同一天、同一块地拍的大白菜叶片背景土壤、光照角度、叶片朝向都几乎一样。如果这些图片被随机分到训练集和验证集模型在训练时已经见过同一场景的“近亲”验证指标自然好看但一到真实环境就穿帮。正确做法是按文件名里的批次号或拍摄日期切分例如DSC_20230501_001.jpg的前 8 位是拍摄日期用日期作为分组单位把同一天的图整体划到同一侧。# 观察文件名规律 ls cabbage_disease/images/train | head -20如果文件名没有明显规律就按目录划分如果数据集本身只有 train 和 val 两个目录至少确认 val 目录不是从 train 里复制出来的。这一步做不好后面所有的分数都要打个问号。4.2 YOLOv8 最小训练命令从 pretrained 权重起步目录整理好之后训练命令非常短yolo detect train \ modelyolov8n.pt \ datacabbage_disease/cabbage_disease.yaml \ epochs150 \ imgsz640 \ batch16 \ patience30 \ workers4这条命令用 COCO 预训练权重yolov8n.pt初始化模型而不是从随机权重开始训。迁移学习是 2800 张数据能做检测的核心原因模型已经会识别通用物体边缘、纹理和形状只需要在叶片病斑上做微调收敛速度比随机初始化快得多也不容易过拟合。参数选择上用yolov8n而不是yolov8m或yolov8l是因为小数据配小模型。模型参数量越大需要的样本量越大硬上大模型的结果就是验证集上指标好看测试集一测就崩。imgsz640是常规选择如果病斑普遍很小改成 960代价是训练时间增加但小目标召回率会明显提升。patience30表示验证指标连续 30 轮不提升就自动停止避免无效训练耗时间。batch 大小按显存调整16G 显存跑 batch16 没问题显存小就降到 8。4.3 类别不均衡的处理过采样复制比调损失函数更可控前面摸底发现少数类图片少时别指望模型自己学会。最直接的方法是过采样把少数类图片复制几份放进训练集。复制不增加信息量但改变了每个 batch 里少数类样本的占比让梯度更新不会被高频类完全淹没。# 对少数类过采样复制图片和对应的标签 txt import shutil from pathlib import Path cls_dir Path(cabbage_disease/images/train/heiban) label_dir Path(cabbage_disease/labels/train/heiban) files list(cls_dir.glob(*.jpg)) for i in range(120): # 额外复制 120 张 src_img files[i % len(files)] dst_img cls_dir / fdup_{i}_{src_img.name} shutil.copy(src_img, dst_img) # 同步复制标签文件 src_txt label_dir / src_img.with_suffix(.txt).name if src_txt.exists(): shutil.copy(src_txt, label_dir / fdup_{i}_{src_txt.name})逻辑说明脚本遍历少数类目录的图片循环复制出 120 张新图片和同名标签文件名加dup_前缀避免覆盖原文件。YOLO 训练按文件名匹配图片和标签复制时同步复制 txt 就不影响训练。要注意的是过采样只改变采样比例不改变特征多样性。所以过采样之后还要配合增强让复制的图片在每次 epoch 里经过不同的裁剪、旋转和颜色变化实际上是给少数类制造了多种变体。4.4 训练监控看验证损失和 mAP 曲线不只盯最后的模型文件训练过程中留意runs/detect/train目录下的results.png它会画出训练损失、验证损失和 mAP 曲线。判断是否过拟合的标准很简单训练损失持续下降验证损失却开始上升说明模型开始背训练集而不是学特征。此时应该早停而不是继续烧显存。另外不要只看 mAP50。大白菜叶片病害检测里病斑通常是不规则的、小面积的区域mAP50-95 更能反映模型对小目标的定位精度。如果 mAP50-95 明显低于 mAP50说明模型定位能力弱优先考虑提高 imgsz 或增加小目标增强而不是单纯加训练轮次。4.5 数据增强参数2800 张的增强边界在哪YOLOv8 自带数据增强参数但默认值是为通用目标检测调的。针对叶片病害数据有三个参数值得重点调整参数推荐值理由hsv_h0.010.02微调配色模拟不同光照但别改到叶片变绿或变枯rotate05 度叶片朝向稍有变化即可大角度旋转会让叶脉纹理变得不真实mosaic0.3 左右四张图拼一张能增加背景多样性但病害图本来背景就干净mosaic 太高会引入大量噪声这组参数的逻辑是大白菜病害识别主要依赖叶片上的病斑颜色和纹理属性和自然光强相关。HSV 色彩增强是对抗光照变化最有效的工具但幅度过大反而会生成现实中不存在的病斑颜色。旋转角度也一样叶片在图像里通常朝向固定转 45 度对模型来说没有实际意义还容易让叶形失真。注意mosaic 增强在默认配置下是 1.0对小目标病害检测建议主动调低。四张图拼接后每张图被缩小一半病斑可能从十几个像素变成几个像素直接漏检。5. 大白菜病害数据集的 5 个踩坑点从标签错位到实拍翻车训练跑通只是第一步。以下五个坑是我在处理作物病害检测任务时真实遇到过的基本覆盖了“标注质量、数据划分、类别不均衡、小目标、部署验证”这几类典型问题。每条按现象、原因、解决来写可以直接对照排查。5.1 标签错位XML 里的中文名与类别表对不上现象训练时 loss 正常下降验证 mAP 也不差但实际预测时把霜霉病全部判成软腐病而且模型很“自信”置信度都超过 0.8。原因XML 标注文件里的 name 是中文“霜霉病”转换脚本里 class_map 的枚举顺序和 yaml 里 names 的顺序不一致。YOLO 训练按数字读取类别写入 txt 的 0 实际代表霜霉病但 yaml 的第 0 项写的是软腐病模型从第一轮开始就在错位学习收敛得再好看也没用。解决训练前写一段脚本遍历所有 XML 统计 name 的集合再和 yaml 的 names 列表逐一对比确认顺序完全一致。这种错位光靠画框验证看不出来因为框的位置是对的只是类别语义被整体平移了。花两分钟做一次映射校验能避免整个训练周期白跑。5.2 验证分数虚高同一批次照片被随机切分导致实拍翻车现象数据集上验证 mAP 能到 0.9用手机在真实大棚里拍了一批叶子准确率掉到一半以下。原因数据集的 2800 张图里有相当一部分是同一天、同一块地连续拍摄的。按单张图片随机划分训练集和验证集后同一场景的照片同时出现在两侧。模型实际上记住了这块地的背景土壤、光照角度和叶片姿态而不是病斑本身的特征。验证时它认的是“场景相似度”实拍时场景一变立刻失效。解决按拍摄批次划分数据把同一天或同一拍摄序列的图片整体放到训练集或验证集保证验证集和训练集没有场景重叠。判断批次最简单的方法是看文件名是否带序号、日期或采集设备编号。找不到批次信息时宁可把验证集比例调高一点也不要让同场景泄漏进去。5.3 少数类直接学不出来整体 mAP 不错单独一类 precision 为 0现象训练结束后整体 mAP 达到 0.75但查看每个类别的 PR 曲线黑斑病的 precision 一直为 0模型在一个病斑都没框出来。原因黑斑病只有 40 张图而霜霉病有 1200 张。训练时每个 batch 里绝大多数是霜霉病样本梯度更新方向被高频类主导黑斑病的特征对 loss 的贡献可以忽略模型最终选择把所有病斑都预测成霜霉病来降低整体 loss。解决先过采样把黑斑病补齐到 150 张以上再训练如果补完效果仍差果断把黑斑病并入“其他病害”类。这是 2800 张小数据集控制风险的必要妥协总比模型对所有少见的病种全部误诊要强。数据量不够时输出粒度让位于输出可靠性。5.4 小病斑漏检imgsz640 把特征缩没了现象大片病斑能框出来但针尖大的初期病斑一个都检不到。用户在田间最关心的恰恰是初期病斑因为他们要的是“还没扩散就发现”漏检等于功能失效。原因原始图片是 1600×1600 的分辨率初期病斑只有 30×30 像素。缩放到 640×640 后病斑只剩 12×12 像素经过几次卷积下采样到检测头时特征已经几乎不存在。解决把imgsz从 640 调到 960模型输入分辨率提升小病斑保留下来的特征明显更多另一个做法是把原图切成 1280×1280 的 patch 分别训练和预测预测时再做拼接本质上是在放大病斑的像素占比。两种方法都有效前一种改一个参数就行后一种对部署端要求更高按实际硬件条件选。5.5 标注工具里的漏标与误标靠模型自己纠错不现实现象用 labelimg 或 cvat 检查标注时发现部分图里叶片的病斑被漏标有的把一个病斑拆成了两个标注框还有一些框的边界贴着图片边缘明显是拖拽时手滑。原因2800 张图人工标注出现漏标和边界不准是正常现象不是标注者不认真而是病害图片里有时一片叶子上有几十个小病斑肉眼俯视根本数不全。解决先切出一个“干净子集”训练第一版模型再用这个模型的预测结果辅助标注剩余数据。具体做法是用模型对全部图片预测一遍把置信度低于 0.3 的预测框和高置信度但没对应标注的区域导出来这些是最可能的漏标位置人工只需重点检查这些区域。第一版模型不用追求高精度它是用来筛错的不比人工逐张过一遍慢。6. 训练之后的验收技巧混淆矩阵、实测抽检与坏例归档模型训完不要急着部署。2800 张数据训出来的模型验收环节至少要补三刀看混淆矩阵里有没有系统性误判、做一次完全独立的实测抽检、把预测失败的图归档成下一个版本的数据。第一刀是混淆矩阵。用验证集跑yolo detect val输出目录里会有confusion_matrix.png重点看对角线之外的集中误判尤其是霜霉病和软腐病互相误判、健康叶被误判成病叶。健康叶误判比病害漏检更致命因为用户看到的是“没病说有病”。如果健康叶误判率高检查训练集里的负样本数量是否足够不够就补采。yolo detect val \ modelruns/detect/train/weights/best.pt \ datacabbage_disease/cabbage_disease.yaml第二刀是实测抽检。拿手机去菜市场或大棚拍 2030 张完全不在数据集里的叶子分辨率、角度、背景尽量贴近真实使用场景跑一遍推理并记录每张的预测置信度。这一步如果准确率掉得比验证集低很多基本可以断定之前的数据划分存在场景泄漏回到第四章 4.1 重新划分而不是调模型参数。第三刀是坏例归档这是我最推荐养成的习惯。把第二刀里预测错误的图片全部复制到一个feedback目录积累到 50100 张后人工补标加入下一轮训练集。这个闭环比单纯增加 epochs 有用得多模型犯错的地方就是数据最缺的地方补充这些样本相当于每一轮迭代都在定向修补模型短板。我自己以前拿到标注数据集就开训吃过实拍翻车的大亏之后才养成先画框验证、再按批次划分、最后归档坏例的习惯。这套流程走下来2800 张的大白菜叶片病害数据已经足够跑出能看的结果真正拉开差距的是后面每一轮数据迭代而不是那 150 个 epoch。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网