简介:基于深度学习的粮食作物病虫害识别系统,是一套面向高校毕业设计及人工智能农业应用开发者的一体化项目包。方案融合云计算与计算机视觉技术,构建跨平台、易操作的病虫害自动识别系统,重在解决农业数据共享不足、算法门槛高、算力获取难等问题,为智慧农业提供低门槛工具。压缩包共281个文件,以txt说明文档、Python源码、Vue/JS前端页面、JPG/PNG图像样本、PDF论文及PPT答辩材料为主,整体约522.63MB,目录结构清晰,便于按模块查阅学习。资源内附毕业设计论文、教程文档、演示图像与技术路线图,完整覆盖从数据准备、模型训练、前端交互到系统部署的关键环节;丰富的农作物图像样本可直接用于模型训练与效果验证。目前已有462人学习下载,适合需要快速搭建深度学习识别项目或完成相关课题的高校学生与开发者参考。
1. 深度学习识别农作物病虫害:为什么毕业设计都选CNN这条路线
农业场景里,人工智能落地最难的不是模型本身,而是“数据共享不足、算法门槛过高、算力环境不一致”这几座大山。粮食作物病虫害识别系统把问题和答案压缩成一个完整的毕业设计:用云端的算力训练好模型,把识别能力封装成一个跨平台的Web服务,用户打开浏览器上传照片就能判断是稻瘟病还是小麦条锈病。这个项目面向两类人:一类是临近毕设的计算机或农学专业学生,需要一份能在答辩现场跑通的完整链路;另一类是做农业信息化的工程师,想参考“训练—部署—前端”的最小闭环。我拆这套源码时发现,真正值钱的部分不在分类准确率,而在数据组织、迁移学习和部署参数这三层细节上。下面按我自己的复现顺序展开。
2. 从目录结构到标签文件:数据组织是识别系统的地基
2.1 源码包里那些文件到底谁是谁
拿到压缩包之后,先不要急着跑代码,因为里面不只有一套识别系统,还混入了好几篇参考论文和导航文件。.browserslistrc和.gitignore属于前端工程化配置,index.html是系统的前端入口,而正常label的index.html则是带标注结果展示的页面变体,在答辩演示时通常用后者。几个.caj和.docx文件是毕业论文和参考论文,包括 GIS 种植信息系统、WebGIS 决策系统、行人再识别等内容,这几篇更像用来支撑“系统设计”章节的背景资料,而不是核心算法来源。
真正决定整个项目能否复现的,是训练数据的目录结构和标签文件。绝大多数毕业设计源码会把数据集放在单独的data/目录里,每个类别一个子文件夹,文件夹名就是标签名。如果源码包没有附带完整数据集,你需要在训练前准备一份train/和val/,并保证两份数据里的类别目录名完全一致,否则后面的torchvision.datasets.ImageFolder会直接报错。这里有一个经常被忽略的坑:Windows 下解压后,标签文件夹名可能带多余空格或中文括号,模型训练时不会报错,但部署后前端展示的类别名称会很难看,所以我在整理数据时通常会顺手处理一遍标签文本。
2.2 图像分类数据集的目录约定与划分脚本
最稳妥的做法是先把原始图片按类别归拢到data/raw/类别名/下,再写一个小脚本按 8:2 比例划分到data/train和data/val。代码逻辑并不复杂,但能避免手工拖动文件导致类别比例失衡。下面这个脚本我常用:
import os import shutil import random from collections import defaultdict random.seed(42) root = "data/raw" train_dir = "data/train" val_dir = "data/val" # 统计每类图片数量 for cls in os.listdir(root): cls_path = os.path.join(root, cls) if not os.path.isdir(cls_path): continue images = [f for f in os.listdir(cls_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(images) split = int(len(images) * 0.8) train_list = images[:split] val_list = images[split:] for subset, subset_list in [("train", train_list), ("val", val_list)]: target_dir = os.path.join(train_dir if subset == "train" else val_dir, cls) os.makedirs(target_dir, exist_ok=True) for f in subset_list: shutil.copy(os.path.join(cls_path, f), os.path.join(target_dir, f))这个脚本按类别子目录遍历,先读取全部图片,再按random.shuffle打乱后取前 80% 为训练集,后 20% 为验证集。random.seed(42)保证每次运行划分结果一致,这在复现论文实验时很重要;如果不设置固定种子,两次跑出来的训练集不同,对比实验就失去意义。exist_ok=True可以避免文件夹已存在时报错,但要注意它不会清空旧文件,如果之前运行过且类别有删减,需要手动删掉data/train和data/val里的旧目录。
2.3 数据增强与样本不平衡处理
粮食作物的病虫害图像有一个特点:同一病害在不同生长阶段、不同光照下表现差异极大。如果只用原始图片训练,模型很容易过拟合到背景而不是病斑。常见做法是在训练集上做随机裁剪、水平翻转和颜色抖动,在验证集上只做尺寸缩放和归一化。我在这个项目里会按下面的参数配置数据增强:
| 处理环节 | 训练集策略 | 验证集策略 |
|---|---|---|
| 缩放 | 随机缩放到 224x224 | 中心裁剪 224x224 |
| 翻转 | 水平翻转概率 0.5 | 不翻转 |
| 旋转 | 随机旋转 ±15 度 | 不旋转 |
| 颜色抖动 | 亮度/对比度/饱和度 ±0.2 | 不做 |
| 归一化 | ImageNet 均值标准差 | 与训练集一致 |
这里面有个容易被忽视的细节:PyTorch 的RandomResizedCrop会改变物体在图像中的尺度和位置,这能让模型学到“病斑在叶片上的相对分布”,而不是死记硬背整张图的像素模式。但训练集和验证集的归一化参数必须一致,否则验证集的像素分布偏移会直接拉低指标。归一化通常用 ImageNet 的均值[0.485, 0.456, 0.406]和标准差[0.229, 0.224, 0.225],如果使用随机初始化的模型,则可以取消这层归一化;但迁移学习场景下,它直接关系到预训练权重是否还能生效。
如果数据集中某些病害类别样本显著偏少,Loss 会被大类样本主导。我一般会给损失函数加一个基于类别频率的权重向量,用小类别更大的权重。具体做法是先统计每个类别的训练图片数,计算各类别占比倒数并归一化,然后传入CrossEntropyLoss(weight=class_weights)。这一步不需要改模型结构,但对水稻稻瘟病、玉米大斑病这类样本数量较少的类别,提升通常是几个百分点。
3. 用PyTorch实现一个可训练、可评估的识别模型
3.1 迁移学习:从预训练ResNet到作物病害分类器
粮食作物病虫害识别本质上是一个细粒度图像分类任务,类别之间差异很小,比如小麦叶锈病和条锈病在早期外观很接近。从头训练一个深度卷积网络需要百万级数据,而毕业设计场景下能拿到的数据集一般只有几千到两万张,这时候最常见的选择是迁移学习。用 ImageNet 上预训练的 ResNet50 或 EfficientNet-B0 作为骨干网络,把最后的全连接层替换成自己的分类头。
我按下述方式加载模型:
import torchvision.models as models import torch.nn as nn num_classes = 8 # 根据实际病虫害类别数修改 model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) )weights=models.ResNet50_Weights.IMAGENET1K_V2表示加载 ImageNet 预训练权重,而不是随机初始化。把全连接层替换成Dropout + Linear,一是为了抑制过拟合,二是可以把输出维度压缩到具体的病虫害类别数量。这里没有额外加 BatchNorm,因为骨干网络已经包含足够的归一化结构;在分类头上强行加 BatchNorm 反而可能因为 batch 太小导致统计量不稳定。
3.2 训练脚本:损失函数、优化器与学习率调度
训练阶段的关键参数直接影响收敛速度。我的经验是:优化器用 SGD 而不是 Adam,迁移学习场景下 SGD 配合 Nesterov 动量的泛化能力更稳;初始学习率不要太大,ResNet50 在 ImageNet 预训练权重上微调时,0.001 左右比较安全。下面是一段可以直接运行的训练主循环:
import torch import torch.nn as nn from torch.optim import SGD from torch.optim.lr_scheduler import StepLR criterion = nn.CrossEntropyLoss() optimizer = SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) scheduler = StepLR(optimizer, step_size=7, gamma=0.1) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) scheduler.step() print(f"Epoch {epoch+1}, Loss: {running_loss / len(train_loader.dataset):.4f}")这里weight_decay=5e-4对全连接层和卷积层同时生效,能缓解过拟合,但它也会让所有参数都往零方向收缩。如果发现后期损失降不下去,可以把weight_decay调整到1e-4甚至1e-5。StepLR每 7 个 epoch 把学习率乘以 0.1,属于“阶段性衰减”策略;如果你用多尺度训练,可以换成 CosineAnnealingLR,它的学习率曲线更平滑。训练时一定要把model.train()显式调用,因为它会开启 Dropout 和 BatchNorm 的“训练模式”,默认情况下 PyTorch 不会自动切换,漏掉这一行会在验证集上得到完全不准的结果。
3.3 验证与模型保存:不要只看last epoch
训练过程中每轮结束都要记录验证准确率,把最好的权重存成单独文件。我通常不采用最后一次 epoch 的模型,因为学习率衰减后模型可能已经在验证集上出现过拟合拐点。下面这段代码演示了如何在验证后保存最优模型:
best_acc = 0.0 model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pth")验证集上计算准确率时,必须用torch.no_grad()包裹,否则 PyTorch 会为每个中间张量构建计算图,导致显存翻倍并拖慢推理速度。model.eval()同样不可省略,它会把 BatchNorm 切换到使用全局统计量,把 Dropout 关闭。文件best_model.pth只保存了模型参数,没有保存类别映射关系;实际部署时,还需要单独保存一个class_names.txt,否则预测结果只是数字编号,前端无法显示可读的病害名称。
4. 把模型封装成Web服务:让农业从业人员直接用浏览器识别
4.1 Flask后端的图像接收与预处理
模型训练完只是第一步,这套系统强调“跨平台、易使用”,所以必须用 Web 服务把推理能力暴露出来。技术选型上,Flask 轻量、依赖少、上手快,适合这种单机版毕业设计系统;如果要做高并发,可以换成 FastAPI 并提供异步接口。以下代码是一个最小可用的推理接口:
from flask import Flask, request, jsonify from PIL import Image import torch from torchvision import transforms app = Flask(__name__) model.load_state_dict(torch.load("best_model.pth", map_location="cpu")) model.to("cpu") model.eval() val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) @app.route("/predict", methods=["POST"]) def predict(): file = request.files["image"] img = Image.open(file.stream).convert("RGB") tensor = val_transform(img).unsqueeze(0) with torch.no_grad(): logits = model(tensor) pred_id = torch.argmax(logits, dim=1).item() prob = torch.softmax(logits, dim=1)[0, pred_id].item() return jsonify({"pred_id": pred_id, "pred_name": class_names[pred_id], "confidence": prob})map_location="cpu"可以避免服务器没有 GPU 时无法加载权重的问题,这也是“跨平台”的落地点之一。convert("RGB")的作用是把 PNG 的 RGBA 四通道图转换成三通道,否则矩阵维度不匹配会直接报错。预处理环节必须与验证集保持一致,这里用的是Resize(256)后CenterCrop(224),不要改成别的尺寸,否则输入分布偏差会降低准确率。接口返回 JSON,前端可以随时扩展字段,业务层不需要关心模型内部实现。
4.2 前端页面调用与结果渲染
源码包里的index.html和正常label的index.html提供了两个可直接使用的页面,后者内置了标签展示逻辑。如果自己从头写前端,可以借助 FormData 把图片传给/predict接口。下面是一个简化版的页面调用思路:
<input type="file" id="fileInput" accept="image/*"> <button onclick="upload()">开始识别</button> <div id="result"></div> <script> async function upload() { const file = document.getElementById("fileInput").files[0]; const formData = new FormData(); formData.append("image", file); const resp = await fetch("/predict", { method: "POST", body: formData }); const data = await resp.json(); document.getElementById("result").innerText = `识别结果:${data.pred_name},置信度:${Math.round(data.confidence * 100)}%`; } </script>这里的formData.append("image", file)里的字段名必须和 Flask 端request.files["image"]一致,前端改成"photo"则后端也要同步修改。fetch默认不带 Content-Type 头部,浏览器会按 multipart/form-data 自动生成边界分隔符,手工强制设置反而会破坏文件上传格式。为了让内网其他设备也能访问,Flask 启动时不要只监听 127.0.0.1,应该绑定 0.0.0.0。
4.3 服务启动参数与日志排查
在服务器上启动识别服务,通常用下面的命令:
nohup python app.py --host 0.0.0.0 --port 5000 > server.log 2>&1 &nohup让进程脱离终端会话,SSH 断开后服务也不会被终止。> server.log 2>&1把标准输出和标准错误重定向到日志文件,方便排查启动崩溃或请求异常。加&把进程放到后台,终端可以继续执行其他命令。启动后用curl -X POST -F "image=@test.jpg" http://127.0.0.1:5000/predict验证接口,-F表示以表单方式上传文件,和浏览器行为一致。
| 常见报错 | 原因 | 解决办法 |
|---|---|---|
FileNotFoundError: No such file or directory | 模型权重路径写错或未解压 | 检查best_model.pth所在目录 |
IndexError: list index out of range | class_names列表长度小于预测到的数字索引 | 确认类别列表与训练时一致 |
RuntimeError: Input type (torch.FloatTensor) and weight type | 模型权重在 GPU 上训练,当前环境是 CPU | 加载时加map_location='cpu' |
Address already in use | 端口被占用 | 改成其他端口或杀旧进程 |
后端服务稳定后,可以用前端页面连续上传多张样本图做冒烟测试。注意模型在单一背景下识别率高,到了真实田间复杂背景图片上会下降,因此部署时还要设置一个置信度阈值,比如低于 0.4 时返回“无法判断,请重拍”。这个逻辑可以写在 Flask 端,也可以在 JavaScript 端做,我更推荐写在后端,因为前端版本多,改后端只需动一处。
5. 进阶:导出ONNX模型并在CPU环境下验证一致性
毕业设计验收后,通常还有一个延长需求:把识别能力嵌入到更高层的农业信息化平台,比如与 GIS 系统结合,在病虫害地图上叠加识别结果。此时 PyTorch 模型直接部署会带来较大的运行时依赖,常见做法是把模型导出为 ONNX,用 ONNX Runtime 做 CPU 推理。导出代码非常短:
import torch import onnxruntime as ort from PIL import Image from torchvision import transforms dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, "crop_disease.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})dynamic_axes声明 batch 维度可变,这样同一个 ONNX 文件既能处理单张图片,也能在批处理推理时复用。导出后加载方式从torch.load变成ort.InferenceSession,输入数据要转成 NumPy 数组,并且预处理必须与原训练流程保持一致。下面这段代码可以验证 ONNX 输出与 PyTorch 输出是否一致:
img = Image.open("test.jpg").convert("RGB") tensor = val_transform(img).unsqueeze(0).numpy() sess = ort.InferenceSession("crop_disease.onnx") onnx_out = sess.run(None, {"input": tensor})[0] torch_out = model(torch.from_numpy(tensor)).detach().numpy() print("Max diff:", abs(onnx_out - torch_out).max())两次结果的最大绝对差异如果小于 1e-4,说明导出过程无损,可以直接替换部署端的推理引擎。实际运行中,ONNX Runtime 在纯 CPU 环境下对单张 224x224 图片的推理耗时通常只有 PyTorch 的 1/2 到 2/3,对于农业现场使用来说,这个性能差距足够影响用户体验。你也可以用ort.get_available_providers()确认当前环境是否启用了 CPUExecutionProvider 或其他加速器。把 ONNX 模型与 GIS 系统对接时,只需要把接口返回的pred_name和时间戳一起写入农田地块的字段表,就能实现“识别结果上图”,这才是这套毕业设计从演示走向可用的最后一公里。
本文还有配套的精品资源,点击获取