工地安全帽检测这个需求,最近被问到的频率越来越高。很多同学手里已经有了标注好的安全帽数据集,也能用 YOLOv8 训练出一个 mAP 还不错的模型,但一说到“做成一个能用的检测系统”,就卡住了:不知道模型怎么嵌入桌面应用,不知道摄像头实时流怎么处理,也不知道界面线程和推理线程怎么配合。
这篇文章就围绕大家经常卡住的“模型到应用”这一段展开。我会从 YOLOv8 训练自己的安全帽检测模型开始,到用 PyQt5 编写一个包含图片检测、视频检测、摄像头实时检测的桌面工具,把整个工程链路走通。我的判断是:这类系统的技术难点并不在于“算法有多深”,而在于数据质量、线程设计、模型导出和推理性能这些工程细节。把框架选好,一个人也能完成从模型训练到桌面应用的闭环。
你可以把本文当成一个可直接复用的实战模板。即使你手里的项目不是安全帽,而是要检测安全背心、反光衣、工牌或者抽烟行为,只要改动数据集和界面逻辑,这套流程依然成立。
1. 这篇文章真正要解决的问题
工地现场的安全检查,最传统的方式是人工盯监控或者现场抽查。一个安全员要同时看几十路摄像头,漏判率和疲劳度都很高。所以“智能识别是否佩戴安全帽”就成了安防领域非常典型的落地需求。
但这类需求在开发侧有一个共性痛点:训练模型只是第一步,离“系统”还差很远。很多初学者用 YOLOv8 在 notebook 里能跑出检测框,但做不了以下事情:
- 无法把本地图片、视频文件、摄像头 RTSP/USB 流统一起来处理;
- 没有图形界面,不知道如何给现场管理人员操作;
- 推理循环堵塞界面,窗口一卡一卡,鼠标拖不动;
- 检测结果没有告警逻辑,只是画框,不能作为可用系统。
所以这篇文章要解决的不是“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 目标检测的任务划分
安全帽佩戴检测,第一步要确定“检测什么”。常见的做法有两种:
- 一个类别:person,然后通过业务逻辑判断 person 内部是否包含安全帽颜色特征。这种方案对算法要求高,容易被颜色干扰,不建议新手使用。
- 两个类别:head 和 helmet。head 表示“未佩戴安全帽的头部”,helmet 表示“佩戴安全帽的头部”。这样模型直接输出两种头部区域,界面上只需要判断是否出现 head 类别,即可触发告警。
第二种方案更直观,训练也更容易收敛。因为“是否佩戴安全帽”被转化成了“这个头部外观属于哪一类”的图像分类问题,而 YOLOv8 本身就是在解决定位加分类的联合任务。
对于更严格的工地要求,也可以再加入 person 类别,然后计算 person 框与 helmet 框的交叠率来判断该工人是否佩戴安全帽。但为了保持项目简洁,本文示例使用head和helmet两个类别。
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 helmetPyTorch 安装命令建议去 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 标注方案
建议使用LabelImg或labelme进行标注。如果采用“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.18620对应head,1对应helmet。LabelImg 选择 YOLO 格式后会自动生成这些文本文件,不需要手写。
4.3 数据集目录划分
在datasets/helmet下建立:
images/ train/ val/ labels/ train/ val/将约 80% 的图片放入 train 目录,20% 放入 val 目录。图片和对应标签文件名称必须一致,例如:
img_001.jpg img_001.txtYOLOv8 训练时,会根据配置文件自动匹配图片和标签路径。如果路径不匹配,会报“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: helmetpath 可以写相对路径,也可以写绝对路径。如果你把数据集方在别的盘,建议写绝对路径,避免训练时找不到文件。
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;project和name:保存目录,最终结果会保存到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中的source从0改成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 memory | batch 过大,输入分辨率过高 | 查看训练时显存占用 | 降低 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 仍然可以用于本地显示。此时建议把界面和推理分离得更彻底一些:
- 推理服务通过本地进程间通信与界面交互;
- 界面只负责显示结果帧和接收事件;
- 告警服务独立运行,避免界面关闭后检测停止。
附加建议:如何继续这个项目
如果这个项目你已经跑通,下一步可以从以下方向选一个深入:
- 提升模型鲁棒性:加入多尺度、遮挡、夜间红外场景的样本,改进小目标检测;
- 增加检测类别:在同一个框架内加入安全背心、反光衣、烟雾、明火等类别;
- 工程化改造:将推理封装为 HTTP 服务,使用 Flask 或 FastAPI,接一个 Vue 前端;
- 算法优化:使用 TensorRT 做推理加速,实测在 Jetson 平台上的帧率提升;
- 异常报警升级:把告警结果推送到钉钉或企业微信机器人,形成完整的闭环流程。
安全帽检测是一个很适合练手的目标检测项目。它不像通用目标检测那样类别复杂,又比单纯的人脸识别更有业务价值。做完这个系统,你会同时接触数据标注、模型训练、模型部署、GUI 编程、多线程设计,这些能力在真实项目中都是通用的。
需要提醒的是,如果在工地环境中实际部署,一定要先小规模试点,收集真实场景数据对模型做二次迭代。实验室里的 mAP 不够,现场连续跑一周的数据才是检验系统是否可用的标准。选择 YOLOv8 和 PyQt5 这套方案,并不是因为它有“最新”的光环,而是因为它能在“快速上手”和“真实落地”之间取得一个比较务实的平衡。