资讯动态

YOLOv5+PyQt5实现安全帽检测:从数据集训练到桌面应用部署

发布时间:2026/9/12 13:07:10 来源:尧图企业网站定制
简介这是一套面向智慧工地场景的安全帽佩戴检测完整实战项目基于YOLOv5目标检测框架与PyQt5桌面界面实现适合计算机、人工智能等相关专业学生用于毕业设计、课程设计或期末大作业。项目提供完整源码、训练好的模型与数据集含模型训练、测试、可视化检测及UI交互脚本代码模块划分清晰可帮助学习者快速上手并二次开发。资源共77个文件压缩包约284.4MB主要包括Python源码.py、YOLOv5配置文件.yaml、预训练权重.pt、图片与图标素材.jpg/.png/.ico、演示视频.mp4以及说明文档.md等类型覆盖从数据集准备到界面部署的完整链路。目前已吸引77人学习下载。项目在个人毕业设计中获评高分整体完成度高适合希望以真实项目驱动深度学习实践、以较少成本获得可运行Demo的进阶学习者。1. 从数据集到桌面应用这套 YOLOv5PyQt5 安全帽检测到底怎么落地“智慧工地”里最容易被拿来当毕设或演示项目的就是安全帽佩戴检测。原因很直白YOLOv5 在工地这种相对固定的监控视角下表现稳定PyQt5 又恰好能把模型包装成一个带摄像头、视频和图片输入的桌面程序整套东西看起来完整、现场演示效果好。但正是这种“看起来不难”的项目最容易在几个地方翻车数据集没洗干净导致漏检率高、训练完的权重不知道怎么接进界面、摄像头画面在 PyQt5 里刷新一卡一卡。这篇文章直接按一套可复现的方案来讲从安全帽数据集的组织和 YOLOv5 训练参数到 PyQt5 界面里的多线程推理设计再到推理性能调优和打包分发最后给出验收时该看哪些指标。适合正在做智慧工地相关毕设、课程设计或者想快速搭一个安全帽识别原型的工程师。2. YOLOv5 训练安全帽数据集从标注到模型文件2.1 安全帽数据集的组织方式与标注规范YOLOv5 训练自己的数据集第一步不是跑代码而是把数据组织成它认识的格式。常见做法是建立一个根目录里面分images和labels两个大目录各自再按train、val拆分。安全帽检测通常只分两类戴了安全帽的helmet和没戴安全帽的头部head。也有人把类别设为person、helmet两类由程序判断头部是否落在帽子框内但这样会把逻辑复杂化我一般不建议。最稳妥的是双类别方案后处理时看到head且没有对应的helmet框交叠就判定为未佩戴。标注时用 LabelImg 或 x-anylabeling 都行导出为 YOLO 格式的 txt。每行是“类别 id、中心点 x、中心点 y、宽度 w、高度 h”五个值都是相对于图片宽高的归一化小数。例如0 0.52 0.31 0.18 0.26表示一个 helmet 框。这里有两个容易踩的坑一个是标注框把头发丝也圈进去导致模型学到的是“头顶有东西”而不是“戴了安全帽”另一个是只标正面不标背面、侧面的样本工地摄像头很多是俯视角度训练集里缺少这种视角就会在实测时疯狂漏检。helmet/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ ├── helmet.yaml └── classes.txt # 可选标注工具生成目录结构里labels下的 txt 文件名必须和图片文件名完全一致后缀不同没关系。校验方法很简单用 Python 读一张图和一个 txt把框画回图上肉眼扫一遍看看有没有越界、错位、漏标。这一步虽然原始但比任何自动清洗都可靠。数据量方面双类别安全帽检测每类 1500 到 3000 个实例基本够用不需要追求上万张图关键是把视角和光线分布做均衡。2.2 训练前的 data.yaml 与模型选型数据准备好了接下来写helmet.yaml。YOLOv5 的 data 文件里path是数据集根目录train和val是相对路径nc是类别数names是类别名列表。注意nc必须和names的长度一致且顺序要和你标注时的 id 对应上否则训练出来的模型类别就是乱的这是新手最容易忽略的问题。path: D:/datasets/helmet train: images/train val: images/val nc: 2 names: 0: helmet 1: head模型选型上我建议直接用yolov5s.pt作为预训练权重起步。安全帽检测属于中等难度目标检测不需要上yolov5l或yolov5x那会让推理帧率明显下降而且小模型在大模型蒸馏之前差距也没有想象中大。如果你手里的 GPU 显存只有 4G 到 6Gyolov5s配合 640 输入尺寸跑 batch 16 是能顺畅训练的。如果主要跑 CPU 推理yolov5n更合适但精度会掉几个点需要在你的验证集上实测后再决定。2.3 训练命令与关键参数在 YOLOv5 仓库根目录下执行训练命令下面是一份可以直接改路径使用的模板python train.py --data helmet.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 32 \ --epochs 120 \ --device 0 \ --patience 30 \ --project runs/train \ --name helmet_v1参数含义如下--img 640会把训练图片缩放到 640x640这个尺寸对安全帽这种小目标来说性价比最高再大收益有限且显存翻倍--batch 32根据显存调整显存不够就降到 16梯度累积不一定比直接降 batch 稳定--epochs 120够一个中等数据集收敛配合--patience 30表示 30 个 epoch 内验证集 mAP 没有提升就提前停止训练--device 0指定第一块 GPUCPU 训练则写--device cpu但速度会慢一个数量级。--project和--name是输出目录训练权重最后会落在runs/train/helmet_v1/weights/下里面有best.pt和last.pt部署时无脑选best.pt。训练过程中终端每轮会打印P、R、mAP0.5、mAP0.5:0.95、val/box_loss等指标。安全帽检测建议重点看mAP0.5它反映的是框位置和类别都正确的比例对于这种业务场景比mAP0.5:0.95更直观。我一般要求模型在验证集上mAP0.5达到 92% 以上、head类的召回率不低于 85%低于这个线拿去做界面演示一到现场多角度画面就会露馅。2.4 超参数怎么调普通工地场景一个够用的组合YOLOv5 的超参数默认写在data/hyps/hyp.scratch-low.yaml里训练时通过--hyp指定。下面是安全帽场景里几个值得手动改的项以及我常用的推荐值。超参数默认值推荐值调整理由lr00.010.005数据集小时降低初始学习率避免前期震荡mosaic1.01.0保持开启提升小目标泛化能力scale0.50.6安全帽属于中小目标轻微放大尺度抖动fliplr0.50.5水平翻转安全帽左右对称安全可用hsv_h0.0150.02工地光线变化大略微增强色相扰动copy_paste0.00.2把安全帽粘贴到新背景增强背景多样性修改方式很简单复制一份 yaml改完传--hyp my_hyp.yaml。需要提醒的是copy_paste在yolov5里依赖detect分割掩码只有部分版本默认开启如果训练报错找不到相关函数直接去掉这一项即可。超参数调整不要贪多一次只动一两个变量否则训练完模型变好了也不知道是哪个改动起了作用。python train.py --data helmet.yaml --weights yolov5s.pt \ --img 640 --batch 32 --epochs 120 --device 0 \ --hyp data/hyps/my_hyp.yaml训练完成后把best.pt复制到项目根目录的weights/文件夹下备用。下一步就是把它接进 PyQt5 界面。3. PyQt5 界面实现摄像头、视频、图片三路输入3.1 PyQt5 安装与界面骨架PyQt5 的安装相比 PySide6 更简单直接pip 装完就能用不需要额外配置 Qt 工具链。需要注意 Python 版本匹配Python 3.9 及以下装PyQt55.15.9Python 3.10 以上也建议锁定同一个版本避免 Qt 库预编译包不兼容。pip install pyqt55.15.9 pyqt5-toolspyqt5-tools主要提供 Qt Designer 可视化设计界面如果你更习惯手写布局也可以不装。在 PyCharm 里配置 PyQt5 时把解释器指向当前虚拟环境即可不用额外设置环境变量。界面骨架我习惯用QMainWindow加中心部件的方式左侧放检测画面QLabel右侧放控制按钮和统计信息。核心代码如下import sys from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QPushButton, QVBoxLayout, QWidget class HelmetApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(智慧工地安全帽佩戴检测) self.setMinimumSize(1024, 640) self.video_label QLabel(检测画面) btn_camera QPushButton(打开摄像头) btn_video QPushButton(打开视频文件) btn_image QPushButton(打开图片) btn_exit QPushButton(退出) layout QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(btn_camera) layout.addWidget(btn_video) layout.addWidget(btn_image) layout.addWidget(btn_exit) container QWidget() container.setLayout(layout) self.setCentralWidget(container) # 按钮点击事件 btn_camera.clicked.connect(lambda: self.start_source(0)) btn_video.clicked.connect(lambda: self.start_source(self.choose_file())) btn_exit.clicked.connect(self.close) if __name__ __main__: app QApplication(sys.argv) win HelmetApp() win.show() sys.exit(app.exec_())这段代码先把界面结构跑起来按钮点击目前只是占位。start_source和choose_file下一节实现。这里要注意QLabel只负责显示 QImage检测后的帧要先转成 QImage 再 set 到 label 上不要在 label 里直接做 cv2 绘制那样刷新效率很低。3.2 把检测从界面线程里拆出来信号槽与工作线程如果直接在按钮的槽函数里写 while 循环读摄像头界面会立刻卡死因为 Qt 的主线程被VideoCapture.read()阻塞了。正确做法是把视频帧读取、模型推理、绘制都放到QThread里通过信号把处理完的结果传回主线程更新 UI。这是整个 PyQt5 界面设计里最关键的一步也是能否流畅运行的分水岭。import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class DetectThread(QThread): frame_ready pyqtSignal(QImage) def __init__(self, source, detector, parentNone): super().__init__(parent) self.source source self.detector detector self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ok, frame cap.read() if not ok: break # 模型推理 dets self.detector.infer(frame) # 在副本上绘制结果不污染原帧 vis frame.copy() for det in dets: x1, y1, x2, y2, conf, cls det if int(cls) 0: color (0, 255, 0) label fhelmet {conf:.2f} else: color (0, 0, 255) label fhead {conf:.2f} cv2.rectangle(vis, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(vis, label, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # BGR 转 RGB 再转 QImage rgb cv2.cvtColor(vis, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, w * ch, QImage.Format_RGB888) self.frame_ready.emit(qimg) cap.release()信号frame_ready携带的是QImage主线程收到后直接setPixmap就能显示。这里没有直接用pyqtSignal(dict)回传统计结果是为了把绘制层和数据处理层分离开。detector这个对象在DetectThread创建之前就初始化好并且不做任何 UI 操作保证线程安全。running变量用于退出时终止循环关闭窗口前把它置 False。3.3 检测结果的绘制、计数与显示刷新绘制逻辑看起来简单但有个细节值得注意不要在原帧上绘制尤其是frame在后续还要传给其他模块时。上面代码用vis frame.copy()绘制全部在副本上进行这样既保留了原图也给后续的日志记录留了便利。计数信息我建议放在界面右上角用一个QLabel显示“总人数 x / 未戴帽 y”数据由主线程根据信号里的检测结果累加后更新。刷新效率方面setPixmap每帧都触发一次重绘在 1080p 分辨率下会占用不少 CPU。常见的优化是只对 ROI 区域做缩放后显示或者在帧率超过 30FPS 时人为跳帧保持 25FPS 左右即可。界面卡顿的另一个隐藏原因是QImage的声明周期问题上面的写法直接复制了rgb.dataQImage 有自己的引用计数安全。如果你发现画面出现花屏或残影先检查是不是rgb变量被提前释放了。4. 推理引擎与工地场景性能调优4.1 模型加载与 warmup先跑两帧再做正式检测YOLOv5 官方推荐用torch.hub.load(ultralytics/yolov5, custom, pathbest.pt)加载模型但这个方式会从 GitHub 拉取代码离线环境很容易失败。更稳妥的做法是从本地 YOLOv5 仓库直接导入并使用DetectMultiBackend加载权重。这个类支持 PyTorch、ONNX、TensorRT 等多种后端切换部署格式时不需要改业务代码。import torch from models.common import DetectMultiBackend from utils.augmentations import letterbox from utils.general import non_max_suppression class Detector: def __init__(self, weightsweights/best.pt, device0, imgsz640): self.device torch.device(device if device ! cpu else cpu) self.model DetectMultiBackend(weights, deviceself.device) self.model.eval() self.imgsz imgsz self.stride self.model.stride self.names self.model.names # warmup让 CUDA 完成初始化避免第一帧卡顿 dummy torch.zeros(1, 3, imgsz, imgsz).to(self.device) with torch.no_grad(): self.model(dummy)DetectMultiBackend构造时传weights路径即可它会自动识别文件后缀是.pt还是.onnx。warmup 那一步很多人忽略但在桌面应用里直接影响用户第一印象第一次点击摄像头时如果卡了 3 秒才开始出画面体验就很差。dummy张量的尺寸要与imgsz一致形状是(batch, channel, height, width)。4.2 detect.py 的推理逻辑抽取官方detect.py的推理流程可以浓缩成四个步骤letterbox缩放、BGR 转 RGB 且归一化、前向推理、NMS。抽取成infer方法后要特别注意输入图像的维度顺序。OpenCV 读出来的是 HWC 布局 BGR 数据模型需要的是 CHW 布局 RGB 数据必须转置并反色通道。import cv2 import numpy as np import torch from utils.augmentations import letterbox def preprocess(self, frame, imgsz640, stride32): # 等比缩放并填充灰边保持宽高比 img letterbox(frame, imgsz, stridestride, autoTrue)[0] img img.transpose((2, 0, 1))[::-1] img np.ascontiguousarray(img) img torch.from_numpy(img).to(self.device).float() img / 255.0 if img.ndimension() 3: img img.unsqueeze(0) return img, frame.shape[:2] def infer(self, frame, conf_thres0.25, iou_thres0.45): img, (h0, w0) self.preprocess(frame) with torch.no_grad(): pred self.model(img, augmentFalse, visualizeFalse) pred non_max_suppression(pred, conf_thres, iou_thres) dets [] for det in pred[0]: x1, y1, x2, y2, conf, cls det.cpu().numpy() # 坐标映射回原图尺寸 x1 * w0 / self.imgsz x2 * w0 / self.imgsz y1 * h0 / self.imgsz y2 * h0 / self.imgsz dets.append([x1, y1, x2, y2, conf, cls]) return detsletterbox之后的坐标是相对于缩放图的直接送进界面画框会错位。上面代码用缩放比例把坐标映射回原图但这是简化写法严格来说还要考虑 padding 偏移。更准确的做法是解析letterbox返回的(ratio, (dw, dh))然后把 (x1 - dw) / ratio 映射。推荐直接用官方scale_coords函数处理它把这套坐标变换封装好了不容易算错。4.3 帧率、半精度与队列水位推理是整套系统里最耗时的部分。一块普通显卡上YOLOv5s 640 输入大概能跑 60 到 90 FPS但如果加了绘制、日志落盘和 UI 刷新实际帧率会被拉低很多。性能优化的顺序我一般先调后端再调代码优先开半精度fp16然后考虑torch.jit或 ONNX 后端最后才去动代码结构。推理后端相对耗时说明PyTorch FP321.0最稳兼容性最好作为基准PyTorch FP160.55显存减半精度损失可忽略ONNX Runtime CPU0.7无 GPU 时的选择适配性好TensorRT FP160.4需要 N 卡且要转 engine部署步骤复杂开启 fp16 只需在Detector.__init__里加一行self.model.model.half()但注意输入张量也要调用.half()否则会报类型不匹配。线程队列方面视频线程和推理线程如果共用同一个queue.Queue队列太深会积压旧帧导致画面越来越延迟。常见做法是给队列设maxsize2当队列满时主动丢弃旧帧只保留最新帧import queue frame_q queue.Queue(maxsize2) # 生产端 if frame_q.full(): try: frame_q.get_nowait() except queue.Empty: pass frame_q.put(frame)这样画面延迟始终控制在一到两帧而不是队列无限堆积导致的秒级延迟。安全帽检测对实时性很敏感尤其是需要给现场报警的场景画面延迟超过 1 秒就会造成误判。4.4 安全帽检测特有的过滤规则模型输出的原始检测框不能直接用还要加几条规则来适配工地场景。最典型的是小目标过滤摄像头装在门口或塔吊上时距离远的人脸只有几个像素宽YOLOv5 会把这类小框也输出出来但置信度很低。我一般会滤掉宽度或高度小于 20 像素的框同时对head类别提高置信度阈值到 0.4比helmet的 0.25 更高。原因是安全帽有颜色和纹理特征误报率低而head类别经常和背景里的人形物体混淆需要更严格的阈值。另一个实用规则是戴盔与未戴盔的配对逻辑如果某个head框和某个helmet框的 IoU 大于 0.3则认为该头部已被帽子覆盖不判定为未佩戴。这个规则能让“帽子戴歪但还在头上”的场景通过验证不会产生披着衣服的误报。5. 界面与模型的联调及打包发布5.1 用信号槽串起检测结果与统计面板前面的DetectThread只发了帧信号现在补上统计信号。让它每次推理完成后把当前帧的人数和未戴盔人数发出去。主线程根据这些数据更新界面上的计数 QLabel。这里要注意的是不能把汇总逻辑放在推理线程里累计因为线程退出后数据无法复位应该由主线程负责累计和清零。result_ready pyqtSignal(dict) # 主线程槽函数 def on_result(self, stat: dict): self.total_count stat[total] self.nomask_count stat[nomask] self.total_label.setText(f累计人数 {self.total_count}) self.nomask_label.setText(f未戴帽 {self.nomask_count}) # 连续多帧未戴帽时触发提示 if stat[nomask] 0 and self.nomask_frame_count 3: self.nomask_frame_count 1 if self.nomask_frame_count 3: self.alert_label.setText(警告检测到未佩戴安全帽) else: self.nomask_frame_count 0连续 3 帧才报警是为了避免某一帧误检导致界面闪烁。工地现场的摄像头会有轻微抖动单人短暂出画会造成检测结果跳变加一个帧窗口缓冲后体验会好很多。报警条件可以写在on_result里也可以单独抽一个AlarmWorker看你的工程规模而定。5.2 检测记录落盘与过程日志现场系统需要事后追溯所以检测结果要落盘。我一般用一个 CSV 文件按行追加记录每行包含时间、帧号、总人数、未戴帽人数、模型平均置信度。文件读写操作放到主线程外面用一个单独队列承接避免磁盘 IO 拖慢推理循环。import csv from datetime import datetime class LogWriter: def __init__(self, pathlogs/detection_log.csv): self.file open(path, a, newline, encodingutf-8) self.writer csv.writer(self.file) def write(self, frame_id, total, nomask, avg_conf): ts datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.writer.writerow([ts, frame_id, total, nomask, avg_conf]) self.file.flush()flush()每写一行都调用是为了防止程序崩溃时丢失最近的日志。CSV 文件被 Excel 或 pandas 读取都非常方便后续做误报分析时可以直接按时间段筛选。日志文件按天滚动更好用RotatingFileHandler的思路套到 CSV 上也行不过单会场一天的数据量一般不会超过 10MB滚动不是刚需。5.3 PyInstaller 把整个工程打包成 exe打包这一步坑最多。PyInstaller 不会自动收集 PyQt5 的插件和 YOLOv5 的模型文件必须手动指定。weights/best.pt在打包后要以资源文件的形式释放到临时目录代码里最好用sys._MEIPASS判断路径。下面是一个可以跑通的命令模板pyinstaller -w -F \ --name HelmetDetector \ --add-data weights/best.pt;weights \ --hidden-import PyQt5.sip \ --collect-data ultralytics \ main.py-w表示无控制台窗口-F是打包成单文件。--add-data在 Windows 上分隔符是分号;Linux 和 macOS 用冒号:。YOLOv5 依赖utils和models下的模块这些是仓库内的相对导入PyInstaller 有时抓不全最笨但最有效的办法是把你用到的modelsutils目录直接放在项目根目录下和main.py同级打包时一并包含进去。打包完先在一台干净机器上测试缺少 DLL 或模型路径不对是最常见的两类报错。6. 落地前的验证自测指标和几个提点小技巧6.1 给自己建一个不掺水的评测集很多人拿训练集里的图片验证模型结论自然好看但现场效果完全是另一回事。正确的做法是单独留出几百张工地实拍图片覆盖白天、傍晚、背光、多人、俯视这五种情况跑一遍推理脚本统计mAP和误报数。写一个简单的评估循环把模型输出和人工标注的 ground truth 做对比输出混淆矩阵重点关注head类被识别成helmet的交叉误报。python val.py --data helmet.yaml \ --weights weights/best.pt \ --img 640 \ --conf-thres 0.25 \ --iou-thres 0.45val.py是 YOLOv5 自带的验证脚本它输出的mAP0.5、Precision、Recall是行业通用的交付指标。如果现场有 N 卡但不想折腾 TensorRT可以用onnxruntime跑best.onnx精度和 PyTorch 差不多但不需要torch整套依赖打包体积能小很多。转换命令是python export.py --weights weights/best.pt --include onnx --opset 12 --dynamic转换后务必对比几帧输出确认坐标和置信度差异在可接受范围内。6.2 安全帽数据增强的边界与量化兜底数据增强方面安全帽场景有一类特殊风险过度拉伸或旋转会让帽子边缘的弧形结构变形导致小目标检测性能下降。建议scale不要超过 0.6旋转角度控制在正负 15 度以内。工地监控经常有斜下方视角这种视角在公开安全帽数据集里很少见有条件的话自己用手机去工地拍几十张补进去比疯狂堆增强参数更有效。最后一个保底技巧是量化。如果目标机器没有 GPUfp32的 PyTorch 模型跑 640 输入在普通 CPU 上只有 10 到 15 FPS可以先用torch.quantization做动态量化推理速度提升两倍左右代价是 mAP 下降 1 到 2 个点还在可用范围内。如果机器是 N 卡且部署时间宽裕建议走 TensorRT FP16trtexec --onnxbest.onnx --saveEnginebest.engine --fp16一行命令生成 engine然后把Detector的weights参数指向 engine 文件DetectMultiBackend会自动识别并加载。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价