☰
ResNet50/VGG16/DenseNet农作物病害识别系统(含Flask界面与Grad-CAM可解释性)
2026/10/12 22:46:35 网站建设 项目流程

简介:本资源是一套完整的农作物病虫害智能识别检测系统毕设项目,面向计算机、人工智能及农林信息化相关专业本科生,解决农业场景中病害图像分类与目标检测的工程落地问题。资源包含56个文件,涵盖9个Jupyter Notebook(含ResNet50、VGG16/19、DenseNet121、Fastai、PyTorch、TensorFlow等多框架实现)、2个Python主程序、2个Docker配置文件、部署指南(AWS/GCP/本地Flask)、模型权重(pkl)、静态资源(CSS/JS/HTML)及完整README与贡献规范,包体88.27MB,结构清晰、模块解耦,便于学习迁移与二次开发。已有349人学习下载,项目经导师指导并获98分高分评价,所有代码均本地实测可运行,附详细环境配置(requirements.txt)与多平台部署说明,特别适合毕设选题、课程设计及深度学习实战训练者快速上手与深入理解CNN在农业视觉任务中的应用范式。

1. 这不是又一个“猫狗分类”Demo:一套跑通ResNet50/VGG16/DenseNet121三模型、带Flask Web界面、含真实田间拍摄病叶图像的农作物病虫害识别系统(毕设级可交付)

你手头正赶着计算机/农林信息化/智慧农业方向的毕业设计,导师说“得有模型、有数据、有界面、能跑通”,但搜到的90%所谓“病虫害识别代码”要么是Kaggle上改个路径就打包卖的猫狗分类套壳,要么是TensorFlow 1.x老古董+无标注图+报错堆栈没人修。而这份资源——它真在田埂边拍过37类病斑叶片,用ResNet50在本地RTX 3060上训出94.2%验证准确率,Flask服务启动后输入一张玉米锈病叶片照片,3秒内返回带置信度的病名+防治建议文本;更关键的是,它把PyTorch/TensorFlow/Keras/Fastai四套主流框架的训练脚本全摊开给你,不是让你选一个,而是让你看清:为什么VGG16在小样本下容易过拟合、DenseNet121为何对早期褐斑更敏感、ResNet50微调时layer4要不要冻结——这些不是论文里的结论,是作者在Plant_Disease_RESNET50.ipynb里用lr=1e-4和lr=5e-5各跑3轮后画出的loss曲线告诉你的。它专为毕设场景打磨:requirements.txt里没一行冗余包,Dockerfile适配Ubuntu 20.04+Python 3.8,连deployment_guide/aws_deployment.md都写了怎么用EC2 t2.medium部署而不超学生免费额度。如果你需要的不是一个玩具,而是一份能答辩、能演示、能写进论文方法论章节的可复现、可解释、可扩展的农作物病虫害识别基线系统,这就是目前能找到最接近“开箱即用”的那一份。


2. 从数据集结构到模型选型:为什么这37类病害图像必须按train/val/test三级划分,且VGG16要配batch_size=16而DenseNet121必须用32

2.1 数据集的真实构成与预处理逻辑:不是ImageNet子集,而是田间实拍+专家标注的37类病害

项目根目录下的images/文件夹并非简单堆放图片,而是严格遵循深度学习工业实践的三级结构:

images/ ├── train/ │ ├── apple_scab/ # 苹果黑星病(127张) │ ├── corn_rust/ # 玉米锈病(98张) │ ├── tomato_blight/ # 番茄晚疫病(142张) │ └── ... # 共37个子目录,每类平均85~156张 ├── val/ │ ├── apple_scab/ # 验证集,每类取训练集20%(向下取整) │ └── ... └── test/ ├── apple_scab/ # 测试集,独立采集的未参与训练的田间图(每类30张) └── ...

注意:test/目录下的图像从未出现在任何训练或验证阶段,且由农科院植保站提供原始拍摄记录(EXIF中含GPS坐标与拍摄时间),这是区别于普通公开数据集的核心价值——它模拟了真实部署时模型面对新地块、新光照条件的泛化能力。你在notebook/Plant_Disease_Detection_Keras.ipynb中会看到ImageDataGenerator的validation_split=0.2仅作用于train/目录,而test/目录通过flow_from_directory(directory='images/test')单独加载,确保评估不污染训练过程。

2.2 四大模型脚本的差异化设计:为什么Fastai版自动调参却不如手动调ResNet50稳定

项目提供6个Jupyter Notebook,覆盖主流框架,但它们绝非简单复制粘贴:

Notebook文件名核心框架关键设计点适用场景
Plant_Disease_RESNET50.ipynbPyTorch手动冻结layer1-layer3,仅微调layer4+fc;学习率分段衰减(1e-4→1e-5→1e-6)毕设答辩需展示调参逻辑,强调模型可解释性
Plant_Disease_VGG16.ipynbKeras使用imagenet权重初始化,batch_size=16(显存限制),添加Dropout(0.5)防过拟合小样本(<100张/类)场景,突出基础模型鲁棒性
Plant_Disease_DenseNet121.ipynbTensorFlow启用tf.data流水线,prefetch(2)+cache()提升IO效率;特征图可视化显示早期卷积层对叶脉纹理的响应需分析模型关注区域,支撑论文“注意力机制”章节
Plant_Disease_Fastai.ipynbFastailearner.fine_tune(epochs=15, base_lr=3e-3)自动LR查找,但cbs=[ShowGraphCallback(), SaveModelCallback()]强制保存最佳epoch快速验证想法,但需人工检查learn.recorder.plot_loss()确认收敛

关键参数说明:Plant_Disease_RESNET50.ipynb中torchvision.models.resnet50(pretrained=True)加载的是PyTorch官方torchvision==0.13.1对应的ImageNet权重,而非自训权重。model.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(2048, 37))重置分类头,其中0.3是作者在val集上交叉验证得出的最优Dropout率——低于0.2则过拟合,高于0.4则欠拟合。这个数字不是玄学,是notebook/Plant_Disease_RESNET50.ipynb第12 cell里for p in model.layer4.parameters(): p.requires_grad = True解冻后,用torch.optim.AdamW(model.parameters(), lr=1e-4)训出来的。

2.3 模型性能对比的硬指标:不是准确率数字,而是test/集上的混淆矩阵与F1-score

所有Notebook最终都会生成results/目录下的.csv报告,但真正决定毕设分数的是test/集结果。以ResNet50为例,其test_results.csv包含:

class_nameprecisionrecallf1-scoresupport
apple_scab0.960.920.9430
corn_rust0.890.950.9230
tomato_blight0.930.880.9030
macro avg0.920.910.911110

为什么看macro avg而非weighted avg:37类病害样本量不均衡(最少类仅67张,最多类156张),macro avg对每一类平等加权,更能反映模型对罕见病害(如grape_esca)的识别能力——这正是农技推广中最关键的指标。你在app/models/plant_classifier.py里会发现预测函数predict_image()返回的不仅是class_name,还有confidence和recommendation(基于models/recommendations.json映射),这才是毕设里“智能决策支持”的落地点。


3. Flask Web服务部署实战:从server.py启动到Nginx反向代理,绕过端口冲突与静态资源404

3.1app/server.py的轻量级架构:为什么不用FastAPI而坚持Flask

项目选择Flask而非更现代的FastAPI,是出于毕设场景的务实考量:

  • 依赖极简:requirements.txt中flask==2.2.5无额外异步依赖,pip install -r requirements.txt在校园网环境下1分钟内完成;
  • 调试友好:if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)开启热重载,修改view/templates/index.html后刷新即生效;
  • 部署平滑:gunicorn -w 2 -b 127.0.0.1:8000 app.server:app可直接替换app.run(),无需重构路由逻辑。

核心路由逻辑在app/view/routes.py中定义:

from flask import render_template, request, jsonify, send_from_directory import os from app.models.plant_classifier import predict_image @app.route('/') def index(): return render_template('index.html') # 加载前端页面 @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 # 保存上传文件到临时目录 upload_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) file.save(upload_path) # 调用模型预测 result = predict_image(upload_path) # 清理临时文件 os.remove(upload_path) return jsonify(result)

参数说明:app.config['UPLOAD_FOLDER']在app/__init__.py中定义为os.path.join(os.path.dirname(__file__), 'static', 'uploads'),确保文件存入app/static/uploads/而非项目根目录,避免Git误提交。predict_image()函数内部会自动将图像resize为224x224(ResNet50输入尺寸),并应用与训练时完全一致的transforms.Compose([transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(...)]),这是保证线上推理与离线训练结果一致的关键。

3.2 Docker部署全流程:Dockerfile如何解决CUDA版本与PyTorch兼容性问题

Dockerfile采用多阶段构建,精准控制环境:

# 构建阶段:使用nvidia/cuda:11.3-cudnn8-runtime-ubuntu20.04 FROM nvidia/cuda:11.3-cudnn8-runtime-ubuntu20.04 # 安装Python3.8及系统依赖 RUN apt-get update && apt-get install -y \ python3.8 \ python3.8-venv \ python3.8-dev \ && rm -rf /var/lib/apt/lists/* # 创建虚拟环境 RUN python3.8 -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" # 复制requirements并安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY . /app WORKDIR /app # 暴露端口 EXPOSE 5000 # 启动命令 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app.server:app"]

关键避坑点:nvidia/cuda:11.3-cudnn8-runtime-ubuntu20.04镜像与torch==1.12.1+cu113完全匹配(见requirements.txt),若强行升级PyTorch到2.x会导致torch.cuda.is_available()返回False。作者在Dockerfile第1行就锁死CUDA版本,而非用FROM python:3.8-slim再装CUDA——后者在校园网内下载cudnn包常超时失败。执行docker build -t plant-detect . && docker run -p 5000:5000 plant-detect后,访问http://localhost:5000即可看到Web界面,所有静态资源(CSS/JS/图片)均通过send_from_directory()正确路由。

3.3 Nginx反向代理配置:解决/static/路径404与HTTPS强制跳转

当部署到云服务器时,需用Nginx代理Flask(默认5000端口)并处理静态资源。nginx.conf关键段落:

upstream plant_backend { server 127.0.0.1:5000; } server { listen 80; server_name your-domain.com; # 强制HTTPS跳转 return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; # 静态资源直接由Nginx服务,不经过Flask location /static/ { alias /app/app/static/; expires 1h; } # API请求转发给Flask location / { proxy_pass http://plant_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

为什么location /static/必须用alias而非root:root会拼接路径为/app/app/static//static/xxx.css,而alias将/static/直接映射到/app/app/static/,确保<link href="/static/css/style.css">正确加载。此配置使Nginx承担静态文件服务压力,Flask专注模型推理,QPS从单进程3.2提升至12.7(实测ab -n 1000 -c 50 http://localhost:5000/predict)。


4. 避坑指南:毕设调试中最常踩的5个坑,以及血泪换来的解决方案

4.1 现象:Plant_Disease_RESNET50.ipynb运行到model.train()时报CUDA out of memory

原因:默认batch_size=32在RTX 3060(12GB显存)上超出容量,尤其DenseNet121因特征图通道数多,显存占用比ResNet50高40%。
解决:在Notebook第7 cell中将DataLoader的batch_size从32改为16,并添加pin_memory=True加速数据传输:

train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=4, pin_memory=True)

验证方法:运行nvidia-smi观察Memory-Usage是否稳定在8.2GB以下。若仍OOM,进一步降至8并启用torch.cuda.empty_cache()。

4.2 现象:Flask启动后上传图片返回{"error": "No file uploaded"},但前端确已选择文件

原因:HTML表单缺少enctype="multipart/form-data"属性,导致浏览器未以二进制流发送文件。
解决:检查app/view/templates/index.html中form标签:

<form id="uploadForm" enctype="multipart/form-data"> <!-- 必须存在! --> <input type="file" name="file" accept="image/*" required> <button type="submit">识别病害</button> </form>

血泪经验:曾因复制某博客代码漏掉enctype,调试2小时才发现request.files为空。建议在routes.py的predict()函数开头加日志:app.logger.info(f"Files received: {list(request.files.keys())}")。

4.3 现象:Docker容器内gunicorn启动后立即退出,docker logs显示Address already in use

原因:app/server.py中if __name__ == '__main__': app.run(...)未被注释,导致Flask与Gunicorn双重启动,端口5000被抢占。
解决:打开app/server.py,将if __name__ == '__main__':块整段注释掉,只保留app = create_app()。Docker CMD中gunicorn才是唯一入口。

验证:docker exec -it <container_id> bash进入容器,执行netstat -tuln | grep :5000应只显示gunicorn进程。

4.4 现象:test/集上某类病害(如pepper_spot)F1-score仅0.32,远低于其他类

原因:该类图像在images/test/pepper_spot/中存在大量强反光叶片(拍摄时未打柔光),而训练集images/train/pepper_spot/均为均匀光照,模型未学习到反光鲁棒性。
解决:在notebook/Plant_Disease_RESNET50.ipynb的数据增强部分,为train_transform添加transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1),并在val_transform中禁用该增强,确保验证公平。

技巧:用torchvision.utils.make_grid可视化增强效果,确认反光区域被有效扰动。

4.5 现象:AWS EC2部署后,Web界面CSS失效,按钮样式错乱

原因:Nginx配置中location /static/的alias路径错误,指向/app/static/而非/app/app/static/(因应用代码在/app目录,app/是子目录)。
解决:修改nginx.conf:

location /static/ { alias /app/app/static/; # 注意双层app/ expires 1h; }

排查命令:在服务器执行curl -I http://localhost/static/css/style.css,返回200 OK才正确;若为404,检查Nginx error log:tail -f /var/log/nginx/error.log。


5. 模型可解释性进阶:用Grad-CAM可视化ResNet50的决策依据,让答辩老师看到“AI为什么认为这是稻瘟病”

5.1 Grad-CAM原理极简版:不是画热力图,而是定位模型“看哪里”

Grad-CAM(Gradient-weighted Class Activation Mapping)的核心思想是:用目标类别对最后一层卷积特征图的梯度,加权求和该特征图,生成热力图。它不依赖模型结构(ResNet/VGG/DenseNet通用),且能精确定位病斑区域——这比单纯说“准确率94%”更有说服力。项目在notebook/Plant_Disease_RESNET50.ipynb的“Explainability”章节已集成,只需3步:

  1. 加载预训练模型并提取目标层:ResNet50中model.layer4[2].conv3是最后一个卷积层(输出2048通道);
  2. 注册梯度钩子(hook):在前向传播时捕获该层输出,反向传播时捕获梯度;
  3. 计算热力图:对梯度全局平均池化,加权求和特征图,再插值到原图尺寸。
import torch import torch.nn.functional as F from PIL import Image import numpy as np def grad_cam(model, img_tensor, target_class, layer): """生成Grad-CAM热力图""" model.eval() features = None gradients = None def forward_hook(module, input, output): nonlocal features features = output def backward_hook(module, grad_input, grad_output): nonlocal gradients gradients = grad_output[0] hook_f = layer.register_forward_hook(forward_hook) hook_b = layer.register_backward_hook(backward_hook) output = model(img_tensor.unsqueeze(0)) model.zero_grad() output[0, target_class].backward() # 反向传播目标类别 # 计算权重:对梯度全局平均池化 weights = torch.mean(gradients, dim=(2, 3), keepdim=True) # 加权求和特征图 cam = torch.sum(weights * features, dim=1, keepdim=True) # ReLU激活,插值到原图尺寸 cam = F.relu(cam) cam = F.interpolate(cam, size=(224, 224), mode='bilinear', align_corners=False) hook_f.remove() hook_b.remove() return cam.squeeze().cpu().numpy() # 使用示例 img_pil = Image.open("images/test/rice_blast/IMG_001.jpg").convert('RGB') img_tensor = transform(img_pil) # transform同训练时 target_class = 12 # rice_blast在classes.txt中的索引 cam_map = grad_cam(model, img_tensor, target_class, model.layer4[2].conv3) # 可视化叠加 import matplotlib.pyplot as plt plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(img_pil) plt.title("Original Image") plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(img_pil) plt.imshow(cam_map, cmap='jet', alpha=0.5) plt.title("Grad-CAM Heatmap") plt.axis('off') plt.show()

参数说明:layer=model.layer4[2].conv3是ResNet50的精确目标层(layer4是第四残差块,[2]是第三个Bottleneck,conv3是1x1卷积输出层)。若用VGG16,则目标层为model.features[-2](倒数第二个卷积层)。cam_map数值范围0~1,值越高表示模型越关注该区域——你会清晰看到热力图高亮在稻叶上的典型梭形病斑,而非叶脉或背景。

5.2 将Grad-CAM集成到Web服务:让老师上传一张图,立刻看到AI的“思考过程”

修改app/models/plant_classifier.py的predict_image()函数,使其返回热力图base64编码:

def predict_image(image_path): # ... 原有预测逻辑 ... class_idx = pred.argmax().item() confidence = pred.max().item() # 生成Grad-CAM热力图 cam_map = grad_cam(model, img_tensor, class_idx, model.layer4[2].conv3) # 归一化到0-255并转为uint8 cam_normalized = (cam_map - cam_map.min()) / (cam_map.max() - cam_map.min() + 1e-8) * 255 cam_uint8 = np.uint8(cam_normalized) # 叠加到原图 img_pil = Image.open(image_path).convert('RGB').resize((224, 224)) overlay = Image.fromarray(cam_uint8).convert('RGB') blended = Image.blend(img_pil, overlay, alpha=0.4) # 转base64 import io, base64 buffered = io.BytesIO() blended.save(buffered, format="PNG") cam_base64 = base64.b64encode(buffered.getvalue()).decode() return { "class_name": classes[class_idx], "confidence": float(confidence), "recommendation": recommendations.get(classes[class_idx], "请咨询农技专家"), "heatmap": f"data:image/png;base64,{cam_base64}" }

然后在app/view/templates/index.html中添加显示区域:

<div id="heatmap-container" style="display:none;"> <h3>AI决策依据</h3> <img id="heatmap-img" src="" alt="Grad-CAM Heatmap" style="max-width:100%;"> </div> <script> document.getElementById('uploadForm').addEventListener('submit', function(e) { e.preventDefault(); // ... 原有AJAX逻辑 ... fetch('/predict', {method:'POST', body: formData}) .then(r => r.json()) .then(data => { document.getElementById('result').innerHTML = ` <p><strong>识别结果:</strong>${data.class_name}</p> <p><strong>置信度:</strong>${(data.confidence*100).toFixed(1)}%</p> <p><strong>防治建议:</strong>${data.recommendation}</p> `; // 显示热力图 document.getElementById('heatmap-img').src = data.heatmap; document.getElementById('heatmap-container').style.display = 'block'; }); }); </script>

答辩技巧:当老师问“你怎么知道模型没瞎猜”,直接点击上传一张稻瘟病图,3秒后热力图高亮病斑区域——这比10页公式推导更直观。我当年答辩时,导师盯着热力图看了半分钟,说:“这个能进论文附录。”

5.3 用混淆矩阵指导模型迭代:不是追求最高准确率,而是降低“误诊成本”

农作物病害识别中,“把健康作物判为病害”(假阳性)成本远低于“把严重病害判为健康”(假阴性)。因此,毕设论文的模型优化目标应是最大化召回率(Recall),而非准确率(Accuracy)。notebook/Plant_Disease_RESNET50.ipynb末尾的plot_confusion_matrix()函数生成的矩阵,需重点分析两类错误:

真实\预测rice_blasthealthy...
rice_blast282...
healthy525...
  • 假阴性(FN=2):2张稻瘟病被误判为其他病害,需检查rice_blast类图像是否混入非典型样本(如早期症状不明显);
  • 假阳性(FP=5):5张健康稻叶被误判为稻瘟病,应增加healthy类的负样本(如不同光照下的健康叶)并调整损失函数权重。

实操方案:在train.py中修改WeightedRandomSampler,为rice_blast类设置更高采样权重(weights[rice_blast_idx] = 1.5),或在CrossEntropyLoss中传入weight=torch.tensor([1.0, 1.0, ..., 1.5, ...])。从那以后我每次调参,都强制先画混淆矩阵再看准确率——因为准确率会掩盖致命的假阴性。希望帮到你。

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

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

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

立即咨询