简介:面向计算机视觉与深度学习研究者的烟叶病害检测技术讲解PPT,围绕改进YOLOv5算法在烟草病害识别中的应用展开。内容系统覆盖烟叶病害的经济危害背景、传统诊断方法的时效性局限、嵌套式YOLOv5识别方法、环境搭建、数据集采集与标注、模型训练及系统整合等完整技术链路;预览内容还涉及优化函数(Adam/SGD)、激活函数(Leaky-ReLU/Sigmoid)、损失函数(GIOU/CIOU)对比,以及添加注意力模块等YOLO改进思路,有助于快速梳理算法脉络与论文研究方向。包体为1个pptx演示文稿,约10.34MB,图文结构完整,涵盖从问题提出到方案落地的全过程,适合学术汇报、方案答辩或算法入门参考。目前已有3118人学习/下载,是快速了解烟叶病害检测课题方案、获取汇报素材的实用资料。
1. 烟叶病害检测为什么值得用改进YOLOv5来做
烟草种植是很多产区农户的主要收入来源,而烟叶病害一旦在团棵期到旺长期爆发,往往几天内就蔓延到整片烟田。传统的人工巡检靠植保员一片片翻叶子,经验差异大,漏检率高,等发现时已经过了最佳防治窗口。把目标检测模型接到田间拍照设备上,让系统自动标出病斑位置和病害类别,是这几年植保信息化里落地最快的一条路。而 YOLOv5 之所以被选作基底,不是因为它的 mAP 最高,而是因为它在边缘设备上的推理速度和工程成熟度最平衡——改进 YOLOv5 去做烟叶病害检测,本质上是在「检测精度」和「可部署性」之间找一个现实可用的折中方案。这套系统适合谁?适合有 Python 基础、想把手头烟叶图像数据变成检测模型的学生或一线技术人员,也适合正在做智慧农业项目汇报、需要交付可演示原型的团队。
2. 先把数据和环境铺平:烟叶病害数据集的整理与训练环境搭建
2.1 病害类别怎么定:从植保标准到检测标签
做检测系统第一步不是写模型,而是把「病害」这个业务概念翻译成计算机能学习的标签体系。烟叶常见病害里,最容易和 YOLOv5 检出能力匹配的是具有明显病斑的类别——赤星病、野火病、角斑病、气候斑点病,加上病毒类的脉斑病。这里要注意,不是所有病害都适合目标检测:像花叶病这种整叶均匀褪绿的,用分类网络做整叶判别更合适,强行画框反而会增加标注噪音。
我一般会先拉一份产区植保站的病害图谱,用 Excel 把每类病害的典型症状、高发时期、病斑形状列出来,再对着原始图片确认哪些类别视觉可分。这一步的产出是标签类别清单,比如:
classes = ['juxing_bing', 'yehuo_bing', 'jiaoban_bing', 'qihou_bandian', 'maiban_bing']类别数量控制在 5 类以内时,YOLOv5 的训练难度和漏检率都比较好控制。如果一张叶片上同时有赤星病和野火病,标注时两个框都要画,这也是后面评估时容易打架的地方。
2.2 数据增强脚本:用小样本撑起烟叶病害的多样性
烟叶病害图像采集往往受季节限制,一个产区一年能拍到的有效病害图可能只有几千张。直接拿这点数据训 YOLOv5 很容易过拟合,尤其是病斑纹理变化大的野火病。这里需要在训练前对数据集做增强,常见做法是先用脚本把原始图扩增 5 到 8 倍。
下面这个脚本是增强阶段比较实用的一份参考,生成 mosaic、旋转、亮度和噪声扰动样本,输出为 YOLO 格式的 txt 标注:
import cv2 import numpy as np import os, random # 原始图片与标签目录 src_img_dir = 'raw_images/' src_lbl_dir = 'raw_labels/' out_img_dir = 'aug_images/' out_lbl_dir = 'aug_labels/' os.makedirs(out_img_dir, exist_ok=True) os.makedirs(out_lbl_dir, exist_ok=True) for img_name in os.listdir(src_img_dir): img_path = os.path.join(src_img_dir, img_name) label_path = os.path.join(src_lbl_dir, img_name.replace('.jpg', '.txt')) img = cv2.imread(img_path) h, w = img.shape[:2] # 亮度与对比度扰动,模拟不同光照 alpha = random.uniform(0.8, 1.2) beta = random.randint(-25, 25) img_aug = cv2.convertScaleAbs(img, alpha=alpha, beta=beta) # 旋转 90 度的整数倍,避免标注框变形 k = random.choice([0, 1, 2, 3]) img_aug = np.rot90(img_aug, k) # 高斯噪声,模拟田间手机拍摄的噪点 noise = np.random.normal(0, 8, img_aug.shape).astype(np.uint8) img_aug = cv2.add(img_aug, noise) # 写增强图 aug_name = img_name.replace('.jpg', f'_aug{random.randint(100,999)}.jpg') cv2.imwrite(os.path.join(out_img_dir, aug_name), img_aug) # 旋转后原标注框坐标也跟着旋转,这里要同步处理 boxes = [] with open(label_path, 'r') as f: for line in f.readlines(): cls, xc, yc, bw, bh = line.strip().split() boxes.append([int(cls), float(xc), float(yc), float(bw), float(bh)]) with open(os.path.join(out_lbl_dir, aug_name.replace('.jpg', '.txt')), 'w') as f: for cls, xc, yc, bw, bh in boxes: # 90度旋转的坐标变换:中心点 (xc,yc) -> (1-yc, xc) xc_new, yc_new = 1 - yc, xc bw_new, bh_new = bh, bw f.write(f'{cls} {xc_new:.6f} {yc_new:.6f} {bw_new:.6f} {bh_new:.6f}\n')这段脚本的关键在于旋转增强时坐标必须跟着转,否则标签和图像对不上,训练时模型会学着预测错误位置。YOLO 格式的坐标是归一化后的中心点加宽高,旋转 90 度时中心点坐标互换算一次、宽高互换一次即可。亮度扰动系数 alpha 建议不要低于 0.7,否则暗光样本会把病斑细节抹掉,反而让模型学到错误纹理。
2.3 环境配置:Python 版本与 YOLOv5 依赖的匹配
训练环境这块踩坑最多的是 Python 版本和依赖库版本打架。YOLOv5 官方仓库对 Python 3.8 到 3.11 都有较好支持,但如果你用的是较新的 Python 3.12,torch 和 torchvision 的版本匹配要格外小心。我一般建议用 conda 单独建一个环境,Python 固定为 3.9,torch 选 1.13 或 2.0 系列,这样能避开很多兼容性报错。
用 vscode 配置完 Python 环境后,安装关键依赖的命令大致是:
conda create -n tobacco python=3.9 -y conda activate tobacco pip install torch==2.0.1 torchvision==0.15.2 pip install -r requirements.txt其中 requirements.txt 里的 numpy 版本要注意,YOLOv5 老版本代码对 numpy 1.24 以上某些接口不兼容,常出现np.int报错。解决方式是锁定 numpy 版本为 1.23.5,或者升级 YOLOv5 代码到较新版本。训练时显存不够的话,batch size 调小比换显卡更实在,YOLOv5 对小 batch 的 BN 层处理在 8 以下时衰减明显,所以数据增强阶段尽量把每个类别的样本量补足,缓解 batch 小带来的统计不稳定。
3. 改网络:从YOLOv5基线到烟叶病害检测的四个改进点
3.1 小病斑漏检的根源:下采样次数与特征图尺寸
烟叶病斑的特点是面积占比小、边缘不规则,赤星病初期的病斑直径可能只有几个像素到十几个像素。YOLOv5 的默认检测头在 80x80、40x40、20x20 三张特征图上做预测,小目标主要靠 80x80 的 P3 层,但经典结构里 P3 层的语义信息偏弱,病斑纹理容易在多次卷积后被背景淹没。改进的常见思路是增加一次上采样融合,让 P2 层(160x160)也参与检测,或者调整 anchor 的尺寸分布。
改 yaml 是最直接的做法。以 YOLOv5s 为基础,把检测层从 3 层扩到 4 层:
# yolov5s_tobacco.yaml nc: 5 # 烟叶病害类别数 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [5, 6, 8, 10, 12, 15] # P2 层小 anchor - [10, 13, 16, 30, 33, 23] # P3 层 - [30, 61, 62, 45, 59, 119] # P4 层 - [116, 90, 156, 198, 373, 326] # P5 层这里 anchor 的数值来自对烟叶病斑标注框的聚类统计,不是照搬 COCO 的默认值。跑训练前用 kmeans 脚本对训练集 txt 里的宽高做一次聚类,再把结果填进 yaml。有些同学不做这一步,直接沿用默认 anchor,小病斑的 recall 会明显偏低——这就是泥潭里最容易陷进去的细节。
3.2 注意力模块插入:让网络盯着病斑而不是叶脉
网上的改进 YOLOv5 十有八九会加注意力机制,但加的位置和方式差别很大。烟叶病害里,赤星病和角斑病都需要模型区分「病斑」和「叶脉纹理」,SE 或 CBAM 加在主干尾部效果不明显,因为病斑尺度小,通道注意力在深层已经丢了空间细节。更适合的做法是在 Neck 的 P3 层前插入一个轻量注意力块。
下面这段代码是在 YOLOv5 的 common.py 里追加一个简化的坐标注意力模块(CA)并注册:
import torch import torch.nn as nn import torch.nn.functional as F class CoordAtt(nn.Module): def __init__(self, inp, oup, reduction=8): super().__init__() # 先做 1x1 卷积压缩通道,减少计算量 self.conv = nn.Sequential( nn.Conv2d(inp, inp // reduction, 1, bias=False), nn.BatchNorm2d(inp // reduction), nn.ReLU(inplace=True) ) # 水平方向和垂直方向各自生成注意力权重 self.conv_h = nn.Conv2d(inp // reduction, oup, 1, bias=False) self.conv_w = nn.Conv2d(inp // reduction, oup, 1, bias=False) def forward(self, x): b, c, h, w = x.size() x_h = x.mean(dim=3, keepdim=True) # 水平池化 x_w = x.mean(dim=2, keepdim=True) # 垂直池化 x_cat = torch.cat([x_h, x_w], dim=2) x_cat = self.conv(x_cat) x_h, x_w = x_cat.split([h, w], dim=2) x_h = torch.sigmoid(self.conv_h(x_h)) x_w = torch.sigmoid(self.conv_w(x_w)) return x * x_h * x_w这段代码里,水平池化和垂直池化分别捕获病斑在行、列方向上的分布响应,得到的两个权重图相乘后作用回原特征图。和 SE 模块只做通道加权相比,它保留了空间位置信息,对细长形病斑更友好。插入位置是在 Neck 的 Concat 之后、Detect 之前,以 YOLOv5s 的 yaml 为例,在 Head 部分对应层号的 Concat 后加一行CoordAtt即可。加了之后参数量涨得不多,但推理时间会略微增加,边缘设备上要考虑取舍。
3.3 损失函数与正样本匹配:病斑样本不平衡怎么办
烟叶病害数据里,背景占比极高,一张 640x640 的图里病斑区域可能只占 2% 到 5%。YOLOv5 默认的 CIoU 损失对框的位置回归比较友好,但分类损失在极端不平衡下容易让模型偏向预测为背景。常见的处理方式有两种:一是调整 loss 里的类别权重,让少数类别的梯度贡献更大;二是替换成 VariFocal 或 Focal Loss 变体,把难样本的权重提上来。
YOLOv5 的 loss.py 中分类损失默认是 BCEWithLogitsLoss,改成带 alpha 参数的 Focal Loss 是相对可控的改进:
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, preds, targets): # preds: 预测 logits, targets: 0/1 标签 bce = F.binary_cross_entropy_with_logits(preds, targets, reduction='none') p = torch.sigmoid(preds) # 对难样本放大损失,对易分样本压低损失 focal_weight = torch.where(targets == 1, (1 - p) ** self.gamma, p ** self.gamma) return (self.alpha * focal_weight * bce).mean()alpha 取 0.25 表示正样本的损失权重被压低,避免背景和前景极端不平衡时模型只学背景。gamma 越大,模型越关注难分样本,但 gamma 超过 2.5 后训练容易震荡,烟叶病害场景下 2.0 是稳妥起点。改完 loss 后要留意训练日志里的cls_loss,如果下降曲线比 baseline 更陡,说明正样本梯度确实被激活了,如果反而更慢,就是权重设大了。
3.4 后处理 NMS 的边界:病斑重叠框怎么压下来
YOLOv5 默认的 NMS 在病斑密集区域有个典型问题:相邻病斑的预测框互相抑制,导致一个病斑被删掉。赤星病爆发期的叶片上病斑间距极小,IOU 很容易超过默认的 0.45 阈值,模型输出时就会发现「同一个位置只留一个框,另一个病斑丢了」。改进思路是换用 Soft-NMS 或 DIoU-NMS,把重叠框的分数衰减而不是直接置零。
在 YOLOv5 的 general.py 里,把 NMS 函数换成 DIoU-NMS 的核心差异是衰减公式:
def diou_nms(bboxes, scores, iou_thresh=0.5): # bboxes: [N, 4] 的 xyxy 格式, scores: [N] # 这里的实现省略排序细节,核心是衰减而非删除 keep = [] order = scores.argsort(descending=True) while order.numel() > 0: i = order[0] keep.append(i) # 计算其余框和当前框的 DIoU ious = compute_diou(bboxes[order[1:]], bboxes[i]) # 把 IoU 超过阈值的框分数乘以衰减系数,而不是直接丢掉 scores[order[1:]][ious > iou_thresh] *= 0.6 order = order[1:][scores[order[1:]] > 0.1] return keep改成衰减而不是置零之后,重叠病斑的两个框都有机会保留,代价是假阳性可能略微上涨。如果实际测试时发现误检变多,可以把衰减系数从 0.6 调到 0.4,或者把阈值从 0.5 提到 0.55。这一步属于工程调参的范畴,没有绝对最优值,要在产区验证集上跑了之后才能定。
4. 训练跑通与超参数调优:让改进YOLOv5在烟叶病害上收敛
4.1 训练数据组织:train/val 划分与数据 yaml 的写法
YOLOv5 的数据组织方式比较固定,图片和标注要分目录放好,图片用 jpg 或 png,标注 txt 和图片同名。train 和 val 的比例我习惯按 8:2 切,如果某类病害样本特别少,先把该类样本全部放进 train,在 val 里留少量代表即可,否则 val 的 mAP 波动会非常大。数据 yaml 这样写:
# tobacco_data.yaml train: /data/tobacco/train/ val: /data/tobacco/val/ nc: 5 names: 0: 'juxing_bing' 1: 'yehuo_bing' 2: 'jiaoban_bing' 3: 'qihou_bandian' 4: 'maiban_bing'训练前用一行命令检查标注是否可读:
python train.py --data tobacco_data.yaml --weights '' --cfg yolov5s_tobacco.yaml --img 640 --epochs 100如果标签文件缺失或格式错误,训练会直接跳过该图,日志里skipped数量过多时就要停下来检查标注。常见翻车点是用 LabelImg 导出时类别名和 yaml 里的 names 对不上,训练集里某一类的标注被全部跳过,模型训练完对这一类完全无感。
4.2 关键超参数的含义与建议区间
YOLOv5 的超参数在 hyp.scratch-low.yaml 里定义,烟叶病害检测需要重点调的是以下几个:
| 参数 | 默认值 | 烟叶场景建议 | 说明 |
|---|---|---|---|
| lr0 | 0.01 | 0.005~0.01 | 初始学习率,小数据集偏高容易震荡 |
| lrf | 0.2 | 0.1~0.2 | 最终学习率系数,影响后期收敛精度 |
| momentum | 0.937 | 0.9~0.937 | 动量,微调阶段可以降到 0.9 |
| weight_decay | 0.0005 | 0.0005~0.001 | 正则化,样本少时适当加大 |
| warmup_epochs | 3.0 | 3~5 | 预热轮数,让 BN 统计先稳定 |
| iou_t | 0.20 | 0.20~0.25 | 正样本匹配阈值,小目标建议 0.2 |
训练命令里通过--hyp指定修改后的超参数文件。跑第一个 epoch 时要观察 loss 曲线是否下降,如果前 3 个 epoch loss 不降反升,大概率是学习率太大或数据标注有大量错框。还有一种常见情况是背景占比太高,模型前 20 轮一直在学习「不检出」,这时可以考虑把 mosaic 增强概率调低,让模型更早看到真实病斑形态。
4.3 迁移学习:用 COCO 预训练权重还是从零训
烟叶病害数据集再大也很难和 COCO 的千万级图像规模相比。从零训练 YOLOv5s 在 5000 张图的数据集上,mAP 往往不如用 COCO 预训练权重微调后的效果。训练时加载官方预训练权重的命令是:
python train.py --data tobacco_data.yaml --cfg yolov5s_tobacco.yaml --weights yolov5s.pt --epochs 150 --batch-size 16 --img 640 --device 0加载预训练权重后,前几轮骨干网络的梯度会被冻结权重主导,收敛速度显著加快。这里有个实践细节:烟叶图像和 COCO 图像风格差异大,冻结前 10 层骨干往往不如只冻结 BN 层效果好。做法是在 train.py 里把 backbone 参数的requires_grad全部设为 True,但把 BN 层的track_running_stats保持默认,让网络快速适应病斑纹理的同时不破坏已学到的通用特征。epoch 150 对 5000 张级别数据来说基本够用,超过 200 轮容易过拟合,验证集的 mAP 曲线如果出现明显回退就要早停。
4.4 训练过程中看什么:日志指标与曲线判读
训练时终端会输出每个 epoch 的 loss 组成,包括 box_loss、obj_loss、cls_loss 和最终的 mAP。这里有一个判断模型是否学到位的小技巧:obj_loss 如果降不下去,说明模型对「哪里是目标」的定位还不准,问题往往出在 anchor 设置或数据标注的框边界太松;cls_loss 降不下去则说明类别区分不够,注意力模块或损失函数权重需要调整。
训练完成后会生成runs/train/exp目录,里面包含 results.png 的曲线图和混淆矩阵。PR 曲线上如果 P 高 R 低,说明模型检出的基本都是对的,但漏了很多病斑,这时需要向下调 NMS 阈值或增大 anchor 数量;R 高 P 低则相反,误检太多,需要提高置信度阈值或加强背景样本的抑制。这些判断直接影响后面的部署策略,一定要在训练阶段就看清楚,而不是等部署到田间的设备上再后悔。
5. 避坑与排查:烟叶病害检测训练里最常翻车的5个问题
5.1 标注框越界导致训练 loss 变成 nan
现象:训练到中途,box_loss 突然变成 nan,之后 mAP 一直停在 0。
原因:YOLO 格式标注的中心点坐标和宽高归一化后超过 0 到 1 的范围,尤其是旋转增强脚本里坐标算错时,边界框一部分在图像外,模型回归时梯度爆炸。
解决:训练前对 train 和 val 的每个 txt 做一次合法性检查,把超出边界的框裁剪回图像范围内,或者直接丢弃该样本。可以写个脚本遍历所有标注文件,检测xc + bw / 2 > 1或xc - bw / 2 < 0的情况。
5.2 叶子互相遮挡导致病斑框标注不一致
现象:验证集上模型对某一类病斑的 mAP 特别低,但训练集 loss 已经很低,明显过拟合。
原因:叶片重叠区域里,标注员对同一张图的理解不同,有的人把遮挡部分画在框内,有的人只画可见区域。模型学习到的边界语义是混乱的。
解决:重新看一遍该类别的全部标注图,统一规则——可见部分超过病斑总面积一半就完整画框,否则不标。把标注规则写进项目文档,多人标注时还要每张图抽查,保证前后一致。这是数据质量里最耗时但最值得投入的部分。
5.3 增强脚本把病斑纹理磨没了,模型学不到特征
现象:加了旋转和亮度增强后,训练 loss 下降很快,但验证集效果比不增强还差。
原因:亮度扰动范围太大,低亮度下病斑和健康叶肉的对比度消失;高斯噪声太强,把病斑边缘的细小锯齿纹理抹平了。
解决:把亮度扰动系数限制在 0.85 到 1.15,高斯噪声标准差从 8 降到 4,随机裁剪时保证裁剪区域里至少包含一个完整病斑。增强的目的不是把图变花哨,而是模拟不同光照和拍摄角度下的真实变化,超过真实范围的扰动只会教坏模型。
5.4 部署到树莓派或 Jetson 上 fps 太低,达不到实时
现象:训练好的模型在 PC 上跑 30ms 一帧,换到树莓派 5 上变成 300ms 一帧,演示时卡顿明显。
原因:float32 的 YOLOv5s 模型在 ARM 设备上计算量大,而且没有用硬件加速库做算子优化。
解决:先用torch.onnx.export导出 ONNX,再转成 TensorRT 的 fp16 或 int8 模型(Jetson 平台)或者 rknn 模型(瑞芯微平台)。树莓派上没有专用 NPU 的话,只能把输入尺寸从 640 降到 416,同时把 NMS 的置信度阈值从 0.25 提到 0.4,减少后处理的计算量。实测 fps 能提升 3 到 5 倍,代价是 mAP 下降 1 到 3 个点,在病害检测这种场景里可接受。
5.5 病毒类病害和缺素症状分不清,误检率飙升
现象:把花叶病样本加进训练集后,健康但缺氮的叶片也被检测成花叶病。
原因:花叶病和缺素症状在视觉上都表现为叶片颜色不均匀,而且没有明显的病斑边界,检测框的语义本身就不清晰。
解决:这类病害不要硬塞进目标检测系统,在业务上做分流——有清晰病斑的用 YOLOv5 检测,整叶变色的用分类网络做第二级判断。检测系统输出的低置信度框交给分类模型复核,可以避免误检。这个方案比强行调模型更符合植保业务的真实判断逻辑。
6. 验证模型并收进PPT:从混淆矩阵到演示脚本的一站做法
训练完成后,评估环节要跑全部验证集而不是只看训练时的末轮指标。YOLOv5 自带的 val.py 可以输出各类别的 precision、recall 和 mAP50,但更好的做法是直接看保存的 confusion_matrix.png——它比 mAP 数字更能反映哪两类病害互相混淆。烟叶病害里最常见的是野火病和角斑病互相认错,因为两者病斑都偏褐色且形状不规则。如果混淆矩阵里这两类的互相误判率超过 10%,回看标注时重点观察这两类的边界图例,往往能找到标注判据不统一的证据。
演示脚本方面,给 PPT 答辩准备一段推理演示比贴训练曲线更有说服力。写一个简单的推理脚本,从视频或图片序列里逐帧检测,实时标注出病斑框和类别:
import cv2 import torch model = torch.hub.load('./yolov5', 'custom', path='best.pt', source='local') model.conf = 0.3 model.iou = 0.45 cap = cv2.VideoCapture('field_video.mp4') while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame) # 把检测结果画回原帧 annotated = results.render()[0] cv2.imshow('tobacco_detect', annotated) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这里的model.conf = 0.3在演示时要根据现场画面动态调,光线好的时候提到 0.4 减少误检,光线差的时候降到 0.25 保证召回。视频演示容易暴露模型的真实弱点,建议提前用产区实拍视频试跑一遍,把容易误检的帧挑出来,思考怎么在 PPT 里解释——可以在现场展示时主动说「这种缺素叶片被模型标记为可疑病害,置信度只有 0.3,可以通过第二级分类进一步确认」,这比掩饰翻车更能体现工程思维。这几年做烟叶检测项目,我最大的教训就是数据和标注规则决定上限,模型改进只是把上限兑现出来,希望帮到你。
本文还有配套的精品资源,点击获取