拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于改进YOLO的玉米病害识别:数据、注意力与部署全攻略

基于改进YOLO的玉米病害识别:数据、注意力与部署全攻略

简介:面向计算机视觉与农业智能化方向的开发者,这份基于改进YOLO的玉米病害识别系统提供完整部署教程与源码,可用于玉米叶部病害识别与定位,适合具备一定深度学习基础的读者作为课程设计或毕业设计的参考。资源包为zip格式,共16个文件,包含8张图片演示、6个Python脚本与2个Markdown说明文档,压缩包整体仅7.02MB,便于快速下载。源码在YOLOv5基础上引入RepVGGBlock等改进模块,从网络深度与宽度两方面增强模型对高密度、小尺寸病斑的检测能力,并配合旋转、缩放、平移等数据增强手段提升泛化效果。Python脚本涵盖模型定义、图像检测、实验对比等环节,例如tf.py、check_img.py、experimental.py分别对应生产环境推理、单图检查与多组试验;README文档则给出部署流程与目录结构说明,帮助使用者快速理清代码逻辑。目前已有293人学习下载,整体代码结构清晰,适合农业病害识别相关项目的二次开发。

1. 基于改进YOLO的玉米病害识别:难的不是检测,是让它在大田里不翻车

我最初把“基于改进YOLO的玉米病害识别系统”当成普通目标检测项目来做,拉个预训练模型就开始训练,演示图片上看着挺好,一到田间拍回来的照片就露馅:早期病斑漏检、叶片重叠时框被截断、露水和反光把置信度拉到零点几。这个标题真正要解决的不是“跑通YOLO”,而是让通用检测器变成能吃玉米叶片数据、能上现场设备、还能跟上季节更新的专用模型。适合课题研究、农业信息化比赛,以及真要往田里推的农技人员。本文按数据、结构改进、训练部署和踩坑一条线往下写,尽量做到每一步都能复现。

2. 数据决定改进上限:玉米叶片数据集、标注与划分的落地做法

2.1 公开数据集和自采样本怎么配比

玉米病害识别的公开数据集不少,常见的有PlantVillage、PlantDoc这类植物病虫害集合,也有科研团队在田间拍的大斑病、灰斑病、锈病和纹枯病图片。公开数据的好处是类别齐全、标注基本干净,坏处是背景太干净——叶片常常平铺在纯色背景上,和真实玉米地里叶片交错、泥土反光、老叶枯黄混在一起的状态差很远。直接用公开数据训练出来的模型,很容易把“健康的绿色纹理”当成模板,一旦遇到带泥土的叶片就误报成病。

我的做法是:公开数据做预训练和类别补充,实际部署前至少自采300到500张田间照片。自采时不要把手机怼在病斑上拍特写,要按植保人员的习惯拍整叶或半叶,因为识别系统的价值在于“扫描式发现”,而不是对着一张已经裁好的病斑图做分类。每张图里的病斑、病叶边界手动标注,健康叶片也要保留一部分,不然模型分不清“没病”和“没见过”。

2.2 VOC转YOLO格式:转换脚本与参数说明

标注工具最常见的是LabelImg,输出Pascal VOC格式的XML,而YOLO训练需要的是每张图一个txt,每行一个目标,格式为“类别ID cx cy w h”,四个坐标都要做归一化。下面这个脚本把XML批量转成YOLO训练所需的txt:

# xml2yolo.py # 将Pascal VOC的XML标注转换为YOLO格式的txt import os from glob import glob import xml.etree.ElementTree as ET # 类别顺序就是训练时的类别ID,训练yaml里的names必须和这里一致 CLASSES = ["healthy", "northern_leaf_blight", "gray_leaf_spot", "rust"] def convert_xml(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in CLASSES: continue box = obj.find("bndbox") x1 = float(box.findtext("xmin")) y1 = float(box.findtext("ymin")) x2 = float(box.findtext("xmax")) y2 = float(box.findtext("ymax")) # 转成中心点坐标再归一化 cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 防止手工标注时跑到图外 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) w = min(w, 1.0) h = min(h, 1.0) # 太小的框会干扰回归损失,直接扔掉 if w < 0.003 or h < 0.003: continue lines.append(f"{CLASSES.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") txt_path = os.path.join(out_dir, os.path.basename(xml_path).replace(".xml", ".txt")) with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) xml_dir = "annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for xml_file in glob(os.path.join(xml_dir, "*.xml")): convert_xml(xml_file, out_dir) print("ok")

脚本逻辑不复杂,但有两个地方容易错。一是CLASSES顺序,你必须和后续训练配置文件里的names顺序完全一致,顺序错了,类别ID就错位,训练出的模型会一直把大斑病当成锈病。二是归一化时要用XML里的图像宽高,而不是自己去读图片尺寸,LabelImg导出的size字段有时候和实际图片不一致,以XML为准更稳。过滤小于0.003的目标,相当于在640分辨率下过滤掉不到2像素的框,这种框本身也标不准,留到训练里只会放大回归噪声。

2.3 按叶片或植株分组切分,不按图片随机切

训练集和验证集如果只是随机按图片切,极有可能出现同一株玉米的照片一部分进了训练集、另一部分进了验证集。玉米病害的纹理、光照条件在同一株上高度相似,这会让验证集mAP虚高,到了新地块马上打回原形。我一般按文件名前缀代表植株或叶片分组,整组划分。

import os import random import shutil random.seed(42) image_dir = "images" pairs = {} for f in os.listdir(image_dir): if not f.lower().endswith((".jpg", ".jpeg", ".png")): continue group = f.split("_")[0] # 按前缀分组,例如 D082_leaf01.jpg -> D082 pairs.setdefault(group, []).append(f) groups = list(pairs.keys()) random.shuffle(groups) split = max(1, int(len(groups) * 0.8)) train_groups = groups[:split] val_groups = groups[split:] for split_name, gs in [("train", train_groups), ("val", val_groups)]: os.makedirs(f"data/corn/{split_name}/images", exist_ok=True) os.makedirs(f"data/corn/{split_name}/labels", exist_ok=True) for g in gs: for f in pairs[g]: base = os.path.splitext(f)[0] shutil.copy(f"{image_dir}/{f}", f"data/corn/{split_name}/images/{f}") lab = f"labels/{base}.txt" if os.path.exists(lab): shutil.copy(lab, f"data/corn/{split_name}/labels/{base}.txt")

固定随机种子是防止同一份数据每次跑出不同结果。按组分组的代价是验证集会损失一部分样本,尤其当自采数据来自十几个地块时,组数少会导致验证集不足。我验证过,宁可验证集少一点,也要保证数据不串组,否则模型的真实泛化能力会被严重高估。

2.4 离线增强补早期病斑:亮度扰动比旋转更管用

YOLO训练本身带Mosaic、MixUp这些在线增强,但玉米病害有个特点:大斑病沿叶脉成长条,灰斑病是密集小点,锈病早期像针尖大小的黄褐色斑点。离线增强时不要对病斑做无脑旋转90度,那会破坏大斑病长条形的形态先验。我更常做的是亮度扰动和局部裁剪放大。

# crop_aug.py # 随机裁剪叶片局部并缩放到640,增加小目标样本 import cv2 import numpy as np def crop_and_resize(src, target_size=640, crop_ratio=(0.7, 1.0)): h, w = src.shape[:2] ch = int(h * np.random.uniform(*crop_ratio)) cw = int(w * np.random.uniform(*crop_ratio)) x0 = int(np.random.uniform(0, w - cw)) y0 = int(np.random.uniform(0, h - ch)) crop = src[y0:y0 + ch, x0:x0 + cw] return cv2.resize(crop, (target_size, target_size))

这种裁剪增强会把原本只有二三十像素的病斑放大到上百像素,对训练小目标检测头很有帮助。要注意的是裁剪会连带改变标注框坐标,所以离线增强之后标注也要跟着变换,落地时麻烦一点但值得。与其一次性生成十万张离线图,不如先把在线增强和这个裁剪增强结合起来,先跑小批量试验。

3. 改进YOLO:注意力、损失函数和训练策略怎么组合

3.1 以YOLOv8s为基线,先定义改进目标

改进YOLO的第一步不是改结构,而是定基线。我通常以YOLOv8s为起点:比v8n有更高精度,比v8m更省显存,部署到工控机和Jetson这类设备都够用。如果终端设备内存特别紧张,可以退到v8n;如果数据量大且GPU显存充足,再试v8m。玉米病害场景里,模型要解决的三类问题是:早期病斑小、病斑颜色与叶片背景接近、叶片重叠遮挡。围绕这三个问题去改,而不是为了发论文堆模块。

改进优先级我会这样排:数据和增强排在第一位,然后是检测头或注意力,最后才是损失函数。很多人上来就换注意力模块,结果训练几天mAP没有变化,原因不是注意力没用,而是数据里根本没有足够的难例。先做难例挖掘,再谈结构。

3.2 在SPPF前插一个轻量注意力模块:CBAM的写法与插入位置

给YOLOv8加注意力模块,常见的做法是把CBAM放在骨干网络最后一个stage之后、SPPF之前。理由是:经过多层下采样后特征图分辨率降低,通道注意力能帮模型抓住病斑的颜色差异,空间注意力能帮模型在叶片重叠区域聚焦边缘纹理。这个位置计算量小,对整体推理速度影响不大。

# cbam.py # 一个不依赖特定Ultralytics版本的CBAM实现 import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, in_channels, reduction=16): super().__init__() self.mlp = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, max(in_channels // reduction, 8), 1), nn.ReLU(inplace=True), nn.Conv2d(max(in_channels // reduction, 8), in_channels, 1), nn.Sigmoid(), ) self.spatial = nn.Sequential( nn.Conv2d(2, 1, 7, padding=3), nn.Sigmoid(), ) def forward(self, x): x = x * self.mlp(x) # 通道注意力 avg = x.mean(dim=1, keepdim=True) mx = x.max(dim=1, keepdim=True).values x = x * self.spatial(torch.cat([avg, mx], dim=1)) # 空间注意力 return x

接入时需要把CBAM注册到Ultralytics的模块列表里,再在模型配置文件中引用。由于不同版本模块注册路径有差异,这里只说通用做法:在conv.py或其他模块文件里import CBAM,然后在forward里把SPPF的输入替换成CBAM的输出。插入后务必跑一次ablation,不是插得越多越好。我试过在三个下采样层后都插CBAM,mAP50-95没有明显涨,推理延迟倒是涨了15%。如果数据量只有一两千张,插一处就够。

3.3 损失函数:先看懂box、cls、dfl各自管什么

YOLOv8的损失函数包含三部分:box_loss负责框回归,cls_loss负责分类,dfl_loss负责边框分布建模。很多教程喜欢直接让你把CIoU换成WIoU或Inner-IoU,但换之前要先判断你的模型到底在哪个环节漏检。如果是小病斑根本没框出来,那问题更多在cls_loss,模型把病斑和叶片纹理归成了一类;如果是框出来了但位置偏、导致mAP50不涨,才是box_loss的问题。

from ultralytics import YOLO model = YOLO("yolov8s_corn.yaml") model.train( data="corn.yaml", epochs=80, imgsz=640, batch=16, device=0, box=7.5, cls=0.7, dfl=1.5, )

这里把cls权重从默认的0.5提到0.7,是因为玉米病害里早期病斑容易漏检,加大分类权重会让模型更积极地把疑似病斑判为正样本。如果发现框偏移严重,可以反过来加大box权重到8.0到9.0。注意,这只是经验起点。训练时盯住三类loss曲线:如果box_loss降得很慢,但cls_loss正常,优先怀疑标注框质量,而不是模型结构;如果cls_loss一直震荡,优先怀疑类别不均衡或数据增强过强。

3.4 换WIoU或Inner-IoU的适用边界

把CIoU换成WIoU是最近项目里常见的一步。WIoU对低质量样本的权重更动态,能降低遮挡、露水反光这类难样本对梯度方向的干扰。但我不会把它放在第一版实验里,因为收益在数据量少时不明显,还容易让训练震荡。

损失函数适合情况注意事项
CIoU(默认)正常叶片、中等尺寸目标多改动最小,先跑基线
WIoU v1/v2/v3低质量样本多、遮挡或露水反光v3动态权重更激进,需要降学习率
Inner-IoU小目标多,想加快回归收敛需要额外设置ratio,一般取0.7到0.9

跑结构对比时,最少要用三个随机种子各跑一遍,取mAP50-95的平均值,因为单次实验的方差可能比改动带来的收益还大。玉米病害里mAP50-95比mAP50更敏感,特别是灰斑病这种密集小斑点,mAP50只看粗重合度,往往会骗人。

4. 部署教程:从预训练模型下载到训练、导出、图片演示

4.1 环境准备和预训练模型下载

部署教程的第一步是把环境装干净。常见做法是建一个独立的conda环境,Python版本选3.10左右,然后安装Ultralytics。代码:

conda create -n corn python=3.10 -y conda activate corn pip install ultralytics

训练玉米病害模型时,推荐从COCO预训练权重开始,而不是随机初始化。Ultralytics在命令行里指定model=yolov8s.pt,联网状态下会自动下载预训练模型到当前目录。如果训练机没有外网,需要先在下载好的环境里执行一次YOLO("yolov8s.pt")把权重缓存下来,再拷贝到训练机。手动保存权重时注意文件名后缀不要改错,.pt文件是PyTorch序列化格式,改错了加载会报错。

4.2 数据目录与训练命令的关键参数

数据集目录建议按YOLO约定组织,一个Data YAML指向训练集和验证集路径。

# corn.yaml path: ../datasets/corn train: train/images val: val/images nc: 4 names: ["healthy", "northern_leaf_blight", "gray_leaf_spot", "rust"]

训练命令我一般这样写:

yolo detect train \ model=yolov8s.pt \ data=corn.yaml \ epochs=80 \ imgsz=640 \ batch=16 \ device=0 \ patience=15 \ project=runs/corn \ name=cbam_run

几个关键参数再解释一下。imgsz=640是沿用预训练分辨率,不要一上来就上960,显存占用会涨很多,而玉米早期病斑的增益要到后面对比实验里才看得见。batch=16是我在这个数据规模下的经验值,太小BN统计不稳,太大容易过拟合到公开数据的干净背景。patience=15表示15轮指标没提升就提前停止,比硬跑完80轮更能防过拟合。

参数建议值理由
epochs80到150自采数据少时,100轮内基本收敛
imgsz640起步沿用预训练尺度,显存可控
batch16或以上尽量让BN统计稳定
lr00.01以下迁移学习时学习率大了容易崩
patience15到20玉米病害收敛慢,太早停会丢最佳点

训练完成以后,输出会在runs/corn/cbam_run/weights/目录下生成best.pt和last.pt。best.pt是按验证集mAP选出来的,部署只用它,last.pt留着继续训练或做EMA对比。

4.3 验证:mAP50和mAP50-95分别看什么

验证命令:

yolo detect val \ model=runs/corn/cbam_run/weights/best.pt \ data=corn.yaml \ imgsz=640 \ conf=0.25 \ iou=0.5

这个命令会在项目输出目录下生成混淆矩阵、PR曲线、F1曲线和每类的AP报告。玉米病害任务里,我习惯把每类的AP50单独列出来看。如果锈病AP50只在0.2上下,而健康叶片AP50到0.95,说明模型把大部分锈病当成了健康纹理,这时就不要急着改模型,先回数据里看锈病样本的框是不是标得太粗糙。验证集图片的命名也要保留,方便反向查到每一张误检图。

4.4 导出ONNX,并写一个三行代码的图片演示

部署到边缘设备前,先把PyTorch模型导出成ONNX。这样后续用ONNX Runtime,或者再转OpenVINO、TensorRT都有基础。

yolo export \ model=runs/corn/cbam_run/weights/best.pt \ format=onnx \ imgsz=640 \ dynamic=True \ simplify=True

dynamic=True是让ONNX支持动态输入尺寸,否则后面换不同分辨率推理会报错。simplify=True会做一轮图优化,有时候能把模型文件缩小不少。导出后写一个最小推理脚本,验证图片演示效果:

from ultralytics import YOLO model = YOLO("runs/corn/cbam_run/weights/best.pt") results = model.predict( "demo_images/", # 可以是目录,也可以是单张图片路径 conf=0.25, iou=0.5, imgsz=640, save=True, save_txt=True, ) for r in results: print(r.boxes.cls) # 打印每张图检测到的类别ID

带save=True会把标注好的图片保存到runs/detect目录,save_txt=True会额外输出每个目标的坐标txt,方便后续统计病害数量或生成报告。如果要把摄像头接到系统里,把source参数改成0即可,但摄像头推理时建议先把imgsz固定,不要动态切换,保证推理延迟稳定。

5. 避坑:玉米病害训练里最容易翻车的四个点

5.1 BN崩溃:loss突然变成NaN

现象:训练到第30到50轮,box_loss突然变成NaN,后面所有指标都归零,重启训练还是在一个相近位置崩掉。

原因:最常见是batch太小导致BN统计不稳定,其次是学习率过大,在训练中段把梯度推到数值溢出。还有一个隐藏原因是混合精度下某些自定义注意力模块的数值范围控制不到位,FP16计算时容易溢出。

解决:先保证batch至少16,如果显存不够,用更小的模型或降低输入尺寸,不要硬扛。再把lr0降到0.005到0.01,并增加warmup轮数。代码里显式设置:

model.train( data="corn.yaml", epochs=80, batch=16, lr0=0.005, warmup_epochs=3, amp=True, # 如果还崩,就改成amp=False )

如果关闭AMP后不再崩溃,说明问题出在FP16数值溢出,可以对自定义模块单独保持FP32计算,而不是整个训练关掉混合精度。

5.2 混淆矩阵总合对不上:这是框数不是图数

现象:测试集2000张图,我统计大约有3200个标注目标,但训练生成的混淆矩阵里所有格子的总数既不是3200,也不是2000,怎么加都对不上。

原因:混淆矩阵统计的是框级别的事件,不是图片级别。一张图里可以同时有多个GT框和多个预测框,低置信度预测会被归到“background”一类,多个预测命中一个GT时也只算一个命中。它的作用是看类别之间的混淆模式,不是用来对总数的。

解决:矩阵只看主对角线占比和最容易混淆的类别对,比如灰斑病被误判成健康叶片的比例。如果确实需要图级误检率,就自己写一个统计脚本,把“一张图没有任何预警但GT存在”的情况单独算成漏检,而不是从混淆矩阵反推。

5.3 锈病样本少,整体mAP被平均拉低

现象:训练损失平稳下降,但验证时健康叶片AP501接近0.9,锈病AP50只有0.15。

原因:公开数据和自采数据里健康叶片占比过高,锈病早期样本太少。模型学会了“绿色叶片就是健康”,锈病针尖大小的褐点被当作背景噪声滤掉。

解决:先做类别数量统计,把锈病样本扩充到至少占总目标数的10%以上,不够就去拍或做局部裁剪增强。训练时把cls权重提高一点,让分类损失对少数类更敏感。还不行就按类组织训练集,每个batch里强制采样锈病样本,不是全局随机采样。

5.4 导出ONNX后掉点:PyTorch里正常,ONNX Runtime里丢框

现象:用best.pt做图片推理一切正常,但导出成ONNX后用onnxruntime推理,原本能检出的灰斑病小目标变成漏检,或者同一张图输出框数量减少。

原因:最常见是导出时没有开动态轴,模型被锁死在训练时的固定尺寸;其次是opset版本过老,某些新算子被替换成兼容版本后精度损失;再有一个就是自定义注意力模块里有对动态shape敏感的操作,simplify阶段把它优化坏了。

解决:导出时明确设置dynamic=True、opset=12或更高。导出的ONNX先用onnx.shape_inference.infer_shapes检查一遍输入输出维度,再用同一张图对比PyTorch输出,逐类看置信度。如果自定义CBAM导致掉点,优先把CBAM从模型里去掉再导出对比一次,确认是不是它引起的。

6. 进阶:把改进YOLO推到每天能用的位置

6.1 用OpenVINO或TensorRT把推理延迟压下来

模型能跑通只是第一步,真正常年放在田间的设备多半是CPU工控机或嵌入式GPU。CPU上优先转OpenVINO,NVIDIA设备上优先转TensorRT。OpenVINO导出命令是:

yolo export \ model=runs/corn/cbam_run/weights/best.pt \ format=openvino \ imgsz=640

转完以后不要只测一张图,要拿整个验证集跑一遍,对比转换前后每类的mAP50。如果转换后某类掉点超过两个百分点,先看是不是动态输入尺寸没处理好,再看是不是模型里有特殊算子被转换器替换成精度较差的实现。调用新格式时记得改模型后缀:

from ultralytics import YOLO model = YOLO("runs/corn/cbam_run/weights/best_openvino_model") results = model.predict("field_test/", conf=0.25, iou=0.5)

6.2 用置信度回流做半自动标注,让模型跟着季节更新

玉米病害随季节变化明显,七月的锈病和九月的大斑病形态差异很大,模型不能训完就扔在一边。我现在的方法是每周收集一批当季照片,先用当前模型自动标注:

model = YOLO("best.pt") results = model.predict("new_images/", conf=0.8, save_txt=True, project="auto_label")

置信度大于0.8的框直接作为预标注,低于0.3的框删掉,中间部分人工复核。这样做比从头标注省不少时间,而且能积累大量当季难例,然后和旧数据合并重新训练。每次重新训练后都做一次验证集回归测试,确认新模型没有把上个月的病害类型“忘掉”。我自己现在的习惯是:每来一批新照片,先自动标注、再人工抽检、最后增量训练,三个步骤形成固定流程,比一次性追求一个完美模型实用得多。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表