从零构建红外森林火灾检测系统:YOLOv12实战与UI开发全解析
最近在做一个森林防火相关的项目,客户要求系统能够通过红外摄像头实时监测火情,并且要有直观的操作界面给值班人员使用。市面上虽然有不少现成的解决方案,但要么价格昂贵,要么定制化程度不够。经过一番调研,我决定基于YOLOv12自己搭建一套完整的检测系统。这个过程中踩了不少坑,也积累了一些经验,今天就把整个实现过程详细分享出来,包括环境配置、模型训练、UI开发等各个环节的实战细节。
如果你正在寻找一个能够快速上手的森林火灾检测方案,或者想了解如何将深度学习模型与实际应用场景结合,这篇文章应该能给你提供不少实用的参考。我会从最基础的环境搭建开始,一步步带你完成整个系统的构建,并提供完整的Python源码。
1. 环境配置与项目初始化
1.1 选择合适的开发环境
在开始之前,我们需要搭建一个稳定可靠的开发环境。我强烈建议使用虚拟环境来管理项目依赖,这样可以避免不同项目之间的包版本冲突。
首先安装Miniconda或Anaconda,然后创建一个专门用于本项目的虚拟环境:
# 创建Python 3.9的虚拟环境
conda create -n fire_detection python=3.9 -y
# 激活环境
conda activate fire_detection
为什么选择Python 3.9而不是最新版本?在实际项目中我发现,很多深度学习框架和库在3.9上的兼容性最好,特别是涉及到一些较老的硬件驱动时。如果你使用的是较新的GPU,可以考虑使用Python 3.10或3.11。
1.2 安装核心依赖包
接下来安装项目所需的核心依赖。我整理了一个requirements.txt文件,包含了所有必要的包:
torch==2.1.0
torchvision==0.16.0
ultralytics==8.0.196
opencv-python==4.8.1.78
PyQt5==5.15.9
numpy==1.24.3
pandas==2.0.3
matplotlib==3.7.2
pillow==10.0.0
scikit-learn==1.3.0
tqdm==4.65.0
安装命令很简单:
pip install -r requirements.txt
这里有几个需要注意的地方:
- PyTorch版本:我使用的是2.1.0版本,如果你需要CUDA支持,可以根据自己的显卡型号选择合适的版本。可以通过PyTorch官网的安装命令生成器获取正确的安装命令。
- Ultralytics:这是YOLOv12的官方实现库,版本8.0.196是目前比较稳定的版本。
- OpenCV:4.8.1版本在图像处理和视频处理方面表现稳定,兼容性也很好。
1.3 验证环境配置
安装完成后,我们可以写一个简单的脚本来验证环境是否配置正确:
import torch
import cv2
from PyQt5.QtWidgets import QApplication
import sys
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
print(f"CUDA版本: {torch.version.cuda if torch.cuda.is_available() else 'N/A'}")
print(f"OpenCV版本: {cv2.__version__}")
# 测试PyQt5
app = QApplication(sys.argv)
print("PyQt5环境正常")
如果一切正常,你应该能看到各个库的版本信息,并且没有报错。特别要注意CUDA是否可用,这直接影响到模型训练和推理的速度。
2. 数据集准备与预处理
2.1 理解红外火灾检测数据集的特点
红外图像与普通RGB图像有很大不同。在红外图像中,温度高的物体(如火焰)会显示为亮色,而背景通常是暗色。这种特性使得红外图像在火灾检测中具有独特优势:
- 不受光照影响:无论是白天还是夜晚,红外相机都能正常工作
- 穿透烟雾能力强:红外线可以部分穿透烟雾,这在森林火灾早期检测中特别重要
- 温度信息直观:可以直接通过亮度判断温度高低
我们的数据集包含2000张红外图像,分为训练集(1600张)、验证集(200张)和测试集(200张)。每张图像都标注了火焰(fire)和烟雾(smoke)两类目标。
2.2 数据集目录结构
正确的目录结构对于YOLO训练至关重要。我建议按照以下方式组织数据:
forest_fire_dataset/
├── images/
│ ├── train/ # 训练图像
│ ├── val/ # 验证图像
│ └── test/ # 测试图像
└── labels/
├── train/ # 训练标签
├── val/ # 验证标签
└── test/ # 测试标签
每个标签文件(.txt)的格式如下:
0 0.512 0.634 0.124 0.089 # 类别索引 x_center y_center width height
1 0.234 0.456 0.067 0.123 # 另一个目标...
所有坐标都是归一化后的值(0-1之间),相对于图像的宽度和高度。
2.3 数据增强策略
红外火灾检测面临的一个挑战是数据量相对较少。为了提升模型的泛化能力,我们需要实施有效的数据增强策略。以下是我在实际项目中使用的增强方法:
import albumentations as A
from albumentations.pytorch import ToTensorV2
def get_train_transforms(image_size=640):
return A.Compose([
A.Resize(height=image_size, width=image_size),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.2),
A.RandomBrightnessContrast(p=0.3),
A.HueSaturationValue(p=0.3),
A.GaussNoise(p=0.2),
A.Blur(blur_limit=3, p=0.2),
A.CLAHE(p=0.2),
A.RandomGamma(p=0.2),
ToTensorV2()
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
def get_val_transforms(image_size=640):
return A.Compose([
A.Resize(height=image_size, width=image_size),
ToTensorV2()
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
注意:对于红外图像,某些颜色相关的增强(如HueSaturationValue)需要谨慎使用,因为红外图像本身没有颜色信息。但在实际测试中,适度的颜色增强可以帮助模型学习到更鲁棒的特征。
2.4 创建数据集配置文件
YOLO需要一个YAML格式的配置文件来指定数据集信息:
# data.yaml
path: /path/to/forest_fire_dataset # 数据集根目录
train: images/train # 训练集相对路径
val: images/val # 验证集相对路径
test: images/test # 测试集相对路径
# 类别数量
nc: 2
# 类别名称
names: ['fire', 'smoke']
# 可选:下载命令/URL
# download: https://example.com/dataset.zip
这个文件是连接数据和模型的桥梁,确保路径设置正确非常重要。
3. YOLOv12模型训练实战
3.1 理解YOLOv12的架构改进
YOLOv12在YOLOv11的基础上做了多项重要改进,这些改进对于火灾检测任务特别有帮助:
骨干网络优化:
- 采用了更高效的CSPNet结构,减少了计算量
- 引入了注意力机制,让模型更关注火焰和烟雾区域
- 改进了特征金字塔网络,提升了小目标检测能力
检测头改进:
- 使用解耦头结构,分别处理分类和回归任务
- 引入了动态标签分配策略,提升了训练效率
- 改进了损失函数,对遮挡和模糊目标更鲁棒
推理优化:
- 支持TensorRT加速
- 提供了多种精度模式(FP32、FP16、INT8)
- 优化了后处理流程,提升了实时性
3.2 选择合适的预训练模型
YOLOv12提供了多个不同大小的模型,我们需要根据实际需求选择:
| 模型名称 | 参数量 | 计算量 (GFLOPs) | 适用场景 | 推理速度 (FPS) |
|---|---|---|---|---|
| YOLOv12n | 2.5M | 4.2 | 嵌入式设备,资源受限 | 120+ |
| YOLOv12s | 9.1M | 23.6 | 移动端应用,实时检测 | 80-100 |
| YOLOv12m | 25.3M | 65.2 | 服务器部署,平衡型 | 50-70 |
| YOLOv12l | 43.7M | 115.4 | 高精度要求场景 | 30-50 |
| YOLOv12x | 68.2M | 180.3 | 研究用途,最高精度 | 20-30 |
对于森林火灾检测,我推荐使用YOLOv12m或YOLOv12l。YOLOv12m在精度和速度之间取得了很好的平衡,而YOLOv12l在复杂场景下的检测精度更高。
3.3 训练参数配置
训练YOLOv12模型需要仔细调整超参数。以下是我经过多次实验得出的最优配置:
from ultralytics import YOLO
import yaml
def train_model():
# 加载预训练模型
model = YOLO('yolov12m.pt') # 使用中等大小的模型
# 训练参数配置
train_args = {
'data': 'data.yaml', # 数据集配置文件
'epochs': 150, # 训练轮数
'batch': 16, # 批次大小
'imgsz': 640, # 输入图像尺寸
'device': '0', # 使用GPU 0,如果是CPU则设为'cpu'
'workers': 8, # 数据加载线程数
'patience': 30, # 早停耐心值
'save_period': 10, # 每10轮保存一次检查点
'project': 'fire_detection', # 项目名称
'name': 'yolov12m_fire', # 实验名称
'exist_ok': True, # 允许覆盖现有目录
'pretrained': True, # 使用预训练权重
'optimizer': 'AdamW', # 优化器
'lr0': 0.001, # 初始学习率
'lrf': 0.01, # 最终学习率因子
'momentum': 0.937, # 动量
'weight_decay': 0.0005, # 权重衰减
'warmup_epochs': 3, # 热身轮数
'warmup_momentum': 0.8, # 热身动量
'box': 7.5, # 框损失权重
'cls': 0.5, # 分类损失权重
'dfl': 1.5, # DFL损失权重
'fl_gamma': 0.0, # Focal Loss gamma
'label_smoothing': 0.0, # 标签平滑
'nbs': 64, # 名义批次大小
'overlap_mask': True, # 训练时重叠掩码
'scale': 0.5, # 图像缩放比例
'mixup': 0.0, # MixUp增强
'copy_paste': 0.0, # 复制粘贴增强
}
# 开始训练
results = model.train(**train_args)
return results
if __name__ == '__main__':
# 验证数据集配置
with open('data.yaml', 'r') as f:
data_config = yaml.safe_load(f)
print(f"数据集配置: {data_config}")
# 开始训练
train_results = train_model()
print("训练完成!")
3.4 训练过程监控与调优
训练过程中需要密切关注几个关键指标:
损失函数变化:
- box_loss:边界框回归损失,应该稳步下降
- cls_loss:分类损失,反映模型区分火焰和烟雾的能力
- dfl_loss:分布焦点损失,YOLOv12特有的损失项
性能指标:
- mAP50:IoU阈值为0.5时的平均精度
- mAP50-95:IoU阈值从0.5到0.95的平均精度
- Precision:精确率,检测出的目标中真正是火焰/烟雾的比例
- Recall:召回率,所有真实目标中被检测出来的比例
我通常使用TensorBoard来监控训练过程:
# 启动TensorBoard
tensorboard --logdir fire_detection/
# 然后在浏览器中访问 http://localhost:6006
如果发现模型过拟合(训练损失持续下降但验证损失上升),可以尝试:
- 增加数据增强的强度
- 使用更小的模型
- 增加Dropout或权重衰减
- 提前停止训练
3.5 模型评估与测试
训练完成后,我们需要在测试集上评估模型性能:
def evaluate_model(model_path='fire_detection/yolov12m_fire/weights/best.pt'):
# 加载训练好的模型
model = YOLO(model_path)
# 在测试集上评估
metrics = model.val(
data='data.yaml',
split='test', # 使用测试集
imgsz=640,
batch=16,
conf=0.25, # 置信度阈值
iou=0.45, # IoU阈值
device='0',
half=False, # 是否使用半精度
plots=True, # 生成评估图表
save_json=True, # 保存JSON格式结果
save_hybrid=False, # 是否保存混合标签
)
# 打印关键指标
print(f"mAP50: {metrics.box.map50:.4f}")
print(f"mAP50-95: {metrics.box.map:.4f}")
print(f"Precision: {metrics.box.p:.4f}")
print(f"Recall: {metrics.box.r:.4f}")
# 保存评估结果
with open('evaluation_results.txt', 'w') as f:
f.write(f"模型: {model_path}\n")
f.write(f"评估时间: {metrics.speed['inference']:.2f} ms/img\n")
f.write(f"mAP50: {metrics.box.map50:.4f}\n")
f.write(f"mAP50-95: {metrics.box.map:.4f}\n")
f.write(f"Precision: {metrics.box.p:.4f}\n")
f.write(f"Recall: {metrics.box.r:.4f}\n")
return metrics
4. PyQt5界面开发详解
4.1 界面设计理念
一个好的检测系统不仅要有准确的算法,还要有友好的用户界面。我设计的界面遵循以下几个原则:
- 直观性:操作流程符合用户习惯,减少学习成本
- 实时性:检测结果和状态信息实时更新
- 美观性:采用深色主题,减少长时间使用的视觉疲劳
- 功能性:提供完整的检测、配置、保存功能
4.2 主界面架构设计
主界面采用经典的左右布局:左侧是控制面板,右侧是显示区域。下面是核心的UI类结构:
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QSlider, QSpinBox, QComboBox,
QTableWidget, QTableWidgetItem, QGroupBox, QFileDialog,
QMessageBox, QStatusBar)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QImage, QPixmap, QFont, QColor
import cv2
import numpy as np
from ultralytics import YOLO
class FireDetectionUI(QMainWindow):
def __init__(self):
super().__init__()
self.model = None
self.detection_thread = None
self.current_mode = None # 'image', 'video', 'camera'
self.video_writer = None
self.is_detecting = False
self.init_ui()
self.load_default_model()
def init_ui(self):
# 设置窗口属性
self.setWindowTitle("红外森林火灾检测系统")
self.setGeometry(100, 100, 1400, 800)
# 设置深色主题
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QGroupBox {
font-size: 14px;
font-weight: bold;
border: 2px solid #3a3a3a;
border-radius: 8px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #4fc3f7;
}
""")
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QHBoxLayout(central_widget)
# 左侧控制面板
control_panel = self.create_control_panel()
main_layout.addWidget(control_panel, 1) # 1份宽度
# 右侧显示区域
display_panel = self.create_display_panel()
main_layout.addWidget(display_panel, 2) # 2份宽度
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.update_status("系统就绪")
def create_control_panel(self):
# 控制面板容器
panel = QWidget()
layout = QVBoxLayout(panel)
# 模型选择组
model_group = QGroupBox("模型配置")
model_layout = QVBoxLayout()
self.model_combo = QComboBox()
self.model_combo.addItems(['yolov12n', 'yolov12s', 'yolov12m', 'yolov12l', 'yolov12x'])
self.model_combo.setCurrentText('yolov12m')
self.model_combo.currentTextChanged.connect(self.on_model_changed)
model_layout.addWidget(QLabel("选择模型:"))
model_layout.addWidget(self.model_combo)
model_group.setLayout(model_layout)
layout.addWidget(model_group)
# 检测模式组
mode_group = QGroupBox("检测模式")
mode_layout = QVBoxLayout()
self.image_btn = self.create_styled_button("图片检测", "#4CAF50")
self.video_btn = self.create_styled_button("视频检测", "#2196F3")
self.camera_btn = self.create_styled_button("实时摄像头", "#FF9800")
self.stop_btn = self.create_styled_button("停止检测", "#F44336")
self.image_btn.clicked.connect(self.on_image_detect)
self.video_btn.clicked.connect(self.on_video_detect)
self.camera_btn.clicked.connect(self.on_camera_detect)
self.stop_btn.clicked.connect(self.on_stop_detect)
mode_layout.addWidget(self.image_btn)
mode_layout.addWidget(self.video_btn)
mode_layout.addWidget(self.camera_btn)
mode_layout.addWidget(self.stop_btn)
mode_group.setLayout(mode_layout)
layout.addWidget(mode_group)
# 参数配置组
param_group = QGroupBox("检测参数")
param_layout = QVBoxLayout()
# 置信度阈值
conf_layout = QHBoxLayout()
conf_layout.addWidget(QLabel("置信度阈值:"))
self.conf_slider = QSlider(Qt.Horizontal)
self.conf_slider.setRange(0, 100)
self.conf_slider.setValue(25) # 默认0.25
self.conf_spinbox = QSpinBox()
self.conf_spinbox.setRange(0, 100)
self.conf_spinbox.setValue(25)
self.conf_spinbox.setSuffix("%")
self.conf_slider.valueChanged.connect(
lambda v: self.conf_spinbox.setValue(v))
self.conf_spinbox.valueChanged.connect(
lambda v: self.conf_slider.setValue(v))
conf_layout.addWidget(self.conf_slider)
conf_layout.addWidget(self.conf_spinbox)
param_layout.addLayout(conf_layout)
# IoU阈值
iou_layout = QHBoxLayout()
iou_layout.addWidget(QLabel("IoU阈值:"))
self.iou_slider = QSlider(Qt.Horizontal)
self.iou_slider.setRange(0, 100)
self.iou_slider.setValue(45) # 默认0.45
self.iou_spinbox = QSpinBox()
self.iou_spinbox.setRange(0, 100)
self.iou_spinbox.setValue(45)
self.iou_spinbox.setSuffix("%")
self.iou_slider.valueChanged.connect(
lambda v: self.iou_spinbox.setValue(v))
self.iou_spinbox.valueChanged.connect(
lambda v: self.iou_slider.setValue(v))
iou_layout.addWidget(self.iou_slider)
iou_layout.addWidget(self.iou_spinbox)
param_layout.addLayout(iou_layout)
param_group.setLayout(param_layout)
layout.addWidget(param_group)
# 结果保存组
save_group = QGroupBox("结果保存")
save_layout = QVBoxLayout()
self.save_btn = self.create_styled_button("保存当前结果", "#9C27B0")
self.save_btn.clicked.connect(self.on_save_result)
save_layout.addWidget(self.save_btn)
self.auto_save_check = QCheckBox("自动保存检测结果")
self.auto_save_check.setChecked(True)
save_layout.addWidget(self.auto_save_check)
save_group.setLayout(save_layout)
layout.addWidget(save_group)
# 添加弹性空间
layout.addStretch()
return panel
4.3 多线程检测实现
为了保证UI的流畅性,检测任务必须在单独的线程中运行。我设计了一个DetectionThread类来处理所有的检测逻辑:
class DetectionThread(QThread):
# 定义信号
frame_processed = pyqtSignal(np.ndarray, np.ndarray, list, float)
detection_finished = pyqtSignal()
error_occurred = pyqtSignal(str)
def __init__(self, model, source, conf_thresh=0.25, iou_thresh=0.45):
super().__init__()
self.model = model
self.source = source
self.conf_thresh = conf_thresh
self.iou_thresh = iou_thresh
self.is_running = True
self.is_video = isinstance(source, str) and source.endswith(('.mp4', '.avi', '.mov', '.mkv'))
self.is_camera = isinstance(source, int)
def run(self):
try:
if self.is_video or self.is_camera:
self.process_video_stream()
else:
self.process_single_image()
except Exception as e:
self.error_occurred.emit(f"检测错误: {str(e)}")
finally:
self.detection_finished.emit()
def process_video_stream(self):
"""处理视频流或摄像头流"""
cap = cv2.VideoCapture(self.source)
if not cap.isOpened():
self.error_occurred.emit("无法打开视频源")
return
# 获取视频属性
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 计算处理间隔,保持实时性
process_interval = max(1, int(fps / 30)) if fps > 0 else 1
frame_idx = 0
while self.is_running and cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 跳帧处理,保持性能
if frame_idx % process_interval != 0:
frame_idx += 1
continue
# 记录开始时间
start_time = time.time()
# 执行检测
results = self.model(
frame,
conf=self.conf_thresh,
iou=self.iou_thresh,
verbose=False
)
# 处理检测结果
processed_frame = results[0].plot()
detections = self.extract_detections(results[0])
# 计算处理时间
process_time = (time.time() - start_time) * 1000 # 毫秒
# 发射信号
self.frame_processed.emit(
frame,
processed_frame,
detections,
process_time
)
frame_idx += 1
# 控制处理速度
if fps > 0:
time.sleep(1.0 / fps)
else:
time.sleep(0.033) # 默认30fps
cap.release()
def process_single_image(self):
"""处理单张图片"""
image = cv2.imread(self.source)
if image is None:
self.error_occurred.emit("无法读取图片")
return
start_time = time.time()
results = self.model(
image,
conf=self.conf_thresh,
iou=self.iou_thresh,
verbose=False
)
processed_image = results[0].plot()
detections = self.extract_detections(results[0])
process_time = (time.time() - start_time) * 1000
self.frame_processed.emit(
image,
processed_image,
detections,
process_time
)
def extract_detections(self, result):
"""从检测结果中提取详细信息"""
detections = []
if result.boxes is not None:
for box in result.boxes:
# 获取边界框坐标
xyxy = box.xyxy[0].cpu().numpy()
xywh = box.xywh[0].cpu().numpy()
# 获取类别和置信度
cls_id = int(box.cls[0])
conf = float(box.conf[0])
cls_name = result.names[cls_id]
detections.append({
'class': cls_name,
'confidence': conf,
'bbox': {
'x1': float(xyxy[0]),
'y1': float(xyxy[1]),
'x2': float(xyxy[2]),
'y2': float(xyxy[3])
},
'center': {
'x': float(xywh[0]),
'y': float(xywh[1])
},
'size': {
'width': float(xywh[2]),
'height': float(xywh[3])
}
})
return detections
def stop(self):
"""停止检测"""
self.is_running = False
4.4 实时结果显示与交互
显示区域需要同时展示原始图像和检测结果,并提供详细的数据表格:
def create_display_panel(self):
"""创建显示面板"""
panel = QWidget()
layout = QVBoxLayout(panel)
# 图像显示区域
display_group = QGroupBox("检测结果")
display_layout = QVBoxLayout()
# 双画面显示
image_layout = QHBoxLayout()
# 原始图像
self.original_label = QLabel()
self.original_label.setAlignment(Qt.AlignCenter)
self.original_label.setMinimumSize(640, 480)
self.original_label.setStyleSheet("border: 2px solid #3a3a3a;")
image_layout.addWidget(self.original_label)
# 检测结果图像
self.result_label = QLabel()
self.result_label.setAlignment(Qt.AlignCenter)
self.result_label.setMinimumSize(640, 480)
self.result_label.setStyleSheet("border: 2px solid #4fc3f7;")
image_layout.addWidget(self.result_label)
display_layout.addLayout(image_layout)
# 性能信息
info_layout = QHBoxLayout()
self.fps_label = QLabel("FPS: --")
self.fps_label.setStyleSheet("font-size: 12px; color: #4fc3f7;")
self.process_time_label = QLabel("处理时间: -- ms")
self.process_time_label.setStyleSheet("font-size: 12px; color: #4fc3f7;")
self.detection_count_label = QLabel("检测目标: 0")
self.detection_count_label.setStyleSheet("font-size: 12px; color: #4fc3f7;")
info_layout.addWidget(self.fps_label)
info_layout.addWidget(self.process_time_label)
info_layout.addWidget(self.detection_count_label)
info_layout.addStretch()
display_layout.addLayout(info_layout)
display_group.setLayout(display_layout)
layout.addWidget(display_group)
# 检测结果表格
table_group = QGroupBox("检测详情")
table_layout = QVBoxLayout()
self.results_table = QTableWidget()
self.results_table.setColumnCount(6)
self.results_table.setHorizontalHeaderLabels([
"序号", "类别", "置信度", "X坐标", "Y坐标", "尺寸"
])
# 设置表格样式
self.results_table.setStyleSheet("""
QTableWidget {
background-color: #2d2d2d;
color: #ffffff;
gridline-color: #3a3a3a;
}
QHeaderView::section {
background-color: #1a1a1a;
color: #4fc3f7;
padding: 5px;
border: 1px solid #3a3a3a;
}
QTableWidget::item {
padding: 5px;
}
""")
self.results_table.horizontalHeader().setStretchLastSection(True)
self.results_table.setAlternatingRowColors(True)
table_layout.addWidget(self.results_table)
table_group.setLayout(table_layout)
layout.addWidget(table_group)
return panel
def update_display(self, original_frame, result_frame, detections, process_time):
"""更新显示内容"""
# 更新图像显示
self.display_image(self.original_label, original_frame)
self.display_image(self.result_label, result_frame)
# 更新性能信息
fps = 1000.0 / process_time if process_time > 0 else 0
self.fps_label.setText(f"FPS: {fps:.1f}")
self.process_time_label.setText(f"处理时间: {process_time:.1f} ms")
self.detection_count_label.setText(f"检测目标: {len(detections)}")
# 更新结果表格
self.update_results_table(detections)
def display_image(self, label, image):
"""在QLabel中显示图像"""
if image is None:
return
# 转换颜色空间
if len(image.shape) == 3 and image.shape[2] == 3:
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
else:
rgb_image = image
# 调整图像大小以适应标签
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
# 创建QImage
qimage = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
# 创建QPixmap并缩放
pixmap = QPixmap.fromImage(qimage)
scaled_pixmap = pixmap.scaled(
label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
label.setPixmap(scaled_pixmap)
def update_results_table(self, detections):
"""更新检测结果表格"""
self.results_table.setRowCount(len(detections))
for i, detection in enumerate(detections):
# 序号
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
# 类别
class_item = QTableWidgetItem(detection['class'])
if detection['class'] == 'fire':
class_item.setForeground(QColor('#FF5252')) # 红色
else:
class_item.setForeground(QColor('#FF9800')) # 橙色
self.results_table.setItem(i, 1, class_item)
# 置信度
conf_item = QTableWidgetItem(f"{detection['confidence']:.3f}")
# 根据置信度设置颜色
if detection['confidence'] > 0.8:
conf_item.setForeground(QColor('#4CAF50')) # 绿色
elif detection['confidence'] > 0.5:
conf_item.setForeground(QColor('#FFC107')) # 黄色
else:
conf_item.setForeground(QColor('#FF9800')) # 橙色
self.results_table.setItem(i, 2, conf_item)
# 中心坐标
center = detection['center']
self.results_table.setItem(i, 3, QTableWidgetItem(f"{center['x']:.1f}"))
self.results_table.setItem(i, 4, QTableWidgetItem(f"{center['y']:.1f}"))
# 尺寸
size = detection['size']
self.results_table.setItem(i, 5, QTableWidgetItem(
f"{size['width']:.1f}×{size['height']:.1f}"
))
4.5 结果保存与导出
检测结果的保存功能对于后续分析和报告生成非常重要:
def on_save_result(self):
"""保存当前检测结果"""
if not hasattr(self, 'current_result') or self.current_result is None:
QMessageBox.warning(self, "警告", "没有可保存的检测结果")
return
# 选择保存路径
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存检测结果",
f"detection_result_{time.strftime('%Y%m%d_%H%M%S')}",
"图片文件 (*.jpg *.png);;所有文件 (*.*)"
)
if file_path:
try:
# 保存图像
cv2.imwrite(file_path, self.current_result)
# 保存检测数据
data_file = file_path.rsplit('.', 1)[0] + '.json'
detections_data = self.get_current_detections_data()
with open(data_file, 'w') as f:
json.dump(detections_data, f, indent=2)
# 保存表格数据
csv_file = file_path.rsplit('.', 1)[0] + '.csv'
self.save_table_to_csv(csv_file)
self.update_status(f"结果已保存: {os.path.basename(file_path)}")
QMessageBox.information(self, "成功", "检测结果保存完成")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
def save_table_to_csv(self, file_path):
"""将表格数据保存为CSV"""
import csv
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# 写入表头
headers = []
for col in range(self.results_table.columnCount()):
headers.append(self.results_table.horizontalHeaderItem(col).text())
writer.writerow(headers)
# 写入数据
for row in range(self.results_table.rowCount()):
row_data = []
for col in range(self.results_table.columnCount()):
item = self.results_table.item(row, col)
row_data.append(item.text() if item else "")
writer.writerow(row_data)
def get_current_detections_data(self):
"""获取当前检测数据的结构化表示"""
data = {
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
'model': self.model_combo.currentText(),
'parameters': {
'confidence_threshold': self.conf_spinbox.value() / 100.0,
'iou_threshold': self.iou_spinbox.value() / 100.0
},
'detections': []
}
for row in range(self.results_table.rowCount()):
detection = {
'id': int(self.results_table.item(row, 0).text()),
'class': self.results_table.item(row, 1).text(),
'confidence': float(self.results_table.item(row, 2).text()),
'center_x': float(self.results_table.item(row, 3).text()),
'center_y': float(self.results_table.item(row, 4).text()),
'size': self.results_table.item(row, 5).text()
}
data['detections'].append(detection)
return data
5. 系统部署与优化
5.1 性能优化技巧
在实际部署中,性能优化是至关重要的。以下是我总结的几个关键优化点:
模型优化:
def optimize_model_for_deployment(model_path):
"""优化模型用于部署"""
from ultralytics import YOLO
# 加载模型
model = YOLO(model_path)
# 导出为ONNX格式(推荐)
model.export(
format='onnx',
imgsz=640,
opset=12,
simplify=True,
dynamic=False, # 固定输入尺寸,性能更好
)
# 或者导出为TensorRT格式(如果使用NVIDIA GPU)
model.export(
format='engine',
imgsz=640,
device=0, # GPU设备
workspace=4, # GPU内存,单位GB
)
return model
推理优化:
- 使用半精度(FP16)推理,速度提升约2倍
- 启用TensorRT加速,进一步优化计算图
- 批量处理图像,提高GPU利用率
内存优化:
- 及时释放不再使用的张量
- 使用内存池管理大尺寸图像
- 限制同时处理的视频流数量
5.2 错误处理与日志记录
一个健壮的系统需要有完善的错误处理和日志记录机制:
import logging
from logging.handlers import RotatingFileHandler
def setup_logging():
"""配置日志系统"""
# 创建日志目录
log_dir = 'logs'
os.makedirs(log_dir, exist_ok=True)
# 配置根日志记录器
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# 文件处理器(按大小轮转)
file_handler = RotatingFileHandler(
os.path.join(log_dir, 'fire_detection.log'),
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
file_handler.setLevel(logging.INFO)
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(file_formatter)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_formatter = logging.Formatter(
'%(levelname)s: %(message)s'
)
console_handler.setFormatter(console_formatter)
# 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
class ErrorHandler:
"""统一的错误处理器"""
def __init__(self, logger):
self.logger = logger
def handle_detection_error(self, error, context=None):
"""处理检测错误"""
error_msg = f"检测错误: {str(error)}"
if context:
error_msg += f" | 上下文: {context}"
self.logger.error(error_msg)
# 根据错误类型采取不同措施
if "CUDA out of memory" in str(error):
return self.handle_memory_error()
elif "model not found" in str(error):
return self.handle_model_error()
else:
return self.handle_general_error(error_msg)
def handle_memory_error(self):
"""处理内存不足错误"""
self.logger.warning("GPU内存不足,尝试优化内存使用")
# 建议的优化措施
suggestions = [
"减小批次大小",
"降低输入图像分辨率",
"使用更小的模型",
"关闭其他占用GPU的程序"
]
return {
'success': False,
'error': '内存不足',
'suggestions': suggestions
}
5.3 系统监控与维护
对于长期运行的系统,监控和维护是必不可少的:
class SystemMonitor:
"""系统监控器"""
def __init__(self):
self.start_time = time.time()
self.detection_count = 0
self.error_count = 0
self.performance_stats = {
'avg_fps': 0,
'avg_process_time': 0,
'max_memory_usage': 0
}
def update_stats(self, process_time, detections_count):
"""更新性能统计"""
self.detection_count += 1
# 计算平均FPS
fps = 1000.0 / process_time if process_time > 0 else 0
self.performance_stats['avg_fps'] = (
self.performance_stats['avg_fps'] * (self.detection_count - 1) + fps
) / self.detection_count
# 计算平均处理时间
self.performance_stats['avg_process_time'] = (
self.performance_stats['avg_process_time'] * (self.detection_count - 1) + process_time
) / self.detection_count
# 监控内存使用
if hasattr(self, 'get_memory_usage'):
memory_usage = self.get_memory_usage()
self.performance_stats['max_memory_usage'] = max(
self.performance_stats['max_memory_usage'],
memory_usage
)
def get_system_report(self):
"""生成系统报告"""
uptime = time.time() - self.start_time
hours, remainder = divmod(uptime, 3600)
minutes, seconds = divmod(remainder, 60)
report = {
'系统运行时间': f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒",
'总检测次数': self.detection_count,
'错误次数': self.error_count,
'平均FPS': f"{self.performance_stats['avg_fps']:.1f}",
'平均处理时间': f"{self.performance_stats['avg_process_time']:.1f}ms",
'最大内存使用': f"{self.performance_stats['max_memory_usage']:.1f}MB",
'当前时间': time.strftime('%Y-%m-%d %H:%M:%S')
}
return report
def save_report(self, file_path):
"""保存报告到文件"""
report = self.get_system_report()
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
self.logger.info(f"系统报告已保存: {file_path}")
6. 实际应用案例与扩展
6.1 多摄像头监控系统
在实际的森林防火应用中,通常需要同时监控多个区域。我们可以扩展系统以支持多摄像头输入:
class MultiCameraManager:
"""多摄像头管理器"""
def __init__(self, camera_urls):
self.camera_urls = camera_urls
self.camera_threads = []
self.detection_results = {}
def start_all_cameras(self):
"""启动所有摄像头"""
for i, url in enumerate(self.camera_urls):
thread = CameraThread(i, url)
thread.frame_received.connect(
lambda frame, cam_id=i: self.on_camera_frame(frame, cam_id)
)
thread.start()
self.camera_threads.append(thread)
def on_camera_frame(self, frame, camera_id):
"""处理摄像头帧"""
# 执行检测
results = self.model(frame)
# 更新结果
self.detection_results[camera_id] = {
'frame': frame,
'results': results,
'timestamp': time.time()
}
# 检查是否有火灾
if self.check_fire_alarm(results):
self.trigger_alarm(camera_id, results)
def check_fire_alarm(self, results):
"""检查火灾警报"""
fire_detected = False
smoke_detected = False
for result in results:
for box in result.boxes:
cls_name = result.names[int(box.cls)]
conf = float(box.conf)
if cls_name == 'fire' and conf > 0.7:
fire_detected = True
elif cls_name == 'smoke' and conf > 0.6:
smoke_detected = True
# 警报条件:检测到火焰,或者同时检测到烟雾和高置信度
return fire_detected or (smoke_detected and len(results) > 2)
6.2 与地理信息系统集成
将检测结果与GIS系统结合,可以提供更直观的火灾位置信息:
class GISIntegration:
"""GIS系统集成"""
def __init__(self, camera_positions):
"""
camera_positions: 字典,键为摄像头ID,值为(经度, 纬度, 高度, 朝向, 视角)
"""
self.camera_positions = camera_positions
def calculate_fire_position(self, camera_id, detection_bbox):
"""根据检测框计算火灾位置"""
cam_info = self.camera_positions[camera_id]
lon, lat, alt, heading, fov = cam_info
# 将图像坐标转换为地理坐标
# 这里需要根据摄像头的内外参进行计算
# 实际实现中可能需要相机标定数据
# 简化版本:假设火灾在摄像头指向的方向上
fire_distance = self.estimate_distance(detection_bbox)
# 计算火灾位置
fire_lon, fire_lat = self.calculate_coordinates(
lon, lat, heading, fire_distance
)
return {
'longitude': fire_lon,
'latitude': fire_lat,
'altitude': alt,
'camera_id': camera_id,
'confidence': detection_bbox['confidence'],
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S')
}
def generate_heatmap(self, detections, time_window=3600):
"""生成热力图"""
# 收集指定时间窗口内的检测结果
recent_detections = [
d for d in detections
if time.time() - d['timestamp'] < time_window
]
if not recent_detections:
return None
# 创建热力图数据
heatmap_data = []
for detection in recent_detections:
weight = detection['confidence']
# 根据置信度调整权重
if detection['class'] == 'fire':
weight *= 1.5
heatmap_data.append({
'lon': detection['longitude'],
'lat': detection['latitude'],
'weight': weight
})
return self.create_heatmap_layer(heatmap_data)
6.3 移动端部署考虑
对于需要移动巡检的场景,我们可以考虑将模型部署到移动设备:
def prepare_for_mobile_deployment(model_path):
"""准备移动端部署"""
from ultralytics import YOLO
model = YOLO(model_path)
# 导出为TFLite格式(Android/iOS)
model.export(
format='tflite',
imgsz=320, # 移动端使用较小尺寸
int8=True, # 量化到INT8,减少模型大小
)
# 或者导出为CoreML格式(iOS)
model.export(
format='coreml',
imgsz=320,
nms=True, # 包含NMS操作
)
# 移动端优化建议
optimizations = {
'模型大小': '使用YOLOv12n或YOLOv12s',
'输入尺寸': '320×320或416×416',
'量化': 'INT8量化可减少75%模型大小',
'推理框架': 'Android用TFLite,iOS用CoreML',
'性能预期': '高端手机可达15-30 FPS'
}
return optimizations
在移动端部署时还需要考虑:
- 电池消耗优化
- 离线检测能力
- 结果缓存和同步
- 用户界面适配
6.4 系统集成与API设计
对于大型监控系统,通常需要提供API接口供其他系统调用:
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
class DetectionAPI:
"""检测API服务"""
def __init__(self, model_path):
self.model = YOLO(model_path)
self.request_queue = []
self.result_cache = {}
self.lock = threading.Lock()
@app.route('/api/detect', methods=['POST'])
def detect_endpoint():
"""检测端点"""
# 验证请求
if 'image' not in request.files:
return jsonify({'error': '未提供图像'}), 400
image_file = request.files['image']
# 读取图像
image_data = image_file.read()
image = cv2.imdecode(
np.frombuffer(image_data, np.uint8),
cv2.IMREAD_COLOR
)
if image is None:
return jsonify({'error': '无法解码图像'}), 400
# 获取参数
conf_thresh = request.form.get('confidence', 0.25, type=float)
iou_thresh = request.form.get('iou', 0.45, type=float)
# 执行检测
results = self.model(
image,
conf=conf_thresh,
iou=iou_thresh
)
# 格式化结果
formatted_results = self.format_results(results[0])
# 添加元数据
response = {
'success': True,
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
'processing_time': results[0].speed['inference'],
'detections': formatted_results,
'image_size': {
'width': image.shape[1],
'height': image.shape[0]
}
}
return jsonify(response)
def format_results(self, result):
"""格式化检测结果"""
detections = []
if result.boxes is not None:
for box in result.boxes:
detection = {
'class': result.names[int(box.cls[0])],
'confidence': float(box.conf[0]),
'bbox': {
'x1': float(box.xyxy[0][0]),
'y1': float(box.xyxy[0][1]),
'x2': float(box.xyxy[0][2]),
'y2': float(box.xyxy[0][3])
},
'center': {
'x': float(box.xywh[0][0]),
'y': float(box.xywh[0][1])
}
}
detections.append(detection)
return detections
@app.route('/api/status', methods=['GET'])
def status_endpoint():
"""状态端点"""
status = {
'status': 'running',
'model': self.model.__class__.__name__,
'uptime': time.time() - self.start_time,
'requests_processed': len(self.result_cache),
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S')
}
return jsonify(status)
def start_api_server(model_path, host='0.0.0.0', port=5000):
"""启动API服务器"""
api = DetectionAPI(model_path)
api.start_time = time.time()
app.run(host=host, port=port, threaded=True)
这个API服务可以提供:
- 图像检测接口
- 批量处理接口
- 实时视频流接口
- 系统状态监控
- 结果查询和历史记录
在实际项目中,我遇到过几个比较典型的问题。一个是夜间检测的误报问题,红外图像在夜间容易把车灯、路灯等热源误判为火焰。解决方法是增加一个时间上下文判断,结合环境温度和典型热源特征进行过滤。另一个是多摄像头同步问题,当系统需要处理十几个摄像头的视频流时,合理的资源分配和调度就变得非常重要。我最终实现了一个动态资源管理器,根据每个摄像头的检测负载自动调整处理优先级。
系统的响应速度也是一个关键指标。在优化过程中,我发现模型推理只占用了大约60%的处理时间,其余时间花在了图像解码、结果渲染和界面更新上。通过使用硬件加速的图像处理和多级缓存,最终将整体延迟降低到了可接受的范围。
如果你打算在实际项目中应用这个系统,我建议先从单摄像头、固定场景开始测试,逐步扩展到多摄像头和复杂环境。记得定期更新模型,因为实际场景中的火灾特征可能会随着季节和环境变化而改变。数据收集和标注是一个持续的过程,系统运行过程中产生的误报和漏报都是宝贵的训练数据。
&spm=1001.2101.3001.5002&articleId=154216740&d=1&t=3&u=6b3fccce79df41aab10f2694c97598e0)
344

被折叠的 条评论
为什么被折叠?



