設計與聯(lián)調)
簡介面向計算機視覺、深度學習方向的學習者與畢業(yè)設計開發(fā)者這套工程給出基于SpringBoot、Flask、Vue與YOLOv5的前后端分離式目標檢測系統(tǒng)可用于課程設計、項目復現(xiàn)和二次開發(fā)。系統(tǒng)按三端拆分職責Flask端加載YOLOv5權重完成圖像推理并封裝檢測接口SpringBoot端承接業(yè)務請求、串聯(lián)檢測結果Vue端實現(xiàn)圖像上傳、結果展示與頁面交互配合yaml配置與說明文檔能幫助讀者理解跨語言服務協(xié)作、模型調用和前后端聯(lián)調流程。壓縮包共60個文件約82.63MB以28個py業(yè)務與推理腳本、24個yaml配置、2個md說明文檔為主另有pyc緩存和分端zip包目錄按flask端、web端、springboot端組織便于按模塊查閱。已有425人學習適合作為可運行、可拆解的目標檢測全棧范例用于梳理接口邊界與工程結構。1. 需求邊界與選型為什么把 SpringBoot、Flask、Vue、YOLOv5 拆成四段做目標檢測畢設的人常遇到一個尷尬Vue 頁面已經(jīng)能傳圖SpringBoot 接口也寫好了YOLOv5 卻還在另一個 Python 進程里三邊靠手動復制文件對接。這套基于 SpringBoot、Flask、Vue、YOLOv5 的前后端分離式目標檢測系統(tǒng)解決的不是“模型能不能跑”而是模型服務怎么被業(yè)務系統(tǒng)管起來誰存任務、誰調推理、誰把框畫回頁面。適合要交課程設計、畢設或要給內部演示做一套目標檢測系統(tǒng)的人。把四段拆開是因為 Java 擅長用戶、權限、任務表和文件記錄Python 擅長加載深度學習權重Vue 負責工作臺交互。硬把 YOLOv5 塞進 SpringBoot 里JVM 調 Python 的鏈路會變成黑匣子全用 Flask 寫業(yè)務課程設計里的鑒權和任務管理又容易寫薄。注意這套方案默認輸入是圖片視頻抽幀和攝像頭流可以后續(xù)加權重和類別名按你的數(shù)據(jù)集替換接口契約不變。2. 總體架構設計四層鏈路與三個進程的職責劃分把系統(tǒng)拆成三個進程不是擺樣子而是讓模型重啟不拖垮業(yè)務接口。課程設計里最常見的寫法是 SpringBoot 上傳文件后直接Runtime.exec調 Python 腳本演示時能跑一到連續(xù)檢測就翻車腳本路徑、臨時文件、錯誤輸出全混在一起出了問題只能看控制臺。前后端分離式目標檢測系統(tǒng)要把“業(yè)務狀態(tài)”和“推理狀態(tài)”分開SpringBoot 管任務生命周期Flask 只管一張圖進、一組框出Vue 只管交互和展示。2.1 四層職責與進程邊界我一般把系統(tǒng)劃成四層展示層、業(yè)務層、推理層、數(shù)據(jù)層。展示層是 Vue 工作臺負責上傳、輪詢、畫框和歷史回放業(yè)務層是 SpringBoot負責登錄鑒權、文件校驗、任務表、調用 Flask、保存結果推理層是 Flask YOLOv5負責加載權重、預處理、推理、后處理數(shù)據(jù)層是 MySQL 加本地文件目錄任務元數(shù)據(jù)進表原圖和結果 JSON 按任務號歸檔。模塊主要職責輸入輸出失敗時怎么辦Vue 工作臺上傳圖片、展示進度、畫檢測框、查歷史用戶選擇的圖片檢測結果視圖提示任務失敗保留原圖預覽SpringBoot 業(yè)務層鑒權、文件校驗、建任務、調 Flask、落庫multipart 文件、conf 參數(shù)taskId、任務狀態(tài)、結果 JSON把任務置為 FAILED寫 error_msgFlask 推理層加載 YOLOv5、推理、序列化檢測框圖片字節(jié)流、conf、ioudetections、原圖寬高、耗時返回 4xx/5xx 和錯誤信息數(shù)據(jù)層存任務、文件路徑、結果、時間業(yè)務層寫入歷史查詢文件缺失時任務標記異常這張表看著簡單真正落地時邊界要卡死Flask 不做用戶登錄不查數(shù)據(jù)庫不關心前端是誰SpringBoot 不加載best.pt不碰 torch不解析模型輸出以外的業(yè)務規(guī)則Vue 不直接請求 Flask 的 5001 端口。這樣模型換權重、換設備、換 Python 依賴只動推理層頁面改布局只動展示層。黑匣子被拆成三段每段都有日志可查。2.2 請求鏈路與任務狀態(tài)機一次檢測的鏈路通常是Vue 把圖片傳到POST /api/detectSpringBoot 校驗后綴和大小把文件存到uploads/日期/任務號.后綴往detect_task插一條 PENDING業(yè)務層用線程池把任務提交給 Flask 的POST /predictFlask 加載模型并推理返回檢測框 JSONSpringBoot 把狀態(tài)改成 SUCCESS把結果寫進result_jsonVue 每隔 1.5 秒查一次GET /api/tasks/{taskId}拿到 SUCCESS 后畫框。任務狀態(tài)只留四個PENDING、RUNNING、SUCCESS、FAILED。不要加“處理中 30%”這種假進度模型推理沒有真實百分比前端顯示“檢測中”加輪詢次數(shù)就夠了。失敗必須寫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 是前后端的契約坐標統(tǒng)一用原圖像素不用百分比也不在前端二次換算。imageWidth和imageHeight必須和原圖一致前端 canvas 按這個寬高設置CSS 只負責縮放顯示。costMs是 Flask 推理耗時驗收時看它判斷是模型慢還是網(wǎng)絡慢。label用類別名而不是類別 id前端不用再維護一份映射表如果要做多語言再在業(yè)務層轉換。2.3 部署拓撲與端口約定課程設計階段可以三端同機跑Vue 開發(fā)服務器 5173SpringBoot 8080Flask 5001MySQL 3306。正式演示時 Vue 執(zhí)行構建靜態(tài)文件交給靜態(tài)服務器SpringBoot 只暴露/apiFlask 只監(jiān)聽本機回環(huán)地址由 SpringBoot 調用不對瀏覽器開放。這樣文件校驗、權限、任務記錄都在一個入口前端也不會因為跨域配置漏掉 5001 而翻車。組件開發(fā)端口啟動方式誰訪問它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. 前后端業(yè)務閉環(huán)SpringBoot 接口與 Vue 工作臺的落地這一章把業(yè)務層和展示層接起來。很多方案寫到“前端上傳、后端調用模型”就停了真到聯(lián)調時卡在數(shù)據(jù)庫字段、文件路徑、輪詢清理上。我的做法是先定表再定接口最后寫 Vue 頁面表結構定錯后面接口和頁面都要返工。3.1 任務表與文件記錄任務表只存檢測任務需要的字段不要把圖片二進制塞進數(shù)據(jù)庫。原圖放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歷史數(shù)據(jù)還能用。model_name和conf_thres看起來多余答辯時很有用同一張圖換yolov5s和自訓練權重能直接對比結果老師問閾值多少表里有記錄不用翻代碼。finished_at用來算耗時后面驗收表會用到。3.2 上傳與任務下發(fā)接口上傳接口只做三件事校驗、存文件、建任務并提交異步推理。不要在 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從請求參數(shù)進默認 0.25前端調參時不用改后端。文件落盤用絕對路徑transferTo對相對路徑在不同容器里行為不一致這個坑我踩過。3.3 Vue 工作臺的上傳、輪詢與畫框Vue 頁面不用做太花三個區(qū)域夠用上傳區(qū)、結果畫布、歷史列表。上傳成功后拿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必須清定時器否則切換頁面后請求還在發(fā)控制臺一堆重復請求老師一眼就能看出來。畫框函數(shù)只認原圖坐標canvas.width和naturalWidth對齊CSS 里寫width: 100%框不會偏。如果圖片有 EXIF 旋轉Flask 側統(tǒng)一轉正前端不要自己轉否則框和原圖對不上。3.4 聯(lián)調參數(shù)與跨域配置聯(lián)調階段最耗時的不是模型是 10MB 限制、跨域預檢和讀超時。參數(shù)集中寫在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 能承受的并發(fā)課程設計給 2 就夠兩個請求同時進來不會串結果。max-file-size和前端提示保持一致前端限制 10MB后端也限制 10MB避免前端放行、后端 500??缬蛴肅orsFilter允許http://localhost:5173方法包含 GET、POST、OPTIONS。不要圖省事寫allowedOrigins(*)又開allowCredentials(true)瀏覽器會直接拒絕。Spring Security 如果開了要放行 OPTIONS否則預檢請求 403POST 還沒進 Controller 就被攔住。4. 模型服務實現(xiàn)Flask 封裝 YOLOv5 的推理接口與參數(shù)推理層只做一件事接收一張圖返回檢測框。不要在這里寫用戶體系也不要在這里查任務表。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表示只給本機業(yè)務層調用瀏覽器直連不到 5001。threadedTrue讓 Flask 能接并發(fā)請求但 YOLOv5 的model對象會在推理時讀conf、iou多個請求同時改會串參數(shù)所以加model_lock把推理段鎖住。演示環(huán)境這樣最省心真要提吞吐再起多個 Flask 實例或做任務隊列不要在一個進程里開多 worker 重復加載權重。out.pandas().xyxy[0]依賴 pandas環(huán)境里要裝不想裝 pandas 可以遍歷out.xyxy[0]自己取列但課程設計里 pandas 寫法最直觀。4.2 推理參數(shù)怎么調YOLOv5 的參數(shù)不多但每個都影響演示效果。我一般先固定一組能跑通的參數(shù)再根據(jù)漏檢和誤檢微調。參數(shù)常用值作用調整建議conf0.25置信度閾值漏檢多降到 0.15~0.20誤檢多升到 0.35~0.45iou0.45NMS 重疊閾值框重疊嚴重升到 0.50~0.60同類挨得近可降到 0.35imgsz640推理輸入尺寸小目標多升到 960但顯存和耗時上升max_det300單圖最多保留框數(shù)密集場景調大普通演示 300 夠用devicecpu / 0推理設備有 GPU 用 0沒有就 cpu別在代碼里硬編碼conf和iou從 Flask 表單參數(shù)進SpringBoot 透傳前端選擇的值這樣同一張圖可以現(xiàn)場演示不同閾值的效果。imgsz不建議每次請求改改一次模型會重新調整輸入耗時波動大要在啟動參數(shù)里定。device用torch.cuda.is_available()判斷有卡走 GPU沒卡走 CPU答辯換機器不用改代碼。4.3 并發(fā)、顯存與超時控制課程設計常見場景是老師連續(xù)點兩次檢測或者前端輪詢沒停又點了一次。SpringBoot 線程池給 2Flask 側加鎖串行推理結果不會串但第二個請求要等第一個跑完。如果設備是 CPU一張圖幾百毫秒到幾秒等待還能接受如果模型很大等待會超過前端輪詢次數(shù)看起來像“卡死”。我的處理方式是前端在loading為 true 時禁用按鈕SpringBoot 線程池隊列設小一點滿了直接返回“當前檢測任務較多”Flask 的read-timeout給足但不要讓前端無限等。顯存方面imgsz640的單圖推理一般比訓練省很多真正容易爆的是批量推理。課程設計先不做 batch一次一張驗收時用costMs和nvidia-smi看占用再決定要不要加批處理。5. 常見問題與排查四個讓檢測系統(tǒng)聯(lián)調翻車的細節(jié)這一章按“現(xiàn)象 → 原因 → 解決”寫都是前后端分離式目標檢測系統(tǒng)里最容易踩的坑。答辯前把這幾條過一遍能省很多重新打包的時間。5.1 檢測框整體偏移或縮放后錯位現(xiàn)象圖片縮略圖上看框還行點開大圖框偏左上或者前端換了顯示寬度框跟著偏。原因通常有三個Flask 返回的是 letterbox 后的坐標沒有還原到原圖前端 canvas 用了 CSS 顯示尺寸而不是原圖尺寸圖片 EXIF 帶旋轉瀏覽器顯示轉正了模型讀到的還是旋轉前。解決Flask 側統(tǒng)一用原圖坐標返回YOLOv5 的out.pandas().xyxy[0]本身就是原圖坐標別自己再乘縮放比前端canvas.width img.naturalWidthCSS 只控制width: 100%上傳后在后端做一次 EXIF 轉正或者前端用createImageBitmap時指定方向。排查時先看返回 JSON 的imageWidth是否等于原圖寬不等就是后端讀圖環(huán)節(jié)出了問題相等再檢查前端 canvas 寬高。這個坑很玄學但按這兩個數(shù)一對很快能定位。5.2 第一次檢測超時第二次正?,F(xiàn)象啟動服務后第一張圖等很久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 根本沒到接口現(xiàn)象瀏覽器控制臺報 CORS 錯誤Network 里 OPTIONS 請求 403POST 顯示 failed。原因Spring Security 默認攔截 OPTIONS本文還有配套的精品資源點擊獲取