多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Flask与深度学习的农业害虫识别系统构建指南

基于Flask与深度学习的农业害虫识别系统构建指南 去年帮几个农学方向的研究生搭过一套识别系统核心需求就一句话拍一张害虫照片网页上返回虫害种类和置信度。技术栈定的是Python Flask 深度学习预训练模型这套组合到今天依然是做课程设计、毕业设计、甚至小范围农技推广工具时最稳妥的选择。Flask足够轻不像Django那样自带一堆你用不上的东西Python生态又天然适合接图像处理和深度模型再加上现在开源社区有大量预训练好的图像分类权重做农业害虫识别完全不需要从零训练。这篇文章就把整个系统的设计思路、核心代码、部署排坑完整写一遍照着能跑起来。先交代一下系统边界。整套东西包含一个Flask Web应用一个图像分类推理模块一个简洁的上传页面最后是模型文件与类别映射文件。用户通过浏览器上传图片后端调用预训练模型做前向推理返回害虫类别名称、置信度以及同类别下排名前几的预测结果。硬件方面一台普通笔记本就够了CPU也能推理就是慢一点如果换成服务器部署加一块GPU推理速度会有质的提升。1. 农业害虫识别系统的整体设计思路1.1 从人工鉴定的痛点说起农业植保场景里害虫识别一直是个“看似简单、实际很费人力”的活。基层农技人员在田间拍到的害虫照片经常需要翻图鉴、问专家、查数据库才能判断种类。病虫害高发期一天可能积压几十上百张照片靠人工一张张比对效率低且误判率不低。尤其是近缘物种比如小菜蛾和甜菜夜蛾的幼虫外形相似度很高光靠肉眼和经验很难保证准确率。深度学习图像分类模型天然适合干这件事。只要有一个覆盖目标害虫种类的训练数据集模型就能学到不同虫态卵、幼虫、蛹、成虫在不同光照、不同背景下的视觉特征。部署成Web服务之后使用者不需要懂模型原理也不用安装Python环境打开浏览器上传图片就能拿到结果这对基层推广非常关键。1.2 为什么选Flask做Web框架做这类系统的Web框架候选有三个Flask、Django、FastAPI我最终选Flask理由很现实。Flask路由机制简单直接app.route(/predict, methods[POST])一行声明就能搞定接口没有太多约定。模板系统方便做演示页面render_template(index.html)传参数渲染页面对课程设计、毕业设计这种需要“看得见界面”的场景非常友好。Django太重自带ORM、Admin后台、迁移机制对单机推理服务来说属于杀鸡用牛刀。很多时候光是把Django跑起来就要折腾半天。FastAPI性能好但生态偏新FastAPI的并发性能确实强适合高吞吐API服务但它的异步模型对新手不太友好而且模板渲染、静态文件处理不如Flask顺手。Flask的核心优势在于它“轻而不散”。一套代码既能写后端接口也能渲染前端页面学习曲线平缓。结合本项目场景——单用户或少量并发、需要页面展示、后端只做推理服务——Flask是最平衡的选择。如果你是纯后端接口场景没有页面展示需求FastAPI可能更合适。但我个人做这类识别系统时仍会优先选Flask因为直接把前端页面塞进去演示效果比纯API好非常多。1.3 系统架构与处理流程整个系统按功能划分为三层展示层Web前端上传表单、图片预览、识别结果展示。业务层Flask路由与调度接收请求、校验文件格式、调用推理模块、返回结果。推理层模型服务图像预处理、加载模型权重、前向推理、结果后处理。请求处理流程用户在页面选择图片并点击上传 → 浏览器将图片以multipart/form-data格式POST到后端 → Flask路由接收文件对象 → 校验类型和大小 → 读取为图像数组 → 按模型要求预处理缩放、归一化、维度转换 → 加载的PyTorch模型执行推理 → softmax得到类别概率 → 取TopN结果 → JSON返回前端页面渲染。这套流程里最容易被新手忽略的是“预处理必须严格匹配训练时的流程”。很多人在这个环节踩坑之后莫名其妙模型效果差其实就是预处理不一致导致输入分布偏离了模型期望。2. 开发环境准备与依赖清单2.1 Python版本选择Python版本不要盲目追求最新尤其是做深度学习方向。我用的是Python 3.9GPU版PyTorch、TensorFlow、OpenCV的预编译轮子支持都非常好。Python 3.12或更高版本虽然更新但不少依赖包还没有跟上安装时容易碰到“找不到对应版本whl”的问题。如果你只是做纯Flask开发高版本的Python问题不大但一旦涉及PyTorch、OpenCV这种重量级依赖最好用3.9~3.11这个区间。2.2 虚拟环境与开发工具虚拟环境强烈建议用conda、虚拟环境或venv。之前见过有人图省事直接全局pip install结果系统Python环境被各种版本的依赖搞得乌烟瘴气项目之间互相打架。建环境时使用conda创建独立虚拟环境后续所有依赖都在环境内安装互不干扰。conda create -n pest_detect python3.9 conda activate pest_detect开发工具我用VSCode配一个Python插件就够了。选解释器的时候记得指到刚才创建的conda环境不然F5调试时找不到python helper roots报错信息一度很劝退新人。PyCharm用户也可以社区版免费够用在设置里把项目解释器切换为conda环境即可。2.3 核心依赖安装与国内镜像源本项目的关键依赖如下pip install flask3.0.0 pip install flask-cors pip install opencv-python pip install numpy pip install torch torchvision pip install pillow pip install gunicorn如果服务器在国内直接pip install可能会因为网络原因非常慢建议配置国内镜像源实测下来下载速度能提升好几倍。pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txtFlask-CORS主要用在前后端分离场景。我这个项目模板直接由Flask渲染同源访问理论上不需要CORS但加上也不多余特别是后面如果你想做小程序端或独立前端页面访问这个接口没有CORS会被浏览器拦截请求。opencv-python的安装有几个版本细节。opencv-python和opencv-contrib-python后者包含更多扩展模块一般图像分类用前者就够了。在服务器上如果安装遇到“cv2装不上”的情况多半是Python版本或系统架构不匹配可以尝试换用conda安装conda install -c conda-forge opencv。2.4 模型权重文件准备做农业害虫识别我建议走迁移学习路线不推荐从零训练。农业害虫数据集的标注成本极高虫态复杂且不同生长阶段外观差异大靠个人力量收集几万张高质量标注图不现实。公开可用的预训练ImageNet权重已经在千万级图像上学到了通用视觉特征做迁移学习微调就能在几千张害虫图上达到不错效果。本文示例代码直接加载一个残缺训练后的分类模型权重实际项目里你可以在第一阶段先加载torchvision.models.resnet50(pretrainedTrue)冻结特征层只在最后替换全连接层做微调训练。类别映射文件classes.json保存类别名与索引的对应关系推理结束后直接用索引查回中文名称。3. 核心功能与实现细节拆解3.1 图片接收与文件校验Flask里接收上传文件使用request.files对象。前端表单里input typefile nameimage后端拿到request.files[image]。这一层要注意三个坑空文件判断用户没选文件就点提交request.files[image]可能为空对象直接读取会报错。文件类型校验不能只看扩展名建议用PIL.Image.open()实际尝试打开验证是否为合法图片。文件大小限制Flask默认不限制请求体大小恶意用户传一个大文件会让推理进程内存直接被打满。建议设置app.config[MAX_CONTENT_LENGTH] 8 * 1024 * 1024即8MB上限。if image not in request.files: return jsonify({error: 未找到文件字段}), 400 file request.files[image] if file.filename : return jsonify({error: 未选择文件}), 4003.2 图像预处理流水线与维度问题这是最容易出错的地方需要特别解释清楚。PyTorch模型期待的输入是一个四维张量(batch_size, channels, height, width)。而用OpenCV读出来的图片是三维数组(height, width, channels)。所以必须两步走加一个batch维度unsqueeze(0)让模型认为这是一批一张图。通道顺序转换。OpenCV读图默认是BGR通道顺序而PyTorch预训练模型期望RGB。不做转换的话模型结果的准确率会明显下降——因为颜色通道信息全错位了。image cv2.imdecode(np.fromstring(file.read(), np.uint8), cv2.IMREAD_COLOR) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image cv2.resize(image, (224, 224)) image image.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) image (image - mean) / std image np.transpose(image, (2, 0, 1)) input_tensor torch.from_numpy(image).unsqueeze(0)归一化的mean和std用的是ImageNet的标准值。如果模型是在别的数据集上训练的自行微调过需要换成训练时的归一化参数否则输入分布不一致会严重影响预测结果。3.3 模型加载与推理封装模型加载要放在全局做一次不要在每次请求时才去调用。写进路由函数里会导致每个请求都重新加载一次模型推理速度慢得让人崩溃。我在代码里用模块级变量保存加载好的模型实例Flask多线程请求下所有请求共享同一个模型实例做推理。推理时也要注意别让梯度计算拖慢速度。PyTorch默认开启了自动求导预测阶段不需要记录梯度用torch.no_grad()包裹推理过程能明显减少内存开销并提升速度。with torch.no_grad(): outputs model(input_tensor) probabilities torch.nn.functional.softmax(outputs[0], dim0) top_probs, top_indices torch.topk(probabilities, 3)这里取Top3而不仅是Top1非常有实际意义。农业害虫的种间相似度高模型经常会把真正的类别排在前三但不在第一用户看到前三个候选类别结合人工判断比单一结果更实用。3.4 Flask路由设计与前后端交互路由设计一共两个核心接口GET /返回上传页面。POST /predict接收图片返回JSON结果。前端页面里的JS通过fetch提交表单数据不需要刷新页面就能拿到结果。具体交互逻辑是前端FormData对象包含文件字段fetch POST到后端拿到JSON后解析并渲染到页面上。“Flask如何绑定到网页元素”这个问题本质是Flask服务端只负责两件事页面模板渲染和接口数据返回。网页元素的操作由前端JS完成Flask本身不会主动去修改某个DOM元素。你可以在Flask路由里渲染模板时传入数据或者通过fetch从后端接口获取数据后再手动更新页面元素。results { success: True, predictions: [ {class: 小菜蛾, confidence: 0.933}, {class: 甜菜夜蛾, confidence: 0.041}, ] } return jsonify(results)需要注意Flask的jsonify默认会将中文转成Unicode转义字符返回的JSON里中文类别名会变成\u5c0f\u83dc\u86fe这种形式。虽然前端JS解析后能显示正常中文但调试时看起来很不舒服。可以设置app.json.ensure_ascii False这样接口返回的就是明文中文。3.5 模板渲染与识别结果展示Flask使用Jinja2模板引擎。前端页面上传图片后我做了即时预览用户还没提交就能看到自己选的图片体验感好很多。识别结果区域默认隐藏等fetch成功返回后显示结果列表。展示字段包括类别中文名、置信度百分比、占比进度条。进度条是纯CSS实现的宽度按置信度动态设置效果直观高效不需要引入前端图表库。div classresult-item span classlabel{{ item.class }}/span div classprogress-bar div classprogress stylewidth: {{ (item.confidence * 100)|round(1) }}%/div /div span classpercent{{ (item.confidence * 100)|round(1) }}%/span /div4. 完整实操过程与核心代码实现4.1 项目目录结构pest_detect/ ├── app.py # Flask应用主入口 ├── model.py # 模型加载与推理模块 ├── requirements.txt # 依赖清单 ├── models/ │ └── pest_resnet50.pth # 微调训练后的模型权重 ├── classes.json # 类别映射文件 ├── templates/ │ └── index.html # 上传页面模板 └── static/ └── style.css # 页面样式4.2 后端推理模块model.pymodel.py负责模型加载和推理封装核心思路是懒加载单例模式第一次调用时加载模型到内存之后直接复用。import json import torch import torchvision.models as models import torch.nn.functional as F device torch.device(cuda if torch.cuda.is_available() else cpu) _model None _class_names None def load_classes(pathclasses.json): global _class_names with open(path, r, encodingutf-8) as fp: data json.load(fp) _class_names data[classes] return _class_names def get_model(): global _model if _model is None: _model models.resnet50(weightsNone) num_features _model.fc.in_features _model.fc torch.nn.Linear(num_features, len(_class_names)) state_dict torch.load(models/pest_resnet50.pth, map_locationdevice) _model.load_state_dict(state_dict) _model.to(device) _model.eval() return _model def predict(image_tensor, top_k3): model get_model() with torch.no_grad(): outputs model(image_tensor.to(device)) probs F.softmax(outputs[0], dim0) top_probs, top_indices torch.topk(probs, top_k) results [] for prob, idx in zip(top_probs.tolist(), top_indices.tolist()): results.append({class: _class_names[idx], confidence: prob}) return results这里有个小细节model.eval()一定要调用。它会关闭Dropout和BatchNorm的训练行为避免推理结果不稳定。很多人加载模型后忘记切到eval模式预测结果每次都不一样就是这个原因。代码里torch.load在PyTorch 1.6以上版本默认会将权重加载到CPU内存再通过map_locationdevice控制。如果你的服务器只有CPU没有GPU统一指定map_locationcpu避免因GPU不可用而报错。4.3 Flask主应用app.pyapp.py把路由、文件处理、图像预处理、推理调用串联起来。import os import cv2 import numpy as np import torch from flask import Flask, render_template, request, jsonify from PIL import Image import model as model_utils app Flask(__name__) app.config[MAX_CONTENT_LENGTH] 8 * 1024 * 1024 app.json.ensure_ascii False MODEL_CONFIG { input_size: 224, mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225] } app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict_pest(): if image not in request.files: return jsonify({success: False, error: 未找到图片文件}), 400 file request.files[image] if file.filename : return jsonify({success: False, error: 未选择图片}), 400 try: image_bytes file.read() image cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR) if image is None: return jsonify({success: False, error: 图片格式不支持}), 400 image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image cv2.resize(image, (MODEL_CONFIG[input_size], MODEL_CONFIG[input_size])) image image.astype(np.float32) / 255.0 mean np.array(MODEL_CONFIG[mean], dtypenp.float32) std np.array(MODEL_CONFIG[std], dtypenp.float32) image (image - mean) / std image np.transpose(image, (2, 0, 1)) input_tensor torch.from_numpy(image).unsqueeze(0) results model_utils.predict(input_tensor, top_k3) return jsonify({success: True, predictions: results}) except Exception as exc: return jsonify({success: False, error: str(exc)}), 500 if __name__ __main__: model_utils.load_classes() app.run(host0.0.0.0, port5000, debugFalse)两个细节说下。端口用5000是Flask默认如果被占用就换一个比如5001。debugTrue在开发时开启方便热重载和错误信息显示但部署到公网环境务必改成debugFalse否则任何人访问你的接口都能看到完整堆栈信息等于把服务器内部暴露了。host改成0.0.0.0后局域网内其他设备也能通过服务器IP访问这个Web服务。演示环境里手机和电脑连同一个WiFi手机浏览器输入http://服务器IP:5000就能上手测试不用部署到公网就能完成现场演示。4.4 前端页面模板index.html页面保持极简风格核心是一个上传控件和一个结果展示区。隐藏的玉米文件输入框配合一个样式化的上传按钮上传后立即在页面显示预览图识别完成后展示Top3结果。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title农业害虫识别系统/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body div classcontainer h1农业害虫识别系统/h1 p classsubtitle上传害虫图片自动识别害虫种类与置信度/p div classupload-area input typefile idimageInput acceptimage/* hidden button iduploadBtn选择图片/button div idpreviewContainer img idpreviewImg alt预览图 styledisplay: none; /div button idpredictBtn styledisplay:none;开始识别/button /div div idresultArea styledisplay:none; h2识别结果/h2 div idresultList/div /div /div script const imageInput document.getElementById(imageInput); const previewImg document.getElementById(previewImg); const predictBtn document.getElementById(predictBtn); const resultArea document.getElementById(resultArea); const resultList document.getElementById(resultList); document.getElementById(uploadBtn).addEventListener(click, () { imageInput.click(); }); imageInput.addEventListener(change, (event) { const file event.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload (e) { previewImg.src e.target.result; previewImg.style.display block; predictBtn.style.display inline-block; resultArea.style.display none; }; reader.readAsDataURL(file); }); predictBtn.addEventListener(click, async () { const file imageInput.files[0]; if (!file) return; predictBtn.disabled true; predictBtn.textContent 识别中...; const formData new FormData(); formData.append(image, file); try { const response await fetch(/predict, { method: POST, body: formData }); const data await response.json(); renderResults(data); } catch (error) { resultList.innerHTML p stylecolor:red;请求失败 error /p; } finally { predictBtn.disabled false; predictBtn.textContent 开始识别; } }); function renderResults(data) { resultArea.style.display block; if (!data.success) { resultList.innerHTML p stylecolor:red; data.error /p; return; } resultList.innerHTML ; data.predictions.forEach(item { const div document.createElement(div); div.className result-item; const percent (item.confidence * 100).toFixed(1); div.innerHTML span classlabel${item.class}/span div classprogress-bar div classprogress stylewidth: ${percent}%/div /div span classpercent${percent}%/span ; resultList.appendChild(div); }); } /script /body /html4.5 运行与本地测试一切准备就绪后在项目根目录执行python app.py启动成功后控制台显示Running on http://0.0.0.0:5000浏览器访问本地地址就能看到上传页面。选一张害虫图片测试正常情况下几秒内返回识别结果。本地测试阶段建议多准备几张不同类型害虫的图片覆盖不同光源、不同背景、不同拍摄角度确认模型的泛化能力。测试用的图片集建议单独放一个test_images目录后续做模型迭代评估也用得上。5. 部署到服务器与常见问题排查5.1 Windows服务器部署与附件路径错误Windows服务器上部署Flask有一个典型坑app.run()默认工作目录是启动脚本所在目录但如果你使用系统服务或计划任务启动当前工作目录可能变成C:\Windows\System32这时候模型文件和classes.json的相对路径全部失效就会出现“附件路径错误”、“找不到模型文件”一类的提示。解决方式很简单在app.py顶部根据__file__动态构造绝对路径不依赖当前工作目录。import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_PATH os.path.join(BASE_DIR, models, pest_resnet50.pth) CLASSES_PATH os.path.join(BASE_DIR, classes.json)在Linux服务器上同理建议一律使用绝对路径避免cron或systemd启动时因工作目录不同而找不到文件。5.2 生产环境部署建议Flask自带的开发服务器app.run()性能有限不能直接用于生产。Linux服务器上部署推荐用gunicorn做WSGI服务器搭配Nginx反向代理。pip install gunicorn gunicorn -w 4 -b 0.0.0.0:5000 app:app-w 4表示启动4个worker进程能利用多核CPU并行处理请求。每个worker进程在启动时都会加载一次模型4个worker意味着模型权重在我们当前示例RAM占用224x224x3的输入ResNet50大概占几百MB内存会放大到4倍服务器内存小的话要注意。如果你的模型文件很大调整worker数量需要你先做个权衡。Nginx反向代理要留意请求体大小限制。Nginx默认client_max_body_size是1MB大于1MB的图片上传会被Nginx直接拒掉。在上传图片比较大的场景这个配置必须调整server { listen 80; server_name your_domain; client_max_body_size 10m; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }5.3 常见问题速查表问题现象可能原因解决方案VSCode运行时找不到python helper rootsPython解释器没有指向当前虚拟环境在VSCode左下角选择正确的conda/virtualenv解释器或是cmd下where python检查环境变量。cv2安装失败Python版本或系统架构不匹配pip源切换Python 3.9~3.11使用conda安装opencv。模型加载时报错“Missing key(s) in state_dict”模型结构与权重文件不匹配检查全连接层输出类别数是否与训练时一致检查weights是否为None。首次请求特别慢懒加载导致模型在第一个请求时才加载进内存在app.run()之前主动调用一次model_utils.get_model()提前预热。返回结果中文变成\u转义jsonify默认开启ascii编码设置app.json.ensure_ascii False。Windows部署提示附件路径错误工作目录不是项目目录所有文件路径基于__file__构建绝对路径。上传大图时进程崩溃图片未压缩直接resize内存占用过高先按最大边缩放比如最长边超过1024就先缩到1024内再resize到224x224。每个请求都重新加载模型响应极慢模型加载代码写在路由函数内把模型加载放在模块顶层或用单例缓存。局域网访问不到服务Flask默认只绑定127.0.0.1app.run(host0.0.0.0)。Flask端口被占用5000端口被其他进程抢占换端口或先查进程再处理。上面最后那条多说一句Windows下常用netstat -ano | findstr :5000查端口占用Linux下用lsof -i:5000。5.4 性能优化建议一个识别服务上线后最重要的性能指标就是单次推理耗时长不长。实测下来在CPU上ResNet50单张224x224图片推理大概需要200~500毫秒GPU上可以压缩到10毫秒以内。由于本项目基于Flask多线程高并发场景下多个请求同时触发模型推理GPU显存会同时被多个线程占用需要考虑用线程锁控制推理队列。进一步优化有以下方向模型轻量化把ResNet50换成MobileNetV3或EfficientNet-Lite精度损失不大的前提下推理速度能提升数倍。图片压缩上传接口先对图片做尺寸压缩一方面减少传输耗时另一方面避免超大图拖慢预处理。结果缓存相同图片MD5做缓存重复请求直接返回缓存结果减少无效推理。批处理把多个请求的图像堆成一个batch一次推理返回多个结果能明显提升GPU利用率。我在实际项目里做过对比MobileNetV3-Large在保持Top1精度只下降不到2个点的情况下CPU推理速度从ResNet50的400毫秒左右降到80毫秒左右应对中小规模并发绰绰有余。如果你只需要做一个课程设计或展示原型ResNet50完全够用如果要正式部署到基层农技站我建议换成MobileNetV3部署和维护压力小很多。个人实操中的几点体会最后分享这套系统做下来最深刻的三点体会。第一数据质量永远比模型结构重要。农业害虫识别模型的准确率天花板不是由模型决定的而是由训练数据决定的。不同虫态、不同背景、不同地域的样本覆盖度直接决定模型在真实场景下的可靠性。如果只做演示系统网上开源的IP102数据集就够用了但要想真正下地使用一定要自己采集一批目标区域的害虫照片做补充训练。第二识别结果展示方式影响系统实际价值。只显示一个类别的命中结果用户很难判断这个结果是否可信。我把Top3候选类别和置信度都展示出来用户在田间结合自己的经验判断“第一候选和第二候选哪个更像”比纯黑盒输出一个结果实用得多。农业领域技术落地的核心是辅助决策不是替代决策。第三部署环节的坑永远比开发环境多。我在项目里吃过工作目录不对导致模型文件找不到的亏也见过同事因为忘记设置Nginx上传大小限制而被客户截图吐槽接口报错的尴尬。部署前把路径、依赖、反向代理配置都梳理清楚上线后能省去非常多麻烦。这套“Python Flask 预训练模型”的害虫识别系统整体工程不大但麻雀虽小五脏俱全。后续要扩展也很容易前端加摄像头调用实现实时识别后端接入更多害虫类别推理层换成ONNX Runtime提升效率甚至加个数据库记录识别历史和GIS位置信息。方向很清晰动手做一个就能把Web开发、深度学习实践、服务部署全串起来。
返回列表