火焰煙霧檢測系統(tǒng)全棧實(shí)現(xiàn))
森林防火這件事真正做過一線部署的人都知道難的不是把模型跑起來而是讓它在沒人值守的山頭、在逆光、在霧天、在只有幾幀畫面能看到白煙的情況下還能穩(wěn)定給出可信的告警。我前后折騰過幾套方案從最早的 OpenCV 閾值運(yùn)動檢測到后來的 YOLO 系列目標(biāo)檢測中間踩過的坑基本能寫成一本書。這套基于 YOLOv8/v10/v11/v12/26 的森林野外火災(zāi)火焰煙霧檢測系統(tǒng)就是把這些年零散的經(jīng)驗收攏成一個能落地的完整工程后端用 Spring Boot 管業(yè)務(wù)與任務(wù)前端用 Vue 做可視化和視頻播放推理側(cè)用 Flask 把 YOLO 權(quán)重封成服務(wù)再掛上 DeepSeek、千問這類大模型做告警語義解讀和值班問答。它解決的核心問題是——把檢測到火這個動作變成誰在什么位置、置信度多少、要不要立刻派人、該帶什么裝備這一整套可執(zhí)行的判斷鏈路。適合兩類人看一類是想拿它當(dāng)畢業(yè)設(shè)計或課程項目練手的學(xué)生另一類是真的要在林場、景區(qū)、輸電線路走廊做試點(diǎn)部署的工程同學(xué)。下面我按從架構(gòu)到落地、從訓(xùn)練到排障的順序把每一步講透。1. 為什么值得做這套森林火災(zāi)檢測系統(tǒng)1.1 野外火情檢測的真實(shí)痛點(diǎn)林火檢測和城市安防里的煙火識別完全不是一回事。城市里攝像頭有補(bǔ)光、有固定背景、有穩(wěn)定的網(wǎng)絡(luò)回傳檢測目標(biāo)大概率是廚房油煙或者燒烤攤的白煙而野外場景面對的是大面積山林、隨風(fēng)擺動的樹冠、飄動的云、清晨的低空霧氣、夕陽的強(qiáng)逆光甚至一只飛過的白鷺都能讓早期模型瘋狂誤報。我最早用純顏色閾值的方法做過一版思路是提取畫面里偏橙紅和偏灰白的區(qū)域再判斷面積增長結(jié)果夏天日落時段幾乎每十分鐘就報一次警值班人員直接把這套系統(tǒng)關(guān)了。真正的痛點(diǎn)在三個層面。第一是早期煙霧比明火更值得抓因為等看到明火火場面積往往已經(jīng)擴(kuò)散到幾公頃撲救成本呈指數(shù)上升但煙霧的特征恰恰最弱半透明的灰白煙在灰綠色林地里對比度極低小目標(biāo)檢測難度遠(yuǎn)高于火焰。第二是誤報代價和漏報代價不對稱漏報一次可能是幾百畝林地和人員傷亡誤報一次只是浪費(fèi)一次出警所以系統(tǒng)設(shè)計上寧可偏向召回率但要通過多幀確認(rèn)、區(qū)域掩碼、時間聚合把誤報壓回到可接受范圍。第三是算力與帶寬的現(xiàn)實(shí)約束很多林區(qū)點(diǎn)位靠太陽能4G 回傳邊緣設(shè)備可能是 Jetson 或者低功耗工控機(jī)不可能無腦堆大模型。理解這三點(diǎn)之后整套系統(tǒng)的設(shè)計取向就清楚了模型要輕、召回要高、要有時間維度的確認(rèn)機(jī)制、要有可解釋的告警輸出。這也是我最終選擇 YOLO 系而不是兩階段檢測器的根本原因——單階段檢測在保持可接受精度的前提下推理延遲和部署復(fù)雜度都低得多邊緣側(cè)友好。1.2 技術(shù)選型的整體思路拆解先把幾個關(guān)鍵詞的定位說清楚避免混淆。YOLOv8/v10/v11/v12/26是檢測算法版本的候選池實(shí)際部署時只會選其中一個作為生產(chǎn)模型其余用于對比實(shí)驗和論文里的消融分析。Flask是推理服務(wù)的載體它的定位很純粹加載權(quán)重、接收圖像、吐回檢測框不承擔(dān)任何業(yè)務(wù)邏輯。Spring Boot是業(yè)務(wù)中樞負(fù)責(zé)用戶、設(shè)備、任務(wù)、告警、歷史記錄的增刪改查和權(quán)限控制。Vue是操作臺值班人員看的就是這一層。DeepSeek 和千問負(fù)責(zé)的是把結(jié)構(gòu)化檢測結(jié)果翻譯成人話比如結(jié)合風(fēng)向、地形、歷史火點(diǎn)數(shù)據(jù)生成一段可讀的處置建議。為什么要把 Flask 單獨(dú)拆出來而不是在 Spring Boot 里用 Java 直接推理這是我很早就定下的原則。Python 側(cè)的深度學(xué)習(xí)生態(tài)是碾壓性的Ultralytics 的庫、ONNX Runtime、TensorRT 的 Python 綁定、各種后處理和可視化工具鏈都在 Python 里最成熟而 Java 側(cè)強(qiáng)行接 ONNX 或者 DJL 雖然可行但版本兼容、CUDA 綁定、自定義算子支持都會成為長期維護(hù)負(fù)擔(dān)。用 Flask 做一層 HTTP 服務(wù)把兩邊的邊界劃清楚各用各的強(qiáng)項代價只是多一次網(wǎng)絡(luò)跳轉(zhuǎn)——在內(nèi)網(wǎng)里這點(diǎn)延遲完全可以接受。至于大模型這一層很多人會問是不是畫蛇添足。我的判斷是在大規(guī)模值班場景里告警的可讀性和處置建議的即時性確實(shí)能省人力。傳統(tǒng)做法是彈一個框、顯示smoke 0.87值班員還要自己判斷嚴(yán)重程度接入大模型后可以直接輸出東南坡 3 號點(diǎn)位持續(xù) 40 秒檢測到煙霧置信度中高當(dāng)前風(fēng)向東南、風(fēng)力 3 級建議優(yōu)先核查是否向西北蔓延。這個體驗差距是很實(shí)在的。當(dāng)然大模型只做解讀和輔助絕不能讓它參與是否告警的判定否則不可控因素太多。2. 系統(tǒng)架構(gòu)設(shè)計與各層職責(zé)劃分2.1 四層架構(gòu)的分工邊界整套系統(tǒng)我按四層來切采集層、推理層、業(yè)務(wù)層、展示層。采集層負(fù)責(zé)從攝像頭、無人機(jī)圖傳或本地視頻文件拿到幀序列野外點(diǎn)位通常用 RTSP 拉流邊緣側(cè)做抽幀后壓縮上傳如果帶寬允許也可以直接把幀送到中心推理服務(wù)。推理層就是 Flask 服務(wù)可以單機(jī)多進(jìn)程也可以橫向擴(kuò)成多個實(shí)例由 Nginx 做負(fù)載均衡。業(yè)務(wù)層是 Spring Boot所有狀態(tài)變更、持久化、權(quán)限、定時任務(wù)都在這里。展示層是 Vue 單頁應(yīng)用。這樣切分的好處是每一層都能獨(dú)立替換和擴(kuò)縮容。比如模型從 YOLOv8 換成改進(jìn)版只動推理層前端要改成大屏只動展示層。我在實(shí)際部署里遇到過客戶要求把推理從中心機(jī)房下沉到邊緣盒子如果當(dāng)初把推理邏輯寫死在 Java 里這個改動會非常痛苦。數(shù)據(jù)庫我用的是 MySQL 存業(yè)務(wù)數(shù)據(jù)、Redis 做幀任務(wù)隊列和告警去重緩存、MinIO 或者說對象存儲存告警截圖和短視頻片段。截圖這一步很關(guān)鍵沒有證據(jù)留存的告警系統(tǒng)在事后復(fù)盤時基本沒有價值值班人員需要能回看到當(dāng)時那一幀到底是什么樣。2.2 視頻流接入與抽幀調(diào)度設(shè)計野外點(diǎn)位如果是 24 小時不間斷拉流一路 1080p 每秒 25 幀直接全量推理是不現(xiàn)實(shí)的。我的做法是抽幀 雙速率調(diào)度。正常情況下每秒抽 1 到 2 幀做快速掃描用一個更輕量的模型或降低輸入分辨率一旦某個區(qū)域連續(xù)多幀出現(xiàn)可疑響應(yīng)自動切換到高頻率、高分辨率的精檢模式。這個機(jī)制在代碼上表現(xiàn)為一個狀態(tài)機(jī)初始狀態(tài)是巡航檢測到可疑目標(biāo)進(jìn)入確認(rèn)中連續(xù) N 幀命中或總置信度累積超過閾值后進(jìn)入告警并鎖存一段時間。抽幀的實(shí)現(xiàn)我推薦在邊緣側(cè)完成用 FFmpeg 的-vf fps1或者 OpenCV 按幀號取模而不是把全量流推到中心再抽。原因是 4G 上行帶寬太寶貴一路 1080p 25fps 的 H.264 流大概 4Mbps四個點(diǎn)位就把一條百兆上行吃滿了而抽幀后的圖片即使按 1fps 傳碼率也就幾百 Kbps。# 邊緣側(cè)抽幀示例每秒取1幀輸出到共享目錄 ffmpeg -rtsp_transport tcp -i rtsp://user:passcamera_ip:554/stream \ -vf fps1,scale1280:-1 -q:v 3 -f image2 /data/frames/cam01_%08d.jpg注意-rtsp_transport tcp這一項在野外弱網(wǎng)環(huán)境下一定要加UDP 丟包會導(dǎo)致花屏幀被送進(jìn)模型產(chǎn)生大量莫名其妙的誤檢。我一開始沒加排查了兩天才發(fā)現(xiàn)是傳輸層的問題不是模型的問題。2.3 前后端通信協(xié)議與數(shù)據(jù)契約前后端之間我傾向用REST WebSocket 混合。REST 處理設(shè)備列表、歷史告警查詢、模型切換這類低頻操作WebSocket 推送實(shí)時檢測結(jié)果因為檢測框是每秒都在變的用輪詢會把服務(wù)器打爆。WebSocket 的消息體我用 JSON結(jié)構(gòu)大概是這樣幀號、時間戳、檢測框數(shù)組含類別、置信度、坐標(biāo)、當(dāng)前模式巡航/確認(rèn)/告警。坐標(biāo)統(tǒng)一用歸一化值 0 到 1這樣前端不管畫布多大都能正確縮放避免了分辨率適配的麻煩。這里有個容易忽略的細(xì)節(jié)Vue 前端渲染檢測框不要每收到一條消息就把 canvas 整個重繪那樣在 25fps 下會明顯卡頓。正確做法是用 requestAnimationFrame 做節(jié)流把最新的檢測結(jié)果存在一個 ref 里動畫幀到了再統(tǒng)一繪制。另外如果視頻播放用的是 HLS 或 m3u8 切片會天然存在幾秒延遲檢測框必須跟播放時間戳對齊而不是跟服務(wù)器時間對齊否則畫面上框會飄到未來或者過去。這個對齊問題我在做vue播放m3u8那版時踩得很深最后是給每個切片打上起始時間戳前端用 HLS 實(shí)例的currentTime去反查對應(yīng)幀的檢測結(jié)果。3. YOLO 系列模型縱向?qū)Ρ扰c選型3.1 各版本的核心差異梳理先把這幾個版本的譜系講明白很多人容易搞混。YOLOv8 是 Ultralytics 的旗艦作品業(yè)界默認(rèn)錨點(diǎn)模型工程資料最豐富、部署工具鏈最全、社區(qū)問題最好查作為基線選它幾乎不會有錯。YOLOv10 的核心賣點(diǎn)是端到端免 NMS通過一致性雙分配策略去掉了后處理里的非極大值抑制這對部署是個不小的簡化因為 NMS 在邊緣側(cè)是實(shí)打?qū)嵉暮臅r環(huán)節(jié)而且閾值調(diào)參很煩。YOLOv11 是 Ultralytics 在 v8 基礎(chǔ)上的一次迭代主要改動在骨干和頸部結(jié)構(gòu)以及訓(xùn)練策略精度和速度都有小幅提升同時也補(bǔ)上了分類和姿態(tài)估計等任務(wù)的配套權(quán)重。至于YOLOv12 和 YOLOv26這里必須說句實(shí)在話這兩個版本號在社區(qū)里出現(xiàn)得比較晚命名來源和官方倉庫之間存在分歧有的是研究者在 v11 基礎(chǔ)上做的注意力機(jī)制改造有的是第三方對某一代架構(gòu)的重新編號。我用下來的態(tài)度是——可以納入對比實(shí)驗但作為生產(chǎn)模型前一定要核實(shí)權(quán)重來源、訓(xùn)練數(shù)據(jù)、許可證和是否可復(fù)現(xiàn)。我見過不少同學(xué)論文里寫了某個版本結(jié)果一查權(quán)重是別人在私有數(shù)據(jù)集上微調(diào)的換到自己的數(shù)據(jù)上直接崩掉。所以下面的對比表里我會把這兩個版本標(biāo)注為需驗證來源而不是當(dāng)成可靠的默認(rèn)選項。3.2 火焰煙霧場景下的實(shí)測對比維度在野外火情這個特定任務(wù)上通用 COCO 榜單的 mAP 參考價值有限因為我們的目標(biāo)只有火焰和煙霧兩類而且煙霧是弱紋理、低對比度的半透明區(qū)域。我更關(guān)注的是這幾個指標(biāo)小目標(biāo)召回率早期煙點(diǎn)往往只占畫面很小一塊、逆光和霧天條件下的穩(wěn)定性、單幀推理延遲要在邊緣設(shè)備上跑、誤報率尤其日落和云層時段、模型體積決定能不能塞進(jìn)顯存緊張的設(shè)備。我做過一組對比實(shí)驗數(shù)據(jù)來自自建的野外火情數(shù)據(jù)集約 1.2 萬張圖覆蓋白天、黃昏、夜間紅外、霧天四類場景。測試硬件是一張消費(fèi)級顯卡和一臺 Jetson 邊緣設(shè)備。結(jié)果顯示在同等輸入分辨率下v8 的基線表現(xiàn)穩(wěn)健v10 由于免 NMS在邊緣設(shè)備上的端到端延遲反而最低但小目標(biāo)表現(xiàn)略遜v11 在整體精度上有優(yōu)勢尤其是煙和火類間混淆的問題有所緩解v12 和 v26 我拿到的權(quán)重在小目標(biāo)上確實(shí)有提升但訓(xùn)練腳本和依賴版本比較特殊遷移成本高。3.3 版本對比速查表與選型結(jié)論版本核心特點(diǎn)邊緣部署友好度小目標(biāo)表現(xiàn)社區(qū)資料豐富度生產(chǎn)推薦度YOLOv8基線錨點(diǎn)工具鏈最全高中極高強(qiáng)推薦YOLOv10免 NMS后處理簡化很高中偏下中推薦延遲敏感場景YOLOv11結(jié)構(gòu)迭代精度小幅提升高中偏上高強(qiáng)推薦YOLOv12注意力改造小目標(biāo)有提升中上低需驗證來源YOLOv26命名存疑來源不一中偏低不確定極低僅作對比結(jié)論很直接生產(chǎn)環(huán)境我首選 YOLOv8 或 YOLOv11 的中小規(guī)格權(quán)重比如 n 或 s 尺寸在邊緣設(shè)備上做 TensorRT 或 ONNX 加速v10 留作延遲優(yōu)化的備選v12 和 v26 放進(jìn)對比實(shí)驗章節(jié)用來支撐我做過充分調(diào)研這個論點(diǎn)但不作為線上主力。這個選擇不是保守而是工程上對可維護(hù)性的尊重——出了問題能立刻搜到答案比紙面上高 1 個點(diǎn) mAP 重要得多。4. 數(shù)據(jù)集準(zhǔn)備與訓(xùn)練全流程實(shí)操4.1 數(shù)據(jù)采集、標(biāo)注與清洗策略數(shù)據(jù)集是這套系統(tǒng)里最耗時的部分沒有之一。我的采集策略是多源拼湊 主動挖掘。公開數(shù)據(jù)集能拿到一部分林火圖像但數(shù)量和質(zhì)量都不夠必須自己補(bǔ)。補(bǔ)的方式包括爬取公開的森林火災(zāi)新聞圖注意版權(quán)只用于研究、在安全可控的條件下做小型明火試驗拍攝、從林場歷史監(jiān)控錄像里抽幀、以及用數(shù)據(jù)增強(qiáng)合成煙霧。合成煙霧我用過幾種方式在干凈林地背景上疊加高斯噪聲加透明度漸變模擬煙霧用類似 SMOG 的物理仿真生成效果只能算湊合能提升召回但也會引入一些不真實(shí)的紋理所以我一般控制合成數(shù)據(jù)占比不超過總量的 20%。標(biāo)注用 LabelImg 或者 CVAT類別就兩個fire和smoke。這里有個爭議點(diǎn)——要不要把疑似煙霧濃煙白煙拆成細(xì)分類。我的經(jīng)驗是別拆早期階段細(xì)分只會讓每類樣本都不足反而拖累整體指標(biāo)等基礎(chǔ)檢測穩(wěn)定后再考慮做屬性分類。標(biāo)注過程中最要命的是邊界判定煙霧沒有清晰邊緣標(biāo)注框大小因人而異。解決辦法是制定標(biāo)注規(guī)范明確框住肉眼可見煙霧主體的最小外接矩形并且所有標(biāo)注由同一人復(fù)核保證一致性。我吃過標(biāo)注不一致的虧同一個模型在 A 標(biāo)注員的數(shù)據(jù)上訓(xùn)完在 B 的標(biāo)準(zhǔn)上評估指標(biāo)直接掉 5 個點(diǎn)。4.2 訓(xùn)練參數(shù)配置與凍結(jié)策略訓(xùn)練用的是 Ultralytics 框架命令行風(fēng)格很統(tǒng)一?;A(chǔ)配置我一般這么起yolo detect train \ modelyolov8s.pt \ dataforest_fire.yaml \ epochs200 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ mosaic1.0 \ mixup0.1 \ copy_paste0.1 \ freeze0 \ patience30 \ device0參數(shù)背后的邏輯得說清楚。imgsz640是精度和速度的平衡點(diǎn)如果場景里早期煙點(diǎn)特別小可以提到 960 或 1280但延遲會明顯上升邊緣設(shè)備可能扛不住。mosaic1.0對火情檢測幫助很大因為它把四張圖拼在一起變相增加了小目標(biāo)和復(fù)雜背景的樣本但注意訓(xùn)練后期建議關(guān)掉 mosaicUltralytics 默認(rèn)最后 10 個 epoch 自動關(guān)閉否則目標(biāo)的真實(shí)尺度分布會被扭曲。mixup和copy_paste是錦上添花的增強(qiáng)copy_paste尤其適合煙霧這種需要貼到不同背景上的類別。關(guān)于freeze參數(shù)這是很多人問的。凍結(jié)骨干網(wǎng)絡(luò)在兩種情況下有用一是數(shù)據(jù)量很小比如只有兩三千張圖全量微調(diào)容易過擬合凍結(jié)前幾層能讓模型更多地學(xué)習(xí)任務(wù)相關(guān)的高層特征二是算力特別緊張凍結(jié)能顯著減少反向傳播的計算量。我在小數(shù)據(jù)集上試過freeze10凍結(jié)前 10 層收斂更快也更穩(wěn)。但如果數(shù)據(jù)量上萬還是老老實(shí)實(shí)全量訓(xùn)練效果更好。4.3 訓(xùn)練過程監(jiān)控與損失曲線分析訓(xùn)練跑起來之后別當(dāng)甩手掌柜損失曲線是排查問題最直接的窗口。Ultralytics 訓(xùn)練完會自動生成results.png包含 box loss、cls loss、dfl loss 以及各類 mAP 曲線。我關(guān)注的幾個信號如果 box loss 下降但在驗證集上 mAP 不漲反跌基本可以判定過擬合該上更強(qiáng)的增強(qiáng)或者更早停如果 cls loss 一直高居不下往往是類別不平衡或者標(biāo)注有噪聲煙霧樣本通常遠(yuǎn)少于火焰樣本需要調(diào)類別權(quán)重或者補(bǔ)充煙霧數(shù)據(jù)如果 loss 曲線出現(xiàn)大幅震蕩多半是學(xué)習(xí)率太大或者 batch 太小降低 lr0 或者開啟梯度累積。畫損失曲線如果不想用自帶圖也可以自己導(dǎo)出 CSV 用 matplotlib 畫這樣能自由組合對比多組實(shí)驗。我在做多版本對比時就是自己寫腳本把 v8、v10、v11 的曲線畫在同一張圖上答辯和匯報時一目了然。訓(xùn)練結(jié)束后一定要做混淆矩陣分析看 fire 被誤判成 smoke 的比例、背景被誤判成目標(biāo)的 FP 有多少。煙和火之間的混淆是最常見的尤其在濃煙裹著暗紅火光的時候兩者的視覺特征會重疊。解決辦法是補(bǔ)充這類邊界樣本或者在推理階段做類別后處理。提示訓(xùn)練前務(wù)必用yolo checks檢查環(huán)境確認(rèn) CUDA、cuDNN 和 PyTorch 版本匹配。我遇到過 PyTorch 版本和顯卡驅(qū)動不匹配導(dǎo)致訓(xùn)練速度只有正常值三分之一的坑查了半天才發(fā)現(xiàn)是環(huán)境問題而不是數(shù)據(jù)問題。5. Flask 推理服務(wù)的工程化實(shí)現(xiàn)5.1 模型加載、預(yù)熱與服務(wù)結(jié)構(gòu)Flask 服務(wù)看起來簡單但要做得穩(wěn)有幾個點(diǎn)必須處理。首先是模型全局加載一次而不是每次請求加載這個錯誤新手常犯每次請求都YOLO(best.pt)會導(dǎo)致第一個請求延遲幾秒而且顯存會逐漸泄漏。正確做法是在應(yīng)用啟動時加載到全局變量并且做一次預(yù)熱推理用一張空白圖跑一遍把 CUDA 的初始化、算子編譯這些開銷提前消化掉。服務(wù)結(jié)構(gòu)我通常拆成三層app.py負(fù)責(zé)路由和請求校驗detector.py封裝模型加載和推理邏輯postprocess.py處理 NMS如果模型不是端到端的和結(jié)果格式化。這樣拆的好處是換模型時只改 detector路由層不動。# detector.py 核心結(jié)構(gòu)示意 from ultralytics import YOLO import numpy as np class FireDetector: def __init__(self, weights_path, devicecuda:0, imgsz640, conf0.25): self.model YOLO(weights_path) self.device device self.imgsz imgsz self.conf conf self._warmup() def _warmup(self): dummy np.zeros((self.imgsz, self.imgsz, 3), dtypenp.uint8) self.model.predict(dummy, deviceself.device, verboseFalse) def infer(self, image_bgr): results self.model.predict( image_bgr, imgszself.imgsz, confself.conf, deviceself.device, verboseFalse ) return self._format(results)預(yù)熱這一步千萬別省。我在 Jetson 上測過不加預(yù)熱第一個請求要 6 到 8 秒值班場景下這就是幾個漏幀。5.2 推理接口設(shè)計與批量處理接口設(shè)計上我提供兩個入口/predict接收單張圖用于實(shí)時幀/predict_batch接收多張圖用于離線視頻分析。返回結(jié)構(gòu)統(tǒng)一包含檢測框列表、類別、置信度、推理耗時和模型版本號。返回里一定要帶模型版本號因為在做多模型對比時前端要顯示當(dāng)前用的是哪個版本日志里也要能追溯。批量處理有個細(xì)節(jié)野外點(diǎn)位的幀是連續(xù)的同一時刻可能有好幾個點(diǎn)位的幀同時到達(dá)。如果串行處理會排隊積壓我用一個簡單的線程池或者讓 Flask 配 gunicorn 起多 worker。但注意GPU 顯存是共享資源多 worker 同時推理會 OOM所以要么用單 worker 內(nèi)部隊列要么每個 worker 綁定不同的 GPU。實(shí)測下來單卡最穩(wěn)的方案是單 worker 配一個推理隊列前面用 Redis 做緩沖。5.3 多模型切換與性能優(yōu)化手段系統(tǒng)要支持多版本對比所以推理服務(wù)要能動態(tài)切換模型。我的做法是啟動時把所有候選權(quán)重都加載到內(nèi)存前提是顯存夠或者懶加載加 LRU 淘汰。接口上加一個model_name參數(shù)前端就能一鍵切換。對比實(shí)驗時同一個測試集分別跑五個模型把結(jié)果存成 JSON前端用圖表展示。性能優(yōu)化上如果目標(biāo)平臺是 NVIDIA 邊緣設(shè)備一定要走TensorRT。把 PyTorch 權(quán)重導(dǎo)出成 ONNX 再用 trtexec 轉(zhuǎn)成 engine推理速度通常能提升兩到三倍。導(dǎo)出時注意 opset 版本還有動態(tài) batch 和動態(tài)尺寸的設(shè)置這些參數(shù)錯了會導(dǎo)出失敗或者精度異常。如果是普通顯卡ONNX Runtime 加 CUDA provider 也是不錯的選擇配置簡單性能損失可接受。# 導(dǎo)出 ONNX yolo export modelbest.pt formatonnx imgsz640 opset12 simplifyTrue # 轉(zhuǎn)換 TensorRT engine在目標(biāo)設(shè)備上執(zhí)行 trtexec --onnxbest.onnx --saveEnginebest.engine --fp16 --workspace4096注意TensorRT engine 是和硬件、驅(qū)動版本強(qiáng)綁定的在同一臺設(shè)備上轉(zhuǎn)出來的 engine 換到另一臺同型號設(shè)備上都可能加載失敗所以部署腳本里最好把轉(zhuǎn)換步驟包含進(jìn)去在目標(biāo)機(jī)器上現(xiàn)場轉(zhuǎn)而不是直接把 engine 打包進(jìn)鏡像。6. Spring Boot 與 Vue 前后端協(xié)同實(shí)現(xiàn)6.1 后端業(yè)務(wù)建模與任務(wù)調(diào)度Spring Boot 這邊要建模的實(shí)體包括用戶、設(shè)備攝像頭點(diǎn)位、檢測任務(wù)、告警記錄、模型版本、系統(tǒng)配置。設(shè)備實(shí)體里要存 RTSP 地址加密存儲、經(jīng)緯度、所屬林班、責(zé)任人。告警記錄要存觸發(fā)時間、點(diǎn)位、類別、置信度、截圖路徑、處置狀態(tài)。模型版本實(shí)體存權(quán)重路徑、版本號、mAP 指標(biāo)用于對比展示。定時任務(wù)用 Spring 的Scheduled或者 Quartz負(fù)責(zé)周期性拉取各點(diǎn)位的幀并轉(zhuǎn)發(fā)給 Flask。這里我建議用異步 線程池不要用同步循環(huán)否則一個點(diǎn)位卡住會阻塞整個調(diào)度。每個點(diǎn)位的拉幀任務(wù)獨(dú)立提交到線程池設(shè)置超時超時后記錄日志并跳過本輪。告警去重是很關(guān)鍵的一環(huán)。同一個火點(diǎn)會在連續(xù)幀里反復(fù)被檢測到如果每幀都入庫十分鐘就能產(chǎn)生上千條告警。我的做法是在 Redis 里維護(hù)一個點(diǎn)位區(qū)域的活躍告警集合設(shè)置一個時間窗口比如 5 分鐘窗口內(nèi)同區(qū)域的告警只更新置信度和持續(xù)時間不新增記錄。窗口過期后再觸發(fā)新告警。// 告警去重的核心思路示意 public void handleDetection(DetectionResult result) { String key alert:active: result.getDeviceId() : gridHash(result.getBbox()); if (redis.hasKey(key)) { redis.expire(key, 300, TimeUnit.SECONDS); alertRepo.updateConfidence(result.getAlertId(), result.getConf()); return; } Alert alert buildAlert(result); alertRepo.save(alert); redis.set(key, alert.getId(), 300, TimeUnit.SECONDS); pushToWebSocket(alert); }gridHash是把檢測框中心點(diǎn)映射到網(wǎng)格坐標(biāo)這樣鄰近區(qū)域的告警會被合并。網(wǎng)格粒度要根據(jù)實(shí)際視野調(diào)整太大容易漏掉同一點(diǎn)位的兩個火點(diǎn)太小又起不到合并作用。6.2 前端視頻播放與檢測框疊加Vue 前端最重要的兩個組件是視頻播放器和檢測框畫布。視頻源有兩種實(shí)時 RTSP 轉(zhuǎn)碼后的 HLS 流以及告警回放的歷史片段。實(shí)時播放用 hls.js 加載 m3u8注意延遲會累積可以用低延遲模式或者把切片時長設(shè)短一點(diǎn)2 秒左右的切片延遲大概在 3 到 5 秒。檢測框疊加我前面提過用 canvas 覆蓋在 video 元素上坐標(biāo)是歸一化的直接乘畫布寬高就行。這里要處理同名類別多框的重疊可以給不同類別不同顏色火焰用暖色、煙霧用冷色置信度用透明度表示越高越不透明。別用太粗的描邊野外畫面本來就復(fù)雜畫太粗會擋住火點(diǎn)本身。前端還有一個容易被忽略的功能是告警時間軸。把一天內(nèi)的告警點(diǎn)按時間鋪在一個橫向時間軸上點(diǎn)一下跳到對應(yīng)回放這個交互對值班人員非常友好。我做完這個之后值班反饋說排查效率至少提升一半。6.3 大模型接入與告警語義解讀大模型這一層我定位為解讀器 問答助手不參與判定。具體做兩件事第一把結(jié)構(gòu)化告警轉(zhuǎn)換成自然語言描述結(jié)合點(diǎn)位信息、風(fēng)向、歷史記錄生成處置建議第二提供值班問答比如今天哪些點(diǎn)位告警最多3 號點(diǎn)位歷史情況如何。調(diào)用方式就是標(biāo)準(zhǔn)的 HTTP 接口把檢測結(jié)果、點(diǎn)位元數(shù)據(jù)、環(huán)境數(shù)據(jù)拼成 prompt 發(fā)過去。DeepSeek 和千問都可以接我在代碼里做了適配層配置不同的 base_url 和 model name 就能切換。關(guān)鍵的工程細(xì)節(jié)是異步化和超時控制大模型響應(yīng)通常要幾秒絕不能阻塞告警入庫和推送應(yīng)該先推檢測結(jié)果大模型解讀結(jié)果作為后續(xù)消息異步補(bǔ)充推送。# 告警解讀的 prompt 構(gòu)造思路 prompt f 你是一名林火值班助手請根據(jù)以下結(jié)構(gòu)化檢測信息用一到兩句話給出簡潔的處置建議 不要編造未提供的信息。 點(diǎn)位{device.name}{device.location_desc} 檢測類別{alert.category} 置信度{alert.confidence:.2f} 持續(xù)時間{alert.duration_sec}秒 當(dāng)前風(fēng)向{weather.wind_dir}風(fēng)力{weather.wind_level}級 近期該點(diǎn)位告警次數(shù){alert.recent_count} 要求給出優(yōu)先級別高/中/低和建議動作。 提示一定要在 prompt 里明確不要編造未提供的信息否則大模型很容易腦補(bǔ)出周邊有村莊預(yù)計兩小時內(nèi)蔓延這類沒法驗證的話值班人員一旦當(dāng)真就麻煩了。另外大模型輸出不要直接落庫作為正式記錄作為輔助信息單獨(dú)標(biāo)注來源。7. 常見問題與排查技巧實(shí)錄7.1 環(huán)境依賴與部署類問題問題一Spring Boot 啟動不顯示端口號不確定是否起來了。這個我遇到過通常是因為日志級別配置把 Tomcat 的啟動日志屏蔽了或者啟動時被某個 Bean 初始化卡住。排查方法是開啟 debug 日志聽logging.level.org.springframework.boot.webDEBUG或者直接查端口占用netstat -ano | findstr 8080。如果是在 IDEA 里跑檢查 run configuration 里有沒有勾選正確的 profile很多同學(xué) profile 選錯導(dǎo)致連到了錯誤的數(shù)據(jù)庫自然看不到正常啟動日志。問題二Flask 服務(wù)在 Windows 上能跑到 Linux 上報 CUDA 相關(guān)錯誤。十有八九是 CUDA 版本和 PyTorch 版本不匹配或者顯卡驅(qū)動太舊。用nvidia-smi看驅(qū)動支持的 CUDA 上限用torch.version.cuda看 PyTorch 編譯的版本兩者要兼容。另外容器化部署時記得裝 NVIDIA Container Toolkit否則容器里看不到 GPU。問題三前端安裝依賴慢或報錯。vue安裝依賴卡住通常是 npm 源的問題換國內(nèi)鏡像或者用 pnpm 會好很多。如果報 node 版本不兼容用 nvm 切換 LTS 版本。還有vue安裝及環(huán)境配置階段最常見的坑是全局裝了舊版腳手架導(dǎo)致項目創(chuàng)建失敗升級 CLI 就行。7.2 推理性能與資源占用類問題問題一推理延遲高幀處理不過來積壓。先分清瓶頸在推理還是在傳輸。用 Flask 返回的耗時字段看純推理時間如果推理就要幾百毫秒說明模型太大或者沒做加速如果推理很快但端到端很慢那就是傳輸或者隊列的問題。模型側(cè)優(yōu)化順序是換小規(guī)格權(quán)重 → 降輸入分辨率 → 導(dǎo)出 ONNX → 轉(zhuǎn) TensorRT。傳輸側(cè)優(yōu)化是改用邊緣抽幀、降低上傳分辨率、用 JPEG 質(zhì)量參數(shù)換體積。問題二多路視頻同時推理導(dǎo)致顯存溢出。這個幾乎是必然的除非你的顯卡很大。解決方案有幾種顯存不夠就減少并發(fā)路數(shù)用隊列排隊處理或者用批處理把多個點(diǎn)位的幀合成一個 batch 一起推理提升顯存利用率再或者用 INT8 量化把模型壓小。量化會掉一點(diǎn)精度必須先評估對煙霧小目標(biāo)的影響別對著火點(diǎn)掉召回了。問題現(xiàn)象可能原因排查手段處理方案推理延遲突然升高顯存不夠觸發(fā)換頁看 nvidia-smi 顯存占用降并發(fā)或量化第一幀特別慢未預(yù)熱觀察首個請求耗時啟動時預(yù)熱幀處理積壓吞吐小于輸入速率對比輸入幀率和處理幀率抽幀或擴(kuò)實(shí)例服務(wù)偶發(fā)崩潰CUDA OOM 未捕獲查日志中的 OOM 報錯加異常兜底和降級7.3 檢測效果與誤報漏報類問題誤報集中在日落和薄霧時段這是野外場景最普遍的難題。我從三個角度處理一是數(shù)據(jù)上補(bǔ)充大量這兩種場景的負(fù)樣本尤其是日落天空、云層、霧中遠(yuǎn)山讓模型學(xué)會區(qū)分二是推理后處理上加區(qū)域掩碼把畫面里固定不動的高風(fēng)險區(qū)域比如一片反光的湖面、一個經(jīng)常起霧的山谷屏蔽掉三是時間維度的確認(rèn)單幀命中不算數(shù)要求連續(xù)多幀或者短時間窗口內(nèi)命中累積達(dá)到閾值。漏報主要體現(xiàn)在小煙點(diǎn)遠(yuǎn)處剛起的煙在 640 分辨率下可能只有十幾個像素。處理方式包括提高輸入分辨率、用帶 P2 小目標(biāo)檢測頭的模型結(jié)構(gòu)、在數(shù)據(jù)增強(qiáng)里增加小目標(biāo)的過采樣。Ultralytics 支持自定義模型 yaml加一個 P2 輸出層對小目標(biāo)幫助明顯代價是推理變慢要權(quán)衡。煙和火互相誤判靠增加邊界樣本解決同時在損失函數(shù)層面可以對混淆嚴(yán)重的類別對做加權(quán)。這個屬于調(diào)優(yōu)后期的工作前期別糾結(jié)先把整體召回和誤報壓到可用范圍。8. 現(xiàn)場部署與長期運(yùn)維經(jīng)驗8.1 點(diǎn)位布設(shè)與現(xiàn)場調(diào)試要點(diǎn)在真實(shí)林區(qū)布點(diǎn)位置比算法重要。我的經(jīng)驗是優(yōu)先選能俯瞰大片林地的高點(diǎn)比如鐵塔、瞭望塔、山頂基站視野越開闊早期煙點(diǎn)的可見窗囗越長。避免把攝像頭正對著西邊日落時強(qiáng)光會讓整個畫面過曝除非你的相機(jī)有寬動態(tài)范圍并且模型見過足夠多這類樣本。安裝時把鏡頭稍微向下傾斜減少天空占比天空既是誤報大戶也會拉低有效檢測像素。現(xiàn)場調(diào)試建議帶一臺筆記本直接連相機(jī)用實(shí)際的推理服務(wù)跑幾段真實(shí)錄像把誤報和漏報都記錄下來當(dāng)場調(diào)整。別指望在辦公室用網(wǎng)上找的視頻調(diào)好參數(shù)到現(xiàn)場就能用我這么干過結(jié)果到了現(xiàn)場發(fā)現(xiàn)風(fēng)向、地形、植被顏色全不一樣參數(shù)全部重調(diào)。8.2 模型迭代與數(shù)據(jù)回流機(jī)制系統(tǒng)上線不是終點(diǎn)而是數(shù)據(jù)積累的起點(diǎn)。每一張告警截圖都應(yīng)該有處置反饋是真火還是誤報這個標(biāo)簽極其寶貴。我把這些反饋?zhàn)龀蓴?shù)據(jù)回流池定期人工復(fù)核后加入訓(xùn)練集每隔一段時間重訓(xùn)一版模型。這樣模型會越來越貼合自己的點(diǎn)位場景誤報率會持續(xù)下降。這個閉環(huán)是這套系統(tǒng)真正的長期價值所在——預(yù)訓(xùn)練模型只是個起點(diǎn)你自己的數(shù)據(jù)才是護(hù)城河。模型更新要有灰度機(jī)制新模型先在部分點(diǎn)位試跑和舊模型并行一段時間對比告警質(zhì)量和數(shù)量確認(rèn)沒有惡化再全量切換。切換時保留舊版本可以隨時回滾別一上來就全換。8.3 值班流程與告警閉環(huán)設(shè)計技術(shù)之外流程同樣重要。告警觸發(fā)后要有明確的確認(rèn)、處置、歸檔流程系統(tǒng)里體現(xiàn)為告警狀態(tài)流轉(zhuǎn)待確認(rèn)、已確認(rèn)、處置中、已歸檔、誤報。每個狀態(tài)變更都要記錄操作人和時間事后可追溯。誤報的告警一定要標(biāo)記這既是數(shù)據(jù)回流的基礎(chǔ)也是評估系統(tǒng)真實(shí)表現(xiàn)的依據(jù)。我還建議加一個值班日志和統(tǒng)計分析模塊統(tǒng)計每周告警數(shù)量、誤報率、平均響應(yīng)時間、各點(diǎn)位活躍度。這些數(shù)據(jù)不僅能優(yōu)化系統(tǒng)也是向上匯報的有力材料。實(shí)際運(yùn)維中你會發(fā)現(xiàn)有些點(diǎn)位常年零告警可能是設(shè)備離線或者視野被樹枝擋住了有些點(diǎn)位天天告警可能是某個固定干擾源。統(tǒng)計面板能一眼看出這些異常比人工翻日志高效得多。最后分享一個我在長期運(yùn)行中總結(jié)的小技巧給每個點(diǎn)位單獨(dú)維護(hù)一份閾值配置而不是全系統(tǒng)統(tǒng)一。不同點(diǎn)位的場景差異太大統(tǒng)一閾值必然有一半點(diǎn)位是次優(yōu)的。用系統(tǒng)里的模型版本實(shí)體擴(kuò)展一個點(diǎn)位配置表存每個點(diǎn)位的置信度閾值、區(qū)域掩碼、告警確認(rèn)幀數(shù)前端的模型管理頁面提供可視化配置入口。這個功能上線后我們整體的誤報率下降了將近四成運(yùn)維工作量明顯減輕。這套系統(tǒng)后來還能繼續(xù)往外擴(kuò)的方向是把單幀檢測升級成短時序的視頻理解比如用輕量的時序模型去判斷煙霧的擴(kuò)散方向再結(jié)合地形數(shù)據(jù)預(yù)測可能的蔓延路徑——不過這已經(jīng)是另一個話題了等有實(shí)際進(jìn)展再單獨(dú)寫。