
简介面向计算机视觉、深度学习方向的学习者与毕业设计开发者这套工程给出基于SpringBoot、Flask、Vue与YOLOv5的前后端分离式目标检测系统可用于课程设计、项目复现和二次开发。系统按三端拆分职责Flask端加载YOLOv5权重完成图像推理并封装检测接口SpringBoot端承接业务请求、串联检测结果Vue端实现图像上传、结果展示与页面交互配合yaml配置与说明文档能帮助读者理解跨语言服务协作、模型调用和前后端联调流程。压缩包共60个文件约82.63MB以28个py业务与推理脚本、24个yaml配置、2个md说明文档为主另有pyc缓存和分端zip包目录按flask端、web端、springboot端组织便于按模块查阅。已有425人学习适合作为可运行、可拆解的目标检测全栈范例用于梳理接口边界与工程结构。1. 需求边界与选型为什么把 SpringBoot、Flask、Vue、YOLOv5 拆成四段做目标检测毕设的人常遇到一个尴尬Vue 页面已经能传图SpringBoot 接口也写好了YOLOv5 却还在另一个 Python 进程里三边靠手动复制文件对接。这套基于 SpringBoot、Flask、Vue、YOLOv5 的前后端分离式目标检测系统解决的不是“模型能不能跑”而是模型服务怎么被业务系统管起来谁存任务、谁调推理、谁把框画回页面。适合要交课程设计、毕设或要给内部演示做一套目标检测系统的人。把四段拆开是因为 Java 擅长用户、权限、任务表和文件记录Python 擅长加载深度学习权重Vue 负责工作台交互。硬把 YOLOv5 塞进 SpringBoot 里JVM 调 Python 的链路会变成黑匣子全用 Flask 写业务课程设计里的鉴权和任务管理又容易写薄。注意这套方案默认输入是图片视频抽帧和摄像头流可以后续加权重和类别名按你的数据集替换接口契约不变。2. 总体架构设计四层链路与三个进程的职责划分把系统拆成三个进程不是摆样子而是让模型重启不拖垮业务接口。课程设计里最常见的写法是 SpringBoot 上传文件后直接Runtime.exec调 Python 脚本演示时能跑一到连续检测就翻车脚本路径、临时文件、错误输出全混在一起出了问题只能看控制台。前后端分离式目标检测系统要把“业务状态”和“推理状态”分开SpringBoot 管任务生命周期Flask 只管一张图进、一组框出Vue 只管交互和展示。2.1 四层职责与进程边界我一般把系统划成四层展示层、业务层、推理层、数据层。展示层是 Vue 工作台负责上传、轮询、画框和历史回放业务层是 SpringBoot负责登录鉴权、文件校验、任务表、调用 Flask、保存结果推理层是 Flask YOLOv5负责加载权重、预处理、推理、后处理数据层是 MySQL 加本地文件目录任务元数据进表原图和结果 JSON 按任务号归档。模块主要职责输入输出失败时怎么办Vue 工作台上传图片、展示进度、画检测框、查历史用户选择的图片检测结果视图提示任务失败保留原图预览SpringBoot 业务层鉴权、文件校验、建任务、调 Flask、落库multipart 文件、conf 参数taskId、任务状态、结果 JSON把任务置为 FAILED写 error_msgFlask 推理层加载 YOLOv5、推理、序列化检测框图片字节流、conf、ioudetections、原图宽高、耗时返回 4xx/5xx 和错误信息数据层存任务、文件路径、结果、时间业务层写入历史查询文件缺失时任务标记异常这张表看着简单真正落地时边界要卡死Flask 不做用户登录不查数据库不关心前端是谁SpringBoot 不加载best.pt不碰 torch不解析模型输出以外的业务规则Vue 不直接请求 Flask 的 5001 端口。这样模型换权重、换设备、换 Python 依赖只动推理层页面改布局只动展示层。黑匣子被拆成三段每段都有日志可查。2.2 请求链路与任务状态机一次检测的链路通常是Vue 把图片传到POST /api/detectSpringBoot 校验后缀和大小把文件存到uploads/日期/任务号.后缀往detect_task插一条 PENDING业务层用线程池把任务提交给 Flask 的POST /predictFlask 加载模型并推理返回检测框 JSONSpringBoot 把状态改成 SUCCESS把结果写进result_jsonVue 每隔 1.5 秒查一次GET /api/tasks/{taskId}拿到 SUCCESS 后画框。任务状态只留四个PENDING、RUNNING、SUCCESS、FAILED。不要加“处理中 30%”这种假进度模型推理没有真实百分比前端显示“检测中”加轮询次数就够了。失败必须写error_msg否则答辩时老师问“这张为什么没框”你只能重新跑一遍。{ taskId: a3f1c9e2b7d84f6c, status: SUCCESS, imageWidth: 1280, imageHeight: 720, detections: [ { label: person, score: 0.9123, x1: 214.5, y1: 96.2, x2: 438.8, y2: 601.4 } ], costMs: 186 }这段 JSON 是前后端的契约坐标统一用原图像素不用百分比也不在前端二次换算。imageWidth和imageHeight必须和原图一致前端 canvas 按这个宽高设置CSS 只负责缩放显示。costMs是 Flask 推理耗时验收时看它判断是模型慢还是网络慢。label用类别名而不是类别 id前端不用再维护一份映射表如果要做多语言再在业务层转换。2.3 部署拓扑与端口约定课程设计阶段可以三端同机跑Vue 开发服务器 5173SpringBoot 8080Flask 5001MySQL 3306。正式演示时 Vue 执行构建静态文件交给静态服务器SpringBoot 只暴露/apiFlask 只监听本机回环地址由 SpringBoot 调用不对浏览器开放。这样文件校验、权限、任务记录都在一个入口前端也不会因为跨域配置漏掉 5001 而翻车。组件开发端口启动方式谁访问它Vue5173npm run dev浏览器SpringBoot8080IDE 或java -jar浏览器、FlaskFlask5001python app.py仅 SpringBootMySQL3306本机服务SpringBoot提示Flask 的host写127.0.0.1不要图省事写0.0.0.0再让浏览器直连。所有检测请求走 SpringBoot 的/api/detect模型端口不暴露后面加鉴权、限流、文件白名单都只改一处。3. 前后端业务闭环SpringBoot 接口与 Vue 工作台的落地这一章把业务层和展示层接起来。很多方案写到“前端上传、后端调用模型”就停了真到联调时卡在数据库字段、文件路径、轮询清理上。我的做法是先定表再定接口最后写 Vue 页面表结构定错后面接口和页面都要返工。3.1 任务表与文件记录任务表只存检测任务需要的字段不要把图片二进制塞进数据库。原图放uploads表里存相对路径结果 JSON 放result_json课程设计查询方便结果特别大时再改成存文件路径。CREATE TABLE detect_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_no VARCHAR(64) NOT NULL UNIQUE COMMENT 任务号前端轮询用, original_name VARCHAR(255) NOT NULL COMMENT 原始文件名, stored_path VARCHAR(500) NOT NULL COMMENT 相对 uploads 的路径, status VARCHAR(16) NOT NULL DEFAULT PENDING COMMENT PENDING/RUNNING/SUCCESS/FAILED, model_name VARCHAR(128) DEFAULT NULL COMMENT 权重文件名便于对比实验, conf_thres DECIMAL(4,3) DEFAULT 0.250 COMMENT 本次检测的置信度阈值, result_json LONGTEXT DEFAULT NULL COMMENT 检测框 JSON, error_msg VARCHAR(1000) DEFAULT NULL COMMENT 失败原因, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, finished_at DATETIME DEFAULT NULL, KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;task_no用 UUID 去横线前端拿到后只认这个号不暴露自增 id。stored_path存相对路径换机器时只改app.upload-dir历史数据还能用。model_name和conf_thres看起来多余答辩时很有用同一张图换yolov5s和自训练权重能直接对比结果老师问阈值多少表里有记录不用翻代码。finished_at用来算耗时后面验收表会用到。3.2 上传与任务下发接口上传接口只做三件事校验、存文件、建任务并提交异步推理。不要在 Controller 里等 Flask 返回首次加载模型可能十几秒同步接口很容易超时。RestController RequestMapping(/api) public class DetectController { Value(${app.upload-dir}) private String uploadDir; private final DetectTaskService taskService; public DetectController(DetectTaskService taskService) { this.taskService taskService; } PostMapping(/detect) public ResultMapString, Object detect( RequestParam(file) MultipartFile file, RequestParam(defaultValue 0.25) float conf) throws IOException { String original file.getOriginalFilename(); String ext original null ? : original.substring(original.lastIndexOf(.) 1).toLowerCase(); if (!Set.of(jpg, jpeg, png, bmp).contains(ext)) { return Result.fail(只支持 jpg/jpeg/png/bmp); } if (file.getSize() 10 * 1024 * 1024) { return Result.fail(图片不能超过 10MB); } String date LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); Path dir Paths.get(uploadDir, date).toAbsolutePath().normalize(); Files.createDirectories(dir); String stored date / UUID.randomUUID().toString().replace(-, ) . ext; file.transferTo(dir.resolve(Path.of(stored).getFileName()).toFile()); Long taskId taskService.createTask(original, stored, conf); taskService.submit(taskId, stored, conf); return Result.ok(Map.of(taskId, taskId)); } }这段代码的关键不在语法在顺序先校验后缀和大小再落盘再建任务最后提交线程池。submit方法加Async内部用RestTemplate调 Flask调用失败要catch住把任务置为 FAILED 并写error_msg不能只打日志。conf从请求参数进默认 0.25前端调参时不用改后端。文件落盘用绝对路径transferTo对相对路径在不同容器里行为不一致这个坑我踩过。3.3 Vue 工作台的上传、轮询与画框Vue 页面不用做太花三个区域够用上传区、结果画布、历史列表。上传成功后拿taskId轮询组件卸载时清定时器画框用 canvas宽高设成原图尺寸CSS 控制显示宽度。script setup import { ref, onBeforeUnmount } from vue import axios from axios const file ref(null) const preview ref() const task ref(null) const loading ref(false) let timer null function onFile(e) { file.value e.target.files[0] preview.value URL.createObjectURL(file.value) } async function startDetect() { if (!file.value) return loading.value true const fd new FormData() fd.append(file, file.value) const { data } await axios.post(/api/detect, fd) poll(data.data.taskId) } function poll(taskId) { timer setInterval(async () { const { data } await axios.get(/api/tasks/${taskId}) task.value data.data if ([SUCCESS, FAILED].includes(task.value.status)) { clearInterval(timer) loading.value false } }, 1500) } function drawBoxes(canvas, img, dets) { const ctx canvas.getContext(2d) canvas.width img.naturalWidth canvas.height img.naturalHeight ctx.drawImage(img, 0, 0) ctx.lineWidth 3 ctx.strokeStyle #ff3b30 ctx.fillStyle #ff3b30 dets.forEach(d { ctx.strokeRect(d.x1, d.y1, d.x2 - d.x1, d.y2 - d.y1) ctx.fillText(${d.label} ${d.score.toFixed(2)}, d.x1 4, d.y1 - 6) }) } onBeforeUnmount(() clearInterval(timer)) /script轮询间隔 1.5 秒是折中太快给后端压力太慢演示时显得卡。onBeforeUnmount必须清定时器否则切换页面后请求还在发控制台一堆重复请求老师一眼就能看出来。画框函数只认原图坐标canvas.width和naturalWidth对齐CSS 里写width: 100%框不会偏。如果图片有 EXIF 旋转Flask 侧统一转正前端不要自己转否则框和原图对不上。3.4 联调参数与跨域配置联调阶段最耗时的不是模型是 10MB 限制、跨域预检和读超时。参数集中写在application.yml改的时候只改一处。server: port: 8080 spring: servlet: multipart: max-file-size: 10MB max-request-size: 12MB app: upload-dir: ./uploads flask: base-url: http://127.0.0.1:5001 connect-timeout: 3000 read-timeout: 60000 task: pool-size: 2read-timeout给 60 秒是留给模型首次加载和 CPU 推理的余量如果每次检测都接近 60 秒说明模型太大或设备太慢改用更小权重或降低imgsz。pool-size不要超过 Flask 能承受的并发课程设计给 2 就够两个请求同时进来不会串结果。max-file-size和前端提示保持一致前端限制 10MB后端也限制 10MB避免前端放行、后端 500。跨域用CorsFilter允许http://localhost:5173方法包含 GET、POST、OPTIONS。不要图省事写allowedOrigins(*)又开allowCredentials(true)浏览器会直接拒绝。Spring Security 如果开了要放行 OPTIONS否则预检请求 403POST 还没进 Controller 就被拦住。4. 模型服务实现Flask 封装 YOLOv5 的推理接口与参数推理层只做一件事接收一张图返回检测框。不要在这里写用户体系也不要在这里查任务表。Flask 服务启动时预加载模型接口里只做预处理、推理、后处理这样 SpringBoot 第一次调用不会卡在权重加载上。4.1 Flask 服务骨架与健康检查服务至少两个接口/health给 SpringBoot 和验收脚本探活/predict做推理。启动时调一次get_model()把权重加载进内存再app.run。import io import time import threading import torch from flask import Flask, request, jsonify from PIL import Image app Flask(__name__) model None model_lock threading.Lock() def get_model(): global model if model is None: # yolov5 源码目录和权重路径按实际工程替换 model torch.hub.load(./yolov5, custom, pathweights/best.pt, sourcelocal) model.conf 0.25 model.iou 0.45 model.max_det 300 model.eval() return model app.get(/health) def health(): return jsonify({ status: ok, device: cuda if torch.cuda.is_available() else cpu }) app.post(/predict) def predict(): if file not in request.files: return jsonify({error: file required}), 400 conf float(request.form.get(conf, 0.25)) iou float(request.form.get(iou, 0.45)) img Image.open(io.BytesIO(request.files[file].read())).convert(RGB) m get_model() t0 time.time() with model_lock: m.conf conf m.iou iou out m(img, size640) cost_ms int((time.time() - t0) * 1000) detections [] for _, row in out.pandas().xyxy[0].iterrows(): detections.append({ label: row[name], score: round(float(row[confidence]), 4), x1: float(row[xmin]), y1: float(row[ymin]), x2: float(row[xmax]), y2: float(row[ymax]) }) return jsonify({ detections: detections, imageWidth: img.width, imageHeight: img.height, costMs: cost_ms }) if __name__ __main__: get_model() # 启动即加载避免首请求超时 app.run(host127.0.0.1, port5001, threadedTrue)host127.0.0.1表示只给本机业务层调用浏览器直连不到 5001。threadedTrue让 Flask 能接并发请求但 YOLOv5 的model对象会在推理时读conf、iou多个请求同时改会串参数所以加model_lock把推理段锁住。演示环境这样最省心真要提吞吐再起多个 Flask 实例或做任务队列不要在一个进程里开多 worker 重复加载权重。out.pandas().xyxy[0]依赖 pandas环境里要装不想装 pandas 可以遍历out.xyxy[0]自己取列但课程设计里 pandas 写法最直观。4.2 推理参数怎么调YOLOv5 的参数不多但每个都影响演示效果。我一般先固定一组能跑通的参数再根据漏检和误检微调。参数常用值作用调整建议conf0.25置信度阈值漏检多降到 0.15~0.20误检多升到 0.35~0.45iou0.45NMS 重叠阈值框重叠严重升到 0.50~0.60同类挨得近可降到 0.35imgsz640推理输入尺寸小目标多升到 960但显存和耗时上升max_det300单图最多保留框数密集场景调大普通演示 300 够用devicecpu / 0推理设备有 GPU 用 0没有就 cpu别在代码里硬编码conf和iou从 Flask 表单参数进SpringBoot 透传前端选择的值这样同一张图可以现场演示不同阈值的效果。imgsz不建议每次请求改改一次模型会重新调整输入耗时波动大要在启动参数里定。device用torch.cuda.is_available()判断有卡走 GPU没卡走 CPU答辩换机器不用改代码。4.3 并发、显存与超时控制课程设计常见场景是老师连续点两次检测或者前端轮询没停又点了一次。SpringBoot 线程池给 2Flask 侧加锁串行推理结果不会串但第二个请求要等第一个跑完。如果设备是 CPU一张图几百毫秒到几秒等待还能接受如果模型很大等待会超过前端轮询次数看起来像“卡死”。我的处理方式是前端在loading为 true 时禁用按钮SpringBoot 线程池队列设小一点满了直接返回“当前检测任务较多”Flask 的read-timeout给足但不要让前端无限等。显存方面imgsz640的单图推理一般比训练省很多真正容易爆的是批量推理。课程设计先不做 batch一次一张验收时用costMs和nvidia-smi看占用再决定要不要加批处理。5. 常见问题与排查四个让检测系统联调翻车的细节这一章按“现象 → 原因 → 解决”写都是前后端分离式目标检测系统里最容易踩的坑。答辩前把这几条过一遍能省很多重新打包的时间。5.1 检测框整体偏移或缩放后错位现象图片缩略图上看框还行点开大图框偏左上或者前端换了显示宽度框跟着偏。原因通常有三个Flask 返回的是 letterbox 后的坐标没有还原到原图前端 canvas 用了 CSS 显示尺寸而不是原图尺寸图片 EXIF 带旋转浏览器显示转正了模型读到的还是旋转前。解决Flask 侧统一用原图坐标返回YOLOv5 的out.pandas().xyxy[0]本身就是原图坐标别自己再乘缩放比前端canvas.width img.naturalWidthCSS 只控制width: 100%上传后在后端做一次 EXIF 转正或者前端用createImageBitmap时指定方向。排查时先看返回 JSON 的imageWidth是否等于原图宽不等就是后端读图环节出了问题相等再检查前端 canvas 宽高。这个坑很玄学但按这两个数一对很快能定位。5.2 第一次检测超时第二次正常现象启动服务后第一张图等很久SpringBoot 报Read timed out再点一次几秒就出结果。原因模型懒加载放在/predict里第一次请求才加载权重torch.hub.load还要找本地 yolov5 目录路径不对时会在请求线程里卡住。解决Flask 启动时先get_model()加载完再app.runSpringBoot 的read-timeout给 60 秒验收脚本先调/health返回 ok 再上传图片。如果/health很快但/predict第一次慢说明模型没预加载如果/health就慢说明启动脚本或权重路径有问题。我一般把python app.py的输出重定向到日志文件启动后先curl /health确认设备是 cpu 还是 cuda再打开前端。5.3 跨域预检 403POST 根本没到接口现象浏览器控制台报 CORS 错误Network 里 OPTIONS 请求 403POST 显示 failed。原因Spring Security 默认拦截 OPTIONS本文还有配套的精品资源点击获取