从YOLOv8模型训练到PyQt5桌面应用:安全帽检测系统实战
2026/9/1 4:24:40 网站建设 项目流程

工地安全帽检测这个需求,最近被问到的频率越来越高。很多同学手里已经有了标注好的安全帽数据集,也能用 YOLOv8 训练出一个 mAP 还不错的模型,但一说到“做成一个能用的检测系统”,就卡住了:不知道模型怎么嵌入桌面应用,不知道摄像头实时流怎么处理,也不知道界面线程和推理线程怎么配合。

这篇文章就围绕大家经常卡住的“模型到应用”这一段展开。我会从 YOLOv8 训练自己的安全帽检测模型开始,到用 PyQt5 编写一个包含图片检测、视频检测、摄像头实时检测的桌面工具,把整个工程链路走通。我的判断是:这类系统的技术难点并不在于“算法有多深”,而在于数据质量、线程设计、模型导出和推理性能这些工程细节。把框架选好,一个人也能完成从模型训练到桌面应用的闭环。

你可以把本文当成一个可直接复用的实战模板。即使你手里的项目不是安全帽,而是要检测安全背心、反光衣、工牌或者抽烟行为,只要改动数据集和界面逻辑,这套流程依然成立。

1. 这篇文章真正要解决的问题

工地现场的安全检查,最传统的方式是人工盯监控或者现场抽查。一个安全员要同时看几十路摄像头,漏判率和疲劳度都很高。所以“智能识别是否佩戴安全帽”就成了安防领域非常典型的落地需求。

但这类需求在开发侧有一个共性痛点:训练模型只是第一步,离“系统”还差很远。很多初学者用 YOLOv8 在 notebook 里能跑出检测框,但做不了以下事情:

  1. 无法把本地图片、视频文件、摄像头 RTSP/USB 流统一起来处理;
  2. 没有图形界面,不知道如何给现场管理人员操作;
  3. 推理循环堵塞界面,窗口一卡一卡,鼠标拖不动;
  4. 检测结果没有告警逻辑,只是画框,不能作为可用系统。

所以这篇文章要解决的不是“YOLOv8 原理讲解”,也不是“PyQt5 控件大全”,而是一套最小可用工程方案:用 YOLOv8 完成安全帽佩戴检测模型的训练,用 PyQt5 做一个包含图片检测、视频检测、摄像头实时检测、检测结果统计与告警提示的桌面应用。

从读这篇文章后,你至少能:

  • 理解安全帽检测任务的目标检测建模方式;
  • 使用 ultralytics 训练自己的安全帽检测模型;
  • 写出一个结构清晰的 PyQt5 桌面应用,并让模型推理在独立线程中运行;
  • 知道常见坑是什么,遇到问题能自己排查。

这套方案不算复杂,但它把“算法”和“软件”两个模块串在了一起,很适合作为深度学习入门后第一个完整项目。

2. 核心概念与整体技术选型

2.1 YOLOv8 为什么适合这个项目

YOLOv8 是 Ultralytics 在 2023 年发布的 YOLO 系列版本。它在模型结构上沿用“CSPDarknet + PAN-FPN + 解耦头”的整体框架,同时引入了 C2f 模块、anchor-free 检测方式、更稳定的损失函数和更友好的训练封装。

对工地安全帽检测这种目标类别少、实时性要求高、要部署到普通 PC 或边缘设备的场景,YOLOv8 的优势非常明显:

  • 官方提供了 n/s/m/l/x 多种尺寸,从 Jetson 到服务器都能跑;
  • 训练、验证、导出、推理都通过统一的 CLI 和 Python API 完成,上手成本低;
  • 自带数据增强和自动调参策略;
  • 可以导出 ONNX、TensorRT、OpenVINO 等格式,方便后续工程化。

这里多说一句:很多人会在 YOLOv8 和 YOLOv5 之间纠结。从检测效果上看,V8 在相同模型尺寸下通常有小幅提升;更重要的是 V8 代码结构更整洁,后续做增量训练或自定义模块改造更方便。如果你是从零开始,我建议直接使用 YOLOv8 或更新的 Ultralytics 版本。

2.2 目标检测的任务划分

安全帽佩戴检测,第一步要确定“检测什么”。常见的做法有两种:

  1. 一个类别:person,然后通过业务逻辑判断 person 内部是否包含安全帽颜色特征。这种方案对算法要求高,容易被颜色干扰,不建议新手使用。
  2. 两个类别:head 和 helmet。head 表示“未佩戴安全帽的头部”,helmet 表示“佩戴安全帽的头部”。这样模型直接输出两种头部区域,界面上只需要判断是否出现 head 类别,即可触发告警。

第二种方案更直观,训练也更容易收敛。因为“是否佩戴安全帽”被转化成了“这个头部外观属于哪一类”的图像分类问题,而 YOLOv8 本身就是在解决定位加分类的联合任务。

对于更严格的工地要求,也可以再加入 person 类别,然后计算 person 框与 helmet 框的交叠率来判断该工人是否佩戴安全帽。但为了保持项目简洁,本文示例使用headhelmet两个类别。

2.3 PyQt5 在项目中的角色

PyQt5 是 Qt 的 Python 绑定,它可以快速创建桌面界面。但在这个项目中,PyQt5 最重要的角色不是“画出一堆美观的控件”,而是提供多线程入口图像渲染能力

识别系统的典型流程是:

视频流/图片输入 -> 进行 YOLOv8 推理 -> 绘制检测结果 -> 在 QLabel 中显示 -> 统计并显示告警信息

如果推理直接在 Qt 主线程中执行,视频流每帧的推理耗时会导致界面无法响应。所以项目里必须用 QThread 把视频读取和推理放在后台线程,通过信号把结果帧回传给主线程刷新界面。这也是后面代码实现的核心。

2.4 系统整体架构

整个系统的模块划分如下:

模块职责技术选型
模型训练数据准备、YOLOv8 训练、模型验证ultralytics
模型封装加载权重、执行推理、解析结果YOLO 类
视频接入读取图片、视频文件、USB/RTSP 摄像头OpenCV
界面层按钮、标签、状态栏、告警显示PyQt5
后台线程视频循环、推理调用、信号回传QThread
告警逻辑连续 N 帧出现 head 则告警自定义逻辑

这种架构的好处是每一层职责独立。即使以后从桌面应用换成 Web 服务,模型推理模块也可以复用,只需要调整界面层。

3. 环境准备与项目结构

3.1 环境要求

本文示例以下面环境为主,但具体版本不必完全一致,关键在于保证 PyTorch、CUDA、Ultralytics 之间的兼容。

  • 操作系统:Windows 10/11 或 Ubuntu 20.04/22.04;
  • Python:3.9 或 3.10;
  • CUDA:11.8 或 12.1,具体根据 PyTorch 官方安装命令决定;
  • GPU:NVIDIA 显卡,4GB 以上显存更适合训练;没有 GPU 也能用 CPU 跑小模型,但速度会慢很多。
  • 主要 Python 库:
    • torch、torchvision
    • ultralytics
    • opencv-python
    • PyQt5
    • numpy
    • pillow

3.2 创建虚拟环境与安装依赖

推荐使用 conda 创建独立环境:

conda create -n helmet python=3.10 conda activate helmet

PyTorch 安装命令建议去 PyTorch 官网获取,这里给出一个常见示例:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

然后安装项目依赖:

pip install ultralytics PyQt5 opencv-python numpy pillow

安装完成后,可以用下面命令验证:

python -c "import torch, ultralytics; print(torch.__version__, ultralytics.__version__)"

如果输出两个版本号,说明环境基本可用。

3.3 项目目录结构

为了后续扩展,推荐按下面结构组织文件:

helmet_detection/ ├── datasets/ │ └── helmet/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/ ├── models/ │ └── best.pt ├── ui/ │ ├── main_window.py │ ├── detector.py │ ├── worker.py │ └── main.py ├── data.yaml ├── train.py └── test_image.py

实际项目中,你可能会把data.yaml放到 datasets 目录下,也可以放在项目根目录。这里强调目录结构,是为了让训练和界面功能有一个清晰的分类,避免所有.py文件堆在一起。

4. 数据集准备与标注

4.1 数据集来源

工地安全帽数据集的公开选项不算多,常见的有:

  • SHWD(Safety Helmet Wearing Dataset):包含“带安全帽的人头”和“不带安全帽的人头”两类,比较接近本项目需求;
  • SCUT-HEAD:华南理工大学公开的人头检测数据集,主要用于头部检测,需要自己再分类;
  • 企业自有项目数据:从工地摄像头中截取并标注。

需要注意数据集许可问题。尤其是商用项目,一定要确认数据是否允许商业使用。如果没有合适的公开数据,可以从公开视频中抽帧,人工筛选图片,再使用标注工具手动标注。

4.2 标注方案

建议使用LabelImglabelme进行标注。如果采用“head/helmet”两个类别,标注时遵循以下原则:

  • 能看到完全未被遮挡的头部,且没有安全帽 / 只戴普通帽子,标注为head
  • 头部被安全帽覆盖,或者能明显看到佩戴了安全帽,标注为helmet
  • 头部过小、严重模糊、遮挡超过 50% 的样本,建议删除或忽略,避免干扰模型;
  • 同一张图片中,多个头部都要标注,不能只标注一部分。

YOLO 使用的标注格式为:

class_id x_center y_center width height

其中坐标都是归一化到 0~1 的小数。例如:

0 0.5234 0.4123 0.1120 0.2245 1 0.7821 0.3105 0.0930 0.1862

0对应head1对应helmet。LabelImg 选择 YOLO 格式后会自动生成这些文本文件,不需要手写。

4.3 数据集目录划分

datasets/helmet下建立:

images/ train/ val/ labels/ train/ val/

将约 80% 的图片放入 train 目录,20% 放入 val 目录。图片和对应标签文件名称必须一致,例如:

img_001.jpg img_001.txt

YOLOv8 训练时,会根据配置文件自动匹配图片和标签路径。如果路径不匹配,会报“Label not found”或训练时丢失标签。

5. YOLOv8 模型训练完整流程

5.1 编写数据集配置文件

在项目根目录创建helmet.yaml

# helmet.yaml path: datasets/helmet train: images/train val: images/val nc: 2 names: 0: head 1: helmet

path 可以写相对路径,也可以写绝对路径。如果你把数据集方在别的盘,建议写绝对路径,避免训练时找不到文件。

5.2 选择预训练模型

Ultralytics 提供了多种预训练权重:

  • yolov8n.pt:最小最快,适合边缘设备,精度相对低;
  • yolov8s.pt:速度和精度均衡;
  • yolov8m.pt:精度更高,需要更多显存;
  • yolov8l.pt / yolov8x.pt:适合高精度服务端模型。

对于安全帽检测,如果训练数据量不大(几千张),推荐从yolov8s.pt开始。你的 GPU 显存不足时,用yolov8n.pt也可以。

5.3 训练命令

在控制台进入项目根目录后,运行:

yolo detect train data=helmet.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 device=0 project=run name=helmet

参数含义:

  • data:数据集配置文件;
  • model:预训练权重,也可以传入 yaml 文件从头训练;
  • epochs:训练轮数,先跑 100 轮看效果;
  • imgsz:训练图像尺寸,640 是平衡速度和精度的常用值;
  • batch:批次大小,根据显存调整,显存不足就调小到 8 或 4;
  • device:使用哪个 GPU,CPU 则写device=cpu
  • projectname:保存目录,最终结果会保存到run/helmet/

训练过程中,控制台会每隔一段时间打印一次 loss 和 mAP 信息。训练结束后,run/helmet/weights/下会生成两个权重文件:

  • best.pt:验证集上指标最优的模型,部署时优先使用;
  • last.pt:最后一个 epoch 的模型,可用于继续训练。

5.4 模型验证

训练完成后,可以用验证命令查看在验证集上的效果:

yolo detect val data=helmet.yaml model=run/helmet/weights/best.pt

这个命令会输出各类别的 precision、recall 和 mAP50 等指标。安全帽检测项目中,如果head类别的 recall 偏低,说明“未戴安全帽”的情况容易被漏检,这是需要重点关注的。

5.5 训练时如何判断模型是否正常

训练是否成功,不能只看最后一个 epoch 的数值。建议观察:

  • 训练 loss 应该逐渐下降,而不是震荡上升;
  • 验证集 mAP50 应该逐步上升;
  • 如果数据集中 head 和 helmet 两类样本数量不均衡,需要关注数量较少类别的指标;
  • 如果模型老是误报,可能是样本中相似外观的干扰太多,比如把普通帽子、头发边缘、广告牌上的脑袋当成人头。

在标注数据时,尽量保持类别内的外观一致性,不要让“head”类里混入大量戴了安全帽但反光很强的样本。

6. PyQt5 界面与推理代码实现

6.1 模型推理封装

为了让界面代码更简洁,先把 YOLOv8 模型封装成一个独立类。创建ui/detector.py

# ui/detector.py from ultralytics import YOLO class HelmetDetector: def __init__(self, weights_path: str): self.model = YOLO(weights_path) self.names = self.model.names # 类别名称字典 def detect(self, frame, conf=0.4): """ 对一帧图像进行检测,返回检测框列表。 每个框为 [x1, y1, x2, y2, conf, cls_id] """ results = self.model(frame, conf=conf, verbose=False) boxes = results[0].boxes.data.cpu().numpy() return boxes

这里使用results[0].boxes.data获取所有检测框数据。注意不同版本的 Ultralytics API 可能略有差异,如果提示没有data属性,可以换成:

boxes = results[0].boxes.xyxy.cpu().numpy() confs = results[0].boxes.conf.cpu().numpy() clses = results[0].boxes.cls.cpu().numpy()

6.2 后台视频线程

视频读取和模型推理都不能放在 Qt 主线程。创建ui/worker.py

# ui/worker.py import cv2 from PyQt5.QtCore import QThread, pyqtSignal class VideoThread(QThread): frame_signal = pyqtSignal(object) def __init__(self, source=0, detector=None): super().__init__() self.source = source self.detector = detector self.running = True def run(self): cap = cv2.VideoCapture(self.source) if not cap.isOpened(): self.frame_signal.emit(None) return while self.running: ret, frame = cap.read() if not ret: break if self.detector is not None: frame = self.annotate(frame) self.frame_signal.emit(frame) cap.release() def annotate(self, frame): boxes = self.detector.detect(frame) for box in boxes: x1, y1, x2, y2, conf, cls_id = [float(v) for v in box] x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) label = self.detector.names[int(cls_id)] color = (0, 0, 255) if label == "head" else (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText( frame, f"{label} {conf:.2f}", (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2, ) return frame def stop(self): self.running = False self.wait()

这个线程类既可以读取图片(通过 source 指定图片路径时,OpenCV 会读取为单帧,但循环只执行一次),也可以读取视频文件或摄像头。source=0表示默认摄像头,也可以传 RTSP 地址。

6.3 主窗口与界面布局

创建ui/main_window.py,实现最基本的界面:左侧是视频显示区域,右侧是按钮区域和状态标签。

# ui/main_window.py import cv2 from PyQt5.QtWidgets import ( QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog ) from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt from detector import HelmetDetector from worker import VideoThread class MainWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle("工地安全帽佩戴检测系统") self.detector = HelmetDetector("../models/best.pt") self.thread = None self.current_frame = None self.init_ui() def init_ui(self): self.image_label = QLabel("等待图片或视频") self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(640, 480) self.image_label.setStyleSheet("background-color: #000; color: #fff;") self.status_label = QLabel("未检测") self.btn_image = QPushButton("选择图片") self.btn_video = QPushButton("选择视频") self.btn_camera = QPushButton("打开摄像头") self.btn_stop = QPushButton("停止") self.btn_image.clicked.connect(self.open_image) self.btn_video.clicked.connect(self.open_video) self.btn_camera.clicked.connect(self.open_camera) self.btn_stop.clicked.connect(self.stop_thread) right_layout = QVBoxLayout() right_layout.addWidget(self.status_label) right_layout.addWidget(self.btn_image) right_layout.addWidget(self.btn_video) right_layout.addWidget(self.btn_camera) right_layout.addWidget(self.btn_stop) right_layout.addStretch() main_layout = QHBoxLayout() main_layout.addWidget(self.image_label, 3) main_layout.addLayout(right_layout, 1) self.setLayout(main_layout) def open_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Images (*.jpg *.jpeg *.png)" ) if not file_path: return frame = cv2.imread(file_path) frame = self.detector.annotate_frame(frame) self.show_frame(frame) def open_video(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择视频", "", "Videos (*.mp4 *.avi *.mov)" ) if not file_path: return self.start_thread(file_path) def open_camera(self): self.start_thread(0) def start_thread(self, source): self.stop_thread() self.thread = VideoThread(source, self.detector) self.thread.frame_signal.connect(self.show_frame) self.thread.start() def stop_thread(self): if self.thread is not None: self.thread.stop() self.thread = None def show_frame(self, frame): if frame is None: self.status_label.setText("无法打开视频源") return rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimg).scaled( self.image_label.size(), aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation ) self.image_label.setPixmap(pixmap) def closeEvent(self, event): self.stop_thread() event.accept()

这里有一个需要补充的方法detector.annotate_frame。前面detector.py中只写了detect,可以把绘制逻辑抽出来,放到 detector 中,这样线程类和主窗口都能复用。我们把它完善一下:

# ui/detector.py 完整示例 import cv2 from ultralytics import YOLO class HelmetDetector: def __init__(self, weights_path: str): self.model = YOLO(weights_path) self.names = self.model.names def detect(self, frame, conf=0.4): results = self.model(frame, conf=conf, verbose=False) boxes = results[0].boxes.data.cpu().numpy() return boxes def annotate_frame(self, frame, conf=0.4): boxes = self.detect(frame, conf) head_count = 0 helmet_count = 0 for box in boxes: x1, y1, x2, y2, score, cls_id = [float(v) for v in box] x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) label = self.names[int(cls_id)] if label == "head": head_count += 1 color = (0, 0, 255) else: helmet_count += 1 color = (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText( frame, f"{label} {score:.2f}", (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2, ) text = f"head: {head_count} helmet: {helmet_count}" cv2.putText(frame, text, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) return frame, head_count, helmet_count

然后在main_window.py中调用:

frame, head_count, helmet_count = self.detector.annotate_frame(frame) if head_count > 0: self.status_label.setText(f"告警:检测到 {head_count} 个未佩戴安全帽的头部!") else: self.status_label.setText(f"正常:helmet {helmet_count},head 0")

6.4 程序入口

创建ui/main.py

# ui/main.py import sys from PyQt5.QtWidgets import QApplication from main_window import MainWindow def main(): app = QApplication(sys.argv) window = MainWindow() window.resize(1024, 640) window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()

运行:

cd ui python main.py

如果模型文件路径正确,界面会弹出。点击“选择图片”可以直接测试单张图片,点击“打开摄像头”可以启动电脑摄像头。

注意:摄像头测试时,如果视频流很卡,优先检查推理耗时,而不是怀疑 PyQt5 显示有问题。可以打印每次推理的耗时。

7. 运行结果与效果验证

7.1 使用图片验证

先用一张测试图片验证模型推理流程。创建test_image.py

# test_image.py import cv2 from ui.detector import HelmetDetector detector = HelmetDetector(r"models/best.pt") frame, head_count, helmet_count = detector.annotate_frame( cv2.imread("demo.jpg") ) print(f"head: {head_count}, helmet: {helmet_count}") cv2.imshow("result", frame) cv2.waitKey(0) cv2.destroyAllWindows()

如果图片中有人未戴安全帽,输出中head应该大于 0,界面上红色框表示未戴帽。

判断“系统可用”的标准不是模型跑了没有,而是:

  • 正脸、侧脸、背身小目标都能基本检测;
  • 未戴帽的头部不会大面积漏检;
  • 普通背景下的非人物目标不会频繁误报;
  • 视频回放或摄像头画面能稳定显示,界面不卡死。

7.2 使用视频或摄像头验证

打开摄像头时,建议先在控制台查看是否报错。如果摄像头打不开,可以尝试把VideoThread中的source0改成1,或者改用某个视频文件先验证流程。

如果摄像头画面正常但推理帧率很低,可以:

  • 使用yolov8n.pt代替yolov8s.pt
  • 降低输入分辨率,例如只处理640x480的 ROI 区域;
  • detect方法中将conf提高到0.5,减少低置信度框的数量。

7.3 告警逻辑验证

最佳做法不是单帧看到head就告警,而是连续若干帧都检测到head才告警,避免闪烁。可以在VideoThread中加入一个简单的帧计数器:

# 伪代码,示意 head_frames = 0 while running: frame = read() boxes = detect(frame) has_head = any(cls_id == 0 for box in boxes) if has_head: head_frames += 1 else: head_frames = 0 if head_frames >= 5: emit_alarm("未佩戴安全帽")

这种“连续 N 帧”规则在实际工程中很常见,能显著降低误报率。

8. 常见问题与排查思路

下面几个问题是我在类似项目中遇到频率最高的,列成表格方便对照排查。

问题现象可能原因排查方式解决方案
训练 loss 不下降学习率过高或过低,标签错误,数据太乱查看 loss 曲线,抽样检查标注图片调整学习率,清理错误标签,增加数据增强
模型对 head 漏检严重head 样本数量不足,类别不均衡统计 train 目录中 head/helmet 数量增加 head 样本或对 head 类别做过采样
普通帽子、头发被识别成 head训练数据中 head 类别内样式单一,背景干扰收集包含各类帽子的负样本,重新标注补充数据,或将 head 类别改为“无安全帽覆盖的头部”
PyQt5 界面启动后无响应推理放在主线程,视频循环阻塞了事件循环观察 CPU 和线程占用使用 QThread 在后台执行推理,界面只负责显示
摄像头打不开摄像头编号不对,或资源被其他应用占用使用 OpenCV 单独测试 cv2.VideoCapture(0)更换编号,释放摄像头资源,或者检查权限
CUDA out of memorybatch 过大,输入分辨率过高查看训练时显存占用降低 batch,降低 imgsz,或换更小的模型
模型导出到 ONNX 后结果不一致预处理、后处理差异对比 pt 和 onnx 的推理点数使用 ultralytics 提供的导出接口,避免自己重写后处理
视频画面明显卡顿推理耗时过长,视频解码帧率过高打印每帧推理耗时使用更小模型,降低帧率,限制检测分辨率

9. 最佳实践与工程建议

9.1 数据集管理

数据是这类系统最重要的资产。标注时建议保持统一的类别定义。比如“head”是“未佩戴安全帽的头部”,那么即使工人戴了普通鸭舌帽,只要没有安全帽,也应该标成head,否则模型会认为普通帽子也是安全帽。

另外,工地场景中经常有多个工人距离镜头远近不同。训练数据中要加入不同尺度的头部样本。如果模型在小目标上漏检,可以尝试:

  • 提高训练图像的imgsz,如从 640 升到 960;
  • 使用马赛克增强之外的针对性裁剪增强;
  • 在摄像头画面中划定检测区域,只检测施工区域,减少远处干扰。

9.2 训练策略

从预训练权重开始训练,而不是从零开始。安全帽数据虽然属于特定场景,但预训练模型理解的底层特征(边缘、纹理、形状)仍然能直接迁移。

训练轮数不一定要很大。如果数据量在几千张,100 到 200 轮通常已经足够。可以打开patience参数,当验证集指标连续多轮不提升时自动停止,节省时间:

yolo detect train data=helmet.yaml model=yolov8s.pt epochs=200 patience=20

训练完成后,优先使用best.pt部署,而不是last.pt

9.3 推理性能优化

桌面 demo 直接用.pt文件即可,但生产环境建议导出为 ONNX 或 TensorRT 引擎。

导出 ONNX:

yolo export model=models/best.pt format=onnx imgsz=640

导出 TensorRT:

yolo export model=models/best.pt format=engine imgsz=640

使用 ONNX 时,可以通过onnxruntime在 CPU 上进行推理,推理速度通常比 PyTorch 模型快。如果目标是 Jetson 等英伟达边缘设备,TensorRT 是更好的选择。

但导出只是第一步。导出后要对比.pt和导出模型在同一样本上的输出,确保检测框和类别一致,否则部署时容易出现“训练效果很好,上线就崩”的问题。

9.4 告警与事件记录

不要只在界面上显示红色框。完整的系统应该把“未佩戴安全帽”的事件保存到本地或者推送出来。最简单的实现:

  • 当触发告警时,保存当前帧图片到logs/目录;
  • 在帧上叠加时间戳和摄像头编号;
  • 通过邮件、企业微信机器人或消息队列推送告警。

这些功能不影响模型本身,但能让系统真正用于现场管理,这也是“检测识别系统”和“模型 demo”之间的分界线。

9.5 安全与隐私

这属于生产环境中的重要问题。摄像头数据如果涉及人员信息,需要考虑隐私合规。建议:

  • 系统部署在内部网络,不直接暴露到公网;
  • 对视频流做好访问控制;
  • 告警截图和录像设置保留期限,避免非授权访问;
  • 模型和数据集中包含的人员图像,应确认采集和使用的合法授权。

不要为了图方便,把包含员工面部信息的工地图片公开上传到不安全的第三方平台。

9.6 界面体验

安全帽检测系统的使用者很可能是工地安全员,不是程序员。界面应该尽量简单,默认显示一个大画面即可。按钮不要超过 6 个,状态用颜色区分:

  • 绿色:所有工人均佩戴安全帽;
  • 红色:存在未佩戴安全帽,并显示数量;
  • 黄色:视频源不可用或正在初始化。

还可以加入简单的统计面板:今日检测次数、今日违规次数、最新告警时间。这些数据可以从本地 SQLite 中读取,不需要引入重量级数据库。

10. 模型训练需要避开的几个坑

除了代码层面的坑,训练数据本身的坑也很关键。

很多第一次做安全帽项目的同学,直接去网上下载现成的数据集,没有检查类别定义就开训。结果模型把“head”和“helmet”学混了,因为原始数据里两类标注边界不清晰。比如一张图片中,安全帽反光严重,标注人员把它标成了head;另一张图片中,工人把安全帽拿在手上,又被标成了helmet。这种标签噪声会让模型很难收敛。

所以在训练前,至少做一次标签质量检查:

  • 随机抽 100 张训练图片,同时绘制标签框,人眼检查类别是否合理;
  • 统计 head 和 helmet 的框数量比例;
  • 检查是否存在大量重复图片,如果训练集和验证集有重复图片,验证指标会虚高。

如果你发现自己标注的数据只有几百张,优先收集数据,而不是不停调整模型结构。对于安全帽检测这种“头部外观分类”任务,稳定的数据往往比算法改进带来的收益更大。

11. 从 Demo 到可部署系统还需要做什么

本文给出的 PyQt5 桌面应用已经能完成基本检测,但距离真正的工地部署还有几步路。

如果用 USB 摄像头或本地视频文件,当前方案可以直接运行。但工地在现实工程中一般使用 IP 摄像头,接入方式是 RTSP 或 GB28181。对于 RTSP 流,只需要把VideoThread的 source 改为:

self.source = "rtsp://user:password@ip:port/stream"

OpenCV 的VideoCapture可以直接拉流。但 RTSP 流可能因为网络波动导致延迟或卡顿,更稳妥的做法是使用支持断线重连的拉流线程,例如在read()返回False后等待 1 秒重新连接。

如果要把系统部署到边缘设备,比如 Jetson Nano、Jetson Orin,需要把模型导出为 TensorRT 引擎,并用 GStreamer 或 V4L2 接入摄像头,PyQt5 仍然可以用于本地显示。此时建议把界面和推理分离得更彻底一些:

  • 推理服务通过本地进程间通信与界面交互;
  • 界面只负责显示结果帧和接收事件;
  • 告警服务独立运行,避免界面关闭后检测停止。

附加建议:如何继续这个项目

如果这个项目你已经跑通,下一步可以从以下方向选一个深入:

  1. 提升模型鲁棒性:加入多尺度、遮挡、夜间红外场景的样本,改进小目标检测;
  2. 增加检测类别:在同一个框架内加入安全背心、反光衣、烟雾、明火等类别;
  3. 工程化改造:将推理封装为 HTTP 服务,使用 Flask 或 FastAPI,接一个 Vue 前端;
  4. 算法优化:使用 TensorRT 做推理加速,实测在 Jetson 平台上的帧率提升;
  5. 异常报警升级:把告警结果推送到钉钉或企业微信机器人,形成完整的闭环流程。

安全帽检测是一个很适合练手的目标检测项目。它不像通用目标检测那样类别复杂,又比单纯的人脸识别更有业务价值。做完这个系统,你会同时接触数据标注、模型训练、模型部署、GUI 编程、多线程设计,这些能力在真实项目中都是通用的。

需要提醒的是,如果在工地环境中实际部署,一定要先小规模试点,收集真实场景数据对模型做二次迭代。实验室里的 mAP 不够,现场连续跑一周的数据才是检验系统是否可用的标准。选择 YOLOv8 和 PyQt5 这套方案,并不是因为它有“最新”的光环,而是因为它能在“快速上手”和“真实落地”之间取得一个比较务实的平衡。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询