
簡介基于YOLOv8的森林火災早期煙霧預警項目面向計算機視覺、深度學習方向的畢設與課程設計場景適合計科、人工智能、自動化等專業學生作為完整實踐參考。包內集成源碼、可視化界面、完整數據集和部署說明一并提供核心指標曲線、混淆矩陣、F1分數曲線、PR曲線、驗證集預測結果及標簽分布圖可直接支撐論文圖表與答辯展示。資源共8個文件主要包括3個Python腳本模型訓練、視頻檢測、可視化頁面、3個模型權重文件yolov8n、best、yolo11n以及2個說明文檔壓縮包15.91MB結構精簡、即下即用。已有53人學習瀏覽代碼經測試運行成功對于希望快速搭建森林火災預警演示系統的讀者能節省大量調試時間拿來即可二次開發。1. 為什么煙霧比明火更難預警YOLOv8 在森林火災早期發現里的定位森林火災預警最值錢的時間窗口在煙霧階段明火出現時往往已經錯過了最佳處置時機。但煙霧恰好是最不適合目標檢測的目標半透明、缺少硬邊緣顏色與云、霧、山間水汽重疊晴天高光還會讓煙柱在幾百毫秒內從清晰變成不可見。這也是為什么這類畢設、課設項目普遍選擇 YOLOv8——它生態完善、權重文件不大、訓練到部署的鏈路短配合 PyQt5 或 Web 界面就能在幾天內包出一套能演示的預警系統。對要交付課程設計或畢業設計的人難點從來不只在訓練精度而在于把數據、訓練、界面和誤報控制串成一條可復現的路徑。下面按這條路徑展開先立住數據基礎再講訓練驗證最后落到部署和壓誤報。2. 訓練自己的煙霧數據集標注格式、目錄劃分與 data.yaml 配置監控視頻里的早期煙霧目標通常很小標注質量直接決定最終效果。訓練開始前先要確認數據以 YOLOv8 要求的格式存在數據集根目錄下并列 images 和 labels 兩個目錄train 與 val 里圖像和標簽一一對應每個 txt 與同名圖片對應每行是class x_center y_center width height四個坐標都相對圖片寬高做 0 到 1 歸一化。很多第一次跑的人在這里出錯從標注軟件導出的是 VOC 的 xml 或 COCO 的 json沒有轉成 txt或者是標注時把整棵樹冠框進去導致模型學習到的不是煙霧輪廓而是大片背景。早期煙霧檢測的標注原則是框要貼住煙團主體寧可多標幾個小框也不要擴展到大片天空。2.1 數據來源與負樣本比例不能只收集“好看的煙”公開的 smoke detection、森林火災檢測類數據集里晴天藍天背景的白煙占絕大多數。用這類數據訓出來的模型遇到陰天晨霧、山體陰影、逆光云縫就會瘋狂誤報。常見做法是拿一到兩個場景較豐富的公開煙霧集做底再從自然監控錄像里截取幾百張完全沒有煙的林區畫面作為負樣本。負樣本不需要標注僅僅放在 images 的 train 和 val 目錄里模型會自動把它們歸為背景。正負樣本比例我一般控制在 3:1 到 5:1負樣本太少部署階段攝像頭稍微轉個角度模型就會把云和山影識別成煙。把樣本收集齊之后需要檢查是否有損壞圖片、空標簽、標簽文件缺失。空標簽在早期煙霧場景里屬于正常負樣本沒有標簽但如果正樣本里有空 txt說明標注遺漏這種樣本在訓練時會直接被當作背景模型會因此認為“這張圖沒有煙”對降低召回率影響很明顯需要補標或直接刪掉。2.2 目錄劃分與標簽格式檢查腳本我一般會先用腳本把數據集按 90% / 10% 拆成 train 和 val再掃一遍標簽把坐標越界、類別 ID 超范圍、字段數量不對的文件單獨列出來。下面的腳本可以直接改路徑運行使用 shutil 復制而不是移動文件保證原始標注數據不會被破壞。import os import random import shutil from pathlib import Path SRC_IMG Path(datasets/images_all) SRC_LAB Path(datasets/labels_all) DST Path(datasets) VAL_RATIO 0.1 random.seed(42) for split in (train, val): # 創建 images 與 labels 的子目錄 (DST / images / split).mkdir(parentsTrue, exist_okTrue) (DST / labels / split).mkdir(parentsTrue, exist_okTrue) imgs sorted(SRC_IMG.glob(*.jpg)) sorted(SRC_IMG.glob(*.png)) random.shuffle(imgs) val_n max(1, int(len(imgs) * VAL_RATIO)) for i, img_path in enumerate(imgs): split val if i val_n else train label_path SRC_LAB / (img_path.stem .txt) if not label_path.exists(): # 缺標簽時打印警告便于定位漏標樣本 print(f[WARN] label not found: {label_path}) continue shutil.copy(img_path, DST / images / split / img_path.name) shutil.copy(label_path, DST / labels / split / label_path.name)這段腳本里VAL_RATIO 0.1表示驗證集占 10%數據集在一兩千張級別時這個比例夠用樣本再多可以用 8:2。隨機種子固定為 42 是為了讓每次劃分完全一致訓練結果可以橫向對比如果打印出來的 WARN 太多說明源頭數據缺標注不要直接訓練回到標注階段補齊。劃分之后還要做一次標簽內容檢查重點看坐標是否越界、類別 ID 是否從 0 開始連續排列。單類別煙霧項目里類別 ID 只有 0如果項目同時檢測 smoke 和 fire則 0 是 smoke1 是 fire類別映射需要與 data.yaml 保持一致。def check_label(file, class_num1): errors [] with open(file, encodingutf-8) as f: for line in f: parts line.split() if len(parts) ! 5: errors.append((field, line)) continue cls, xc, yc, w, h parts xc, yc, w, h map(float, (xc, yc, w, h)) if int(cls) class_num: errors.append((class, line)) if not (0 xc 1 and 0 yc 1 and 0 w 1 and 0 h 1): errors.append((bbox, line)) return errorsclass_num是類別總數單類煙霧寫 1遇到報錯行可以直接輸出文件名和內容定位。坐標檢查這一步很容易被跳過尤其是用半自動標注工具時偶爾會出現框超出圖片邊界歸一化后 w 或 h 大于 1 的數據YOLOv8 的訓練過程不會直接報錯但 mAP 會被異常框拉低且問題非常隱蔽。2.3 data.yaml 與訓前必改的 4 個參數數據格式就緒后寫 data.yaml 是訓練前最后一步。它的作用是把數據根目錄、子目錄、類別名告訴訓練入口。path: D:/datasets/smoke_data # 數據集根目錄用絕對路徑 train: images/train # 相對 path 的訓練圖像目錄 val: images/val # 相對 path 的驗證圖像目錄 names: 0: smoke # 類別 ID 從 0 開始 # 1: fire # 如果有明火類別取消注釋并保證標注一致這里最容易踩坑的 4 個點第一path必須是絕對路徑且不要包含中文和空格Windows 下中文路徑在 PyTorch DataLoader 中偶爾會產生編碼報錯排查起來很花時間第二train和val是相對path的子路徑不要寫絕對路徑也不要寫./images/train這種帶當前目錄標識的寫法第三names的順序必須和標簽文件里的類別 ID 完全對應容量不大時經常出現標注軟件導出 ID 從 1 開始的情況第四如果 Windows 下訓練時報共享內存或 worker 相關錯誤在訓練參數里加workers2并在第一次運行時觀察 cache 是否正常生成。提示訓練前先跑一條yolo detect train datasmoke.yaml modelyolov8s.pt epochs1作為 smoke test。它會啟動訓練流程但立刻結束如果 10 秒內沒有報錯說明數據目錄、YAML 格式、依賴庫都是通的再跑正式訓練。3. 訓練與驗證C2f 結構特點、訓練命令與損失曲線判斷數據準備好之后訓練階段的核心不是把命令跑起來而是知道每個參數在改什么以及訓練結束后如何從曲線里判斷模型狀態。這一章從 YOLOv8 backbone 的 C2f 結構講起給出適配普通顯卡的訓練參數組合最后落到 loss 曲線讀取。3.1 YOLOv8 的 C2f 結構對煙霧目標提取的影響YOLOv8 在 backbone 中用 C2f 替換了之前的 C3 模塊。C3 是 split 后走一條 Bottleneck 支路再 concatC2f 則讓輸入經過多個 Bottleneck 后把不同層級的輸出拼到一起再做跨階段融合。這意味著梯度可以沿多條支路回傳淺層紋理信息能更完整地保留到后續層。煙霧這種目標紋理弱、邊緣模糊靠的是“灰白漸變區域”的整體上下文C2f 對這種低頻特征的保留能力比 C3 有明顯改善。但如果漏檢集中在極遠處的細煙柱不要指望改 C2f 結構就能解決。小目標問題在 YOLOv8 上更多受限于輸入分辨率和特征金字塔的 stride。常見做法是把 imgsz 從 640 提到 896或者給檢測頭加一個 stride4 的 P2 輸出層再激進一點可以引入注意力模塊比如在 backbone 輸出后接一個輕量的通道注意力層。做這些改進前先跑出基線用同一份驗證集對比否則無法判斷改動是否有效。3.2 用命令行跑通訓練GTX 1660 Ti 也能用的參數組合訓練煙霧檢測推薦從 yolov8s 開始不要直接上 yolov8x。煙霧目標相對單一類數少yolov8s 的容量足夠擬合幾千張數據顯存占用也友好。命令行示例如下yolo detect train \ modelyolov8s.pt \ datasmoke.yaml \ imgsz640 \ epochs100 \ batch8 \ device0 \ patience20 \ workers2 \ ampTrue \ projectruns/smoke \ nametrain參數作用GTX 1660 Ti 6G 推薦值model預訓練權重啟動速度快yolov8s.ptimgsz訓練輸入分辨率越高小目標越好640顯存夠可試 896epochs最大訓練輪數100配合早停足夠batch每輪迭代的圖片數8顯存不足改 4patience驗證損失連續多少輪不降就停20workers數據加載線程數2Windows 下太高易卡死amp混合精度訓練TruedeviceGPU 編號CPU 用 -10這段參數組合的核心邏輯是用預訓練權重做遷移學習epochs 不需要太大靠 patience 早停自動收斂batch 優先保證能放進顯存gtx1660ti 6G 上 batch8 加 imgsz640 是穩定組合out of memory 就降到 4不要為了湊 batch 去關 amp。訓練完成后best.pt 和 last.pt 會存放在runs/smoke/train/weights/下best.pt 是驗證集指標最好的權重部署時只用它。訓練中途如果想看當前效果可以另開一個終端對單張圖片做推理yolo detect predict \ modelruns/smoke/train/weights/best.pt \ sourcetest_images/smoke_001.jpg \ conf0.25這個小命令不需要重新加載訓練環境模型還在跑也能執行適合在訓練過程中抽查漏檢情況。3.3 從 results.csv 畫損失函數曲線圖判斷過擬合Ultralytics 在訓練目錄下會自動生成 results.csv每一行是一個 epoch 的各項指標。直接用 pandas 和 matplotlib 讀取即可不需要自己寫訓練日志。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/smoke/train/results.csv) df.columns [col.strip() for col in df.columns] # 對 train loss 做 5 幀滑動平均曲線更平滑 for col in [train/box_loss, train/cls_loss]: df[col _smooth] df[col].rolling(5).mean() plt.figure(figsize(10, 6)) plt.plot(df[epoch], df[train/box_loss_smooth], labeltrain/box_loss) plt.plot(df[epoch], df[val/box_loss], labelval/box_loss) plt.plot(df[epoch], df[train/cls_loss_smooth], labeltrain/cls_loss) plt.plot(df[epoch], df[val/cls_loss], labelval/cls_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True) plt.show()注意 train loss 是最后一個 batch 的瞬時值抖動很大做滑動平均再看趨勢。判斷要點train loss 持續下降而 val loss 在第 20 輪左右開始回升屬于典型過擬合可以把 epochs 降到 60 或把 patience 從 20 調到 10cls_loss 降不下去而 box_loss 正常優先懷疑負樣本不足補充云、霧、樹影圖片后重新訓練val loss 和 train loss 同步下降但 mAP50 不漲問題出在驗證集標注質量上回去檢查標簽。訓練目錄下的 confusion_matrix.png 也要看重點看背景類被預測為 smoke 的比例。如果這個值高于 5%說明誤報風險高部署時不能直接用默認 conf0.25。4. 可視化界面與部署用 PyQt5 封裝 best.pt 跑通圖片、視頻、攝像頭訓練完成后進入交付階段。畢設場景里的“可視化界面”有兩個常見路線Gradio Web 界面和 PyQt5 桌面界面。Gradio 啟動快、三行代碼就能用瀏覽器訪問但做監控預警的觀感不如桌面程序且攝像頭輸入要走額外橋接。PyQt5 的優勢是畫面渲染、檢測畫框、狀態報警都在一個進程里邏輯直觀部署依賴也簡單。下面按 PyQt5 路線給出工程結構。4.1 項目結構與推理引擎封裝一個最小可用的部署項目包含四個文件main.py 負責啟動界面engine.py 封裝模型推理weights/best.pt 放訓練產物requirements.txt 固定依賴。推理引擎的核心是把單幀圖像傳進模型返回邊界框和置信度不摻任何界面邏輯。from ultralytics import YOLO import numpy as np class SmokeEngine: def __init__(self, weightsweights/best.pt, conf0.25, iou0.6, imgsz640): self.model YOLO(weights) self.conf conf self.iou iou self.imgsz imgsz def predict(self, frame): # frame 是 OpenCV 讀出來的 BGR ndarray results self.model.predict( frame, imgszself.imgsz, confself.conf, iouself.iou, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy() scores results.boxes.conf.cpu().numpy() return boxes, scores這里 conf 和 iou 做成實例屬性是為了讓界面上的閾值滑塊可以直接改engine.conf下一幀立即生效。iou0.6是 NMS 閾值煙霧檢測場景目標重疊少一般不需要動。conf 在訓練時常用 0.25部署階段建議提到 0.4 以上云層干擾會明顯減少。4.2 PyQt5 主窗口畫框、置信度與報警狀態界面部分的核心是 QTimer 周期性讀取畫面送入推理引擎再把檢測結果畫回圖像。下面的代碼是主窗口里的核心槽函數按鈕和布局邏輯按常規 PyQt5 寫法補充即可。import cv2 from PyQt5.QtCore import QTimer from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow from engine import SmokeEngine class SmokeUI(QMainWindow): def __init__(self): super().__init__() self.engine SmokeEngine(conf0.40, iou0.6) self.cap None self.timer QTimer(self) self.timer.timeout.connect(self.process_frame) def start_camera(self, index0): self.cap cv2.VideoCapture(index) self.timer.start(33) # 約 30 FPS 的取幀間隔 def process_frame(self): ok, frame self.cap.read() if not ok: return boxes, scores self.engine.predict(frame) alert False for box, score in zip(boxes, scores): if score self.engine.conf: alert True x1, y1, x2, y2 box.astype(int) color (0, 0, 255) if alert else (0, 165, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, fsmoke {score:.2f}, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) self.set_alert_light(alert) self.show_frame(frame)QTimer 的 33ms 只是取幀間隔實際幀率取決于推理耗時。1660 Ti 上跑 yolov8s 大約 20 到 40 FPS界面刷新會自動跟隨推理速度不需要額外做線程同步。要是出現界面卡頓優先把cv2.putText和畫框邏輯移到推理前的原圖上或者降低imgsz到 480 試跑。報警狀態這里只做了單幀判斷下一章會加入幀間投票來壓低誤報。4.3 部署清單與一鍵啟動requirements.txt 建議寫成ultralytics8.0.0 PyQt55.15 opencv-python4.8啟動流程是三條命令。conda 環境用 Python 3.10兼容性和第三方庫支持都比 3.12 穩妥。conda create -n smoke python3.10 -y conda activate smoke pip install -r requirements.txt python main.py沒有 N 卡的機器也能跑CPU 推理單幀會到 500ms 以上演示圖片和視頻足夠但攝像頭實時預覽會不流暢。要把模型部署到 RK3588 這類邊緣設備需要先轉 ONNX 再用板端推理框架這就不是畢設演示范疇的事了。5. 誤報抑制與驗收幀間投票、ROI 蒙版與置信度閾值掃描訓練集以晴天白煙為主時部署后最常見的假報警來自云縫光斑、晨霧和山體陰影。這三類干擾在單幀檢測里幾乎無法和煙霧區分可靠的做法是加時間約束和空間約束。5.1 幀間投票 ROI 蒙版連續命中才報警單幀置信度高的誤檢通常是隨機出現的而真實煙霧會在畫面中連續存在幾十秒到幾分鐘。用一個固定長度的隊列記錄最近若干幀是否檢測到煙只有投票數達到閾值才觸發報警同時用 ROI 蒙版把檢測范圍限制在山脊上空的重點區域。from collections import deque class TemporalAlert: def __init__(self, window5, vote3, min_score0.45, roiNone): self.window window self.vote vote self.min_score min_score self.roi roi # 二值蒙版檢測框中心落在蒙版內才生效 self.history deque(maxlenwindow) def update(self, detections): # detections: [(score, (cx, cy)), ...] hit False for score, (cx, cy) in detections: if score self.min_score: continue if self.roi is not None and self.roi[cy, cx] 0: continue hit True break self.history.append(1 if hit else 0) return sum(self.history) self.votewindow5、vote3 表示最近 5 幀里至少有 3 幀檢測到煙霧才觸發報警單個幀的隨機誤報最多貢獻 1 票無法觸發。真實煙霧持續存在最多延遲 2 幀報警對人眼幾乎無感。釋放報警也有延遲煙霧消散后需要再連續 2 幀無檢測才會釋放這個滯后特性對監控場景是合理的。ROI 蒙版用np.zeros建畫布再用cv2.fillPoly畫一個覆蓋山頂區域的多邊形區域外的云影直接不參與投票誤報率能再降一截。要特別注意如果不用 ROI窗口投票會被不同位置先后出現的誤檢累積票數此時應改為按檢測框中心距離做目標關聯只對同一位置的連續命中累計。5.2 在驗證集上掃描置信度閾值調好時間維度后還需要確定 conf 值。訓練默認的 0.25 是目標檢測通用值不等于這個數據集的部署值。做法是對驗證集跑一個循環conf 從 0.05 到 0.90 按 0.05 步進統計每次的 precision、recall 和 F1選 F1 最大時的 conf 作為部署參數。這個流程不需要重新訓練幾分鐘就能跑完。實際項目中我的默認組合是 conf0.45、window5、vote3再用 ROI 覆蓋重點監控區域。最后用一段完全沒有參與訓練的霧天錄像做回歸測試統計誤報次數和煙霧漏報情況一小時內誤報事件不超過兩次報警能在煙霧可見后 5 秒內觸發這套預警系統才算真正驗收通過。本文還有配套的精品資源點擊獲取