简介:围绕SpringBoot、Vue、Flask与YOLOv5搭建的前后端分离目标检测系统,面向做课程设计、毕业设计或计算机视觉项目实战的学生与开发者,可用于玉米病虫害识别等图像检测场景。项目将Web界面、业务后端与模型推理服务拆分为三端:Vue负责上传与结果展示,SpringBoot承接业务请求,Flask调用YOLOv5完成推理并通过接口回传,便于理解检测系统从模型到页面的完整链路。压缩包共60个文件、约82.63MB,以28个py源码和24个yaml配置为主,另有少量pyc、md说明及zip分端压缩包,基本覆盖三端代码、模型配置与运行材料。已有425人学习,适合参考接口联调、目录组织与检测结果可视化实现,并在此基础上替换数据集或扩展业务功能。
1. 拆开一套 SpringBoot+Flask+Vue+YOLOv5 的目标检测系统,先看它到底解决什么问题
很多做深度学习落地的团队,卡住的不是模型训练,而是“模型训完了,业务系统怎么接”。我见过太多项目把 YOLOv5 的权重训到 mAP 挺好看,最后交付时却是一个detect.py加几行命令行,业务方根本没法用。这套基于 SpringBoot、Flask、Vue 和 YOLOv5 的前后端分离目标检测系统,解决的正是这个断层:把推理能力包成 HTTP 服务,用 Java 侧承接业务与权限,用 Vue 侧做可视化交互,让“检测”变成系统里的一个接口调用而不是一段脚本。它适合三类人:想把检测能力嵌入现有业务系统的后端开发者、需要做课程设计或工程实践的学生,以及想把算法 Demo 升级成可演示产品的算法同学。下面按可复现的路径拆。
2. 四层架构:为什么用 Python 做推理、Java 做业务、Vue 做壳
2.1 分层选型的真实理由
先说清为什么不是“一个 Python 项目全干完”。YOLOv5 生态在 Python,训练、导出、推理都在这条线上,PyTorch 依赖重、显存占用高,把它直接塞进 SpringBoot 的 JVM 进程里,常见做法要么是调命令行、要么走 ONNX Runtime 的 Java 绑定,前者进程管理混乱,后者版本对齐和算子支持经常翻车。所以这套系统把职责切开:
| 层 | 技术 | 职责 | 不该干的事 |
|---|---|---|---|
| 前端 | Vue | 上传、结果渲染、历史列表 | 不做任何推理与图片持久化判断 |
| 业务后端 | SpringBoot | 鉴权、任务编排、记录落库、文件元数据 | 不加载模型权重 |
| 推理服务 | Flask | 加载 YOLOv5、执行推理、返回结构化结果 | 不管用户、不管权限 |
| 算法 | YOLOv5 | 检测本身 | 不关心 HTTP |
这个切法的核心收益是解耦部署与解耦扩缩。推理服务是 GPU 密集的,业务后端是 IO 密集的,两者放一台机器只是图省事,真实场景里推理服务可以独立扩容、独立重启,模型换版本不影响业务侧。代价是多了一跳网络调用和一套序列化约定,这也是后面坑最多的地方。
Flask 在这里的角色很轻:它只需要一个/predict接口,接收图片、跑模型、把[x1,y1,x2,y2,conf,cls]这种结果序列化回去。别指望它做并发,Flask 自带开发服务器是单线程的,生产环境常见做法是用 gunicorn 起多 worker,但每个 worker 会各加载一份模型,显存会翻倍——这一点后面避坑章节细说。
2.2 推理服务的落地写法
先把 Flask 侧的推理服务写出来。这一步的关键不是“能跑”,而是“输出结构稳定”,因为 Java 侧要按固定字段解析。
# app.py —— Flask 推理服务 import io import torch from flask import Flask, request, jsonify from PIL import Image app = Flask(__name__) # 启动时加载一次模型,绝不要放在请求处理函数里 MODEL_PATH = "weights/best.pt" DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu" model = torch.hub.load("ultralytics/yolov5", "custom", path=MODEL_PATH, source="local") model.to(DEVICE).eval() model.conf = 0.25 # 置信度阈值 model.iou = 0.45 # NMS 的 IoU 阈值 @app.route("/predict", methods=["POST"]) def predict(): if "file" not in request.files: return jsonify({"code": 400, "msg": "no file"}), 400 file = request.files["file"] img = Image.open(io.BytesIO(file.read())).convert("RGB") with torch.no_grad(): # 关掉梯度,省显存 results = model(img, size=640) # 推理输入尺寸 dets = results.xyxy[0].cpu().numpy() # [x1,y1,x2,y2,conf,cls] boxes = [] for x1, y1, x2, y2, conf, cls_id in dets: boxes.append({ "x1": float(x1), "y1": float(y1), "x2": float(x2), "y2": float(y2), "conf": float(conf), "cls": int(cls_id), "label": results.names[int(cls_id)] }) return jsonify({"code": 200, "count": len(boxes), "boxes": boxes}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)逻辑说明:模型加载放在模块顶层,Flask 启动即完成一次加载,请求只做前向;torch.no_grad()必须加,否则每次推理都建计算图,显存会缓慢上涨直到 OOM。参数上conf控制召回与误检的平衡,业务场景里如果漏检代价高就降到 0.15~0.2,误检代价高就提到 0.4 以上;size=640要和训练时的输入尺寸尽量对齐,训练用 640 推理用 1280 不一定会更好,小目标可能受益但速度掉一半。
坐标返回的是原图坐标系下的绝对像素值,results.xyxy已经做过缩放还原,这点比自己去乘比例靠谱。如果前端要画框,需要知道原图的宽高,所以实际项目里我一般让接口同时返回width和height,省得前端再猜。
2.3 SpringBoot 侧的调用与编排
Java 侧不碰模型,只做三件事:收前端上传、转发给 Flask、把结果和文件信息落库。用RestTemplate或WebClient都行,这里给同步版本,够用且好排查。
// DetectService.java —— 业务侧转发推理请求 @Service public class DetectService { @Value("${infer.url}") private String inferUrl; // 配置化,别写死 private final RestTemplate restTemplate = new RestTemplate(); public DetectResult detect(MultipartFile file) throws IOException { MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); // 用 ByteArrayResource 重写文件名,否则 Flask 侧拿不到后缀 body.add("file", new ByteArrayResource(file.getBytes()) { @Override public String getFilename() { return file.getOriginalFilename(); } }); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers); // 超时一定要设,模型冷启动或 GPU 排队时会长时间不返回 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(30000); restTemplate.setRequestFactory(factory); ResponseEntity<DetectResult> resp = restTemplate.postForEntity(inferUrl + "/predict", entity, DetectResult.class); return resp.getBody(); } }关键点在两个地方。第一,infer.url走配置文件,本地是http://127.0.0.1:5000,部署到内网就换成服务地址,不要让 IP 散落在代码里。第二,ByteArrayResource必须重写getFilename(),否则 multipart 里文件名是 null,某些图像库按后缀判断格式时会直接抛异常,这个坑很隐蔽:本地测试一切正常,换个图片库版本就报错。
至于上传文件本身,常见做法是先落到本地临时目录或对象存储,拿到 URL 再传给推理服务;这套系统如果只是演示,直接内存转发最省事,但要注意 SpringBoot 默认单文件大小限制是 1MB,得在配置里放开:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB不放开的话,上传稍微大一点的图就返回MaxUploadSizeExceededException,前端只看到一个 500,很容易误判成推理挂了。
3. Vue 前端怎么把检测结果画对:坐标、缩放与交互
3.1 画框前必须对齐的三件事
前端最容易出的问题不是接口调不通,而是框画偏了。原因几乎都是坐标空间不一致。假设后端返回的是原图像素坐标,前端展示区却把图片用 CSS 缩到了 600px 宽,那框就得按同样的比例缩放。我一般这么做:拿到图片后先new Image()读出naturalWidth/naturalHeight,再按容器的clientWidth算出缩放比,画布和图片用同一个容器尺寸。
// drawBoxes.js —— 按显示尺寸等比缩放画框 export function drawBoxes(canvas, imgEl, boxes) { const ctx = canvas.getContext('2d') const ratio = imgEl.clientWidth / imgEl.naturalWidth canvas.width = imgEl.clientWidth canvas.height = imgEl.naturalHeight * ratio ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.lineWidth = 2 ctx.font = '14px sans-serif' boxes.forEach(b => { const x = b.x1 * ratio const y = b.y1 * ratio const w = (b.x2 - b.x1) * ratio const h = (b.y2 - b.y1) * ratio ctx.strokeStyle = '#00e676' ctx.strokeRect(x, y, w, h) // 标签背景,避免文字和图像混在一起看不清 const text = `${b.label} ${(b.conf * 100).toFixed(0)}%` const tw = ctx.measureText(text).width + 8 ctx.fillStyle = 'rgba(0,0,0,0.6)' ctx.fillRect(x, Math.max(0, y - 20), tw, 18) ctx.fillStyle = '#fff' ctx.fillText(text, x + 4, Math.max(14, y - 6)) }) }逻辑上,ratio把原图坐标映射到显示坐标,宽高都要乘同一个系数,只乘宽不乘高是最常见的错。canvas的尺寸要手动设在 DOM 属性上而不是只用 CSS,否则高分屏下会模糊。标签贴边时y - 20可能是负数,所以要Math.max兜一下,不然文字被裁掉。
3.2 前后端字段约定与联调顺序
联调顺序建议按“推理服务 → 业务接口 → 前端”走,不要反过来。先用 curl 直接打 Flask 确认输出结构,再接 Java,最后接 Vue。
# 第一步:单测推理服务,确认字段名和坐标范围 curl -X POST http://127.0.0.1:5000/predict \ -F "file=@test.jpg" | python -m json.tool # 第二步:走业务后端,确认转发链路和超时 curl -X POST http://127.0.0.1:8080/api/detect \ -F "file=@test.jpg" -H "Authorization: Bearer <token>"字段约定要提前定死:boxes[].x1/y1/x2/y2是绝对像素还是归一化,cls是数字还是直接给label。这套系统里我倾向同时给数字和标签,数字用于前端按类别配色,标签用于展示,前端不做 id 到名字的映射,映射关系留在服务端,加类别时不用改前端。
4. 避坑与排查:这套组合最容易翻车的五个地方
4.1 现象:第二次请求开始推理变慢甚至超时
原因:Flask 开发服务器默认单进程单线程,前一个请求在跑 GPU 推理时后续请求排队;如果开了threaded=True又没有锁,多个请求同时进模型,显存直接爆。 解决:别用app.run上生产。用gunicorn -w 1 --threads 4 -b 0.0.0.0:5000 app:app这种“单 worker 多线程 + 内部串行推理”的配置,模型只加载一份,请求排队但不炸显存。需要更高吞吐再考虑多进程加显存预算。
4.2 现象:中文路径或中文文件名上传后报解码错误
原因:multipart 的文件名编码在不同容器和库版本间不一致,Java 侧和 Python 侧对文件名的处理不统一。 解决:服务端不信文件名,落盘时自己生成 UUID 加白名单后缀,原始文件名只作为元数据存库。这一步同时解决了路径穿越问题,一举两得。
4.3 现象:本地好好的,部署后接口 404 或跨域失败
原因:Vue 打包后是静态资源,开发时靠 devServer 代理,生产环境代理没了;或者 SpringBoot 的 context-path 和前端写死的/api前缀对不上。 解决:前端请求地址统一走环境变量或运行时配置,别写死。Nginx 侧把/api转发到 SpringBoot、把/指向静态目录,跨域在同源后就自动消失了,比在 Java 里配 CORS 干净。
4.4 现象:检测框整体偏移或尺寸不对
原因:前端按显示尺寸画框但用了原图坐标,或者后端对图片做了 resize 却没把坐标还原。 解决:约定“服务端永远返回原图坐标系”,谁缩放谁负责还原。加一个带明显边界的测试图(比如画了四角标记的图),一眼就能看出是不是等比缩放错了。
4.5 现象:GPU 显存随请求数缓慢增长直到 OOM
原因:推理没包在torch.no_grad()里,或者返回的 tensor 被 Python 侧引用着没释放,也可能是多次torch.hub.load重复加载模型。 解决:确认no_grad包裹、结果及时转 numpy、模型全局只加载一次。排查时用nvidia-smi隔几秒看一次显存曲线,稳定不涨才是对的。
提示:这套系统的排错顺序永远是“推理服务单测 → 网络链路 → 业务编排 → 前端渲染”,从后往前查会把网络问题误判成渲染问题。
5. 把阈值和性能调明白:一次可复现的验证流程
模型接进系统只是开始,真正决定体验的是阈值和响应时间。我一般用一个固定的小样本集做回归验证,而不是每次凭感觉调。做法是准备 20~50 张覆盖主要场景的图,写个脚本批量打推理服务,统计不同conf下的检出数量与耗时,画成表再定参数。
# eval_threshold.py —— 批量跑阈值对比,输出可贴进表格的结果 import time, requests, glob URL = "http://127.0.0.1:5000/predict" imgs = glob.glob("samples/*.jpg") for conf in [0.15, 0.25, 0.35, 0.45]: # 注意:conf 若在服务启动时固定,需要改成接口参数透传 total, cost = 0, 0.0 for p in imgs: t0 = time.time() with open(p, "rb") as f: r = requests.post(URL, files={"file": f}).json() cost += time.time() - t0 total += r["count"] print(f"conf={conf} 平均检出={total/len(imgs):.1f} " f"平均耗时={cost/len(imgs)*1000:.0f}ms")这里有个设计取舍要提前想清楚:conf写死在服务端,调参就得重启服务,验证效率很低。更实用的做法是让/predict接受可选的conf和iou表单字段,缺省用全局值,这样上面这段脚本才能真正跑出对比。参数说明上,conf影响查准率与查全率的权衡,iou影响重叠框的合并强度,密集小目标场景把iou适当调高能减少误合并,但太高会把相邻目标吞掉。耗时统计要包含网络往返,因为业务侧感知的就是这个数,纯推理时间好看但链路慢没有意义。
验证时还要盯住一个容易被忽略的指标:首请求耗时。模型第一次前向会做 CUDA 初始化和显存分配,往往比稳态慢好几倍,如果业务侧读超时设得紧,第一个用户必然失败。我的习惯是服务启动后先拿一张固定图跑一次“热身推理”,把这次结果丢掉,之后再对外提供。从那以后我每次部署推理服务,都强制走一遍“热身后再验证”的流程,否则线上第一个请求报错的问题能查半天。希望帮到你。
本文还有配套的精品资源,点击获取