基于YOLOv8与PyQt6的实时吸烟行为检测系统实战

发布时间:2026/8/2 14:52:43
基于YOLOv8与PyQt6的实时吸烟行为检测系统实战 1. 项目缘起从“烟雾缭绕”到“智能识别”的实战需求几年前我在参与一个智慧园区项目时遇到了一个非常具体且棘手的需求如何在不侵犯个人隐私的前提下对园区内多个重点区域如仓库、配电房、楼道的违规吸烟行为进行有效监控。传统的方案是安排保安人员轮巡或查看监控录像但这不仅人力成本高而且存在严重的滞后性往往是事后追责无法做到实时预警。当时基于深度学习的视觉识别技术已经兴起我们团队决定尝试用技术手段来解决这个“看得见却管不住”的痛点。这就是“基于YOLOv8的摄像头吸烟行为检测系统”诞生的背景。它不是一个简单的算法演示而是一个从真实业务场景出发集成了最新目标检测算法、本地化图形界面和完整数据工程流程的实战项目。今天我就把这个项目的完整实现思路、核心代码、踩过的坑以及最终的GUI界面设计毫无保留地分享出来。无论你是想学习如何将YOLO模型落地到具体应用还是想用PyQt6搭建一个专业的本地化检测工具这篇文章都能给你提供一条清晰的路径。整个系统的核心逻辑很直观利用部署在服务器或边缘设备上的普通网络摄像头IPC实时采集视频流送入训练好的YOLOv8模型进行推理识别画面中是否有人以及此人手中或口部是否有香烟或类似烟雾的疑似物一旦检测到系统通过GUI界面实时标注并触发告警如画面框选、日志记录、声音提示等。下面我们就从最核心的模型选型与训练开始拆解。2. 模型核心为什么是YOLOv8以及如何“教”它识别吸烟在目标检测领域YOLO系列一直是平衡速度与精度的标杆。从YOLOv5到v8Ultralytics公司持续在易用性和性能上做优化。选择YOLOv8nnano版本作为本项目的基础模型主要基于以下几点实战考量2.1 YOLOv8的实战优势与选型理由首先是部署友好性。相较于两阶段检测器如Faster R-CNNYOLO的单阶段特性使其推理速度极快这对于需要实时处理视频流的应用至关重要。YOLOv8进一步优化了网络结构和训练策略在保持精度的同时模型体积更小。我们使用的YOLOv8n预训练模型仅6MB左右即使在算力有限的设备如Jetson Nano、树莓派搭配Intel神经计算棒上也能达到不错的帧率。其次是生态与易用性。Ultralytics提供的PyTorch实现和API设计得非常“傻瓜化”。通过几行代码就能完成模型的加载、训练和验证大大降低了开发门槛。其提供的丰富数据增强、超参数配置和详细的训练日志让我们能把精力更多集中在业务逻辑和数据本身而不是调试复杂的模型代码。最后是精度满足需求。吸烟行为检测本质上是一个小目标检测问题香烟在画面中通常只占几十个像素同时需要兼顾人体检测。YOLOv8在小目标检测上的性能经过COCO等大数据集验证通过针对性的数据训练完全可以满足我们的精度要求。2.2 构建“吸烟检测”专属数据集模型再强也离不开高质量的数据。公开数据集中几乎没有现成的“吸烟行为”数据集这就需要我们自己动手构建。我们的数据来源主要有两个一是从公开视频网站注意版权爬取相关片段二是利用园区已有的部分监控录像经脱敏处理进行标注。注意数据标注是项目中最耗时但最关键的一环。务必确保标注的准确性特别是香烟这类细长小目标边界框Bounding Box要紧贴物体类别标签要正确。我们使用LabelImg或更高效的CVAT在线工具进行标注。标注时只定义一个类别smoking。但这里有一个重要的技巧为了提高模型对“吸烟行为”的鲁棒性我们不仅标注“手持点燃的香烟”也标注“口衔香烟”以及在某些角度下产生的“明显烟雾轮廓”。这相当于让模型学习“吸烟”这个行为的多种视觉表现形态。数据集的组织结构遵循YOLO格式dataset/ ├── images/ │ ├── train/ │ │ ├── video_frame_001.jpg │ │ └── ... │ └── val/ │ ├── val_frame_001.jpg │ └── ... └── labels/ ├── train/ │ ├── video_frame_001.txt │ └── ... └── val/ ├── val_frame_001.txt └── ...每个.txt标签文件内容格式为class_id x_center y_center width height坐标均为归一化后的值0-1之间。2.3 YOLOv8模型训练细节与调参心得准备好数据后就可以开始训练了。Ultralytics提供了命令行和Python API两种方式这里我推荐使用Python脚本灵活性更高。from ultralytics import YOLO # 加载一个预训练模型强烈推荐可以加速收敛 model YOLO(yolov8n.pt) # 使用nano版本平衡速度与精度 # 训练模型 results model.train( datadataset/smoking.yaml, # 数据集配置文件路径 epochs100, # 训练轮数根据数据集大小调整 imgsz640, # 输入图像大小保持640在精度和速度上较好 batch16, # 批次大小取决于你的GPU内存 device0, # 使用GPU如果是CPU则设为cpu workers4, # 数据加载线程数 projectruns/train, # 训练结果保存目录 namesmoking_detection_v1, # 实验名称 pretrainedTrue, # 使用预训练权重 optimizerAdamW, # 优化器AdamW通常表现稳定 lr00.01, # 初始学习率 lrf0.01, # 最终学习率因子 (lr0 * lrf) weight_decay0.0005, # 权重衰减防止过拟合 )关键参数解析与调参经验imgsz图像尺寸默认640x640。如果您的摄像头分辨率较低或目标香烟极小可以尝试增大到832甚至1024但这会显著增加显存消耗和降低速度。我们的经验是在640分辨率下对室内3-5米距离的吸烟行为模型已经能较好捕捉。batch批次大小在GPU显存允许的前提下尽可能设大。大的batch size能使梯度估计更准确训练更稳定。如果出现CUDA out of memory错误就减小batch或imgsz。optimizer与lr0优化器与学习率SGD和AdamW都是常用选择。对于小数据集AdamW收敛更快。初始学习率lr0是超参数中的关键设得太大会导致损失震荡不收敛太小则收敛慢。可以从0.01开始观察训练初期损失下降情况。如果损失几乎不变适当增大如果损失变成NaN爆炸则必须减小。数据增强YOLOv8内置了丰富的数据增强Mosaic, MixUp等。对于小目标检测我建议谨慎使用过于激进的空间变换如大幅度的旋转、裁剪这可能导致小目标被裁掉或变形过度。可以在args中调整hsv_h,hsv_s,hsv_v色彩抖动和translate平移等参数来增加色彩和位置鲁棒性而减少scale缩放和shear剪切的幅度。训练过程中务必关注runs/train/smoking_detection_v1目录下的结果weights/best.pt: 验证集上表现最好的模型这是我们最终要用的。results.png: 损失曲线、精度曲线图。健康的曲线应该是训练损失平稳下降验证损失在后期趋于平稳或轻微上升警惕过拟合。mAP50平均精度应持续上升。val_batch0_pred.jpg: 随机验证批次的预测结果可视化直观检查模型识别效果。如果发现验证集精度一直上不去大概率是数据问题要么数据量太少要么标注质量差。这时需要回头补充和清洗数据这是提升模型性能最根本的方法。3. 系统骨架用PyQt6搭建专业级本地检测客户端模型训练好后我们需要一个界面把它“包”起来让非技术人员也能方便地使用。PyQt6是一个功能强大且跨平台的GUI框架用它来打造本地客户端再合适不过。我们的界面需要实现以下核心功能摄像头选择与启停、实时视频流显示、检测结果叠加绘制、告警日志列表、以及简单的配置管理。3.1 主界面布局设计与控件选择PyQt6的设计可以采用Qt Designer拖拽完成再转换为Python代码但我更喜欢纯代码编写便于版本控制和逻辑管理。主窗口我们继承自QMainWindow。import sys from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QLabel, QListWidget, QGroupBox, QSpinBox, QCheckBox) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QThread from PyQt6.QtGui import QImage, QPixmap, QFont import cv2 class DetectionThread(QThread): 专门处理视频流和模型推理的线程防止界面卡顿 frame_signal pyqtSignal(QImage, list) # 发送处理后的帧和检测结果 def __init__(self, camera_index, model_path, conf_threshold): super().__init__() self.camera_index camera_index self.model_path model_path self.conf_threshold conf_threshold self.is_running True # 这里稍后初始化YOLO模型 def run(self): cap cv2.VideoCapture(self.camera_index) while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: break # 在此处调用YOLO模型进行推理 # results model(frame, confself.conf_threshold) # 处理结果绘制框... # 转换frame为QImage并发射信号 rgb_image cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape qt_image QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) self.frame_signal.emit(qt_image, []) # 第二个参数放检测结果列表 cap.release() def stop(self): self.is_running False self.wait() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.init_ui() self.detection_thread None def init_ui(self): self.setWindowTitle(吸烟行为实时检测系统 v1.0) self.setGeometry(100, 100, 1400, 800) # 设置窗口大小 # 中央部件 central_widget QWidget() self.setCentralWidget(central_widget) main_layout QHBoxLayout(central_widget) # 左侧面板视频显示区 left_panel QVBoxLayout() self.video_label QLabel(视频流显示) self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.video_label.setMinimumSize(800, 600) self.video_label.setStyleSheet(border: 2px solid #cccccc; background-color: black;) left_panel.addWidget(self.video_label) # 摄像头控制栏 control_bar QHBoxLayout() self.camera_combo QComboBox() # 这里可以扫描可用摄像头索引通常0是默认 self.camera_combo.addItems([摄像头0, 摄像头1, 摄像头2]) control_bar.addWidget(QLabel(选择摄像头:)) control_bar.addWidget(self.camera_combo) self.start_btn QPushButton(开始检测) self.start_btn.clicked.connect(self.start_detection) control_bar.addWidget(self.start_btn) self.stop_btn QPushButton(停止检测) self.stop_btn.clicked.connect(self.stop_detection) self.stop_btn.setEnabled(False) control_bar.addWidget(self.stop_btn) left_panel.addLayout(control_bar) main_layout.addLayout(left_panel, 3) # 左侧占3份空间 # 右侧面板控制与信息区 right_panel QVBoxLayout() # 检测参数组 param_group QGroupBox(检测参数) param_layout QVBoxLayout() conf_layout QHBoxLayout() conf_layout.addWidget(QLabel(置信度阈值:)) self.conf_spinbox QSpinBox() self.conf_spinbox.setRange(1, 99) self.conf_spinbox.setValue(25) # 默认25%即0.25 self.conf_spinbox.setSuffix(%) conf_layout.addWidget(self.conf_spinbox) param_layout.addLayout(conf_layout) self.save_checkbox QCheckBox(自动保存告警截图) param_layout.addWidget(self.save_checkbox) param_group.setLayout(param_layout) right_panel.addWidget(param_group) # 告警日志组 log_group QGroupBox(实时告警日志) log_layout QVBoxLayout() self.log_list QListWidget() log_layout.addWidget(self.log_list) log_group.setLayout(log_layout) right_panel.addWidget(log_group) # 状态信息 self.status_label QLabel(状态: 就绪) self.status_label.setFont(QFont(Arial, 10)) right_panel.addWidget(self.status_label) main_layout.addLayout(right_panel, 1) # 右侧占1份空间 def start_detection(self): camera_idx self.camera_combo.currentIndex() # 简单映射实际应更健壮 conf_thres self.conf_spinbox.value() / 100.0 model_path runs/train/smoking_detection_v1/weights/best.pt self.detection_thread DetectionThread(camera_idx, model_path, conf_thres) self.detection_thread.frame_signal.connect(self.update_frame) self.detection_thread.start() self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText(状态: 检测运行中...) def stop_detection(self): if self.detection_thread: self.detection_thread.stop() self.detection_thread None self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.status_label.setText(状态: 已停止) self.video_label.clear() self.video_label.setText(视频流显示) def update_frame(self, qt_image, detections): 更新视频显示和日志 pixmap QPixmap.fromImage(qt_image) scaled_pixmap pixmap.scaled(self.video_label.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.video_label.setPixmap(scaled_pixmap) # 如果有检测结果更新日志 for det in detections: log_entry f[{datetime.now().strftime(%H:%M:%S)}] 检测到吸烟行为置信度: {det[conf]:.2f} self.log_list.addItem(log_entry) # 如果勾选了自动保存在此处保存当前帧截图 if self.save_checkbox.isChecked(): self.save_alert_image(qt_image) def closeEvent(self, event): self.stop_detection() event.accept() if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())这段代码搭建了一个具备基本功能的骨架。DetectionThread是一个工作线程确保耗时的视频捕获和模型推理不会阻塞主界面UI线程的响应。这是PyQt6开发中的关键技巧否则界面会在检测时“卡死”。3.2 界面美化与交互优化上面的界面比较朴素我们可以通过QSSQt样式表进行美化使其更专业。例如修改主窗口和按钮的样式self.setStyleSheet( QMainWindow { background-color: #f0f0f0; } QGroupBox { font-weight: bold; border: 1px solid #aaa; border-radius: 5px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; } QPushButton { background-color: #4CAF50; border: none; color: white; padding: 8px 16px; font-size: 14px; border-radius: 4px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #cccccc; } QListWidget { border: 1px solid #ccc; border-radius: 3px; background-color: white; } )此外还可以增加更多功能如实时统计在界面角落显示当前帧率FPS、累计告警次数。历史记录查询将告警日志时间、截图路径保存到SQLite数据库并提供查询界面。模型热切换在不重启程序的情况下加载不同的模型文件如针对不同场景优化的模型。区域入侵检测结合OpenCV只对画面中设定的特定区域ROI进行检测减少误报和计算量。4. 核心引擎YOLOv8模型与OpenCV的实时推理集成现在我们把最关键的推理部分填充到之前预留的DetectionThread.run()方法中。这里涉及到YOLO模型的加载、前向推理、结果解析以及用OpenCV在帧上绘制检测框。4.1 模型加载与推理流程首先需要在线程的初始化或运行时加载模型。为了避免每次循环都重复加载我们在线程初始化时完成。# 在DetectionThread类的__init__方法中或run方法开始时加载模型 from ultralytics import YOLO import torch class DetectionThread(QThread): frame_signal pyqtSignal(QImage, list, float) # 增加FPS def __init__(self, camera_index, model_path, conf_threshold): super().__init__() self.camera_index camera_index self.model_path model_path self.conf_threshold conf_threshold self.is_running True self.model None self.device cuda if torch.cuda.is_available() else cpu print(fUsing device: {self.device}) def run(self): # 加载模型 try: self.model YOLO(self.model_path) self.model.to(self.device) # 预热模型可选 _ self.model.predict(np.zeros((640,640,3), dtypenp.uint8), verboseFalse) except Exception as e: print(f模型加载失败: {e}) return cap cv2.VideoCapture(self.camera_index) # 设置摄像头参数例如分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) fps_timer time.time() frame_count 0 while self.is_running and cap.isOpened(): ret, frame cap.read() if not ret: print(无法从摄像头读取帧) break # 推理 results self.model.predict(frame, confself.conf_threshold, imgsz640, # 推理尺寸可与训练不同但建议一致 deviceself.device, verboseFalse, max_det10) # 每帧最大检测数 # 处理结果 detections [] annotated_frame frame.copy() if results[0].boxes is not None: boxes results[0].boxes.xyxy.cpu().numpy() # 边界框 [x1, y1, x2, y2] confs results[0].boxes.conf.cpu().numpy() # 置信度 clss results[0].boxes.cls.cpu().numpy() # 类别ID for box, conf, cls in zip(boxes, confs, clss): x1, y1, x2, y2 map(int, box) label fSmoking {conf:.2f} # 绘制边界框和标签 cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(annotated_frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) detections.append({bbox: box, conf: conf, cls: cls}) # 计算FPS frame_count 1 if frame_count 30: elapsed time.time() - fps_timer fps frame_count / elapsed fps_text fFPS: {fps:.1f} cv2.putText(annotated_frame, fps_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) fps_timer time.time() frame_count 0 # 转换颜色空间并发射信号 rgb_image cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qt_image QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format.Format_RGB888) self.frame_signal.emit(qt_image, detections, fps if fps in locals() else 0.0) cap.release() print(检测线程结束)4.2 性能优化与推理加速技巧实时检测中帧率FPS是关键指标。以下是一些提升性能的实战技巧推理尺寸imgsz在model.predict()中imgsz参数可以小于训练尺寸。例如训练时用640推理时尝试用480甚至320能大幅提升速度但会牺牲一些小目标的检测精度。需要根据实际场景权衡。半精度FP16推理如果使用GPUCUDA启用半精度推理可以几乎不损失精度的情况下提升速度并减少显存占用。在加载模型后设置self.model.model.half()。注意确保你的PyTorch和CUDA支持FP16。TensorRT部署对于生产环境或边缘设备如Jetson系列将YOLOv8模型转换为TensorRT引擎是终极加速方案。Ultralytics官方支持导出为ONNX然后可以用TensorRT的trtexec工具或Python API进一步转换为.engine文件通常能获得数倍的性能提升。这部分涉及较多环境配置后续可以单独展开。跳帧处理Frame Skipping对于非严格实时场景可以每N帧处理一次中间帧直接显示或简单跟踪。例如if frame_count % 2 0:才进行模型推理可以将理论FPS翻倍但会引入延迟。OpenCV的CAP_PROP_BUFFERSIZE设置摄像头缓冲区大小可以减少延迟。cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)。但并非所有摄像头驱动都支持此属性。在我们的测试中在RTX 3060 GPU上使用YOLOv8n模型处理720p视频流开启FP16推理尺寸为640可以达到45-50 FPS完全满足实时要求。在CPUi7-12700上同样的设置可能只有3-5 FPS这时就需要考虑使用更小的模型如YOLOv8n和更小的推理尺寸如320或者使用OpenVINO等针对CPU优化的推理后端。5. 从演示到部署工程化考量与常见问题排查一个能跑通的Demo和一个健壮的可用系统之间还有不少距离。这部分分享我们在项目落地时遇到的典型问题及解决方案。5.1 环境配置与依赖管理项目依赖的核心库包括PyTorch、Ultralytics、OpenCV-Python、PyQt6等。为了避免环境冲突强烈建议使用Conda或venv创建独立的Python环境。一个典型的requirements.txt文件如下torch2.0.0 torchvision0.15.0 ultralytics8.0.0 opencv-python4.8.0 PyQt66.5.0 numpy1.24.0 # 其他可能需要的pillow, pandas (用于日志处理), pycocotools (用于评估)注意PyTorch需要根据你的CUDA版本单独安装。去PyTorch官网获取正确的安装命令如pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118。5.2 模型泛化与误报处理在实际部署中最大的挑战不是检测不出香烟而是误报。常见的误报源包括细长物体笔、筷子、手指在特定角度下。高亮反光金属边缘、玻璃反光。复杂背景纹理某些格子或条纹图案。我们的应对策略是“数据后处理”双管齐下数据层面在数据集中加入“困难负样本”Hard Negative Samples。即收集大量包含上述易误报物体但无香烟的图片标注为背景或不标注加入训练集。这能有效告诉模型“这些不是你要找的东西”。后处理层面置信度过滤适当提高置信度阈值conf_threshold比如从0.25提高到0.4或0.5可以过滤掉很多不确定的预测。尺寸过滤香烟在画面中的大小有一定范围。可以设定一个最小检测框面积阈值过滤掉过大或过小的框过大的可能是误报的人体其他部分过小的可能是噪声。位置逻辑吸烟行为通常与人体关联。可以引入一个轻量级的人体检测器或使用YOLOv8本身检测的person类别如果您的数据集也标注了人的话只对那些靠近人体手部或嘴部区域的香烟检测框进行告警。这需要一些简单的目标关联逻辑。5.3 多摄像头支持与流管理一个监控系统往往需要接入多个摄像头。我们的PyQt6界面可以通过QComboBox切换摄像头索引但这只是简单的切换。真正的多路并发处理有两种思路单线程轮询在主线程或一个工作线程中循环遍历所有摄像头索引依次抓取一帧、处理、显示。这种方法简单但路数一多总帧率会下降因为摄像头是串行工作的。多线程/进程池为每个摄像头创建一个独立的DetectionThread实例。每个线程管理自己的视频流和模型推理。这是更专业的做法能充分利用多核CPU实现真正的多路并发。但需要注意线程间的资源竞争如模型加载、日志写入和界面更新的同步问题。可以使用PyQt6的信号槽机制让每个线程将处理好的帧和结果发送给主界面统一调度显示。5.4 系统稳定性与异常处理程序长时间运行可能会遇到摄像头断开、模型推理异常、磁盘写满等问题。必须增加健壮的异常处理。摄像头重连在DetectionThread的循环中如果cap.read()多次失败可以尝试释放当前cap并重新初始化。模型推理容错用try...except包裹model.predict()调用发生异常时记录日志并跳过当前帧避免整个线程崩溃。资源监控可以定期检查GPU内存、系统内存和磁盘空间在资源不足时进行预警或降级处理如停止保存截图。5.5 打包与分发最后如果你想将程序分享给没有Python环境的人需要打包成可执行文件。推荐使用PyInstaller。pip install pyinstaller # 基本打包命令 pyinstaller --onefile --windowed --name SmokingDetector main.py # 添加隐藏导入PyQt6, torch等可能需要 pyinstaller --onefile --windowed --name SmokingDetector \ --hidden-importultralytics \ --hidden-importtorch \ --add-data runs/train/smoking_detection_v1/weights/best.pt;. \ main.py打包过程可能会遇到各种动态库链接问题特别是涉及PyTorch和CUDA时。一个更可靠的方法是在纯净的虚拟环境中安装所有依赖后再打包或者使用conda-pack将整个Conda环境打包分发。经过以上五个部分的拆解从模型训练、界面开发、核心集成到工程化部署一个完整的“基于YOLOv8的摄像头吸烟行为检测系统”就构建完成了。这个项目麻雀虽小五脏俱全涵盖了深度学习项目落地的全流程。在实际操作中最花时间的往往是数据收集标注和调优模型以降低误报率。希望我的这些经验能帮你少走弯路。如果遇到具体问题欢迎在评论区交流。