农作物病虫害图像识别毕设:从源码复现到边缘部署的完整指南
2026/9/24 22:52:36 网站建设 项目流程

简介:这份资源是2024年毕业设计级别的农作物病虫害图像识别项目,面向计算机、人工智能、自动化、通信工程等专业的高校学生与科研人员,可用于毕设、课程设计、作业或项目立项演示,也适合具备一定基础的学习者进阶参考。压缩包共373个文件,约91.56MB,包含18个Python源码文件、12个pyc编译文件,以及大量深度学习模型权重与BN层参数文件(如conv、fc系列的weights、offset、scale、variance等),另附txt说明、md文档、conf配置、jpg示例图和log日志,完整覆盖模型定义、训练与推理所需的核心文件。目前已有169人学习下载。项目代码完整、资料齐全,含设计文档,经过测试可正常运行,读者可据此理解卷积神经网络在病虫害图像分类中的实现流程,借鉴网络结构、权重加载与配置管理方式,并在此基础上修改扩展功能,遇到配置或运行问题还可获得远程指导,适合作为深度学习入门与实战的参考案例。

1. 农作物病虫害图像识别项目:一份毕设源码里真正值得复现的东西

每年三四月,计算机和农学交叉方向的毕设选题里,农作物病虫害图像识别几乎是出现频率最高的一个。原因很直接:数据集公开、任务定义清晰、效果肉眼可见,答辩时放几张识别前后对比图就能讲明白。但真正动手做过的人都知道,从「下载一个 zip 源码包」到「跑出能写进论文的准确率」,中间隔着一堆没人告诉你的细节——类别不均衡、叶片背景干扰、训练集和田间实拍分布不一致,任何一个都能让模型在测试集上好看、到真实场景里翻车。

这个标题指向的是一套完整的毕设交付物:Python 源代码加上说明文档,核心任务是用深度学习做农作物叶部病害的分类或检测。它适合三类人:正在找毕设选题、需要一份能跑通且能讲清楚技术路线的本科生;想快速搭一个图像分类 baseline 再往上加改进的研究生;以及需要把模型部署到边缘设备做田间巡检的工程人员。下面我按「数据怎么准备、模型怎么选、代码怎么跑、坑在哪」的顺序,把这类项目里真正决定成败的环节拆开讲。

2. 从 zip 到可运行环境:依赖、目录与数据管线的搭建

拿到一个图像识别项目的源码包,第一件事不是急着python train.py,而是先搞清楚它的目录约定和数据组织方式。绝大多数毕设级项目的结构是固定的:一个data目录放按类别分文件夹的图像,一个models目录放网络定义,一个train.py加一个predict.py,再加一个requirements.txt。理解这套约定,比记住任何一行代码都重要,因为后面所有的路径报错、标签错位、类别数不匹配,根源都在这里。

2.1 用 Anaconda 隔离环境并锁定依赖版本

深度学习项目最怕的就是版本冲突。你系统里可能已经装了某个版本的 PyTorch,但源码包依赖的是另一个版本,直接pip install会把全局环境搞乱。我一般用 conda 建一个独立环境,把 Python 版本也钉死。以这类项目常见的 PyTorch 技术栈为例:

# 创建独立环境,Python 版本按源码说明选,3.8 是兼容性最稳的一档 conda create -n crop_disease python=3.8 -y conda activate crop_disease # 安装 PyTorch,CUDA 版本按自己显卡驱动选,没有 GPU 就用 cpu 版 # 下面这行是 CUDA 11.3 的示例,具体以官方安装页为准 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html # 再装项目其余依赖 pip install opencv-python pillow numpy matplotlib scikit-learn tqdm

这段命令的逻辑是:先隔离,再装框架,最后补通用库。参数上最关键的是 Python 版本和 CUDA 版本——Python 3.8 对老版本 PyTorch 兼容最好,CUDA 版本必须和你的显卡驱动匹配,装错了会在torch.cuda.is_available()返回 False 时才发现。装完先跑一句验证:

import torch print(torch.__version__) print(torch.cuda.is_available()) # 有 GPU 且装对了会返回 True

如果这里返回 False 而你有显卡,八成是 CUDA 版本和驱动不匹配,别急着改代码,先把环境问题解决掉。

2.2 数据集的目录约定与标签映射

图像分类项目的数据组织几乎都是「一个类别一个文件夹」,这是torchvision.datasets.ImageFolder的默认约定,也是这类源码包最通用的做法。典型结构长这样:

data/ ├── train/ │ ├── 健康/ │ ├── 早疫病/ │ ├── 晚疫病/ │ └── 叶霉病/ └── val/ ├── 健康/ ├── 早疫病/ ├── 晚疫病/ └── 叶霉病/

这里有个血泪经验:类别文件夹的名字会被ImageFolder按字母序映射成 0、1、2……的标签,中文名排序结果和你的直觉往往不一致。所以训练完保存类别映射,预测时才能把数字还原成病名:

import os from torchvision import datasets train_dir = "data/train" dataset = datasets.ImageFolder(train_dir) # class_to_idx 就是文件夹名到数字标签的映射,务必存下来 print(dataset.class_to_idx) # 保存映射,预测阶段要用 import json with open("class_indices.json", "w", encoding="utf-8") as f: json.dump(dataset.class_to_idx, f, ensure_ascii=False, indent=2)

参数说明:ImageFolder会自动扫描子目录,每个子目录名就是一个类别。class_to_idx是字典,键是文件夹名,值是标签。把它存成 JSON,预测脚本加载后就能输出中文病名。这一步看着简单,但每年都有人的毕设因为训练和预测的类别顺序对不上,导致识别结果张冠李戴。

2.3 数据增强与训练/验证划分的实操

田间拍的照片和数据集里的照片分布差异很大,光照、角度、背景都不同。数据增强是缩小这个差距最便宜的手段。常见做法是在训练时随机裁剪、翻转、调亮度对比度:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((224, 224)), # 统一到网络输入尺寸 transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ColorJitter(brightness=0.2, contrast=0.2), # 亮度对比度扰动 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], # ImageNet 统计量 std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

逻辑说明:训练集用带随机的增强,验证集只做确定性的缩放和归一化,这是铁律——验证集加随机增强会让评估结果不稳定。归一化用的均值和方差是 ImageNet 的统计量,因为后面用的多半是 ImageNet 预训练模型,输入分布要对齐。参数上,Resize的尺寸必须和网络输入一致,RandomRotation的角度别开太大,叶片旋转 15 度以内合理,转到 90 度就不像真实拍摄了。

3. 模型选型与训练:迁移学习为什么是毕设的最优解

毕设的时间通常只有几周到一两个月,从零训练一个卷积网络既不现实也没必要。农作物病害数据集规模普遍不大,几千到几万张,这时候迁移学习几乎是唯一正确的选择。用 ImageNet 上预训练好的骨干网络,换掉最后的分类头,冻结前面几层再微调,往往用十分之一的算力就能拿到比从零训练高得多的准确率。这一章讲清楚为什么这么选,以及具体怎么落地。

3.1 ResNet、MobileNet、EfficientNet 怎么选

选骨干网络要看两个约束:你的显卡显存,以及最终要不要部署到边缘设备。下面这张表是我在几个类似项目里总结的对比:

骨干网络参数量224 输入单张推理耗时(GPU)适合场景
ResNet18约 11M很快显存小、要快速出结果
ResNet50约 25M中等追求准确率、显存够
MobileNetV3约 5M极快要部署到手机或树莓派
EfficientNet-B0约 5M准确率和体积平衡

如果只是做毕设答辩,ResNet50 是稳妥选择,准确率有保障,论文里也好写。如果说明文档里提到要部署到边缘设备做田间巡检,那 MobileNetV3 更合适,它的深度可分离卷积就是为移动端设计的。选型没有绝对优劣,关键是和你的硬件、部署目标匹配。

3.2 迁移学习训练脚本的关键参数

下面是一个最小可运行的训练循环,把迁移学习的关键点都标出来了:

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, models, transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 数据加载 train_ds = datasets.ImageFolder("data/train", transform=train_transform) val_ds = datasets.ImageFolder("data/val", transform=val_transform) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 加载预训练 ResNet50,替换分类头 model = models.resnet50(pretrained=True) num_classes = len(train_ds.classes) # 类别数由数据决定,别写死 model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) # 损失函数和优化器 criterion = nn.CrossEntropyLoss() # 只优化分类头和最后几层,学习率别开太大 optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) # 训练循环 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f"epoch {epoch}, val acc = {correct/total:.4f}")

逻辑说明:pretrained=True加载 ImageNet 权重,model.fc是 ResNet 的全连接层,替换成你数据集的类别数。优化器只用一个较小的学习率1e-4,因为预训练权重已经很好,学习率太大会把学到的特征破坏掉。参数上,batch_size受显存限制,32 是常见起点,显存不够就降到 16 或 8。num_workers是数据加载的进程数,设成 CPU 核心数能加快读取,Windows 上有时要设成 0 避免多进程报错。

3.3 类别不均衡与过拟合的处理

农作物病害数据集几乎必然类别不均衡——健康叶片样本多,某些罕见病害样本少。这会让模型偏向多数类,少数类识别率极低。两个常用手段:一是给损失函数加类别权重,二是用重采样。类别权重更简单:

from collections import Counter import numpy as np # 统计每个类别的样本数 targets = [s[1] for s in train_ds.samples] counts = Counter(targets) # 权重和样本数成反比,样本越少权重越大 weights = [1.0 / counts[i] for i in range(num_classes)] weights = torch.tensor(weights, dtype=torch.float).to(device) criterion = nn.CrossEntropyLoss(weight=weights)

参数说明:weights是每个类别的损失权重,样本少的类别权重大,模型会更关注它们。注意权重别设得太极端,否则少数类会过拟合。过拟合的另一个信号是训练准确率远高于验证准确率,这时候加 dropout、加数据增强、或者早停都能缓解。

4. 推理与可视化:把模型输出变成能写进论文的结果

训练完模型只是完成了一半,毕设答辩和论文里真正需要的是可解释的结果:单张图片的识别、批量测试的混淆矩阵、以及能直观展示模型关注区域的类激活图。这一章讲怎么把模型输出变成这些材料。

4.1 单张图片预测脚本

预测脚本要解决的核心问题是:加载模型、加载类别映射、把图片预处理成和训练时一致的格式。任何一步不一致,结果都会错。

import torch import json from PIL import Image from torchvision import models, transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 加载类别映射 with open("class_indices.json", "r", encoding="utf-8") as f: class_to_idx = json.load(f) idx_to_class = {v: k for k, v in class_to_idx.items()} # 重建模型结构,加载权重 model = models.resnet50(pretrained=False) model.fc = torch.nn.Linear(model.fc.in_features, len(class_to_idx)) model.load_state_dict(torch.load("best_model.pth", map_location=device)) model = model.to(device).eval() # 预处理必须和验证集一致 preprocess = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) img = Image.open("test_leaf.jpg").convert("RGB") tensor = preprocess(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): probs = torch.softmax(model(tensor), dim=1)[0] conf, pred = probs.max(0) print(f"识别结果:{idx_to_class[pred.item()]},置信度:{conf.item():.4f}")

逻辑说明:pretrained=False是因为要加载自己训练的权重,不是 ImageNet 的。unsqueeze(0)给单张图加上 batch 维度,因为模型期望输入是四维张量。softmax把 logits 转成概率,max取最大概率对应的类别。参数上,预处理必须和验证集完全一致,少一个归一化都会让结果偏差很大。

4.2 混淆矩阵与分类报告

答辩时老师最爱问「每个类别的识别率怎么样」,混淆矩阵和分类报告就是答案。用 scikit-learn 几行就能出:

from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) # 分类报告:每个类别的精确率、召回率、F1 print(classification_report(all_labels, all_preds, target_names=list(class_to_idx.keys()))) # 混淆矩阵 cm = confusion_matrix(all_labels, all_preds) print(cm)

参数说明:target_names传类别名,报告里就会显示中文病名而不是数字。混淆矩阵的对角线是正确分类数,非对角线是错分,哪个类别容易被误判一眼就能看出来。这份报告直接可以放进论文的实验章节。

4.3 用 Grad-CAM 展示模型关注区域

Grad-CAM 能生成一张热力图,标出模型判断时重点看了图片的哪个区域。对农作物病害识别来说,如果热力图集中在病斑上,说明模型学到了正确特征;如果集中在背景上,说明模型可能走了捷径。这在论文里是很好的可解释性材料。

from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import numpy as np # 目标层选最后一个卷积块 target_layers = [model.layer4[-1]] cam = GradCAM(model=model, target_layers=target_layers) rgb_img = np.array(img.resize((224, 224))) / 255.0 input_tensor = preprocess(img).unsqueeze(0).to(device) grayscale_cam = cam(input_tensor=input_tensor)[0] visualization = show_cam_on_image(rgb_img.astype(np.float32), grayscale_cam, use_rgb=True) Image.fromarray(visualization).save("gradcam_result.jpg")

逻辑说明:target_layers指定要可视化的卷积层,一般选最后一个卷积块,它的特征最语义化。GradCAM会计算该层梯度并加权特征图,得到热力图。参数上,输入图片要和训练时同样的预处理。生成的热力图叠加在原图上,红色区域是模型关注的重点。

5. 避坑与排查:那些让毕设卡住一整周的问题

这一章是我和身边人踩过的坑,按「现象 → 原因 → 解决」写,每一条都对应一个真实会卡住进度的问题。

现象:训练时 loss 一直是 nan,或者准确率卡在随机水平不动。原因:最常见的是学习率太大,或者数据标签有问题。标签问题往往是类别文件夹里混进了非图片文件,或者某个类别文件夹是空的,导致ImageFolder映射错位。 解决:先把学习率降到1e-5试一次;再检查每个类别文件夹的图片数量,用for c in train_ds.classes: print(c, len(os.listdir(...)))逐个确认,空文件夹直接删掉。

现象:验证集准确率很高,但拿真实田间照片预测全错。原因:训练集和真实场景的分布差异太大。公开数据集多是实验室或标准拍摄条件下的叶片特写,背景干净、光照均匀,而田间照片背景杂乱、有土壤和阴影。 解决:在训练增强里加大亮度和对比度扰动,加入随机裁剪模拟不同构图;如果条件允许,自己拍几十张田间照片做微调,哪怕每类只有十几张,效果提升也很明显。

现象:DataLoader在 Windows 上报多进程错误,或者训练中途卡死。原因:Windows 上num_workers大于 0 时,多进程数据加载有时会和主进程冲突,尤其是脚本没有放在if __name__ == "__main__":保护下。 解决:把训练代码包进if __name__ == "__main__":,或者直接把num_workers设成 0。代价是数据加载慢一点,但稳定。

现象:显存不够,报 CUDA out of memory。原因:batch_size太大,或者输入分辨率太高。224 输入下 ResNet50 用 batch 32 大概要 6G 显存,ResNet18 只要 3G 左右。 解决:先把batch_size减半,还不行就换更小的骨干网络,或者把输入尺寸从 224 降到 192。别硬扛,毕设阶段稳定跑通比追求大 batch 重要。

现象:预测时置信度普遍很低,或者所有图片都预测成同一个类别。原因:预测脚本的预处理和训练时不一致,最常见的是漏了归一化,或者Resize尺寸对不上。另一个可能是加载权重时模型结构没对上,load_state_dict静默失败。 解决:把预测脚本的preprocess和训练时的val_transform逐行对比,确保完全一致;加载权重时用strict=True(默认),如果键不匹配会直接报错而不是静默跳过。

6. 从毕设到能用:模型量化与边缘部署的验证方法

毕设做完,很多人会想再往前一步——把模型放到树莓派或者手机上,做成一个能拿着去田里拍的实际工具。这一步的门槛比训练高,但也不是遥不可及。核心思路是模型量化加推理引擎替换,把 PyTorch 模型转成体积更小、推理更快的格式。

先说量化。PyTorch 自带的动态量化对卷积网络效果不错,几行就能把模型体积压到原来的四分之一左右:

import torch.quantization # 动态量化,主要量化全连接层,对卷积网络友好 quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "model_quantized.pth")

参数说明:{torch.nn.Linear}指定要量化的层类型,动态量化对 Linear 层收益最大。量化后模型体积变小,CPU 推理速度提升明显,代价是准确率可能掉一两个百分点,需要重新在验证集上评估确认可接受。

如果目标是手机端,更彻底的做法是导出 ONNX 再用推理引擎跑:

# 导出 ONNX,opset 版本按推理引擎支持选 dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export(model, dummy_input, "crop_disease.onnx", input_names=["input"], output_names=["output"], opset_version=11)

导出后可以用 ONNX Runtime 验证推理结果和 PyTorch 是否一致,这是部署前必须做的对齐检查:

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("crop_disease.onnx") # 用同一张图分别跑 PyTorch 和 ONNX,对比输出 onnx_out = sess.run(None, {"input": tensor.cpu().numpy()})[0] torch_out = model(tensor).cpu().detach().numpy() print("最大差异:", np.abs(onnx_out - torch_out).max())

差异在1e-4量级以内说明导出正确,差太多就是算子不兼容或者输入格式不对。这一步是部署的后悔药——等到设备上才发现结果不对,排查成本会高得多。

验证方法上,我一般会准备一个「田间小测试集」:自己用手机在不同光照、不同角度下拍二三十张,人工标好类别,专门用来评估部署后的真实表现。这个集合不参与任何训练,纯粹当验收标准。如果在这个集合上准确率能到 80% 以上,说明模型有实用价值;如果掉到 50% 以下,说明训练数据和真实场景差距还是太大,得回去补数据。

最后说一个我自己的习惯:每次改完模型或者换完部署格式,我都会把「训练集准确率、验证集准确率、田间测试集准确率」三个数字并排记下来。这三个数的差距比任何单个指标都更能说明问题——训练和验证差距大是过拟合,验证和田间差距大是分布不匹配。这个习惯帮我省了很多次盲目调参的时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询