简介:一套面向智慧工地场景的深度学习实战项目,基于YOLOv5与PyQt5实现工人安全帽佩戴检测和危险区域入侵告警,适合计算机、人工智能等相关专业的毕业设计、课程设计及初期项目演示。系统采用PyTorch框架和Python语言,自带PyQt5图形界面,按钮名称可自定义,支持手动绘制危险区域,一旦有人入侵立即触发告警。资源包共2000个文件,包含743个txt标注文件、743个xml文件、406张jpg图像、49个yaml配置、49个py源码脚本、5个sh部署脚本及项目说明文档等,压缩包大小约236MB,目录结构清晰,便于按模块查阅与二次开发。项目代码均经过运行验证,并配有详细操作文档,按说明即可完成环境配置、模型推理与界面交互。目前已有4152人学习下载,对于希望快速上手YOLOv5目标检测、熟悉PyQt5桌面应用开发或完成智慧工地类毕设的读者都有较高参考价值。
1. 智慧工地里的“AI哨兵”:YOLOv5+PyQt5这套系统在管什么
工地安全员最头痛的事,是监控摄像头装了不少,最后还是靠人盯屏:有人没戴安全帽进了作业区、有人误闯塔吊回转半径,发现时要么已经出事,要么只能事后翻录像。基于YOLOv5和PyQt5的安全帽佩戴检测系统,把“人盯屏”变成“AI盯屏”:YOLOv5实时识别画面里的戴帽/未戴帽目标,PyQt5把识别结果做成带告警面板的GUI界面,再配合电子围栏做危险区域入侵检测告警。它能同时覆盖工地安防最常见的两类隐患,适合做智慧工地落地的开发者、安防集成商,以及拿它当课程设计主线的学生。下文按训练到部署的顺序,把能跑通这套系统的步骤、参数和坑讲清楚。
2. 训练安全帽检测模型:从YOLOv5数据集到超参数的完整落法
2.1 安全帽数据集的标注格式与目录组织
训练YOLOv5的第一步不是写代码,而是把数据整理成它认得的格式。YOLOv5要求数据集按 images 和 labels 两个目录存放,标注文件是等名的txt,每行写一个目标:类别ID、归一化后的中心点x、中心点y、宽度w、高度h。
dataset/ ├── images/ │ ├── train/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── val/ ├── labels/ │ ├── train/ │ │ ├── 001.txt │ │ └── 002.txt │ └── val/ └── helmet.yaml以安全帽检测为例,我一般定义三个类别:0 表示“戴了安全帽”(helmet),1 表示“头部裸露”(head),2 表示“人员整体”(person)。第三类的作用是给模型一个显式的“人”上下文,比只标帽子和头更容易训练,推理时也好做“人头在框内但无帽”的后处理判断。一个标注txt文件内容大致如下:
0 0.521484 0.392188 0.142188 0.204688 1 0.715625 0.521875 0.101875 0.142188 2 0.534375 0.456250 0.297500 0.459375坐标全部相对图片宽高归一化,取值在0到1之间。标注工具常见的用labelImg或labelme,都可以导出这个格式,但注意这两个工具依赖PyQt5/PyQt4作为GUI后端,环境中PyQt5版本太新或太旧都可能起不来,这块的坑在第5章专门说。标注建议覆盖白天顺光、逆光、阴天、扬尘、佩戴不同颜色安全帽的情况,至少要保证每个类别样本数在3000个目标以上,否则后面mAP上不去。
2.2 用conda建环境与训练命令
先把环境用conda隔离好。很多人直接在全局Python里装依赖,最后torch和opencv互相打架,返工成本极高。我一般这样创建环境:
conda create -n helmet python=3.8 conda activate helmet cd yolov5 pip install -r requirements.txtrequirements.txt是YOLOv5仓库自带的依赖清单。这里有个细节:默认会安装最新版torch,如果你的显卡比较老,建议先查一下驱动支持的CUDA版本,再手动装对应torch轮子。训练命令最简形式如下:
python train.py --data helmet.yaml --weights yolov5s.pt \ --img 640 --batch 16 --epochs 100 --device 0--data指向helmet.yaml,里面写明train/val路径和类别名;--weights用yolov5s.pt做预训练起点,比从零训练收敛快得多;--img设为640是检测精度和速度的平衡点,显存吃紧就降到416;--batch 16对应6GB以上显存,8GB以下的卡设8更稳妥;--epochs我建议起步100,安全帽不是细粒度任务,100轮足够看出模型有没有学起来。
2.3 训练结果怎么看:metrics与后处理
训练结束后看runs/train/exp目录下的results.png,这是判断模型好坏的“成绩单”。重点看三列:mAP@0.5、mAP@0.5:0.95、val/obj_loss。安全帽检测里mAP@0.5达到0.9以上才算能用,低于0.85说明样本或标注有问题,先别急着调超参数,回去查数据集。训练完成后做推理测试,最常用的命令是:
python detect.py --weights runs/train/exp/weights/best.pt \ --source test_01.mp4 --conf-thres 0.4 --iou-thres 0.45 \ --save-txt --save-conf这里--conf-thres是置信度阈值,低于它的框会被丢掉。检测安全帽这种单一场景,0.35到0.5之间都是可调区间,默认的0.25太低,会把墙上的人形广告牌、阴影里的安全帽轮廓都框出来。--iou-thres是NMS(非极大值抑制)的IoU阈值,默认0.45,一般不用动,如果同一目标频繁出现双框,就往下调到0.4。yolov5后处理的核心就是这个NMS步骤,很多“线上漏检、线下正常”的怪问题最后都出在阈值和NMS参数上,后面避坑章细说。
3. 用PyQt5把检测做成GUI界面:视频流、线程与告警面板的配合
3.1 界面布局与主窗口骨架
PyQt5在安全帽检测系统里干的活,是把模型推理结果“翻译”成能值班的界面。窗口布局我建议用QMainWindow,左侧放QGraphicsView或QLabel显示实时画面,右侧放控制按钮和告警日志列表,底部是状态栏显示FPS和模型加载状态。界面代码骨架如下:
from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QTextEdit from PyQt5.QtCore import QThread, pyqtSignal, Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("智慧工地安全帽检测与区域入侵告警") self.video_label = QLabel("等待视频源...") self.log_panel = QTextEdit() self.btn_start = QPushButton("启动检测") self.btn_stop = QPushButton("停止检测") self.setCentralWidget(self._build_ui()) def _build_ui(self): layout = QVBoxLayout() layout.addWidget(self.video_label) layout.addWidget(self.btn_start) layout.addWidget(self.btn_stop) layout.addWidget(self.log_panel) container = QWidget() container.setLayout(layout) return container这段代码里QTextEdit承担告警日志显示,QPushButton绑启动/停止操作。实际项目里右侧还会有“加载模型”“选择视频文件”等按钮,但核心就三件事:画面、按钮、日志。界面的关键是别把推理逻辑写进按钮回调里,否则点一下“启动”界面就卡死。
3.2 视频采集与推理解耦:QThread和信号槽
摄像头读取和YOLOv5推理都吃CPU/GPU,如果放在UI主线程,视频帧每来一帧要等推理完才刷新,界面会像幻灯片。标准做法是把视频采集和推理塞进QThread,通过信号把结果帧传回主线程。一个可复用的工作线程类如下:
class DetectThread(QThread): frame_ready = pyqtSignal(object, list, float) # 原始帧、检测结果、FPS def __init__(self, model, source): super().__init__() self.model = model self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) infer_results = [] prev_time = time.time() while self.running: ret, frame = cap.read() if not ret: break # 推理 results = self.model(frame) infer_results = results.xyxy[0].cpu().numpy() fps = 1.0 / (time.time() - prev_time) prev_time = time.time() self.frame_ready.emit(frame, infer_results, fps) cap.release()信号frame_ready把三样东西送回主线程:原始帧BGR数组、检测结果(每个元素是x1, y1, x2, y2, conf, cls)、实时FPS。为什么传原始帧而不是画好框的帧?因为绘制工作也放主线程做,QPainter不是线程安全的,在子线程画完再传容易偶发崩溃。测下来的经验是:绘制放主线程,推理放子线程,FPS稳定性和界面流畅度都最好。
3.3 绘制检测框与状态栏联动
主线程拿到检测结果后,先用QImage把BGR帧显示到QLabel上,再根据检测类别画绿框或红框。这个环节的细节决定系统好不好用:
def update_frame(self, frame, results, fps): for box in results: x1, y1, x2, y2, conf, cls_id = box x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) color = (0, 255, 0) if int(cls_id) == 0 else (0, 0, 255) label = f"{self.class_names[int(cls_id)]} {conf:.2f}" cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qt_img = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qt_img)) self.statusBar().showMessage(f"FPS: {fps:.1f}")这段的逻辑要点:类别0映射绿色“helmet”,类别1映射红色“head”,颜色本身就是给值班人员的第一层告警视觉信号。QImage的构造要小心数据生命周期,rgb_image是局部变量,setPixmap之后Qt默认深拷贝,线程传帧不会出现悬空指针。状态栏实时刷新FPS,方便现场调试时一眼看出推理性能有没有掉。
4. 危险区域入侵检测:电子围栏坐标与告警去抖的实现细节
4.1 电子围栏怎么定义:多边形区域与坐标序列
危险区域入侵检测的第二条业务线,是对“人进了不该进的地方”告警。常见做法是用电子围栏圈出塔吊回转半径、基坑边缘、爆破区等位置,而不是用矩形框硬套。因为工地区域形状不规则,矩形框会把大量正常通行区域囊括进去,误报高到没法用。我用JSON保存围栏坐标:
{ "zone_name": "tower_crane_radius", "points": [[326, 184], [482, 196], [530, 372], [268, 428], [214, 300]], "alert_interval_sec": 10, "max_queued_frames": 5 }points是多边形顶点坐标,按顺时针或逆时针顺序给一组像素坐标。系统启动时把这组点加载进内存,转成numpy数组。坐标来源有两种:一是用程序在视频画面里鼠标点选,二是GIS测绘坐标投影到画面。对于固定摄像头,直接鼠标点选最省事。
4.2 点在多边形内的判定:射线法实现与参数考虑
判定“人脚底点是否落在危险区域内”要用点在多边形内算法。最简单的可靠实现是射线法:从待测点向右水平射一条线,统计与多边形边的交点个数,奇数为内、偶数为外。代码贴在下面:
def point_in_polygon(point, polygon): """ point: (x, y) 检测点 polygon: [(x1, y1), (x2, y2), ...] 多边形顶点列表 """ x, y = point n = len(polygon) inside = False for i in range(n): x1, y1 = polygon[i] x2, y2 = polygon[(i + 1) % n] # 射线条件:点y在边两端y之间,且点x在交点左侧 if (y1 > y) != (y2 > y): x_intersect = (x2 - x1) * (y - y1) / (y2 - y1) + x1 if x < x_intersect: inside = not inside return inside这里注意取检测点不要用“人框中心点”,而应该用框底边的中点。因为人的中心点可能在围栏外,但双脚已经踩进基坑边缘,中心点判不出来。yolov5检测框底边中点更接近人员站立位置,在爬梯、弯腰这类姿态下误判明显减少。这个细节是很多人忽视的。另一个细节是摄像头安装有俯角,围栏画在画面地面位置时,底边中点比顶部像素更可靠。
4.3 告警去抖、冷却与记录
检测到入侵不等于立刻告警。单帧判定噪声很大,可能是检测框抖动,或者人员只是在围栏边界擦过。我一般用连续帧确认的方式去抖:只有当连续N帧(N取3~5)检测点都在多边形内,才触发一次告警。触发后进入冷却期,默认10秒内不重复告警,避免同一人站在危险区里,日志被刷屏。告警动作可以同时做三件事:写日志、截图留档、声音提示。
zone_hit_count = 0 last_alert_time = 0 alert_cooldown_sec = 10 # 默认冷却时间 hit_threshold = 4 # 连续4帧才确认 for result in results: x1, y1, x2, y2, conf, cls_id = result if cls_id == 2: # person foot_point = ((x1 + x2) / 2, y2) if point_in_polygon(foot_point, current_zone): zone_hit_count += 1 else: zone_hit_count = 0 if zone_hit_count >= hit_threshold: now = time.time() if now - last_alert_time > alert_cooldown_sec: last_alert_time = now trigger_alert(frame, foot_point) # 截图+写日志+播放提示音参数配置上,告警阈值hit_threshold不要设成1,也不要设成大于10。设太小抖帧误报,设太大人员快速跑过时可能来不及触发。冷却时间应该按危险等级区分:塔吊半径这种高风险区域冷却5秒,一般禁入区10秒。还建议点一下“记录”功能把告警截图按时间命名保存,这是事后追溯和向安全管理部门说明的依据。
5. 安全帽检测系统避坑手册:从环境冲突到漏报误报的五个现场问题
5.1 labelme安装时PyQt5版本冲突
现象:为了标注安全帽数据集,执行pip install labelme,结果把PyQt5从5.15降级到5.10或直接卸载重装成PyQt5-Qt5,项目里的界面代码import QtCore直接报错。
原因:labelme的setup.py里声明了PyQt5依赖,但没锁版本,pip解析依赖时会顺手把环境中已存在的PyQt5替换成它兼容的版本。如果直接用yolov5的requirements.txt装上来的PyQt5版本比labelme要求的旧,冲突就爆发了。
解决:隔离环境。标注和训练分两个conda环境,一个装labelme专门标数据,一个装yolov5和PyQt5跑检测。如果非要在同一环境里共存,先pip install pyqt5==5.15.9固定版本,再装labelme,标注完再跑训练前重新验证import PyQt5不报错。血泪经验是:别在同一个环境里既折腾标注工具又折腾推理GUI,冲突概率极高。
5.2 中文路径导致模型加载失败或视频黑屏
现象:项目放在D盘“智慧工地-安全帽检测”目录下,模型加载报FileNotFoundError,或cv2.VideoCapture打开视频文件返回False,界面黑屏。
原因:OpenCV和部分PyTorch版本对中文路径的编码支持不完善,Windows下宽字符路径会解析失败。yolov5仓库里的utils/datasets.py对路径做了很多处理,但遇到中文目录名依然可能翻车。
解决:项目根目录、数据集目录、模型路径全部用英文,比如D:/helmet_project,视频文件名也别带中文。已经标好的数据集如果目录是中文,复制一份改英文路径再训练,模型权重加载路径里也不要出现中文。这个坑在部署到现场工控机时特别常见,因为现场操作员习惯建中文文件夹。
5.3 CUDA显存不足:OOM报错与batch参数权衡
现象:训练时RuntimeError: CUDA out of memory。推理时摄像头连续跑十几分钟,界面掉帧然后进程被杀。
原因:检测推理看起来只加载了一个模型,但PyQt5界面会缓存历史帧,PyTorch的推理图也会在显存里留中间张量,时间一长加上其他程序占用,显存就满了。4GB显卡跑yolov5s的640推理本来就紧张。
解决:训练时把batch从16降到8或4,img从640降到512;推理时给模型加torch.no_grad(),并把帧处理逻辑包在with torch.no_grad()上下文里,阻止梯度图累积。启动时加一行os.environ["CUDA_VISIBLE_DEVICES"]="0",避免PyTorch默认占用全部GPU。接口卡如GTX 1660 Super建议直接--half半精度推理,显存占用直接砍半。
5.4 摄像头延迟越来越大,画面像慢放
现象:系统刚启动时FPS有25,运行半小时后掉到8,延迟肉眼可见增加,重启后又恢复正常。
原因:视频采集线程往推理线程丢帧的速度大于推理消费速度,中间队列无界增长。我见过有人用Python list.append存帧,结果内存涨到2GB以上,这是典型的线程模型设计错误。
解决:采集线程和推理线程之间用队列,但固定队列长度,满了直接丢最旧帧,保最新帧。上面3.2节的DetectThread直接循环read+推理,天然是同步背压模型,不会积累帧。如果拆成独立采集线程,给queue.Queue设置maxsize=1或2,放不进去就continue。这个设计对长时间运行的监控系统是生死问题。
5.5 误报多:安全帽检测把反光板、墙面人形广告当人
现象:现场反馈,阳光照在彩钢板上、雨天反光区域经常误报“未戴安全帽”,告警日志一天几百条。
原因:一是置信度阈值太低,模型对低质量目标输出0.3左右的框也被当成有效结果;二是训练数据里缺少“非人目标”的负样本,模型没有见过彩钢板反光这类干扰,特征上误判。误报率有两个评估指标要盯:精确率(Precision)和每帧平均告警次数。
解决:先调推理参数,--conf-thres从0.25提高到0.45,大多误报在0.3-0.4区间,提阈值后立刻减少。再做数据增广,把标注好的图片里加一些反光板、工程机械、柱子作为背景样本。如果还是报,加一个规则:检测到head类目标的置信度大于conf_thres,同时该目标框面积占比小于画面5%且大于0.5%,否则丢弃,这一类“面积过滤”在固定摄像头场景里效果明显。最后一步是现场画ROI(感兴趣区域),只对围栏内区域做检测推理,围栏外的反光一概不管。
6. 部署验证与轻量化:让系统在工地现场跑得更稳更省
模型训练好、GUI也跑通后,下一步是往现场工控机部署。我习惯先把best.pt导出ONNX,再在目标机器上用ONNX Runtime推理。导出命令一行就能完成:
python export.py --weights runs/train/exp/weights/best.pt \ --include onnx --img 640 --batch 1导出后用onnxruntime替代torch推理,在无独显的工控机上也能跑,CPU推理yolov5s大约90到140毫秒一帧,配RTX 3060时降到15到25毫秒。树莓派5这类ARM板可以跑优化过的ONNX模型,但建议把img降到416,类别只保留helmet和head两类,去掉person类,FPS能从3提升到8左右。
验证方法上,不建议只看demo视频。我的习惯是录一段至少30分钟、包含不同时段光线的现场视频,离线跑一遍,把每个告警帧截图,人工标出真阳性和假阳性,算准确率和漏报率。记录格式用表格:告警时间、视频帧号、检测类别、置信度、是否真实。这套验证跑完,再放上线。如果现场摄像头画面角度和训练数据差异大,在固定机位重新标注200张画面做增量训练,比直接换模型效果提升明显。
做这个系统一年多下来,我最大的教训是:模型和界面都不是最难的部分,最难的是确认“告警阈值和去抖参数是针对这个现场调的”。换一个工地,摄像头高度、角度、光线全变,参数就得重新调一轮。所以我习惯把所有阈值参数写进一个config.py,标注清楚每个参数影响哪个环节,现场调参数只用改这一个文件。希望帮到你。
本文还有配套的精品资源,点击获取