首页 / 资讯中心 / 文章详情

SpringBoot+Flask+Vue+YOLOv5目标检测系统部署避坑

SpringBoot+Flask+Vue+YOLOv5目标检测系统部署避坑 ★ FEATURED ARTICLE
简介围绕SpringBoot、Vue、Flask与YOLOv5搭建的前后端分离目标检测系统面向做课程设计、毕业设计或计算机视觉项目实战的学生与开发者可用于玉米病虫害识别等图像检测场景。项目将Web界面、业务后端与模型推理服务拆分为三端Vue负责上传与结果展示SpringBoot承接业务请求Flask调用YOLOv5完成推理并通过接口回传便于理解检测系统从模型到页面的完整链路。压缩包共60个文件、约82.63MB以28个py源码和24个yaml配置为主另有少量pyc、md说明及zip分端压缩包基本覆盖三端代码、模型配置与运行材料。已有425人学习适合参考接口联调、目录组织与检测结果可视化实现并在此基础上替换数据集或扩展业务功能。1. 拆开一套 SpringBootFlaskVueYOLOv5 的目标检测系统先看它到底解决什么问题很多做深度学习落地的团队卡住的不是模型训练而是“模型训完了业务系统怎么接”。我见过太多项目把 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, pathMODEL_PATH, sourcelocal) 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, size640) # 推理输入尺寸 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(host0.0.0.0, port5000)逻辑说明模型加载放在模块顶层Flask 启动即完成一次加载请求只做前向torch.no_grad()必须加否则每次推理都建计算图显存会缓慢上涨直到 OOM。参数上conf控制召回与误检的平衡业务场景里如果漏检代价高就降到 0.15~0.2误检代价高就提到 0.4 以上size640要和训练时的输入尺寸尽量对齐训练用 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 { MultiValueMapString, 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); HttpEntityMultiValueMapString, Object entity new HttpEntity(body, headers); // 超时一定要设模型冷启动或 GPU 排队时会长时间不返回 SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(30000); restTemplate.setRequestFactory(factory); ResponseEntityDetectResult 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 filetest.jpg | python -m json.tool # 第二步走业务后端确认转发链路和超时 curl -X POST http://127.0.0.1:8080/api/detect \ -F filetest.jpg -H Authorization: Bearer token字段约定要提前定死boxes[].x1/y1/x2/y2是绝对像素还是归一化cls是数字还是直接给label。这套系统里我倾向同时给数字和标签数字用于前端按类别配色标签用于展示前端不做 id 到名字的映射映射关系留在服务端加类别时不用改前端。4. 避坑与排查这套组合最容易翻车的五个地方4.1 现象第二次请求开始推理变慢甚至超时原因Flask 开发服务器默认单进程单线程前一个请求在跑 GPU 推理时后续请求排队如果开了threadedTrue又没有锁多个请求同时进模型显存直接爆。 解决别用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(fconf{conf} 平均检出{total/len(imgs):.1f} f平均耗时{cost/len(imgs)*1000:.0f}ms)这里有个设计取舍要提前想清楚conf写死在服务端调参就得重启服务验证效率很低。更实用的做法是让/predict接受可选的conf和iou表单字段缺省用全局值这样上面这段脚本才能真正跑出对比。参数说明上conf影响查准率与查全率的权衡iou影响重叠框的合并强度密集小目标场景把iou适当调高能减少误合并但太高会把相邻目标吞掉。耗时统计要包含网络往返因为业务侧感知的就是这个数纯推理时间好看但链路慢没有意义。验证时还要盯住一个容易被忽略的指标首请求耗时。模型第一次前向会做 CUDA 初始化和显存分配往往比稳态慢好几倍如果业务侧读超时设得紧第一个用户必然失败。我的习惯是服务启动后先拿一张固定图跑一次“热身推理”把这次结果丢掉之后再对外提供。从那以后我每次部署推理服务都强制走一遍“热身后再验证”的流程否则线上第一个请求报错的问题能查半天。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站