简介:目标检测技术作为计算机视觉的核心任务,在农业智能化中发挥着关键作用。通过深度学习模型对农作物图像进行实时分析,能够有效识别病害区域并精准定位。YOLO凭借速度与精度的平衡,成为此类实际项目的首选框架。围绕玉米病害识别场景,完整展示了数据准备、标注规范、模型训练与调优、部署推理的工程化流程,并针对训练指标为0、小目标漏检等典型问题给出了排查方案。无论入门开发者还是农业工程师,都能从中获得可复用的实践经验。 玉米病害对产量的影响有多狠,干农业的人心里都有数。大斑病、小斑病、锈病这些常见病害,一旦大面积爆发,减产百分之二三十是常事,碰上流行年份甚至更高。但问题是,病害识别这件事,以前靠人眼,经验老到的植保员能看出来,普通种植户往往等病斑蔓延了才发现,一耽误就是最佳防治窗口。我做的这个"基于YOLO的玉米病害识别"项目,说白了就是把这套人眼识别经验搬到模型里,用手机或者电脑摄像头对着玉米叶片拍一张照片,模型能在几百毫秒内告诉你是哪种病害,给出置信度。整个项目打包成了一个zip,里面包含数据集、标注文件、训练代码、权重文件和推理脚本,拿到手就可以直接跑。
需要说明的是,本文不是某个论文的理论复现,而是我在实际做这个项目时的完整记录,包括数据集怎么整理、模型怎么调参、部署时踩了哪些坑,以及最让我头疼的"训练指标全是0"是怎么解决的。无论你是刚入门的开发者想找个实际项目练手,还是农业领域的工程师要做类似的目标检测任务,这篇文章应该都能给你省下不少弯路。
1. 项目整体设计:为什么是YOLO,以及这个包里面到底有什么
1.1 目标检测选型时的几个考量
在动手之前,我其实也纠结过用分类网络还是检测网络。如果只是判断一张叶片"有没有病",用ResNet这类分类网络就够了。但实际场景里,一张照片上可能同时有健康叶片和病斑叶片,病斑位置、大小、形状都不一样,分类网络要么把整张图裁出来逐块判断,要么就会丢失位置信息,根本没法告诉用户"病斑在叶片的哪个区域"。所以这个项目从一开始就确定要目标检测,而不是图像分类。
目标检测框架里,YOLO系列是绕不开的选择。我当时的对比思路是这样的:Faster R-CNN精度高但速度慢,嵌入式设备跑不动;SSD速度快但小目标检测能力一般;YOLO系列在精度和速度之间平衡得最好,而且生态成熟,从数据标注到训练部署的工具链都非常完整。尤其对于玉米病害这种需要快速响应的场景,YOLO的实时性优势非常明显。而且项目后续打算部署到边缘设备上,YOLO轻量级的特性也更合适。
我选的基线是YOLOv8。原因很简单:它是当时生态最成熟的版本,ultralytics库封装得很好,训练、验证、导出一条龙,而且默认的anchor-free设计对小目标病斑的检测更友好。后来社区出了YOLO11,我也试过,但最终交付版本还是基于v8微调的,因为稳定压倒一切。
1.2 .zip包里到底装了什么
既然标题叫"基于YOLO的玉米病害识别.zip",那这个包的目录结构我得先交代清楚,不然拿到手都不知道从哪儿开始看。我最终交付的包结构是这样的:
corn-disease-yolo/ ├── dataset/ │ ├── images/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ ├── labels/ │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── data.yaml ├── weights/ │ ├── best.pt │ └── last.pt ├── scripts/ │ ├── train.py │ ├── detect.py │ ├── export_onnx.py │ └── split_dataset.py ├── configs/ │ └── train_config.yaml └── requirements.txtdataset目录放的是整理好的数据集,images和labels一一对应,全部是YOLO格式。weights目录放的是训练好的权重,best.pt是验证集上表现最好的模型,last.pt是最后一轮的结果。scripts目录里的train.py负责训练,detect.py是推理脚本,export_onnx.py用于把模型导出成ONNX格式做跨平台部署,split_dataset.py用来划分数据集。
这个结构的好处是:训练、推理、部署全部解耦,你在数据集上重新训练,或者直接用我给的权重推理,都不需要改其他模块的代码。我后面第三节会详细讲每个脚本的用法和参数,这里先对整体有个概念就行。
2. 数据集构建:玉米病害识别里最容易被低估的一环
2.1 数据从哪来:采集和开源结合,但一定要做清洗
很多人拿到这个项目第一件事就想跑训练,但我得说一句实际的:模型精度百分之七八十由数据决定,模型架构只占剩下的部分。玉米病害识别的数据集,如果只靠网上随便爬的图片,训练出来基本是废的,因为图片质量参差不齐,标注混乱,类别分布离谱。
我的数据来源有三个。第一是公开数据集,最经典的是PlantVillage,里面有玉米大斑病、锈病、健康叶片的分类图片,但PlantVillage本身是分类数据集,不是检测数据集,我需要先做裁剪和重新标注,这个后面细说。第二是自己去田间拍的,找了两片玉米地,在不同天气、不同时段、不同角度拍叶片,拍的时候注意光线的变化——晴天强光、阴天散射光、早晚低照度下的叶片表现差异很大,如果只在一种光照下训练,模型到实际场景里立刻露馅。第三是从农业植保网站和论文配图里搜集的图像,这部分版权风险要注意,只用来补充样本量,我实际交付的时候把有明确版权问题的图片都过滤掉了,这是必须做的合规动作。
数据拿到手之后,清洗工作才是重头戏。我踩过最大的坑是:PlantVillage里很多图片是单张叶片放在纯色背景上拍的,而田间的照片是复杂的背景,有土壤、杂草、其他叶片重叠。如果训练集里全是干净背景,模型会"偷懒",直接学习背景特征而不是病害特征。做了个简单测试,用纯背景图片训练的模型,拿到田间复杂背景的照片上,mAP直接掉了一半。所以清洗时要尽量删掉过于理想的纯色背景图片,或者把它们作为数据增强的一部分做背景融合。
2.2 YOLO格式标注规范和转换流程
YOLO格式的标注是每个目标一行文本,格式是class_id x_center y_center width height,前三个类别索引从0开始,四个坐标值都是归一化后的比例值,取值范围0到1。举个例子,如果一张640x640的图片里,一个病斑的边界框是左上角(160, 200)、右下角(480, 500),那么归一化之后的x_center就是(160+480)/(2640)=0.5,y_center是(200+500)/(2640)=0.546875,width是(480-160)/640=0.5,height是(500-200)/640=0.46875。这一行的完整内容就是0 0.5 0.546875 0.5 0.46875。
我当时用的标注工具是LabelImg,因为它可以直接导出YOLO格式,不需要自己做坐标转换。但用LabelImg有个要注意的地方:它输出的标注文件是txt,每张图片对应一个同名的txt文件,如果图片是img_001.jpg,标注就是img_001.txt。图片放images目录,标注放labels目录,目录层级和文件名必须完全一致,否则训练时根本加载不到标注。
如果你手里的数据是COCO格式的JSON标注,或者VOC格式的XML标注,需要转换。我写了一个简单的转换脚本,核心逻辑如下:
def voc_to_yolo(xml_path, output_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) with open(os.path.join(output_dir, os.path.basename(xml_path).replace('.xml', '.txt')), 'w') as f: for obj in root.iter('object'): class_name = obj.find('name').text class_id = class_names.index(class_name) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")转换完成之后,一定要抽查几个标注文件和图片做可视化对比。我自己写了个简单的可视化脚本,把标注框画回原图,用肉眼看边界框是否贴合病斑边缘。这一步不能偷懒,因为坐标转换如果有偏移,模型训练出来边界框位置会系统性偏左或偏上,而且很难排查。
2.3 data.yaml配置:类别名称和路径必须严格对应
数据准备的最后一步是写data.yaml,这是YOLO训练时的数据配置文件。我的data.yaml长这样:
path: /path/to/corn-disease-yolo/dataset train: images/train val: images/val test: images/test nc: 4 names: ['corn_blight', 'corn_rust', 'corn_gray_leaf_spot', 'healthy']这里有个细节,我配了4个类别,前三类是病害——大斑病、锈病、灰斑病,第四类是健康叶片。加健康叶片这个类别看似没必要,但实际非常有用。因为模型在推理时会把整张图里所有目标都框出来,如果不设健康叶片类,健康叶片也会被强行归到某个病害类里,产生大量误报。加上健康叶片类之后,模型学会了区分"有病的区域"和"没病的区域",误报率明显下降。
path字段我一开始写的是相对路径,结果换个机器跑训练就报错找不到数据集,所以后来都改成绝对路径了,这也是一个经验。
3. 模型训练全流程:从参数配置到调优心得
3.1 环境准备和硬件要求
训练YOLOv8,Python环境我用的是3.10,PyTorch版本2.1.0以上,CUDA 11.8。显卡方面,如果你手头是GTX 1660这种入门卡,跑小的模型也凑合,但建议至少RTX 3060级别,显存8G以上,不然batch size开不大,训练速度会很痛苦。
安装依赖非常简单,ultralytics这个库把大部分东西都封装好了,只需要一行命令:
pip install ultralytics但这只是理想情况,我实际遇到过numpy、opencv、torch版本互相打架的情况。建议用conda先建一个干净的虚拟环境:
conda create -n corn_yolo python=3.10 -y conda activate corn_yolo pip install ultralytics torch torchvision --index-url https://download.pytorch.org/whl/cu118注意torch要从PyTorch官方源装,如果直接用pip install torch,装出来的可能是CPU版本,训练速度差得非常远。我第一次就是没注意这茬,CPU版跑了一整夜才跑了几个epoch,后来换成CUDA版,速度提升了二十倍都不止。
3.2 训练脚本和关键超参数解析
我的train.py其实非常短,因为ultralytics把训练逻辑都封装好了:
from ultralytics import YOLO model = YOLO('yolov8s.pt') results = model.train( data='/path/to/dataset/data.yaml', epochs=200, patience=30, batch=16, imgsz=640, device=0, workers=8, optimizer='SGD', lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3, augment=True, seed=42 )关键参数逐个说一下。model用yolov8s.pt做预训练权重而不是从零开始训练,原因是YOLO在COCO上已经学到了通用的物体特征,玉米叶片和病斑虽然不在COCO类别里,但纹理、边缘、颜色这些底层特征是可以迁移的。从零训练的话,数据集得够大才行,我这种几千张的规模根本不够。用s版本而不是n或m,是因为s在精度和速度之间比较平衡,n太小精度不够,m太大训练和部署成本高。
imgsz设为640,这是YOLOv8的默认输入尺寸。有些人会觉得越大越好,比如1024,但我实测下来,对于玉米病斑这种不算特别小的目标,640够了,再大训练速度明显下降,精度提升却微乎其微。如果你的病斑特别小,比如只有十几个像素,那可以考虑用1280,但代价是显存占用成倍增长。
batch设为16,在8G显存下是个安全的数值。如果你的显卡显存不够,调成8甚至4也可以,但要注意batch太小会引入噪声,batch-norm层的统计量不稳定,训练容易震荡。一个实用的做法是先用小batch试跑几个epoch,如果loss正常下降再逐步增大。
优化器我选的是SGD而不是AdamW。这里很多人不理解,YOLOv8默认是AdamW,但我在目标检测任务上实验下来,SGD配合warmup和余弦退火,训练后期loss曲线更平滑,收敛效果更好。AdamW前期收敛快但后期微调能力一般,容易在最优解附近震荡。
3.3 训练过程的监控和判断
训练跑起来之后,不能只看loss数值就完事,我习惯盯几个关键指标。第一个是box_loss和cls_loss的下降趋势,这两个loss应该在训练前期快速下降,中后期平缓。如果loss训练集降但验证集不降,那就是过拟合的苗头,patience参数会自动触发早停。第二个是mAP50和mAP50-95的变化,前者是IoU阈值0.5时的平均精度,后者是0.5到0.95区间内的平均值。对于玉米病害识别场景,mAP50更直观,因为它对定位框要求没那么苛刻,更接近实际应用的判断标准。
我还习惯在训练完看混淆矩阵,这个ultralytics会自动生成。混淆矩阵能直观看到哪些类别之间容易混淆:比如大斑病和灰斑病,因为它们的病斑都出现在叶片上,颜色接近,边界模糊,模型有时会分不清。看到混淆矩阵之后,我会回头检查这两类的标注样本是否清晰,如果标注本身就有歧义,模型学出来的效果也会打折扣。补充样本或删掉模糊标注是常见的修正手段。
3.4 训练过程中的一个意外情况:指标全是0
这里必须单独说一个我遇到的问题,因为热搜词里就有"yolo训练指标全是0",这确实是个常见坑。
我刚开始训练的一版,跑完100个epoch之后,训练日志里显示的mAP50、mAP50-95全部是0,precision和recall也是0,但loss一直在下降。这种情况看着特别迷惑。排查了一下午,最后发现问题出在数据集标注上:labels目录里的txt文件其实都是空的。原因是我的split_dataset.py脚本在做训练验证集划分时,把图片复制到了images目录,但标注文件因为文件名匹配逻辑写错,没有跟着图片走,导致labels目录下是空的。
YOLO训练时遇到空的标注文件不会报错,只会跳过这个样本,所以loss还在正常下降,但模型没有学到任何真实的目标信息,验证时自然检测不到任何物体,所有指标都是0。
排查方法很简单:训练前跑一遍数据集验证脚本,统计images和labels目录中文件的数量是否一致,并检测txt文件内容是否为空:
import os img_dir = 'path/to/images/train' label_dir = 'path/to/labels/train' img_count = len([f for f in os.listdir(img_dir) if f.endswith('.jpg')]) label_count = len([f for f in os.listdir(label_dir) if f.endswith('.txt')]) print(f"Images: {img_count}, Labels: {label_count}") empty_labels = 0 for f in os.listdir(label_dir): if f.endswith('.txt'): if os.path.getsize(os.path.join(label_dir, f)) == 0: empty_labels += 1 print(f"Empty label files: {empty_labels}")这个脚本我现在每次训练前都跑一遍,花半分钟,能避免浪费十几个小时的训练时间。各位如果遇到指标全是0的情况,第一步就查标注文件,八成是标注文件缺失、为空、或者类别索引越界。
4. 模型部署:从训练机到实际场景的距离
4.1 模型导出:从PyTorch到ONNX再到边缘部署
训练好的模型是PyTorch格式的best.pt,但要在不同平台部署,通常需要导出成更通用的格式。我的export_onnx.py脚本核心逻辑如下:
from ultralytics import YOLO model = YOLO('weights/best.pt') model.export(format='onnx', imgsz=640, dynamic=False)导出的ONNX模型可以直接用onnxruntime或OpenCV的DNN模块加载。这里有个参数需要注意:dynamic=False,也就是固定输入尺寸为640x640。如果设置成True,输入尺寸可以动态变化,但推理速度会下降。我实际使用中,固定尺寸完全够用,而且能获得更好的推理性能。
如果你的目标是部署到手机端,还可以进一步导出成NCNN格式,把模型量化成FP16甚至INT8,体积可以压缩到几MB,推理速度也更快。但代价是精度会有轻微下降,需要在设备上实测确认,不能想当然认为量化无损。
4.2 推理脚本实现细节
detect.py是我提供的推理脚本,它的核心流程是:读图、预处理、模型推理、后处理、画框、输出结果。这里有个容易被忽视的细节是预处理必须与训练时一致,否则精度会受影响。训练时图片被缩放到640x640,同时做了letterbox处理(保持宽高比,多余部分填充灰色),推理时也必须做同样的预处理。如果不做letterbox直接拉伸,图片变形会导致边界框位置偏移,检测精度明显下降。
我实际用的推理代码如下:
from ultralytics import YOLO import cv2 model = YOLO('weights/best.pt') img = cv2.imread('test.jpg') results = model.predict(img, conf=0.35, iou=0.45, device='cpu') for result in results: boxes = result.boxes for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) coords = box.xyxy[0].cpu().numpy().astype(int) print(f"Class: {model.names[cls_id]}, " f"Confidence: {conf:.2f}, " f"BBox: {coords.tolist()}")conf=0.35是置信度阈值,低于这个值的检测框会被过滤掉。这个阈值设置很关键,设太高会漏检,设太低会误报。我的测试结论是,在手机拍摄的清晰照片上,0.35到0.45之间表现比较稳定。实际应用中,如果你用的是无人机拍的远距离图像,病斑比较小且模糊,建议把阈值降到0.25,能明显减少漏检的数量。
4.3 部署形态推荐:从PC到边缘设备
不同场景适合不同的部署形态,我总结了三种。
第一种是PC端部署,最简单,直接把detect.py跑在Windows或Linux电脑上,适合实验室、农业技术站等场景。配合摄像头可以做成实时监测,对着叶片移动摄像头,屏幕上实时框出病害位置。
第二种是边缘设备部署,典型的是Jetson Nano、Raspberry Pi加摄像头。这种方案适合温室或田间的固定监测点,每隔一段时间自动拍照上传分析。Jetson Nano上可以跑TensorRT加速,用半精度FP16推理,检测速度能到30FPS以上,完全满足实时需求。树莓派4B性能弱一点,但加上NPU加速棒也能跑。
第三种是手机端部署,用NCNN或MNN框架把模型量化后集成进App,农户拿出手机对着玉米叶片拍照,App直接给出病害类别和置信度。这是最贴近终端用户的使用方式,但开发工作量也最大,需要处理相机适配、模型下载更新、离线推理等一系列问题。我给这个项目做的是PC端和边缘端的部署方案,手机端目前只做了技术验证,还没做成完整的App。
5. 常见问题与排查技巧实录
5.1 小目标漏检:玉米病斑太小怎么办
玉米病害早期,病斑可能只有十几个到几十个像素,YOLOv8在640分辨率下对小目标的检测能力有限。我实际测试过,当病斑直径小于20像素时,漏检率会显著上升。针对这个问题,我试过几个办法。
第一个办法是提高输入分辨率,把imgsz从640调到1024甚至1280。代价是训练速度和推理速度都下降,而且显存占用变大。如果显卡只有8G,1280分辨率下batch只能开到4,训练稳定性会受影响。我实际测试下来,1024相比640能提升约8%的小目标召回率,但训练时间增加了约一倍,是否值得取决于你的场景需求。
第二个办法是对叶片区域做切片。如果应用场景是无人机拍的整片玉米地,不要直接检测整张大图,先把图像切分成多个小图块,每个图块单独送给模型检测,再合并结果。这样相当于放大了病斑在输入图像中的占比,小目标变成了相对较大的目标,检测效果立竿见影。切片重叠率建议设在20%左右,避免病斑正好落在切分线上。
第三个办法是尝试YOLO系列中带attention机制的变体,我试过在C2f模块后加SE模块,小目标的召回率有一点提升,但训练时间增加了不少,最终没有采用,因为在玉米病害识别场景里,牺牲训练效率换取几个百分点的召回率不值得。如果你对精度有极致追求,可以试试这个方向。
5.2 数据不平衡:锈病样本太多,灰斑病样本太少怎么办
我的数据集里,锈病的样本明显比其他病害多,因为田间锈病本身就高发,拍起来容易。这导致模型对锈病的检测精度很高,但灰斑病的精度就差强人意。最直接的办法是给少数类增加数据,或者对少数类做更强的数据增强,比如翻转、旋转、光照变化。还有一个办法是设置类别权重,ultralytics里没有直接暴露这个参数,但损失函数本质上是可以改的,用加权的BCE loss替代默认的loss,能有效提升少样本类别的检测精度。
实际工作中,我给灰斑病增加了样本量,从200多张补到500张左右,mAP50从0.68提升到了0.83。这个提升比任何模型结构优化都明显,再次印证了数据的价值。
5.3 不同环境下泛化能力差:室内的模型到田间就失灵
这是所有农业视觉项目都会遇到的核心问题。模型在室内拍的单张叶片照片上测mAP能到0.9,一到田间复杂背景、不同光照下就掉到0.5甚至更低。原因是训练集和测试集存在很大的分布差异。
解决办法有几个方向。一是采集数据时尽量覆盖多样化的场景,不同天气、不同时段、不同生育期都拍一些。二是使用更强的数据增强,包括HSV变换、随机光照、添加噪声等,让模型对光照变化不敏感。我在训练时开启了ultralytics自带的augment增强,包含mosaic、翻转、旋转、缩放等,能在一定程度上增加泛化能力。三是用自适应对比度增强做预处理,因为玉米叶片在强光下容易过曝,暗光下细节看不清,通过调整对比度可以让模型更关注病害特征而不是光照变化。
还有一个小技巧:在训练集里加入一些"负样本",就是没有任何病害的复杂背景图,比如土壤、杂草、玉米秆的局部特写。这样模型能学会区分"背景区域"和"叶片区域",误检率会大幅下降。我加了100多张负样本之后,误检率降了一半左右。
5.4 推理速度慢:如何优化到实时检测
如果目标是实时检测,比如对着玉米叶片移动摄像头,推理速度是关键指标。我测量过,在PC上(RTX 3060),ONNX格式的模型推理一张640x640图片大约需要30-50ms,可以跑30FPS左右,已经算实时了。但在CPU上,推理时间可能到200-400ms,达不到流畅效果。
CPU推理优化有几个方向:一是用ONNXRuntime的CPU算子优化,打开线程数配置;二是用OpenVINO框架做推理,CPU上相比onnxruntime还能快一倍左右;三是将模型量化成INT8,用OpenVINO的量化工具跑一遍校准,速度提升明显,精度损失在可接受范围内。
如果是在Jetson设备上,TensorRT是必选项。我导出的最优配置是FP16精度,推理时间约15ms,60FPS以上完全没问题。TensorRT的优点是它能把网络层融合优化,减少计算量,但缺点是模型结构如果有自定义层,导出时容易报错。ultralytics对TensorRT的兼容性做得不错,我用v8没有遇到问题。
结尾:一点感受
做完这个项目,我最深的体会是:农业AI真正难的不是算法,而是数据和质量意识。YOLO本身是一个非常成熟的开源工具,训练流程快得甚至让人产生一种"随便跑跑就能出好模型"的错觉。但实际做到能上线、能给农户用的程度,大部分时间都花在了数据清洗、标注检查、场景适配这些"脏活累活"上。回头看看,收益最大的几个改进,全都是在数据和预处理上做的功夫,而非模型结构本身。如果你也想做类似的农业识别项目,我的建议很直接:先把你手里的数据弄得干干净净,再去纠结用YOLOv8还是YOLO11,前者的收益至少占七成。
这个zip项目已经开源了完整的数据集划分和训练推理代码,拿到手不需要改太多就能复现。如果你在复现的过程中遇到问题,尤其是数据加载、指标为0这类经典坑,欢迎随时来交流,我已经把这些坑都踩过一遍了。
本文还有配套的精品资源,点击获取