設(shè)計與大模型智能分析)
做密集行人檢測最讓人頭疼的時刻不是模型精度不夠而是模型明明能檢測出目標(biāo)一到真實場景商場扶梯口、地鐵站臺、景區(qū)檢票口就各種翻車——人擠人的時候互相遮擋遠處的人小到只有十幾個像素近處的人又大到超出邊界框傳統(tǒng)的NMS后處理在人群密集區(qū)域還會把本應(yīng)保留的檢測框誤刪。這個項目就是圍繞這個問題展開的用YOLOv8、YOLOv10、YOLOv11、YOLOv12四個版本的模型作為檢測引擎SpringBoot作為后端服務(wù)框架再配合千問和DeepSeek兩個大模型做場景級智能分析最后用前后端分離的Web界面把整個流程串起來形成一個從圖片/視頻上傳、目標(biāo)檢測、智能分析到結(jié)果可視化的完整閉環(huán)。這套系統(tǒng)做出來之后既能作為獨立的行人檢測服務(wù)供其他業(yè)務(wù)調(diào)用也能直接在瀏覽器里看檢測效果和AI分析結(jié)論。無論你是算法工程師想了解YOLO系列在密集場景下的選型和部署還是Java后端想學(xué)習(xí)SpringBoot怎么集成檢測和大模型能力或者是在做畢設(shè)、搞工業(yè)級Demo這篇內(nèi)容都值得花幾分鐘看完。我會把架構(gòu)設(shè)計、模型選型對比、關(guān)鍵代碼實現(xiàn)、踩過的坑全部攤開來講。1. 密集行人檢測為什么難——項目定位與核心問題1.1 密集場景的三座大山遮擋、小目標(biāo)、尺度不均行人檢測在公開數(shù)據(jù)集上刷點已經(jīng)不難難的是在真實密集場景里穩(wěn)定工作。我總結(jié)下來密集人群場景主要卡在三個問題上。第一是遮擋。人群一密集人與人之間的IoU極高目標(biāo)之間互相覆蓋檢測器只能看到人的上半身甚至只有頭部。這時候如果模型本身沒有充足的上下文感知能力很容易把兩個人當(dāng)成一個人或者干脆漏檢。第二是小目標(biāo)。監(jiān)控攝像頭的角度決定了遠處的人很小在1080P畫面里可能只有15×30像素。常規(guī)檢測頭在小目標(biāo)上特征響應(yīng)弱加上下采樣倍數(shù)高小目標(biāo)的特征圖空間分辨率嚴(yán)重不足。第三是尺度分布極度不均。同一個畫面里近處的人占據(jù)大半個邊界框遠處的人只有幾個像素模型需要同時兼顧兩種極端尺度。大多數(shù)單尺度訓(xùn)練出來的模型在均勻尺度的常規(guī)數(shù)據(jù)集上表現(xiàn)良好但到了這種場景就露餡。1.2 傳統(tǒng)檢測方案在密集場景下的失效模式很多團隊直接拿通用目標(biāo)檢測模型套到密集人群場景結(jié)果在評估時發(fā)現(xiàn)mAP看著還行實際落地卻問題不斷。典型的表現(xiàn)有三類NMS誤殺兩個高度重疊的真實行人被NMS當(dāng)成一個目標(biāo)處理置信度較低的框被直接抑制。這是密集場景最典型的失效模式。定位漂移遮擋導(dǎo)致檢測框的回歸不穩(wěn)定框的位置在頭和身體之間搖擺實際畫出來沒法用。召回率虛高但精確率低模型把所有疑似行人的區(qū)域全框出來結(jié)果畫面里一半的框都是誤檢。這類問題在人群密度不均勻時特別明顯。1.3 為什么這套系統(tǒng)要YOLO SpringBoot 大模型組合檢測模型解決的是人在哪里的問題但客戶和業(yè)務(wù)方真正關(guān)心的往往是人群現(xiàn)在是什么狀態(tài)有沒有安全隱患需不需要預(yù)警。這一層語義分析能力傳統(tǒng)目標(biāo)檢測給不了。所以我把系統(tǒng)拆成三層YOLO負責(zé)感知層快速輸出檢測框、類別和置信度SpringBoot服務(wù)層負責(zé)串聯(lián)所有能力包括文件上傳、推理調(diào)度、結(jié)果持久化千問和DeepSeek負責(zé)語義分析層把檢測結(jié)果轉(zhuǎn)化為對人類友好的結(jié)構(gòu)化結(jié)論。三層之間用HTTP和WebSocket通信前后端徹底分離前端只負責(zé)展示和交互所有計算都收斂到后端服務(wù)。2. 多版本YOLO選型v8、v10、v11、v12到底怎么選2.1 四代YOLO的核心差異與技術(shù)演進先說結(jié)論YOLO系列遠不止是版本號遞增每一代的架構(gòu)改動都會直接影響密集場景下的表現(xiàn)。我結(jié)合源碼和實測把這四個版本的核心差異做了個對比。版本發(fā)布方核心亮點對密集行人場景的影響YOLOv8UltralyticsC2f模塊、Anchor-Free、集成分類/檢測/分割/姿態(tài)生態(tài)最成熟資料最多穩(wěn)定首選YOLOv10清華去除NMS的端到端檢測、雙標(biāo)簽分配大幅緩解NMS在密集場景的誤殺問題YOLOv11UltralyticsC3k2模塊、改進的C2PSA注意力、更強特征提取精度和速度均有提升小目標(biāo)表現(xiàn)更好YOLOv12社區(qū)/學(xué)術(shù)界注意力中心架構(gòu)、Area Attention全局建模更強遮擋場景下的上下文利用更好YOLOv10的端到端特性值得多說一句。它通過One-to-One匹配策略替代了傳統(tǒng)NMS相當(dāng)于從機制上繞過了重疊框被抑制的死結(jié)。我在CrowdHuman密集子集上測試YOLOv10在人群高度重疊區(qū)域的召回率比v8高了約4-6個百分點這個差距在真實監(jiān)控畫面里就是少漏檢好幾個人的差距。YOLOv12引入的注意力機制則更擅長捕捉行人之間的關(guān)系當(dāng)一個人被另一個人遮擋50%以上的時候v8和v10基本只能靠猜測v12卻可以利用周圍行人的上下文信息輔助判斷。當(dāng)然注意力機制也帶來了更高的計算開銷實際部署時要在速度和準(zhǔn)確率之間做權(quán)衡。2.2 密集行人場景下的實測對比我基于同一個數(shù)據(jù)集約1.2萬張標(biāo)注圖像混合了監(jiān)控視角和手持設(shè)備視角分別訓(xùn)練了四個版本的YOLO模型輸入分辨率統(tǒng)一設(shè)置為640×640硬件環(huán)境是單張RTX 3090。版本模型體積推理耗時(GPU)mAP0.5人群密集子集Recall小目標(biāo)APYOLOv8s22.5MB6.2ms0.8310.7420.386YOLOv10s24.1MB5.8ms0.8450.7940.412YOLOv11s26.8MB5.5ms0.8620.8030.434YOLOv12s32.3MB7.9ms0.8710.8110.455只看這個表YOLOv12好像全面勝出但實際部署要考慮硬件差異。我的目標(biāo)運行環(huán)境有一部分是CPU服務(wù)器v8和v10在CPU上的推理速度能跑到300ms左右v12直接飆到600ms以上。所以我的策略是GPU環(huán)境用v12CPU環(huán)境切回v8或者v10后端寫了一個模型路由邏輯根據(jù)當(dāng)前運行環(huán)境自動選擇最優(yōu)模型。2.3 我的選型結(jié)論與切換策略沒有絕對的最優(yōu)模型只有最適合當(dāng)前場景的模型。我最終的落地配置是雙模型策略提示生產(chǎn)環(huán)境別只部署一個模型。我的做法是同一套接口背后維護兩個模型文件一個極致追求精度的YOLOv12用于GPU推理服務(wù)器一個均衡型的YOLOv8s用于CPU兜底。后端感知到GPU資源緊張或推理超時時自動降級。這個策略在流量高峰時特別有用。GPU的推理隊列打滿之后新的檢測請求自動路由到CPU上的輕量模型雖然精度略有下降但至少保證服務(wù)不掛、響應(yīng)不超時。Java后端通過一個簡單的權(quán)重配置和模型ID參數(shù)就能實現(xiàn)動態(tài)切換不需要重啟服務(wù)。# 模型加載層抽象后端通過模型ID指定需要加載的版本 from ultralytics import YOLO MODEL_REGISTRY { v8: weights/yolov8s_crowd.pt, v10: weights/yolov10s_crowd.pt, v11: weights/yolov11s_crowd.pt, v12: weights/yolov12s_crowd.pt, } def load_any_model(version: str) - YOLO: if version not in MODEL_REGISTRY: raise ValueError(fUnsupported YOLO version: {version}) return YOLO(MODEL_REGISTRY[version])3. 基于SpringBoot的后端服務(wù)體系架構(gòu)設(shè)計3.1 前后端分離架構(gòu)下的后端模塊拆解SpringBoot在這個項目里不是簡單起個HTTP接口就完事。整套后端按照職責(zé)拆成了六個模塊每個模塊之間通過接口通信互不干擾gateway-controller統(tǒng)一接收前端請求負責(zé)參數(shù)校驗、鑒權(quán)和路由分發(fā)。detection-service管理YOLO推理服務(wù)封裝了HTTP調(diào)用Python推理服務(wù)的邏輯。analysis-service對接千問和DeepSeek大模型API負責(zé)提示詞拼接、接口調(diào)用、響應(yīng)解析。>RestController RequestMapping(/api/detection) public class DetectionController { private final DetectionService detectionService; private final AnalysisService analysisService; PostMapping(/image) public ApiResultDetectionResponse detectImage(RequestParam(file) MultipartFile file, RequestParam(value modelVersion, defaultValue v11) String modelVersion, RequestParam(value enableAnalysis, defaultValue false) boolean enableAnalysis) { // 1. 文件校驗與存儲 String fileUrl fileService.storeFile(file); // 2. 調(diào)用YOLO推理服務(wù)獲取檢測框 DetectionRawResult rawResult detectionService.detectImage(fileUrl, modelVersion); // 3. 可選調(diào)用大模型進行智能分析 if (enableAnalysis) { AnalysisResult analysis analysisService.analyzeDetection(rawResult, fileUrl); return ApiResult.success(DetectionResponse.of(rawResult, analysis)); } return ApiResult.success(DetectionResponse.of(rawResult, null)); } }這里有一個值得注意的細節(jié)大模型分析我沒法和檢測做成同步的因為DeepSeek和千問的API響應(yīng)時間波動很大從幾百毫秒到十幾秒都有可能。同步調(diào)用會讓前端一直等待體驗很差。所以我把檢測和分析拆成兩個階段檢測走同步接口分析走異步任務(wù)WebSocket推送。數(shù)據(jù)流是這樣的前端上傳圖片 → SpringBoot存儲文件 → 調(diào)用Python YOLO服務(wù)檢測 → 檢測結(jié)果寫入數(shù)據(jù)庫 → 如果開啟了智能分析則把檢測框數(shù)據(jù)拼接成結(jié)構(gòu)化文本發(fā)送給大模型 → 拿到分析結(jié)果后通過WebSocket推送給前端。這個流程下用戶先看到檢測框幾秒后再看到AI分析文字體驗很自然。3.3 與Python推理服務(wù)的通信方案選擇YOLO模型用Python訓(xùn)練和推理效率最高但SpringBoot的主體是Java。兩者通信我對比過三種方案方案優(yōu)點缺點我的結(jié)論HTTP REST調(diào)用實現(xiàn)簡單、調(diào)試方便、語言無關(guān)序列化開銷、單次請求延遲略高最推薦適合大部分場景gRPC性能高、支持流式傳輸需要生成stub、調(diào)試相對麻煩高并發(fā)專用場景推薦Java直接加載ONNX模型省掉中間網(wǎng)絡(luò)開銷部署復(fù)雜、算子支持不全不推薦維護成本高我最終選了HTTP REST方案。Python端用FastAPI封裝YOLO推理接口SpringBoot通過RestTemplate做調(diào)用。實測下來單次檢測請求從Java到Python再返回網(wǎng)絡(luò)和序列化開銷大約15-20ms對檢測這個場景完全可接受。FastAPI的異步特性也能輕松扛住并發(fā)請求。# Python端FastAPI推理服務(wù) from fastapi import FastAPI, UploadFile import numpy as np from ultralytics import YOLO app FastAPI() model_pool { v8: YOLO(weights/yolov8s_crowd.pt), v10: YOLO(weights/yolov10s_crowd.pt), v11: YOLO(weights/yolov11s_crowd.pt), v12: YOLO(weights/yolov12s_crowd.pt), } app.post(/detect) async def detect(file: UploadFile, model_version: str v11, conf_thres: float 0.25): img_bytes await file.read() results model_pool[model_version].predict( sourceimg_bytes, confconf_thres, verboseFalse ) boxes results[0].boxes return { boxes: boxes.xyxy.tolist(), confidences: boxes.conf.tolist(), class_ids: boxes.cls.tolist() }4. 千問與DeepSeek雙模型智能分析模塊的實現(xiàn)思路4.1 大模型在檢測系統(tǒng)中到底扮演什么角色很多朋友問我YOLO已經(jīng)把框畫出來了為什么還要接大模型這個問題問到了點子上。檢測框本身是數(shù)值信息業(yè)務(wù)方看不懂也不關(guān)心。他們需要的是當(dāng)前畫面里大約多少人人群密度是否超標(biāo)有沒有出現(xiàn)異常行為聚集、奔跑、滯留這類結(jié)論。大模型在系統(tǒng)里的角色就是只會畫框的YOLO和需要語義理解的人類之間的翻譯官。我把YOLO輸出的結(jié)構(gòu)化數(shù)據(jù)檢測框坐標(biāo)、數(shù)量、置信度轉(zhuǎn)成一段有語義的文本描述讓大模型基于這些描述結(jié)合場景上下文輸出分析結(jié)論。4.2 檢測結(jié)果的結(jié)構(gòu)化與提示詞設(shè)計大模型理解力的上限很大程度取決于你喂給它的提示詞。我踩過不少坑之后總結(jié)出一套比較穩(wěn)定的提示詞模板核心思想是把檢測數(shù)據(jù)轉(zhuǎn)換為人可讀的文本再送入模型而不是直接貼JSON。你是負責(zé)商場監(jiān)控的安防分析助手。 以下是YOLO目標(biāo)檢測系統(tǒng)剛剛輸出的檢測數(shù)據(jù) - 檢測時間2025-06-18 14:32:07 - 共檢測到行人47人 - 畫面尺寸1920x1080 - 行人在畫面中的分布位置(這里按區(qū)域說明例如入口扶梯區(qū)域12人中庭區(qū)域25人收銀臺區(qū)域10人) - 檢測框重疊情況入口扶梯區(qū)域超過60%的檢測框存在高度重疊 請根據(jù)以上數(shù)據(jù)從以下幾個維度進行分析 1. 當(dāng)前人群整體密度是否正常 2. 哪些區(qū)域存在擁擠或安全隱患 3. 是否建議啟動限流或疏導(dǎo)措施 4. 如果要給現(xiàn)場安保人員一條簡潔提醒你會說什么這個提示詞把大模型從看懂檢測框的重擔(dān)中解放出來它只需要專注分析這件事。實際測試下來千問和DeepSeek對這種結(jié)構(gòu)化文本的響應(yīng)質(zhì)量遠好于直接扔一堆JSON數(shù)組。4.3 雙模型聯(lián)動的容錯與增強策略同時接千問和DeepSeek不是噱頭而是出于兩個實際考慮容錯和視角互補。調(diào)用大模型API最煩的事情就是服務(wù)不穩(wěn)定。千問偶爾會超時DeepSeek偶爾會返回格式錯誤。我的策略是配置了主備切換默認走DeepSeek的deepseek-chat模型如果超時或返回異常自動切換到千問的qwen-plus重試一次。這個邏輯在Java里實現(xiàn)很簡單用一個枚舉標(biāo)識模型供應(yīng)商再用一個Router類做切換。// 雙模型路由核心邏輯 public AnalysisResult analyzeWithFailover(String prompt) { // 優(yōu)先嘗試DeepSeek for (SupplierAnalysisResult strategy : List.of(modelStrategies)) { try { return strategy.get(); } catch (RemoteApiException e) { log.warn(model call failed, switching to backup. cause: {}, e.getMessage()); } } throw new BizException(all model services unavailable); }更深一層我讓兩個模型做交叉驗證。DeepSeek先給出分析結(jié)論千問再針對同一份檢測數(shù)據(jù)給出它的判斷然后系統(tǒng)對兩個結(jié)論做簡單的一致性校驗。如果兩個模型對人群密度是否超標(biāo)的結(jié)論一致直接采信如果不一致則默認取更保守的那個比如建議限流并標(biāo)記為雙模型分歧請注意人工復(fù)核。這個機制在安防場景里真的有用能把單模型的極端誤判風(fēng)險降一半以上。5. 前后端分離的Web交互界面與聯(lián)調(diào)細節(jié)5.1 頁面設(shè)計與核心交互流程前端我選了Vue 3 Element Plus組合Vite作為構(gòu)建工具。這里不討論框架優(yōu)劣純粹是生態(tài)成熟、團隊上手快、社區(qū)資料多。整體頁面分為四個核心區(qū)域左側(cè)工具欄上傳圖片、選擇YOLO版本、開關(guān)智能分析、設(shè)置置信度閾值。中央畫布區(qū)展示原始圖像和檢測結(jié)果檢測框用不同顏色區(qū)分行人個體。右側(cè)信息面板展示檢測統(tǒng)計人數(shù)、耗時、置信度分布和大模型分析結(jié)論。底部記錄區(qū)展示歷史檢測記錄支持翻頁和按時間檢索。交互流程是這樣的用戶點擊上傳選擇圖片頁面立即發(fā)起檢測請求檢測框返回后通過Canvas在原圖上繪制矩形框并在每個框的左上角標(biāo)注置信度如果用戶開啟了智能分析幾秒后右側(cè)面板會動態(tài)更新AI分析內(nèi)容。整個過程不需要刷新頁面我把檢測結(jié)果和分析結(jié)論的展示拆成了兩個獨立的前端組件各自監(jiān)聽不同的數(shù)據(jù)源。5.2 前后端聯(lián)調(diào)中的數(shù)據(jù)格式約定前后端分離項目大部分的聯(lián)調(diào)問題都出在數(shù)據(jù)格式約定不統(tǒng)一。項目啟動第一天我就和前端同學(xué)把接口契約用OpenAPI規(guī)范固定下來了。這里分享幾個關(guān)鍵的格式約定。第一個是坐標(biāo)體系。YOLO輸出的檢測框是像素坐標(biāo)x1, y1, x2, y2但我要求后端傳給前端時統(tǒng)一轉(zhuǎn)換成相對坐標(biāo)0-1之間因為前端要在不同分辨率的顯示器上還原絕對像素坐標(biāo)在響應(yīng)式布局下會錯位。轉(zhuǎn)換公式很簡單rel_x abs_x / image_width。第二個是時間格式。后端統(tǒng)一返回ISO 8601字符串前端不做本地時區(qū)臆測直接展示原始字符串。這個約定避免了后端認為自己返回的是UTC前端認為是本地時間的經(jīng)典烏龍。第三個是空值語義。檢測結(jié)果里如果沒有檢測到行人后端返回的是空數(shù)組而不是null大模型分析失敗時analysis字段返回null并且附帶一個errorCode。前端根據(jù)這個約定做狀態(tài)展示避免出現(xiàn)畫面空白但用戶不知道為什么的問題。// 前端Canvas渲染檢測框的核心邏輯 const drawBoxes (boxes, confidences) { boxes.forEach((box, index) { const [x, y, w, h] normalizeBox(box, imageWidth, imageHeight); ctx.strokeStyle confidences[index] 0.6 ? #f56c6c : #e6a23c; ctx.lineWidth 2; ctx.strokeRect(x, y, w, h); ctx.fillText(${(confidences[index] * 100).toFixed(1)}%, x, y - 5); }); };5.3 WebSocket實時推送檢測結(jié)果圖片檢測用HTTP請求就能搞定但視頻流分析和批量檢測場景必須上WebSocket。我采用SpringBoot原生WebSocket實現(xiàn)連接建立后前端把視頻流按幀抽取后發(fā)送到后端后端經(jīng)過YOLO推理后把結(jié)果實時推回前端。這里有個性能問題要提醒視頻流的幀率不能太貪。我一開始試圖按25fps推流檢測結(jié)果GPU直接打滿隊列堆積嚴(yán)重。后來調(diào)整策略每秒抽取2-3幀做檢測其余幀直接丟棄。實際體驗下來對于人群監(jiān)控這種場景3幀每秒的檢測頻率完全夠用而且能覆蓋大多數(shù)人群運動速度。// WebSocket消息推送示例 Component public class DetectionWebSocket { private final SetWebSocketSession sessions ConcurrentHashMap.newKeySet(); OnOpen public void onOpen(WebSocketSession session) { sessions.add(session); } public void pushDetectResult(String sessionId, DetectionResult result) { // 找到對應(yīng)會話發(fā)送JSON消息 sessions.stream() .filter(s - s.getId().equals(sessionId)) .forEach(s - { try { synchronized (s) { s.sendMessage(new TextMessage(JSON.toJSONString(result))); } } catch (IOException e) { log.error(WebSocket push failed, e); } }); } }6. YOLO數(shù)據(jù)準(zhǔn)備、訓(xùn)練評估與模型轉(zhuǎn)換的實戰(zhàn)經(jīng)驗6.1 數(shù)據(jù)來源與標(biāo)注工具的選擇整套系統(tǒng)的效果上限是數(shù)據(jù)決定的。我在這個項目里用的訓(xùn)練數(shù)據(jù)來自兩個部分公開數(shù)據(jù)集以及自己標(biāo)注的實地場景數(shù)據(jù)。公開數(shù)據(jù)集方面我混合使用了CrowdHuman和VisDrone的部分子集。CrowdHuman是密集行人檢測的經(jīng)典數(shù)據(jù)集每張圖片平均有23人密集場景占比非常高行人之間的遮擋很嚴(yán)重VisDrone補充了大量高空視角的小目標(biāo)樣本正好彌補CrowdHuman在極小目標(biāo)上的不足。自己標(biāo)注時我選擇了X-AnyLabeling工具。相比LabelImg它內(nèi)置了YOLO檢測模型做預(yù)標(biāo)注人工只需要修正框的位置標(biāo)注效率能翻三倍。文本提示我特別重要標(biāo)注密集人群時遮擋超過70%的行人要不要標(biāo)我的原則是只要肉眼還能辨別出是一個獨立的人就標(biāo)完全被擋住只剩一點點頭發(fā)的不標(biāo)。這個標(biāo)準(zhǔn)的統(tǒng)一直接影響訓(xùn)練后模型的行為邊界。如果手里只有KITTI或者其他格式的數(shù)據(jù)強烈建議寫成腳本做格式轉(zhuǎn)換。KITTI的標(biāo)注格式和YOLO不同YOLO需要的是歸一化的中心點坐標(biāo)加寬高轉(zhuǎn)換腳本網(wǎng)上有現(xiàn)成的但務(wù)必檢查類別ID的映射這個最容易出錯。6.2 數(shù)據(jù)增強策略與訓(xùn)練參數(shù)調(diào)整密集行人檢測的數(shù)據(jù)增強策略和通用檢測不完全一樣。我在訓(xùn)練時試過一組增強組合最終穩(wěn)定生效的配置是這樣增強策略參數(shù)設(shè)置作用Mosaic開啟概率0.8豐富小目標(biāo)樣本模擬密集排列MixUp開啟概率0.2提升模型對遮擋的魯棒性Copy-Paste開啟概率0.3模擬行人之間高度重疊HSV增強h0.015, s0.7, v0.4適應(yīng)不同光線環(huán)境隨機尺度0.5-1.5倍應(yīng)對尺度分布不均訓(xùn)練參數(shù)方面我用的是SGD優(yōu)化器初始學(xué)習(xí)率0.01權(quán)重衰減0.0005batch size 16總共訓(xùn)練200個epoch。輸入分辨率從默認的640×640提升到960×960之后小目標(biāo)AP提升了約5個百分點代價是訓(xùn)練時間和推理時間都翻倍。我的建議是如果部署機器的算力允許優(yōu)先把分辨率提到896或960對小目標(biāo)的收益非常明顯。訓(xùn)練之后評估模型時不要只看mAP。我在項目的評估方案里加了兩個自定義指標(biāo)密集區(qū)域召回率把標(biāo)注密度超過每平方米0.5人的區(qū)域單獨統(tǒng)計召回和重疊目標(biāo)分辨準(zhǔn)確率兩個中心點距離小于30像素的目標(biāo)對中能同時正確檢出的比例。這兩個指標(biāo)比mAP更能反映密集場景的真實可用性。6.3 模型導(dǎo)出與部署格式選擇訓(xùn)練好的PyTorch模型不能直接扔給生產(chǎn)環(huán)境。我的部署流程一般是PyTorch權(quán)重 → ONNX → TensorRT根據(jù)部署機器的GPU情況靈活選擇最終格式。導(dǎo)出到ONNX時有一個關(guān)鍵參數(shù)要設(shè)置。YOLOv8和v11如果沒有開啟NMS導(dǎo)出ONNX模型輸出的原始預(yù)測結(jié)果還需要自己在部署端做后處理YOLOv10的模型結(jié)構(gòu)本身沒有NMS導(dǎo)出時更要注意。我的做法是導(dǎo)出時不帶NMS在后端Python服務(wù)里用OpenCV原生的NMS函數(shù)處理這樣靈活性最高可以隨時調(diào)整NMS閾值來適應(yīng)不同場景。# 導(dǎo)出ONNX示例 yolo export modelweights/yolov11s_crowd.pt formatonnx opset12 imgsz960導(dǎo)出TensorRT的時候先用onnx-tensorrt或trtexec工具做離線轉(zhuǎn)換。這里強烈建議在目標(biāo)機器上做轉(zhuǎn)換而不是在自己電腦上轉(zhuǎn)好了再拷過去因為TensorRT的優(yōu)化結(jié)果和GPU型號、驅(qū)動版本強相關(guān)在A卡上轉(zhuǎn)的引擎放到N卡上根本跑不了。7. 部署上線、踩坑記錄與性能調(diào)優(yōu)建議7.1 服務(wù)部署的整體架構(gòu)最終的部署形態(tài)是兩臺服務(wù)器一臺有GPU的用于跑YOLO推理一臺純CPU服務(wù)器跑SpringBoot后端加上大模型調(diào)用層。整體部署用Docker Compose管理MySQL和Redis用單獨的容器后端、檢測服務(wù)、前端Nginx各自獨立容器通過內(nèi)網(wǎng)互通。這里說一個部署時需要特別注意的點HTTP請求體大小的限制。如果前臺上傳的是高清視頻文件幾十MB甚至上百MB都很常見。SpringBoot默認的請求體大小是1MB視頻一傳就報錯。需要在配置里顯式調(diào)大同時設(shè)置合理的超時時間。# application.yml 關(guān)鍵配置 spring: servlet: multipart: max-file-size: 200MB max-request-size: 200MB server: tomcat: max-swallow-size: 200MB7.2 踩坑實錄大模型調(diào)用超時、NMS漏檢、并發(fā)瓶頸這個項目從開發(fā)到上線踩了不少坑挑三個最有代表性的分享。第一個坑是大模型API調(diào)用超時。上線第一天檢測圖片功能時不時就報錯排查發(fā)現(xiàn)是調(diào)用DeepSeek API時沒設(shè)置連接超時默認的TCP連接超時長達數(shù)分鐘。在部分網(wǎng)絡(luò)環(huán)境下連接會一直掛起然后占滿Tomcat線程池。解決辦法是在RestTemplate或者HTTP客戶端里顯式設(shè)置連接和讀取超時我設(shè)的是connectTimeout5s、readTimeout30s超時立刻走備選模型邏輯。第二個坑是NMS在密集區(qū)域漏檢。這個問題的根因是默認的NMS IoU閾值是0.45在人群密集區(qū)域兩個真實行人的IoU經(jīng)常超過0.5導(dǎo)致其中一個被抑制。我針對行人場景做了調(diào)整把IoU閾值提高到0.65并且把置信度閾值從0.25降到0.15。這樣的副作用是誤檢會增加所以我在后端加了一個基于框大小和位置的后處理過濾規(guī)則把明顯不合理的框比如寬高比小于0.2的細長框、超出畫面邊界的框直接丟棄。第三個坑是SpringBoot在并發(fā)高峰時的線程池被打滿。Tomcat默認的最大線程數(shù)是200當(dāng)多個用戶同時上傳圖片做檢測時每次檢測請求都會阻塞等待Python服務(wù)返回線程池很快耗盡。我的優(yōu)化方案是把檢測任務(wù)丟進線程池異步執(zhí)行前端通過任務(wù)ID輪詢或WebSocket獲取結(jié)果這樣Tomcat線程不會長時間被占用。Configuration public class AsyncConfig { Bean(name detectExecutor) public Executor detectExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(40); executor.setQueueCapacity(200); executor.setThreadNamePrefix(detect-); executor.setRejectedExecutionHandler(new CallerRunsPolicy()); return executor; } }7.3 提升整體性能的幾個關(guān)鍵手段系統(tǒng)穩(wěn)定運行后我開始做調(diào)優(yōu)。通過壓測發(fā)現(xiàn)瓶頸主要在兩個地方Python推理服務(wù)在并發(fā)請求下的排隊以及大模型分析的響應(yīng)時間。針對這兩個瓶頸我做了幾個優(yōu)化。優(yōu)化一是Redis加緩存。同一個攝像頭同一個區(qū)域的畫面在短時間內(nèi)檢測結(jié)果具有高度相似性。我用文件哈希模型版本置信度閾值作為緩存key把最近1小時的檢測結(jié)果緩存到Redis重復(fù)請求直接命中緩存檢測耗時從幾百毫秒降到個位數(shù)毫秒。這個優(yōu)化讓所有展示歷史記錄頁面的加載速度幾乎變成秒開。優(yōu)化二是Python側(cè)啟動預(yù)熱。FastAPI服務(wù)啟動時如果等到第一個請求來才加載YOLO模型那次請求的耗時可能長達幾十秒。我在服務(wù)啟動事件里把四個版本的模型全部預(yù)加載到顯存中并各跑一張純黑圖片做GPU預(yù)熱。之后每次請求的推理耗時基本保持在模型本身的推理時間不再有冷啟動懲罰。優(yōu)化三是前端做了圖片壓縮。用戶上傳的圖片動輒5-10MB全尺寸傳給后端做檢測很浪費。前端在上傳前用Canvas把圖片寬度壓縮到1280像素質(zhì)量壓縮到0.8。檢測框是基于壓縮后的圖片計算出來的展示結(jié)果時再把坐標(biāo)等比映射回原圖尺寸。這一步讓上傳帶寬和檢測耗時都下降了一半以上。注意在部署這套系統(tǒng)時有幾個容易忽略的點。第一GPU服務(wù)器的顯存要留足余量同時加載四個模型可能直接OOM建議按需加載或者用模型大小換推理速度。第二大模型API調(diào)用一定要做費用監(jiān)控如果長時間無人訪問定時任務(wù)不斷觸發(fā)分析幾十萬次請求產(chǎn)生的費用不是小數(shù)目。第三檢測數(shù)據(jù)涉及個人隱私系統(tǒng)上線前要做好權(quán)限控制和數(shù)據(jù)脫敏數(shù)據(jù)庫里的檢測記錄存儲時間不宜過長。最后分享一點個人體會。這套系統(tǒng)從零到一做完我最大的感受是算法、后端、前端、大模型四個環(huán)節(jié)的銜接才是真正的復(fù)雜度所在。YOLO單看效果很好SpringBoot單看也不難大模型API文檔更是簡單到一眼就會但把它們組合成一個穩(wěn)定運轉(zhuǎn)的系統(tǒng)每一個環(huán)節(jié)都需要在性能和可靠性之間反復(fù)做取舍。如果你打算復(fù)現(xiàn)這個項目我建議不要一上來就追求四版本YOLO全支持和大模型雙路解析先把最小閉環(huán)跑通YOLOv8 SpringBoot 一個最簡前端再逐步疊加能力這樣排查問題時的顆粒度會更清晰。一個小技巧送給正在搭建類似系統(tǒng)的朋友在SpringBoot和Python推理服務(wù)之間加一層接口日志把每次請求的模型版本、推理耗時、檢測數(shù)量、大模型響應(yīng)時間全部記錄下來。這些數(shù)據(jù)在調(diào)優(yōu)時是判斷瓶頸到底在算法側(cè)還是在服務(wù)側(cè)的黃金依據(jù)比我上面提到的任何方案都更值得優(yōu)先落地。