选毕设题目那段日子,我基本把网页上那些"推荐题目"翻了个遍,最后定了"基于Django的农业害虫识别系统"。原因很直接:这个题目同时压中了Web开发和深度学习两块内容,既有工程量又有算法亮点,不会让答辩变成简单的系统演示。真正动手以后才发现,从数据集整理、模型训练、Django工程集成,到后期把源码和文档整理成一套能交付的东西,每一环都有不少值得记录的细节。这篇文章就把这套农业害虫识别系统的设计与实现完整复盘一遍,从技术选型到模型训练,从核心模块编码到远程调试和部署验收,把我踩过的坑和总结的经验都写出来。准备做类似毕设、或者想复现一个"图片识别加Web管理"系统的开发者,可以直接把这篇当参考。
1. 为什么选农业害虫识别当毕设:需求与选题逻辑
1.1 农业植保场景里的真实需求
农田害虫防治讲究"治早治小",虫子刚起来那几天是防控窗口期。但实际情况是,基层农户识别害虫主要靠肉眼和经验,碰到不常见的虫子很容易认错:把棉铃虫当玉米螟、把蚜虫危害当病害,最后打错药、误了时机。植保技术人员数量有限,也不可能天天跑田里。所以"拍照识虫"这个需求是真实存在的,不是拍脑袋想的。
图像识别技术这几年已经很成熟,把手机拍一张害虫照片传到服务器,跑一遍深度学习模型,返回害虫种类和防治建议,这条链路在技术上有成熟路径,在业务上有落地价值。做这种题目,答辩的时候你有东西可讲:需求来自实际场景,不是凭空造一个增删改查系统。
1.2 作为毕业设计,这个题目的难度评估
我选题目时主要评估了三点。
第一,复杂度适中。如果一个题目里只有Django,写出来就是一个普通的管理系统,算法上没什么亮点;如果一个题目只有模型训练,又不像一个完整的软件工程。农业害虫识别系统把两者结合了:Django负责业务逻辑、数据管理、页面展示,深度学习模型负责核心识别能力。两边都有,但每一块的深度都可以自己控制。
第二,知识覆盖面够全。这个系统完整走下来,会涉及数据库设计(ORM模型)、后端视图编写、模板渲染、文件上传处理、深度学习模型训练与推理、环境部署和远程调试。一套流程做完,等于把大学里Web开发和机器学习两门课的知识串起来了,老师问你什么你都能展开说。
第三,演示效果好。答辩现场最怕的就是"假大空"的项目,页面点来点去只有表格。这个系统不一样,你现场上传一张稻飞虱的照片,几秒钟返回识别结果和防治建议,效果直观,老师一眼就能看懂系统在做什么。
1.3 我最后划定的功能范围
为了防止需求发散、最后做不完,我把功能边界划得很清楚:
- 害虫识别:上传图片,返回害虫类别、置信度、形态特征和防治建议
- 害虫百科:按类别维护每种害虫的详细信息,供用户检索学习
- 识别记录:保存每次识别历史,用户可以复查,管理员可以在后台看到使用情况
- 后台管理:用Django自带的Admin维护害虫数据、管理用户
这个范围下,系统核心链路就是"图片上传到模型推理再到结果返回",围绕这条链路把Django的请求处理、文件上传、数据库写入、模板渲染这些关键点全部串起来了。
2. 系统整体架构与关键技术选型:Django为什么够用
2.1 系统的整体架构拆解
整个系统是典型的B/S结构,浏览器作为客户端,Django作为服务端。浏览器端用Bootstrap模板渲染页面,用户上传害虫图片后,Django的视图函数接收图片文件,做格式校验,然后把图片保存到服务器的media目录;接着视图层调用独立的识别模块,识别模块负责加载深度学习模型、对图片做预处理、执行推理,返回结构化的识别结果;视图拿到结果后一方面传给模板渲染展示给用户,另一方面写入数据库保存识别记录。
我特意把识别模块从视图函数里拆出来,单独放到一个prediction目录下。这样做的好处是:视图层代码只关心HTTP请求和响应,具体的图像预处理、模型推理逻辑都封闭在独立模块里,以后换模型、换框架、加缓存,都不会动到业务代码。
2.2 为什么是Django而不是Flask或者SpringBoot
很多同学做这种题目会纠结框架,我的经验是你先把技术栈定下来再想其他。这里给一个我当时的对比思路:
| 对比维度 | Django | Flask | SpringBoot |
|---|---|---|---|
| 开发效率 | 高,ORM、Admin、认证都是内置的 | 高,但很多功能要自己集成 | 中,配置多,上手慢 |
| 深度学习生态 | Python生态,TensorFlow/PyTorch直接调用 | 同样可以,但工程结构要靠自己搭 | Java生态访问Python模型要额外做服务 |
| 答辩讲解 | MVT结构清晰,好讲 | 自由度高,但代码组织容易散 | 适合Java方向,但如果模型是Python,要绕一层 |
| 学习成本 | 中,文档和中文资料极多 | 低 | 高 |
我最终选Django,核心原因有三个。第一,项目需要的用户认证、后台管理、ORM、表单处理,Django全都有,不需要从零搭建,毕设周期短,时间要花在刀刃上。第二,深度学习模型的训练和推理都在Python这边,Django用Python写,两者在同一个进程里直接互相调用,省去跨语言通信的麻烦。第三,MVT模式规整,模型、视图、模板分开,答辩时按着这个结构讲,逻辑非常通顺。
2.3 数据库设计:三张核心表就够了
数据库我用Django自带的关系型数据库SQLite起步,方便开发和打包演示;如果要上线,改配置切到MySQL也很容易,Django的ORM把这层屏蔽掉了。核心数据表设计如下:
| 表名 | 主要字段 | 用途 |
|---|---|---|
| user | 用户名、密码、邮箱、创建时间 | 用户认证 |
| pest_info | 名称、学名、形态特征、危害作物、防治方法、图片 | 害虫百科数据 |
| recognition_record | 用户、上传图片路径、识别结果、置信度、识别时间 | 识别历史记录 |
从这张表能看出来,这个系统的数据结构并不复杂,但每一张表都有实际用途。pest_info是百科数据,recognition_record记录用户的每一次识别行为,这两个表直接支撑了系统的主要业务。用户表我直接用了Django的AbstractUser做扩展,加了一个手机号字段方便后面做用户电话绑定。
2.4 工程目录与代码分层
一个典型的Django工程,我建议把代码分层清楚,不要全堆在views.py里。我的工程结构大概是这样:
pest_project/ ├── manage.py ├── pest_app/ # 核心业务代码 │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── admin.py │ └── ... ├── prediction/ # 识别模块,独立封装 │ ├── __init__.py │ ├── model.py # 模型加载和预测 │ └── preprocess.py # 图片预处理 ├── media/ # 上传的害虫图片 ├── static/ # CSS、JS、静态资源 └── requirements.txt # 依赖清单分层清楚带来的直接好处是:远程调试的时候定位问题非常快,前端报错找模板和静态文件,后端报错找views和prediction,一眼就能锁定范围。
3. 害虫识别模型:从数据集到训练再到接入Django
3.1 数据集怎么选:公开数据集加筛选
训练害虫识别模型,第一个绕不开的问题就是数据。市面上有公开的农业害虫数据集,比如IP102,里面包含一百多种害虫,图片数量也有几万张。但直接拿来训练会有两个问题:一是类别太多,很多类别之间形态差异很小,识别难度大;二是原始标注噪声比较多,直接用会影响模型效果。
我的做法是筛选出十种具有代表性的常见农田害虫,保证每种至少500到800张图片,覆盖不同生长阶段、不同拍摄角度、不同光照条件。类别不用多,十类左右对毕设来说完全够,演示效果也稳定。数据准备好之后按目录划分,每个类别一个文件夹,这是最简单的标注方式,对后续加载和训练都很友好。
如果自己拍图或者从搜索引擎收集,注意版权问题,尽量用公开数据集或自己拍摄。数据质量比数据量更重要,模糊的、重复的、标注错误的图片宁可删掉,留着只会拉低准确率。
3.2 模型选型:ResNet50还是MobileNet
模型选择上,很多教程一上来就让你从零训练一个卷积神经网络,我强烈不建议这么做。设备不够、数据量不够、训练时间不够,最后准确率上不去,整个项目就废了。正确做法是迁移学习:用在大规模数据集上预训练好的模型作为特征提取器,只需要微调最后的全连接层。我最终对比了两个主流模型:
| 模型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ResNet50 | 准确率高,结构经典,答辩好讲 | 模型文件大(约90MB),推理稍慢 | 对性能要求不高的Web服务,毕设首选 |
| MobileNetV3 | 模型小(约10MB),推理快 | 准确率略低 | 部署到低配服务器或移动端 |
坦白说,在几百到一千张图的小数据集上,ResNet50和MobileNet的差距没有想象中大,关键还是数据质量和训练策略。我最后选了ResNet50,理由是答辩时可以讲残差网络的结构、为什么能解决深层网络退化问题,这些都是老师爱听的加分内容。
3.3 训练细节:一组能复现的参数配置
训练脚本我整理成了独立的Python文件,数据加载用ImageFolder方式,直接从目录读取。代码核心部分大概是这样的:
import torch import torch.nn as nn from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 数据增强:随机旋转、翻转、亮度调整 transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 加载预训练ResNet50,替换最后一层 model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 10) # 10类害虫 # 冻结除最后一层外的参数,先训练fc层,再逐步解冻 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)这里有一个很重要的训练策略:先用较小的学习率只训练新加的全连接层,等loss下降后再解冻部分卷积层,用更小的学习率微调。如果一开始就把所有层都放开训练,小数据集很容易过拟合,训练集准确率99%,验证集只有70%。我当时用7:2:1划分训练集、验证集和测试集,输入图片缩放到224x224,batch size设32,先训练20个epoch,再解冻后几层微调10个epoch,最终测试集Top-1准确率在92%左右。这个结果用于演示已经相当能打了。
3.4 模型接入Django时最容易踩的三个坑
模型训练完成后,把模型文件放进Django工程里才是真正开始踩坑的地方。
第一个坑是图像预处理不一致。训练的时候图片做了resize、归一化,推理的时候也必须完全一样地做一遍。如果训练时用的是Resize((224, 224)),推理时却用了别的尺寸,模型很可能给出错误的预测结果。这个错误非常隐蔽,因为程序不会报错,只是结果不准。我的解决办法是把预处理步骤单独封装成函数,训练和推理共用一份代码。
第二个坑是模型加载慢。ResNet50模型文件90多MB,如果每次用户请求都重新加载一次,首次请求可能要等好几秒。我的做法是在模型加载函数上加了一个单例缓存,只在第一次请求时加载模型,后面的请求复用同一个模型实例。
_model = None def get_model(): global _model if _model is None: _model = load_model('pest_model.pth') return _model第三个坑是类别映射顺序混乱。训练时类别是按文件夹名排序的,推理时如果搞不清楚每个索引对应哪个类别,返回的结果就会张冠李戴。我训练完就保存了一份类别索引和中文名称的映射文件,推理的时候直接加载,避免重新拼。
4. Django核心模块实现:图片上传、识别、展示一条龙
4.1 图片上传:别只写功能,一定要做校验
图片上传是识别链路的第一步,也是很多初学者翻车的地方。我的视图函数里接收request.FILES,拿到上传的文件之后先做两件事:校验文件扩展名,只允许jpg、jpeg、png;限制文件大小,超过5MB直接拒绝,防止大文件拖垮服务器。
import os from django.conf import settings from django.shortcuts import render from django.utils import timezone from .models import RecognitionRecord from prediction import predict_image def upload_and_recognize(request): if request.method == 'POST': image_file = request.FILES.get('image') if not image_file: return render(request, 'pest/error.html', {'error': '请选择图片'}) ext = os.path.splitext(image_file.name)[1].lower() if ext not in ['.jpg', '.jpeg', '.png']: return render(request, 'pest/error.html', {'error': '仅支持jpg和png格式'}) if image_file.size > 5 * 1024 * 1024: return render(request, 'pest/error.html', {'error': '图片大小不能超过5MB'}) # 保存图片 image_path = os.path.join(settings.MEDIA_ROOT, 'uploads', f'{timezone.now().strftime("%Y%m%d%H%M%S")}_{image_file.name}') os.makedirs(os.path.dirname(image_path), exist_ok=True) with open(image_path, 'wb+') as f: for chunk in image_file.chunks(): f.write(chunk) # 调用识别模块 result = predict_image(image_path) ...这里我特别想说一句:文件名的生成一定要用时间戳加随机串,不要直接用用户上传的原始文件名。原因有两点:一是避免文件名中有特殊字符导致路径问题,二是避免不同用户上传同名文件时互相覆盖。
4.2 核心视图函数:串联整个识别逻辑
识别模块的predict_image函数封装了模型加载、预处理、推理和结果映射的全部逻辑。视图层拿到返回结果之后,把结果连同图片路径一起存入RecognitionRecord表,最后将数据传入模板。
# prediction/predict_image.py import torch from PIL import Image from .model import get_model from .preprocess import preprocess_image def predict_image(image_path): model = get_model() tensor = preprocess_image(image_path) # 与训练时保持一致 model.eval() with torch.no_grad(): output = model(tensor.unsqueeze(0)) probabilities = torch.softmax(output, dim=1) confidence, index = torch.max(probabilities, dim=1) return { 'class_name': class_names[index.item()], 'confidence': round(confidence.item(), 4), 'suggestion': pest_info[class_names[index.item()]] }视图函数接收这个结果后,查询PestInfo表补充形态特征和防治方法,然后渲染页面。整个链路里,prediction模块只负责识别,Django视图只负责业务流程,互不干扰,这个设计后面做远程调试时帮了我大忙,哪一段出问题直接看哪一段。
4.3 页面展示:结果卡片加历史记录
页面展示我用的Bootstrap栅格布局。识别结果页展示四样东西:上传的图片、识别出的害虫名称、置信度百分比、防治建议。置信度我保留两位小数显示,低于60%的结果页面会提示"识别置信度较低,建议咨询当地植保站",这个是考虑到模型永远不可能100%准确,要引导用户理性看待结果。
历史记录页面单独做了分页展示,每页十条,按照识别时间倒序排列。每条记录显示缩略图、害虫名称、置信度、识别时间,点击可以跳转到详细结果页。这个模块虽然简单,但体现了系统对用户数据的管理能力,答辩时也算是一个加分点。
4.4 Django Admin:维护百科数据的利器
PestInfo的增删改查我完全没有自己写页面,直接注册进了Django Admin后台。
from django.contrib import admin from .models import PestInfo, RecognitionRecord @admin.register(PestInfo) class PestInfoAdmin(admin.ModelAdmin): list_display = ('name', 'scientific_name', 'crop_host', 'created_at') search_fields = ('name', 'scientific_name', 'crop_host') list_filter = ('crop_host',)Admin后台的好处是免费自带一套完整的管理界面,我只需要在admin.py里配置列表展示字段和搜索字段。害虫百科的维护、防治方法的更新,全部通过后台完成,不用写一行前端代码。这个思路建议做类似系统时直接沿用:用户功能自己做,管理功能优先考虑Admin。
5. 远程调试与部署交付:环境、工具和容易翻车的地方
5.1 远程调试到底怎么搞
标题里带了"远程调试"这个词,很多同学可能以为是什么特殊技能,其实就是把开发环境搬到云端或者让另一个人连接到你的开发环境,共同排查问题。我实践下来最顺手的方案是VS Code Remote-SSH:
- 本地装好VS Code,安装Remote-SSH扩展
- 在远程服务器上开一个SSH账号,配上公钥免密登录
- VS Code左侧选择Remote Explorer,连接到服务器的IP地址
- 连接后直接打开服务器上的Django工程目录,本地写代码,服务器上运行
这样做的最大好处是:本地和远程环境完全一致,不存在"我本地能跑你服务器跑不了"的问题。调试Django应用时,我把DEBUG = True打开,在views.py里打上断点,浏览器发一次请求,代码就能停在断点上,变量值、调用栈一目了然。
如果对方没有服务器,也有一个替代方案:Django的报错页面本身就是最好的调试工具。只要DEBUG = True,程序出错时页面会显示完整的调用堆栈、异常信息、出错文件和行号,直接把这些信息截图或复制给帮你远程定位的人,基本上能解决80%的问题。
5.2 环境一致性:虚拟环境和依赖锁版本
远程调试和交付过程中,我遇到的最常见问题就是环境不一致。本机Python 3.10开发得好好的,换到另外一台机器上装依赖就报错。解决这个问题没有捷径,就是老老实实用虚拟环境加依赖清单。
python -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt里面必须把关键依赖的版本号锁死,尤其是Django、torch、pillow、numpy这几个。我实际遇到过一个问题:pillow版本过高导致图片处理时报错,降级一个版本就好了。像这类跟深度学习框架强相关的依赖,版本差一点就容易出问题,所以锁版本是必须的。
说到模型文件,还有个细节要注意:ResNet50模型文件接近100MB,放Git仓库既不合适也容易超限。交付时我把模型文件单独放在一个目录,并在README里写明下载链接和解压放置的路径,这样既保证了工程代码的干净,也不影响别人复现。
5.3 静态文件和媒体文件配置:404重灾区
Django项目里静态文件和上传图片404是新人踩坑最多的点。在调试环境下的配置是这样的:
# settings.py MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]然后要在项目的根urls.py里把媒体文件的路由补上:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ...你的路由 ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)不加这一行,用户上传的图片在页面上永远显示不出来,只有DEBUG模式能访问。网上很多帖子提过这个问题,我这次也在这里卡了半小时,查来查去最后发现只是忘了加媒体路由。
5.4 局域网演示与公网部署
毕设答辩前经常要在现场演示,实验室的网络环境和笔记本可能隔着一堵墙。如果只在本机跑python manage.py runserver,别人的浏览器根本访问不到。这时候要这样启动服务:
python manage.py runserver 0.0.0.0:8000同时记得去settings.py里改ALLOWED_HOSTS = ['*'],否则访问时会出现DisallowedHost错误。这样的话,同一局域网内的设备通过你的局域IP加8000端口就能访问系统了。手机现场演示拍一张虫子图片,直接页面上传,效果非常炸场。
如果要在云服务器上长期运行,我建议还是用ginicorn加nginx的组合,比直接用runserver稳。Django本身不擅长处理高并发静态文件,生产环境下让nginx挡在前面,静态文件直接交给nginx服务。不过对毕设来说,runserver方式演示已经够用,服务器部署只是一个加分项。
5.5 交付源码时的目录和文档规范
既然涉及源码交付,我多说一句目录规范。交付包里的README至少要写清几件事:项目简介和功能列表、环境要求(Python版本、依赖安装命令)、数据库初始化步骤、模型文件的获取方式、启动命令、默认管理员账号。文档不要写流水账,要让一个从没运行过项目的人按步骤10分钟内跑起来。我这次整理交付内容时把requirements.txt、模型说明、部署笔记、答辩演示脚本都分类放好,后面重新复现这个项目时省了非常多时间。
6. 这套系统还能怎么升级:从毕设到真正可用的产品
6.1 提升识别精度的两个方向
92%的准确率做演示够了,但要往深了做,可以从两个方向改进。第一是细粒度识别,很多害虫不同发育阶段的形态差别很大,比如幼虫和成虫,可以训练一个先判断虫态再识别种类的两级模型。第二是多模型融合,对预测置信度不高的样本同时跑MobileNet和ResNet,取投票结果,能有效降低误判率。
6.2 加入用户反馈闭环
一个好用的识别系统应该有"用户反馈"机制。用户对识别结果不满意时,可以点"识别错误"按钮,选择正确的类别,这些反馈数据会进入数据库,定期汇成新的训练集,让模型越用越准。这个功能技术上不复杂,但把系统从单向识别变成了有数据闭环的产品,答辩讲到这里基本就是高分点。
6.3 从Web端扩展到视频与移动端
当前系统是单张图片识别,如果想扩展,可以做视频帧检测,用OpenCV从视频里抽帧,对每一帧执行识别,遇到置信度高于阈值的结果就截帧保存并告警。这是植物保护站做虫情监测的常见模式。移动端方面,Django可以写一套DRF接口,把识别逻辑做成API,小程序和App直接调用,后端代码几乎不用动。
6.4 结合地域与天气数据做精准防治建议
害虫的发生和地域、季节、天气关系极大。如果系统能拿到用户的地理位置和当前天气数据,比如温湿度、降雨量,就可以在防治建议里加上"当前温度适合该害虫繁殖,建议尽快处理"这类针对性提示。这个功能会让系统从"能识别"升级成"能指导",实用价值完全不一样。
我个人做完这套农业害虫识别系统,最大的体会是:毕设不是把代码写完就完了,把工程分层做好、把环境整理干净、把文档写清楚,这些"看不见的工作"反而决定了项目能不能顺利交付和演示。远程调试时省下的时间、答辩现场演示的流畅度,都来自这些前期准备。如果你正在做类似的题目,建议不要在模型训练上死磕刷分,把Django的集成、部署和文档做好,整个项目会稳得多。