ARTICLE DETAIL

资讯详情

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

基于卷积神经网络的农作物病虫害检测系统设计与实战

基于卷积神经网络的农作物病虫害检测系统设计与实战 简介面向计算机专业毕业设计及深度学习入门者的农作物病虫害识别检测系统基于卷积神经网络实现图像分类与病害检测提供可直接运行的Python源码与配套数据集解决从模型训练到检测部署的全流程问题。整个压缩包为88.27MB共56个文件其中包含Python脚本、9个Jupyter Notebook涉及TensorFlow、PyTorch、Keras、Fastai等框架、模型权重文件、YAML/yml部署配置、HTML前端页面以及Markdown说明文档等能够覆盖数据预处理、模型训练、精度评估与Web端展示等环节。代码已经过严格调试本地编译可运行且附带Dockerfile、部署指南等辅助内容适合快速复现项目或迁移到自己的毕业设计中。目前已有349人学习下载对于需要完整项目经验的学生而言具有较高参考价值同时也能帮助理解多种深度学习框架在农业场景下的应用方式。1. 从病叶到检测框这个农作物病虫害识别系统不只是“分类器”一张带着零星病斑的水稻叶片照片丢进系统几秒后返回一条记录“稻瘟病置信度0.92位置在图片左下角那个框里”——这就是标题里那套“基于深度学习卷积神经网络的农作物病虫害识别检测系统”真正在做的事。它用Python组织数据、训练和推理配一份标注好的数据集把“这图有什么病”升级成“病在哪一块、有多大、属于什么病”。很多第一次做这个方向的人以为它是个图像分类项目实际上检测比分类在农业场景里实用得多因为田间病斑经常只占叶片很小一部分整图分类很容易被大面积的健康背景带偏。这篇文章适合正在做同类毕业设计、以及想快速搭一套可复现识别检测原型的开发者。2. 为什么是卷积神经网络从图像特征到病虫害“找得到”2.1 分类和检测是两回事你的毕设到底该做什么标题里“识别检测”四个字背后是两条技术路线。图像分类的输入是一整张图输出是一个类别标签比如“稻瘟病”或“健康”。目标检测的输出则是若干个边界框每个框有自己的类别和置信度。病虫害在田间不是铺满整张图的往往是一片叶子上长了两块斑旁边还有阴影和泥土干扰。你要是做整图分类卷积神经网络很容易学到“背景越绿越像健康”这种假特征最后在复杂稻田照片上翻车。检测的思路是先把病斑当独立目标“抠”出来再做分类所以它对位置、尺寸、重叠遮挡的鲁棒性都更好。从毕业设计答辩的角度看检测模型能画框结果天然比一个分类准确率数字更有说服力。评委会问“你这系统到底识别出了什么”你直接把带框的测试图放出来比念一段指标强得多。但这不意味着所有情况都要上检测。如果你的数据本身就是裁剪好的病斑特写每张图只有一种病、背景干净那么分类网络也能做得很好强行上检测反而增加标注成本。我的判断标准很简单打开十张原始图看病斑面积占整张图的平均比例低于20%就果断走检测路线标题里“检测”两个字也才站得住。2.2 卷积神经网络如何把病斑“抠”出来要理解为什么卷积神经网络是这套系统的核心得先看它跟传统手工特征的做法差在哪。传统方法要人工设计颜色、纹理、形状特征病斑种类一多特征组合就爆炸。卷积神经网络不用你手工定义特征它靠堆叠卷积层自动学习。前几层学到的是边缘、颜色块这类通用图案中间层学到纹理和局部形状后几层才组合出“这种斑点和周围健康组织完全不同”的高层语义。这个逐层抽象的过程正好和病斑识别需要的“先找局部异常再判断类型”天然对齐。具体到网络里面卷积核在图像上滑动每个位置做一次加权求和相当于在找图片上“最像这个特征模板”的区域。池化层不断缩小特征图尺寸让模型对病斑在叶片上的位置不那么敏感这就是平移不变性。很多教程把卷积核讲得很玄学其实你可以把它想象成一组可学习的病斑模板训练就是在调整这些模板里的数字直到它们能激活真实病斑所在的区域。这也是为什么用预训练权重比自己从头训快得多——预训练权重里已经有大量边缘和纹理模板缺的只是最后那层“什么样的斑点算病斑”的组合方式。2.3 从VGG到YOLO轻量级骨干网络更适合农业现场卷积神经网络的结构选择直接影响这套系统能不能在普通电脑上跑起来。经典VGG系列卷积层很深精度不错但参数集中在全连接层一张512x512的图在CPU上推理要好几秒不适合做实时的田间检测。ResNet引入残差连接解决了深层网络梯度消失但ResNet50又太重。农业场景里最常见的设备就是带个普通GPU的台式机甚至可能是只有CPU的笔记本轻量级骨干在速度和精度之间更实用。我一般会在YOLOv5s和YOLOv8s之间选一个当基线这两个模型本身就把骨干网络、检测头和训练策略封装好了不需要自己再一行行写卷积层。YOLOv5s的骨干在640x640输入下GPU推理只要几十毫秒CPU也能扛住。如果你还想更轻可以把骨干换成MobileNetV3或者ShuffleNet但那样要手动改模型结构工作量大了不少。对毕设来说直接用成熟目标检测框架做二次数值训练把省下的时间拿来做数据和调参性价比最高。骨干网络参数量典型推理速度适用场景VGG16138M慢图像分类demo不推荐用于检测ResNet5025M中等精度优先硬件较好的场景YOLOv5s约7M快病虫害检测默认首选MobileNetV3约4M很快移动端或CPU推理2.4 数据集是系统的“隐藏得分项”公开数据与自标注数据集决定模型准确率的天花板。公开的病虫害数据集大多是分类形式一张叶子特写配一个类别而检测系统需要的是“图片边界框类别”三件套。有些公开数据集自带框但农业类的很少。所以很多毕设会选择自己用LabelImg或者X-AnyLabeling标注几百张图这完全够用前提是照片质量和类别分布要控制好。我拿到图片后第一件事就是统计每个类别的样本数量避免训练到一半才发现某个类别只有十张图。下面这段Python脚本遍历分类文件夹统计常见图片格式的文件数量import os from collections import Counter img_ext {.jpg, .jpeg, .png, .bmp, .JPG} def count_images(root): counter Counter() for cls_name in os.listdir(root): cls_path os.path.join(root, cls_name) if not os.path.isdir(cls_path): continue files os.listdir(cls_path) n sum(1 for f in files if os.path.splitext(f)[1] in img_ext) counter[cls_name] n return counter if __name__ __main__: data_root data/raw for cls_name, num in count_images(data_root).items(): print(f{cls_name}: {num}张)这段代码逻辑很直接遍历raw目录下的每个类别文件夹用后缀过滤出图片按类别计数。运行后你能立刻看到数据分布是“大类别300张、小类别20张”还是“整体均衡”后面的训练策略就跟着这个结果走。不要嫌这个步骤简单我见过太多项目直接训练最后发现小类别完全学不出来还得回来补数据。3. 用Python把数据集跑起来标注格式、训练脚本与最小实现3.1 目录结构与标签格式让YOLO认你的数据训练检测模型前先要把数据整理成目标检测框架认识的目录结构。以YOLO格式为例常见的目录长这样datasets/ crop_insect/ images/ train/ val/ labels/ train/ val/ crop_disease.yaml图片放在images下标签放在labels下train和val一一对应。每张图片的同名txt文件里每一行代表一个目标格式是“类别id 中心点x 中心点y 框宽 框高”所有坐标都归一化到0到1之间。这个格式再看一遍很啰嗦但几乎所有主流检测框架都认它所以值得花十分钟把数据转成这个格式。如果你用的是LabelImg默认导出的是Pascal VOC格式的XML需要转一下。下面这段脚本读取xml里的坐标和类别换算成归一化坐标后写到txt里import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_file, class_names, out_dir): tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) txt_name os.path.splitext(os.path.basename(xml_file))[0] .txt lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in class_names: continue cls_id class_names.index(cls) box obj.find(bndbox) x_min float(box.find(xmin).text) y_min float(box.find(ymin).text) x_max float(box.find(xmax).text) y_max float(box.find(ymax).text) x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines))这里最关键的一步是坐标归一化。VOC里存的是像素坐标YOLO要求相对图片宽高的比例所以先用xmin和xmax算出中心点x再除以图片宽度。图片尺寸必须从xml的size节点读不能写死否则不同分辨率的图片转换后全部错位。脚本里还做了类别过滤class_names列表之外的标注会被跳过这个机制在处理“unknown”这种脏标注时很有用。3.2 数据划分按场景切分别让一张叶子的照片同时出现在两边数据划分看起来只是按比例分文件里面的坑其实很隐蔽。如果同一株植物的多张照片被随机分成训练集和验证集模型在训练时已经“见过”这片叶子的背景验证时再遇到它mAP会虚高真正到新场景测试时马上现原形。正确做法是按场景或者按拍摄对象分组。下面这段脚本按文件前缀分组划分数据适合文件命名包含现场编号的情况import os import random import shutil random.seed(42) src_img_dir datasets/crop_insect/images/all src_lbl_dir datasets/crop_insect/labels/all train_img_dir datasets/crop_insect/images/train val_img_dir datasets/crop_insect/images/val train_lbl_dir datasets/crop_insect/labels/train val_lbl_dir datasets/crop_insect/labels/val groups {} for img_file in os.listdir(src_img_dir): if not img_file.lower().endswith(.jpg): continue prefix img_file.split(_)[0] # 例如 field01_001.jpg - field01 groups.setdefault(prefix, []).append(img_file) all_groups list(groups.keys()) random.shuffle(all_groups) split_idx int(len(all_groups) * 0.8) train_groups set(all_groups[:split_idx]) val_groups set(all_groups[split_idx:]) for group, img_files in groups.items(): dst_img train_img_dir if group in train_groups else val_img_dir dst_lbl train_lbl_dir if group in train_groups else val_lbl_dir for img_file in img_files: base os.path.splitext(img_file)[0] shutil.copy(os.path.join(src_img_dir, img_file), os.path.join(dst_img, img_file)) shutil.copy(os.path.join(src_lbl_dir, base .txt), os.path.join(dst_lbl, base .txt))核心逻辑是先用文件名里的前缀把图片分组再按组划分而不是按单张图片划分。这样同一个现场的照片不会同时出现在训练集和验证集里评估结果更接近真实场景。random.seed(42)固定随机数保证别人复现你时划分结果一致。后缀过滤记得带上大写很多相机导出的图片后缀就是.JPG。3.3 训练一个YOLO检测模型一个能跑通的配置数据准备好后训练配置的yaml文件是第一个要写的东西。里面指定数据集路径、类别数量和类别名建议所有路径都用相对路径方便换机器重跑。# crop_disease.yaml path: ../datasets/crop_insect train: images/train val: images/val nc: 3 names: [rice_blast, rice_brown_spot, rice_leaf_roller]path是数据集根目录train和val只写到图片文件夹框架会自动到同级的labels目录找文本标签。nc是类别数量names里的顺序必须和生成标签时的class_names完全一致比如转换脚本里class_names是[rice_blast, rice_brown_spot, rice_leaf_roller]这里也必须是这个顺序。这个顺序错一位模型训练的类别映射就全乱了。训练命令用下面这个模板python train.py --img 640 --batch 16 --epochs 100 --data crop_disease.yaml --weights pretrained.pt --name disease_run1img 640表示训练时把图片缩放到640x640病斑较小的图片可以试着用960但显存占用会涨一倍。batch 16在8G显存的GPU上已经很紧显存只有6G的话降到8或4。epochs 100看起来多但配上早停机制通常五六十轮就到平台期了。weights指定预训练权重建议拿在COCO上训过的通用权重别用随机初始化病虫害数据量通常不够从零开始训练。3.4 从训练日志到可视化如何判断模型真正能用了训练过程中每轮会输出box_loss、cls_loss、mAP等指标。很多新手只盯着loss看它在降就放心其实loss下降不一定代表检测效果好。更可靠的信号是验证集上的mAP0.5这个值指IoU阈值取0.5时的平均精度做到0.8以上基本能说明模型找到了病斑。如果训练集mAP很高、验证集mAP很低那就是过拟合这时候要回头查数据划分和增强参数。训练结束后用best.pt做推理验证并把结果画出来看import torch model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/disease_run1/weights/best.pt, force_reloadTrue) model.conf 0.25 model.iou 0.45 img datasets/crop_insect/images/val/test_001.jpg results model(img) results.save(save_diroutputs) print(results.pandas().xyxy[0])torch.hub.load加载本地模型文件force_reloadTrue是为了规避缓存旧权重。model.conf控制置信度阈值低于0.25的框会被丢弃田间小病斑容易漏掉可以调到0.15看效果。model.iou是NMS的IoU阈值框重叠严重时降低这个值框太碎就升高。results.save会生成带检测框的图片print出来的表格里有每个框的坐标、类别和置信度这是排查漏检和误检的第一手材料。4. 避坑数据标注、类别不平衡和过拟合的血泪经验4.1 标注框边缘不贴合小病斑漏检到怀疑人生现象训练完的模型只认大片病斑图片里那些很小的斑点全被当成背景mAP0.5始终在0.5以下。原因标注的时候为了省时间把病斑区域框得很大边缘包进大量健康叶组织。卷积神经网络学到的是“框内所有纹理都是病斑”推理时碰到真正的小病斑因为缺少那些健康纹理置信度就被压下去。解决标注时把图片放大到能看清病斑边界用LabelImg的自动保存宁可多花时间也要让每个框贴着病斑边缘。对特别小的目标YOLO默认的anchor可能覆盖不到可以在训练yaml里加一句自定义anchor或者开启多尺度训练。标注完成后抽二十张图出来目检看框和病斑边缘的贴合程度这一步比调十个参数都值。4.2 类别样本悬殊模型只会预测“大概率类”现象训练损失曲线很漂亮但验证结果里稀有病斑几乎全漏甚至所有图片都被预测成数量最多的那个类别。原因水稻稻瘟病300张稻曲病只有20张损失函数被大类主导。模型发现只要把所有目标都预测成稻瘟病整体损失就能压得很低小类别根本没机会学出有效特征。解决先做类别重采样让每个类别在每个epoch里出现的次数接近。最简单的实现是给每张图片按类别设置采样权重每个epoch按权重抽取训练集。还可以在损失函数上加类别权重YOLO的配置里cls_pw就是干这个的。最稳的办法是扩充小类别的数据把病斑区域裁剪出来做旋转、翻转、亮度变化再放回数据集。注意别做整图复制粘贴模型会学习到复制痕迹而不是病斑本身。4.3 训练集上很完美验证集上稀碎现象训练集mAP一路涨到0.97验证集mAP只有0.6两者差距大得离谱。原因这是典型的场景泄漏加过拟合。同一株植物连拍几十张照片随机分到训练集和验证集后模型在训练时已经见过这片叶子的背景纹理所以验证时靠背景就能蒙对。真到一片新田里背景完全不一样精度立刻崩掉。解决数据划分时按场景分组同一个现场拍摄的照片进同一侧。比如文件名带field01前缀的照片全部放进训练集或全部放进验证集不交叉。同时把数据增强参数拉大YOLO里可以调整hsv_h、hsv_s、hsv_v、degrees、translate、scale这些参数增强相当于免费的模型正则化能缓解过拟合。增强不是越大越好旋转超过45度后病斑形态就失真了要控制在合理范围。4.4 标签类别名错位模型学到混乱映射现象训练能正常启动但每个epoch的cls_loss都是负数或者验证集上precision突然变0。原因data yaml里的names顺序和标注txt里的类别id对不上。比如标签文件里0代表稻瘟病yaml里names[0]却写成了稻曲病模型学的映射从一开始就是错的指标自然全乱。解决在训练前写个校验脚本随机读几个txt打印每一行的第一个数字然后和yaml里的names序号对比。正规做法是先维护一个classes.txt固定顺序所有转换脚本和训练yaml都从它读取类别名保证程序全链路一致。这个脚本花五分钟写能省掉后面一整天的排错时间。4.5 换台电脑复现结果mAP波动超过5个点现象同一份代码、同一个best.pt在自己电脑和实验室电脑上跑验证mAP差了5个点以上。原因检测模型推理结果受CUDA、PyTorch版本影响不同版本实现的算子略有差异。再加上NMS阈值、置信度阈值如果没写死而是依赖配置文件默认值换机器后结果就跑偏。解决训练和推理的环境都能用requirements.txt锁死版本必要时把显卡驱动和CUDA版本记在README里。更保险的做法是把模型推理参数在代码里显式写上比如model.conf 0.3、model.iou 0.45不要在推理时临时从外部读取。这样换了机器至少验证结果对得上不会被质疑实验不严谨。5. 从模型到毕设系统用PyQt或本地Web端把检测结果交出去5.1 封装推理函数别在界面里重复加载模型很多毕设代码把模型加载写在界面控件的响应函数里每点一次按钮就加载一次模型用起来卡得没法看。正确做法是把模型加载提到顶层只加载一次后续所有推理调用同一个模型实例。import torch _model None def get_model(): global _model if _model is None: _model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/disease_run1/weights/best.pt) _model.conf 0.3 _model.iou 0.45 return _model def predict(image_path): model get_model() results model(image_path) detections [] for _, row in results.pandas().xyxy[0].iterrows(): detections.append({ name: row[name], confidence: float(row[confidence]), bbox: [int(row[xmin]), int(row[ymin]), int(row[xmax]), int(row[ymax])] }) return detectionsget_model里用了全局变量_model做缓存第一次调用时加载权重之后直接返回已有实例。predict负责把模型输出整理成Python字典列表里面的name、confidence、bbox都是前端展示要用的字段。bbox坐标统一转成整数避免后续在PyQt或Web端画线时碰到float类型报错。5.2 用Flask搭一个本地识别接口浏览器也能跑如果你的毕设要求“有界面”Flask是最轻量的方案。老师直接在浏览器里访问一个地址不用装PyQt、不用打包exe。下面是一个完整的本地识别接口from flask import Flask, request, jsonify import tempfile import os from predict import predict app Flask(__name__) app.route(/detect, methods[POST]) def detect(): file request.files[image] suffix os.path.splitext(file.filename)[1] with tempfile.NamedTemporaryFile(suffixsuffix, deleteFalse) as tmp: file.save(tmp.name) tmp_path tmp.name result predict(tmp_path) os.remove(tmp_path) return jsonify({detections: result}) if __name__ __main__: app.run(host0.0.0.0, port5000)接口接收POST上传的图片先保存到临时文件再调用predict得到结果返回JSON。tempfile是避免多个用户同时上传时文件名冲突。用命令行测接口是最快的验证方式curl -X POST -F imagetest.jpg http://127.0.0.1:5000/detect如果返回的JSON里能看到detections列表整个流程就已经通了。想扩展成批量识别就把循环放在predict外面每次传入一张图片路径。5.3 把检测框画到图上用PIL而不是matplotlib界面端要展示检测框很多人习惯用matplotlib但它在PyQt或Flask场景里既慢又容易阻塞线程。PIL的ImageDraw更轻几行代码就够from PIL import Image, ImageDraw def draw_boxes(image_path, detections, output_path): img Image.open(image_path).convert(RGB) draw ImageDraw.Draw(img) for det in detections: x1, y1, x2, y2 det[bbox] draw.rectangle([x1, y1, x2, y2], outline(255, 0, 0), width3) draw.text((x1, y1 - 12), det[name], fill(255, 0, 0)) img.save(output_path) return output_pathdraw.rectangle的width控制框的粗细低分辨率图片建议用2到4像素。draw.text在默认字体下遇到中文会显示成方块所以类别名最好先用英文或拼音或者显式加载一个中文字体文件。如果前端要显示图片可以直接把绘制后的图片转成base64字符串返回省去文件传输这一步。5.4 想要桌面端PyQt的调用方式如果老师指定要桌面程序PyQt5是常见选择。核心思路和Flask一致界面线程只负责选择图片和展示结果推理永远在后台函数里完成。from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QFileDialog, QVBoxLayout, QWidget from predict import predict class MainWindow(QWidget): def __init__(self): super().__init__() self.label QLabel(选择一张图片) self.btn QPushButton(打开图片) self.btn.clicked.connect(self.open_image) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) self.setLayout(layout) def open_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.jpg *.png)) if path: detections predict(path) print(detections)这个示例只展示了按钮和标签的基本结构。实际项目里你可以在open_image里调用predict后把返回的检测框坐标画到QLabel里的QPixmap上。重点还是先有predict这个干净接口再谈界面顺序反了代码会越写越乱。6. 把准确率再往上推迁移学习、错误分析和参数微调迁移学习是检测项目里最有效的提分手段。很多人拿到模型就从头随机初始化训练几百张病虫害图片根本学不动。正确做法是加载一份预训练权重把骨干网络浅层冻结只让检测头和后几层卷积跟着自己的数据集更新。Ultralytics框架里直接在训练命令加--freeze就行python train.py --img 640 --batch 16 --epochs 50 --data crop_disease.yaml --weights pretrained.pt --freeze 10freeze 10代表冻结前10层卷积这些层学的是边缘、颜色这类通用特征对病虫害同样有效没必要重新训。如果你的数据集只有两三百张建议freeze开到15以上数据量上千张后可以只冻结5层或者不冻结效果反而更好。提分之外答辩时最容易被问的是“你这系统的短板在哪”。与其说空话不如做一份错误分析。把验证集所有图片跑一遍推理把置信度低和类别判错的图单独抽出来看。我自己在做水稻病害检测时发现所有误检都集中在反光水珠和阴影上它们和病斑在RGB图里确实很像。针对这个问题我在预处理里加了去高光步骤误检率直接降了一半。这种分析比再调一轮参数更能说服人。最后记得调NMS参数。训练完的mAP看着可以但实际推理时框叠成一团多半是IoU阈值太松。在验证集上画出来看框叠得多就把iou从0.45降到0.3框太稀疏就升到0.6。没有统一答案按你自己的目标尺寸调一轮就行。做这个方向最深的教训是把时间花在数据清洗和错误分析上比反复改网络结构收益更高。希望这些踩坑经验能帮你少走弯路早点跑出一个自己满意的检测系统。本文还有配套的精品资源点击获取
返回列表