简介:这份资源面向计算机视觉与深度学习方向的开发者、学生及项目实践者,提供一套基于SpringBoot、Flask、Vue与YOLOv5的前后端分离式目标检测系统完整源码,可用于安防监控、视频流分析等实时检测场景的学习与二次开发。压缩包共60个文件,约82.63MB,以28个Python脚本和24个YAML配置文件为主,前者承载Flask端推理服务与YOLOv5模型调用逻辑,后者多用于模型结构与训练参数配置,另含少量pyc缓存、md说明及zip子包。资源按flask端、web端、springboot端三部分组织,Flask负责图像输入与YOLOv5检测并输出API结果,Vue组件完成上传与结果展示,SpringBoot处理后端请求与业务逻辑,形成完整闭环。目前已有424人学习下载,适合希望理解多框架协作、掌握目标检测系统落地流程的读者参考,可据此快速搭建可运行环境并扩展自身业务模块。
1. 前后端分离的目标检测系统:为什么我劝你别把 YOLOv5 塞进 SpringBoot
很多团队第一次做目标检测系统,第一反应是「一个 Java 服务全包了」,结果模型加载慢、显存抢不到、接口一并发就雪崩。我踩过这个坑:某次把 YOLOv5 的推理逻辑硬塞进 SpringBoot 的 Service 层,单张图 800ms,并发到 5 就 OOM。后来改成 SpringBoot 管业务、Flask 管推理、Vue 管交互的三段式,同样的机器单张压到 120ms,并发 20 稳如老狗。这套「基于 springboot+flask+vue+yolov5 实现前后端分离式目标检测系统」的核心思路,就是把「业务编排」和「模型推理」彻底解耦——SpringBoot 做用户、权限、任务、存储,Flask 只做一件事:收图、推理、吐 JSON。适合谁?适合手里有标注数据、想快速搭一套能演示能落地的检测平台的团队,也适合想理解「模型服务化」到底怎么拆的工程师。下面我按选型、搭环境、写推理服务、联调、避坑、进阶的顺序,把能抄的作业全给你。
2. 三段式架构怎么拆:SpringBoot、Flask、Vue 各管什么
2.1 为什么不让 SpringBoot 直接调 Python 脚本
最常见的偷懒做法是 Java 里用Runtime.exec("python detect.py")或者 Jython 调 Python。这两种我都试过,结论是:演示可以,上线必翻车。Runtime.exec每次推理都要起一个新进程,加载一次模型权重,YOLOv5s 的权重加载在普通 CPU 上就要 1.5 到 2 秒,等于每张图白等两秒;而且进程间传图片只能靠临时文件,磁盘 IO 直接成为瓶颈。Jython 更不现实,它只支持 Python 2.7 语法,PyTorch 根本装不上。
正确的拆法是让 Flask 常驻,模型在 Flask 进程启动时加载一次,之后所有请求复用这个模型对象。SpringBoot 通过 HTTP 调用 Flask 的/predict接口,传图片的 base64 或 multipart,Flask 返回检测框的 JSON。这样模型只加载一次,SpringBoot 也不用关心 PyTorch 的版本问题。
提示:Flask 不要用开发服务器
app.run()上生产,用 gunicorn 起多 worker,但 worker 数不要超过 GPU 能承载的并发数,否则每个 worker 都加载一份模型,显存直接爆。
2.2 三个服务的职责边界与端口规划
职责划清楚,后面联调才不会互相甩锅。我一般这样分:
| 服务 | 职责 | 默认端口 | 不该做的事 |
|---|---|---|---|
| Vue 前端 | 上传图片、展示检测框、历史记录 | 5173(dev) | 不直接调 Flask |
| SpringBoot | 用户鉴权、任务落库、文件存储、调 Flask | 8080 | 不加载模型、不做推理 |
| Flask | 加载 YOLOv5、推理、返回坐标 | 5000 | 不碰数据库、不做业务逻辑 |
端口规划上有个细节:Vue 开发时用 vite 的 proxy 把/api转发到 SpringBoot 的 8080,SpringBoot 再用 RestTemplate 或 WebClient 调 Flask 的 5000。这样前端只需要认一个地址,跨域问题在开发阶段就解决了。生产环境用 Nginx 把/api转给 SpringBoot,/flask转给 Flask,前端打包成静态文件。
2.3 数据流:一张图从上传到画出框的完整链路
把链路走一遍,你就知道该在哪一层加日志。用户在 Vue 里选一张图,前端用 FormData 把文件 POST 到/api/detect/upload;SpringBoot 收到后先存到本地或对象存储,生成一个 taskId,然后把图片字节流用 base64 编码,POST 到 Flask 的/predict;Flask 解码成 PIL Image,做 letterbox 预处理,送进 YOLOv5 模型,拿到[x1,y1,x2,y2,conf,cls]的数组,转成 JSON 返回;SpringBoot 把结果和 taskId 一起写库,返回给前端;前端拿到坐标后,按原图尺寸缩放,用 canvas 或绝对定位的 div 把框画上去。
这条链路里最容易出问题的是坐标缩放。YOLOv5 推理时做了 letterbox,返回的坐标是相对于 640x640 输入图的,你要把它映射回原图尺寸。这个映射逻辑我放在 Flask 里做,因为 Flask 最清楚预处理时用了多少 padding。
3. 把 YOLOv5 推理服务跑起来:Flask 接口的四个关键参数
3.1 模型加载:只加载一次,别在请求里 load
Flask 里加载模型的位置很关键。放在视图函数里,每次请求都重新加载,必卡;放在if __name__ == '__main__'里,gunicorn 多 worker 时每个 worker 都会执行一次,这是对的,但要注意显存。我一般用模块级全局变量,配合torch.no_grad()。
# app.py import torch from flask import Flask, request, jsonify from PIL import Image import io, base64 import numpy as np app = Flask(__name__) # 模块级加载,gunicorn 每个 worker 加载一次 DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.to(DEVICE).eval() model.conf = 0.25 # 置信度阈值 model.iou = 0.45 # NMS 的 IoU 阈值 @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() img_b64 = data['image'] img_bytes = base64.b64decode(img_b64) img = Image.open(io.BytesIO(img_bytes)).convert('RGB') with torch.no_grad(): results = model(img, size=640) # results.xyxy[0] 是 tensor,转 list detections = results.xyxy[0].cpu().numpy().tolist() return jsonify({'boxes': detections, 'orig_shape': img.size})这段代码里model.conf和model.iou是两个必调参数。conf调低召回高但误检多,调高则漏检;iou控制 NMS 合并重叠框的力度,密集场景要适当调低。size=640是推理分辨率,和训练时一致最好,改大精度略升但速度下降。
3.2 请求体格式:base64 还是 multipart
两种都行,但我推荐 base64。原因:SpringBoot 用 RestTemplate 发 multipart 要额外配MultiValueMap和FormHttpMessageConverter,而 base64 就是一个 JSON 字段,序列化简单。代价是体积膨胀约 33%,但内网传输可以忽略。如果图片很大(超过 4MB),base64 字符串会很长,记得在 SpringBoot 和 Flask 两边都调大请求体限制。
# Flask 端限制请求体 16MB app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024SpringBoot 端要在application.yml里设spring.servlet.multipart.max-request-size,如果用 WebClient 还要设maxInMemorySize。
3.3 返回结构设计:坐标、类别、置信度一个都不能少
返回的 JSON 结构直接决定前端画框的难易。我习惯返回这样的结构:
{ "boxes": [[x1, y1, x2, y2, conf, cls_id], ...], "names": ["person", "car", ...], "orig_shape": [width, height] }boxes里每个元素是六个值,前四个是左上右下坐标,第五个是置信度,第六个是类别 id。names是类别 id 到名称的映射,前端画框时显示名称用。orig_shape是原图宽高,前端按这个缩放。注意坐标是相对于 letterbox 后的 640 图的,前端如果直接按原图尺寸画会偏,所以我在 Flask 里已经做了反 letterbox 映射,返回的就是原图坐标。
3.4 用 gunicorn 起服务:worker 数和超时怎么定
开发时python app.py够了,生产用 gunicorn:
gunicorn -w 2 -b 0.0.0.0:5000 --timeout 120 app:app-w 2是两个 worker,每个 worker 加载一份模型。如果你的 GPU 显存是 8GB,YOLOv5s 推理大概占 1.5GB,两个 worker 就是 3GB,够用。如果是 YOLOv5x,一个 worker 就占 4GB 以上,那就只能-w 1,靠 Flask 内部的线程池处理并发。--timeout 120是防止大图推理超时被 gunicorn 杀掉,默认 30 秒对高分辨率图不够。
注意:gunicorn 的 worker 是进程级,不是线程级。
-w 2意味着两个独立进程,各自有模型副本。如果你用--threads,那是每个 worker 内部的线程,PyTorch 推理本身会释放 GIL,所以多线程也能提吞吐,但要注意显存竞争。
4. SpringBoot 侧怎么调 Flask:RestTemplate 配置与超时重试
4.1 用 RestTemplate 还是 WebClient
SpringBoot 里调外部 HTTP,老项目用 RestTemplate,新项目用 WebClient。RestTemplate 是同步阻塞的,代码直观;WebClient 是响应式的,适合高并发。目标检测这种场景,单次推理就要几百毫秒,同步阻塞反而好控制,我一般用 RestTemplate,配一个连接池。
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); // 连接超时 5s factory.setReadTimeout(60000); // 读取超时 60s,推理可能慢 return new RestTemplate(factory); } }setReadTimeout必须设大,默认是无限等待,但如果你用了某些连接池默认 10 秒,大图推理就会抛SocketTimeoutException。我设 60 秒是保险值,实际 YOLOv5s 在 GPU 上单图 100ms 左右,CPU 上 1 到 2 秒。
4.2 图片转 base64 的坑:别用 toString
Java 里把byte[]转 base64 要用Base64.getEncoder().encodeToString(bytes),别用bytes.toString(),那出来的是对象地址。这个坑我见过不止一个新手踩。
@Service public class DetectService { @Autowired private RestTemplate restTemplate; public DetectResult detect(MultipartFile file) throws IOException { byte[] bytes = file.getBytes(); String b64 = Base64.getEncoder().encodeToString(bytes); Map<String, String> body = new HashMap<>(); body.put("image", b64); // Flask 地址配在 yml 里 String flaskUrl = "http://localhost:5000/predict"; ResponseEntity<Map> resp = restTemplate.postForEntity(flaskUrl, body, Map.class); // 解析 resp.getBody() 里的 boxes return parse(resp.getBody()); } }postForEntity的第二个参数是请求体,Spring 会自动用 Jackson 序列化成 JSON。返回的Map里boxes是List<List<Double>>,解析时注意类型转换。
4.3 超时与重试:推理失败要不要重试
推理失败分两种:一种是网络抖动,Flask 没收到请求,这种可以重试;另一种是 Flask 收到了但推理报错(比如图片格式不对),重试也没用。我一般只对ResourceAccessException(连接超时)重试一次,对HttpServerErrorException(5xx)不重试,直接返回错误给前端。
@Retryable(value = ResourceAccessException.class, maxAttempts = 2, backoff = @Backoff(delay = 500)) public DetectResult detectWithRetry(MultipartFile file) throws IOException { return detect(file); }用 Spring Retry 要加@EnableRetry注解。重试次数别超过 2,否则用户等太久。
4.4 结果落库:检测框存 JSON 还是拆表
检测结果我建议存 JSON 字符串,别拆成box表。原因:检测框数量不固定,拆表后查询要 join,而且前端展示时本来就是一次性拿全部框,拆表没收益。用 MySQL 的JSON类型或者TEXT存序列化后的数组,查询时反序列化即可。
CREATE TABLE detect_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, image_path VARCHAR(255), result_json JSON, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );result_json里存{"boxes": [...], "names": [...]}。如果要做统计(比如统计某类目标出现次数),可以在写入时额外算好存到另一个字段,别在查询时遍历 JSON。
5. 联调与部署避坑:坐标偏移、跨域、显存泄漏
5.1 画框偏移:letterbox 的 padding 没还原
现象:前端画的框整体偏下或偏右,偏移量随图片宽高比变化。原因:YOLOv5 推理时把原图等比缩放到 640x640,短边补灰边(letterbox),返回的坐标是相对于 640 图的。如果你直接按原图尺寸画,就偏了。解决:在 Flask 里记录缩放比例和 padding,把坐标反算回原图。
def scale_coords(img1_shape, coords, img0_shape): # img1_shape: 640x640, img0_shape: 原图 gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2) coords[:, [0, 2]] -= pad[0] coords[:, [1, 3]] -= pad[1] coords[:, :4] /= gain return coords这段逻辑 YOLOv5 源码里有,直接抄。关键是gain和pad的计算,宽高别搞反。
5.2 跨域:开发环境用 vite proxy,生产用 Nginx
现象:前端 5173 调 8080 报 CORS 错误。原因:浏览器同源策略。解决:开发时在vite.config.js里配 proxy:
export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样前端请求/api/detect/upload会被转发到 8080,浏览器认为是同源。生产环境用 Nginx:
location /api/ { proxy_pass http://127.0.0.1:8080/; } location /flask/ { proxy_pass http://127.0.0.1:5000/; }别在 Flask 里加CORS(app)就完事,那只是开发方便,生产环境跨域配置要收敛。
5.3 显存泄漏:torch.no_grad 和 del 别省
现象:Flask 跑一段时间后 GPU 显存占满,推理变慢甚至 OOM。原因:PyTorch 默认会构建计算图,即使你不 backward,中间变量也占显存。解决:推理代码必须包在with torch.no_grad():里,大图处理后手动del中间变量,必要时torch.cuda.empty_cache()。
with torch.no_grad(): results = model(img, size=640) detections = results.xyxy[0].cpu().numpy().tolist() del results torch.cuda.empty_cache() # 谨慎用,频繁调用会拖慢empty_cache()别每张图都调,它会让 CUDA 重新分配内存,反而慢。我一般每 100 张调一次,或者发现显存涨到阈值再调。
5.4 图片格式:PIL 打不开的 CMYK 和透明 PNG
现象:用户上传某些 PNG 报cannot identify image file。原因:PIL 默认不支持 CMYK 模式的 JPEG,透明 PNG 转 RGB 时背景变黑。解决:打开时统一convert('RGB'),对 CMYK 先转 RGB。
img = Image.open(io.BytesIO(img_bytes)) if img.mode != 'RGB': img = img.convert('RGB')透明 PNG 转 RGB 后透明区域会变黑,如果业务在意,可以先铺白底:
background = Image.new('RGB', img.size, (255, 255, 255)) background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None) img = background5.5 并发下 Flask 单 worker 排队
现象:并发 10 个请求,后面的等前面推理完才处理,总耗时线性增长。原因:Flask 默认单线程,gunicorn 单 worker 也是串行。解决:gunicorn 加--threads 4,或者多 worker。但多 worker 显存翻倍,要权衡。
gunicorn -w 1 --threads 4 -b 0.0.0.0:5000 --timeout 120 app:app--threads 4表示每个 worker 起 4 个线程,PyTorch 推理时 GIL 会释放,所以多线程能提吞吐。实测 YOLOv5s 在 4 线程下 QPS 从 8 提到 20 左右。
6. 进阶:把检测结果做成可检索的结构化数据
6.1 从「画框」到「可查询」:结果二次加工
检测系统只画框,价值有限。真正有用的是把框变成结构化数据,比如「今天有多少人没戴安全帽」「哪个区域车最多」。我的做法是在 SpringBoot 落库前加一层解析:把boxes按类别聚合,算出每类的数量和平均置信度,存到detect_summary表。
public void saveSummary(Long taskId, List<List<Double>> boxes, List<String> names) { Map<String, Integer> countMap = new HashMap<>(); for (List<Double> box : boxes) { int clsId = box.get(5).intValue(); String name = names.get(clsId); countMap.merge(name, 1, Integer::sum); } // 存 countMap 到 summary 表 }这样前端可以做一个「统计面板」,按时间范围查各类目标数量,不用每次重新推理。
6.2 批量推理:一次传多图,减少 HTTP 往返
单图推理 HTTP 开销占比不小,尤其是内网。Flask 可以支持批量:请求体传images数组,Flask 循环推理,返回结果数组。SpringBoot 侧用CompletableFuture并发调多个单图接口也行,但批量接口更省连接。
@app.route('/predict_batch', methods=['POST']) def predict_batch(): data = request.get_json() images = data['images'] # base64 数组 results = [] for b64 in images: img = Image.open(io.BytesIO(base64.b64decode(b64))).convert('RGB') with torch.no_grad(): r = model(img, size=640) results.append(r.xyxy[0].cpu().numpy().tolist()) return jsonify({'results': results})批量大小别超过 8,否则单次请求时间太长,前端体验差。
6.3 模型热切换:不重启 Flask 换权重
业务上经常要换模型(比如从 yolov5s 换到自训练的权重)。重启 Flask 会中断服务。我的做法是加一个/reload接口,接收新权重路径,重新加载模型到全局变量。
@app.route('/reload', methods=['POST']) def reload_model(): global model new_path = request.get_json()['path'] model = torch.hub.load('ultralytics/yolov5', 'custom', path=new_path) model.to(DEVICE).eval() return jsonify({'status': 'ok'})这个接口要加鉴权,别暴露公网。切换时会有短暂的空窗,可以用双缓冲:新模型加载到临时变量,加载成功再原子替换全局引用。
6.4 一个我常被问到的参数:conf 和 iou 到底怎么调
最后说个高频问题。conf默认 0.25,iou默认 0.45。如果你的场景漏检多,把conf降到 0.1 到 0.2;如果误检多,升到 0.4 到 0.5。iou在密集小目标场景(比如人群)要降到 0.3 到 0.4,否则重叠框会被 NMS 误删;在稀疏场景可以升到 0.5 到 0.6。调参没有银弹,我一般用一批验证图跑网格搜索,看 mAP 和 FPS 的平衡点。血泪经验:别在测试集上调参,调完看着好,上线就翻车。
这套三段式架构我前后搭过三次,每次都有新坑,但核心逻辑没变:让专业的服务做专业的事。Flask 只管推理,SpringBoot 只管业务,Vue 只管展示。你要是正准备动手,先把 Flask 的/predict跑通,用 curl 测通,再接 SpringBoot,最后接 Vue。顺序反了,排查问题会让你怀疑人生。希望帮到你。
本文还有配套的精品资源,点击获取